Skip to content

Commit

Permalink
include negative inbound fees in fee computation
Browse files Browse the repository at this point in the history
this doesn't influence path finding, just the actual payments
  • Loading branch information
C-Otto committed May 3, 2024
1 parent 08fec81 commit 9bc6399
Show file tree
Hide file tree
Showing 2 changed files with 113 additions and 11 deletions.
42 changes: 37 additions & 5 deletions model/src/main/java/de/cotto/lndmanagej/model/Route.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package de.cotto.lndmanagej.model;

import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
Expand Down Expand Up @@ -43,7 +44,9 @@ public Coins getFeesWithFirstHop() {
return Coins.NONE;
}
Coins forwardAmountForFirstHop = getFees().add(amount);
return getFees().add(getFeesForEdgeAndAmount(forwardAmountForFirstHop, edgesWithLiquidityInformation.get(0)));
return getFees().add(
getFeesForEdgeAndAmount(forwardAmountForFirstHop, null, edgesWithLiquidityInformation.get(0))
);
}

public double getProbability() {
Expand Down Expand Up @@ -139,18 +142,47 @@ private static List<Coins> computeFees(List<EdgeWithLiquidityInformation> edges,
List<Coins> feesForHops = new ArrayList<>();
feesForHops.add(Coins.NONE);
for (int i = edges.size() - 1; i > 0; i--) {
Coins feesForHop = getFeesForEdgeAndAmount(amountWithFees, edges.get(i));
EdgeWithLiquidityInformation edge = edges.get(i);
EdgeWithLiquidityInformation previousEdge = edges.get(i - 1);
Coins feesForHop = getFeesForEdgeAndAmount(amountWithFees, previousEdge, edge);
amountWithFees = amountWithFees.add(feesForHop);
fees = fees.add(feesForHop);
feesForHops.add(0, feesForHop);
}
return feesForHops;
}

private static Coins getFeesForEdgeAndAmount(Coins amountWithFees, EdgeWithLiquidityInformation edge) {
private static Coins getFeesForEdgeAndAmount(
Coins amountWithFees,
@Nullable EdgeWithLiquidityInformation previousEdge,
EdgeWithLiquidityInformation edge
) {
long feeRate = edge.policy().feeRate();
Coins relativeFees = getRelativeFees(amountWithFees, feeRate);
Coins baseFeeForHop = edge.policy().baseFee();
Coins relativeFees = Coins.ofMilliSatoshis(feeRate * amountWithFees.milliSatoshis() / 1_000_000);
return baseFeeForHop.add(relativeFees);
Coins outboundFees = baseFeeForHop.add(relativeFees);

Coins inboundFees = getInboundFees(amountWithFees.add(outboundFees), previousEdge);
Coins combinedFees = outboundFees.add(inboundFees);
if (combinedFees.isNegative()) {
return Coins.NONE;
}
return combinedFees;
}

private static Coins getInboundFees(Coins amountWithFees, @Nullable EdgeWithLiquidityInformation previousEdge) {
if (previousEdge == null) {
return Coins.NONE;
}
Policy policyForInboundFees = previousEdge.edge().reversePolicy();

Coins inboundBaseFeeForHop = policyForInboundFees.inboundBaseFee();
long inboundFeeRate = policyForInboundFees.inboundFeeRate();
Coins inboundRelativeFees = getRelativeFees(amountWithFees, inboundFeeRate);
return inboundBaseFeeForHop.add(inboundRelativeFees);
}

private static Coins getRelativeFees(Coins amount, long feeRate) {
return Coins.ofMilliSatoshis(feeRate * amount.milliSatoshis() / 1_000_000);
}
}
82 changes: 76 additions & 6 deletions model/src/test/java/de/cotto/lndmanagej/model/RouteTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -164,8 +164,7 @@ void fees_amount_with_milli_sat() {
Coins baseFee1 = Coins.ofMilliSatoshis(15);
Coins baseFee2 = Coins.ofMilliSatoshis(10);
Coins expectedFees =
Coins.ofMilliSatoshis((long) (amount.milliSatoshis() * 1.0 * ppm2 / ONE_MILLION))
.add(baseFee2);
totalFeesFor(amount, ppm2, baseFee2);
Policy policy1 = policy(baseFee1, ppm1);
Edge hop1 = new Edge(CHANNEL_ID, PUBKEY, PUBKEY_2, CAPACITY, policy1, Policy.UNKNOWN);
Policy policy2 = policy(baseFee2, ppm2);
Expand Down Expand Up @@ -195,8 +194,7 @@ void fees_two_hops() {
int ppm1 = 100;
int ppm2 = 200;
Coins expectedFees2 =
Coins.ofMilliSatoshis((long) (amount.milliSatoshis() * 1.0 * ppm2 / ONE_MILLION))
.add(baseFee2);
totalFeesFor(amount, ppm2, baseFee2);
Coins expectedFees1 = Coins.NONE;
Coins expectedFees = expectedFees1.add(expectedFees2);
BasicRoute basicRoute = new BasicRoute(List.of(
Expand All @@ -218,8 +216,7 @@ void fees_three_hops() {
int ppm3 = 300;
Coins expectedFees3 = Coins.NONE;
Coins expectedFees2 =
Coins.ofMilliSatoshis((long) (amount.milliSatoshis() * 1.0 * ppm3 / ONE_MILLION))
.add(baseFee3);
totalFeesFor(amount, ppm3, baseFee3);
long amountWithFeesLastHop = amount.add(expectedFees2).milliSatoshis();
Coins expectedFees1 = Coins.ofMilliSatoshis(
(long) (amountWithFeesLastHop * 1.0 * ppm2 / ONE_MILLION)
Expand All @@ -234,6 +231,71 @@ void fees_three_hops() {
assertThat(route.getFees()).isEqualTo(expectedFees);
}

@Test
void fees_two_hops_with_negative_inbound_fees() {
Coins amount = Coins.ofSatoshis(2_500_000);
Coins baseFee1 = Coins.ofMilliSatoshis(20);
Coins inboundBaseFee1 = Coins.ofMilliSatoshis(-3);
Coins baseFee2 = Coins.ofMilliSatoshis(6);
int ppm1 = 100;
int inboundPpm1 = -30;
int ppm2 = 200;
Coins expectedOutboundFees = totalFeesFor(amount, ppm2, baseFee2);
Coins expectedInboundFees = totalFeesFor(amount.add(expectedOutboundFees), inboundPpm1, inboundBaseFee1);
Coins expectedFees = expectedOutboundFees.add(expectedInboundFees);
BasicRoute basicRoute = new BasicRoute(List.of(
new Edge(
CHANNEL_ID,
PUBKEY,
PUBKEY_3,
CAPACITY,
policy(baseFee1, ppm1),
inboundPolicy(inboundBaseFee1, inboundPpm1)
),
new Edge(
CHANNEL_ID_2,
PUBKEY_3,
PUBKEY_4,
CAPACITY,
policy(baseFee2, ppm2),
inboundPolicy(Coins.NONE, 0)
)
), amount);
Route route = new Route(basicRoute);
assertThat(route.getFees()).isEqualTo(expectedFees);
}

@Test
void fees_two_hops_with_very_negative_inbound_fees() {
Coins amount = Coins.ofSatoshis(1_500_000);
Coins baseFee1 = Coins.ofMilliSatoshis(10);
Coins inboundBaseFee1 = Coins.ofMilliSatoshis(-15);
Coins baseFee2 = Coins.ofMilliSatoshis(6);
int ppm1 = 100;
int inboundPpm1 = -300;
int ppm2 = 200;
BasicRoute basicRoute = new BasicRoute(List.of(
new Edge(
CHANNEL_ID,
PUBKEY,
PUBKEY_2,
CAPACITY,
policy(baseFee1, ppm1),
inboundPolicy(inboundBaseFee1, inboundPpm1)
),
new Edge(
CHANNEL_ID_2,
PUBKEY,
PUBKEY_2,
CAPACITY,
policy(baseFee2, ppm2),
inboundPolicy(Coins.NONE, 0)
)
), amount);
Route route = new Route(basicRoute);
assertThat(route.getFees()).isEqualTo(Coins.NONE);
}

@Test
void feesWithFirstHop_empty() {
BasicRoute basicRoute = new BasicRoute(List.of(), Coins.ofSatoshis(1_500_000));
Expand Down Expand Up @@ -504,4 +566,12 @@ private List<Edge> edgesWithTimeLockDeltas(int... timeLockDeltas) {
private Policy policy(Coins baseFee, int ppm) {
return new Policy(ppm, baseFee, true, TIME_LOCK_DELTA, MIN_HTLC, MAX_HTLC);
}

private Policy inboundPolicy(Coins inboundBaseFee, int inboundPpm) {
return new Policy(0, Coins.NONE, inboundPpm, inboundBaseFee, true, TIME_LOCK_DELTA, MIN_HTLC, MAX_HTLC);
}

private Coins totalFeesFor(Coins amount, int ppm, Coins baseFee) {
return Coins.ofMilliSatoshis((long) (amount.milliSatoshis() * 1.0 * ppm / ONE_MILLION)).add(baseFee);
}
}

0 comments on commit 9bc6399

Please sign in to comment.