take known liquidity into account, add as 0-cost arc

fixes #10
This commit is contained in:
Carsten Otto
2022-03-27 15:44:04 +02:00
parent 3d630e9073
commit abd5c1bc4e
6 changed files with 191 additions and 22 deletions

View File

@@ -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<EdgeWithLiquidityInformation> 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;
}
}

View File

@@ -94,28 +94,25 @@ public class FlowComputation {
private Optional<Coins> 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<Coins> 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<Coins> getLocalChannelAvailableRemote(ChannelId channelId) {
return channelService.getLocalChannel(channelId)
.map(c -> balanceService.getAvailableRemoteBalance(channelId))
.orElse(capacity);
.map(c -> balanceService.getAvailableRemoteBalance(channelId));
}
private Coins getAvailableLiquidityUpperBound(Edge edge) {

View File

@@ -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);
}
}

View File

@@ -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;

View File

@@ -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);

View File

@@ -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));