Skip to content

Improve test coverage of #2575 router fixes #2625

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Oct 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 11 additions & 13 deletions lightning/src/ln/outbound_payment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -885,12 +885,10 @@ impl OutboundPayments {
RetryableSendFailure::RouteNotFound
})?;

if let Some(route_route_params) = route.route_params.as_mut() {
if route_route_params.final_value_msat != route_params.final_value_msat {
debug_assert!(false,
"Routers are expected to return a route which includes the requested final_value_msat");
route_route_params.final_value_msat = route_params.final_value_msat;
}
if route.route_params.as_ref() != Some(&route_params) {
debug_assert!(false,
"Routers are expected to return a Route which includes the requested RouteParameters");
route.route_params = Some(route_params.clone());
}

let onion_session_privs = self.add_new_pending_payment(payment_hash,
Expand Down Expand Up @@ -947,12 +945,10 @@ impl OutboundPayments {
}
};

if let Some(route_route_params) = route.route_params.as_mut() {
if route_route_params.final_value_msat != route_params.final_value_msat {
debug_assert!(false,
"Routers are expected to return a route which includes the requested final_value_msat");
route_route_params.final_value_msat = route_params.final_value_msat;
}
if route.route_params.as_ref() != Some(&route_params) {
debug_assert!(false,
"Routers are expected to return a Route which includes the requested RouteParameters");
route.route_params = Some(route_params.clone());
}

for path in route.paths.iter() {
Expand Down Expand Up @@ -1935,7 +1931,9 @@ mod tests {
router.expect_find_route(route_params.clone(), Ok(route.clone()));
let mut route_params_w_failed_scid = route_params.clone();
route_params_w_failed_scid.payment_params.previously_failed_channels.push(failed_scid);
router.expect_find_route(route_params_w_failed_scid, Ok(route.clone()));
let mut route_w_failed_scid = route.clone();
route_w_failed_scid.route_params = Some(route_params_w_failed_scid.clone());
router.expect_find_route(route_params_w_failed_scid, Ok(route_w_failed_scid));
router.expect_find_route(route_params.clone(), Ok(route.clone()));
router.expect_find_route(route_params.clone(), Ok(route.clone()));

Expand Down
26 changes: 14 additions & 12 deletions lightning/src/ln/payment_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ fn mpp_retry() {
// Check the remaining max total routing fee for the second attempt is 50_000 - 1_000 msat fee
// used by the first path
route_params.max_total_routing_fee_msat = Some(max_total_routing_fee_msat - 1_000);
route.route_params = Some(route_params.clone());
nodes[0].router.expect_find_route(route_params, Ok(route));
nodes[0].node.process_pending_htlc_forwards();
check_added_monitors!(nodes[0], 1);
Expand Down Expand Up @@ -253,12 +254,12 @@ fn mpp_retry_overpay() {

route.paths.remove(0);
route_params.final_value_msat -= first_path_value;
route.route_params.as_mut().map(|p| p.final_value_msat -= first_path_value);
route_params.payment_params.previously_failed_channels.push(chan_4_update.contents.short_channel_id);

// Check the remaining max total routing fee for the second attempt accounts only for 1_000 msat
// base fee, but not for overpaid value of the first try.
route_params.max_total_routing_fee_msat.as_mut().map(|m| *m -= 1000);

route.route_params = Some(route_params.clone());
nodes[0].router.expect_find_route(route_params, Ok(route));
nodes[0].node.process_pending_htlc_forwards();

Expand Down Expand Up @@ -2738,7 +2739,7 @@ fn retry_multi_path_single_failed_payment() {

let mut retry_params = RouteParameters::from_payment_params_and_value(pay_params, 100_000_000);
retry_params.max_total_routing_fee_msat = None;
route.route_params.as_mut().unwrap().final_value_msat = 100_000_000;
route.route_params = Some(retry_params.clone());
nodes[0].router.expect_find_route(retry_params, Ok(route.clone()));

{
Expand Down Expand Up @@ -2809,9 +2810,7 @@ fn immediate_retry_on_failure() {
maybe_announced_channel: true,
}], blinded_tail: None },
],
route_params: Some(RouteParameters::from_payment_params_and_value(
PaymentParameters::from_node_id(nodes[1].node.get_our_node_id(), TEST_FINAL_CLTV),
100_000_001)),
route_params: Some(route_params.clone()),
};
nodes[0].router.expect_find_route(route_params.clone(), Ok(route.clone()));
// On retry, split the payment across both channels.
Expand All @@ -2821,9 +2820,9 @@ fn immediate_retry_on_failure() {
route.paths[1].hops[0].fee_msat = 50_000_001;
let mut pay_params = route_params.payment_params.clone();
pay_params.previously_failed_channels.push(chans[0].short_channel_id.unwrap());
nodes[0].router.expect_find_route(
RouteParameters::from_payment_params_and_value(pay_params, amt_msat),
Ok(route.clone()));
let retry_params = RouteParameters::from_payment_params_and_value(pay_params, amt_msat);
route.route_params = Some(retry_params.clone());
nodes[0].router.expect_find_route(retry_params, Ok(route.clone()));

nodes[0].node.send_payment(payment_hash, RecipientOnionFields::secret_only(payment_secret),
PaymentId(payment_hash.0), route_params, Retry::Attempts(1)).unwrap();
Expand Down Expand Up @@ -2933,6 +2932,7 @@ fn no_extra_retries_on_back_to_back_fail() {
route.paths[0].hops[1].fee_msat = amt_msat;
let mut retry_params = RouteParameters::from_payment_params_and_value(second_payment_params, amt_msat);
retry_params.max_total_routing_fee_msat = None;
route.route_params = Some(retry_params.clone());
nodes[0].router.expect_find_route(retry_params, Ok(route.clone()));

nodes[0].node.send_payment(payment_hash, RecipientOnionFields::secret_only(payment_secret),
Expand Down Expand Up @@ -3137,7 +3137,7 @@ fn test_simple_partial_retry() {
route.paths.remove(0);
let mut retry_params = RouteParameters::from_payment_params_and_value(second_payment_params, amt_msat / 2);
retry_params.max_total_routing_fee_msat = None;
route.route_params.as_mut().unwrap().final_value_msat = amt_msat / 2;
route.route_params = Some(retry_params.clone());
nodes[0].router.expect_find_route(retry_params, Ok(route.clone()));

nodes[0].node.send_payment(payment_hash, RecipientOnionFields::secret_only(payment_secret),
Expand Down Expand Up @@ -3316,7 +3316,7 @@ fn test_threaded_payment_retries() {

// from here on out, the retry `RouteParameters` amount will be amt/1000
route_params.final_value_msat /= 1000;
route.route_params.as_mut().unwrap().final_value_msat /= 1000;
route.route_params = Some(route_params.clone());
route.paths.pop();

let end_time = Instant::now() + Duration::from_secs(1);
Expand Down Expand Up @@ -3358,6 +3358,7 @@ fn test_threaded_payment_retries() {
new_route_params.payment_params.previously_failed_channels = previously_failed_channels.clone();
new_route_params.max_total_routing_fee_msat.as_mut().map(|m| *m -= 100_000);
route.paths[0].hops[1].short_channel_id += 1;
route.route_params = Some(new_route_params.clone());
nodes[0].router.expect_find_route(new_route_params, Ok(route.clone()));

let bs_fail_updates = get_htlc_update_msgs!(nodes[1], nodes[0].node.get_our_node_id());
Expand Down Expand Up @@ -3720,7 +3721,7 @@ fn test_retry_custom_tlvs() {
send_payment(&nodes[2], &vec!(&nodes[1])[..], 1_500_000);

let amt_msat = 1_000_000;
let (route, payment_hash, payment_preimage, payment_secret) =
let (mut route, payment_hash, payment_preimage, payment_secret) =
get_route_and_payment_hash!(nodes[0], nodes[2], amt_msat);

// Initiate the payment
Expand Down Expand Up @@ -3772,6 +3773,7 @@ fn test_retry_custom_tlvs() {

// Retry the payment and make sure it succeeds
route_params.payment_params.previously_failed_channels.push(chan_2_update.contents.short_channel_id);
route.route_params = Some(route_params.clone());
nodes[0].router.expect_find_route(route_params, Ok(route));
nodes[0].node.process_pending_htlc_forwards();
check_added_monitors!(nodes[0], 1);
Expand Down
197 changes: 197 additions & 0 deletions lightning/src/routing/router.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7695,6 +7695,203 @@ mod tests {
assert_eq!(route.paths.len(), 1);
assert_eq!(route.get_total_amount(), amt_msat);
}

#[test]
fn first_hop_preferred_over_hint() {
// Check that if we have a first hop to a peer we'd always prefer that over a route hint
// they gave us, but we'd still consider all subsequent hints if they are more attractive.
let secp_ctx = Secp256k1::new();
let logger = Arc::new(ln_test_utils::TestLogger::new());
let network_graph = Arc::new(NetworkGraph::new(Network::Testnet, Arc::clone(&logger)));
let gossip_sync = P2PGossipSync::new(Arc::clone(&network_graph), None, Arc::clone(&logger));
let scorer = ln_test_utils::TestScorer::new();
let keys_manager = ln_test_utils::TestKeysInterface::new(&[0u8; 32], Network::Testnet);
let random_seed_bytes = keys_manager.get_secure_random_bytes();
let config = UserConfig::default();

let amt_msat = 1_000_000;
let (our_privkey, our_node_id, privkeys, nodes) = get_nodes(&secp_ctx);

add_channel(&gossip_sync, &secp_ctx, &our_privkey, &privkeys[0],
ChannelFeatures::from_le_bytes(id_to_feature_flags(1)), 1);
update_channel(&gossip_sync, &secp_ctx, &our_privkey, UnsignedChannelUpdate {
chain_hash: genesis_block(Network::Testnet).header.block_hash(),
short_channel_id: 1,
timestamp: 1,
flags: 0,
cltv_expiry_delta: 42,
htlc_minimum_msat: 1_000,
htlc_maximum_msat: 10_000_000,
fee_base_msat: 800,
fee_proportional_millionths: 0,
excess_data: Vec::new()
});
update_channel(&gossip_sync, &secp_ctx, &privkeys[0], UnsignedChannelUpdate {
chain_hash: genesis_block(Network::Testnet).header.block_hash(),
short_channel_id: 1,
timestamp: 1,
flags: 1,
cltv_expiry_delta: 42,
htlc_minimum_msat: 1_000,
htlc_maximum_msat: 10_000_000,
fee_base_msat: 800,
fee_proportional_millionths: 0,
excess_data: Vec::new()
});

add_channel(&gossip_sync, &secp_ctx, &privkeys[0], &privkeys[1],
ChannelFeatures::from_le_bytes(id_to_feature_flags(1)), 2);
update_channel(&gossip_sync, &secp_ctx, &privkeys[0], UnsignedChannelUpdate {
chain_hash: genesis_block(Network::Testnet).header.block_hash(),
short_channel_id: 2,
timestamp: 2,
flags: 0,
cltv_expiry_delta: 42,
htlc_minimum_msat: 1_000,
htlc_maximum_msat: 10_000_000,
fee_base_msat: 800,
fee_proportional_millionths: 0,
excess_data: Vec::new()
});
update_channel(&gossip_sync, &secp_ctx, &privkeys[1], UnsignedChannelUpdate {
chain_hash: genesis_block(Network::Testnet).header.block_hash(),
short_channel_id: 2,
timestamp: 2,
flags: 1,
cltv_expiry_delta: 42,
htlc_minimum_msat: 1_000,
htlc_maximum_msat: 10_000_000,
fee_base_msat: 800,
fee_proportional_millionths: 0,
excess_data: Vec::new()
});

let dest_node_id = nodes[2];

let route_hint = RouteHint(vec![RouteHintHop {
src_node_id: our_node_id,
short_channel_id: 44,
fees: RoutingFees {
base_msat: 234,
proportional_millionths: 0,
},
cltv_expiry_delta: 10,
htlc_minimum_msat: None,
htlc_maximum_msat: Some(5_000_000),
},
RouteHintHop {
src_node_id: nodes[0],
short_channel_id: 45,
fees: RoutingFees {
base_msat: 123,
proportional_millionths: 0,
},
cltv_expiry_delta: 10,
htlc_minimum_msat: None,
htlc_maximum_msat: None,
}]);

let payment_params = PaymentParameters::from_node_id(dest_node_id, 42)
.with_route_hints(vec![route_hint]).unwrap()
.with_bolt11_features(channelmanager::provided_invoice_features(&config)).unwrap();
let route_params = RouteParameters::from_payment_params_and_value(
payment_params, amt_msat);

// First create an insufficient first hop for channel with SCID 1 and check we'd use the
// route hint.
let first_hop = get_channel_details(Some(1), nodes[0],
channelmanager::provided_init_features(&config), 999_999);
let first_hops = vec![first_hop];

let route = get_route(&our_node_id, &route_params.clone(), &network_graph.read_only(),
Some(&first_hops.iter().collect::<Vec<_>>()), Arc::clone(&logger), &scorer,
&Default::default(), &random_seed_bytes).unwrap();
assert_eq!(route.paths.len(), 1);
assert_eq!(route.get_total_amount(), amt_msat);
assert_eq!(route.paths[0].hops.len(), 2);
assert_eq!(route.paths[0].hops[0].short_channel_id, 44);
assert_eq!(route.paths[0].hops[1].short_channel_id, 45);
assert_eq!(route.get_total_fees(), 123);

// Now check we would trust our first hop info, i.e., fail if we detect the route hint is
// for a first hop channel.
let mut first_hop = get_channel_details(Some(1), nodes[0], channelmanager::provided_init_features(&config), 999_999);
first_hop.outbound_scid_alias = Some(44);
let first_hops = vec![first_hop];

let route_res = get_route(&our_node_id, &route_params.clone(), &network_graph.read_only(),
Some(&first_hops.iter().collect::<Vec<_>>()), Arc::clone(&logger), &scorer,
&Default::default(), &random_seed_bytes);
assert!(route_res.is_err());

// Finally check we'd use the first hop if has sufficient outbound capacity. But we'd stil
// use the cheaper second hop of the route hint.
let mut first_hop = get_channel_details(Some(1), nodes[0],
channelmanager::provided_init_features(&config), 10_000_000);
first_hop.outbound_scid_alias = Some(44);
let first_hops = vec![first_hop];

let route = get_route(&our_node_id, &route_params.clone(), &network_graph.read_only(),
Some(&first_hops.iter().collect::<Vec<_>>()), Arc::clone(&logger), &scorer,
&Default::default(), &random_seed_bytes).unwrap();
assert_eq!(route.paths.len(), 1);
assert_eq!(route.get_total_amount(), amt_msat);
assert_eq!(route.paths[0].hops.len(), 2);
assert_eq!(route.paths[0].hops[0].short_channel_id, 1);
assert_eq!(route.paths[0].hops[1].short_channel_id, 45);
assert_eq!(route.get_total_fees(), 123);
}

#[test]
fn allow_us_being_first_hint() {
// Check that we consider a route hint even if we are the src of the first hop.
let secp_ctx = Secp256k1::new();
let logger = Arc::new(ln_test_utils::TestLogger::new());
let network_graph = Arc::new(NetworkGraph::new(Network::Testnet, Arc::clone(&logger)));
let scorer = ln_test_utils::TestScorer::new();
let keys_manager = ln_test_utils::TestKeysInterface::new(&[0u8; 32], Network::Testnet);
let random_seed_bytes = keys_manager.get_secure_random_bytes();
let config = UserConfig::default();

let (_, our_node_id, _, nodes) = get_nodes(&secp_ctx);

let amt_msat = 1_000_000;
let dest_node_id = nodes[1];

let first_hop = get_channel_details(Some(1), nodes[0], channelmanager::provided_init_features(&config), 10_000_000);
let first_hops = vec![first_hop];

let route_hint = RouteHint(vec![RouteHintHop {
src_node_id: our_node_id,
short_channel_id: 44,
fees: RoutingFees {
base_msat: 123,
proportional_millionths: 0,
},
cltv_expiry_delta: 10,
htlc_minimum_msat: None,
htlc_maximum_msat: None,
}]);

let payment_params = PaymentParameters::from_node_id(dest_node_id, 42)
.with_route_hints(vec![route_hint]).unwrap()
.with_bolt11_features(channelmanager::provided_invoice_features(&config)).unwrap();

let route_params = RouteParameters::from_payment_params_and_value(
payment_params, amt_msat);


let route = get_route(&our_node_id, &route_params, &network_graph.read_only(),
Some(&first_hops.iter().collect::<Vec<_>>()), Arc::clone(&logger), &scorer,
&Default::default(), &random_seed_bytes).unwrap();

assert_eq!(route.paths.len(), 1);
assert_eq!(route.get_total_amount(), amt_msat);
assert_eq!(route.get_total_fees(), 0);
assert_eq!(route.paths[0].hops.len(), 1);

assert_eq!(route.paths[0].hops[0].short_channel_id, 44);
}
}

#[cfg(all(any(test, ldk_bench), not(feature = "no-std")))]
Expand Down
2 changes: 1 addition & 1 deletion lightning/src/util/test_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ impl<'a> Router for TestRouter<'a> {
if let Some((find_route_query, find_route_res)) = self.next_routes.lock().unwrap().pop_front() {
assert_eq!(find_route_query, *params);
if let Ok(ref route) = find_route_res {
assert_eq!(route.route_params.as_ref().unwrap().final_value_msat, find_route_query.final_value_msat);
assert_eq!(route.route_params, Some(find_route_query));
let scorer = self.scorer.read().unwrap();
let scorer = ScorerAccountingForInFlightHtlcs::new(scorer, &inflight_htlcs);
for path in &route.paths {
Expand Down