Skip to content

Commit ed12429

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 2fbda5b commit ed12429

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

38323865
let mut errs = Vec::new();
38333866
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
@@ -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

Lines changed: 51 additions & 0 deletions
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)