Skip to content

Commit 2893ef5

Browse files
committed
Do additional pre-flight checks before claiming a payment
As additional sanity checks, before claiming a payment, we check that we have the full amount available in `claimable_htlcs` that the payment should be for. Concretely, this prevents one somewhat-absurd edge case where a user may receive an MPP payment, wait many *blocks* before claiming it, allowing us to fail the pending HTLCs and the sender to retry some subset of the payment before we go to claim. More generally, this is just good belt-and-suspenders against any edge cases we may have missed.
1 parent f58e7fe commit 2893ef5

File tree

3 files changed

+91
-2
lines changed

3 files changed

+91
-2
lines changed

lightning/src/ln/channelmanager.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3801,14 +3801,47 @@ impl<Signer: Sign, M: Deref, T: Deref, K: Deref, F: Deref, L: Deref> ChannelMana
38013801
// provide the preimage, so worrying too much about the optimal handling isn't worth
38023802
// it.
38033803
let mut claimable_amt_msat = 0;
3804+
let mut expected_amt_msat = None;
38043805
let mut valid_mpp = true;
38053806
for htlc in sources.iter() {
38063807
if let None = channel_state.as_ref().unwrap().short_to_id.get(&htlc.prev_hop.short_channel_id) {
38073808
valid_mpp = false;
38083809
break;
38093810
}
3811+
match &htlc.onion_payload {
3812+
OnionPayload::Spontaneous(_) => {
3813+
// We don't currently support MPP for spontaneous payments, so just check
3814+
// that there's one payment here and move on.
3815+
expected_amt_msat = Some(htlc.value);
3816+
if sources.len() != 1 {
3817+
log_error!(self.logger, "Somehow ended up with an MPP spontaneous payment - this should not be reachable!");
3818+
debug_assert!(false);
3819+
valid_mpp = false;
3820+
break;
3821+
}
3822+
},
3823+
OnionPayload::Invoice(hop_data) => {
3824+
if expected_amt_msat.is_some() && expected_amt_msat != Some(hop_data.total_msat) {
3825+
log_error!(self.logger, "Somehow ended up with an MPP payment with different total amounts - this should not be reachable!");
3826+
debug_assert!(false);
3827+
valid_mpp = false;
3828+
break;
3829+
}
3830+
expected_amt_msat = Some(hop_data.total_msat);
3831+
},
3832+
}
3833+
38103834
claimable_amt_msat += htlc.value;
38113835
}
3836+
if sources.is_empty() || expected_amt_msat.is_none() {
3837+
log_info!(self.logger, "Attempted to claim an incomplete payment which no longer had any available HTLCs!");
3838+
return;
3839+
}
3840+
if claimable_amt_msat != expected_amt_msat.unwrap() {
3841+
log_info!(self.logger, "Attempted to claim an incomplete payment, expected {} msat, had {} available to claim.",
3842+
expected_amt_msat.unwrap(), claimable_amt_msat);
3843+
return;
3844+
}
38123845

38133846
let mut errs = Vec::new();
38143847
let mut claimed_any_htlcs = false;

lightning/src/ln/functional_test_utils.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1713,13 +1713,18 @@ pub fn send_payment<'a, 'b, 'c>(origin: &Node<'a, 'b, 'c>, expected_route: &[&No
17131713
claim_payment(&origin, expected_route, our_payment_preimage);
17141714
}
17151715

