Skip to content

Commit

Permalink
Merge pull request #6934 from bottlepay/inbound-fees-send-support
Browse files Browse the repository at this point in the history
routing: inbound fees send support
  • Loading branch information
Roasbeef authored Mar 31, 2024
2 parents a6d4bb5 + 0bae781 commit 5599b3c
Show file tree
Hide file tree
Showing 15 changed files with 675 additions and 179 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
61 changes: 49 additions & 12 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,24 +3929,61 @@ 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))

getSingleChannel := func() *DirectedChannel {
var ch *DirectedChannel
err = graph.ForEachNodeDirectedChannel(nil, node1.PubKeyBytes,
func(c *DirectedChannel) error {
require.Nil(t, ch)
ch = c

return nil
},
)
require.NoError(t, err)

return ch
}

// We should be able to accumulate the single channel added, even
// though we have a nil edge policy here.
var numChans int
err = graph.ForEachNodeDirectedChannel(nil, node1.PubKeyBytes,
func(_ *DirectedChannel) error {
numChans++
require.NotNil(t, getSingleChannel())

return nil
},
)
require.NoError(t, err)
// 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.Equal(t, numChans, 1)
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 @@ -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
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
7 changes: 5 additions & 2 deletions 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 All @@ -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)
Expand Down
15 changes: 10 additions & 5 deletions routing/heap.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package routing
import (
"container/heap"

"github.com/lightningnetwork/lnd/channeldb/models"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/routing/route"
)
Expand All @@ -19,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 All @@ -39,7 +44,7 @@ type nodeWithDist struct {
weight int64

// nextHop is the edge this route comes from.
nextHop *models.CachedEdgePolicy
nextHop *unifiedEdge

// routingInfoSize is the total size requirement for the payloads field
// in the onion packet from this hop towards the final destination.
Expand Down
Loading

0 comments on commit 5599b3c

Please sign in to comment.