top-up: impose stricter fee limit for all hops (but not the ones from the top-up peer)

This commit is contained in:
Carsten Otto
2022-05-26 10:09:02 +02:00
parent 22c5866f6b
commit da4abdc2f8
9 changed files with 115 additions and 65 deletions

View File

@@ -9,6 +9,7 @@ import de.cotto.lndmanagej.model.EdgeWithLiquidityInformation;
import de.cotto.lndmanagej.model.Node;
import de.cotto.lndmanagej.model.Policy;
import de.cotto.lndmanagej.model.Pubkey;
import de.cotto.lndmanagej.pickhardtpayments.model.PaymentOptions;
import de.cotto.lndmanagej.service.BalanceService;
import de.cotto.lndmanagej.service.ChannelService;
import de.cotto.lndmanagej.service.LiquidityBoundsService;
@@ -37,6 +38,7 @@ import static de.cotto.lndmanagej.model.PolicyFixtures.POLICY_WITH_BASE_FEE;
import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY;
import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY_2;
import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY_4;
import static de.cotto.lndmanagej.pickhardtpayments.model.PaymentOptions.DEFAULT_PAYMENT_OPTIONS;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.lenient;
@@ -79,23 +81,24 @@ class EdgeComputationTest {
@Test
void no_graph() {
assertThat(edgeComputation.getEdges().edges()).isEmpty();
assertThat(edgeComputation.getEdges(DEFAULT_PAYMENT_OPTIONS).edges()).isEmpty();
}
@Test
void does_not_add_edge_for_disabled_channel() {
DirectedChannelEdge edge = new DirectedChannelEdge(CHANNEL_ID, CAPACITY, PUBKEY, PUBKEY_2, POLICY_DISABLED);
when(grpcGraph.getChannelEdges()).thenReturn(Optional.of(Set.of(edge)));
assertThat(edgeComputation.getEdges().edges()).isEmpty();
assertThat(edgeComputation.getEdges(DEFAULT_PAYMENT_OPTIONS).edges()).isEmpty();
}
@Test
void does_not_add_edge_with_fee_rate_at_or_above_limit() {
int feeRateLimit = 199;
Policy policyExpensive = new Policy(200, Coins.NONE, true, 40, Coins.ofSatoshis(0));
PaymentOptions paymentOptions = PaymentOptions.forFeeRateLimit(feeRateLimit);
Policy policyExpensive = policy(200);
// needs to be excluded to avoid sending top-up payments in a tiny loop: S-X-S
Policy policyAtLimit = new Policy(199, Coins.NONE, true, 40, Coins.ofSatoshis(0));
Policy policyOk = new Policy(198, Coins.NONE, true, 40, Coins.ofSatoshis(0));
Policy policyAtLimit = policy(199);
Policy policyOk = policy(198);
DirectedChannelEdge edgeExpensive =
new DirectedChannelEdge(CHANNEL_ID, CAPACITY, PUBKEY, PUBKEY_2, policyExpensive);
DirectedChannelEdge edgeAtLimit =
@@ -103,8 +106,33 @@ class EdgeComputationTest {
DirectedChannelEdge edgeOk =
new DirectedChannelEdge(CHANNEL_ID_3, CAPACITY, PUBKEY, PUBKEY_2, policyOk);
when(grpcGraph.getChannelEdges()).thenReturn(Optional.of(Set.of(edgeExpensive, edgeAtLimit, edgeOk)));
assertThat(edgeComputation.getEdges(feeRateLimit).edges().stream().map(EdgeWithLiquidityInformation::channelId))
.containsExactly(CHANNEL_ID_3);
assertThat(
edgeComputation.getEdges(paymentOptions).edges().stream().map(EdgeWithLiquidityInformation::channelId)
).containsExactly(CHANNEL_ID_3);
}
@Test
void does_not_add_first_hop_edge_with_fee_rate_at_or_above_limit_for_first_hops() {
Pubkey ownPubkey = EDGE.startNode();
Pubkey topUpPeer = PUBKEY_4;
int feeRateLimit = 200;
int feeRateLimitForFirstHops = 100;
when(grpcGetInfo.getPubkey()).thenReturn(ownPubkey);
PaymentOptions paymentOptions = PaymentOptions.forTopUp(feeRateLimit, feeRateLimitForFirstHops, topUpPeer);
Policy lastHopPolicy = policy(199);
Policy firstHopPolicyExpensive = policy(100);
Policy firstHopPolicyOk = policy(99);
DirectedChannelEdge lastHop =
new DirectedChannelEdge(CHANNEL_ID, CAPACITY, topUpPeer, ownPubkey, lastHopPolicy);
DirectedChannelEdge firstHopExpensive =
new DirectedChannelEdge(CHANNEL_ID_2, CAPACITY, ownPubkey, PUBKEY_2, firstHopPolicyExpensive);
DirectedChannelEdge firstHopOk =
new DirectedChannelEdge(CHANNEL_ID_3, CAPACITY, ownPubkey, PUBKEY_2, firstHopPolicyOk);
when(grpcGraph.getChannelEdges()).thenReturn(Optional.of(Set.of(lastHop, firstHopExpensive, firstHopOk)));
assertThat(
edgeComputation.getEdges(paymentOptions).edges().stream().map(EdgeWithLiquidityInformation::channelId)
).containsExactlyInAnyOrder(CHANNEL_ID, CHANNEL_ID_3);
}
@Test
@@ -112,7 +140,7 @@ class EdgeComputationTest {
DirectedChannelEdge edge =
new DirectedChannelEdge(CHANNEL_ID, CAPACITY, PUBKEY, PUBKEY_2, POLICY_WITH_BASE_FEE);
when(grpcGraph.getChannelEdges()).thenReturn(Optional.of(Set.of(edge)));
assertThat(edgeComputation.getEdges().edges()).isNotEmpty();
assertThat(edgeComputation.getEdges(DEFAULT_PAYMENT_OPTIONS).edges()).isNotEmpty();
}
@Test
@@ -124,7 +152,7 @@ class EdgeComputationTest {
Coins availableKnownLiquidity = getAvailableKnownLiquidity(knownLiquidity);
when(balanceService.getAvailableLocalBalance(EDGE.channelId())).thenReturn(knownLiquidity);
assertThat(edgeComputation.getEdges().edges())
assertThat(edgeComputation.getEdges(DEFAULT_PAYMENT_OPTIONS).edges())
.contains(EdgeWithLiquidityInformation.forKnownLiquidity(EDGE, availableKnownLiquidity));
}
@@ -143,7 +171,7 @@ class EdgeComputationTest {
edge.policy()
)
));
assertThat(edgeComputation.getEdges().edges())
assertThat(edgeComputation.getEdges(DEFAULT_PAYMENT_OPTIONS).edges())
.contains(EdgeWithLiquidityInformation.forKnownLiquidity(edge, fiftyCoins));
}
@@ -152,7 +180,7 @@ class EdgeComputationTest {
mockEdge();
when(grpcGetInfo.getPubkey()).thenReturn(EDGE.startNode());
assertThat(edgeComputation.getEdges().edges())
assertThat(edgeComputation.getEdges(DEFAULT_PAYMENT_OPTIONS).edges())
.contains(EdgeWithLiquidityInformation.forKnownLiquidity(EDGE, Coins.NONE));
}
@@ -162,7 +190,7 @@ class EdgeComputationTest {
mockOfflinePeer();
when(grpcGetInfo.getPubkey()).thenReturn(EDGE.startNode());
assertThat(edgeComputation.getEdges().edges())
assertThat(edgeComputation.getEdges(DEFAULT_PAYMENT_OPTIONS).edges())
.contains(EdgeWithLiquidityInformation.forKnownLiquidity(EDGE, Coins.NONE));
verify(balanceService, never()).getAvailableLocalBalance(EDGE.channelId());
}
@@ -176,7 +204,7 @@ class EdgeComputationTest {
Coins availableKnownLiquidity = getAvailableKnownLiquidity(knownLiquidity);
when(balanceService.getAvailableRemoteBalance(EDGE.channelId())).thenReturn(knownLiquidity);
assertThat(edgeComputation.getEdges().edges())
assertThat(edgeComputation.getEdges(DEFAULT_PAYMENT_OPTIONS).edges())
.contains(EdgeWithLiquidityInformation.forKnownLiquidity(EDGE, availableKnownLiquidity));
}
@@ -185,7 +213,7 @@ class EdgeComputationTest {
mockEdge();
when(grpcGetInfo.getPubkey()).thenReturn(EDGE.endNode());
assertThat(edgeComputation.getEdges().edges())
assertThat(edgeComputation.getEdges(DEFAULT_PAYMENT_OPTIONS).edges())
.contains(EdgeWithLiquidityInformation.forKnownLiquidity(EDGE, Coins.NONE));
}
@@ -195,7 +223,7 @@ class EdgeComputationTest {
mockOfflinePeer();
when(grpcGetInfo.getPubkey()).thenReturn(EDGE.endNode());
assertThat(edgeComputation.getEdges().edges())
assertThat(edgeComputation.getEdges(DEFAULT_PAYMENT_OPTIONS).edges())
.contains(EdgeWithLiquidityInformation.forKnownLiquidity(EDGE, Coins.NONE));
verify(balanceService, never()).getAvailableLocalBalance(EDGE.channelId());
}
@@ -211,14 +239,14 @@ class EdgeComputationTest {
mockEdge();
Coins upperBound = Coins.ofSatoshis(100);
mockUpperBound(upperBound);
assertThat(edgeComputation.getEdges().edges())
assertThat(edgeComputation.getEdges(DEFAULT_PAYMENT_OPTIONS).edges())
.contains(EdgeWithLiquidityInformation.forUpperBound(EDGE, upperBound));
}
@Test
void default_if_no_liquidity_information_is_known() {
mockEdge();
assertThat(edgeComputation.getEdges().edges())
assertThat(edgeComputation.getEdges(DEFAULT_PAYMENT_OPTIONS).edges())
.contains(EdgeWithLiquidityInformation.forUpperBound(EDGE, EDGE.capacity()));
}
@@ -321,4 +349,8 @@ class EdgeComputationTest {
Coins withOnChainReserve = withFeeReserve.subtract(Coins.ofSatoshis(1_000));
return withOnChainReserve.maximum(Coins.NONE);
}
private static Policy policy(int feeRate) {
return new Policy(feeRate, Coins.NONE, true, 40, Coins.ofSatoshis(0));
}
}

