Skip to content

Commit d804a6c

Browse files
committed
Validate the Router is meeting MPP and max-fee limitations given
When `OutboundPayments` calls the provided `Router` to fetch a `Route` it passes a `RouteParameters` with a specific max-fee. Here we validate that the `Route` returned sticks to the limits provided, and also that it meets the MPP rules of not having any single MPP part which can be removed while still meeting the desired payment amount.
1 parent 3497b59 commit d804a6c

6 files changed

Lines changed: 106 additions & 24 deletions

File tree

lightning/src/ln/chanmon_update_fail_tests.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2309,6 +2309,7 @@ fn test_path_paused_mpp() {
23092309
route.paths[1].hops[0].pubkey = node_c_id;
23102310
route.paths[1].hops[0].short_channel_id = chan_2_ann.contents.short_channel_id;
23112311
route.paths[1].hops[1].short_channel_id = chan_4_id;
2312+
route.route_params.as_mut().unwrap().final_value_msat *= 2;
23122313

23132314
// Set it so that the first monitor update (for the path 0 -> 1 -> 3) succeeds, but the second
23142315
// (for the path 0 -> 2 -> 3) fails.
@@ -4252,7 +4253,7 @@ fn do_test_partial_claim_mon_update_compl_actions(reload_a: bool, reload_b: bool
42524253
let chan_4_scid = chan_4_update.contents.short_channel_id;
42534254

42544255
let (mut route, payment_hash, preimage, payment_secret) =
4255-
get_route_and_payment_hash!(&nodes[0], nodes[3], 100000);
4256+
get_route_and_payment_hash!(&nodes[0], nodes[3], 100_000);
42564257
let path = route.paths[0].clone();
42574258
route.paths.push(path);
42584259
route.paths[0].hops[0].pubkey = node_b_id;
@@ -4261,6 +4262,8 @@ fn do_test_partial_claim_mon_update_compl_actions(reload_a: bool, reload_b: bool
42614262
route.paths[1].hops[0].pubkey = node_c_id;
42624263
route.paths[1].hops[0].short_channel_id = chan_2_scid;
42634264
route.paths[1].hops[1].short_channel_id = chan_4_scid;
4265+
route.route_params.as_mut().unwrap().final_value_msat *= 2;
4266+
42644267
let paths = &[&[&nodes[1], &nodes[3]][..], &[&nodes[2], &nodes[3]][..]];
42654268
send_along_route_with_secret(&nodes[0], route, paths, 200_000, payment_hash, payment_secret);
42664269

lightning/src/ln/channelmanager.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20292,6 +20292,7 @@ mod tests {
2029220292
route.paths[1].hops[0].pubkey = nodes[2].node.get_our_node_id();
2029320293
route.paths[1].hops[0].short_channel_id = chan_2_id;
2029420294
route.paths[1].hops[1].short_channel_id = chan_4_id;
20295+
route.route_params.as_mut().unwrap().final_value_msat *= 2;
2029520296

2029620297
nodes[0].node.send_payment_with_route(route, payment_hash,
2029720298
RecipientOnionFields::spontaneous_empty(), PaymentId(payment_hash.0)).unwrap();

lightning/src/ln/functional_tests.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7205,7 +7205,7 @@ pub fn test_simple_mpp() {
72057205
let chan_4_id = create_announced_chan_between_nodes(&nodes, 2, 3).0.contents.short_channel_id;
72067206

72077207
let (mut route, payment_hash, payment_preimage, payment_secret) =
7208-
get_route_and_payment_hash!(&nodes[0], nodes[3], 100000);
7208+
get_route_and_payment_hash!(&nodes[0], nodes[3], 100_000);
72097209
let path = route.paths[0].clone();
72107210
route.paths.push(path);
72117211
route.paths[0].hops[0].pubkey = node_b_id;
@@ -7214,6 +7214,7 @@ pub fn test_simple_mpp() {
72147214
route.paths[1].hops[0].pubkey = node_c_id;
72157215
route.paths[1].hops[0].short_channel_id = chan_2_id;
72167216
route.paths[1].hops[1].short_channel_id = chan_4_id;
7217+
route.route_params.as_mut().unwrap().final_value_msat = 200_000;
72177218
let paths: &[&[_]] = &[&[&nodes[1], &nodes[3]], &[&nodes[2], &nodes[3]]];
72187219
send_along_route_with_secret(&nodes[0], route, paths, 200_000, payment_hash, payment_secret);
72197220
claim_payment_along_route(ClaimAlongRouteArgs::new(&nodes[0], paths, payment_preimage));

lightning/src/ln/outbound_payment.rs

Lines changed: 31 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -894,6 +894,30 @@ impl OutboundPayments {
894894
}
895895
}
896896

897+
/// Validate that a [`Route`] picked by our [`Router`] is sane for the [`RouteParameters`] used to
898+
/// request it. Failure here indicates a critical bug in the [`Router`].
899+
fn validate_found_route<L: Logger>(
900+
route: &mut Route, route_params: &RouteParameters, logger: &WithContext<L>,
901+
) -> Result<(), ()> {
902+
if route.route_params.as_ref() != Some(route_params) {
903+
debug_assert!(
904+
false,
905+
"Routers are expected to return a Route which includes the requested RouteParameters. Got {:?}, expected {route_params:?}",
906+
route.route_params
907+
);
908+
log_error!(
909+
logger,
910+
"Routers are expected to return a Route which includes the requested RouteParameters. Got {:?}, expected {route_params:?}",
911+
route.route_params
912+
);
913+
route.route_params = Some(route_params.clone());
914+
}
915+
916+
route.debug_assert_route_meets_params(logger)?;
917+
918+
Ok(())
919+
}
920+
897921
impl OutboundPayments {
898922
#[rustfmt::skip]
899923
pub(super) fn send_payment<R: Router, ES: EntropySource, NS: NodeSigner, IH, SP, L: Logger>(
@@ -1462,12 +1486,8 @@ impl OutboundPayments {
14621486
RetryableSendFailure::RouteNotFound
14631487
})?;
14641488

1465-
if route.route_params.as_ref() != Some(route_params) {
1466-
debug_assert!(false,
1467-
"Routers are expected to return a Route which includes the requested RouteParameters. Got {:?}, expected {:?}",
1468-
route.route_params, route_params);
1469-
route.route_params = Some(route_params.clone());
1470-
}
1489+
validate_found_route(&mut route, route_params, logger)
1490+
.map_err(|()| RetryableSendFailure::RouteNotFound)?;
14711491

14721492
Ok(route)
14731493
}
@@ -1552,18 +1572,9 @@ impl OutboundPayments {
15521572
}
15531573
};
15541574

1555-
if route.route_params.as_ref() != Some(&route_params) {
1556-
debug_assert!(false,
1557-
"Routers are expected to return a Route which includes the requested RouteParameters");
1558-
route.route_params = Some(route_params.clone());
1559-
}
1560-
1561-
for path in route.paths.iter() {
1562-
if path.hops.len() == 0 {
1563-
log_error!(logger, "Unusable path in route (path.hops.len() must be at least 1");
1564-
self.abandon_payment(payment_id, PaymentFailureReason::UnexpectedError, pending_events);
1565-
return
1566-
}
1575+
if validate_found_route(&mut route, &route_params, logger).is_err() {
1576+
self.abandon_payment(payment_id, PaymentFailureReason::RouteNotFound, pending_events);
1577+
return
15671578
}
15681579

15691580
macro_rules! abandon_with_entry {
@@ -2967,15 +2978,15 @@ mod tests {
29672978
let sender_pk = PublicKey::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap());
29682979
let receiver_pk = PublicKey::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[43; 32]).unwrap());
29692980
let payment_params = PaymentParameters::from_node_id(sender_pk, 0);
2970-
let route_params = RouteParameters::from_payment_params_and_value(payment_params.clone(), 0);
2981+
let route_params = RouteParameters::from_payment_params_and_value(payment_params.clone(), 1);
29712982
let failed_scid = 42;
29722983
let route = Route {
29732984
paths: vec![Path { hops: vec![RouteHop {
29742985
pubkey: receiver_pk,
29752986
node_features: NodeFeatures::empty(),
29762987
short_channel_id: failed_scid,
29772988
channel_features: ChannelFeatures::empty(),
2978-
fee_msat: 0,
2989+
fee_msat: 1,
29792990
cltv_expiry_delta: 0,
29802991
maybe_announced_channel: true,
29812992
}], blinded_tail: None }],

lightning/src/ln/payment_tests.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,8 @@ fn mpp_failure() {
9797
route.paths[1].hops[0].pubkey = node_c_id;
9898
route.paths[1].hops[0].short_channel_id = chan_2_id;
9999
route.paths[1].hops[1].short_channel_id = chan_4_id;
100+
route.route_params.as_mut().unwrap().final_value_msat *= 2;
101+
100102
let paths: &[&[_]] = &[&[&nodes[1], &nodes[3]], &[&nodes[2], &nodes[3]]];
101103
send_along_route_with_secret(&nodes[0], route, paths, 200_000, payment_hash, payment_secret);
102104
fail_payment_along_route(&nodes[0], paths, false, payment_hash);
@@ -137,6 +139,7 @@ fn mpp_retry() {
137139
route.paths[1].hops[0].pubkey = node_c_id;
138140
route.paths[1].hops[0].short_channel_id = chan_2_update.contents.short_channel_id;
139141
route.paths[1].hops[1].short_channel_id = chan_4_update.contents.short_channel_id;
142+
route.route_params.as_mut().unwrap().final_value_msat *= 2;
140143

141144
// Initiate the MPP payment.
142145
let id = PaymentId(hash.0);
@@ -360,6 +363,7 @@ fn do_mpp_receive_timeout(send_partial_mpp: bool) {
360363
route.paths[1].hops[0].pubkey = node_c_id;
361364
route.paths[1].hops[0].short_channel_id = chan_2_update.contents.short_channel_id;
362365
route.paths[1].hops[1].short_channel_id = chan_4_update.contents.short_channel_id;
366+
route.route_params.as_mut().unwrap().final_value_msat *= 2;
363367

364368
// Initiate the MPP payment.
365369
let onion = RecipientOnionFields::secret_only(payment_secret);

lightning/src/routing/router.rs

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -633,7 +633,7 @@ impl Path {
633633
}
634634
}
635635

636-
/// Gets the final hop's CLTV expiry delta.
636+
/// Gets the final hop's CLTV expiry delta, if there's a final non-blinded hop.
637637
#[rustfmt::skip]
638638
pub fn final_cltv_expiry_delta(&self) -> Option<u32> {
639639
match &self.blinded_tail {
@@ -688,6 +688,66 @@ impl Route {
688688
pub fn get_total_amount(&self) -> u64 {
689689
self.paths.iter().map(|path| path.final_value_msat()).sum()
690690
}
691+
692+
pub(crate) fn debug_assert_route_meets_params<L: Logger>(&self, logger: L) -> Result<(), ()> {
693+
if let Some(route_params) = self.route_params.as_ref() {
694+
// Check that we actually pay less than the max fee we set.
695+
if let Some(max_total_fee) = route_params.max_total_routing_fee_msat {
696+
let total_fee = self.get_total_fees();
697+
if total_fee > max_total_fee {
698+
let err = format!("Router returned an attempt to pay with a higher fee ({total_fee}msat) than we allowed ({max_total_fee}msat). Your router is critically buggy!");
699+
debug_assert!(false, "{}", err);
700+
log_error!(logger, "{}", err);
701+
return Err(());
702+
}
703+
}
704+
705+
// Test that we don't contain any "extra" MPP parts - while we're allowed to overshoot
706+
// the `final_value_msat` specified in the `route_params`, we aren't allowed to have
707+
// any MPP parts which aren't needed to meet `route_params.final_value_msat`.
708+
let min_mpp_part = self.paths.iter().map(|h| h.final_value_msat()).min().unwrap_or(0);
709+
if self.get_total_amount() - min_mpp_part >= route_params.final_value_msat {
710+
let err = format!(
711+
"Router returned an attempt to include more MPP parts than needed. The smallest MPP part ({min_mpp_part}msat) was not needed for a payment of {}msat. Your router is critically buggy!",
712+
route_params.final_value_msat
713+
);
714+
debug_assert!(false, "{}", err);
715+
log_error!(logger, "{}", err);
716+
return Err(());
717+
}
718+
719+
if self.paths.is_empty() {
720+
let err = "Selected route had no paths. Your router is buggy!";
721+
debug_assert!(false, "{}", err);
722+
log_error!(logger, "{}", err);
723+
return Err(());
724+
}
725+
726+
for path in self.paths.iter() {
727+
if path.hops.is_empty() {
728+
let err = "Unusable path in route (path.hops.len() must be at least 1)";
729+
debug_assert!(false, "{}", err);
730+
log_error!(logger, "{}", err);
731+
return Err(());
732+
}
733+
734+
if path.hops.len() > route_params.payment_params.max_path_length.into() {
735+
let err = format!(
736+
"Path had a length of {}, which is greater than the maximum we're allowed ({})",
737+
path.hops.len(),
738+
route_params.payment_params.max_path_length,
739+
);
740+
#[cfg(any(test, feature = "_test_utils"))]
741+
debug_assert!(false, "{}", err);
742+
log_error!(logger, "{}", err);
743+
// This is a bug, but there's not a material safety risk to making this
744+
// payment, so we don't bother to error here.
745+
}
746+
}
747+
}
748+
749+
Ok(())
750+
}
691751
}
692752

693753
impl fmt::Display for Route {
@@ -2491,9 +2551,11 @@ pub fn find_route<L: Logger, GL: Logger, S: ScoreLookUp>(
24912551
scorer: &S, score_params: &S::ScoreParams, random_seed_bytes: &[u8; 32]
24922552
) -> Result<Route, &'static str> {
24932553
let graph_lock = network_graph.read_only();
2494-
let mut route = get_route(our_node_pubkey, &route_params, &graph_lock, first_hops, logger,
2554+
let mut route = get_route(our_node_pubkey, &route_params, &graph_lock, first_hops, &logger,
24952555
scorer, score_params, random_seed_bytes)?;
24962556
add_random_cltv_offset(&mut route, &route_params.payment_params, &graph_lock, random_seed_bytes);
2557+
route.debug_assert_route_meets_params(&logger)
2558+
.map_err(|()| "Generated route doesn't comply with the parameters you specified. This indicates a bug in the router. Please report this bug!")?;
24972559
Ok(route)
24982560
}
24992561

0 commit comments

Comments
 (0)