From abd5c1bc4e20522c6d52e3033ac13b9f1efff2d6 Mon Sep 17 00:00:00 2001 From: Carsten Otto Date: Sun, 27 Mar 2022 15:44:04 +0200 Subject: [PATCH] take known liquidity into account, add as 0-cost arc fixes #10 --- .../pickhardtpayments/ArcInitializer.java | 31 +++-- .../pickhardtpayments/FlowComputation.java | 15 +-- .../model/EdgeWithLiquidityInformation.java | 18 +++ .../pickhardtpayments/ArcInitializerTest.java | 114 ++++++++++++++++++ .../FlowComputationTest.java | 10 +- .../EdgeWithLiquidityInformationTest.java | 25 ++++ 6 files changed, 191 insertions(+), 22 deletions(-) diff --git a/pickhardt-payments/src/main/java/de/cotto/lndmanagej/pickhardtpayments/ArcInitializer.java b/pickhardt-payments/src/main/java/de/cotto/lndmanagej/pickhardtpayments/ArcInitializer.java index f30749c0..9fb03802 100644 --- a/pickhardt-payments/src/main/java/de/cotto/lndmanagej/pickhardtpayments/ArcInitializer.java +++ b/pickhardt-payments/src/main/java/de/cotto/lndmanagej/pickhardtpayments/ArcInitializer.java @@ -41,16 +41,21 @@ class ArcInitializer { } private void addArcs(EdgeWithLiquidityInformation edgeWithLiquidityInformation, Coins maximumCapacity) { - long capacitySat = edgeWithLiquidityInformation.availableLiquidityUpperBound().satoshis(); - if (capacitySat < quantization) { - return; - } Edge edge = edgeWithLiquidityInformation.edge(); int startNode = pubkeyToIntegerMapping.getMappedInteger(edge.startNode()); int endNode = pubkeyToIntegerMapping.getMappedInteger(edge.endNode()); - long capacity = capacitySat / quantization; - long unitCost = maximumCapacity.satoshis() / capacitySat; - long capacityPiece = capacity / piecewiseLinearApproximations; + + long quantizedLowerBound = quantize(edgeWithLiquidityInformation.availableLiquidityLowerBound()); + addArcForKnownLiquidity(edge, startNode, endNode, quantizedLowerBound); + + Coins upperBound = edgeWithLiquidityInformation.availableLiquidityUpperBound(); + long quantizedUpperBound = quantize(upperBound); + long uncertainButPossibleLiquidity = quantizedUpperBound - quantizedLowerBound; + long capacityPiece = uncertainButPossibleLiquidity / piecewiseLinearApproximations; + if (capacityPiece == 0) { + return; + } + long unitCost = quantize(maximumCapacity) / uncertainButPossibleLiquidity; for (int i = 1; i <= piecewiseLinearApproximations; i++) { int arcIndex = minCostFlow.addArcWithCapacityAndUnitCost( startNode, @@ -62,6 +67,14 @@ class ArcInitializer { } } + private void addArcForKnownLiquidity(Edge edge, int startNode, int endNode, long quantizedLowerBound) { + if (quantizedLowerBound <= 0) { + return; + } + int arcIndex = minCostFlow.addArcWithCapacityAndUnitCost(startNode, endNode, quantizedLowerBound, 0); + edgeMapping.put(arcIndex, edge); + } + private Coins getMaximumCapacity(Collection edgesWithLiquidityInformation) { return edgesWithLiquidityInformation.stream() .map(EdgeWithLiquidityInformation::edge) @@ -69,4 +82,8 @@ class ArcInitializer { .max(Comparator.naturalOrder()) .orElse(Coins.NONE); } + + private long quantize(Coins coins) { + return coins.milliSatoshis() / 1_000 / quantization; + } } diff --git a/pickhardt-payments/src/main/java/de/cotto/lndmanagej/pickhardtpayments/FlowComputation.java b/pickhardt-payments/src/main/java/de/cotto/lndmanagej/pickhardtpayments/FlowComputation.java index 4cb1c08f..949b77b4 100644 --- a/pickhardt-payments/src/main/java/de/cotto/lndmanagej/pickhardtpayments/FlowComputation.java +++ b/pickhardt-payments/src/main/java/de/cotto/lndmanagej/pickhardtpayments/FlowComputation.java @@ -94,28 +94,25 @@ public class FlowComputation { private Optional getKnownLiquidity(Edge edge, Pubkey ownPubKey) { Pubkey source = edge.startNode(); - Coins capacity = edge.capacity(); ChannelId channelId = edge.channelId(); if (ownPubKey.equals(source)) { - return Optional.of(getLocalChannelAvailableLocal(capacity, channelId)); + return getLocalChannelAvailableLocal(channelId); } Pubkey target = edge.endNode(); if (ownPubKey.equals(target)) { - return Optional.of(getLocalChannelAvailableRemote(capacity, channelId)); + return getLocalChannelAvailableRemote(channelId); } return Optional.empty(); } - private Coins getLocalChannelAvailableLocal(Coins capacity, ChannelId channelId) { + private Optional getLocalChannelAvailableLocal(ChannelId channelId) { return channelService.getLocalChannel(channelId) - .map(c -> balanceService.getAvailableLocalBalance(channelId)) - .orElse(capacity); + .map(c -> balanceService.getAvailableLocalBalance(channelId)); } - private Coins getLocalChannelAvailableRemote(Coins capacity, ChannelId channelId) { + private Optional getLocalChannelAvailableRemote(ChannelId channelId) { return channelService.getLocalChannel(channelId) - .map(c -> balanceService.getAvailableRemoteBalance(channelId)) - .orElse(capacity); + .map(c -> balanceService.getAvailableRemoteBalance(channelId)); } private Coins getAvailableLiquidityUpperBound(Edge edge) { diff --git a/pickhardt-payments/src/main/java/de/cotto/lndmanagej/pickhardtpayments/model/EdgeWithLiquidityInformation.java b/pickhardt-payments/src/main/java/de/cotto/lndmanagej/pickhardtpayments/model/EdgeWithLiquidityInformation.java index 543663fe..f943d6ab 100644 --- a/pickhardt-payments/src/main/java/de/cotto/lndmanagej/pickhardtpayments/model/EdgeWithLiquidityInformation.java +++ b/pickhardt-payments/src/main/java/de/cotto/lndmanagej/pickhardtpayments/model/EdgeWithLiquidityInformation.java @@ -7,11 +7,29 @@ public record EdgeWithLiquidityInformation( Coins availableLiquidityLowerBound, Coins availableLiquidityUpperBound ) { + public EdgeWithLiquidityInformation { + if (availableLiquidityLowerBound.compareTo(availableLiquidityUpperBound) > 0) { + throw new IllegalArgumentException("lower bound must not be higher than upper bound"); + } + } + public static EdgeWithLiquidityInformation forKnownLiquidity(Edge edge, Coins knownLiquidity) { return new EdgeWithLiquidityInformation(edge, knownLiquidity, knownLiquidity); } + public static EdgeWithLiquidityInformation forLowerBound(Edge edge, Coins availableLiquidityLowerBound) { + return new EdgeWithLiquidityInformation(edge, availableLiquidityLowerBound, edge.capacity()); + } + public static EdgeWithLiquidityInformation forUpperBound(Edge edge, Coins availableLiquidityUpperBound) { return new EdgeWithLiquidityInformation(edge, Coins.NONE, availableLiquidityUpperBound); } + + public static EdgeWithLiquidityInformation forLowerAndUpperBound( + Edge edge, + Coins availableLiquidityLowerBound, + Coins availableLiquidityUpperBound + ) { + return new EdgeWithLiquidityInformation(edge, availableLiquidityLowerBound, availableLiquidityUpperBound); + } } diff --git a/pickhardt-payments/src/test/java/de/cotto/lndmanagej/pickhardtpayments/ArcInitializerTest.java b/pickhardt-payments/src/test/java/de/cotto/lndmanagej/pickhardtpayments/ArcInitializerTest.java index 5b2bced8..3cddabef 100644 --- a/pickhardt-payments/src/test/java/de/cotto/lndmanagej/pickhardtpayments/ArcInitializerTest.java +++ b/pickhardt-payments/src/test/java/de/cotto/lndmanagej/pickhardtpayments/ArcInitializerTest.java @@ -7,6 +7,8 @@ import de.cotto.lndmanagej.model.Pubkey; import de.cotto.lndmanagej.pickhardtpayments.model.Edge; import de.cotto.lndmanagej.pickhardtpayments.model.EdgeWithLiquidityInformation; import de.cotto.lndmanagej.pickhardtpayments.model.IntegerMapping; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import java.util.LinkedHashMap; @@ -67,6 +69,118 @@ class ArcInitializerTest { assertThat(minCostFlow.getNumArcs()).isOne(); } + @Test + @SuppressWarnings("PMD.JUnitTestContainsTooManyAsserts") + void edge_with_known_liquidity_is_added_as_arc_without_cost() { + Coins capacity = Coins.ofSatoshis(100); + Coins knownLiquidity = Coins.ofSatoshis(25); + EdgeWithLiquidityInformation edgeWithLiquidityInformation = + EdgeWithLiquidityInformation.forKnownLiquidity(EDGE.withCapacity(capacity), knownLiquidity); + + arcInitializer.addArcs(Set.of(edgeWithLiquidityInformation)); + + assertThat(minCostFlow.getNumArcs()).isEqualTo(1); + assertThat(minCostFlow.getUnitCost(0)).isEqualTo(0); + assertThat(minCostFlow.getCapacity(0)).isEqualTo(25); + } + + @Nested + class EdgeWithLowerAndUpperBound { + private EdgeWithLiquidityInformation edgeWithLiquidityInformation; + + @BeforeEach + void setUp() { + Coins capacity = Coins.ofSatoshis(100); + Coins knownLiquidity = Coins.ofSatoshis(25); + edgeWithLiquidityInformation = EdgeWithLiquidityInformation.forLowerAndUpperBound( + EDGE.withCapacity(capacity), + knownLiquidity, + capacity + ); + } + + @Test + void added_as_arc_without_cost() { + arcInitializer.addArcs(Set.of(edgeWithLiquidityInformation)); + assertThat(minCostFlow.getUnitCost(0)).isEqualTo(0); + assertThat(minCostFlow.getCapacity(0)).isEqualTo(25); + } + + @Test + void adds_uncertain_liquidity_as_second_arc() { + arcInitializer.addArcs(Set.of(edgeWithLiquidityInformation)); + assertThat(minCostFlow.getUnitCost(1)).isEqualTo(1); + assertThat(minCostFlow.getCapacity(1)).isEqualTo(75); + } + + @Test + void splits_uncertain_liquidity_as_additional_arcs() { + ArcInitializer arcInitializer = new ArcInitializer( + minCostFlow, + integerMapping, + edgeMapping, + QUANTIZATION, + 5 + ); + arcInitializer.addArcs(Set.of(edgeWithLiquidityInformation)); + assertThat(minCostFlow.getNumArcs()).isEqualTo(6); + } + + @Test + void known_amount_matches_quantization() { + ArcInitializer arcInitializer = new ArcInitializer( + minCostFlow, + integerMapping, + edgeMapping, + edgeWithLiquidityInformation.availableLiquidityLowerBound().satoshis(), + PIECEWISE_LINEAR_APPROXIMATIONS + ); + arcInitializer.addArcs(Set.of(edgeWithLiquidityInformation)); + assertThat(minCostFlow.getUnitCost(0)).isEqualTo(0); + } + + @Test + void known_amount_rounded_due_to_quantization() { + ArcInitializer arcInitializer = new ArcInitializer( + minCostFlow, + integerMapping, + edgeMapping, + 20, + PIECEWISE_LINEAR_APPROXIMATIONS + ); + arcInitializer.addArcs(Set.of(edgeWithLiquidityInformation)); + assertThat(minCostFlow.getCapacity(0)).isEqualTo(1); + } + + @Test + void splits_remaining_liquidity_after_rounding_known_liquidity() { + ArcInitializer arcInitializer = new ArcInitializer( + minCostFlow, + integerMapping, + edgeMapping, + 10, + QUANTIZATION + ); + arcInitializer.addArcs(Set.of(edgeWithLiquidityInformation)); + // one arc for the known liquidity (25 / 10 = 2), 100 / 10 - 2 = 8 remaining + assertThat(minCostFlow.getCapacity(1)).isEqualTo(8); + } + + @Test + void does_not_add_arcs_without_capacity() { + ArcInitializer arcInitializer = new ArcInitializer( + minCostFlow, + integerMapping, + edgeMapping, + 20, + 5 + ); + arcInitializer.addArcs(Set.of(edgeWithLiquidityInformation)); + // one arc for the known liquidity (25 / 20 = 1), 100 / 20 - 1 = 4 remaining: 4 < 5, no additional arc added + assertThat(minCostFlow.getNumArcs()).isEqualTo(1); + } + } + @Test void adds_edge_to_edgeMapping() { int piecesPerChannel = 2; diff --git a/pickhardt-payments/src/test/java/de/cotto/lndmanagej/pickhardtpayments/FlowComputationTest.java b/pickhardt-payments/src/test/java/de/cotto/lndmanagej/pickhardtpayments/FlowComputationTest.java index e7fb171a..264803bf 100644 --- a/pickhardt-payments/src/test/java/de/cotto/lndmanagej/pickhardtpayments/FlowComputationTest.java +++ b/pickhardt-payments/src/test/java/de/cotto/lndmanagej/pickhardtpayments/FlowComputationTest.java @@ -100,11 +100,10 @@ class FlowComputationTest { } @Test - void solve_avoids_sending_from_local_channel_lacking_capacity() { - // TODO use balance of local channel as known balance, not upper bound + void solve_avoids_sending_from_depleted_local_channel() { when(channelService.getLocalChannel(CHANNEL_ID)).thenReturn(Optional.of(LOCAL_OPEN_CHANNEL)); when(channelService.getLocalChannel(CHANNEL_ID_2)).thenReturn(Optional.empty()); - when(balanceService.getAvailableLocalBalance(CHANNEL_ID)).thenReturn(Coins.ofSatoshis(1)); + when(balanceService.getAvailableLocalBalance(CHANNEL_ID)).thenReturn(Coins.NONE); Coins amount = Coins.ofSatoshis(100); DirectedChannelEdge largerButDepletedChannel = new DirectedChannelEdge(CHANNEL_ID, LARGE, PUBKEY, PUBKEY_2, POLICY_1); @@ -119,11 +118,10 @@ class FlowComputationTest { } @Test - void solve_avoids_sending_to_local_channel_lacking_capacity() { - // TODO use balance of local channel as known balance, not upper bound + void solve_avoids_sending_to_depleted_local_channel() { when(channelService.getLocalChannel(CHANNEL_ID)).thenReturn(Optional.of(LOCAL_OPEN_CHANNEL)); when(channelService.getLocalChannel(CHANNEL_ID_2)).thenReturn(Optional.empty()); - when(balanceService.getAvailableRemoteBalance(CHANNEL_ID)).thenReturn(Coins.ofSatoshis(1)); + when(balanceService.getAvailableRemoteBalance(CHANNEL_ID)).thenReturn(Coins.NONE); Coins amount = Coins.ofSatoshis(100); DirectedChannelEdge largerButDepletedChannel = new DirectedChannelEdge(CHANNEL_ID, LARGE, PUBKEY_2, PUBKEY, POLICY_1); diff --git a/pickhardt-payments/src/test/java/de/cotto/lndmanagej/pickhardtpayments/model/EdgeWithLiquidityInformationTest.java b/pickhardt-payments/src/test/java/de/cotto/lndmanagej/pickhardtpayments/model/EdgeWithLiquidityInformationTest.java index fdc7631b..5d0d5720 100644 --- a/pickhardt-payments/src/test/java/de/cotto/lndmanagej/pickhardtpayments/model/EdgeWithLiquidityInformationTest.java +++ b/pickhardt-payments/src/test/java/de/cotto/lndmanagej/pickhardtpayments/model/EdgeWithLiquidityInformationTest.java @@ -6,6 +6,7 @@ import org.junit.jupiter.api.Test; import static de.cotto.lndmanagej.pickhardtpayments.model.EdgeFixtures.EDGE; import static de.cotto.lndmanagej.pickhardtpayments.model.EdgeWithLiquidityInformationFixtures.EDGE_WITH_LIQUIDITY_INFORMATION; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; class EdgeWithLiquidityInformationTest { @Test @@ -20,6 +21,13 @@ class EdgeWithLiquidityInformationTest { .isEqualTo(new EdgeWithLiquidityInformation(EDGE, knownLiquidity, knownLiquidity)); } + @Test + void forLowerBound() { + Coins lowerBound = Coins.ofSatoshis(300); + assertThat(EdgeWithLiquidityInformation.forLowerBound(EDGE, lowerBound)) + .isEqualTo(new EdgeWithLiquidityInformation(EDGE, lowerBound, EDGE.capacity())); + } + @Test void forUpperBound() { Coins upperBound = Coins.ofSatoshis(300); @@ -27,6 +35,23 @@ class EdgeWithLiquidityInformationTest { .isEqualTo(new EdgeWithLiquidityInformation(EDGE, Coins.NONE, upperBound)); } + @Test + void forLowerAndUpperBound() { + Coins lowerBound = Coins.ofSatoshis(100); + Coins upperBound = Coins.ofSatoshis(300); + assertThat(EdgeWithLiquidityInformation.forLowerAndUpperBound(EDGE, lowerBound, upperBound)) + .isEqualTo(new EdgeWithLiquidityInformation(EDGE, lowerBound, upperBound)); + } + + @Test + void forLowerAndUpperBound_lower_more_than_upper() { + Coins lowerBound = Coins.ofSatoshis(301); + Coins upperBound = Coins.ofSatoshis(300); + assertThatIllegalArgumentException().isThrownBy( + () -> EdgeWithLiquidityInformation.forLowerAndUpperBound(EDGE, lowerBound, upperBound) + ); + } + @Test void availableLiquidityUpperBound() { assertThat(EDGE_WITH_LIQUIDITY_INFORMATION.availableLiquidityUpperBound()).isEqualTo(Coins.ofSatoshis(123));