Skip to content

Commit 8d8f15b

Browse files
committed
Use floor when computing max value of a path, not ceil
When calculating the maximum contribution of a path to a larger route, we want to ensure we don't overshoot as that might cause us to violate a maximum value limit. In 209cb2a, we started by calculating with `ceil`, which can trigger exactly that, so here we drop the extra addition, switching us to `floor`. Found both by the `router` fuzzer as well as the `generate_large_mpp_routes` test.
1 parent a530263 commit 8d8f15b

File tree

1 file changed

+94
-2
lines changed

1 file changed

+94
-2
lines changed

lightning/src/routing/router.rs

Lines changed: 94 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1992,11 +1992,10 @@ impl<'a> PaymentPath<'a> {
19921992
let (next_hops_aggregated_base, next_hops_aggregated_prop) =
19931993
crate::blinded_path::payment::compute_aggregated_base_prop_fee(next_hops_feerates_iter).unwrap();
19941994

1995-
// ceil(((hop_max_msat - agg_base) * 1_000_000) / (1_000_000 + agg_prop))
1995+
// floor(((hop_max_msat - agg_base) * 1_000_000) / (1_000_000 + agg_prop))
19961996
let hop_max_final_value_contribution = (hop_max_msat as u128)
19971997
.checked_sub(next_hops_aggregated_base as u128)
19981998
.and_then(|f| f.checked_mul(1_000_000))
1999-
.and_then(|f| f.checked_add(1_000_000 - 1))
20001999
.and_then(|f| f.checked_add(next_hops_aggregated_prop as u128))
20012000
.map(|f| f / ((next_hops_aggregated_prop as u128).saturating_add(1_000_000)));
20022001

@@ -8504,6 +8503,99 @@ mod tests {
85048503
assert_eq!(route.get_total_fees(), 123);
85058504
}
85068505

8506+
#[test]
8507+
fn test_max_final_contribution() {
8508+
// When `compute_max_final_value_contribution` was added, it had a bug where it would
8509+
// over-estimate the maximum value contribution of a hop by using `ceil` rather than
8510+
// `floor`. This tests that case by attempting to send 1 million sats over a channel where
8511+
// the remaining hops have a base fee of zero and a proportional fee of 1 millionth.
8512+
8513+
let (secp_ctx, network_graph, gossip_sync, _, logger) = build_graph();
8514+
let (our_privkey, our_id, privkeys, nodes) = get_nodes(&secp_ctx);
8515+
let scorer = ln_test_utils::TestScorer::new();
8516+
let random_seed_bytes = [42; 32];
8517+
8518+
// Enable channel 1, setting max HTLC to 1M sats
8519+
update_channel(&gossip_sync, &secp_ctx, &our_privkey, UnsignedChannelUpdate {
8520+
chain_hash: ChainHash::using_genesis_block(Network::Testnet),
8521+
short_channel_id: 1,
8522+
timestamp: 2,
8523+
message_flags: 1, // Only must_be_one
8524+
channel_flags: 0,
8525+
cltv_expiry_delta: (1 << 4) | 1,
8526+
htlc_minimum_msat: 0,
8527+
htlc_maximum_msat: 1_000_000,
8528+
fee_base_msat: 0,
8529+
fee_proportional_millionths: 0,
8530+
excess_data: Vec::new()
8531+
});
8532+
8533+
// Set the fee on channel 3 to zero
8534+
update_channel(&gossip_sync, &secp_ctx, &privkeys[0], UnsignedChannelUpdate {
8535+
chain_hash: ChainHash::using_genesis_block(Network::Testnet),
8536+
short_channel_id: 3,
8537+
timestamp: 2,
8538+
message_flags: 1, // Only must_be_one
8539+
channel_flags: 0,
8540+
cltv_expiry_delta: (3 << 4) | 1,
8541+
htlc_minimum_msat: 0,
8542+
htlc_maximum_msat: 1_000_000_000,
8543+
fee_base_msat: 0,
8544+
fee_proportional_millionths: 0,
8545+
excess_data: Vec::new()
8546+
});
8547+
8548+
// Set the fee on channel 6 to 1 millionth
8549+
update_channel(&gossip_sync, &secp_ctx, &privkeys[2], UnsignedChannelUpdate {
8550+
chain_hash: ChainHash::using_genesis_block(Network::Testnet),
8551+
short_channel_id: 6,
8552+
timestamp: 2,
8553+
message_flags: 1, // Only must_be_one
8554+
channel_flags: 0,
8555+
cltv_expiry_delta: (6 << 4) | 1,
8556+
htlc_minimum_msat: 0,
8557+
htlc_maximum_msat: 1_000_000_000,
8558+
fee_base_msat: 0,
8559+
fee_proportional_millionths: 1,
8560+
excess_data: Vec::new()
8561+
});
8562+
8563+
// Now attempt to pay over the channel 1 -> channel 3 -> channel 6 path
8564+
// This should fail as we need to send 1M + 1 sats to cover the fee but channel 1 only
8565+
// allows for 1M sats to flow over it.
8566+
let config = UserConfig::default();
8567+
let payment_params = PaymentParameters::from_node_id(nodes[4], 42)
8568+
.with_bolt11_features(channelmanager::provided_bolt11_invoice_features(&config))
8569+
.unwrap();
8570+
let route_params = RouteParameters::from_payment_params_and_value(payment_params, 1_000_000);
8571+
get_route(&our_id, &route_params, &network_graph.read_only(), None,
8572+
Arc::clone(&logger), &scorer, &Default::default(), &random_seed_bytes).unwrap_err();
8573+
8574+
// Now set channel 1 max HTLC to 1M + 1 sats
8575+
update_channel(&gossip_sync, &secp_ctx, &our_privkey, UnsignedChannelUpdate {
8576+
chain_hash: ChainHash::using_genesis_block(Network::Testnet),
8577+
short_channel_id: 1,
8578+
timestamp: 3,
8579+
message_flags: 1, // Only must_be_one
8580+
channel_flags: 0,
8581+
cltv_expiry_delta: (1 << 4) | 1,
8582+
htlc_minimum_msat: 0,
8583+
htlc_maximum_msat: 1_000_001,
8584+
fee_base_msat: 0,
8585+
fee_proportional_millionths: 0,
8586+
excess_data: Vec::new()
8587+
});
8588+
8589+
// And attempt the same payment again, but this time it should work.
8590+
let route = get_route(&our_id, &route_params, &network_graph.read_only(), None,
8591+
Arc::clone(&logger), &scorer, &Default::default(), &random_seed_bytes).unwrap();
8592+
assert_eq!(route.paths.len(), 1);
8593+
assert_eq!(route.paths[0].hops.len(), 3);
8594+
assert_eq!(route.paths[0].hops[0].short_channel_id, 1);
8595+
assert_eq!(route.paths[0].hops[1].short_channel_id, 3);
8596+
assert_eq!(route.paths[0].hops[2].short_channel_id, 6);
8597+
}
8598+
85078599
#[test]
85088600
fn allow_us_being_first_hint() {
85098601
// Check that we consider a route hint even if we are the src of the first hop.

0 commit comments

Comments
 (0)