Skip to content

Commit

Permalink
routing: add inbound fee support to pathfinding
Browse files Browse the repository at this point in the history
Add sender-side support for inbound fees in pathfinding
and route building.
  • Loading branch information
joostjager committed Jan 24, 2024
1 parent f841789 commit 96903da
Show file tree
Hide file tree
Showing 13 changed files with 513 additions and 127 deletions.
11 changes: 11 additions & 0 deletions channeldb/graph.go
Original file line number Diff line number Diff line change
Expand Up @@ -490,13 +490,24 @@ 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,
OtherNode: e.NodeKey2Bytes,
Capacity: e.Capacity,
OutPolicySet: p1 != nil,
InPolicy: cachedInPolicy,
InboundFee: inboundFee,
}

if node == e.NodeKey2Bytes {
Expand Down
13 changes: 13 additions & 0 deletions channeldb/graph_cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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()

Expand All @@ -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.
Expand Down
16 changes: 15 additions & 1 deletion channeldb/graph_cache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
38 changes: 35 additions & 3 deletions channeldb/graph_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand All @@ -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
Expand Down Expand Up @@ -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))
Expand All @@ -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
Expand Down
5 changes: 2 additions & 3 deletions docs/release-notes/release-notes-0.18.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,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
Expand Down
27 changes: 12 additions & 15 deletions itest/lnd_multi-hop-payments_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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(
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
5 changes: 4 additions & 1 deletion routing/graph.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
12 changes: 9 additions & 3 deletions routing/heap.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 96903da

Please sign in to comment.