mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-11-10 14:17:56 +01:00
Merge pull request #8886 from bitromortac/buildroute-inbound-fees
routing: inbound fees support for BuildRoute
This commit is contained in:
@@ -24,6 +24,7 @@ import (
|
||||
"github.com/lightningnetwork/lnd/channeldb"
|
||||
"github.com/lightningnetwork/lnd/channeldb/models"
|
||||
"github.com/lightningnetwork/lnd/clock"
|
||||
"github.com/lightningnetwork/lnd/fn"
|
||||
"github.com/lightningnetwork/lnd/graph"
|
||||
"github.com/lightningnetwork/lnd/htlcswitch"
|
||||
"github.com/lightningnetwork/lnd/input"
|
||||
@@ -1536,10 +1537,10 @@ func TestBuildRoute(t *testing.T) {
|
||||
}, 6),
|
||||
|
||||
// Create two channels from b to c. For building routes, we
|
||||
// expect the lowest cost channel to be selected. Note that this
|
||||
// isn't a situation that we are expecting in reality. Routing
|
||||
// nodes are recommended to keep their channel policies towards
|
||||
// the same peer identical.
|
||||
// expect the highest cost channel to be selected. Note that
|
||||
// this isn't a situation that we are expecting in reality.
|
||||
// Routing nodes are recommended to keep their channel policies
|
||||
// towards the same peer identical.
|
||||
symmetricTestChannel("b", "c", chanCapSat, &testChannelPolicy{
|
||||
Expiry: 144,
|
||||
FeeRate: 50000,
|
||||
@@ -1555,6 +1556,8 @@ func TestBuildRoute(t *testing.T) {
|
||||
Features: paymentAddrFeatures,
|
||||
}, 7),
|
||||
|
||||
// Create some channels that have conflicting min/max
|
||||
// constraints.
|
||||
symmetricTestChannel("a", "e", chanCapSat, &testChannelPolicy{
|
||||
Expiry: 144,
|
||||
FeeRate: 80000,
|
||||
@@ -1569,9 +1572,50 @@ func TestBuildRoute(t *testing.T) {
|
||||
MaxHTLC: lnwire.NewMSatFromSatoshis(chanCapSat),
|
||||
Features: paymentAddrFeatures,
|
||||
}, 4),
|
||||
|
||||
// Create some channels that have a conflicting max HTLC
|
||||
// constraint for one node pair, similar to the b->c channels.
|
||||
symmetricTestChannel("b", "z", chanCapSat, &testChannelPolicy{
|
||||
Expiry: 144,
|
||||
FeeRate: 50000,
|
||||
MinHTLC: lnwire.NewMSatFromSatoshis(20),
|
||||
MaxHTLC: lnwire.NewMSatFromSatoshis(25),
|
||||
Features: paymentAddrFeatures,
|
||||
}, 3),
|
||||
symmetricTestChannel("b", "z", chanCapSat, &testChannelPolicy{
|
||||
Expiry: 144,
|
||||
FeeRate: 60000,
|
||||
MinHTLC: lnwire.NewMSatFromSatoshis(20),
|
||||
MaxHTLC: lnwire.MilliSatoshi(20100),
|
||||
Features: paymentAddrFeatures,
|
||||
}, 8),
|
||||
|
||||
// Create a route with inbound fees.
|
||||
symmetricTestChannel("a", "d", chanCapSat, &testChannelPolicy{
|
||||
Expiry: 144,
|
||||
FeeRate: 20000,
|
||||
MinHTLC: lnwire.NewMSatFromSatoshis(5),
|
||||
MaxHTLC: lnwire.NewMSatFromSatoshis(
|
||||
chanCapSat,
|
||||
),
|
||||
InboundFeeBaseMsat: -1000,
|
||||
InboundFeeRate: -1000,
|
||||
}, 9),
|
||||
symmetricTestChannel("d", "f", chanCapSat, &testChannelPolicy{
|
||||
Expiry: 144,
|
||||
FeeRate: 60000,
|
||||
MinHTLC: lnwire.NewMSatFromSatoshis(20),
|
||||
MaxHTLC: lnwire.NewMSatFromSatoshis(120),
|
||||
Features: paymentAddrFeatures,
|
||||
// The inbound fee will not be active for the last hop.
|
||||
InboundFeeBaseMsat: 2000,
|
||||
InboundFeeRate: 2000,
|
||||
}, 10),
|
||||
}
|
||||
|
||||
testGraph, err := createTestGraphFromChannels(t, true, testChannels, "a")
|
||||
testGraph, err := createTestGraphFromChannels(
|
||||
t, true, testChannels, "a",
|
||||
)
|
||||
require.NoError(t, err, "unable to create graph")
|
||||
|
||||
const startingBlockHeight = 101
|
||||
@@ -1583,15 +1627,10 @@ func TestBuildRoute(t *testing.T) {
|
||||
|
||||
t.Helper()
|
||||
|
||||
if len(rt.Hops) != len(expected) {
|
||||
t.Fatal("hop count mismatch")
|
||||
}
|
||||
require.Len(t, rt.Hops, len(expected))
|
||||
|
||||
for i, hop := range rt.Hops {
|
||||
if hop.ChannelID != expected[i] {
|
||||
t.Fatalf("expected channel %v at pos %v, but "+
|
||||
"got channel %v",
|
||||
expected[i], i, hop.ChannelID)
|
||||
}
|
||||
require.Equal(t, expected[i], hop.ChannelID)
|
||||
}
|
||||
|
||||
lastHop := rt.Hops[len(rt.Hops)-1]
|
||||
@@ -1603,101 +1642,204 @@ func TestBuildRoute(t *testing.T) {
|
||||
_, err = rand.Read(payAddr[:])
|
||||
require.NoError(t, err)
|
||||
|
||||
noAmt := fn.None[lnwire.MilliSatoshi]()
|
||||
|
||||
// Test that we can't build a route when no hops are given.
|
||||
hops = []route.Vertex{}
|
||||
_, err = ctx.router.BuildRoute(noAmt, hops, nil, 40, nil)
|
||||
require.Error(t, err)
|
||||
|
||||
// Create hop list for an unknown destination.
|
||||
hops := []route.Vertex{ctx.aliases["b"], ctx.aliases["y"]}
|
||||
_, err = ctx.router.BuildRoute(noAmt, hops, nil, 40, &payAddr)
|
||||
noChanErr := ErrNoChannel{}
|
||||
require.ErrorAs(t, err, &noChanErr)
|
||||
require.Equal(t, 1, noChanErr.position)
|
||||
|
||||
// Create hop list from the route node pubkeys.
|
||||
hops := []route.Vertex{
|
||||
ctx.aliases["b"], ctx.aliases["c"],
|
||||
}
|
||||
hops = []route.Vertex{ctx.aliases["b"], ctx.aliases["c"]}
|
||||
amt := lnwire.NewMSatFromSatoshis(100)
|
||||
|
||||
// Build the route for the given amount.
|
||||
rt, err := ctx.router.BuildRoute(
|
||||
&amt, hops, nil, 40, &payAddr,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rt, err := ctx.router.BuildRoute(fn.Some(amt), hops, nil, 40, &payAddr)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Check that we get the expected route back. The total amount should be
|
||||
// the amount to deliver to hop c (100 sats) plus the max fee for the
|
||||
// connection b->c (6 sats).
|
||||
checkHops(rt, []uint64{1, 7}, payAddr)
|
||||
if rt.TotalAmount != 106000 {
|
||||
t.Fatalf("unexpected total amount %v", rt.TotalAmount)
|
||||
}
|
||||
require.Equal(t, lnwire.MilliSatoshi(106000), rt.TotalAmount)
|
||||
|
||||
// Build the route for the minimum amount.
|
||||
rt, err = ctx.router.BuildRoute(
|
||||
nil, hops, nil, 40, &payAddr,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rt, err = ctx.router.BuildRoute(noAmt, hops, nil, 40, &payAddr)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Check that we get the expected route back. The minimum that we can
|
||||
// send from b to c is 20 sats. Hop b charges 1200 msat for the
|
||||
// forwarding. The channel between hop a and b can carry amounts in the
|
||||
// range [5, 100], so 21200 msats is the minimum amount for this route.
|
||||
checkHops(rt, []uint64{1, 7}, payAddr)
|
||||
if rt.TotalAmount != 21200 {
|
||||
t.Fatalf("unexpected total amount %v", rt.TotalAmount)
|
||||
}
|
||||
require.Equal(t, lnwire.MilliSatoshi(21200), rt.TotalAmount)
|
||||
|
||||
// The receiver gets sent the minimal HTLC amount.
|
||||
require.Equal(t, lnwire.MilliSatoshi(20000), rt.Hops[1].AmtToForward)
|
||||
|
||||
// Test a route that contains incompatible channel htlc constraints.
|
||||
// There is no amount that can pass through both channel 5 and 4.
|
||||
hops = []route.Vertex{
|
||||
ctx.aliases["e"], ctx.aliases["c"],
|
||||
}
|
||||
_, err = ctx.router.BuildRoute(
|
||||
nil, hops, nil, 40, nil,
|
||||
)
|
||||
errNoChannel, ok := err.(ErrNoChannel)
|
||||
if !ok {
|
||||
t.Fatalf("expected incompatible policies error, but got %v",
|
||||
err)
|
||||
}
|
||||
if errNoChannel.position != 0 {
|
||||
t.Fatalf("unexpected no channel error position")
|
||||
}
|
||||
if errNoChannel.fromNode != ctx.aliases["a"] {
|
||||
t.Fatalf("unexpected no channel error node")
|
||||
}
|
||||
hops = []route.Vertex{ctx.aliases["e"], ctx.aliases["c"]}
|
||||
_, err = ctx.router.BuildRoute(noAmt, hops, nil, 40, nil)
|
||||
require.Error(t, err)
|
||||
noChanErr = ErrNoChannel{}
|
||||
require.ErrorAs(t, err, &noChanErr)
|
||||
require.Equal(t, 0, noChanErr.position)
|
||||
|
||||
// Test a route that contains channel constraints that lead to a
|
||||
// different selection of a unified edge, when the amount is rescaled
|
||||
// for the final edge. From a backward pass we expect the policy of
|
||||
// channel 8 to be used, because its policy has the highest fee rate,
|
||||
// bumping the amount to 20000 msat leading to a sender amount of 21200
|
||||
// msat including the fees for hop over channel 8. In the forward pass
|
||||
// however, we subtract that fee again, resulting in the min HTLC
|
||||
// amount. The forward pass doesn't check for a different policy that
|
||||
// could me more applicable, which is why we don't get back the highest
|
||||
// amount that could be delivered to the receiver of 21819 msat, using
|
||||
// policy of channel 3.
|
||||
hops = []route.Vertex{ctx.aliases["b"], ctx.aliases["z"]}
|
||||
rt, err = ctx.router.BuildRoute(noAmt, hops, nil, 40, &payAddr)
|
||||
require.NoError(t, err)
|
||||
checkHops(rt, []uint64{1, 8}, payAddr)
|
||||
require.Equal(t, lnwire.MilliSatoshi(21200), rt.TotalAmount)
|
||||
require.Equal(t, lnwire.MilliSatoshi(20000), rt.Hops[1].AmtToForward)
|
||||
|
||||
// Check that we compute a correct forwarding amount that involves
|
||||
// inbound fees. We expect a similar amount as for the above case of
|
||||
// b->c, but reduced by the inbound discount on the channel a->d.
|
||||
// We get 106000 - 1000 (base in) - 0.001 * 106000 (rate in) = 104894.
|
||||
hops = []route.Vertex{ctx.aliases["d"], ctx.aliases["f"]}
|
||||
amt = lnwire.NewMSatFromSatoshis(100)
|
||||
rt, err = ctx.router.BuildRoute(fn.Some(amt), hops, nil, 40, &payAddr)
|
||||
require.NoError(t, err)
|
||||
checkHops(rt, []uint64{9, 10}, payAddr)
|
||||
require.EqualValues(t, 104894, rt.TotalAmount)
|
||||
|
||||
// Also check the min amount with inbound fees. The min amount bumps
|
||||
// this to 20000 msat for the last hop. The outbound fee is 1200 msat,
|
||||
// the inbound fee is -1021.2 msat (rounded down). This results in a
|
||||
// total fee of 179 msat, giving a sender amount of 20179 msat. The
|
||||
// determined receiver amount however reduces this to 20001 msat again
|
||||
// due to rounding. This would not be compatible with the sender amount
|
||||
// of 20179 msat, which results in underpayment of 1 msat in fee. There
|
||||
// is a third pass through newRoute in which this gets corrected to end
|
||||
hops = []route.Vertex{ctx.aliases["d"], ctx.aliases["f"]}
|
||||
rt, err = ctx.router.BuildRoute(noAmt, hops, nil, 40, &payAddr)
|
||||
require.NoError(t, err)
|
||||
checkHops(rt, []uint64{9, 10}, payAddr)
|
||||
require.EqualValues(t, 20180, rt.TotalAmount, "%v", rt.TotalAmount)
|
||||
}
|
||||
|
||||
// TestGetPathEdges tests that the getPathEdges function returns the expected
|
||||
// edges and amount when given a set of unifiers and does not panic.
|
||||
func TestGetPathEdges(t *testing.T) {
|
||||
// TestReceiverAmtForwardPass tests that the forward pass returns the expected
|
||||
// receiver amount when given a set of edges and does not panic.
|
||||
func TestReceiverAmtForwardPass(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const startingBlockHeight = 101
|
||||
ctx := createTestCtxFromFile(t, startingBlockHeight, basicGraphFilePath)
|
||||
|
||||
testCases := []struct {
|
||||
sourceNode route.Vertex
|
||||
amt lnwire.MilliSatoshi
|
||||
unifiers []*edgeUnifier
|
||||
bandwidthHints *bandwidthManager
|
||||
hops []route.Vertex
|
||||
name string
|
||||
amt lnwire.MilliSatoshi
|
||||
unifiedEdges []*unifiedEdge
|
||||
hops []route.Vertex
|
||||
|
||||
expectedEdges []*models.CachedEdgePolicy
|
||||
expectedAmt lnwire.MilliSatoshi
|
||||
expectedErr string
|
||||
}{{
|
||||
sourceNode: ctx.aliases["roasbeef"],
|
||||
unifiers: []*edgeUnifier{
|
||||
{
|
||||
edges: []*unifiedEdge{},
|
||||
localChan: true,
|
||||
},
|
||||
expectedAmt lnwire.MilliSatoshi
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
expectedErr: "no edges to forward through",
|
||||
},
|
||||
expectedErr: fmt.Sprintf("no matching outgoing channel "+
|
||||
"available for node 0 (%v)", ctx.aliases["roasbeef"]),
|
||||
}}
|
||||
{
|
||||
name: "single edge, no valid policy",
|
||||
amt: 1000,
|
||||
unifiedEdges: []*unifiedEdge{
|
||||
{
|
||||
policy: &models.CachedEdgePolicy{
|
||||
MinHTLC: 1001,
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedErr: fmt.Sprintf("no matching outgoing " +
|
||||
"channel available for node index 0"),
|
||||
},
|
||||
{
|
||||
name: "single edge",
|
||||
amt: 1000,
|
||||
unifiedEdges: []*unifiedEdge{
|
||||
{
|
||||
policy: &models.CachedEdgePolicy{
|
||||
MinHTLC: 1000,
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedAmt: 1000,
|
||||
},
|
||||
{
|
||||
name: "outbound fee, no rounding",
|
||||
amt: 1e9,
|
||||
unifiedEdges: []*unifiedEdge{
|
||||
{
|
||||
// The first hop's outbound fee is
|
||||
// irrelevant in fee calculation.
|
||||
policy: &models.CachedEdgePolicy{
|
||||
FeeBaseMSat: 1234,
|
||||
FeeProportionalMillionths: 1234,
|
||||
},
|
||||
},
|
||||
{
|
||||
// No rounding is done here.
|
||||
policy: &models.CachedEdgePolicy{
|
||||
FeeBaseMSat: 1000,
|
||||
FeeProportionalMillionths: 1000,
|
||||
},
|
||||
},
|
||||
},
|
||||
// From an outgoing amount of 999000000 msat, we get
|
||||
// in = out + base + out * rate = 1000000000.0
|
||||
//
|
||||
// The inverse outgoing amount for this is
|
||||
// out = (in - base) / (1 + rate) =
|
||||
// (1e9 - 1000) / (1 + 1e-3) = 999000000.0000001,
|
||||
// which is rounded down.
|
||||
expectedAmt: 999000000,
|
||||
},
|
||||
{
|
||||
name: "outbound fee, rounding",
|
||||
amt: 1e9,
|
||||
unifiedEdges: []*unifiedEdge{
|
||||
{
|
||||
// The first hop's outbound fee is
|
||||
// irrelevant in fee calculation.
|
||||
policy: &models.CachedEdgePolicy{
|
||||
FeeBaseMSat: 1234,
|
||||
FeeProportionalMillionths: 1234,
|
||||
},
|
||||
},
|
||||
{
|
||||
// This policy is chosen such that we
|
||||
// round down.
|
||||
policy: &models.CachedEdgePolicy{
|
||||
FeeBaseMSat: 1000,
|
||||
FeeProportionalMillionths: 999,
|
||||
},
|
||||
},
|
||||
},
|
||||
// The float amount for this is
|
||||
// out = (in - base) / (1 + rate) =
|
||||
// (1e9 - 1000) / (1 + 999e-6) = 999000998.002995,
|
||||
// which is rounded up.
|
||||
expectedAmt: 999000999,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
pathEdges, amt, err := getPathEdges(
|
||||
tc.sourceNode, tc.amt, tc.unifiers, tc.bandwidthHints,
|
||||
tc.hops,
|
||||
)
|
||||
amt, err := receiverAmtForwardPass(tc.amt, tc.unifiedEdges)
|
||||
|
||||
if tc.expectedErr != "" {
|
||||
require.Error(t, err)
|
||||
@@ -1707,11 +1849,285 @@ func TestGetPathEdges(t *testing.T) {
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, pathEdges, tc.expectedEdges)
|
||||
require.Equal(t, amt, tc.expectedAmt)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSenderAmtBackwardPass tests that the computation of the sender amount is
|
||||
// done correctly for route building.
|
||||
func TestSenderAmtBackwardPass(t *testing.T) {
|
||||
bandwidthHints := bandwidthManager{
|
||||
getLink: func(chanId lnwire.ShortChannelID) (
|
||||
htlcswitch.ChannelLink, error) {
|
||||
|
||||
return nil, nil
|
||||
},
|
||||
localChans: make(map[lnwire.ShortChannelID]struct{}),
|
||||
}
|
||||
|
||||
var (
|
||||
capacity btcutil.Amount = 1_000_000
|
||||
testReceiverAmt lnwire.MilliSatoshi = 1_000_000
|
||||
minHTLC lnwire.MilliSatoshi = 1_000
|
||||
)
|
||||
|
||||
edgeUnifiers := []*edgeUnifier{
|
||||
{
|
||||
edges: []*unifiedEdge{
|
||||
{
|
||||
// This outbound fee doesn't have an
|
||||
// effect (sender doesn't pay outbound).
|
||||
policy: &models.CachedEdgePolicy{
|
||||
FeeBaseMSat: 112,
|
||||
},
|
||||
inboundFees: models.InboundFee{
|
||||
Base: 111,
|
||||
},
|
||||
capacity: capacity,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
edges: []*unifiedEdge{
|
||||
{
|
||||
policy: &models.CachedEdgePolicy{
|
||||
FeeBaseMSat: 222,
|
||||
},
|
||||
inboundFees: models.InboundFee{
|
||||
Base: 222,
|
||||
},
|
||||
capacity: capacity,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
edges: []*unifiedEdge{
|
||||
{
|
||||
policy: &models.CachedEdgePolicy{
|
||||
FeeBaseMSat: 333,
|
||||
MinHTLC: minHTLC,
|
||||
},
|
||||
// In pathfinding, inbound fees are not
|
||||
// populated for exit hops because the
|
||||
// newNodeEdgeUnifier enforces this.
|
||||
// This is important as otherwise we
|
||||
// would not fail the min HTLC check in
|
||||
// getEdge.
|
||||
capacity: capacity,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// A search for an amount that is below the minimum HTLC amount should
|
||||
// fail.
|
||||
_, _, err := senderAmtBackwardPass(
|
||||
edgeUnifiers, fn.Some(minHTLC-1), &bandwidthHints,
|
||||
)
|
||||
require.Error(t, err)
|
||||
|
||||
// Do a min amount search.
|
||||
_, senderAmount, err := senderAmtBackwardPass(
|
||||
edgeUnifiers, fn.None[lnwire.MilliSatoshi](), &bandwidthHints,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, minHTLC+333+222+222+111, senderAmount)
|
||||
|
||||
// Do a search for a specific amount.
|
||||
unifiedEdges, senderAmount, err := senderAmtBackwardPass(
|
||||
edgeUnifiers, fn.Some(testReceiverAmt), &bandwidthHints,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, testReceiverAmt+333+222+222+111, senderAmount)
|
||||
|
||||
// Check that we arrive at the same receiver amount by doing a forward
|
||||
// pass.
|
||||
receiverAmt, err := receiverAmtForwardPass(senderAmount, unifiedEdges)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, testReceiverAmt, receiverAmt)
|
||||
|
||||
// Insert a policy that leads to rounding.
|
||||
edgeUnifiers[1] = &edgeUnifier{
|
||||
edges: []*unifiedEdge{
|
||||
{
|
||||
policy: &models.CachedEdgePolicy{
|
||||
FeeBaseMSat: 20,
|
||||
FeeProportionalMillionths: 100,
|
||||
},
|
||||
inboundFees: models.InboundFee{
|
||||
Base: -10,
|
||||
Rate: -50,
|
||||
},
|
||||
capacity: capacity,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
unifiedEdges, senderAmount, err = senderAmtBackwardPass(
|
||||
edgeUnifiers, fn.Some(testReceiverAmt), &bandwidthHints,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
// For this route, we have some rounding errors, so we can't expect the
|
||||
// exact amount, but it should be higher than the exact amount, to not
|
||||
// end up below a min HTLC constraint.
|
||||
receiverAmt, err = receiverAmtForwardPass(senderAmount, unifiedEdges)
|
||||
require.NoError(t, err)
|
||||
require.NotEqual(t, testReceiverAmt, receiverAmt)
|
||||
require.InDelta(t, int64(testReceiverAmt), int64(receiverAmt), 1)
|
||||
require.GreaterOrEqual(t, int64(receiverAmt), int64(testReceiverAmt))
|
||||
}
|
||||
|
||||
// TestInboundOutbound tests the functions that computes the incoming and
|
||||
// outgoing amounts based on the fees of the incoming and outgoing channels.
|
||||
func TestInboundOutbound(t *testing.T) {
|
||||
var outgoingAmt uint64 = 10_000_000
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
incomingBase int32
|
||||
incomingRate int32
|
||||
outgoingBase uint64
|
||||
outgoingRate uint64
|
||||
}{
|
||||
{
|
||||
name: "only outbound fee",
|
||||
incomingBase: 0,
|
||||
incomingRate: 0,
|
||||
outgoingBase: 20,
|
||||
outgoingRate: 100,
|
||||
},
|
||||
{
|
||||
name: "positive inbound and outbound fee",
|
||||
incomingBase: 20,
|
||||
incomingRate: 100,
|
||||
outgoingBase: 20,
|
||||
outgoingRate: 100,
|
||||
},
|
||||
{
|
||||
name: "small negative inbound and outbound fee",
|
||||
incomingBase: -10,
|
||||
incomingRate: -50,
|
||||
outgoingBase: 20,
|
||||
outgoingRate: 100,
|
||||
},
|
||||
{
|
||||
name: "equal negative inbound and outbound fee",
|
||||
incomingBase: -20,
|
||||
incomingRate: -100,
|
||||
outgoingBase: 20,
|
||||
outgoingRate: 100,
|
||||
},
|
||||
{
|
||||
name: "large negative inbound and outbound fee",
|
||||
incomingBase: -30,
|
||||
incomingRate: -200,
|
||||
outgoingBase: 20,
|
||||
outgoingRate: 100,
|
||||
},
|
||||
{
|
||||
name: "order of PPM negative inbound and " +
|
||||
"outbound fee (m=0)",
|
||||
incomingBase: -30,
|
||||
incomingRate: -1_000_000,
|
||||
outgoingBase: 20,
|
||||
outgoingRate: 100,
|
||||
},
|
||||
{
|
||||
name: "huge negative inbound and " +
|
||||
"outbound fee (m<0)",
|
||||
incomingBase: -30,
|
||||
incomingRate: -2_000_000,
|
||||
outgoingBase: 20,
|
||||
outgoingRate: 100,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
tc := tc
|
||||
|
||||
t.Run(tc.name, func(tt *testing.T) {
|
||||
testInboundOutboundFee(
|
||||
tt, outgoingAmt, tc.incomingBase,
|
||||
tc.incomingRate, tc.outgoingBase,
|
||||
tc.outgoingRate,
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// testInboundOutboundFee is a helper function that tests the outgoing and
|
||||
// incoming amount relationship.
|
||||
func testInboundOutboundFee(t *testing.T, outgoingAmt uint64, inBase,
|
||||
inRate int32, outBase, outRate uint64) {
|
||||
|
||||
debugStr := fmt.Sprintf(
|
||||
"outAmt=%d, inBase=%d, inRate=%d, outBase=%d, outRate=%d",
|
||||
outgoingAmt, inBase, inRate, outBase, outRate,
|
||||
)
|
||||
|
||||
incomingEdge := &unifiedEdge{
|
||||
policy: &models.CachedEdgePolicy{},
|
||||
inboundFees: models.InboundFee{
|
||||
Base: inBase,
|
||||
Rate: inRate,
|
||||
},
|
||||
}
|
||||
|
||||
outgoingEdge := &unifiedEdge{
|
||||
policy: &models.CachedEdgePolicy{
|
||||
FeeBaseMSat: lnwire.MilliSatoshi(
|
||||
outBase,
|
||||
),
|
||||
FeeProportionalMillionths: lnwire.MilliSatoshi(
|
||||
outRate,
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
// We compute the incoming amount based on the outgoing amount, which
|
||||
// mimicks the path finding process.
|
||||
incomingAmt := incomingFromOutgoing(
|
||||
lnwire.MilliSatoshi(outgoingAmt), incomingEdge,
|
||||
outgoingEdge,
|
||||
)
|
||||
|
||||
// We do the reverse and compute the outgoing amount based on the
|
||||
// incoming amount.
|
||||
outgoingAmtNew := outgoingFromIncoming(
|
||||
incomingAmt, incomingEdge, outgoingEdge,
|
||||
)
|
||||
|
||||
// We require that the incoming amount is always larger than or equal to
|
||||
// the outgoing amount, because total fees (=incoming-outgoing) should
|
||||
// not become negative.
|
||||
require.GreaterOrEqual(
|
||||
t, int64(incomingAmt), int64(outgoingAmtNew), debugStr,
|
||||
"expected incomingAmt >= outgoingAmtNew",
|
||||
)
|
||||
|
||||
// We check that up to rounding the amounts are equal.
|
||||
require.InDelta(
|
||||
t, int64(outgoingAmt), int64(outgoingAmtNew), 1.0, debugStr,
|
||||
"expected |outgoingAmt - outgoingAmtNew | <= 1",
|
||||
)
|
||||
|
||||
// If we round, the computed outgoing amount should be larger than the
|
||||
// exact outgoing amount, to not hit any min HTLC limits.
|
||||
require.GreaterOrEqual(
|
||||
t, int64(outgoingAmtNew), int64(outgoingAmt), debugStr,
|
||||
"expected outgoingAmtNew >= outgoingAmt",
|
||||
)
|
||||
}
|
||||
|
||||
// FuzzInboundOutbound tests the incoming and outgoing amount calculation
|
||||
// functions with fuzzing.
|
||||
func FuzzInboundOutboundFee(f *testing.F) {
|
||||
f.Add(uint64(0), int32(0), int32(0), uint64(0), uint64(0))
|
||||
|
||||
f.Fuzz(testInboundOutboundFee)
|
||||
}
|
||||
|
||||
// TestSendToRouteSkipTempErrSuccess validates a successful payment send.
|
||||
func TestSendToRouteSkipTempErrSuccess(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
Reference in New Issue
Block a user