View File

@@ -31,7 +31,6 @@ import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY_3;
import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY_4;
import static de.cotto.lndmanagej.pickhardtpayments.model.PaymentOptions.DEFAULT_PAYMENT_OPTIONS;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -62,23 +61,24 @@ class FlowComputationTest {
@Test
void solve_no_edge() {
when(edgeComputation.getEdges()).thenReturn(EdgesWithLiquidityInformation.EMPTY);
when(edgeComputation.getEdges(DEFAULT_PAYMENT_OPTIONS)).thenReturn(EdgesWithLiquidityInformation.EMPTY);
assertThat(flowComputation.getOptimalFlows(PUBKEY, PUBKEY_2, Coins.ofSatoshis(1), DEFAULT_PAYMENT_OPTIONS))
.isEqualTo(new Flows());
}
@Test
void passes_fee_rate_limit_to_get_edges() {
when(edgeComputation.getEdges(anyLong())).thenReturn(EdgesWithLiquidityInformation.EMPTY);
flowComputation.getOptimalFlows(PUBKEY, PUBKEY_2, Coins.ofSatoshis(1), PaymentOptions.forFeeRateLimit(123));
verify(edgeComputation).getEdges(123);
PaymentOptions paymentOptions = PaymentOptions.forFeeRateLimit(123);
when(edgeComputation.getEdges(paymentOptions)).thenReturn(EdgesWithLiquidityInformation.EMPTY);
flowComputation.getOptimalFlows(PUBKEY, PUBKEY_2, Coins.ofSatoshis(1), paymentOptions);
verify(edgeComputation).getEdges(paymentOptions);
}
@Test
void solve() {
Coins amount = Coins.ofSatoshis(1);
EdgeWithLiquidityInformation edge = EdgeWithLiquidityInformation.forUpperBound(EDGE, EDGE.capacity());
when(edgeComputation.getEdges()).thenReturn(new EdgesWithLiquidityInformation(edge));
when(edgeComputation.getEdges(DEFAULT_PAYMENT_OPTIONS)).thenReturn(new EdgesWithLiquidityInformation(edge));
Flow expectedFlow = new Flow(EDGE, amount);
assertThat(flowComputation.getOptimalFlows(PUBKEY, PUBKEY_2, amount, DEFAULT_PAYMENT_OPTIONS))
.isEqualTo(new Flows(expectedFlow));
@@ -89,7 +89,7 @@ class FlowComputationTest {
when(configurationService.getIntegerValue(QUANTIZATION)).thenReturn(Optional.of(10));
Coins amount = Coins.ofSatoshis(9);
EdgeWithLiquidityInformation edge = EdgeWithLiquidityInformation.forUpperBound(EDGE, EDGE.capacity());
when(edgeComputation.getEdges()).thenReturn(new EdgesWithLiquidityInformation(edge));
when(edgeComputation.getEdges(DEFAULT_PAYMENT_OPTIONS)).thenReturn(new EdgesWithLiquidityInformation(edge));
assertThat(flowComputation.getOptimalFlows(PUBKEY, PUBKEY_2, amount, DEFAULT_PAYMENT_OPTIONS))
.isEqualTo(new Flows(new Flow(EDGE, amount)));
}
@@ -98,7 +98,7 @@ class FlowComputationTest {
void solve_avoids_sending_from_depleted_local_channel() {
Edge edge1 = new Edge(CHANNEL_ID, PUBKEY, PUBKEY_2, LARGE, POLICY_1);
Edge edge2 = new Edge(CHANNEL_ID_2, PUBKEY, PUBKEY_2, SMALL, POLICY_2);
when(edgeComputation.getEdges()).thenReturn(new EdgesWithLiquidityInformation(
when(edgeComputation.getEdges(DEFAULT_PAYMENT_OPTIONS)).thenReturn(new EdgesWithLiquidityInformation(
EdgeWithLiquidityInformation.forKnownLiquidity(edge1, Coins.NONE),
EdgeWithLiquidityInformation.forUpperBound(edge2, SMALL)
));
@@ -114,7 +114,7 @@ class FlowComputationTest {
Edge edge1a = new Edge(CHANNEL_ID, PUBKEY_2, PUBKEY_3, LARGE, POLICY_1);
Edge edge1b = new Edge(CHANNEL_ID_2, PUBKEY_3, PUBKEY_4, LARGE, POLICY_1);
Edge edge2 = new Edge(CHANNEL_ID_3, PUBKEY_2, PUBKEY_4, SMALL, POLICY_2);
when(edgeComputation.getEdges()).thenReturn(new EdgesWithLiquidityInformation(
when(edgeComputation.getEdges(DEFAULT_PAYMENT_OPTIONS)).thenReturn(new EdgesWithLiquidityInformation(
EdgeWithLiquidityInformation.forUpperBound(edge1a, amount),
EdgeWithLiquidityInformation.forUpperBound(edge1b, LARGE),
EdgeWithLiquidityInformation.forUpperBound(edge2, SMALL)
@@ -130,7 +130,7 @@ class FlowComputationTest {
Edge edge1a = new Edge(CHANNEL_ID, PUBKEY_3, PUBKEY_4, LARGE, POLICY_2);
Edge edge1b = new Edge(CHANNEL_ID_2, PUBKEY_4, PUBKEY, LARGE, POLICY_2);
Edge edge2 = new Edge(CHANNEL_ID_3, PUBKEY_3, PUBKEY, SMALL, POLICY_1);
when(edgeComputation.getEdges()).thenReturn(new EdgesWithLiquidityInformation(
when(edgeComputation.getEdges(DEFAULT_PAYMENT_OPTIONS)).thenReturn(new EdgesWithLiquidityInformation(
EdgeWithLiquidityInformation.forUpperBound(edge1a, Coins.ofSatoshis(5_000_000)),
EdgeWithLiquidityInformation.forUpperBound(edge1b, LARGE),
EdgeWithLiquidityInformation.forUpperBound(edge2, SMALL)

View File

@@ -123,7 +123,7 @@ class MultiPathPaymentSplitterTest {
int feeRate = 200;
Coins amount = Coins.ofSatoshis(1_000_000);
Policy policy = policyFor(feeRate);
PaymentOptions paymentOptions = PaymentOptions.forTopUp(feeRate - 1, PUBKEY_2);
PaymentOptions paymentOptions = PaymentOptions.forTopUp(feeRate - 1, 0, PUBKEY_2);
mockFlow(amount, policy, paymentOptions);
MultiPathPayment multiPathPayment =
@@ -137,7 +137,7 @@ class MultiPathPaymentSplitterTest {
int feeRate = 200;
Coins amount = Coins.ofSatoshis(2_000_000);
Policy policy = policyFor(feeRate);
PaymentOptions paymentOptions = PaymentOptions.forTopUp(feeRate, PUBKEY_2);
PaymentOptions paymentOptions = PaymentOptions.forTopUp(feeRate, 0, PUBKEY_2);
mockFlow(amount, policy, paymentOptions);
MultiPathPayment multiPathPayment =
@@ -151,7 +151,7 @@ class MultiPathPaymentSplitterTest {
mockExtensionEdge(PUBKEY_3, feeRate);
Coins amount = Coins.ofSatoshis(2_000_000);
Policy policy = policyFor(0);
PaymentOptions paymentOptions = PaymentOptions.forTopUp(feeRate - 1, PUBKEY_2);
PaymentOptions paymentOptions = PaymentOptions.forTopUp(feeRate - 1, 0, PUBKEY_2);
mockFlow(amount, policy, paymentOptions);
MultiPathPayment multiPathPayment =
@@ -197,11 +197,13 @@ class MultiPathPaymentSplitterTest {
@Test
void one_flow_has_fee_rate_above_limit_but_average_fee_rate_is_below_limit_including_fees_from_first_hop() {
// The cheaper flows might fail while the more expensive one settles. This is not what we want, so we have
// to disregard all flows.
mockExtensionEdge(PUBKEY_4, 0);
int feeRate = 200;
Coins halfOfAmount = Coins.ofSatoshis(500_000);
Coins amount = halfOfAmount.add(halfOfAmount);
PaymentOptions paymentOptions = PaymentOptions.forTopUp(feeRate - 1, PUBKEY_2);
PaymentOptions paymentOptions = PaymentOptions.forTopUp(feeRate - 1, 0, PUBKEY_2);
Edge edge1 = new Edge(CHANNEL_ID, PUBKEY, PUBKEY_2, CAPACITY, policyFor(0));
Edge edge2 = new Edge(CHANNEL_ID, PUBKEY, PUBKEY_2, CAPACITY, policyFor(feeRate));
Flow flow1 = new Flow(edge1, halfOfAmount);
@@ -348,7 +350,7 @@ class MultiPathPaymentSplitterTest {
}
private MultiPathPayment attemptTopUpPayment() {
PaymentOptions paymentOptions = PaymentOptions.forTopUp(500, PUBKEY_2);
PaymentOptions paymentOptions = PaymentOptions.forTopUp(500, 123, PUBKEY_2);
when(flowComputation.getOptimalFlows(PUBKEY, PUBKEY_2, AMOUNT, paymentOptions)).thenReturn(new Flows(FLOW));
return multiPathPaymentSplitter.getMultiPathPayment(PUBKEY, PUBKEY_3, AMOUNT, paymentOptions);
}

View File

@@ -184,7 +184,7 @@ class TopUpServiceTest {
private void assertTopUp(Coins expectedTopUpAmount, Duration expiry) {
PaymentStatus paymentStatus = topUpService.topUp(PUBKEY, AMOUNT);
verify(grpcInvoices).createPaymentRequest(expectedTopUpAmount, DESCRIPTION, expiry);
PaymentOptions paymentOptions = PaymentOptions.forTopUp(OUR_FEE_RATE, PUBKEY);
PaymentOptions paymentOptions = PaymentOptions.forTopUp(OUR_FEE_RATE, PEER_FEE_RATE, PUBKEY);
verify(multiPathPaymentSender).payPaymentRequest(DECODED_PAYMENT_REQUEST, paymentOptions);
assertThat(paymentStatus.isPending()).isTrue();
}

View File

@@ -12,19 +12,25 @@ class PaymentOptionsTest {
@Test
void forFeeRateWeight() {
assertThat(PaymentOptions.forFeeRateWeight(12))
.isEqualTo(new PaymentOptions(12, Optional.empty(), true, Optional.empty()));
.isEqualTo(new PaymentOptions(12, Optional.empty(), Optional.empty(), true, Optional.empty()));
}
@Test
void forFeeRateLimit() {
assertThat(PaymentOptions.forFeeRateLimit(123))
.isEqualTo(new PaymentOptions(0, Optional.of(123L), true, Optional.empty()));
.isEqualTo(new PaymentOptions(0, Optional.of(123L), Optional.of(123L), true, Optional.empty()));
}
@Test
void forTopUp() {
assertThat(PaymentOptions.forTopUp(123, PUBKEY))
.isEqualTo(new PaymentOptions(5, Optional.of(123L), false, Optional.of(PUBKEY)));
assertThat(PaymentOptions.forTopUp(123, 100, PUBKEY))
.isEqualTo(new PaymentOptions(5, Optional.of(123L), Optional.of(23L), false, Optional.of(PUBKEY)));
}
@Test
void feeRateLimitFirstHops() {
PaymentOptions paymentOptions = PaymentOptions.forTopUp(200, 30, PUBKEY);
assertThat(paymentOptions.feeRateLimitExceptIncomingHops()).contains(170L);
}
@Test