routing: add inbound fee support to pathfinding
Add sender-side support for inbound fees in pathfinding and route building.
This commit is contained in:
1 parent
d97e7d30fb
commit
0bae781785
13 files changed
+585
-131
No files matched your search
@@ -490,6 +490,16 @@ func (c *ChannelGraph) ForEachNodeDirectedChannel(tx kvdb.RTx,
|
|||||||
cachedInPolicy.ToNodeFeatures = toNodeFeatures
|
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{
|
directedChannel := &DirectedChannel{
|
||||||
ChannelID: e.ChannelID,
|
ChannelID: e.ChannelID,
|
||||||
IsNode1: node == e.NodeKey1Bytes,
|
IsNode1: node == e.NodeKey1Bytes,
|
||||||
@@ -497,6 +507,7 @@ func (c *ChannelGraph) ForEachNodeDirectedChannel(tx kvdb.RTx,
|
|||||||
Capacity: e.Capacity,
|
Capacity: e.Capacity,
|
||||||
OutPolicySet: p1 != nil,
|
OutPolicySet: p1 != nil,
|
||||||
InPolicy: cachedInPolicy,
|
InPolicy: cachedInPolicy,
|
||||||
|
InboundFee: inboundFee,
|
||||||
}
|
}
|
||||||
|
|
||||||
if node == e.NodeKey2Bytes {
|
if node == e.NodeKey2Bytes {
|
||||||
|
|||||||
@@ -59,6 +59,9 @@ type DirectedChannel struct {
|
|||||||
// source, so we're always interested in the edge that arrives to us
|
// source, so we're always interested in the edge that arrives to us
|
||||||
// from the other node.
|
// from the other node.
|
||||||
InPolicy *models.CachedEdgePolicy
|
InPolicy *models.CachedEdgePolicy
|
||||||
|
|
||||||
|
// Inbound fees of this node.
|
||||||
|
InboundFee lnwire.Fee
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeepCopy creates a deep copy of the channel, including the incoming policy.
|
// 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,
|
func (c *GraphCache) UpdatePolicy(policy *models.ChannelEdgePolicy, fromNode,
|
||||||
toNode route.Vertex, edge1 bool) {
|
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()
|
c.mtx.Lock()
|
||||||
defer c.mtx.Unlock()
|
defer c.mtx.Unlock()
|
||||||
|
|
||||||
@@ -240,11 +251,13 @@ func (c *GraphCache) UpdatePolicy(policy *models.ChannelEdgePolicy, fromNode,
|
|||||||
// policy for node 1.
|
// policy for node 1.
|
||||||
case channel.IsNode1 && edge1:
|
case channel.IsNode1 && edge1:
|
||||||
channel.OutPolicySet = true
|
channel.OutPolicySet = true
|
||||||
|
channel.InboundFee = inboundFee
|
||||||
|
|
||||||
// This is node 2, and it is edge 2, so this is the outgoing
|
// This is node 2, and it is edge 2, so this is the outgoing
|
||||||
// policy for node 2.
|
// policy for node 2.
|
||||||
case !channel.IsNode1 && !edge1:
|
case !channel.IsNode1 && !edge1:
|
||||||
channel.OutPolicySet = true
|
channel.OutPolicySet = true
|
||||||
|
channel.InboundFee = inboundFee
|
||||||
|
|
||||||
// The other two cases left mean it's the inbound policy for the
|
// The other two cases left mean it's the inbound policy for the
|
||||||
// node.
|
// node.
|
||||||
|
|||||||
@@ -75,6 +75,10 @@ func TestGraphCacheAddNode(t *testing.T) {
|
|||||||
ChannelID: 1000,
|
ChannelID: 1000,
|
||||||
ChannelFlags: lnwire.ChanUpdateChanFlags(channelFlagA),
|
ChannelFlags: lnwire.ChanUpdateChanFlags(channelFlagA),
|
||||||
ToNode: nodeB,
|
ToNode: nodeB,
|
||||||
|
// Define an inbound fee.
|
||||||
|
ExtraOpaqueData: []byte{
|
||||||
|
253, 217, 3, 8, 0, 0, 0, 10, 0, 0, 0, 20,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
inPolicy1 := &models.ChannelEdgePolicy{
|
inPolicy1 := &models.ChannelEdgePolicy{
|
||||||
ChannelID: 1000,
|
ChannelID: 1000,
|
||||||
@@ -124,8 +128,18 @@ func TestGraphCacheAddNode(t *testing.T) {
|
|||||||
edges map[uint64]*DirectedChannel) error {
|
edges map[uint64]*DirectedChannel) error {
|
||||||
|
|
||||||
nodes[node] = struct{}{}
|
nodes[node] = struct{}{}
|
||||||
for chanID := range edges {
|
for chanID, directedChannel := range edges {
|
||||||
chans[chanID] = struct{}{}
|
chans[chanID] = struct{}{}
|
||||||
|
|
||||||
|
if node == nodeA {
|
||||||
|
require.NotZero(
|
||||||
|
t, directedChannel.InboundFee,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
require.Zero(
|
||||||
|
t, directedChannel.InboundFee,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
+35
-3
@@ -674,7 +674,7 @@ func createChannelEdge(db kvdb.Backend, node1, node2 *LightningNode) (
|
|||||||
FeeBaseMSat: 4352345,
|
FeeBaseMSat: 4352345,
|
||||||
FeeProportionalMillionths: 3452352,
|
FeeProportionalMillionths: 3452352,
|
||||||
ToNode: secondNode,
|
ToNode: secondNode,
|
||||||
ExtraOpaqueData: []byte("new unknown feature2"),
|
ExtraOpaqueData: []byte{1, 0},
|
||||||
}
|
}
|
||||||
edge2 := &models.ChannelEdgePolicy{
|
edge2 := &models.ChannelEdgePolicy{
|
||||||
SigBytes: testSig.Serialize(),
|
SigBytes: testSig.Serialize(),
|
||||||
@@ -688,7 +688,7 @@ func createChannelEdge(db kvdb.Backend, node1, node2 *LightningNode) (
|
|||||||
FeeBaseMSat: 4352345,
|
FeeBaseMSat: 4352345,
|
||||||
FeeProportionalMillionths: 90392423,
|
FeeProportionalMillionths: 90392423,
|
||||||
ToNode: firstNode,
|
ToNode: firstNode,
|
||||||
ExtraOpaqueData: []byte("new unknown feature1"),
|
ExtraOpaqueData: []byte{1, 0},
|
||||||
}
|
}
|
||||||
|
|
||||||
return edgeInfo, edge1, edge2
|
return edgeInfo, edge1, edge2
|
||||||
@@ -3929,7 +3929,17 @@ func TestGraphCacheForEachNodeChannel(t *testing.T) {
|
|||||||
require.Nil(t, err)
|
require.Nil(t, err)
|
||||||
|
|
||||||
// Create an edge and add it to the db.
|
// 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.
|
// Add the channel, but only insert a single edge into the graph.
|
||||||
require.NoError(t, graph.AddChannelEdge(edgeInfo))
|
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
|
// We should be able to accumulate the single channel added, even
|
||||||
// though we have a nil edge policy here.
|
// though we have a nil edge policy here.
|
||||||
require.NotNil(t, getSingleChannel())
|
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
|
// 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
|
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")
|
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
|
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
|
fees. [Send support](https://github.com/lightningnetwork/lnd/pull/6934) is
|
||||||
added yet. We first want to learn more about the impact that inbound fees have
|
implemented as well.
|
||||||
on the routing economy.
|
|
||||||
|
|
||||||
* A new config value,
|
* A new config value,
|
||||||
[sweeper.maxfeerate](https://github.com/lightningnetwork/lnd/pull/7823), is
|
[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
|
// channel edges to relatively large non default values. This makes it
|
||||||
// possible to pick up more subtle fee calculation errors.
|
// possible to pick up more subtle fee calculation errors.
|
||||||
maxHtlc := lntest.CalculateMaxHtlc(chanAmt)
|
maxHtlc := lntest.CalculateMaxHtlc(chanAmt)
|
||||||
const aliceBaseFeeSat = 1
|
const aliceBaseFeeSat = 20
|
||||||
const aliceFeeRatePPM = 100000
|
const aliceFeeRatePPM = 100000
|
||||||
updateChannelPolicy(
|
updateChannelPolicy(
|
||||||
ht, alice, chanPointAlice, aliceBaseFeeSat*1000,
|
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
|
// Define a negative inbound fee for Alice, to verify that this is
|
||||||
// backwards compatible with an older sender ignoring the discount.
|
// backwards compatible with an older sender ignoring the discount.
|
||||||
const (
|
const (
|
||||||
aliceInboundBaseFeeMsat = -1
|
aliceInboundBaseFeeMsat = -2000
|
||||||
aliceInboundFeeRate = -10000
|
aliceInboundFeeRate = -50000 // 5%
|
||||||
)
|
)
|
||||||
|
|
||||||
updateChannelPolicy(
|
updateChannelPolicy(
|
||||||
@@ -119,12 +119,11 @@ func testMultiHopPayments(ht *lntest.HarnessTest) {
|
|||||||
ht.AssertAmountPaid("Alice(local) => Bob(remote)", alice,
|
ht.AssertAmountPaid("Alice(local) => Bob(remote)", alice,
|
||||||
chanPointAlice, expectedAmountPaidAtoB, int64(0))
|
chanPointAlice, expectedAmountPaidAtoB, int64(0))
|
||||||
|
|
||||||
// To forward a payment of 1000 sat, Alice is charging a fee of 1 sat +
|
// To forward a payment of 1000 sat, Alice is charging a fee of 20 sat +
|
||||||
// 10% = 101 sat. Note that this does not include the inbound fee
|
// 10% = 120 sat, plus the inbound fee over 1120 (= 1000 + 120) sat of
|
||||||
// (discount) because there is no sender support yet.
|
// -2 sat - 5% = -58 sat. This makes a total of 62 sat per payment. For
|
||||||
const aliceFeePerPayment = aliceBaseFeeSat +
|
// 5 payments, it works out to 310 sat.
|
||||||
(paymentAmt * aliceFeeRatePPM / 1_000_000)
|
const expectedFeeAlice = 310
|
||||||
const expectedFeeAlice = numPayments * aliceFeePerPayment
|
|
||||||
|
|
||||||
// Dave needs to pay what Alice pays plus Alice's fee.
|
// Dave needs to pay what Alice pays plus Alice's fee.
|
||||||
expectedAmountPaidDtoA := expectedAmountPaidAtoB + expectedFeeAlice
|
expectedAmountPaidDtoA := expectedAmountPaidAtoB + expectedFeeAlice
|
||||||
@@ -134,12 +133,10 @@ func testMultiHopPayments(ht *lntest.HarnessTest) {
|
|||||||
ht.AssertAmountPaid("Dave(local) => Alice(remote)", dave,
|
ht.AssertAmountPaid("Dave(local) => Alice(remote)", dave,
|
||||||
chanPointDave, expectedAmountPaidDtoA, int64(0))
|
chanPointDave, expectedAmountPaidDtoA, int64(0))
|
||||||
|
|
||||||
// To forward a payment of 1101 sat, Dave is charging a fee of
|
// To forward a payment of 1062 sat, Dave is charging a fee of 5 sat +
|
||||||
// 5 sat + 15% = 170.15 sat. This is rounded down in rpcserver to 170.
|
// 15% = 164.3 sat. For 5 payments this is 821.5 sat. This test works
|
||||||
const davePaymentAmt = paymentAmt + aliceFeePerPayment
|
// with sats, so we need to round down to 821.
|
||||||
const daveFeePerPayment = daveBaseFeeSat +
|
const expectedFeeDave = 821
|
||||||
(davePaymentAmt * daveFeeRatePPM / 1_000_000)
|
|
||||||
const expectedFeeDave = numPayments * daveFeePerPayment
|
|
||||||
|
|
||||||
// Carol needs to pay what Dave pays plus Dave's fee.
|
// Carol needs to pay what Dave pays plus Dave's fee.
|
||||||
expectedAmountPaidCtoD := expectedAmountPaidDtoA + expectedFeeDave
|
expectedAmountPaidCtoD := expectedAmountPaidDtoA + expectedFeeDave
|
||||||
|
|||||||
+5
-2
@@ -103,7 +103,10 @@ func (g *CachedGraph) FetchAmountPairCapacity(nodeFrom, nodeTo route.Vertex,
|
|||||||
amount lnwire.MilliSatoshi) (btcutil.Amount, error) {
|
amount lnwire.MilliSatoshi) (btcutil.Amount, error) {
|
||||||
|
|
||||||
// Create unified edges for all incoming connections.
|
// 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)
|
err := u.addGraphPolicies(g)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -116,7 +119,7 @@ func (g *CachedGraph) FetchAmountPairCapacity(nodeFrom, nodeTo route.Vertex,
|
|||||||
nodeFrom, nodeTo)
|
nodeFrom, nodeTo)
|
||||||
}
|
}
|
||||||
|
|
||||||
edge := edgeUnifier.getEdgeNetwork(amount)
|
edge := edgeUnifier.getEdgeNetwork(amount, 0)
|
||||||
if edge == nil {
|
if edge == nil {
|
||||||
return 0, fmt.Errorf("no edge for node pair %v -> %v "+
|
return 0, fmt.Errorf("no edge for node pair %v -> %v "+
|
||||||
"(amount %v)", nodeFrom, nodeTo, amount)
|
"(amount %v)", nodeFrom, nodeTo, amount)
|
||||||
|
|||||||
+9
-3
@@ -18,10 +18,16 @@ type nodeWithDist struct {
|
|||||||
// outgoing edges (channels) emanating from a node.
|
// outgoing edges (channels) emanating from a node.
|
||||||
node route.Vertex
|
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
|
// Either as final payment to the final node or as an intermediate
|
||||||
// amount that includes also the fees for subsequent hops.
|
// amount that includes also the fees for subsequent hops. This node's
|
||||||
amountToReceive lnwire.MilliSatoshi
|
// 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
|
// incomingCltv is the expected absolute expiry height for the incoming
|
||||||
// htlc of this node. This value should already include the final cltv
|
// htlc of this node. This value should already include the final cltv
|
||||||
|
|||||||
+101
-39
@@ -11,6 +11,7 @@ import (
|
|||||||
"github.com/btcsuite/btcd/btcutil"
|
"github.com/btcsuite/btcd/btcutil"
|
||||||
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/lnwire"
|
"github.com/lightningnetwork/lnd/lnwire"
|
||||||
"github.com/lightningnetwork/lnd/record"
|
"github.com/lightningnetwork/lnd/record"
|
||||||
@@ -118,7 +119,7 @@ type finalHopParams struct {
|
|||||||
// makes calculating the totals during route construction difficult if we
|
// makes calculating the totals during route construction difficult if we
|
||||||
// include blinded paths on the first pass).
|
// 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
|
// 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
|
// any feature vectors on all hops have been validated for transitive
|
||||||
// dependencies.
|
// dependencies.
|
||||||
@@ -157,7 +158,7 @@ func newRoute(sourceVertex route.Vertex,
|
|||||||
// we compute the route in reverse.
|
// we compute the route in reverse.
|
||||||
var (
|
var (
|
||||||
amtToForward lnwire.MilliSatoshi
|
amtToForward lnwire.MilliSatoshi
|
||||||
fee lnwire.MilliSatoshi
|
fee int64
|
||||||
totalAmtMsatBlinded lnwire.MilliSatoshi
|
totalAmtMsatBlinded lnwire.MilliSatoshi
|
||||||
outgoingTimeLock uint32
|
outgoingTimeLock uint32
|
||||||
tlvPayload bool
|
tlvPayload bool
|
||||||
@@ -195,7 +196,7 @@ func newRoute(sourceVertex route.Vertex,
|
|||||||
|
|
||||||
// Fee is not part of the hop payload, but only used for
|
// Fee is not part of the hop payload, but only used for
|
||||||
// reporting through RPC. Set to zero for the final hop.
|
// 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
|
// As this is the last hop, we'll use the specified
|
||||||
// final CLTV delta value instead of the value from the
|
// 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
|
// and its policy for the outgoing channel. This policy
|
||||||
// is stored as part of the incoming channel of
|
// is stored as part of the incoming channel of
|
||||||
// the next hop.
|
// 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
|
// We'll take the total timelock of the preceding hop as
|
||||||
// the outgoing timelock or this hop. Then we'll
|
// 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
|
// Finally, we update the amount that needs to flow into the
|
||||||
// *next* hop, which is the amount this hop needs to forward,
|
// *next* hop, which is the amount this hop needs to forward,
|
||||||
// accounting for the fee that it takes.
|
// 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
|
// 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
|
// 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.
|
// that key for the source key in the case we route to ourselves.
|
||||||
partialPath := &nodeWithDist{
|
partialPath := &nodeWithDist{
|
||||||
dist: 0,
|
dist: 0,
|
||||||
weight: 0,
|
weight: 0,
|
||||||
node: target,
|
node: target,
|
||||||
amountToReceive: amt,
|
netAmountReceived: amt,
|
||||||
incomingCltv: finalHtlcExpiry,
|
incomingCltv: finalHtlcExpiry,
|
||||||
probability: 1,
|
probability: 1,
|
||||||
routingInfoSize: lastHopPayloadSize,
|
routingInfoSize: lastHopPayloadSize,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate the absolute cltv limit. Use uint64 to prevent an overflow
|
// Calculate the absolute cltv limit. Use uint64 to prevent an overflow
|
||||||
@@ -703,9 +715,27 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
|
|||||||
|
|
||||||
edgesExpanded++
|
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
|
// Calculate amount that the candidate node would have to send
|
||||||
// out.
|
// out.
|
||||||
amountToSend := toNodeDist.amountToReceive
|
amountToSend := toNodeDist.netAmountReceived +
|
||||||
|
lnwire.MilliSatoshi(inboundFee)
|
||||||
|
|
||||||
// Request the success probability for this edge.
|
// Request the success probability for this edge.
|
||||||
edgeProbability := r.ProbabilitySource(
|
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
|
// Also determine the time lock delta that will be added to the
|
||||||
// route if fromVertex is selected. If fromVertex is the source
|
// route if fromVertex is selected. If fromVertex is the source
|
||||||
// node, no additional timelock is required.
|
// node, no additional timelock is required.
|
||||||
var fee lnwire.MilliSatoshi
|
var (
|
||||||
var timeLockDelta uint16
|
timeLockDelta uint16
|
||||||
|
outboundFee int64
|
||||||
|
)
|
||||||
|
|
||||||
if fromVertex != source {
|
if fromVertex != source {
|
||||||
fee = edge.policy.ComputeFee(amountToSend)
|
outboundFee = int64(
|
||||||
|
edge.policy.ComputeFee(amountToSend),
|
||||||
|
)
|
||||||
timeLockDelta = edge.policy.TimeLockDelta
|
timeLockDelta = edge.policy.TimeLockDelta
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -749,17 +784,19 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// amountToReceive is the amount that the node that is added to
|
// netAmountToReceive is the amount that the node that is added
|
||||||
// the distance map needs to receive from a (to be found)
|
// to the distance map needs to receive from a (to be found)
|
||||||
// previous node in the route. That previous node will need to
|
// previous node in the route. The inbound fee of the receiving
|
||||||
// pay the amount that this node forwards plus the fee it
|
// node is already subtracted from this value. The previous node
|
||||||
// charges.
|
// will need to pay the amount that this node forwards plus the
|
||||||
amountToReceive := amountToSend + fee
|
// fee it charges plus this node's inbound fee.
|
||||||
|
netAmountToReceive := amountToSend +
|
||||||
|
lnwire.MilliSatoshi(outboundFee)
|
||||||
|
|
||||||
// Check if accumulated fees would exceed fee limit when this
|
// Check if accumulated fees would exceed fee limit when this
|
||||||
// node would be added to the path.
|
// node would be added to the path.
|
||||||
totalFee := amountToReceive - amt
|
totalFee := int64(netAmountToReceive) - int64(amt)
|
||||||
if totalFee > r.FeeLimit {
|
if totalFee > 0 && lnwire.MilliSatoshi(totalFee) > r.FeeLimit {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -775,11 +812,21 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
|
|||||||
return
|
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
|
// By adding fromVertex in the route, there will be an extra
|
||||||
// weight composed of the fee that this node will charge and
|
// weight composed of the fee that this node will charge and
|
||||||
// the amount that will be locked for timeLockDelta blocks in
|
// the amount that will be locked for timeLockDelta blocks in
|
||||||
// the HTLC that is handed out to fromVertex.
|
// 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
|
// Compute the tentative weight to this new channel/edge
|
||||||
// which is the weight from our toNode to the target node
|
// 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
|
tempWeight := toNodeDist.weight + weight
|
||||||
|
|
||||||
// Add an extra factor to the weight to take into account the
|
// 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(
|
tempDist := getProbabilityBasedDist(
|
||||||
tempWeight, probability,
|
tempWeight, probability,
|
||||||
absoluteAttemptCost,
|
absoluteAttemptCost,
|
||||||
@@ -854,14 +904,15 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
|
|||||||
// The new better distance is recorded, and also our "next hop"
|
// The new better distance is recorded, and also our "next hop"
|
||||||
// map is populated with this edge.
|
// map is populated with this edge.
|
||||||
withDist := &nodeWithDist{
|
withDist := &nodeWithDist{
|
||||||
dist: tempDist,
|
dist: tempDist,
|
||||||
weight: tempWeight,
|
weight: tempWeight,
|
||||||
node: fromVertex,
|
node: fromVertex,
|
||||||
amountToReceive: amountToReceive,
|
netAmountReceived: netAmountToReceive,
|
||||||
incomingCltv: incomingCltv,
|
outboundFee: lnwire.MilliSatoshi(outboundFee),
|
||||||
probability: probability,
|
incomingCltv: incomingCltv,
|
||||||
nextHop: edge,
|
probability: probability,
|
||||||
routingInfoSize: routingInfoSize,
|
nextHop: edge,
|
||||||
|
routingInfoSize: routingInfoSize,
|
||||||
}
|
}
|
||||||
distance[fromVertex] = withDist
|
distance[fromVertex] = withDist
|
||||||
|
|
||||||
@@ -920,9 +971,13 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
|
|||||||
nodesVisited++
|
nodesVisited++
|
||||||
|
|
||||||
pivot := partialPath.node
|
pivot := partialPath.node
|
||||||
|
isExitHop := partialPath.nextHop == nil
|
||||||
|
|
||||||
// Create unified edges for all incoming connections.
|
// Create unified policies for all incoming connections. Don't
|
||||||
u := newNodeEdgeUnifier(self, pivot, outgoingChanMap)
|
// use inbound fees for the exit hop.
|
||||||
|
u := newNodeEdgeUnifier(
|
||||||
|
self, pivot, !isExitHop, outgoingChanMap,
|
||||||
|
)
|
||||||
|
|
||||||
err := u.addGraphPolicies(g.graph)
|
err := u.addGraphPolicies(g.graph)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -931,6 +986,11 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
|
|||||||
|
|
||||||
// We add hop hints that were supplied externally.
|
// We add hop hints that were supplied externally.
|
||||||
for _, reverseEdge := range additionalEdgesWithSrc[pivot] {
|
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,
|
// Hop hints don't contain a capacity. We set one here,
|
||||||
// since a capacity is needed for probability
|
// since a capacity is needed for probability
|
||||||
// calculations. We set a high capacity to act as if
|
// calculations. We set a high capacity to act as if
|
||||||
@@ -942,12 +1002,13 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
|
|||||||
u.addPolicy(
|
u.addPolicy(
|
||||||
reverseEdge.sourceNode,
|
reverseEdge.sourceNode,
|
||||||
reverseEdge.edge.EdgePolicy(),
|
reverseEdge.edge.EdgePolicy(),
|
||||||
|
inboundFee,
|
||||||
fakeHopHintCapacity,
|
fakeHopHintCapacity,
|
||||||
reverseEdge.edge.IntermediatePayloadSize,
|
reverseEdge.edge.IntermediatePayloadSize,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
amtToSend := partialPath.amountToReceive
|
netAmountReceived := partialPath.netAmountReceived
|
||||||
|
|
||||||
// Expand all connections using the optimal policy for each
|
// Expand all connections using the optimal policy for each
|
||||||
// connection.
|
// connection.
|
||||||
@@ -969,7 +1030,8 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
|
|||||||
}
|
}
|
||||||
|
|
||||||
edge := edgeUnifier.getEdge(
|
edge := edgeUnifier.getEdge(
|
||||||
amtToSend, g.bandwidthHints,
|
netAmountReceived, g.bandwidthHints,
|
||||||
|
partialPath.outboundFee,
|
||||||
)
|
)
|
||||||
|
|
||||||
if edge == nil {
|
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",
|
log.Debugf("Found route: probability=%v, hops=%v, fee=%v",
|
||||||
distance[source].probability, len(pathEdges),
|
distance[source].probability, len(pathEdges),
|
||||||
distance[source].amountToReceive-amt)
|
distance[source].netAmountReceived-amt)
|
||||||
|
|
||||||
return pathEdges, distance[source].probability, nil
|
return pathEdges, distance[source].probability, nil
|
||||||
}
|
}
|
||||||
|
|||||||
+169
-8
@@ -417,14 +417,16 @@ func parseTestGraph(t *testing.T, useCache bool, path string) (
|
|||||||
}
|
}
|
||||||
|
|
||||||
type testChannelPolicy struct {
|
type testChannelPolicy struct {
|
||||||
Expiry uint16
|
Expiry uint16
|
||||||
MinHTLC lnwire.MilliSatoshi
|
MinHTLC lnwire.MilliSatoshi
|
||||||
MaxHTLC lnwire.MilliSatoshi
|
MaxHTLC lnwire.MilliSatoshi
|
||||||
FeeBaseMsat lnwire.MilliSatoshi
|
FeeBaseMsat lnwire.MilliSatoshi
|
||||||
FeeRate lnwire.MilliSatoshi
|
FeeRate lnwire.MilliSatoshi
|
||||||
LastUpdate time.Time
|
InboundFeeBaseMsat int64
|
||||||
Disabled bool
|
InboundFeeRate int64
|
||||||
Features *lnwire.FeatureVector
|
LastUpdate time.Time
|
||||||
|
Disabled bool
|
||||||
|
Features *lnwire.FeatureVector
|
||||||
}
|
}
|
||||||
|
|
||||||
type testChannelEnd struct {
|
type testChannelEnd struct {
|
||||||
@@ -662,6 +664,19 @@ func createTestGraphFromChannels(t *testing.T, useCache bool,
|
|||||||
return nil, err
|
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 {
|
if node1.testChannelPolicy != nil {
|
||||||
var msgFlags lnwire.ChanUpdateMsgFlags
|
var msgFlags lnwire.ChanUpdateMsgFlags
|
||||||
if node1.MaxHTLC != 0 {
|
if node1.MaxHTLC != 0 {
|
||||||
@@ -684,6 +699,7 @@ func createTestGraphFromChannels(t *testing.T, useCache bool,
|
|||||||
FeeBaseMSat: node1.FeeBaseMsat,
|
FeeBaseMSat: node1.FeeBaseMsat,
|
||||||
FeeProportionalMillionths: node1.FeeRate,
|
FeeProportionalMillionths: node1.FeeRate,
|
||||||
ToNode: node2Vertex,
|
ToNode: node2Vertex,
|
||||||
|
ExtraOpaqueData: getExtraData(node1),
|
||||||
}
|
}
|
||||||
if err := graph.UpdateEdgePolicy(edgePolicy); err != nil {
|
if err := graph.UpdateEdgePolicy(edgePolicy); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -713,6 +729,7 @@ func createTestGraphFromChannels(t *testing.T, useCache bool,
|
|||||||
FeeBaseMSat: node2.FeeBaseMsat,
|
FeeBaseMSat: node2.FeeBaseMsat,
|
||||||
FeeProportionalMillionths: node2.FeeRate,
|
FeeProportionalMillionths: node2.FeeRate,
|
||||||
ToNode: node1Vertex,
|
ToNode: node1Vertex,
|
||||||
|
ExtraOpaqueData: getExtraData(node2),
|
||||||
}
|
}
|
||||||
if err := graph.UpdateEdgePolicy(edgePolicy); err != nil {
|
if err := graph.UpdateEdgePolicy(edgePolicy); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -809,6 +826,9 @@ func TestPathFinding(t *testing.T) {
|
|||||||
}, {
|
}, {
|
||||||
name: "with metadata",
|
name: "with metadata",
|
||||||
fn: runFindPathWithMetadata,
|
fn: runFindPathWithMetadata,
|
||||||
|
}, {
|
||||||
|
name: "inbound fees",
|
||||||
|
fn: runInboundFees,
|
||||||
}}
|
}}
|
||||||
|
|
||||||
// Run with graph cache enabled.
|
// Run with graph cache enabled.
|
||||||
@@ -3129,6 +3149,147 @@ func runRouteToSelf(t *testing.T, useCache bool) {
|
|||||||
ctx.assertPath(path, []uint64{1, 3, 2})
|
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 {
|
type pathFindingTestContext struct {
|
||||||
t *testing.T
|
t *testing.T
|
||||||
graph *channeldb.ChannelGraph
|
graph *channeldb.ChannelGraph
|
||||||
|
|||||||
+9
-5
@@ -3161,9 +3161,13 @@ func getRouteUnifiers(source route.Vertex, hops []route.Vertex,
|
|||||||
|
|
||||||
localChan := i == 0
|
localChan := i == 0
|
||||||
|
|
||||||
// Build unified edges for this hop based on the channels known
|
// Build unified policies for this hop based on the channels
|
||||||
// in the graph.
|
// known in the graph. Don't use inbound fees.
|
||||||
u := newNodeEdgeUnifier(source, toNode, outgoingChans)
|
//
|
||||||
|
// TODO: Add inbound fees support for BuildRoute.
|
||||||
|
u := newNodeEdgeUnifier(
|
||||||
|
source, toNode, false, outgoingChans,
|
||||||
|
)
|
||||||
|
|
||||||
err := u.addGraphPolicies(graph)
|
err := u.addGraphPolicies(graph)
|
||||||
if err != nil {
|
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.
|
// 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 {
|
if edge == nil {
|
||||||
log.Errorf("Cannot find policy with amt=%v for node %v",
|
log.Errorf("Cannot find policy with amt=%v for node %v",
|
||||||
runningAmt, fromNode)
|
runningAmt, fromNode)
|
||||||
@@ -3227,7 +3231,7 @@ func getPathEdges(source route.Vertex, receiverAmt lnwire.MilliSatoshi,
|
|||||||
// amount ranges re-checked.
|
// amount ranges re-checked.
|
||||||
var pathEdges []*unifiedEdge
|
var pathEdges []*unifiedEdge
|
||||||
for i, unifier := range unifiers {
|
for i, unifier := range unifiers {
|
||||||
edge := unifier.getEdge(receiverAmt, bandwidthHints)
|
edge := unifier.getEdge(receiverAmt, bandwidthHints, 0)
|
||||||
if edge == nil {
|
if edge == nil {
|
||||||
fromNode := source
|
fromNode := source
|
||||||
if i > 0 {
|
if i > 0 {
|
||||||
|
|||||||
+95
-25
@@ -1,6 +1,8 @@
|
|||||||
package routing
|
package routing
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"math"
|
||||||
|
|
||||||
"github.com/btcsuite/btcd/btcutil"
|
"github.com/btcsuite/btcd/btcutil"
|
||||||
"github.com/lightningnetwork/lnd/channeldb"
|
"github.com/lightningnetwork/lnd/channeldb"
|
||||||
"github.com/lightningnetwork/lnd/channeldb/models"
|
"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 is the node for which the edge unifiers are instantiated.
|
||||||
toNode route.Vertex
|
toNode route.Vertex
|
||||||
|
|
||||||
|
// useInboundFees indicates whether to take inbound fees into account.
|
||||||
|
useInboundFees bool
|
||||||
|
|
||||||
// outChanRestr is an optional outgoing channel restriction for the
|
// outChanRestr is an optional outgoing channel restriction for the
|
||||||
// local channel to use.
|
// local channel to use.
|
||||||
outChanRestr map[uint64]struct{}
|
outChanRestr map[uint64]struct{}
|
||||||
@@ -28,14 +33,15 @@ type nodeEdgeUnifier struct {
|
|||||||
|
|
||||||
// newNodeEdgeUnifier instantiates a new nodeEdgeUnifier object. Channel
|
// newNodeEdgeUnifier instantiates a new nodeEdgeUnifier object. Channel
|
||||||
// policies can be added to this object.
|
// 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 {
|
outChanRestr map[uint64]struct{}) *nodeEdgeUnifier {
|
||||||
|
|
||||||
return &nodeEdgeUnifier{
|
return &nodeEdgeUnifier{
|
||||||
edgeUnifiers: make(map[route.Vertex]*edgeUnifier),
|
edgeUnifiers: make(map[route.Vertex]*edgeUnifier),
|
||||||
toNode: toNode,
|
toNode: toNode,
|
||||||
sourceNode: sourceNode,
|
useInboundFees: useInboundFees,
|
||||||
outChanRestr: outChanRestr,
|
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
|
// graceful shutdown if it is not provided as this indicates that edges are
|
||||||
// incorrectly specified.
|
// 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, inboundFee models.InboundFee,
|
||||||
hopPayloadSizeFn PayloadSizeFunc) {
|
capacity btcutil.Amount, hopPayloadSizeFn PayloadSizeFunc) {
|
||||||
|
|
||||||
localChan := fromNode == u.sourceNode
|
localChan := fromNode == u.sourceNode
|
||||||
|
|
||||||
@@ -75,10 +81,16 @@ func (u *nodeEdgeUnifier) addPolicy(fromNode route.Vertex,
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Zero inbound fee for exit hops.
|
||||||
|
if !u.useInboundFees {
|
||||||
|
inboundFee = models.InboundFee{}
|
||||||
|
}
|
||||||
|
|
||||||
unifier.edges = append(unifier.edges, &unifiedEdge{
|
unifier.edges = append(unifier.edges, &unifiedEdge{
|
||||||
policy: edge,
|
policy: edge,
|
||||||
capacity: capacity,
|
capacity: capacity,
|
||||||
hopPayloadSizeFn: hopPayloadSizeFn,
|
hopPayloadSizeFn: hopPayloadSizeFn,
|
||||||
|
inboundFees: inboundFee,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,9 +109,13 @@ func (u *nodeEdgeUnifier) addGraphPolicies(g routingGraph) error {
|
|||||||
// to the clear hop payload size function because
|
// to the clear hop payload size function because
|
||||||
// `addGraphPolicies` is only used for cleartext intermediate
|
// `addGraphPolicies` is only used for cleartext intermediate
|
||||||
// hops in a route.
|
// hops in a route.
|
||||||
|
inboundFee := models.NewInboundFeeFromWire(
|
||||||
|
channel.InboundFee,
|
||||||
|
)
|
||||||
|
|
||||||
u.addPolicy(
|
u.addPolicy(
|
||||||
channel.OtherNode, channel.InPolicy, channel.Capacity,
|
channel.OtherNode, channel.InPolicy, inboundFee,
|
||||||
defaultHopPayloadSize,
|
channel.Capacity, defaultHopPayloadSize,
|
||||||
)
|
)
|
||||||
|
|
||||||
return nil
|
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
|
// unifiedEdge is the individual channel data that is kept inside an edgeUnifier
|
||||||
// object.
|
// object.
|
||||||
type unifiedEdge struct {
|
type unifiedEdge struct {
|
||||||
policy *models.CachedEdgePolicy
|
policy *models.CachedEdgePolicy
|
||||||
capacity btcutil.Amount
|
capacity btcutil.Amount
|
||||||
|
inboundFees models.InboundFee
|
||||||
|
|
||||||
// hopPayloadSize supplies an edge with the ability to calculate the
|
// hopPayloadSize supplies an edge with the ability to calculate the
|
||||||
// exact payload size if this edge would be included in a route. This
|
// 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
|
// getEdge returns the optimal unified edge to use for this connection given a
|
||||||
// specific amount to send. It differentiates between local and network
|
// specific amount to send. It differentiates between local and network
|
||||||
// channels.
|
// channels.
|
||||||
func (u *edgeUnifier) getEdge(amt lnwire.MilliSatoshi,
|
func (u *edgeUnifier) getEdge(netAmtReceived lnwire.MilliSatoshi,
|
||||||
bandwidthHints bandwidthHints) *unifiedEdge {
|
bandwidthHints bandwidthHints,
|
||||||
|
nextOutFee lnwire.MilliSatoshi) *unifiedEdge {
|
||||||
|
|
||||||
if u.localChan {
|
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
|
// getEdgeLocal returns the optimal unified edge to use for this local
|
||||||
// connection given a specific amount to send.
|
// connection given a specific amount to send.
|
||||||
func (u *edgeUnifier) getEdgeLocal(amt lnwire.MilliSatoshi,
|
func (u *edgeUnifier) getEdgeLocal(netAmtReceived lnwire.MilliSatoshi,
|
||||||
bandwidthHints bandwidthHints) *unifiedEdge {
|
bandwidthHints bandwidthHints,
|
||||||
|
nextOutFee lnwire.MilliSatoshi) *unifiedEdge {
|
||||||
|
|
||||||
var (
|
var (
|
||||||
bestEdge *unifiedEdge
|
bestEdge *unifiedEdge
|
||||||
@@ -184,10 +222,20 @@ func (u *edgeUnifier) getEdgeLocal(amt lnwire.MilliSatoshi,
|
|||||||
)
|
)
|
||||||
|
|
||||||
for _, edge := range u.edges {
|
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.
|
// Check valid amount range for the channel.
|
||||||
if !edge.amtInRange(amt) {
|
if !edge.amtInRange(amt) {
|
||||||
log.Debugf("Amount %v not in range for edge %v",
|
log.Debugf("Amount %v not in range for edge %v",
|
||||||
amt, edge.policy.ChannelID)
|
netAmtReceived, edge.policy.ChannelID)
|
||||||
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -249,6 +297,7 @@ func (u *edgeUnifier) getEdgeLocal(amt lnwire.MilliSatoshi,
|
|||||||
policy: edge.policy,
|
policy: edge.policy,
|
||||||
capacity: edge.capacity,
|
capacity: edge.capacity,
|
||||||
hopPayloadSizeFn: edge.hopPayloadSizeFn,
|
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
|
// 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
|
// policy that maximizes the probability of a successful forward in a non-strict
|
||||||
// forwarding context.
|
// forwarding context.
|
||||||
func (u *edgeUnifier) getEdgeNetwork(amt lnwire.MilliSatoshi) *unifiedEdge {
|
func (u *edgeUnifier) getEdgeNetwork(netAmtReceived lnwire.MilliSatoshi,
|
||||||
|
nextOutFee lnwire.MilliSatoshi) *unifiedEdge {
|
||||||
|
|
||||||
var (
|
var (
|
||||||
bestPolicy *models.CachedEdgePolicy
|
bestPolicy *unifiedEdge
|
||||||
maxFee lnwire.MilliSatoshi
|
maxFee int64 = math.MinInt64
|
||||||
maxTimelock uint16
|
maxTimelock uint16
|
||||||
maxCapMsat lnwire.MilliSatoshi
|
maxCapMsat lnwire.MilliSatoshi
|
||||||
hopPayloadSizeFn PayloadSizeFunc
|
hopPayloadSizeFn PayloadSizeFunc
|
||||||
)
|
)
|
||||||
|
|
||||||
for _, edge := range u.edges {
|
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.
|
// Check valid amount range for the channel.
|
||||||
if !edge.amtInRange(amt) {
|
if !edge.amtInRange(amt) {
|
||||||
log.Debugf("Amount %v not in range for edge %v",
|
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 = lntypes.Max(
|
||||||
maxTimelock, edge.policy.TimeLockDelta,
|
maxTimelock, edge.policy.TimeLockDelta,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
outboundFee := int64(edge.policy.ComputeFee(amt))
|
||||||
|
fee := outboundFee + inboundFee
|
||||||
|
|
||||||
// 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)
|
|
||||||
if fee < maxFee {
|
if fee < maxFee {
|
||||||
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",
|
||||||
@@ -314,7 +377,11 @@ func (u *edgeUnifier) getEdgeNetwork(amt lnwire.MilliSatoshi) *unifiedEdge {
|
|||||||
}
|
}
|
||||||
maxFee = fee
|
maxFee = fee
|
||||||
|
|
||||||
bestPolicy = edge.policy
|
bestPolicy = &unifiedEdge{
|
||||||
|
policy: edge.policy,
|
||||||
|
inboundFees: edge.inboundFees,
|
||||||
|
}
|
||||||
|
|
||||||
// The payload size function for edges to a connected peer is
|
// The payload size function for edges to a connected peer is
|
||||||
// always the same hence there is not need to find the maximum.
|
// always the same hence there is not need to find the maximum.
|
||||||
// This also counts for blinded edges where we only have one
|
// 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
|
// 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
|
// chance for this node pair. But this is all only needed for nodes that
|
||||||
// have distinct policies for channels to the same peer.
|
// have distinct policies for channels to the same peer.
|
||||||
policyCopy := *bestPolicy
|
policyCopy := *bestPolicy.policy
|
||||||
modifiedEdge := unifiedEdge{policy: &policyCopy}
|
modifiedEdge := unifiedEdge{
|
||||||
|
policy: &policyCopy,
|
||||||
|
inboundFees: bestPolicy.inboundFees,
|
||||||
|
}
|
||||||
modifiedEdge.policy.TimeLockDelta = maxTimelock
|
modifiedEdge.policy.TimeLockDelta = maxTimelock
|
||||||
modifiedEdge.capacity = maxCapMsat.ToSatoshis()
|
modifiedEdge.capacity = maxCapMsat.ToSatoshis()
|
||||||
modifiedEdge.hopPayloadSizeFn = hopPayloadSizeFn
|
modifiedEdge.hopPayloadSizeFn = hopPayloadSizeFn
|
||||||
|
|||||||
+109
-27
@@ -46,31 +46,75 @@ func TestNodeEdgeUnifier(t *testing.T) {
|
|||||||
c1 := btcutil.Amount(7)
|
c1 := btcutil.Amount(7)
|
||||||
c2 := btcutil.Amount(8)
|
c2 := btcutil.Amount(8)
|
||||||
|
|
||||||
unifierFilled := newNodeEdgeUnifier(source, toNode, nil)
|
inboundFee1 := models.InboundFee{
|
||||||
unifierFilled.addPolicy(fromNode, &p1, c1, defaultHopPayloadSize)
|
Base: 5,
|
||||||
unifierFilled.addPolicy(fromNode, &p2, c2, defaultHopPayloadSize)
|
Rate: 10000,
|
||||||
|
}
|
||||||
|
|
||||||
unifierNoCapacity := newNodeEdgeUnifier(source, toNode, nil)
|
inboundFee2 := models.InboundFee{
|
||||||
unifierNoCapacity.addPolicy(fromNode, &p1, 0, defaultHopPayloadSize)
|
Base: 10,
|
||||||
unifierNoCapacity.addPolicy(fromNode, &p2, 0, defaultHopPayloadSize)
|
Rate: 10000,
|
||||||
|
}
|
||||||
|
|
||||||
unifierNoInfo := newNodeEdgeUnifier(source, toNode, nil)
|
unifierFilled := newNodeEdgeUnifier(source, toNode, false, nil)
|
||||||
unifierNoInfo.addPolicy(
|
|
||||||
fromNode, &models.CachedEdgePolicy{}, 0, defaultHopPayloadSize,
|
unifierFilled.addPolicy(
|
||||||
|
fromNode, &p1, inboundFee1, c1, defaultHopPayloadSize,
|
||||||
|
)
|
||||||
|
unifierFilled.addPolicy(
|
||||||
|
fromNode, &p2, inboundFee2, c2, defaultHopPayloadSize,
|
||||||
)
|
)
|
||||||
|
|
||||||
unifierLocal := newNodeEdgeUnifier(fromNode, toNode, nil)
|
unifierNoCapacity := newNodeEdgeUnifier(source, toNode, false, nil)
|
||||||
unifierLocal.addPolicy(fromNode, &p1, c1, defaultHopPayloadSize)
|
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 {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
unifier *nodeEdgeUnifier
|
unifier *nodeEdgeUnifier
|
||||||
amount lnwire.MilliSatoshi
|
amount lnwire.MilliSatoshi
|
||||||
expectedFeeBase lnwire.MilliSatoshi
|
expectedFeeBase lnwire.MilliSatoshi
|
||||||
expectedFeeRate lnwire.MilliSatoshi
|
expectedFeeRate lnwire.MilliSatoshi
|
||||||
expectedTimeLock uint16
|
expectedInboundFee models.InboundFee
|
||||||
expectNoPolicy bool
|
expectedTimeLock uint16
|
||||||
expectedCapacity btcutil.Amount
|
expectNoPolicy bool
|
||||||
|
expectedCapacity btcutil.Amount
|
||||||
|
nextOutFee lnwire.MilliSatoshi
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "amount below min htlc",
|
name: "amount below min htlc",
|
||||||
@@ -132,13 +176,49 @@ func TestNodeEdgeUnifier(t *testing.T) {
|
|||||||
expectNoPolicy: true,
|
expectNoPolicy: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "local",
|
name: "local",
|
||||||
unifier: unifierLocal,
|
unifier: unifierLocal,
|
||||||
amount: 100,
|
amount: 100,
|
||||||
expectedFeeBase: p1.FeeBaseMSat,
|
expectedFeeBase: p1.FeeBaseMSat,
|
||||||
expectedFeeRate: p1.FeeProportionalMillionths,
|
expectedFeeRate: p1.FeeProportionalMillionths,
|
||||||
expectedTimeLock: p1.TimeLockDelta,
|
expectedTimeLock: p1.TimeLockDelta,
|
||||||
expectedCapacity: c1,
|
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()
|
t.Parallel()
|
||||||
|
|
||||||
edge := test.unifier.edgeUnifiers[fromNode].getEdge(
|
edge := test.unifier.edgeUnifiers[fromNode].getEdge(
|
||||||
test.amount, bandwidthHints,
|
test.amount, bandwidthHints, test.nextOutFee,
|
||||||
)
|
)
|
||||||
|
|
||||||
if test.expectNoPolicy {
|
if test.expectNoPolicy {
|
||||||
@@ -163,6 +243,8 @@ func TestNodeEdgeUnifier(t *testing.T) {
|
|||||||
policy.FeeBaseMSat, "base fee")
|
policy.FeeBaseMSat, "base fee")
|
||||||
require.Equal(t, test.expectedFeeRate,
|
require.Equal(t, test.expectedFeeRate,
|
||||||
policy.FeeProportionalMillionths, "fee rate")
|
policy.FeeProportionalMillionths, "fee rate")
|
||||||
|
require.Equal(t, test.expectedInboundFee,
|
||||||
|
edge.inboundFees, "inbound fee")
|
||||||
require.Equal(t, test.expectedTimeLock,
|
require.Equal(t, test.expectedTimeLock,
|
||||||
policy.TimeLockDelta, "timelock")
|
policy.TimeLockDelta, "timelock")
|
||||||
require.Equal(t, test.expectedCapacity, edge.capacity,
|
require.Equal(t, test.expectedCapacity, edge.capacity,
|
||||||
|
|||||||
Reference in new issue
Block a user