1716-
pub fn fail_payment_along_route<'a, 'b, 'c>(origin_node: &Node<'a, 'b, 'c>, expected_paths_slice: &[&[&Node<'a, 'b, 'c>]], skip_last: bool, our_payment_hash: PaymentHash) {
1717-
let mut expected_paths: Vec<_> = expected_paths_slice.iter().collect();
1716+
pub fn fail_payment_along_route<'a, 'b, 'c>(origin_node: &Node<'a, 'b, 'c>, expected_paths: &[&[&Node<'a, 'b, 'c>]], skip_last: bool, our_payment_hash: PaymentHash) {
17181717
for path in expected_paths.iter() {
17191718
assert_eq!(path.last().unwrap().node.get_our_node_id(), expected_paths[0].last().unwrap().node.get_our_node_id());
17201719
}
17211720
assert!(expected_paths[0].last().unwrap().node.fail_htlc_backwards(&our_payment_hash));
17221721
expect_pending_htlcs_forwardable!(expected_paths[0].last().unwrap());
1722+
1723+
pass_failed_payment_back(origin_node, expected_paths, skip_last, our_payment_hash);
1724+
}
1725+
1726+
pub fn pass_failed_payment_back<'a, 'b, 'c>(origin_node: &Node<'a, 'b, 'c>, expected_paths_slice: &[&[&Node<'a, 'b, 'c>]], skip_last: bool, our_payment_hash: PaymentHash) {
1727+
let mut expected_paths: Vec<_> = expected_paths_slice.iter().collect();
17231728
check_added_monitors!(expected_paths[0].last().unwrap(), expected_paths.len());
17241729

17251730
let mut per_path_msgs: Vec<((msgs::UpdateFailHTLC, msgs::CommitmentSigned), PublicKey)> = Vec::with_capacity(expected_paths.len());

lightning/src/ln/functional_tests.rs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9589,6 +9589,57 @@ fn test_keysend_payments_to_private_node() {
95899589
claim_payment(&nodes[0], &path, test_preimage);
95909590
}
95919591

9592+
#[test]
9593+
fn test_double_partial_claim() {
9594+
// Test what happens if a node receives a payment, generates a PaymentReceived event, some of
9595+
// the HTLCs time out, the sender resends only some of the MPP parts, then the user processes
9596+
// the PaymentReceived event, ensuring they don't inadvertently claim only part of the full
9597+
// payment amount.
9598+
let chanmon_cfgs = create_chanmon_cfgs(4);
9599+
let node_cfgs = create_node_cfgs(4, &chanmon_cfgs);
9600+
let node_chanmgrs = create_node_chanmgrs(4, &node_cfgs, &[None, None, None, None]);
9601+
let nodes = create_network(4, &node_cfgs, &node_chanmgrs);
9602+
9603+
create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 100_000, 0, InitFeatures::known(), InitFeatures::known());
9604+
create_announced_chan_between_nodes_with_value(&nodes, 0, 2, 100_000, 0, InitFeatures::known(), InitFeatures::known());
9605+
create_announced_chan_between_nodes_with_value(&nodes, 1, 3, 100_000, 0, InitFeatures::known(), InitFeatures::known());
9606+
create_announced_chan_between_nodes_with_value(&nodes, 2, 3, 100_000, 0, InitFeatures::known(), InitFeatures::known());
9607+
9608+
let (mut route, payment_hash, payment_preimage, payment_secret) = get_route_and_payment_hash!(nodes[0], nodes[3], 15_000_000);
9609+
assert_eq!(route.paths.len(), 2);
9610+
route.paths.sort_by(|path_a, _| {
9611+
// Sort the path so that the path through nodes[1] comes first
9612+
if path_a[0].pubkey == nodes[1].node.get_our_node_id() {
9613+
core::cmp::Ordering::Less } else { core::cmp::Ordering::Greater }
9614+
});
9615+
9616+
send_along_route_with_secret(&nodes[0], route.clone(), &[&[&nodes[1], &nodes[3]], &[&nodes[2], &nodes[3]]], 15_000_000, payment_hash, payment_secret);
9617+
// nodes[3] has now received a PaymentReceived event...which it will take some (exorbitant)
9618+
// amount of time to respond to.
9619+
9620+
// Connect some blocks to time out the payment
9621+
connect_blocks(&nodes[3], TEST_FINAL_CLTV);
9622+
connect_blocks(&nodes[0], TEST_FINAL_CLTV); // To get the same height for sending later
9623+
9624+
expect_pending_htlcs_forwardable!(nodes[3]);
9625+
9626+
pass_failed_payment_back(&nodes[0], &[&[&nodes[1], &nodes[3]], &[&nodes[2], &nodes[3]]], false, payment_hash);
9627+
9628+
// nodes[1] now retries one of the two paths...
9629+
nodes[0].node.send_payment(&route, payment_hash, &Some(payment_secret)).unwrap();
9630+
check_added_monitors!(nodes[0], 2);
9631+
9632+
let mut events = nodes[0].node.get_and_clear_pending_msg_events();
9633+
assert_eq!(events.len(), 2);
9634+
pass_along_path(&nodes[0], &[&nodes[1], &nodes[3]], 15_000_000, payment_hash, Some(payment_secret), events.drain(..).next().unwrap(), false, None);
9635+
9636+
// At this point nodes[3] has received one half of the payment, and the user goes to handle
9637+
// that PaymentReceived event they got hours ago and never handled...we should refuse to claim.
9638+
nodes[3].node.claim_funds(payment_preimage);
9639+
check_added_monitors!(nodes[3], 0);
9640+
assert!(nodes[3].node.get_and_clear_pending_msg_events().is_empty());
9641+
}
9642+
95929643
fn do_test_partial_claim_before_restart(persist_both_monitors: bool) {
95939644
// Test what happens if a node receives an MPP payment, claims it, but crashes before
95949645
// persisting the ChannelManager. If `persist_both_monitors` is false, also crash after only

0 commit comments

Comments
 (0)