Skip to content

Commit ddd7ffe

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 552fd51 commit ddd7ffe

File tree

3 files changed

+91
-2
lines changed

3 files changed

+91
-2
lines changed

lightning/src/ln/channelmanager.rs

+33
Original file line numberDiff line numberDiff line change
@@ -3822,14 +3822,47 @@ impl<Signer: Sign, M: Deref, T: Deref, K: Deref, F: Deref, L: Deref> ChannelMana
38223822
// provide the preimage, so worrying too much about the optimal handling isn't worth
38233823
// it.
38243824
let mut claimable_amt_msat = 0;
3825+
let mut expected_amt_msat = None;
38253826
let mut valid_mpp = true;
38263827
for htlc in sources.iter() {
38273828
if let None = channel_state.as_ref().unwrap().short_to_id.get(&htlc.prev_hop.short_channel_id) {
38283829
valid_mpp = false;
38293830
break;
38303831
}
3832+
match &htlc.onion_payload {
3833+
OnionPayload::Spontaneous(_) => {
3834+
// We don't currently support MPP for spontaneous payments, so just check
3835+
// that there's one payment here and move on.
3836+
expected_amt_msat = Some(htlc.value);
3837+
if sources.len() != 1 {
3838+
log_error!(self.logger, "Somehow ended up with an MPP spontaneous payment - this should not be reachable!");
3839+
debug_assert!(false);
3840+
valid_mpp = false;
3841+
break;
3842+
}
3843+
},
3844+
OnionPayload::Invoice(hop_data) => {
3845+
if expected_amt_msat.is_some() && expected_amt_msat != Some(hop_data.total_msat) {
3846+
log_error!(self.logger, "Somehow ended up with an MPP payment with different total amounts - this should not be reachable!");
3847+
debug_assert!(false);
3848+
valid_mpp = false;
3849+
break;
3850+
}
3851+
expected_amt_msat = Some(hop_data.total_msat);
3852+
},
3853+
}
3854+
38313855
claimable_amt_msat += htlc.value;
38323856
}
3857+
if sources.is_empty() || expected_amt_msat.is_none() {
3858+
log_info!(self.logger, "Attempted to claim an incomplete payment which no longer had any available HTLCs!");
3859+
return;
3860+
}
3861+
if claimable_amt_msat != expected_amt_msat.unwrap() {
3862+
log_info!(self.logger, "Attempted to claim an incomplete payment, expected {} msat, had {} available to claim.",
3863+
expected_amt_msat.unwrap(), claimable_amt_msat);
3864+
return;
3865+
}
38333866

38343867
let mut errs = Vec::new();
38353868
let mut claimed_any_htlcs = false;

lightning/src/ln/functional_test_utils.rs

+7-2
Original file line numberDiff line numberDiff line change
@@ -1724,13 +1724,18 @@ pub fn send_payment<'a, 'b, 'c>(origin: &Node<'a, 'b, 'c>, expected_route: &[&No
17241724
claim_payment(&origin, expected_route, our_payment_preimage);
17251725
}
17261726

1727-
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) {
1728-
let mut expected_paths: Vec<_> = expected_paths_slice.iter().collect();
1727+
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) {
17291728
for path in expected_paths.iter() {
17301729
assert_eq!(path.last().unwrap().node.get_our_node_id(), expected_paths[0].last().unwrap().node.get_our_node_id());
17311730
}
17321731
assert!(expected_paths[0].last().unwrap().node.fail_htlc_backwards(&our_payment_hash));
17331732
expect_pending_htlcs_forwardable!(expected_paths[0].last().unwrap());
1733+
1734+
pass_failed_payment_back(origin_node, expected_paths, skip_last, our_payment_hash);
1735+
}
1736+
1737+
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) {
1738+
let mut expected_paths: Vec<_> = expected_paths_slice.iter().collect();
17341739
check_added_monitors!(expected_paths[0].last().unwrap(), expected_paths.len());
17351740

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

lightning/src/ln/functional_tests.rs

+51
Original file line numberDiff line numberDiff line change
@@ -9603,6 +9603,57 @@ fn test_keysend_payments_to_private_node() {
96039603
claim_payment(&nodes[0], &path, test_preimage);
96049604
}
96059605

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

0 commit comments

Comments
 (0)