From b9f4ebdd28ccfedc1dd27dab69c9887eee38320d Mon Sep 17 00:00:00 2001 From: Alec Chen Date: Thu, 23 Mar 2023 14:34:57 -0500 Subject: [PATCH 1/4] Add `total_value_received` to `ClaimableHTLC` for claim validation This is pre-work for allowing nodes to overshoot onion values and changing validation for MPP completion. This adds a field to `ClaimableHTLC` that is separate from the onion values, which represents the actual received amount reported in `PaymentClaimable` which is what we want to validate against when a user goes to claim. --- lightning/src/ln/channelmanager.rs | 35 ++++++++++++++++++++++++------ 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 6bd9148e987..199beeae525 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -194,7 +194,10 @@ struct ClaimableHTLC { value: u64, onion_payload: OnionPayload, timer_ticks: u8, - /// The sum total of all MPP parts + /// The total value received for a payment (sum of all MPP parts if the payment is a MPP). + /// Gets set to the amount reported when pushing [`Event::PaymentClaimable`]. + total_value_received: Option, + /// The sender intended sum total of all MPP parts specified in the onion total_msat: u64, } @@ -3272,7 +3275,7 @@ where panic!("short_channel_id == 0 should imply any pending_forward entries are of type Receive"); } }; - let claimable_htlc = ClaimableHTLC { + let mut claimable_htlc = ClaimableHTLC { prev_hop: HTLCPreviousHopData { short_channel_id: prev_short_channel_id, outpoint: prev_funding_outpoint, @@ -3282,6 +3285,7 @@ where }, value: outgoing_amt_msat, timer_ticks: 0, + total_value_received: None, total_msat: if let Some(data) = &payment_data { data.total_msat } else { outgoing_amt_msat }, cltv_expiry, onion_payload, @@ -3326,7 +3330,7 @@ where fail_htlc!(claimable_htlc, payment_hash); continue } - let (_, htlcs) = claimable_payments.claimable_htlcs.entry(payment_hash) + let (_, ref mut htlcs) = claimable_payments.claimable_htlcs.entry(payment_hash) .or_insert_with(|| (purpose(), Vec::new())); if htlcs.len() == 1 { if let OnionPayload::Spontaneous(_) = htlcs[0].onion_payload { @@ -3357,11 +3361,13 @@ where } else if total_value == $payment_data.total_msat { let prev_channel_id = prev_funding_outpoint.to_channel_id(); htlcs.push(claimable_htlc); + let amount_msat = htlcs.iter().map(|htlc| htlc.value).sum(); + htlcs.iter_mut().for_each(|htlc| htlc.total_value_received = Some(amount_msat)); new_events.push(events::Event::PaymentClaimable { receiver_node_id: Some(receiver_node_id), payment_hash, purpose: purpose(), - amount_msat: total_value, + amount_msat, via_channel_id: Some(prev_channel_id), via_user_channel_id: Some(prev_user_channel_id), }); @@ -3415,6 +3421,8 @@ where } match claimable_payments.claimable_htlcs.entry(payment_hash) { hash_map::Entry::Vacant(e) => { + let amount_msat = claimable_htlc.value; + claimable_htlc.total_value_received = Some(amount_msat); let purpose = events::PaymentPurpose::SpontaneousPayment(preimage); e.insert((purpose.clone(), vec![claimable_htlc])); let prev_channel_id = prev_funding_outpoint.to_channel_id(); @@ -3960,6 +3968,7 @@ where // provide the preimage, so worrying too much about the optimal handling isn't worth // it. let mut claimable_amt_msat = 0; + let mut prev_total_msat = None; let mut expected_amt_msat = None; let mut valid_mpp = true; let mut errs = Vec::new(); @@ -3987,14 +3996,22 @@ where break; } - if expected_amt_msat.is_some() && expected_amt_msat != Some(htlc.total_msat) { - log_error!(self.logger, "Somehow ended up with an MPP payment with different total amounts - this should not be reachable!"); + if prev_total_msat.is_some() && prev_total_msat != Some(htlc.total_msat) { + log_error!(self.logger, "Somehow ended up with an MPP payment with different expected total amounts - this should not be reachable!"); debug_assert!(false); valid_mpp = false; break; } + prev_total_msat = Some(htlc.total_msat); + + if expected_amt_msat.is_some() && expected_amt_msat != htlc.total_value_received { + log_error!(self.logger, "Somehow ended up with an MPP payment with different received total amounts - this should not be reachable!"); + debug_assert!(false); + valid_mpp = false; + break; + } + expected_amt_msat = htlc.total_value_received; - expected_amt_msat = Some(htlc.total_msat); if let OnionPayload::Spontaneous(_) = &htlc.onion_payload { // We don't currently support MPP for spontaneous payments, so just check // that there's one payment here and move on. @@ -6795,6 +6812,7 @@ impl Writeable for ClaimableHTLC { (1, self.total_msat, required), (2, self.value, required), (4, payment_data, option), + (5, self.total_value_received, option), (6, self.cltv_expiry, required), (8, keysend_preimage, option), }); @@ -6808,6 +6826,7 @@ impl Readable for ClaimableHTLC { let mut value = 0; let mut payment_data: Option = None; let mut cltv_expiry = 0; + let mut total_value_received = None; let mut total_msat = None; let mut keysend_preimage: Option = None; read_tlv_fields!(reader, { @@ -6815,6 +6834,7 @@ impl Readable for ClaimableHTLC { (1, total_msat, option), (2, value, required), (4, payment_data, option), + (5, total_value_received, option), (6, cltv_expiry, required), (8, keysend_preimage, option) }); @@ -6842,6 +6862,7 @@ impl Readable for ClaimableHTLC { prev_hop: prev_hop.0.unwrap(), timer_ticks: 0, value, + total_value_received, total_msat: total_msat.unwrap(), onion_payload, cltv_expiry, From f3d8e583744653d701495e21124216a0dc4a6ef6 Mon Sep 17 00:00:00 2001 From: Alec Chen Date: Tue, 28 Feb 2023 18:42:39 -0600 Subject: [PATCH 2/4] Allow overshooting `total_msat` for an MPP While retrying a failed path of an MPP, a node may want to overshoot the `total_msat` in order to use a path with an `htlc_minimum_msat` greater than the remaining value being sent. This commit no longer fails MPPs that overshoot the `total_msat`, however it does fail HTLCs with the same payment hash that are received *after* a payment has become claimable. --- lightning/src/events/mod.rs | 4 +- lightning/src/ln/channelmanager.rs | 14 +++--- lightning/src/ln/functional_tests.rs | 70 ++++++++++++++++++++++++++++ lightning/src/ln/outbound_payment.rs | 1 - 4 files changed, 80 insertions(+), 9 deletions(-) diff --git a/lightning/src/events/mod.rs b/lightning/src/events/mod.rs index 4f3b55804c7..cae92535078 100644 --- a/lightning/src/events/mod.rs +++ b/lightning/src/events/mod.rs @@ -230,7 +230,7 @@ pub enum HTLCDestination { /// /// Some of the reasons may include: /// * HTLC Timeouts - /// * Expected MPP amount to claim does not equal HTLC total + /// * Expected MPP amount has already been reached /// * Claimable amount does not match expected amount FailedPayment { /// The payment hash of the payment we attempted to process. @@ -712,7 +712,7 @@ pub enum Event { /// * Insufficient capacity in the outbound channel /// * While waiting to forward the HTLC, the channel it is meant to be forwarded through closes /// * When an unknown SCID is requested for forwarding a payment. - /// * Claiming an amount for an MPP payment that exceeds the HTLC total + /// * Expected MPP amount has already been reached /// * The HTLC has timed out /// /// This event, however, does not get generated if an HTLC fails to meet the forwarding diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 199beeae525..617ac3968c4 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -2663,7 +2663,7 @@ where } #[cfg(test)] - fn test_send_payment_internal(&self, route: &Route, payment_hash: PaymentHash, payment_secret: &Option, keysend_preimage: Option, payment_id: PaymentId, recv_value_msat: Option, onion_session_privs: Vec<[u8; 32]>) -> Result<(), PaymentSendFailure> { + pub(super) fn test_send_payment_internal(&self, route: &Route, payment_hash: PaymentHash, payment_secret: &Option, keysend_preimage: Option, payment_id: PaymentId, recv_value_msat: Option, onion_session_privs: Vec<[u8; 32]>) -> Result<(), PaymentSendFailure> { let best_block_height = self.best_block.read().unwrap().height(); let _persistence_guard = PersistenceNotifierGuard::notify_on_drop(&self.total_consistency_lock, &self.persistence_notifier); self.pending_outbound_payments.test_send_payment_internal(route, payment_hash, payment_secret, keysend_preimage, payment_id, recv_value_msat, onion_session_privs, &self.node_signer, best_block_height, @@ -3354,11 +3354,13 @@ where _ => unreachable!(), } } - if total_value >= msgs::MAX_VALUE_MSAT || total_value > $payment_data.total_msat { - log_trace!(self.logger, "Failing HTLCs with payment_hash {} as the total value {} ran over expected value {} (or HTLCs were inconsistent)", - log_bytes!(payment_hash.0), total_value, $payment_data.total_msat); + if total_value >= msgs::MAX_VALUE_MSAT { fail_htlc!(claimable_htlc, payment_hash); - } else if total_value == $payment_data.total_msat { + } else if total_value - claimable_htlc.value >= $payment_data.total_msat { + log_trace!(self.logger, "Failing HTLC with payment_hash {} as payment is already claimable", + log_bytes!(payment_hash.0)); + fail_htlc!(claimable_htlc, payment_hash); + } else if total_value >= $payment_data.total_msat { let prev_channel_id = prev_funding_outpoint.to_channel_id(); htlcs.push(claimable_htlc); let amount_msat = htlcs.iter().map(|htlc| htlc.value).sum(); @@ -3689,7 +3691,7 @@ where if let OnionPayload::Invoice { .. } = htlcs[0].onion_payload { // Check if we've received all the parts we need for an MPP (the value of the parts adds to total_msat). // In this case we're not going to handle any timeouts of the parts here. - if htlcs[0].total_msat == htlcs.iter().fold(0, |total, htlc| total + htlc.value) { + if htlcs[0].total_msat <= htlcs.iter().fold(0, |total, htlc| total + htlc.value) { return true; } else if htlcs.into_iter().any(|htlc| { htlc.timer_ticks += 1; diff --git a/lightning/src/ln/functional_tests.rs b/lightning/src/ln/functional_tests.rs index 265bd49ac92..25b0f792cce 100644 --- a/lightning/src/ln/functional_tests.rs +++ b/lightning/src/ln/functional_tests.rs @@ -7926,6 +7926,76 @@ fn test_can_not_accept_unknown_inbound_channel() { } } +fn do_test_overshoot_mpp(msat_amounts: &[u64], total_msat: u64) { + + let routing_node_count = msat_amounts.len(); + let node_count = routing_node_count + 2; + + let chanmon_cfgs = create_chanmon_cfgs(node_count); + let node_cfgs = create_node_cfgs(node_count, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(node_count, &node_cfgs, &vec![None; node_count]); + let nodes = create_network(node_count, &node_cfgs, &node_chanmgrs); + + let src_idx = 0; + let dst_idx = 1; + + // Create channels for each amount + let mut expected_paths = Vec::with_capacity(routing_node_count); + let mut src_chan_ids = Vec::with_capacity(routing_node_count); + let mut dst_chan_ids = Vec::with_capacity(routing_node_count); + for i in 0..routing_node_count { + let routing_node = 2 + i; + let src_chan_id = create_announced_chan_between_nodes(&nodes, src_idx, routing_node).0.contents.short_channel_id; + src_chan_ids.push(src_chan_id); + let dst_chan_id = create_announced_chan_between_nodes(&nodes, routing_node, dst_idx).0.contents.short_channel_id; + dst_chan_ids.push(dst_chan_id); + let path = vec![&nodes[routing_node], &nodes[dst_idx]]; + expected_paths.push(path); + } + let expected_paths: Vec<&[&Node]> = expected_paths.iter().map(|route| route.as_slice()).collect(); + + // Create a route for each amount + let example_amount = 100000; + let (mut route, our_payment_hash, our_payment_preimage, our_payment_secret) = get_route_and_payment_hash!(&nodes[src_idx], nodes[dst_idx], example_amount); + let sample_path = route.paths.pop().unwrap(); + for i in 0..routing_node_count { + let routing_node = 2 + i; + let mut path = sample_path.clone(); + path[0].pubkey = nodes[routing_node].node.get_our_node_id(); + path[0].short_channel_id = src_chan_ids[i]; + path[1].pubkey = nodes[dst_idx].node.get_our_node_id(); + path[1].short_channel_id = dst_chan_ids[i]; + path[1].fee_msat = msat_amounts[i]; + route.paths.push(path); + } + + // Send payment with manually set total_msat + let payment_id = PaymentId(nodes[src_idx].keys_manager.backing.get_secure_random_bytes()); + let onion_session_privs = nodes[src_idx].node.test_add_new_pending_payment(our_payment_hash, Some(our_payment_secret), payment_id, &route).unwrap(); + nodes[src_idx].node.test_send_payment_internal(&route, our_payment_hash, &Some(our_payment_secret), None, payment_id, Some(total_msat), onion_session_privs).unwrap(); + check_added_monitors!(nodes[src_idx], expected_paths.len()); + + let mut events = nodes[src_idx].node.get_and_clear_pending_msg_events(); + assert_eq!(events.len(), expected_paths.len()); + let mut amount_received = 0; + for (path_idx, expected_path) in expected_paths.iter().enumerate() { + let ev = remove_first_msg_event_to_node(&expected_path[0].node.get_our_node_id(), &mut events); + + let current_path_amount = msat_amounts[path_idx]; + amount_received += current_path_amount; + let became_claimable_now = amount_received >= total_msat && amount_received - current_path_amount < total_msat; + pass_along_path(&nodes[src_idx], expected_path, amount_received, our_payment_hash.clone(), Some(our_payment_secret), ev, became_claimable_now, None); + } + + claim_payment_along_route(&nodes[src_idx], &expected_paths, false, our_payment_preimage); +} + +#[test] +fn test_overshoot_mpp() { + do_test_overshoot_mpp(&[100_000, 101_000], 200_000); + do_test_overshoot_mpp(&[100_000, 10_000, 100_000], 200_000); +} + #[test] fn test_simple_mpp() { // Simple test of sending a multi-path payment. diff --git a/lightning/src/ln/outbound_payment.rs b/lightning/src/ln/outbound_payment.rs index 5326d33bcd2..dd6227349f3 100644 --- a/lightning/src/ln/outbound_payment.rs +++ b/lightning/src/ln/outbound_payment.rs @@ -916,7 +916,6 @@ impl OutboundPayments { return Err(PaymentSendFailure::PathParameterError(path_errs)); } if let Some(amt_msat) = recv_value_msat { - debug_assert!(amt_msat >= total_value); total_value = amt_msat; } From ee57738ca8564a31d9f919bd4b642d1449bd61a1 Mon Sep 17 00:00:00 2001 From: Alec Chen Date: Mon, 6 Mar 2023 19:33:45 -0600 Subject: [PATCH 3/4] Allow overshooting final cltv_expiry Final nodes previously had stricter requirements on HTLC contents matching onion value compared to intermediate nodes. This allowed for probing, i.e. the last intermediate node could overshoot the value by a small amount and conclude from the acceptance or rejection of the HTLC whether the next node was the destination. This also applies to the msat amount, however this change was already present. --- lightning/src/ln/channelmanager.rs | 4 +-- lightning/src/ln/onion_route_tests.rs | 48 +++++++++++++++++++++++++-- 2 files changed, 47 insertions(+), 5 deletions(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 617ac3968c4..fb1f6b4a848 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -2095,9 +2095,9 @@ where payment_hash: PaymentHash, amt_msat: u64, cltv_expiry: u32, phantom_shared_secret: Option<[u8; 32]>) -> Result { // final_incorrect_cltv_expiry - if hop_data.outgoing_cltv_value != cltv_expiry { + if hop_data.outgoing_cltv_value > cltv_expiry { return Err(ReceiveError { - msg: "Upstream node set CLTV to the wrong value", + msg: "Upstream node set CLTV to less than the CLTV set by the sender", err_code: 18, err_data: cltv_expiry.to_be_bytes().to_vec() }) diff --git a/lightning/src/ln/onion_route_tests.rs b/lightning/src/ln/onion_route_tests.rs index a6fff59fd4f..d71a5b11c08 100644 --- a/lightning/src/ln/onion_route_tests.rs +++ b/lightning/src/ln/onion_route_tests.rs @@ -552,7 +552,7 @@ fn test_onion_failure() { for f in pending_forwards.iter_mut() { match f { &mut HTLCForwardInfo::AddHTLC(PendingAddHTLCInfo { ref mut forward_info, .. }) => - forward_info.outgoing_cltv_value += 1, + forward_info.outgoing_cltv_value -= 1, _ => {}, } } @@ -602,6 +602,48 @@ fn test_onion_failure() { }, true, Some(23), None, None); } +#[test] +fn test_overshoot_final_cltv() { + let chanmon_cfgs = create_chanmon_cfgs(3); + let node_cfgs = create_node_cfgs(3, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(3, &node_cfgs, &[None; 3]); + let mut nodes = create_network(3, &node_cfgs, &node_chanmgrs); + create_announced_chan_between_nodes(&nodes, 0, 1); + create_announced_chan_between_nodes(&nodes, 1, 2); + let (route, payment_hash, payment_preimage, payment_secret) = get_route_and_payment_hash!(nodes[0], nodes[2], 40000); + + let payment_id = PaymentId(nodes[0].keys_manager.backing.get_secure_random_bytes()); + nodes[0].node.send_payment(&route, payment_hash, &Some(payment_secret), payment_id).unwrap(); + + check_added_monitors!(nodes[0], 1); + let update_0 = get_htlc_update_msgs!(nodes[0], nodes[1].node.get_our_node_id()); + let mut update_add_0 = update_0.update_add_htlcs[0].clone(); + nodes[1].node.handle_update_add_htlc(&nodes[0].node.get_our_node_id(), &update_add_0); + commitment_signed_dance!(nodes[1], nodes[0], &update_0.commitment_signed, false, true); + + assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty()); + for (_, pending_forwards) in nodes[1].node.forward_htlcs.lock().unwrap().iter_mut() { + for f in pending_forwards.iter_mut() { + match f { + &mut HTLCForwardInfo::AddHTLC(PendingAddHTLCInfo { ref mut forward_info, .. }) => + forward_info.outgoing_cltv_value += 1, + _ => {}, + } + } + } + expect_pending_htlcs_forwardable!(nodes[1]); + + check_added_monitors!(&nodes[1], 1); + let update_1 = get_htlc_update_msgs!(nodes[1], nodes[2].node.get_our_node_id()); + let mut update_add_1 = update_1.update_add_htlcs[0].clone(); + nodes[2].node.handle_update_add_htlc(&nodes[1].node.get_our_node_id(), &update_add_1); + commitment_signed_dance!(nodes[2], nodes[1], update_1.commitment_signed, false, true); + + expect_pending_htlcs_forwardable!(nodes[2]); + expect_payment_claimable!(nodes[2], payment_hash, payment_secret, 40_000); + claim_payment(&nodes[0], &[&nodes[1], &nodes[2]], payment_preimage); +} + fn do_test_onion_failure_stale_channel_update(announced_channel: bool) { // Create a network of three nodes and two channels connecting them. We'll be updating the // HTLC relay policy of the second channel, causing forwarding failures at the first hop. @@ -1096,7 +1138,7 @@ fn test_phantom_final_incorrect_cltv_expiry() { &mut HTLCForwardInfo::AddHTLC(PendingAddHTLCInfo { forward_info: PendingHTLCInfo { ref mut outgoing_cltv_value, .. }, .. }) => { - *outgoing_cltv_value += 1; + *outgoing_cltv_value -= 1; }, _ => panic!("Unexpected forward"), } @@ -1114,7 +1156,7 @@ fn test_phantom_final_incorrect_cltv_expiry() { commitment_signed_dance!(nodes[0], nodes[1], update_1.commitment_signed, false); // Ensure the payment fails with the expected error. - let expected_cltv: u32 = 82; + let expected_cltv: u32 = 80; let error_data = expected_cltv.to_be_bytes().to_vec(); let mut fail_conditions = PaymentFailedConditions::new() .blamed_scid(phantom_scid) From 1d31b0e84e2adeb774f4e626c240337c65cb411c Mon Sep 17 00:00:00 2001 From: Alec Chen Date: Mon, 6 Mar 2023 19:51:44 -0600 Subject: [PATCH 4/4] Use onion amount `amt_to_forward` for MPP set calculation If routing nodes take less fees and pay the final node more than `amt_to_forward`, the receiver may see that `total_msat` has been met before all of the sender's intended HTLCs have arrived. The receiver may then prematurely claim the payment and release the payment hash, allowing routing nodes to claim the remaining HTLCs. Using the onion value `amt_to_forward` to determine when `total_msat` has been met allows the sender to control the set total. --- lightning/src/ln/channelmanager.rs | 34 +++++++--- lightning/src/ln/functional_tests.rs | 95 ++++++++++++++++++++++++++++ 2 files changed, 121 insertions(+), 8 deletions(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index fb1f6b4a848..c8adc721235 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -120,7 +120,10 @@ pub(super) struct PendingHTLCInfo { pub(super) routing: PendingHTLCRouting, pub(super) incoming_shared_secret: [u8; 32], payment_hash: PaymentHash, + /// Amount received pub(super) incoming_amt_msat: Option, // Added in 0.0.113 + /// Sender intended amount to forward or receive (actual amount received + /// may overshoot this in either case) pub(super) outgoing_amt_msat: u64, pub(super) outgoing_cltv_value: u32, } @@ -192,6 +195,9 @@ struct ClaimableHTLC { cltv_expiry: u32, /// The amount (in msats) of this MPP part value: u64, + /// The amount (in msats) that the sender intended to be sent in this MPP + /// part (used for validating total MPP amount) + sender_intended_value: u64, onion_payload: OnionPayload, timer_ticks: u8, /// The total value received for a payment (sum of all MPP parts if the payment is a MPP). @@ -2181,7 +2187,7 @@ where payment_hash, incoming_shared_secret: shared_secret, incoming_amt_msat: Some(amt_msat), - outgoing_amt_msat: amt_msat, + outgoing_amt_msat: hop_data.amt_to_forward, outgoing_cltv_value: hop_data.outgoing_cltv_value, }) } @@ -3261,7 +3267,7 @@ where HTLCForwardInfo::AddHTLC(PendingAddHTLCInfo { prev_short_channel_id, prev_htlc_id, prev_funding_outpoint, prev_user_channel_id, forward_info: PendingHTLCInfo { - routing, incoming_shared_secret, payment_hash, outgoing_amt_msat, .. + routing, incoming_shared_secret, payment_hash, incoming_amt_msat, outgoing_amt_msat, .. } }) => { let (cltv_expiry, onion_payload, payment_data, phantom_shared_secret) = match routing { @@ -3283,7 +3289,11 @@ where incoming_packet_shared_secret: incoming_shared_secret, phantom_shared_secret, }, - value: outgoing_amt_msat, + // We differentiate the received value from the sender intended value + // if possible so that we don't prematurely mark MPP payments complete + // if routing nodes overpay + value: incoming_amt_msat.unwrap_or(outgoing_amt_msat), + sender_intended_value: outgoing_amt_msat, timer_ticks: 0, total_value_received: None, total_msat: if let Some(data) = &payment_data { data.total_msat } else { outgoing_amt_msat }, @@ -3339,9 +3349,9 @@ where continue } } - let mut total_value = claimable_htlc.value; + let mut total_value = claimable_htlc.sender_intended_value; for htlc in htlcs.iter() { - total_value += htlc.value; + total_value += htlc.sender_intended_value; match &htlc.onion_payload { OnionPayload::Invoice { .. } => { if htlc.total_msat != $payment_data.total_msat { @@ -3354,9 +3364,11 @@ where _ => unreachable!(), } } + // The condition determining whether an MPP is complete must + // match exactly the condition used in `timer_tick_occurred` if total_value >= msgs::MAX_VALUE_MSAT { fail_htlc!(claimable_htlc, payment_hash); - } else if total_value - claimable_htlc.value >= $payment_data.total_msat { + } else if total_value - claimable_htlc.sender_intended_value >= $payment_data.total_msat { log_trace!(self.logger, "Failing HTLC with payment_hash {} as payment is already claimable", log_bytes!(payment_hash.0)); fail_htlc!(claimable_htlc, payment_hash); @@ -3431,7 +3443,7 @@ where new_events.push(events::Event::PaymentClaimable { receiver_node_id: Some(receiver_node_id), payment_hash, - amount_msat: outgoing_amt_msat, + amount_msat, purpose, via_channel_id: Some(prev_channel_id), via_user_channel_id: Some(prev_user_channel_id), @@ -3691,7 +3703,9 @@ where if let OnionPayload::Invoice { .. } = htlcs[0].onion_payload { // Check if we've received all the parts we need for an MPP (the value of the parts adds to total_msat). // In this case we're not going to handle any timeouts of the parts here. - if htlcs[0].total_msat <= htlcs.iter().fold(0, |total, htlc| total + htlc.value) { + // This condition determining whether the MPP is complete here must match + // exactly the condition used in `process_pending_htlc_forwards`. + if htlcs[0].total_msat <= htlcs.iter().fold(0, |total, htlc| total + htlc.sender_intended_value) { return true; } else if htlcs.into_iter().any(|htlc| { htlc.timer_ticks += 1; @@ -6813,6 +6827,7 @@ impl Writeable for ClaimableHTLC { (0, self.prev_hop, required), (1, self.total_msat, required), (2, self.value, required), + (3, self.sender_intended_value, required), (4, payment_data, option), (5, self.total_value_received, option), (6, self.cltv_expiry, required), @@ -6826,6 +6841,7 @@ impl Readable for ClaimableHTLC { fn read(reader: &mut R) -> Result { let mut prev_hop = crate::util::ser::RequiredWrapper(None); let mut value = 0; + let mut sender_intended_value = None; let mut payment_data: Option = None; let mut cltv_expiry = 0; let mut total_value_received = None; @@ -6835,6 +6851,7 @@ impl Readable for ClaimableHTLC { (0, prev_hop, required), (1, total_msat, option), (2, value, required), + (3, sender_intended_value, option), (4, payment_data, option), (5, total_value_received, option), (6, cltv_expiry, required), @@ -6864,6 +6881,7 @@ impl Readable for ClaimableHTLC { prev_hop: prev_hop.0.unwrap(), timer_ticks: 0, value, + sender_intended_value: sender_intended_value.unwrap_or(value), total_value_received, total_msat: total_msat.unwrap(), onion_payload, diff --git a/lightning/src/ln/functional_tests.rs b/lightning/src/ln/functional_tests.rs index 25b0f792cce..b3f3c764547 100644 --- a/lightning/src/ln/functional_tests.rs +++ b/lightning/src/ln/functional_tests.rs @@ -7926,6 +7926,101 @@ fn test_can_not_accept_unknown_inbound_channel() { } } +#[test] +fn test_onion_value_mpp_set_calculation() { + // Test that we use the onion value `amt_to_forward` when + // calculating whether we've reached the `total_msat` of an MPP + // by having a routing node forward more than `amt_to_forward` + // and checking that the receiving node doesn't generate + // a PaymentClaimable event too early + let node_count = 4; + let chanmon_cfgs = create_chanmon_cfgs(node_count); + let node_cfgs = create_node_cfgs(node_count, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(node_count, &node_cfgs, &vec![None; node_count]); + let mut nodes = create_network(node_count, &node_cfgs, &node_chanmgrs); + + let chan_1_id = create_announced_chan_between_nodes(&nodes, 0, 1).0.contents.short_channel_id; + let chan_2_id = create_announced_chan_between_nodes(&nodes, 0, 2).0.contents.short_channel_id; + let chan_3_id = create_announced_chan_between_nodes(&nodes, 1, 3).0.contents.short_channel_id; + let chan_4_id = create_announced_chan_between_nodes(&nodes, 2, 3).0.contents.short_channel_id; + + let total_msat = 100_000; + let expected_paths: &[&[&Node]] = &[&[&nodes[1], &nodes[3]], &[&nodes[2], &nodes[3]]]; + let (mut route, our_payment_hash, our_payment_preimage, our_payment_secret) = get_route_and_payment_hash!(&nodes[0], nodes[3], total_msat); + let sample_path = route.paths.pop().unwrap(); + + let mut path_1 = sample_path.clone(); + path_1[0].pubkey = nodes[1].node.get_our_node_id(); + path_1[0].short_channel_id = chan_1_id; + path_1[1].pubkey = nodes[3].node.get_our_node_id(); + path_1[1].short_channel_id = chan_3_id; + path_1[1].fee_msat = 100_000; + route.paths.push(path_1); + + let mut path_2 = sample_path.clone(); + path_2[0].pubkey = nodes[2].node.get_our_node_id(); + path_2[0].short_channel_id = chan_2_id; + path_2[1].pubkey = nodes[3].node.get_our_node_id(); + path_2[1].short_channel_id = chan_4_id; + path_2[1].fee_msat = 1_000; + route.paths.push(path_2); + + // Send payment + let payment_id = PaymentId(nodes[0].keys_manager.backing.get_secure_random_bytes()); + let onion_session_privs = nodes[0].node.test_add_new_pending_payment(our_payment_hash, Some(our_payment_secret), payment_id, &route).unwrap(); + nodes[0].node.test_send_payment_internal(&route, our_payment_hash, &Some(our_payment_secret), None, payment_id, Some(total_msat), onion_session_privs).unwrap(); + check_added_monitors!(nodes[0], expected_paths.len()); + + let mut events = nodes[0].node.get_and_clear_pending_msg_events(); + assert_eq!(events.len(), expected_paths.len()); + + // First path + let ev = remove_first_msg_event_to_node(&expected_paths[0][0].node.get_our_node_id(), &mut events); + let mut payment_event = SendEvent::from_event(ev); + let mut prev_node = &nodes[0]; + + for (idx, &node) in expected_paths[0].iter().enumerate() { + assert_eq!(node.node.get_our_node_id(), payment_event.node_id); + + if idx == 0 { // routing node + let session_priv = [3; 32]; + let height = nodes[0].best_block_info().1; + let session_priv = SecretKey::from_slice(&session_priv).unwrap(); + let mut onion_keys = onion_utils::construct_onion_keys(&Secp256k1::new(), &route.paths[0], &session_priv).unwrap(); + let (mut onion_payloads, _, _) = onion_utils::build_onion_payloads(&route.paths[0], 100_000, &Some(our_payment_secret), height + 1, &None).unwrap(); + // Edit amt_to_forward to simulate the sender having set + // the final amount and the routing node taking less fee + onion_payloads[1].amt_to_forward = 99_000; + let new_onion_packet = onion_utils::construct_onion_packet(onion_payloads, onion_keys, [0; 32], &our_payment_hash); + payment_event.msgs[0].onion_routing_packet = new_onion_packet; + } + + node.node.handle_update_add_htlc(&prev_node.node.get_our_node_id(), &payment_event.msgs[0]); + check_added_monitors!(node, 0); + commitment_signed_dance!(node, prev_node, payment_event.commitment_msg, false); + expect_pending_htlcs_forwardable!(node); + + if idx == 0 { + let mut events_2 = node.node.get_and_clear_pending_msg_events(); + assert_eq!(events_2.len(), 1); + check_added_monitors!(node, 1); + payment_event = SendEvent::from_event(events_2.remove(0)); + assert_eq!(payment_event.msgs.len(), 1); + } else { + let events_2 = node.node.get_and_clear_pending_events(); + assert!(events_2.is_empty()); + } + + prev_node = node; + } + + // Second path + let ev = remove_first_msg_event_to_node(&expected_paths[1][0].node.get_our_node_id(), &mut events); + pass_along_path(&nodes[0], expected_paths[1], 101_000, our_payment_hash.clone(), Some(our_payment_secret), ev, true, None); + + claim_payment_along_route(&nodes[0], expected_paths, false, our_payment_preimage); +} + fn do_test_overshoot_mpp(msat_amounts: &[u64], total_msat: u64) { let routing_node_count = msat_amounts.len();