From 6d2c7804285ebe0fffeb91d108e3bd0ab0429c38 Mon Sep 17 00:00:00 2001 From: Wilmer Paulino Date: Thu, 5 Feb 2026 17:12:01 -0800 Subject: [PATCH 01/68] Make get_latest_mon_update_id a helper on TestChainMonitor --- lightning/src/ln/chanmon_update_fail_tests.rs | 71 +++++++++---------- lightning/src/util/test_utils.rs | 5 ++ 2 files changed, 37 insertions(+), 39 deletions(-) diff --git a/lightning/src/ln/chanmon_update_fail_tests.rs b/lightning/src/ln/chanmon_update_fail_tests.rs index 6dc237aeddb..96ac147635d 100644 --- a/lightning/src/ln/chanmon_update_fail_tests.rs +++ b/lightning/src/ln/chanmon_update_fail_tests.rs @@ -49,13 +49,6 @@ use crate::prelude::*; use crate::sync::{Arc, Mutex}; use bitcoin::hashes::Hash; -fn get_latest_mon_update_id<'a, 'b, 'c>( - node: &Node<'a, 'b, 'c>, channel_id: ChannelId, -) -> (u64, u64) { - let monitor_id_state = node.chain_monitor.latest_monitor_update_id.lock().unwrap(); - monitor_id_state.get(&channel_id).unwrap().clone() -} - #[test] fn test_monitor_and_persister_update_fail() { // Test that if both updating the `ChannelMonitor` and persisting the updated @@ -213,7 +206,7 @@ fn do_test_simple_monitor_temporary_update_fail(disconnect: bool) { } chanmon_cfgs[0].persister.set_update_ret(ChannelMonitorUpdateStatus::Completed); - let (latest_update, _) = get_latest_mon_update_id(&nodes[0], channel_id); + let (latest_update, _) = nodes[0].chain_monitor.get_latest_mon_update_id(channel_id); nodes[0].chain_monitor.chain_monitor.force_channel_monitor_updated(channel_id, latest_update); check_added_monitors(&nodes[0], 0); @@ -405,7 +398,7 @@ fn do_test_monitor_temporary_update_fail(disconnect_count: usize) { // Now fix monitor updating... chanmon_cfgs[0].persister.set_update_ret(ChannelMonitorUpdateStatus::Completed); - let (latest_update, _) = get_latest_mon_update_id(&nodes[0], channel_id); + let (latest_update, _) = nodes[0].chain_monitor.get_latest_mon_update_id(channel_id); nodes[0].chain_monitor.chain_monitor.force_channel_monitor_updated(channel_id, latest_update); check_added_monitors(&nodes[0], 0); @@ -758,7 +751,7 @@ fn test_monitor_update_fail_cs() { assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty()); chanmon_cfgs[1].persister.set_update_ret(ChannelMonitorUpdateStatus::Completed); - let (latest_update, _) = get_latest_mon_update_id(&nodes[1], channel_id); + let (latest_update, _) = nodes[1].chain_monitor.get_latest_mon_update_id(channel_id); nodes[1].chain_monitor.chain_monitor.force_channel_monitor_updated(channel_id, latest_update); check_added_monitors(&nodes[1], 0); let responses = nodes[1].node.get_and_clear_pending_msg_events(); @@ -793,7 +786,7 @@ fn test_monitor_update_fail_cs() { } chanmon_cfgs[0].persister.set_update_ret(ChannelMonitorUpdateStatus::Completed); - let (latest_update, _) = get_latest_mon_update_id(&nodes[0], channel_id); + let (latest_update, _) = nodes[0].chain_monitor.get_latest_mon_update_id(channel_id); nodes[0].chain_monitor.chain_monitor.force_channel_monitor_updated(channel_id, latest_update); check_added_monitors(&nodes[0], 0); @@ -869,7 +862,7 @@ fn test_monitor_update_fail_no_rebroadcast() { check_added_monitors(&nodes[1], 1); chanmon_cfgs[1].persister.set_update_ret(ChannelMonitorUpdateStatus::Completed); - let (latest_update, _) = get_latest_mon_update_id(&nodes[1], channel_id); + let (latest_update, _) = nodes[1].chain_monitor.get_latest_mon_update_id(channel_id); nodes[1].chain_monitor.chain_monitor.force_channel_monitor_updated(channel_id, latest_update); assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty()); check_added_monitors(&nodes[1], 0); @@ -939,7 +932,7 @@ fn test_monitor_update_raa_while_paused() { assert!(nodes[0].node.get_and_clear_pending_msg_events().is_empty()); check_added_monitors(&nodes[0], 1); - let (latest_update, _) = get_latest_mon_update_id(&nodes[0], channel_id); + let (latest_update, _) = nodes[0].chain_monitor.get_latest_mon_update_id(channel_id); nodes[0].chain_monitor.chain_monitor.force_channel_monitor_updated(channel_id, latest_update); check_added_monitors(&nodes[0], 0); @@ -1081,7 +1074,7 @@ fn do_test_monitor_update_fail_raa(test_ignore_second_cs: bool) { // Restore monitor updating, ensuring we immediately get a fail-back update and a // update_add update. chanmon_cfgs[1].persister.set_update_ret(ChannelMonitorUpdateStatus::Completed); - let (latest_update, _) = get_latest_mon_update_id(&nodes[1], chan_2.2); + let (latest_update, _) = nodes[1].chain_monitor.get_latest_mon_update_id(chan_2.2); nodes[1].chain_monitor.chain_monitor.force_channel_monitor_updated(chan_2.2, latest_update); check_added_monitors(&nodes[1], 0); expect_and_process_pending_htlcs_and_htlc_handling_failed( @@ -1355,7 +1348,7 @@ fn test_monitor_update_fail_reestablish() { assert_eq!(bs_channel_upd.contents.channel_flags & 2, 0); chanmon_cfgs[1].persister.set_update_ret(ChannelMonitorUpdateStatus::Completed); - let (latest_update, _) = get_latest_mon_update_id(&nodes[1], chan_1.2); + let (latest_update, _) = nodes[1].chain_monitor.get_latest_mon_update_id(chan_1.2); nodes[1].chain_monitor.chain_monitor.force_channel_monitor_updated(chan_1.2, latest_update); check_added_monitors(&nodes[1], 0); @@ -1440,7 +1433,7 @@ fn raa_no_response_awaiting_raa_state() { assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty()); check_added_monitors(&nodes[1], 1); - let (latest_update, _) = get_latest_mon_update_id(&nodes[1], channel_id); + let (latest_update, _) = nodes[1].chain_monitor.get_latest_mon_update_id(channel_id); nodes[1].chain_monitor.chain_monitor.force_channel_monitor_updated(channel_id, latest_update); // nodes[1] should be AwaitingRAA here! check_added_monitors(&nodes[1], 0); @@ -1569,7 +1562,7 @@ fn claim_while_disconnected_monitor_update_fail() { // Now un-fail the monitor, which will result in B sending its original commitment update, // receiving the commitment update from A, and the resulting commitment dances. chanmon_cfgs[1].persister.set_update_ret(ChannelMonitorUpdateStatus::Completed); - let (latest_update, _) = get_latest_mon_update_id(&nodes[1], channel_id); + let (latest_update, _) = nodes[1].chain_monitor.get_latest_mon_update_id(channel_id); nodes[1].chain_monitor.chain_monitor.force_channel_monitor_updated(channel_id, latest_update); check_added_monitors(&nodes[1], 0); @@ -1698,7 +1691,7 @@ fn monitor_failed_no_reestablish_response() { get_event_msg!(nodes[0], MessageSendEvent::SendChannelUpdate, node_b_id); chanmon_cfgs[1].persister.set_update_ret(ChannelMonitorUpdateStatus::Completed); - let (latest_update, _) = get_latest_mon_update_id(&nodes[1], channel_id); + let (latest_update, _) = nodes[1].chain_monitor.get_latest_mon_update_id(channel_id); nodes[1].chain_monitor.chain_monitor.force_channel_monitor_updated(channel_id, latest_update); check_added_monitors(&nodes[1], 0); let bs_responses = get_revoke_commit_msgs(&nodes[1], &node_a_id); @@ -1796,7 +1789,7 @@ fn first_message_on_recv_ordering() { assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty()); chanmon_cfgs[1].persister.set_update_ret(ChannelMonitorUpdateStatus::Completed); - let (latest_update, _) = get_latest_mon_update_id(&nodes[1], channel_id); + let (latest_update, _) = nodes[1].chain_monitor.get_latest_mon_update_id(channel_id); nodes[1].chain_monitor.chain_monitor.force_channel_monitor_updated(channel_id, latest_update); check_added_monitors(&nodes[1], 0); @@ -1895,7 +1888,7 @@ fn test_monitor_update_fail_claim() { // Now restore monitor updating on the 0<->1 channel and claim the funds on B. let channel_id = chan_1.2; - let (latest_update, _) = get_latest_mon_update_id(&nodes[1], channel_id); + let (latest_update, _) = nodes[1].chain_monitor.get_latest_mon_update_id(channel_id); nodes[1].chain_monitor.chain_monitor.force_channel_monitor_updated(channel_id, latest_update); expect_payment_claimed!(nodes[1], payment_hash_1, 1_000_000); check_added_monitors(&nodes[1], 0); @@ -2024,7 +2017,7 @@ fn test_monitor_update_on_pending_forwards() { check_added_monitors(&nodes[1], 1); chanmon_cfgs[1].persister.set_update_ret(ChannelMonitorUpdateStatus::Completed); - let (latest_update, _) = get_latest_mon_update_id(&nodes[1], chan_1.2); + let (latest_update, _) = nodes[1].chain_monitor.get_latest_mon_update_id(chan_1.2); nodes[1].chain_monitor.chain_monitor.force_channel_monitor_updated(chan_1.2, latest_update); check_added_monitors(&nodes[1], 0); @@ -2095,7 +2088,7 @@ fn monitor_update_claim_fail_no_response() { assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty()); chanmon_cfgs[1].persister.set_update_ret(ChannelMonitorUpdateStatus::Completed); - let (latest_update, _) = get_latest_mon_update_id(&nodes[1], channel_id); + let (latest_update, _) = nodes[1].chain_monitor.get_latest_mon_update_id(channel_id); nodes[1].chain_monitor.chain_monitor.force_channel_monitor_updated(channel_id, latest_update); expect_payment_claimed!(nodes[1], payment_hash_1, 1_000_000); check_added_monitors(&nodes[1], 0); @@ -2167,7 +2160,7 @@ fn do_during_funding_monitor_fail( assert!(nodes[0].node.get_and_clear_pending_msg_events().is_empty()); assert!(nodes[0].node.get_and_clear_pending_events().is_empty()); chanmon_cfgs[0].persister.set_update_ret(ChannelMonitorUpdateStatus::Completed); - let (latest_update, _) = get_latest_mon_update_id(&nodes[0], channel_id); + let (latest_update, _) = nodes[0].chain_monitor.get_latest_mon_update_id(channel_id); nodes[0].chain_monitor.chain_monitor.force_channel_monitor_updated(channel_id, latest_update); check_added_monitors(&nodes[0], 0); expect_channel_pending_event(&nodes[0], &node_b_id); @@ -2222,7 +2215,7 @@ fn do_during_funding_monitor_fail( } chanmon_cfgs[1].persister.set_update_ret(ChannelMonitorUpdateStatus::Completed); - let (latest_update, _) = get_latest_mon_update_id(&nodes[1], channel_id); + let (latest_update, _) = nodes[1].chain_monitor.get_latest_mon_update_id(channel_id); nodes[1].chain_monitor.chain_monitor.force_channel_monitor_updated(channel_id, latest_update); check_added_monitors(&nodes[1], 0); @@ -2340,7 +2333,7 @@ fn test_path_paused_mpp() { // And check that, after we successfully update the monitor for chan_2 we can pass the second // HTLC along to nodes[3] and claim the whole payment back to nodes[0]. - let (latest_update, _) = get_latest_mon_update_id(&nodes[0], chan_2_id); + let (latest_update, _) = nodes[0].chain_monitor.get_latest_mon_update_id(chan_2_id); nodes[0].chain_monitor.chain_monitor.force_channel_monitor_updated(chan_2_id, latest_update); let mut events = nodes[0].node.get_and_clear_pending_msg_events(); @@ -2788,7 +2781,7 @@ fn do_channel_holding_cell_serialize(disconnect: bool, reload_a: bool) { // If we finish updating the monitor, we should free the holding cell right away (this did // not occur prior to #756). This should result in a new monitor update. chanmon_cfgs[0].persister.set_update_ret(ChannelMonitorUpdateStatus::Completed); - let (mon_id, _) = get_latest_mon_update_id(&nodes[0], chan_id); + let (mon_id, _) = nodes[0].chain_monitor.get_latest_mon_update_id(chan_id); nodes[0].chain_monitor.chain_monitor.force_channel_monitor_updated(chan_id, mon_id); expect_payment_claimed!(nodes[0], payment_hash_0, 100_000); check_added_monitors(&nodes[0], 1); @@ -3040,7 +3033,7 @@ fn test_temporary_error_during_shutdown() { chanmon_cfgs[0].persister.set_update_ret(ChannelMonitorUpdateStatus::Completed); chanmon_cfgs[1].persister.set_update_ret(ChannelMonitorUpdateStatus::Completed); - let (latest_update, _) = get_latest_mon_update_id(&nodes[0], channel_id); + let (latest_update, _) = nodes[0].chain_monitor.get_latest_mon_update_id(channel_id); nodes[0].chain_monitor.chain_monitor.force_channel_monitor_updated(channel_id, latest_update); nodes[1].node.handle_closing_signed( node_a_id, @@ -3050,7 +3043,7 @@ fn test_temporary_error_during_shutdown() { assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty()); chanmon_cfgs[1].persister.set_update_ret(ChannelMonitorUpdateStatus::Completed); - let (latest_update, _) = get_latest_mon_update_id(&nodes[1], channel_id); + let (latest_update, _) = nodes[1].chain_monitor.get_latest_mon_update_id(channel_id); nodes[1].chain_monitor.chain_monitor.force_channel_monitor_updated(channel_id, latest_update); nodes[0].node.handle_closing_signed( @@ -3096,7 +3089,7 @@ fn double_temp_error() { // `claim_funds` results in a ChannelMonitorUpdate. nodes[1].node.claim_funds(payment_preimage_1); check_added_monitors(&nodes[1], 1); - let (latest_update_1, _) = get_latest_mon_update_id(&nodes[1], channel_id); + let (latest_update_1, _) = nodes[1].chain_monitor.get_latest_mon_update_id(channel_id); chanmon_cfgs[1].persister.set_update_ret(ChannelMonitorUpdateStatus::InProgress); // Previously, this would've panicked due to a double-call to `Channel::monitor_update_failed`, @@ -3105,7 +3098,7 @@ fn double_temp_error() { check_added_monitors(&nodes[1], 1); chanmon_cfgs[1].persister.set_update_ret(ChannelMonitorUpdateStatus::Completed); - let (latest_update_2, _) = get_latest_mon_update_id(&nodes[1], channel_id); + let (latest_update_2, _) = nodes[1].chain_monitor.get_latest_mon_update_id(channel_id); nodes[1].chain_monitor.chain_monitor.force_channel_monitor_updated(channel_id, latest_update_1); assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty()); check_added_monitors(&nodes[1], 0); @@ -3512,7 +3505,7 @@ fn do_test_blocked_chan_preimage_release(completion_mode: BlockedUpdateComplMode reconnect_nodes(a_b_reconnect); reconnect_nodes(ReconnectArgs::new(&nodes[2], &nodes[1])); } else if completion_mode == BlockedUpdateComplMode::Async { - let (latest_update, _) = get_latest_mon_update_id(&nodes[1], chan_id_2); + let (latest_update, _) = nodes[1].chain_monitor.get_latest_mon_update_id(chan_id_2); nodes[1] .chain_monitor .chain_monitor @@ -3690,7 +3683,7 @@ fn do_test_inverted_mon_completion_order( // (Finally) complete the A <-> B ChannelMonitorUpdate, ensuring the preimage is durably on // disk in the proper ChannelMonitor, unblocking the B <-> C ChannelMonitor updating // process. - let (_, ab_update_id) = get_latest_mon_update_id(&nodes[1], chan_id_ab); + let (_, ab_update_id) = nodes[1].chain_monitor.get_latest_mon_update_id(chan_id_ab); nodes[1] .chain_monitor .chain_monitor @@ -3723,7 +3716,7 @@ fn do_test_inverted_mon_completion_order( // ChannelMonitorUpdate hasn't yet completed. reconnect_nodes(ReconnectArgs::new(&nodes[0], &nodes[1])); - let (_, ab_update_id) = get_latest_mon_update_id(&nodes[1], chan_id_ab); + let (_, ab_update_id) = nodes[1].chain_monitor.get_latest_mon_update_id(chan_id_ab); nodes[1] .chain_monitor .chain_monitor @@ -3936,7 +3929,7 @@ fn do_test_durable_preimages_on_closed_channel( // Once the blocked `ChannelMonitorUpdate` *finally* completes, the pending // `PaymentForwarded` event will finally be released. - let (_, ab_update_id) = get_latest_mon_update_id(&nodes[1], chan_id_ab); + let (_, ab_update_id) = nodes[1].chain_monitor.get_latest_mon_update_id(chan_id_ab); nodes[1].chain_monitor.chain_monitor.force_channel_monitor_updated(chan_id_ab, ab_update_id); // If the A<->B channel was closed before we reload, we'll replay the claim against it on @@ -4048,7 +4041,7 @@ fn do_test_reload_mon_update_completion_actions(close_during_reload: bool) { mine_transaction_without_consistency_checks(&nodes[1], &as_closing_tx[0]); } - let (_, bc_update_id) = get_latest_mon_update_id(&nodes[1], chan_id_bc); + let (_, bc_update_id) = nodes[1].chain_monitor.get_latest_mon_update_id(chan_id_bc); let mut events = nodes[1].node.get_and_clear_pending_events(); assert_eq!(events.len(), if close_during_reload { 2 } else { 1 }); expect_payment_forwarded( @@ -4073,7 +4066,7 @@ fn do_test_reload_mon_update_completion_actions(close_during_reload: bool) { // Once we run event processing the monitor should free, check that it was indeed the B<->C // channel which was updated. check_added_monitors(&nodes[1], if close_during_reload { 2 } else { 1 }); - let (_, post_ev_bc_update_id) = get_latest_mon_update_id(&nodes[1], chan_id_bc); + let (_, post_ev_bc_update_id) = nodes[1].chain_monitor.get_latest_mon_update_id(chan_id_bc); assert!(bc_update_id != post_ev_bc_update_id); // Finally, check that there's nothing left to do on B<->C reconnect and the channel operates @@ -4163,7 +4156,7 @@ fn do_test_glacial_peer_cant_hang(hold_chan_a: bool) { // ...but once we complete the A<->B channel preimage persistence, the B<->C channel // unlocks and we send both peers commitment updates. - let (ab_update_id, _) = get_latest_mon_update_id(&nodes[1], chan_id_ab); + let (ab_update_id, _) = nodes[1].chain_monitor.get_latest_mon_update_id(chan_id_ab); assert!(nodes[1] .chain_monitor .chain_monitor @@ -5123,7 +5116,7 @@ fn test_mpp_claim_to_holding_cell() { check_added_monitors(&nodes[3], 2); // Complete the B <-> D monitor update, freeing the first fulfill. - let (latest_id, _) = get_latest_mon_update_id(&nodes[3], chan_3_id); + let (latest_id, _) = nodes[3].chain_monitor.get_latest_mon_update_id(chan_3_id); nodes[3].chain_monitor.chain_monitor.channel_monitor_updated(chan_3_id, latest_id).unwrap(); let mut b_claim = get_htlc_update_msgs(&nodes[3], &node_b_id); @@ -5134,7 +5127,7 @@ fn test_mpp_claim_to_holding_cell() { // Finally, complete the C <-> D monitor update. Previously, this unlock failed to be processed // due to the existence of the blocked RAA update above. - let (latest_id, _) = get_latest_mon_update_id(&nodes[3], chan_4_id); + let (latest_id, _) = nodes[3].chain_monitor.get_latest_mon_update_id(chan_4_id); nodes[3].chain_monitor.chain_monitor.channel_monitor_updated(chan_4_id, latest_id).unwrap(); // Once we process monitor events (in this case by checking for the `PaymentClaimed` event, the // RAA monitor update blocked above will be released. diff --git a/lightning/src/util/test_utils.rs b/lightning/src/util/test_utils.rs index 18d003c7993..bcf39fde482 100644 --- a/lightning/src/util/test_utils.rs +++ b/lightning/src/util/test_utils.rs @@ -581,6 +581,11 @@ impl<'a> TestChainMonitor<'a> { self.added_monitors.lock().unwrap().push((channel_id, monitor)); self.chain_monitor.load_existing_monitor(channel_id, new_monitor) } + + pub fn get_latest_mon_update_id(&self, channel_id: ChannelId) -> (u64, u64) { + let monitor_id_state = self.latest_monitor_update_id.lock().unwrap(); + monitor_id_state.get(&channel_id).unwrap().clone() + } } impl<'a> chain::Watch for TestChainMonitor<'a> { fn watch_channel( From 9e2af0cfe05eb148f859ec72dd7ef061600c0bb3 Mon Sep 17 00:00:00 2001 From: Wilmer Paulino Date: Tue, 3 Feb 2026 16:09:06 -0800 Subject: [PATCH 02/68] Hold in-flight monitor updates until background event processing We previously assumed background events would eventually be processed prior to another `ChannelManager` write, so we would immediately remove all in-flight monitor updates that completed since the last `ChannelManager` serialization. This isn't always the case, so we now keep them all around until we're ready to handle them, i.e., when `process_background_events` is called. This was discovered while fuzzing `chanmon_consistency_target` on the main branch with some changes that allow it to connect blocks. It was triggered by reloading the `ChannelManager` after a monitor update completion for an outgoing HTLC, calling `ChannelManager::best_block_updated`, and reloading the `ChannelManager` once again. A test is included that provides a minimal reproduction of this case. --- lightning/src/ln/channelmanager.rs | 82 +++++++++++++++++++++--------- lightning/src/ln/reload_tests.rs | 81 +++++++++++++++++++++++++++++ 2 files changed, 140 insertions(+), 23 deletions(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 665a79a9610..9a86f64873c 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -1278,7 +1278,11 @@ enum BackgroundEvent { /// Some [`ChannelMonitorUpdate`] (s) completed before we were serialized but we still have /// them marked pending, thus we need to run any [`MonitorUpdateCompletionAction`] (s) pending /// on a channel. - MonitorUpdatesComplete { counterparty_node_id: PublicKey, channel_id: ChannelId }, + MonitorUpdatesComplete { + counterparty_node_id: PublicKey, + channel_id: ChannelId, + highest_update_id_completed: u64, + }, } /// A pointer to a channel that is unblocked when an event is surfaced @@ -8159,8 +8163,21 @@ impl< BackgroundEvent::MonitorUpdateRegeneratedOnStartup { counterparty_node_id, funding_txo, channel_id, update } => { self.apply_post_close_monitor_update(counterparty_node_id, channel_id, funding_txo, update); }, - BackgroundEvent::MonitorUpdatesComplete { counterparty_node_id, channel_id } => { - self.channel_monitor_updated(&channel_id, None, &counterparty_node_id); + BackgroundEvent::MonitorUpdatesComplete { + counterparty_node_id, + channel_id, + highest_update_id_completed, + } => { + // Now that we can finally handle the background event, remove all in-flight + // monitor updates for this channel that we've known to complete, as they have + // already been persisted to the monitor and can be applied to our internal + // state such that the channel resumes operation if no new updates have been + // made since. + self.channel_monitor_updated( + &channel_id, + Some(highest_update_id_completed), + &counterparty_node_id, + ); }, } } @@ -18240,39 +18257,58 @@ impl< ($counterparty_node_id: expr, $chan_in_flight_upds: expr, $monitor: expr, $peer_state: expr, $logger: expr, $channel_info_log: expr ) => { { + // When all in-flight updates have completed after we were last serialized, we + // need to remove them. However, we can't guarantee that the next serialization + // will have happened after processing the + // `BackgroundEvent::MonitorUpdatesComplete`, so removing them now could lead to the + // channel never being resumed as the event would not be regenerated after another + // reload. At the same time, we don't want to resume the channel now because there + // may be post-update actions to handle. Therefore, we're forced to keep tracking + // the completed in-flight updates (but only when they have all completed) until we + // are processing the `BackgroundEvent::MonitorUpdatesComplete`. let mut max_in_flight_update_id = 0; - let starting_len = $chan_in_flight_upds.len(); - $chan_in_flight_upds.retain(|upd| upd.update_id > $monitor.get_latest_update_id()); - if $chan_in_flight_upds.len() < starting_len { + let num_updates_completed = $chan_in_flight_upds + .iter() + .filter(|update| { + max_in_flight_update_id = cmp::max(max_in_flight_update_id, update.update_id); + update.update_id <= $monitor.get_latest_update_id() + }) + .count(); + if num_updates_completed > 0 { log_debug!( $logger, "{} ChannelMonitorUpdates completed after ChannelManager was last serialized", - starting_len - $chan_in_flight_upds.len() + num_updates_completed, ); } + let all_updates_completed = num_updates_completed == $chan_in_flight_upds.len(); + let funding_txo = $monitor.get_funding_txo(); - for update in $chan_in_flight_upds.iter() { - log_debug!($logger, "Replaying ChannelMonitorUpdate {} for {}channel {}", - update.update_id, $channel_info_log, &$monitor.channel_id()); - max_in_flight_update_id = cmp::max(max_in_flight_update_id, update.update_id); - pending_background_events.push( - BackgroundEvent::MonitorUpdateRegeneratedOnStartup { - counterparty_node_id: $counterparty_node_id, - funding_txo: funding_txo, - channel_id: $monitor.channel_id(), - update: update.clone(), - }); - } - if $chan_in_flight_upds.is_empty() { - // We had some updates to apply, but it turns out they had completed before we - // were serialized, we just weren't notified of that. Thus, we may have to run - // the completion actions for any monitor updates, but otherwise are done. + if all_updates_completed { + log_debug!($logger, "All monitor updates completed since the ChannelManager was last serialized"); pending_background_events.push( BackgroundEvent::MonitorUpdatesComplete { counterparty_node_id: $counterparty_node_id, channel_id: $monitor.channel_id(), + highest_update_id_completed: max_in_flight_update_id, }); } else { + $chan_in_flight_upds.retain(|update| { + let replay = update.update_id > $monitor.get_latest_update_id(); + if replay { + log_debug!($logger, "Replaying ChannelMonitorUpdate {} for {}channel {}", + update.update_id, $channel_info_log, &$monitor.channel_id()); + pending_background_events.push( + BackgroundEvent::MonitorUpdateRegeneratedOnStartup { + counterparty_node_id: $counterparty_node_id, + funding_txo: funding_txo, + channel_id: $monitor.channel_id(), + update: update.clone(), + } + ); + } + replay + }); $peer_state.closed_channel_monitor_update_ids.entry($monitor.channel_id()) .and_modify(|v| *v = cmp::max(max_in_flight_update_id, *v)) .or_insert(max_in_flight_update_id); diff --git a/lightning/src/ln/reload_tests.rs b/lightning/src/ln/reload_tests.rs index a8206dfe850..c0432051a62 100644 --- a/lightning/src/ln/reload_tests.rs +++ b/lightning/src/ln/reload_tests.rs @@ -1566,3 +1566,84 @@ fn test_peer_storage() { assert!(res.is_err()); } +#[test] +fn test_hold_completed_inflight_monitor_updates_upon_manager_reload() { + // Test that if a `ChannelMonitorUpdate` completes after the `ChannelManager` is serialized, + // but before it is deserialized, we hold any completed in-flight updates until background event + // processing. Previously, we would remove completed monitor updates from + // `in_flight_monitor_updates` during deserialization, relying on + // [`ChannelManager::process_background_events`] to eventually be called before the + // `ChannelManager` is serialized again such that the channel is resumed and further updates can + // be made. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let (persister_a, persister_b); + let (chain_monitor_a, chain_monitor_b); + + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes_0_deserialized_a; + let nodes_0_deserialized_b; + + let mut nodes = create_network(2, &node_cfgs, &node_chanmgrs); + let chan_id = create_announced_chan_between_nodes(&nodes, 0, 1).2; + + send_payment(&nodes[0], &[&nodes[1]], 1_000_000); + + chanmon_cfgs[0].persister.set_update_ret(ChannelMonitorUpdateStatus::InProgress); + + // Send a payment that will be pending due to an async monitor update. + let (route, payment_hash, _, payment_secret) = + get_route_and_payment_hash!(nodes[0], nodes[1], 1_000_000); + let payment_id = PaymentId(payment_hash.0); + let onion = RecipientOnionFields::secret_only(payment_secret); + nodes[0].node.send_payment_with_route(route, payment_hash, onion, payment_id).unwrap(); + check_added_monitors(&nodes[0], 1); + + assert!(nodes[0].node.get_and_clear_pending_msg_events().is_empty()); + assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty()); + + // Serialize the ChannelManager while the monitor update is still in-flight. + let node_0_serialized = nodes[0].node.encode(); + + // Now complete the monitor update by calling force_channel_monitor_updated. + // This updates the monitor's state, but the ChannelManager still thinks it's pending. + let (_, latest_update_id) = nodes[0].chain_monitor.get_latest_mon_update_id(chan_id); + nodes[0].chain_monitor.chain_monitor.force_channel_monitor_updated(chan_id, latest_update_id); + let monitor_serialized_updated = get_monitor!(nodes[0], chan_id).encode(); + + // Reload the node with the updated monitor. Upon deserialization, the ChannelManager will + // detect that the monitor update completed (monitor's update_id >= the in-flight update_id) + // and queue a `BackgroundEvent::MonitorUpdatesComplete`. + nodes[0].node.peer_disconnected(nodes[1].node.get_our_node_id()); + nodes[1].node.peer_disconnected(nodes[0].node.get_our_node_id()); + reload_node!( + nodes[0], + test_default_channel_config(), + &node_0_serialized, + &[&monitor_serialized_updated[..]], + persister_a, + chain_monitor_a, + nodes_0_deserialized_a + ); + + // If we serialize again, even though we haven't processed any background events yet, we should + // still see the `BackgroundEvent::MonitorUpdatesComplete` be regenerated on startup. + let node_0_serialized = nodes[0].node.encode(); + reload_node!( + nodes[0], + test_default_channel_config(), + &node_0_serialized, + &[&monitor_serialized_updated[..]], + persister_b, + chain_monitor_b, + nodes_0_deserialized_b + ); + + // Reconnect the nodes. We should finally see the `update_add_htlc` go out, as the reconnection + // should first process `BackgroundEvent::MonitorUpdatesComplete, allowing the channel to be + // resumed. + let mut reconnect_args = ReconnectArgs::new(&nodes[0], &nodes[1]); + reconnect_args.pending_htlc_adds = (0, 1); + reconnect_nodes(reconnect_args); +} + From 9bb4043ded8b5ebd3652f166c0c7d15678ebcef7 Mon Sep 17 00:00:00 2001 From: Wilmer Paulino Date: Thu, 5 Feb 2026 08:49:22 -0800 Subject: [PATCH 03/68] Rustfmt ChannelManager::process_background_events --- lightning/src/ln/channelmanager.rs | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 9a86f64873c..13197ea44ec 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -8146,9 +8146,11 @@ impl< /// Free the background events, generally called from [`PersistenceNotifierGuard`] constructors. /// /// Expects the caller to have a total_consistency_lock read lock. - #[rustfmt::skip] fn process_background_events(&self) -> NotifyOption { - debug_assert_ne!(self.total_consistency_lock.held_by_thread(), LockHeldState::NotHeldByThread); + debug_assert_ne!( + self.total_consistency_lock.held_by_thread(), + LockHeldState::NotHeldByThread + ); self.background_events_processed_since_startup.store(true, Ordering::Release); @@ -8160,8 +8162,18 @@ impl< for event in background_events.drain(..) { match event { - BackgroundEvent::MonitorUpdateRegeneratedOnStartup { counterparty_node_id, funding_txo, channel_id, update } => { - self.apply_post_close_monitor_update(counterparty_node_id, channel_id, funding_txo, update); + BackgroundEvent::MonitorUpdateRegeneratedOnStartup { + counterparty_node_id, + funding_txo, + channel_id, + update, + } => { + self.apply_post_close_monitor_update( + counterparty_node_id, + channel_id, + funding_txo, + update, + ); }, BackgroundEvent::MonitorUpdatesComplete { counterparty_node_id, From a997b283478d0364298080120ab418922eba0ba2 Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Sun, 1 Feb 2026 13:24:04 +0000 Subject: [PATCH 04/68] Add missing ` to ChannelManager::send_payment docs. --- lightning/src/ln/channelmanager.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 13197ea44ec..c7fd4823d5c 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -5350,7 +5350,7 @@ impl< /// using [`ChannelMonitorUpdateStatus::InProgress`]), the payment may be lost on restart. See /// [`ChannelManager::list_recent_payments`] for more information. /// - /// Routes are automatically found using the [`Router] provided on startup. To fix a route for a + /// Routes are automatically found using the [`Router`] provided on startup. To fix a route for a /// particular payment, use [`Self::send_payment_with_route`] or match the [`PaymentId`] passed to /// [`Router::find_route_with_id`]. /// From 3c5aa4fff541d1b686e3a5a8113cbae3844b2730 Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Sun, 1 Feb 2026 01:30:13 +0000 Subject: [PATCH 05/68] Add a `custom` TLV read/write variant At various points we've been stuck in our TLV read/write variants but just want to break out and write some damn code to initialize a field and some more code to decide what to write for a TLV. We added the write-side part of this with the `legacy` TLV read/write variant, but its useful to also be able to specify a function which is called on the read side. Here we add a `custom` TLV read/write variant which calls a method both on read and write to either decide what to write or to map a read value (if any) to the final field. --- lightning-macros/src/lib.rs | 6 ++--- lightning/src/util/ser_macros.rs | 43 ++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/lightning-macros/src/lib.rs b/lightning-macros/src/lib.rs index e784acf72fb..778da45ee8f 100644 --- a/lightning-macros/src/lib.rs +++ b/lightning-macros/src/lib.rs @@ -138,7 +138,7 @@ fn process_fields(group: Group) -> proc_macro::TokenStream { if let TokenTree::Group(group) = ty_info { let first_group_tok = group.stream().into_iter().next().unwrap(); if let TokenTree::Ident(ident) = first_group_tok { - if ident.to_string() == "legacy" { + if ident.to_string() == "legacy" || ident.to_string() == "custom" { continue; } } @@ -155,13 +155,13 @@ fn process_fields(group: Group) -> proc_macro::TokenStream { computed_fields } -/// Scans a match statement for legacy fields which should be skipped. +/// Scans a match statement for legacy or custom fields which should be skipped. /// /// This is used internally in LDK's TLV serialization logic and is not expected to be used by /// other crates. /// /// Wraps a `match self {..}` statement and scans the fields in the match patterns (in the form -/// `ref $field_name: $field_ty`) for types marked `legacy`, skipping those fields. +/// `ref $field_name: $field_ty`) for types marked `legacy` or `custom`, skipping those fields. /// /// Specifically, it expects input like the following, simply dropping `field3` and the /// `: $field_ty` after each field name. diff --git a/lightning/src/util/ser_macros.rs b/lightning/src/util/ser_macros.rs index 86b24e1b849..bd2b5d1983a 100644 --- a/lightning/src/util/ser_macros.rs +++ b/lightning/src/util/ser_macros.rs @@ -63,6 +63,9 @@ macro_rules! _encode_tlv { } $crate::_encode_tlv!($stream, $optional_type, value, option); } }; + ($stream: expr, $optional_type: expr, $optional_field: expr, (custom, $fieldty: ty, $read: expr, $write: expr) $(, $self: ident)?) => { { + $crate::_encode_tlv!($stream, $optional_type, $optional_field, (legacy, $fieldty, $write) $(, $self)?); + } }; ($stream: expr, $type: expr, $field: expr, optional_vec $(, $self: ident)?) => { if !$field.is_empty() { $crate::_encode_tlv!($stream, $type, $field, required_vec); @@ -232,6 +235,9 @@ macro_rules! _get_varint_length_prefixed_tlv_length { ($len: expr, $optional_type: expr, $optional_field: expr, (legacy, $fieldty: ty, $write: expr) $(, $self: ident)?) => { $crate::_get_varint_length_prefixed_tlv_length!($len, $optional_type, $write($($self)?), option); }; + ($len: expr, $optional_type: expr, $optional_field: expr, (custom, $fieldty: ty, $read: expr, $write: expr) $(, $self: ident)?) => { + $crate::_get_varint_length_prefixed_tlv_length!($len, $optional_type, $optional_field, (legacy, $fieldty, $write) $(, $self)?); + }; ($len: expr, $type: expr, $field: expr, optional_vec $(, $self: ident)?) => { if !$field.is_empty() { $crate::_get_varint_length_prefixed_tlv_length!($len, $type, $field, required_vec); @@ -317,6 +323,16 @@ macro_rules! _check_decoded_tlv_order { ($last_seen_type: expr, $typ: expr, $type: expr, $field: ident, (legacy, $fieldty: ty, $write: expr)) => {{ // no-op }}; + ($last_seen_type: expr, $typ: expr, $type: expr, $field: ident, (custom, $fieldty: ty, $read: expr, $write: expr) $(, $self: ident)?) => {{ + // Note that $type may be 0 making the second comparison always false + #[allow(unused_comparisons)] + let invalid_order = + ($last_seen_type.is_none() || $last_seen_type.unwrap() < $type) && $typ.0 > $type; + if invalid_order { + let read_result: Result<_, DecodeError> = $read(None); + $field = read_result?.into(); + } + }}; ($last_seen_type: expr, $typ: expr, $type: expr, $field: ident, (required, explicit_type: $fieldty: ty)) => {{ _check_decoded_tlv_order!($last_seen_type, $typ, $type, $field, required); }}; @@ -385,6 +401,15 @@ macro_rules! _check_missing_tlv { ($last_seen_type: expr, $type: expr, $field: ident, (legacy, $fieldty: ty, $write: expr)) => {{ // no-op }}; + ($last_seen_type: expr, $type: expr, $field: ident, (custom, $fieldty: ty, $read: expr, $write: expr)) => {{ + // Note that $type may be 0 making the second comparison always false + #[allow(unused_comparisons)] + let missing_req_type = $last_seen_type.is_none() || $last_seen_type.unwrap() < $type; + if missing_req_type { + let read_result: Result<_, DecodeError> = $read(None); + $field = read_result?.into(); + } + }}; ($last_seen_type: expr, $type: expr, $field: ident, (required, explicit_type: $fieldty: ty)) => {{ _check_missing_tlv!($last_seen_type, $type, $field, required); }}; @@ -441,6 +466,12 @@ macro_rules! _decode_tlv { ($outer_reader: expr, $reader: expr, $field: ident, (legacy, $fieldty: ty, $write: expr)) => {{ $crate::_decode_tlv!($outer_reader, $reader, $field, (option, explicit_type: $fieldty)); }}; + ($outer_reader: expr, $reader: expr, $field: ident, (custom, $fieldty: ty, $read: expr, $write: expr)) => {{ + let read_field: $fieldty; + $crate::_decode_tlv!($outer_reader, $reader, read_field, required); + let read_result: Result<_, DecodeError> = $read(Some(read_field)); + $field = read_result?.into(); + }}; ($outer_reader: expr, $reader: expr, $field: ident, (required, explicit_type: $fieldty: ty)) => {{ let _field: &$fieldty = &$field; _decode_tlv!($outer_reader, $reader, $field, required); @@ -830,6 +861,9 @@ macro_rules! _init_tlv_based_struct_field { ($field: ident, (legacy, $fieldty: ty, $write: expr)) => { $crate::_init_tlv_based_struct_field!($field, option) }; + ($field: ident, (custom, $fieldty: ty, $read: expr, $write: expr)) => { + $crate::_init_tlv_based_struct_field!($field, required) + }; ($field: ident, (option: $trait: ident $(, $read_arg: expr)?)) => { $crate::_init_tlv_based_struct_field!($field, option) }; @@ -896,6 +930,9 @@ macro_rules! _init_tlv_field_var { ($field: ident, (legacy, $fieldty: ty, $write: expr)) => { $crate::_init_tlv_field_var!($field, (option, explicit_type: $fieldty)); }; + ($field: ident, (custom, $fieldty: ty, $read: expr, $write: expr)) => { + $crate::_init_tlv_field_var!($field, required); + }; ($field: ident, (required, explicit_type: $fieldty: ty)) => { let mut $field = $crate::util::ser::RequiredWrapper::<$fieldty>(None); }; @@ -979,6 +1016,12 @@ macro_rules! _decode_and_build { /// called with the object being serialized and a returned `Option` and is written as a TLV if /// `Some`. When reading, an optional field of type `$ty` is read (which can be used in later /// `default_value` or `static_value` fields by referring to the value by name). +/// If `$fieldty` is `(custom, $ty, $read, $write)` then, when writing, the same behavior as +/// `legacy`, above is used. When reading, if a TLV is present, it is read as `$ty` and the +/// `$read` method is called with `Some(decoded_$ty_object)`. If no TLV is present, the field +/// will be initialized by calling `$read(None)`. `$read` should return a +/// `Result` (note that the processed field type may differ from `$ty`; +/// `$ty` is the type as de/serialized, not necessarily the actual field type). /// /// For example, /// ``` From ae4d4cf20c932eec4b82d7e010c3a1cb2d0d568a Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Sun, 1 Feb 2026 12:29:08 +0000 Subject: [PATCH 06/68] 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. --- lightning/src/ln/chanmon_update_fail_tests.rs | 5 +- lightning/src/ln/channelmanager.rs | 1 + lightning/src/ln/functional_tests.rs | 3 +- lightning/src/ln/outbound_payment.rs | 101 ++++++++++++++---- lightning/src/ln/payment_tests.rs | 4 + 5 files changed, 92 insertions(+), 22 deletions(-) diff --git a/lightning/src/ln/chanmon_update_fail_tests.rs b/lightning/src/ln/chanmon_update_fail_tests.rs index 96ac147635d..f3eda9b38ac 100644 --- a/lightning/src/ln/chanmon_update_fail_tests.rs +++ b/lightning/src/ln/chanmon_update_fail_tests.rs @@ -2311,6 +2311,7 @@ fn test_path_paused_mpp() { route.paths[1].hops[0].pubkey = node_c_id; route.paths[1].hops[0].short_channel_id = chan_2_ann.contents.short_channel_id; route.paths[1].hops[1].short_channel_id = chan_4_id; + route.route_params.as_mut().unwrap().final_value_msat *= 2; // Set it so that the first monitor update (for the path 0 -> 1 -> 3) succeeds, but the second // (for the path 0 -> 2 -> 3) fails. @@ -4244,7 +4245,7 @@ fn do_test_partial_claim_mon_update_compl_actions(reload_a: bool, reload_b: bool let chan_4_scid = chan_4_update.contents.short_channel_id; let (mut route, payment_hash, preimage, payment_secret) = - get_route_and_payment_hash!(&nodes[0], nodes[3], 100000); + get_route_and_payment_hash!(&nodes[0], nodes[3], 100_000); let path = route.paths[0].clone(); route.paths.push(path); route.paths[0].hops[0].pubkey = node_b_id; @@ -4253,6 +4254,8 @@ fn do_test_partial_claim_mon_update_compl_actions(reload_a: bool, reload_b: bool route.paths[1].hops[0].pubkey = node_c_id; route.paths[1].hops[0].short_channel_id = chan_2_scid; route.paths[1].hops[1].short_channel_id = chan_4_scid; + route.route_params.as_mut().unwrap().final_value_msat *= 2; + let paths = &[&[&nodes[1], &nodes[3]][..], &[&nodes[2], &nodes[3]][..]]; send_along_route_with_secret(&nodes[0], route, paths, 200_000, payment_hash, payment_secret); diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index c7fd4823d5c..a733cae11d5 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -19963,6 +19963,7 @@ mod tests { route.paths[1].hops[0].pubkey = nodes[2].node.get_our_node_id(); route.paths[1].hops[0].short_channel_id = chan_2_id; route.paths[1].hops[1].short_channel_id = chan_4_id; + route.route_params.as_mut().unwrap().final_value_msat *= 2; nodes[0].node.send_payment_with_route(route, payment_hash, RecipientOnionFields::spontaneous_empty(), PaymentId(payment_hash.0)).unwrap(); diff --git a/lightning/src/ln/functional_tests.rs b/lightning/src/ln/functional_tests.rs index 8e854b31150..21c2af696c1 100644 --- a/lightning/src/ln/functional_tests.rs +++ b/lightning/src/ln/functional_tests.rs @@ -7104,7 +7104,7 @@ pub fn test_simple_mpp() { let chan_4_id = create_announced_chan_between_nodes(&nodes, 2, 3).0.contents.short_channel_id; let (mut route, payment_hash, payment_preimage, payment_secret) = - get_route_and_payment_hash!(&nodes[0], nodes[3], 100000); + get_route_and_payment_hash!(&nodes[0], nodes[3], 100_000); let path = route.paths[0].clone(); route.paths.push(path); route.paths[0].hops[0].pubkey = node_b_id; @@ -7113,6 +7113,7 @@ pub fn test_simple_mpp() { route.paths[1].hops[0].pubkey = node_c_id; route.paths[1].hops[0].short_channel_id = chan_2_id; route.paths[1].hops[1].short_channel_id = chan_4_id; + route.route_params.as_mut().unwrap().final_value_msat = 200_000; let paths: &[&[_]] = &[&[&nodes[1], &nodes[3]], &[&nodes[2], &nodes[3]]]; send_along_route_with_secret(&nodes[0], route, paths, 200_000, payment_hash, payment_secret); claim_payment_along_route(ClaimAlongRouteArgs::new(&nodes[0], paths, payment_preimage)); diff --git a/lightning/src/ln/outbound_payment.rs b/lightning/src/ln/outbound_payment.rs index ea33bb5d263..85f2c99e879 100644 --- a/lightning/src/ln/outbound_payment.rs +++ b/lightning/src/ln/outbound_payment.rs @@ -894,6 +894,80 @@ impl OutboundPayments { } } +/// Validate that a [`Route`] picked by our [`Router`] is sane for the [`RouteParameters`] used to +/// request it. Failure here indicates a critical bug in the [`Router`]. +fn validate_found_route( + route: &mut Route, route_params: &RouteParameters, logger: &WithContext, +) -> Result<(), ()> { + if route.route_params.as_ref() != Some(route_params) { + debug_assert!( + false, + "Routers are expected to return a Route which includes the requested RouteParameters. Got {:?}, expected {route_params:?}", + route.route_params + ); + log_error!( + logger, + "Routers are expected to return a Route which includes the requested RouteParameters. Got {:?}, expected {route_params:?}", + route.route_params + ); + route.route_params = Some(route_params.clone()); + } + + // Check that the path returned actually pays less than the max fee we set. + if let Some(max_total_fee) = route_params.max_total_routing_fee_msat { + let payment_amount = route.get_total_amount(); + let max_payment_amount = route_params.final_value_msat.saturating_add(max_total_fee); + if payment_amount > max_payment_amount { + debug_assert!( + false, + "Router returned an attempt to pay more ({payment_amount}msat) than the limit we gave it ({max_payment_amount}msat). Your router is critically buggy!" + ); + log_error!( + logger, + "Router returned an attempt to pay more ({payment_amount}msat) than the limit we gave it ({max_payment_amount}msat). Your router is critically buggy!" + ); + return Err(()); + } + } + + // Test that the route doesn't contain any "extra" MPP parts - while we're allows to + // overshoot the `final_value_msat` specified in the `route_params`, we aren't allowed to + // have any MPP parts which aren't needed to meet `route_params.final_value_msat`. + let min_mpp_part = route.paths.iter().map(|h| h.final_value_msat()).min().unwrap_or(0); + if route.get_total_amount() - min_mpp_part >= route_params.final_value_msat { + debug_assert!( + false, + "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!", + route_params.final_value_msat + ); + log_error!( + logger, + "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!", + route_params.final_value_msat + ); + return Err(()); + } + + if route.paths.is_empty() { + debug_assert!(false, "Selected route had no paths"); + log_error!(logger, "Selected route had no paths, your router is buggy!"); + return Err(()); + } + + for path in route.paths.iter() { + if path.hops.is_empty() { + debug_assert!(false, "Unusable path in route (path.hops.len() must be at least 1)"); + log_error!( + logger, + "Unusable path in route (path.hops.len() must be at least 1). Your router is buggy!" + ); + return Err(()); + } + } + + Ok(()) +} + impl OutboundPayments { #[rustfmt::skip] pub(super) fn send_payment( @@ -1462,12 +1536,8 @@ impl OutboundPayments { RetryableSendFailure::RouteNotFound })?; - if route.route_params.as_ref() != Some(route_params) { - debug_assert!(false, - "Routers are expected to return a Route which includes the requested RouteParameters. Got {:?}, expected {:?}", - route.route_params, route_params); - route.route_params = Some(route_params.clone()); - } + validate_found_route(&mut route, route_params, logger) + .map_err(|()| RetryableSendFailure::RouteNotFound)?; Ok(route) } @@ -1552,18 +1622,9 @@ impl OutboundPayments { } }; - if route.route_params.as_ref() != Some(&route_params) { - debug_assert!(false, - "Routers are expected to return a Route which includes the requested RouteParameters"); - route.route_params = Some(route_params.clone()); - } - - for path in route.paths.iter() { - if path.hops.len() == 0 { - log_error!(logger, "Unusable path in route (path.hops.len() must be at least 1"); - self.abandon_payment(payment_id, PaymentFailureReason::UnexpectedError, pending_events); - return - } + if validate_found_route(&mut route, &route_params, logger).is_err() { + self.abandon_payment(payment_id, PaymentFailureReason::RouteNotFound, pending_events); + return } macro_rules! abandon_with_entry { @@ -2967,7 +3028,7 @@ mod tests { let sender_pk = PublicKey::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap()); let receiver_pk = PublicKey::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[43; 32]).unwrap()); let payment_params = PaymentParameters::from_node_id(sender_pk, 0); - let route_params = RouteParameters::from_payment_params_and_value(payment_params.clone(), 0); + let route_params = RouteParameters::from_payment_params_and_value(payment_params.clone(), 1); let failed_scid = 42; let route = Route { paths: vec![Path { hops: vec![RouteHop { @@ -2975,7 +3036,7 @@ mod tests { node_features: NodeFeatures::empty(), short_channel_id: failed_scid, channel_features: ChannelFeatures::empty(), - fee_msat: 0, + fee_msat: 1, cltv_expiry_delta: 0, maybe_announced_channel: true, }], blinded_tail: None }], diff --git a/lightning/src/ln/payment_tests.rs b/lightning/src/ln/payment_tests.rs index 2b3a2633205..e6c3c6eb121 100644 --- a/lightning/src/ln/payment_tests.rs +++ b/lightning/src/ln/payment_tests.rs @@ -97,6 +97,8 @@ fn mpp_failure() { route.paths[1].hops[0].pubkey = node_c_id; route.paths[1].hops[0].short_channel_id = chan_2_id; route.paths[1].hops[1].short_channel_id = chan_4_id; + route.route_params.as_mut().unwrap().final_value_msat *= 2; + let paths: &[&[_]] = &[&[&nodes[1], &nodes[3]], &[&nodes[2], &nodes[3]]]; send_along_route_with_secret(&nodes[0], route, paths, 200_000, payment_hash, payment_secret); fail_payment_along_route(&nodes[0], paths, false, payment_hash); @@ -137,6 +139,7 @@ fn mpp_retry() { route.paths[1].hops[0].pubkey = node_c_id; route.paths[1].hops[0].short_channel_id = chan_2_update.contents.short_channel_id; route.paths[1].hops[1].short_channel_id = chan_4_update.contents.short_channel_id; + route.route_params.as_mut().unwrap().final_value_msat *= 2; // Initiate the MPP payment. let id = PaymentId(hash.0); @@ -360,6 +363,7 @@ fn do_mpp_receive_timeout(send_partial_mpp: bool) { route.paths[1].hops[0].pubkey = node_c_id; route.paths[1].hops[0].short_channel_id = chan_2_update.contents.short_channel_id; route.paths[1].hops[1].short_channel_id = chan_4_update.contents.short_channel_id; + route.route_params.as_mut().unwrap().final_value_msat *= 2; // Initiate the MPP payment. let onion = RecipientOnionFields::secret_only(payment_secret); From 142490607d28b1b465cdc6d7b785f254f9cffe71 Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Fri, 6 Feb 2026 13:44:42 +0000 Subject: [PATCH 07/68] f move validation into `Route`, also addressing other feedback --- lightning/src/ln/outbound_payment.rs | 52 +--------------------- lightning/src/routing/router.rs | 66 +++++++++++++++++++++++++++- 2 files changed, 65 insertions(+), 53 deletions(-) diff --git a/lightning/src/ln/outbound_payment.rs b/lightning/src/ln/outbound_payment.rs index 85f2c99e879..3380e4f0120 100644 --- a/lightning/src/ln/outbound_payment.rs +++ b/lightning/src/ln/outbound_payment.rs @@ -913,57 +913,7 @@ fn validate_found_route( route.route_params = Some(route_params.clone()); } - // Check that the path returned actually pays less than the max fee we set. - if let Some(max_total_fee) = route_params.max_total_routing_fee_msat { - let payment_amount = route.get_total_amount(); - let max_payment_amount = route_params.final_value_msat.saturating_add(max_total_fee); - if payment_amount > max_payment_amount { - debug_assert!( - false, - "Router returned an attempt to pay more ({payment_amount}msat) than the limit we gave it ({max_payment_amount}msat). Your router is critically buggy!" - ); - log_error!( - logger, - "Router returned an attempt to pay more ({payment_amount}msat) than the limit we gave it ({max_payment_amount}msat). Your router is critically buggy!" - ); - return Err(()); - } - } - - // Test that the route doesn't contain any "extra" MPP parts - while we're allows to - // overshoot the `final_value_msat` specified in the `route_params`, we aren't allowed to - // have any MPP parts which aren't needed to meet `route_params.final_value_msat`. - let min_mpp_part = route.paths.iter().map(|h| h.final_value_msat()).min().unwrap_or(0); - if route.get_total_amount() - min_mpp_part >= route_params.final_value_msat { - debug_assert!( - false, - "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!", - route_params.final_value_msat - ); - log_error!( - logger, - "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!", - route_params.final_value_msat - ); - return Err(()); - } - - if route.paths.is_empty() { - debug_assert!(false, "Selected route had no paths"); - log_error!(logger, "Selected route had no paths, your router is buggy!"); - return Err(()); - } - - for path in route.paths.iter() { - if path.hops.is_empty() { - debug_assert!(false, "Unusable path in route (path.hops.len() must be at least 1)"); - log_error!( - logger, - "Unusable path in route (path.hops.len() must be at least 1). Your router is buggy!" - ); - return Err(()); - } - } + route.debug_assert_route_meets_params(logger)?; Ok(()) } diff --git a/lightning/src/routing/router.rs b/lightning/src/routing/router.rs index b27dee1a450..39ed9009194 100644 --- a/lightning/src/routing/router.rs +++ b/lightning/src/routing/router.rs @@ -633,7 +633,7 @@ impl Path { } } - /// Gets the final hop's CLTV expiry delta. + /// Gets the final hop's CLTV expiry delta, if there's a final non-blinded hop. #[rustfmt::skip] pub fn final_cltv_expiry_delta(&self) -> Option { match &self.blinded_tail { @@ -688,6 +688,66 @@ impl Route { pub fn get_total_amount(&self) -> u64 { self.paths.iter().map(|path| path.final_value_msat()).sum() } + + pub(crate) fn debug_assert_route_meets_params(&self, logger: L) -> Result<(), ()> { + if let Some(route_params) = self.route_params.as_ref() { + // Check that we actually pay less than the max fee we set. + if let Some(max_total_fee) = route_params.max_total_routing_fee_msat { + let total_fee = self.get_total_fees(); + if total_fee > max_total_fee { + 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!"); + debug_assert!(false, "{}", err); + log_error!(logger, "{}", err); + return Err(()); + } + } + + // Test that we don't contain any "extra" MPP parts - while we're allowed to overshoot + // the `final_value_msat` specified in the `route_params`, we aren't allowed to have + // any MPP parts which aren't needed to meet `route_params.final_value_msat`. + let min_mpp_part = self.paths.iter().map(|h| h.final_value_msat()).min().unwrap_or(0); + if self.get_total_amount() - min_mpp_part >= route_params.final_value_msat { + let err = format!( + "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!", + route_params.final_value_msat + ); + debug_assert!(false, "{}", err); + log_error!(logger, "{}", err); + return Err(()); + } + + if self.paths.is_empty() { + let err = "Selected route had no paths. Your router is buggy!"; + debug_assert!(false, "{}", err); + log_error!(logger, "{}", err); + return Err(()); + } + + for path in self.paths.iter() { + if path.hops.is_empty() { + let err = "Unusable path in route (path.hops.len() must be at least 1)"; + debug_assert!(false, "{}", err); + log_error!(logger, "{}", err); + return Err(()); + } + + if path.hops.len() > route_params.payment_params.max_path_length.into() { + let err = format!( + "Path had a length of {}, which is greater than the maximum we're allowed ({})", + path.hops.len(), + route_params.payment_params.max_path_length, + ); + #[cfg(any(test, feature = "_test_utils"))] + debug_assert!(false, "{}", err); + log_error!(logger, "{}", err); + // This is a bug, but there's not a material safety risk to making this + // payment, so we don't bother to error here. + } + } + } + + Ok(()) + } } impl fmt::Display for Route { @@ -2491,9 +2551,11 @@ pub fn find_route( scorer: &S, score_params: &S::ScoreParams, random_seed_bytes: &[u8; 32] ) -> Result { let graph_lock = network_graph.read_only(); - let mut route = get_route(our_node_pubkey, &route_params, &graph_lock, first_hops, logger, + let mut route = get_route(our_node_pubkey, &route_params, &graph_lock, first_hops, &logger, scorer, score_params, random_seed_bytes)?; add_random_cltv_offset(&mut route, &route_params.payment_params, &graph_lock, random_seed_bytes); + route.debug_assert_route_meets_params(&logger) + .map_err(|()| "Generated route doesn't comply with the parameters you specified. This indicates a bug in the router. Please report this bug!")?; Ok(route) } From 9be5e0cd2643349fc4b06022dc4695aefd783551 Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Sun, 1 Feb 2026 20:42:54 +0000 Subject: [PATCH 08/68] Include MPP payment amount in `RecipientOnionFields` In some uses of LDK we need the ability to send HTLCs for only a portion of some larger MPP payment. This allows payers to make single payments which spend funds from multiple wallets, which may be important for ecash wallets holding funds in multiple mints or graduated wallets which hold funds across a trusted wallet and a self-custodial wallet. In order to allow for this, we need to separate the concept of the payment amount from the onion MPP amount. Here we start this process by adding a `total_mpp_amount_msat` field to `RecipientOnionFields` (which is the appropriate place for a field describing something in the recipient onion). We currently always assert that it is equal to the existing fields, but will relax this in the coming commit(s). We also start including a payment preimage on probe attempts, which appears to have been the intent of the code, but which did not work correctly. The bulk of the test updates were done by Claude. --- fuzz/src/chanmon_consistency.rs | 4 +- fuzz/src/full_stack.rs | 4 +- lightning/src/chain/channelmonitor.rs | 2 +- lightning/src/events/mod.rs | 9 +- lightning/src/ln/accountable_tests.rs | 2 +- lightning/src/ln/async_payments_tests.rs | 2 +- lightning/src/ln/async_signer_tests.rs | 12 +- lightning/src/ln/blinded_payment_tests.rs | 64 +++++----- lightning/src/ln/chanmon_update_fail_tests.rs | 56 ++++----- lightning/src/ln/channelmanager.rs | 70 ++++++++--- lightning/src/ln/functional_test_utils.rs | 4 +- lightning/src/ln/functional_tests.rs | 105 +++++++++-------- lightning/src/ln/htlc_reserve_unit_tests.rs | 76 ++++++------ lightning/src/ln/interception_tests.rs | 2 +- lightning/src/ln/invoice_utils.rs | 5 +- .../src/ln/max_payment_path_len_tests.rs | 13 ++- lightning/src/ln/monitor_tests.rs | 8 +- lightning/src/ln/offers_tests.rs | 2 +- lightning/src/ln/onion_payment.rs | 2 +- lightning/src/ln/onion_route_tests.rs | 46 ++++---- lightning/src/ln/onion_utils.rs | 27 ++++- lightning/src/ln/outbound_payment.rs | 98 ++++++++++++---- lightning/src/ln/payment_tests.rs | 109 ++++++++++-------- lightning/src/ln/priv_short_conf_tests.rs | 18 +-- lightning/src/ln/quiescence_tests.rs | 8 +- lightning/src/ln/reload_tests.rs | 14 +-- lightning/src/ln/shutdown_tests.rs | 8 +- lightning/src/ln/splicing_tests.rs | 2 +- lightning/src/ln/update_fee_tests.rs | 8 +- 29 files changed, 461 insertions(+), 319 deletions(-) diff --git a/fuzz/src/chanmon_consistency.rs b/fuzz/src/chanmon_consistency.rs index 202488d9777..6d98301541f 100644 --- a/fuzz/src/chanmon_consistency.rs +++ b/fuzz/src/chanmon_consistency.rs @@ -585,7 +585,7 @@ fn send_payment( }], route_params: Some(route_params.clone()), }; - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, amt); let res = source.send_payment_with_route(route, payment_hash, onion, payment_id); match res { Err(err) => { @@ -642,7 +642,7 @@ fn send_hop_payment( }], route_params: Some(route_params.clone()), }; - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, amt); let res = source.send_payment_with_route(route, payment_hash, onion, payment_id); match res { Err(err) => { diff --git a/fuzz/src/full_stack.rs b/fuzz/src/full_stack.rs index 8c887ed623a..4dda79dfe90 100644 --- a/fuzz/src/full_stack.rs +++ b/fuzz/src/full_stack.rs @@ -741,7 +741,7 @@ pub fn do_test(mut data: &[u8], logger: &Arc) { payments_sent += 1; let _ = channelmanager.send_payment( payment_hash, - RecipientOnionFields::spontaneous_empty(), + RecipientOnionFields::spontaneous_empty(final_value_msat), PaymentId(payment_hash.0), params, Retry::Attempts(2), @@ -763,7 +763,7 @@ pub fn do_test(mut data: &[u8], logger: &Arc) { payments_sent += 1; let _ = channelmanager.send_payment( payment_hash, - RecipientOnionFields::secret_only(payment_secret), + RecipientOnionFields::secret_only(payment_secret, final_value_msat), PaymentId(payment_hash.0), params, Retry::Attempts(2), diff --git a/lightning/src/chain/channelmonitor.rs b/lightning/src/chain/channelmonitor.rs index a537ff55874..741ac2bf4ba 100644 --- a/lightning/src/chain/channelmonitor.rs +++ b/lightning/src/chain/channelmonitor.rs @@ -6845,7 +6845,7 @@ mod tests { // the update through to the ChannelMonitor which will refuse it (as the channel is closed). let (route, payment_hash, _, payment_secret) = get_route_and_payment_hash!(nodes[1], nodes[0], 100_000); nodes[1].node.send_payment_with_route(route, payment_hash, - RecipientOnionFields::secret_only(payment_secret), PaymentId(payment_hash.0) + RecipientOnionFields::secret_only(payment_secret, 100_000), PaymentId(payment_hash.0) ).unwrap(); check_added_monitors(&nodes[1], 1); diff --git a/lightning/src/events/mod.rs b/lightning/src/events/mod.rs index 3d860e9f363..74f2f34d4d8 100644 --- a/lightning/src/events/mod.rs +++ b/lightning/src/events/mod.rs @@ -41,8 +41,8 @@ use crate::types::payment::{PaymentHash, PaymentPreimage, PaymentSecret}; use crate::types::string::UntrustedString; use crate::util::errors::APIError; use crate::util::ser::{ - BigSize, FixedLengthReader, MaybeReadable, Readable, RequiredWrapper, UpgradableRequired, - WithoutLength, Writeable, Writer, + BigSize, FixedLengthReader, MaybeReadable, Readable, ReadableArgs, RequiredWrapper, + UpgradableRequired, WithoutLength, Writeable, Writer, }; use crate::io; @@ -2398,7 +2398,7 @@ impl MaybeReadable for Event { (6, _user_payment_id, option), (7, claim_deadline, option), (8, payment_preimage, option), - (9, onion_fields, option), + (9, onion_fields, (option: ReadableArgs, amount_msat)), (10, counterparty_skimmed_fee_msat_opt, option), (11, payment_context, option), (13, payment_id, option), @@ -2724,7 +2724,8 @@ impl MaybeReadable for Event { (4, amount_msat, required), (5, htlcs, optional_vec), (7, sender_intended_total_msat, option), - (9, onion_fields, option), + (9, onion_fields, (option: ReadableArgs, + sender_intended_total_msat.unwrap_or(amount_msat))), (11, payment_id, option), }); Ok(Some(Event::PaymentClaimed { diff --git a/lightning/src/ln/accountable_tests.rs b/lightning/src/ln/accountable_tests.rs index 16ca1425817..9b084423c24 100644 --- a/lightning/src/ln/accountable_tests.rs +++ b/lightning/src/ln/accountable_tests.rs @@ -31,7 +31,7 @@ fn test_accountable_forwarding_with_override( PaymentParameters::from_node_id(nodes[2].node.get_our_node_id(), TEST_FINAL_CLTV), 100_000, ); - let onion_fields = RecipientOnionFields::secret_only(payment_secret); + let onion_fields = RecipientOnionFields::secret_only(payment_secret, 100_000); let payment_id = PaymentId(payment_hash.0); nodes[0] .node diff --git a/lightning/src/ln/async_payments_tests.rs b/lightning/src/ln/async_payments_tests.rs index 528cec44c00..264b8131596 100644 --- a/lightning/src/ln/async_payments_tests.rs +++ b/lightning/src/ln/async_payments_tests.rs @@ -615,7 +615,7 @@ fn invalid_keysend_payment_secret() { .node .send_spontaneous_payment( Some(keysend_preimage), - RecipientOnionFields::spontaneous_empty(), + RecipientOnionFields::spontaneous_empty(amt_msat), PaymentId(keysend_preimage.0), route_params, Retry::Attempts(0), diff --git a/lightning/src/ln/async_signer_tests.rs b/lightning/src/ln/async_signer_tests.rs index ddf17907718..be0eb968e49 100644 --- a/lightning/src/ln/async_signer_tests.rs +++ b/lightning/src/ln/async_signer_tests.rs @@ -301,7 +301,7 @@ fn do_test_async_commitment_signature_for_commitment_signed_revoke_and_ack( let (route, our_payment_hash, _our_payment_preimage, our_payment_secret) = get_route_and_payment_hash!(src, dst, 8000000); - let recipient_fields = RecipientOnionFields::secret_only(our_payment_secret); + let recipient_fields = RecipientOnionFields::secret_only(our_payment_secret, 8000000); let payment_id = PaymentId(our_payment_hash.0); src.node .send_payment_with_route(route, our_payment_hash, recipient_fields, payment_id) @@ -528,7 +528,7 @@ fn do_test_async_raa_peer_disconnect( let (route, our_payment_hash, _our_payment_preimage, our_payment_secret) = get_route_and_payment_hash!(src, dst, 8000000); - let recipient_fields = RecipientOnionFields::secret_only(our_payment_secret); + let recipient_fields = RecipientOnionFields::secret_only(our_payment_secret, 8000000); let payment_id = PaymentId(our_payment_hash.0); src.node .send_payment_with_route(route, our_payment_hash, recipient_fields, payment_id) @@ -677,7 +677,7 @@ fn do_test_async_commitment_signature_peer_disconnect( let (route, our_payment_hash, _our_payment_preimage, our_payment_secret) = get_route_and_payment_hash!(src, dst, 8000000); - let recipient_fields = RecipientOnionFields::secret_only(our_payment_secret); + let recipient_fields = RecipientOnionFields::secret_only(our_payment_secret, 8000000); let payment_id = PaymentId(our_payment_hash.0); src.node .send_payment_with_route(route, our_payment_hash, recipient_fields, payment_id) @@ -812,7 +812,7 @@ fn do_test_async_commitment_signature_ordering(monitor_update_failure: bool) { // to the peer. let (route, payment_hash_2, payment_preimage_2, payment_secret_2) = get_route_and_payment_hash!(nodes[0], nodes[1], 1000000); - let recipient_fields = RecipientOnionFields::secret_only(payment_secret_2); + let recipient_fields = RecipientOnionFields::secret_only(payment_secret_2, 1000000); let payment_id = PaymentId(payment_hash_2.0); nodes[0] .node @@ -1352,14 +1352,14 @@ fn test_no_disconnect_while_async_revoke_and_ack_expecting_remote_commitment_sig // We'll send a payment from both nodes to each other. let (route1, payment_hash1, _, payment_secret1) = get_route_and_payment_hash!(&nodes[0], &nodes[1], payment_amount); - let onion1 = RecipientOnionFields::secret_only(payment_secret1); + let onion1 = RecipientOnionFields::secret_only(payment_secret1, payment_amount); let payment_id1 = PaymentId(payment_hash1.0); nodes[0].node.send_payment_with_route(route1, payment_hash1, onion1, payment_id1).unwrap(); check_added_monitors(&nodes[0], 1); let (route2, payment_hash2, _, payment_secret2) = get_route_and_payment_hash!(&nodes[1], &nodes[0], payment_amount); - let onion2 = RecipientOnionFields::secret_only(payment_secret2); + let onion2 = RecipientOnionFields::secret_only(payment_secret2, payment_amount); let payment_id2 = PaymentId(payment_hash2.0); nodes[1].node.send_payment_with_route(route2, payment_hash2, onion2, payment_id2).unwrap(); check_added_monitors(&nodes[1], 1); diff --git a/lightning/src/ln/blinded_payment_tests.rs b/lightning/src/ln/blinded_payment_tests.rs index d78b9dfa4f2..b00a4f360d8 100644 --- a/lightning/src/ln/blinded_payment_tests.rs +++ b/lightning/src/ln/blinded_payment_tests.rs @@ -187,7 +187,7 @@ fn do_one_hop_blinded_path(success: bool) { PaymentParameters::blinded(vec![blinded_path]), amt_msat, ); - nodes[0].node.send_payment(payment_hash, RecipientOnionFields::spontaneous_empty(), + nodes[0].node.send_payment(payment_hash, RecipientOnionFields::spontaneous_empty(amt_msat), PaymentId(payment_hash.0), route_params, Retry::Attempts(0)).unwrap(); check_added_monitors(&nodes[0], 1); pass_along_route(&nodes[0], &[&[&nodes[1]]], amt_msat, payment_hash, payment_secret); @@ -243,7 +243,7 @@ fn one_hop_blinded_path_with_dummy_hops() { .node .send_payment( payment_hash, - RecipientOnionFields::spontaneous_empty(), + RecipientOnionFields::spontaneous_empty(amt_msat), PaymentId(payment_hash.0), route_params, Retry::Attempts(0), @@ -307,7 +307,7 @@ fn mpp_to_one_hop_blinded_path() { PaymentParameters::blinded(vec![blinded_path]).with_bolt12_features(bolt12_features).unwrap(), amt_msat, ); - nodes[0].node.send_payment(payment_hash, RecipientOnionFields::spontaneous_empty(), PaymentId(payment_hash.0), route_params, Retry::Attempts(0)).unwrap(); + nodes[0].node.send_payment(payment_hash, RecipientOnionFields::spontaneous_empty(amt_msat), PaymentId(payment_hash.0), route_params, Retry::Attempts(0)).unwrap(); check_added_monitors(&nodes[0], 2); let expected_route: &[&[&Node]] = &[&[&nodes[1], &nodes[3]], &[&nodes[2], &nodes[3]]]; @@ -399,7 +399,7 @@ fn mpp_to_three_hop_blinded_paths() { RouteParameters::from_payment_params_and_value(pay_params, amt_msat) }; - nodes[0].node.send_payment(payment_hash, RecipientOnionFields::spontaneous_empty(), + nodes[0].node.send_payment(payment_hash, RecipientOnionFields::spontaneous_empty(amt_msat), PaymentId(payment_hash.0), route_params, Retry::Attempts(0)).unwrap(); check_added_monitors(&nodes[0], 2); @@ -464,7 +464,7 @@ fn do_forward_checks_failure(check: ForwardCheckFail, intro_fails: bool) { let route = get_route(&nodes[0], &route_params).unwrap(); node_cfgs[0].router.expect_find_route(route_params.clone(), Ok(route.clone())); - nodes[0].node.send_payment(payment_hash, RecipientOnionFields::spontaneous_empty(), PaymentId(payment_hash.0), route_params, Retry::Attempts(0)).unwrap(); + nodes[0].node.send_payment(payment_hash, RecipientOnionFields::spontaneous_empty(amt_msat), PaymentId(payment_hash.0), route_params, Retry::Attempts(0)).unwrap(); check_added_monitors(&nodes[0], 1); macro_rules! cause_error { @@ -474,7 +474,7 @@ fn do_forward_checks_failure(check: ForwardCheckFail, intro_fails: bool) { $update_add.cltv_expiry = 10; // causes outbound CLTV expiry to underflow }, ForwardCheckFail::ForwardPayloadEncodedAsReceive => { - let recipient_onion_fields = RecipientOnionFields::spontaneous_empty(); + let recipient_onion_fields = RecipientOnionFields::spontaneous_empty(amt_msat); let session_priv = SecretKey::from_slice(&[3; 32]).unwrap(); let mut onion_keys = onion_utils::construct_onion_keys(&Secp256k1::new(), &route.paths[0], &session_priv); let cur_height = nodes[0].best_block_info().1; @@ -594,7 +594,7 @@ fn failed_backwards_to_intro_node() { nodes.iter().skip(1).map(|n| n.node.get_our_node_id()).collect(), &[&chan_upd_1_2], &chanmon_cfgs[2].keys_manager); - nodes[0].node.send_payment(payment_hash, RecipientOnionFields::spontaneous_empty(), PaymentId(payment_hash.0), route_params, Retry::Attempts(0)).unwrap(); + nodes[0].node.send_payment(payment_hash, RecipientOnionFields::spontaneous_empty(amt_msat), PaymentId(payment_hash.0), route_params, Retry::Attempts(0)).unwrap(); check_added_monitors(&nodes[0], 1); let mut events = nodes[0].node.get_and_clear_pending_msg_events(); @@ -680,7 +680,7 @@ fn do_forward_fail_in_process_pending_htlc_fwds(check: ProcessPendingHTLCsCheck, nodes.iter().skip(1).map(|n| n.node.get_our_node_id()).collect(), &[&chan_upd_1_2, &chan_upd_2_3], &chanmon_cfgs[2].keys_manager); - nodes[0].node.send_payment(payment_hash, RecipientOnionFields::spontaneous_empty(), PaymentId(payment_hash.0), route_params, Retry::Attempts(0)).unwrap(); + nodes[0].node.send_payment(payment_hash, RecipientOnionFields::spontaneous_empty(amt_msat), PaymentId(payment_hash.0), route_params, Retry::Attempts(0)).unwrap(); check_added_monitors(&nodes[0], 1); let mut events = nodes[0].node.get_and_clear_pending_msg_events(); @@ -790,7 +790,7 @@ fn do_blinded_intercept_payment(intercept_node_fails: bool) { nodes.iter().skip(1).map(|n| n.node.get_our_node_id()).collect(), &[&intercept_chan_upd], &chanmon_cfgs[2].keys_manager); - nodes[0].node.send_payment(payment_hash, RecipientOnionFields::spontaneous_empty(), + nodes[0].node.send_payment(payment_hash, RecipientOnionFields::spontaneous_empty(amt_msat), PaymentId(payment_hash.0), route_params, Retry::Attempts(0)).unwrap(); check_added_monitors(&nodes[0], 1); let payment_event = { @@ -865,7 +865,7 @@ fn two_hop_blinded_path_success() { nodes.iter().skip(1).map(|n| n.node.get_our_node_id()).collect(), &[&chan_upd_1_2], &chanmon_cfgs[2].keys_manager); - nodes[0].node.send_payment(payment_hash, RecipientOnionFields::spontaneous_empty(), PaymentId(payment_hash.0), route_params, Retry::Attempts(0)).unwrap(); + nodes[0].node.send_payment(payment_hash, RecipientOnionFields::spontaneous_empty(amt_msat), PaymentId(payment_hash.0), route_params, Retry::Attempts(0)).unwrap(); check_added_monitors(&nodes[0], 1); pass_along_route(&nodes[0], &[&[&nodes[1], &nodes[2]]], amt_msat, payment_hash, payment_secret); claim_payment(&nodes[0], &[&nodes[1], &nodes[2]], payment_preimage); @@ -895,7 +895,7 @@ fn three_hop_blinded_path_success() { nodes.iter().skip(2).map(|n| n.node.get_our_node_id()).collect(), &[&chan_upd_2_3, &chan_upd_3_4], &chanmon_cfgs[4].keys_manager); - nodes[0].node.send_payment(payment_hash, RecipientOnionFields::spontaneous_empty(), PaymentId(payment_hash.0), route_params, Retry::Attempts(0)).unwrap(); + nodes[0].node.send_payment(payment_hash, RecipientOnionFields::spontaneous_empty(amt_msat), PaymentId(payment_hash.0), route_params, Retry::Attempts(0)).unwrap(); check_added_monitors(&nodes[0], 1); pass_along_route(&nodes[0], &[&[&nodes[1], &nodes[2], &nodes[3], &nodes[4]]], amt_msat, payment_hash, payment_secret); claim_payment(&nodes[0], &[&nodes[1], &nodes[2], &nodes[3], &nodes[4]], payment_preimage); @@ -920,7 +920,7 @@ fn three_hop_blinded_path_fail() { nodes.iter().skip(1).map(|n| n.node.get_our_node_id()).collect(), &[&chan_upd_1_2, &chan_upd_2_3], &chanmon_cfgs[3].keys_manager); - nodes[0].node.send_payment(payment_hash, RecipientOnionFields::spontaneous_empty(), PaymentId(payment_hash.0), route_params, Retry::Attempts(0)).unwrap(); + nodes[0].node.send_payment(payment_hash, RecipientOnionFields::spontaneous_empty(amt_msat), PaymentId(payment_hash.0), route_params, Retry::Attempts(0)).unwrap(); check_added_monitors(&nodes[0], 1); pass_along_route(&nodes[0], &[&[&nodes[1], &nodes[2], &nodes[3]]], amt_msat, payment_hash, payment_secret); @@ -1021,7 +1021,7 @@ fn do_multi_hop_receiver_fail(check: ReceiveCheckFail) { find_route(&nodes[0], &route_params).unwrap() }; node_cfgs[0].router.expect_find_route(route_params.clone(), Ok(route.clone())); - nodes[0].node.send_payment(payment_hash, RecipientOnionFields::spontaneous_empty(), PaymentId(payment_hash.0), route_params, Retry::Attempts(0)).unwrap(); + nodes[0].node.send_payment(payment_hash, RecipientOnionFields::spontaneous_empty(amt_msat), PaymentId(payment_hash.0), route_params, Retry::Attempts(0)).unwrap(); check_added_monitors(&nodes[0], 1); let mut payment_event_0_1 = { @@ -1064,7 +1064,7 @@ fn do_multi_hop_receiver_fail(check: ReceiveCheckFail) { 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); let cur_height = nodes[0].best_block_info().1; - let recipient_onion_fields = RecipientOnionFields::spontaneous_empty(); + let recipient_onion_fields = RecipientOnionFields::spontaneous_empty(amt_msat); let (mut onion_payloads, ..) = onion_utils::build_onion_payloads( &route.paths[0], amt_msat, &recipient_onion_fields, cur_height, &None, None, None).unwrap(); @@ -1210,7 +1210,7 @@ fn blinded_path_retries() { RouteParameters::from_payment_params_and_value(pay_params, amt_msat) }; - nodes[0].node.send_payment(payment_hash, RecipientOnionFields::spontaneous_empty(), PaymentId(payment_hash.0), route_params.clone(), Retry::Attempts(2)).unwrap(); + nodes[0].node.send_payment(payment_hash, RecipientOnionFields::spontaneous_empty(amt_msat), PaymentId(payment_hash.0), route_params.clone(), Retry::Attempts(2)).unwrap(); check_added_monitors(&nodes[0], 1); pass_along_route(&nodes[0], &[&[&nodes[1], &nodes[3]]], amt_msat, payment_hash, payment_secret); @@ -1309,7 +1309,7 @@ fn min_htlc() { assert_eq!(min_htlc_msat, route_params.payment_params.payee.blinded_route_hints()[0].payinfo.htlc_minimum_msat); - nodes[0].node.send_payment(payment_hash, RecipientOnionFields::spontaneous_empty(), PaymentId(payment_hash.0), route_params.clone(), Retry::Attempts(0)).unwrap(); + nodes[0].node.send_payment(payment_hash, RecipientOnionFields::spontaneous_empty(min_htlc_msat), PaymentId(payment_hash.0), route_params.clone(), Retry::Attempts(0)).unwrap(); check_added_monitors(&nodes[0], 1); pass_along_route(&nodes[0], &[&[&nodes[1], &nodes[2], &nodes[3]]], min_htlc_msat, payment_hash, payment_secret); claim_payment(&nodes[0], &[&nodes[1], &nodes[2], &nodes[3]], payment_preimage); @@ -1322,7 +1322,7 @@ fn min_htlc() { route_hints[0].payinfo.htlc_minimum_msat -= 1; } else { panic!() } route_params.final_value_msat -= 1; - nodes[0].node.send_payment(payment_hash, RecipientOnionFields::spontaneous_empty(), PaymentId(payment_hash.0), route_params, Retry::Attempts(0)).unwrap(); + nodes[0].node.send_payment(payment_hash, RecipientOnionFields::spontaneous_empty(route_params.final_value_msat), PaymentId(payment_hash.0), route_params, Retry::Attempts(0)).unwrap(); check_added_monitors(&nodes[0], 1); let mut payment_event_0_1 = { @@ -1387,7 +1387,7 @@ fn conditionally_round_fwd_amt() { &chanmon_cfgs[4].keys_manager); route_params.max_total_routing_fee_msat = None; - nodes[0].node.send_payment(payment_hash, RecipientOnionFields::spontaneous_empty(), PaymentId(payment_hash.0), route_params, Retry::Attempts(0)).unwrap(); + nodes[0].node.send_payment(payment_hash, RecipientOnionFields::spontaneous_empty(amt_msat), PaymentId(payment_hash.0), route_params, Retry::Attempts(0)).unwrap(); check_added_monitors(&nodes[0], 1); pass_along_route(&nodes[0], &[&[&nodes[1], &nodes[2], &nodes[3], &nodes[4]]], amt_msat, payment_hash, payment_secret); nodes[4].node.claim_funds(payment_preimage); @@ -1432,7 +1432,7 @@ fn custom_tlvs_to_blinded_path() { amt_msat, ); - let recipient_onion_fields = RecipientOnionFields::spontaneous_empty() + let recipient_onion_fields = RecipientOnionFields::spontaneous_empty(amt_msat) .with_custom_tlvs(RecipientCustomTlvs::new(vec![((1 << 16) + 1, vec![42, 42])]).unwrap()); nodes[0].node.send_payment(payment_hash, recipient_onion_fields.clone(), PaymentId(payment_hash.0), route_params, Retry::Attempts(0)).unwrap(); @@ -1487,7 +1487,7 @@ fn fails_receive_tlvs_authentication() { ); // Test authentication works normally. - nodes[0].node.send_payment(payment_hash, RecipientOnionFields::spontaneous_empty(), PaymentId(payment_hash.0), route_params, Retry::Attempts(0)).unwrap(); + nodes[0].node.send_payment(payment_hash, RecipientOnionFields::spontaneous_empty(amt_msat), PaymentId(payment_hash.0), route_params, Retry::Attempts(0)).unwrap(); check_added_monitors(&nodes[0], 1); pass_along_route(&nodes[0], &[&[&nodes[1]]], amt_msat, payment_hash, payment_secret); claim_payment(&nodes[0], &[&nodes[1]], payment_preimage); @@ -1517,7 +1517,7 @@ fn fails_receive_tlvs_authentication() { amt_msat, ); - nodes[0].node.send_payment(payment_hash, RecipientOnionFields::spontaneous_empty(), PaymentId(payment_hash.0), route_params, Retry::Attempts(0)).unwrap(); + nodes[0].node.send_payment(payment_hash, RecipientOnionFields::spontaneous_empty(amt_msat), PaymentId(payment_hash.0), route_params, Retry::Attempts(0)).unwrap(); check_added_monitors(&nodes[0], 1); let mut events = nodes[0].node.get_and_clear_pending_msg_events(); @@ -1574,7 +1574,7 @@ fn blinded_payment_path_padding() { let route_params = RouteParameters::from_payment_params_and_value(PaymentParameters::blinded(vec![blinded_path]), amt_msat); - nodes[0].node.send_payment(payment_hash, RecipientOnionFields::spontaneous_empty(), PaymentId(payment_hash.0), route_params, Retry::Attempts(0)).unwrap(); + nodes[0].node.send_payment(payment_hash, RecipientOnionFields::spontaneous_empty(amt_msat), PaymentId(payment_hash.0), route_params, Retry::Attempts(0)).unwrap(); check_added_monitors(&nodes[0], 1); pass_along_route(&nodes[0], &[&[&nodes[1], &nodes[2], &nodes[3], &nodes[4]]], amt_msat, payment_hash, payment_secret); claim_payment(&nodes[0], &[&nodes[1], &nodes[2], &nodes[3], &nodes[4]], payment_preimage); @@ -1681,7 +1681,7 @@ fn route_blinding_spec_test_vector() { }), }; let cur_height = 747_000; - let (bob_onion, _, _) = onion_utils::create_payment_onion(&secp_ctx, &path, &session_priv, amt_msat, &RecipientOnionFields::spontaneous_empty(), cur_height, &PaymentHash([0; 32]), &None, None, [0; 32]).unwrap(); + let (bob_onion, _, _) = onion_utils::create_payment_onion(&secp_ctx, &path, &session_priv, amt_msat, &RecipientOnionFields::spontaneous_empty(amt_msat), cur_height, &PaymentHash([0; 32]), &None, None, [0; 32]).unwrap(); struct TestEcdhSigner { node_secret: SecretKey, @@ -1904,7 +1904,7 @@ fn test_combined_trampoline_onion_creation_vectors() { let amt_msat = 150_000_000; let cur_height = 800_000; - let recipient_onion_fields = RecipientOnionFields::secret_only(payment_secret); + let recipient_onion_fields = RecipientOnionFields::secret_only(payment_secret, amt_msat); let (bob_onion, htlc_msat, htlc_cltv) = onion_utils::create_payment_onion_internal(&secp_ctx, &path, &outer_session_key, amt_msat, &recipient_onion_fields, cur_height, &associated_data, &None, None, outer_onion_prng_seed, Some(session_priv), Some([0; 32])).unwrap(); let outer_onion_packet_hex = bob_onion.encode().to_lower_hex_string(); @@ -1995,7 +1995,7 @@ fn test_trampoline_inbound_payment_decoding() { let amt_msat = 150_000_001; let cur_height = 800_001; - let recipient_onion_fields = RecipientOnionFields::secret_only(payment_secret); + let recipient_onion_fields = RecipientOnionFields::secret_only(payment_secret, amt_msat); let (bob_onion, _, _) = onion_utils::create_payment_onion(&secp_ctx, &path, &session_priv, amt_msat, &recipient_onion_fields, cur_height, &PaymentHash([0; 32]), &None, None, [0; 32]).unwrap(); struct TestEcdhSigner { @@ -2166,12 +2166,11 @@ fn test_trampoline_forward_payload_encoded_as_receive() { route_params: None, }; - nodes[0].node.send_payment_with_route(route.clone(), payment_hash, RecipientOnionFields::spontaneous_empty(), PaymentId(payment_hash.0)).unwrap(); + nodes[0].node.send_payment_with_route(route.clone(), payment_hash, RecipientOnionFields::spontaneous_empty(amt_msat), PaymentId(payment_hash.0)).unwrap(); check_added_monitors(&nodes[0], 1); let replacement_onion = { // create a substitute onion where the last Trampoline hop is a forward - let recipient_onion_fields = RecipientOnionFields::spontaneous_empty(); let mut blinded_tail = route.paths[0].blinded_tail.clone().unwrap(); @@ -2181,6 +2180,7 @@ fn test_trampoline_forward_payload_encoded_as_receive() { encrypted_payload: vec![], }); + let recipient_onion_fields = RecipientOnionFields::spontaneous_empty(amt_msat); let (mut trampoline_payloads, outer_total_msat, outer_starting_htlc_offset) = onion_utils::build_trampoline_onion_payloads(&blinded_tail, amt_msat, &recipient_onion_fields, 32, &None).unwrap(); // pop the last dummy hop @@ -2195,6 +2195,7 @@ fn test_trampoline_forward_payload_encoded_as_receive() { None, ).unwrap(); + let recipient_onion_fields = RecipientOnionFields::spontaneous_empty(outer_total_msat); let (outer_payloads, _, _) = onion_utils::build_onion_payloads(&route.paths[0], outer_total_msat, &recipient_onion_fields, outer_starting_htlc_offset, &None, None, Some(trampoline_packet)).unwrap(); let outer_onion_keys = onion_utils::construct_onion_keys(&secp_ctx, &route.clone().paths[0], &outer_session_priv); let outer_packet = onion_utils::construct_onion_packet( @@ -2331,7 +2332,7 @@ fn do_test_trampoline_single_hop_receive(success: bool) { route_params: None, }; - nodes[0].node.send_payment_with_route(route.clone(), payment_hash, RecipientOnionFields::spontaneous_empty(), PaymentId(payment_hash.0)).unwrap(); + nodes[0].node.send_payment_with_route(route.clone(), payment_hash, RecipientOnionFields::spontaneous_empty(amt_msat), PaymentId(payment_hash.0)).unwrap(); check_added_monitors(&nodes[0], 1); pass_along_route(&nodes[0], &[&[&nodes[1], &nodes[2]]], amt_msat, payment_hash, payment_secret); @@ -2477,7 +2478,7 @@ fn replacement_onion( ) -> msgs::OnionPacket { let outer_session_priv = SecretKey::from_slice(&override_random_bytes[..]).unwrap(); let trampoline_session_priv = onion_utils::compute_trampoline_session_priv(&outer_session_priv); - let recipient_onion_fields = RecipientOnionFields::spontaneous_empty(); + let recipient_onion_fields = RecipientOnionFields::spontaneous_empty(original_amt_msat); let blinded_tail = route.paths[0].blinded_tail.clone().unwrap(); @@ -2525,6 +2526,7 @@ fn replacement_onion( // Use a different session key to construct the replacement onion packet. Note that the // sender isn't aware of this and won't be able to decode the fulfill hold times. + let recipient_onion_fields = RecipientOnionFields::spontaneous_empty(outer_total_msat); let (mut outer_payloads, _, _) = onion_utils::build_onion_payloads( &route.paths[0], outer_total_msat, @@ -2650,7 +2652,7 @@ fn do_test_trampoline_relay(blinded: bool, test_case: TrampolineTestCase) { .send_payment_with_route( route.clone(), payment_hash, - RecipientOnionFields::spontaneous_empty(), + RecipientOnionFields::spontaneous_empty(original_amt_msat), PaymentId(payment_hash.0), ) .unwrap(); @@ -2832,7 +2834,7 @@ fn test_trampoline_forward_rejection() { route_params: None, }; - nodes[0].node.send_payment_with_route(route.clone(), payment_hash, RecipientOnionFields::spontaneous_empty(), PaymentId(payment_hash.0)).unwrap(); + nodes[0].node.send_payment_with_route(route.clone(), payment_hash, RecipientOnionFields::spontaneous_empty(amt_msat), PaymentId(payment_hash.0)).unwrap(); check_added_monitors(&nodes[0], 1); diff --git a/lightning/src/ln/chanmon_update_fail_tests.rs b/lightning/src/ln/chanmon_update_fail_tests.rs index f3eda9b38ac..ef10258933a 100644 --- a/lightning/src/ln/chanmon_update_fail_tests.rs +++ b/lightning/src/ln/chanmon_update_fail_tests.rs @@ -187,7 +187,7 @@ fn do_test_simple_monitor_temporary_update_fail(disconnect: bool) { chanmon_cfgs[0].persister.set_update_ret(ChannelMonitorUpdateStatus::InProgress); - let onion = RecipientOnionFields::secret_only(payment_secret_1); + let onion = RecipientOnionFields::secret_only(payment_secret_1, 1000000); let id = PaymentId(payment_hash_1.0); nodes[0].node.send_payment_with_route(route, payment_hash_1, onion, id).unwrap(); check_added_monitors(&nodes[0], 1); @@ -254,7 +254,7 @@ fn do_test_simple_monitor_temporary_update_fail(disconnect: bool) { get_route_and_payment_hash!(&nodes[0], nodes[1], 1000000); chanmon_cfgs[0].persister.set_update_ret(ChannelMonitorUpdateStatus::InProgress); - let onion = RecipientOnionFields::secret_only(payment_secret_2); + let onion = RecipientOnionFields::secret_only(payment_secret_2, 1000000); let id = PaymentId(payment_hash_2.0); nodes[0].node.send_payment_with_route(route, payment_hash_2, onion, id).unwrap(); check_added_monitors(&nodes[0], 1); @@ -330,7 +330,7 @@ fn do_test_monitor_temporary_update_fail(disconnect_count: usize) { let (route, payment_hash_2, payment_preimage_2, payment_secret_2) = get_route_and_payment_hash!(nodes[0], nodes[1], 1000000); chanmon_cfgs[0].persister.set_update_ret(ChannelMonitorUpdateStatus::InProgress); - let onion = RecipientOnionFields::secret_only(payment_secret_2); + let onion = RecipientOnionFields::secret_only(payment_secret_2, 1000000); let id = PaymentId(payment_hash_2.0); nodes[0].node.send_payment_with_route(route, payment_hash_2, onion, id).unwrap(); check_added_monitors(&nodes[0], 1); @@ -735,7 +735,7 @@ fn test_monitor_update_fail_cs() { let (route, our_payment_hash, payment_preimage, our_payment_secret) = get_route_and_payment_hash!(nodes[0], nodes[1], 1000000); - let onion = RecipientOnionFields::secret_only(our_payment_secret); + let onion = RecipientOnionFields::secret_only(our_payment_secret, 1000000); let id = PaymentId(our_payment_hash.0); nodes[0].node.send_payment_with_route(route, our_payment_hash, onion, id).unwrap(); check_added_monitors(&nodes[0], 1); @@ -843,7 +843,7 @@ fn test_monitor_update_fail_no_rebroadcast() { let (route, our_payment_hash, payment_preimage_1, payment_secret_1) = get_route_and_payment_hash!(nodes[0], nodes[1], 1000000); - let onion = RecipientOnionFields::secret_only(payment_secret_1); + let onion = RecipientOnionFields::secret_only(payment_secret_1, 1000000); let id = PaymentId(our_payment_hash.0); nodes[0].node.send_payment_with_route(route, our_payment_hash, onion, id).unwrap(); check_added_monitors(&nodes[0], 1); @@ -897,7 +897,7 @@ fn test_monitor_update_raa_while_paused() { send_payment(&nodes[0], &[&nodes[1]], 5000000); let (route, our_payment_hash_1, payment_preimage_1, our_payment_secret_1) = get_route_and_payment_hash!(nodes[0], nodes[1], 1000000); - let onion = RecipientOnionFields::secret_only(our_payment_secret_1); + let onion = RecipientOnionFields::secret_only(our_payment_secret_1, 1000000); let id = PaymentId(our_payment_hash_1.0); nodes[0].node.send_payment_with_route(route, our_payment_hash_1, onion, id).unwrap(); @@ -907,7 +907,7 @@ fn test_monitor_update_raa_while_paused() { let (route, our_payment_hash_2, payment_preimage_2, our_payment_secret_2) = get_route_and_payment_hash!(nodes[1], nodes[0], 1000000); - let onion_2 = RecipientOnionFields::secret_only(our_payment_secret_2); + let onion_2 = RecipientOnionFields::secret_only(our_payment_secret_2, 1000000); let id_2 = PaymentId(our_payment_hash_2.0); nodes[1].node.send_payment_with_route(route, our_payment_hash_2, onion_2, id_2).unwrap(); @@ -1008,7 +1008,7 @@ fn do_test_monitor_update_fail_raa(test_ignore_second_cs: bool) { // holding cell. let (route, payment_hash_2, payment_preimage_2, payment_secret_2) = get_route_and_payment_hash!(nodes[0], nodes[2], 1000000); - let onion_2 = RecipientOnionFields::secret_only(payment_secret_2); + let onion_2 = RecipientOnionFields::secret_only(payment_secret_2, 1000000); let id_2 = PaymentId(payment_hash_2.0); nodes[0].node.send_payment_with_route(route, payment_hash_2, onion_2, id_2).unwrap(); check_added_monitors(&nodes[0], 1); @@ -1034,7 +1034,7 @@ fn do_test_monitor_update_fail_raa(test_ignore_second_cs: bool) { // being paused waiting a monitor update. let (route, payment_hash_3, _, payment_secret_3) = get_route_and_payment_hash!(nodes[0], nodes[2], 1000000); - let onion_3 = RecipientOnionFields::secret_only(payment_secret_3); + let onion_3 = RecipientOnionFields::secret_only(payment_secret_3, 1000000); let id_3 = PaymentId(payment_hash_3.0); nodes[0].node.send_payment_with_route(route, payment_hash_3, onion_3, id_3).unwrap(); check_added_monitors(&nodes[0], 1); @@ -1055,7 +1055,7 @@ fn do_test_monitor_update_fail_raa(test_ignore_second_cs: bool) { // Try to route another payment backwards from 2 to make sure 1 holds off on responding let (route, payment_hash_4, payment_preimage_4, payment_secret_4) = get_route_and_payment_hash!(nodes[2], nodes[0], 1000000); - let onion_4 = RecipientOnionFields::secret_only(payment_secret_4); + let onion_4 = RecipientOnionFields::secret_only(payment_secret_4, 1000000); let id_4 = PaymentId(payment_hash_4.0); nodes[2].node.send_payment_with_route(route, payment_hash_4, onion_4, id_4).unwrap(); check_added_monitors(&nodes[2], 1); @@ -1391,11 +1391,11 @@ fn raa_no_response_awaiting_raa_state() { // immediately after a CS. By setting failing the monitor update failure from the CS (which // requires only an RAA response due to AwaitingRAA) we can deliver the RAA and require the CS // generation during RAA while in monitor-update-failed state. - let onion_1 = RecipientOnionFields::secret_only(payment_secret_1); + let onion_1 = RecipientOnionFields::secret_only(payment_secret_1, 1000000); let id_1 = PaymentId(payment_hash_1.0); nodes[0].node.send_payment_with_route(route.clone(), payment_hash_1, onion_1, id_1).unwrap(); check_added_monitors(&nodes[0], 1); - let onion_2 = RecipientOnionFields::secret_only(payment_secret_2); + let onion_2 = RecipientOnionFields::secret_only(payment_secret_2, 1000000); let id_2 = PaymentId(payment_hash_2.0); nodes[0].node.send_payment_with_route(route.clone(), payment_hash_2, onion_2, id_2).unwrap(); check_added_monitors(&nodes[0], 0); @@ -1444,7 +1444,7 @@ fn raa_no_response_awaiting_raa_state() { // We send a third payment here, which is somewhat of a redundant test, but the // chanmon_fail_consistency test required it to actually find the bug (by seeing out-of-sync // commitment transaction states) whereas here we can explicitly check for it. - let onion_3 = RecipientOnionFields::secret_only(payment_secret_3); + let onion_3 = RecipientOnionFields::secret_only(payment_secret_3, 1000000); let id_3 = PaymentId(payment_hash_3.0); nodes[0].node.send_payment_with_route(route, payment_hash_3, onion_3, id_3).unwrap(); check_added_monitors(&nodes[0], 0); @@ -1546,7 +1546,7 @@ fn claim_while_disconnected_monitor_update_fail() { // the monitor still failed let (route, payment_hash_2, payment_preimage_2, payment_secret_2) = get_route_and_payment_hash!(nodes[0], nodes[1], 1000000); - let onion_2 = RecipientOnionFields::secret_only(payment_secret_2); + let onion_2 = RecipientOnionFields::secret_only(payment_secret_2, 1000000); let id_2 = PaymentId(payment_hash_2.0); nodes[0].node.send_payment_with_route(route, payment_hash_2, onion_2, id_2).unwrap(); check_added_monitors(&nodes[0], 1); @@ -1653,7 +1653,7 @@ fn monitor_failed_no_reestablish_response() { // on receipt). let (route, payment_hash_1, payment_preimage_1, payment_secret_1) = get_route_and_payment_hash!(nodes[0], nodes[1], 1000000); - let onion = RecipientOnionFields::secret_only(payment_secret_1); + let onion = RecipientOnionFields::secret_only(payment_secret_1, 1000000); let id = PaymentId(payment_hash_1.0); nodes[0].node.send_payment_with_route(route, payment_hash_1, onion, id).unwrap(); check_added_monitors(&nodes[0], 1); @@ -1737,7 +1737,7 @@ fn first_message_on_recv_ordering() { // can deliver it and fail the monitor update. let (route, payment_hash_1, payment_preimage_1, payment_secret_1) = get_route_and_payment_hash!(nodes[0], nodes[1], 1000000); - let onion_1 = RecipientOnionFields::secret_only(payment_secret_1); + let onion_1 = RecipientOnionFields::secret_only(payment_secret_1, 1000000); let id_1 = PaymentId(payment_hash_1.0); nodes[0].node.send_payment_with_route(route, payment_hash_1, onion_1, id_1).unwrap(); check_added_monitors(&nodes[0], 1); @@ -1761,7 +1761,7 @@ fn first_message_on_recv_ordering() { // Route the second payment, generating an update_add_htlc/commitment_signed let (route, payment_hash_2, payment_preimage_2, payment_secret_2) = get_route_and_payment_hash!(nodes[0], nodes[1], 1000000); - let onion_2 = RecipientOnionFields::secret_only(payment_secret_2); + let onion_2 = RecipientOnionFields::secret_only(payment_secret_2, 1000000); let id_2 = PaymentId(payment_hash_2.0); nodes[0].node.send_payment_with_route(route, payment_hash_2, onion_2, id_2).unwrap(); @@ -1854,7 +1854,7 @@ fn test_monitor_update_fail_claim() { let (route, payment_hash_2, _, payment_secret_2) = get_route_and_payment_hash!(nodes[2], nodes[0], 1_000_000); - let onion_2 = RecipientOnionFields::secret_only(payment_secret_2); + let onion_2 = RecipientOnionFields::secret_only(payment_secret_2, 1_000_000); let id_2 = PaymentId(payment_hash_2.0); nodes[2].node.send_payment_with_route(route.clone(), payment_hash_2, onion_2, id_2).unwrap(); check_added_monitors(&nodes[2], 1); @@ -1874,7 +1874,7 @@ fn test_monitor_update_fail_claim() { let (_, payment_hash_3, payment_secret_3) = get_payment_preimage_hash!(nodes[0]); let id_3 = PaymentId(payment_hash_3.0); - let onion_3 = RecipientOnionFields::secret_only(payment_secret_3); + let onion_3 = RecipientOnionFields::secret_only(payment_secret_3, 1_000_000); nodes[2].node.send_payment_with_route(route, payment_hash_3, onion_3, id_3).unwrap(); check_added_monitors(&nodes[2], 1); @@ -1998,7 +1998,7 @@ fn test_monitor_update_on_pending_forwards() { let (route, payment_hash_2, payment_preimage_2, payment_secret_2) = get_route_and_payment_hash!(nodes[2], nodes[0], 1000000); - let onion = RecipientOnionFields::secret_only(payment_secret_2); + let onion = RecipientOnionFields::secret_only(payment_secret_2, 1000000); let id = PaymentId(payment_hash_2.0); nodes[2].node.send_payment_with_route(route, payment_hash_2, onion, id).unwrap(); check_added_monitors(&nodes[2], 1); @@ -2069,7 +2069,7 @@ fn monitor_update_claim_fail_no_response() { // Now start forwarding a second payment, skipping the last RAA so B is in AwaitingRAA let (route, payment_hash_2, payment_preimage_2, payment_secret_2) = get_route_and_payment_hash!(nodes[0], nodes[1], 1000000); - let onion = RecipientOnionFields::secret_only(payment_secret_2); + let onion = RecipientOnionFields::secret_only(payment_secret_2, 1000000); let id = PaymentId(payment_hash_2.0); nodes[0].node.send_payment_with_route(route, payment_hash_2, onion, id).unwrap(); check_added_monitors(&nodes[0], 1); @@ -2319,7 +2319,7 @@ fn test_path_paused_mpp() { chanmon_cfgs[0].persister.set_update_ret(ChannelMonitorUpdateStatus::InProgress); // The first path should have succeeded with the second getting a MonitorUpdateInProgress err. - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, 200000); let id = PaymentId(payment_hash.0); nodes[0].node.send_payment_with_route(route, payment_hash, onion, id).unwrap(); check_added_monitors(&nodes[0], 2); @@ -2375,7 +2375,7 @@ fn test_pending_update_fee_ack_on_reconnect() { let (route, payment_hash, payment_preimage, payment_secret) = get_route_and_payment_hash!(&nodes[1], nodes[0], 1_000_000); - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, 1_000_000); let id = PaymentId(payment_hash.0); nodes[1].node.send_payment_with_route(route, payment_hash, onion, id).unwrap(); check_added_monitors(&nodes[1], 1); @@ -2690,14 +2690,14 @@ fn do_channel_holding_cell_serialize(disconnect: bool, reload_a: bool) { // (c) will not be freed from the holding cell. let (payment_preimage_0, payment_hash_0, ..) = route_payment(&nodes[1], &[&nodes[0]], 100_000); - let onion_1 = RecipientOnionFields::secret_only(payment_secret_1); + let onion_1 = RecipientOnionFields::secret_only(payment_secret_1, 100000); let id_1 = PaymentId(payment_hash_1.0); nodes[0].node.send_payment_with_route(route.clone(), payment_hash_1, onion_1, id_1).unwrap(); check_added_monitors(&nodes[0], 1); let send = SendEvent::from_node(&nodes[0]); assert_eq!(send.msgs.len(), 1); - let onion_2 = RecipientOnionFields::secret_only(payment_secret_2); + let onion_2 = RecipientOnionFields::secret_only(payment_secret_2, 100000); let id_2 = PaymentId(payment_hash_2.0); nodes[0].node.send_payment_with_route(route, payment_hash_2, onion_2, id_2).unwrap(); check_added_monitors(&nodes[0], 0); @@ -2874,7 +2874,7 @@ fn do_test_reconnect_dup_htlc_claims(htlc_status: HTLCStatusAtDupClaim, second_f // awaiting a remote revoke_and_ack from nodes[0]. let (route, second_payment_hash, _, second_payment_secret) = get_route_and_payment_hash!(nodes[0], nodes[1], 100_000); - let onion_2 = RecipientOnionFields::secret_only(second_payment_secret); + let onion_2 = RecipientOnionFields::secret_only(second_payment_secret, 100_000); let id_2 = PaymentId(second_payment_hash.0); nodes[0].node.send_payment_with_route(route, second_payment_hash, onion_2, id_2).unwrap(); check_added_monitors(&nodes[0], 1); @@ -4147,7 +4147,7 @@ fn do_test_glacial_peer_cant_hang(hold_chan_a: bool) { // With the A<->B preimage persistence not yet complete, the B<->C channel is stuck // waiting. - let onion_2 = RecipientOnionFields::secret_only(payment_secret_2); + let onion_2 = RecipientOnionFields::secret_only(payment_secret_2, 1_000_000); let id_2 = PaymentId(payment_hash_2.0); nodes[1].node.send_payment_with_route(route, payment_hash_2, onion_2, id_2).unwrap(); check_added_monitors(&nodes[1], 0); @@ -5089,7 +5089,7 @@ fn test_mpp_claim_to_holding_cell() { // Put the C <-> D channel into AwaitingRaa let (preimage_2, paymnt_hash_2, payment_secret_2) = get_payment_preimage_hash!(nodes[3]); - let onion = RecipientOnionFields::secret_only(payment_secret_2); + let onion = RecipientOnionFields::secret_only(payment_secret_2, 400_000); let id = PaymentId([42; 32]); let pay_params = PaymentParameters::from_node_id(node_d_id, TEST_FINAL_CLTV); let route_params = RouteParameters::from_payment_params_and_value(pay_params, 400_000); diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index a733cae11d5..e3094234417 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -1078,7 +1078,7 @@ impl_writeable_tlv_based!(ClaimingPayment, { (4, receiver_node_id, required), (5, htlcs, optional_vec), (7, sender_intended_value, option), - (9, onion_fields, option), + (9, onion_fields, (option: ReadableArgs, amount_msat.0.unwrap())), (11, payment_id, option), }); @@ -1107,6 +1107,18 @@ impl ClaimablePayment { } } +/// We write the [`ClaimableHTLC`] [`RecipientOnionFields`] separately as they were added sometime +/// later. Because [`ClaimableHTLC`] only implements [`ReadableArgs`] and have to add a wrapper +/// which reads them without [`RecipientOnionFields::total_mpp_amount_msat`] and then fill them in +/// later. +struct AmountlessClaimablePaymentHTLCOnion(RecipientOnionFields); + +impl Readable for AmountlessClaimablePaymentHTLCOnion { + fn read(reader: &mut R) -> Result { + Ok(Self(ReadableArgs::read(reader, 0)?)) + } +} + /// Represent the channel funding transaction type. enum FundingType { /// This variant is useful when we want LDK to validate the funding transaction and @@ -7813,6 +7825,7 @@ impl< payment_secret: Some(payment_data.payment_secret), payment_metadata, custom_tlvs, + total_mpp_amount_msat: payment_data.total_msat, }; ( incoming_cltv_expiry, @@ -7841,6 +7854,10 @@ impl< payment_secret: payment_data .as_ref() .map(|data| data.payment_secret), + total_mpp_amount_msat: payment_data + .as_ref() + .map(|data| data.total_msat) + .unwrap_or(outgoing_amt_msat), payment_metadata, custom_tlvs, }; @@ -17472,8 +17489,10 @@ impl<'a, ES: EntropySource, NS: NodeSigner, SP: SignerProvider, L: Logger> let mut fake_scid_rand_bytes: Option<[u8; 32]> = None; let mut probing_cookie_secret: Option<[u8; 32]> = None; let mut claimable_htlc_purposes = None; - let mut claimable_htlc_onion_fields = None; - let mut pending_claiming_payments = None; + let mut amountless_claimable_htlc_onion_fields: Option< + Vec>, + > = None; + let mut pending_claiming_payments = Some(new_hash_map()); let mut monitor_update_blocked_actions_per_peer: Option>)>> = None; let mut events_override = None; @@ -17500,7 +17519,7 @@ impl<'a, ES: EntropySource, NS: NodeSigner, SP: SignerProvider, L: Logger> (9, claimable_htlc_purposes, optional_vec), (10, legacy_in_flight_monitor_updates, option), (11, probing_cookie_secret, option), - (13, claimable_htlc_onion_fields, optional_vec), + (13, amountless_claimable_htlc_onion_fields, optional_vec), (14, decode_update_add_htlcs_legacy, option), (15, inbound_payment_id_secret, option), (17, in_flight_monitor_updates, option), @@ -17565,7 +17584,7 @@ impl<'a, ES: EntropySource, NS: NodeSigner, SP: SignerProvider, L: Logger> if purposes.len() != claimable_htlcs_list.len() { return Err(DecodeError::InvalidValue); } - if let Some(onion_fields) = claimable_htlc_onion_fields { + if let Some(onion_fields) = amountless_claimable_htlc_onion_fields { if onion_fields.len() != claimable_htlcs_list.len() { return Err(DecodeError::InvalidValue); } @@ -17573,7 +17592,20 @@ impl<'a, ES: EntropySource, NS: NodeSigner, SP: SignerProvider, L: Logger> .into_iter() .zip(onion_fields.into_iter().zip(claimable_htlcs_list.into_iter())) { - let claimable = ClaimablePayment { purpose, htlcs, onion_fields: onion }; + let htlcs_total_msat = + htlcs.first().ok_or(DecodeError::InvalidValue)?.total_msat; + let onion_fields = if let Some(mut onion) = onion { + if onion.0.total_mpp_amount_msat != 0 + && onion.0.total_mpp_amount_msat != htlcs_total_msat + { + return Err(DecodeError::InvalidValue); + } + onion.0.total_mpp_amount_msat = htlcs_total_msat; + Some(onion.0) + } else { + None + }; + let claimable = ClaimablePayment { purpose, htlcs, onion_fields }; let existing_payment = claimable_payments.insert(payment_hash, claimable); if existing_payment.is_some() { return Err(DecodeError::InvalidValue); @@ -19635,9 +19667,9 @@ mod tests { // indicates there are more HTLCs coming. let cur_height = CHAN_CONFIRM_DEPTH + 1; // route_payment calls send_payment, which adds 1 to the current height. So we do the same here to match. let session_privs = nodes[0].node.test_add_new_pending_payment(our_payment_hash, - RecipientOnionFields::secret_only(payment_secret), payment_id, &mpp_route).unwrap(); + RecipientOnionFields::secret_only(payment_secret, 200_000), payment_id, &mpp_route).unwrap(); nodes[0].node.test_send_payment_along_path(&mpp_route.paths[0], &our_payment_hash, - RecipientOnionFields::secret_only(payment_secret), 200_000, cur_height, payment_id, &None, session_privs[0]).unwrap(); + RecipientOnionFields::secret_only(payment_secret, 200_000), 200_000, cur_height, payment_id, &None, session_privs[0]).unwrap(); check_added_monitors(&nodes[0], 1); let mut events = nodes[0].node.get_and_clear_pending_msg_events(); assert_eq!(events.len(), 1); @@ -19645,7 +19677,7 @@ mod tests { // Next, send a keysend payment with the same payment_hash and make sure it fails. nodes[0].node.send_spontaneous_payment( - Some(payment_preimage), RecipientOnionFields::spontaneous_empty(), + Some(payment_preimage), RecipientOnionFields::spontaneous_empty(100_000), PaymentId(payment_preimage.0), route.route_params.clone().unwrap(), Retry::Attempts(0) ).unwrap(); check_added_monitors(&nodes[0], 1); @@ -19673,7 +19705,7 @@ mod tests { // Send the second half of the original MPP payment. nodes[0].node.test_send_payment_along_path(&mpp_route.paths[1], &our_payment_hash, - RecipientOnionFields::secret_only(payment_secret), 200_000, cur_height, payment_id, &None, session_privs[1]).unwrap(); + RecipientOnionFields::secret_only(payment_secret, 200_000), 200_000, cur_height, payment_id, &None, session_privs[1]).unwrap(); check_added_monitors(&nodes[0], 1); let mut events = nodes[0].node.get_and_clear_pending_msg_events(); assert_eq!(events.len(), 1); @@ -19763,7 +19795,7 @@ mod tests { PaymentParameters::for_keysend(expected_route.last().unwrap().node.get_our_node_id(), TEST_FINAL_CLTV, false), 100_000); nodes[0].node.send_spontaneous_payment( - Some(payment_preimage), RecipientOnionFields::spontaneous_empty(), + Some(payment_preimage), RecipientOnionFields::spontaneous_empty(100_000), PaymentId(payment_preimage.0), route_params.clone(), Retry::Attempts(0) ).unwrap(); check_added_monitors(&nodes[0], 1); @@ -19801,7 +19833,7 @@ mod tests { None, nodes[0].logger, &scorer, &Default::default(), &random_seed_bytes ).unwrap(); let payment_hash = nodes[0].node.send_spontaneous_payment( - Some(payment_preimage), RecipientOnionFields::spontaneous_empty(), + Some(payment_preimage), RecipientOnionFields::spontaneous_empty(100_000), PaymentId(payment_preimage.0), route.route_params.clone().unwrap(), Retry::Attempts(0) ).unwrap(); check_added_monitors(&nodes[0], 1); @@ -19814,7 +19846,7 @@ mod tests { // Next, attempt a regular payment and make sure it fails. let payment_secret = PaymentSecret([43; 32]); nodes[0].node.send_payment_with_route(route.clone(), payment_hash, - RecipientOnionFields::secret_only(payment_secret), PaymentId(payment_hash.0)).unwrap(); + RecipientOnionFields::secret_only(payment_secret, 100_000), PaymentId(payment_hash.0)).unwrap(); check_added_monitors(&nodes[0], 1); let mut events = nodes[0].node.get_and_clear_pending_msg_events(); assert_eq!(events.len(), 1); @@ -19844,7 +19876,7 @@ mod tests { // To start (3), send a keysend payment but don't claim it. let payment_id_1 = PaymentId([44; 32]); let payment_hash = nodes[0].node.send_spontaneous_payment( - Some(payment_preimage), RecipientOnionFields::spontaneous_empty(), payment_id_1, + Some(payment_preimage), RecipientOnionFields::spontaneous_empty(100_000), payment_id_1, route.route_params.clone().unwrap(), Retry::Attempts(0) ).unwrap(); check_added_monitors(&nodes[0], 1); @@ -19861,7 +19893,7 @@ mod tests { ); let payment_id_2 = PaymentId([45; 32]); nodes[0].node.send_spontaneous_payment( - Some(payment_preimage), RecipientOnionFields::spontaneous_empty(), payment_id_2, route_params, + Some(payment_preimage), RecipientOnionFields::spontaneous_empty(100_000), payment_id_2, route_params, Retry::Attempts(0) ).unwrap(); check_added_monitors(&nodes[0], 1); @@ -19919,9 +19951,9 @@ mod tests { let test_preimage = PaymentPreimage([42; 32]); let mismatch_payment_hash = PaymentHash([43; 32]); let session_privs = nodes[0].node.test_add_new_pending_payment(mismatch_payment_hash, - RecipientOnionFields::spontaneous_empty(), PaymentId(mismatch_payment_hash.0), &route).unwrap(); + RecipientOnionFields::spontaneous_empty(10_000), PaymentId(mismatch_payment_hash.0), &route).unwrap(); nodes[0].node.test_send_payment_internal(&route, mismatch_payment_hash, - RecipientOnionFields::spontaneous_empty(), Some(test_preimage), PaymentId(mismatch_payment_hash.0), None, session_privs).unwrap(); + RecipientOnionFields::spontaneous_empty(10_000), Some(test_preimage), PaymentId(mismatch_payment_hash.0), None, session_privs).unwrap(); check_added_monitors(&nodes[0], 1); let updates = get_htlc_update_msgs(&nodes[0], &nodes[1].node.get_our_node_id()); @@ -19966,7 +19998,7 @@ mod tests { route.route_params.as_mut().unwrap().final_value_msat *= 2; nodes[0].node.send_payment_with_route(route, payment_hash, - RecipientOnionFields::spontaneous_empty(), PaymentId(payment_hash.0)).unwrap(); + RecipientOnionFields::spontaneous_empty(200000), PaymentId(payment_hash.0)).unwrap(); let events = nodes[0].node.get_and_clear_pending_events(); assert_eq!(events.len(), 1); match events[0] { @@ -20783,7 +20815,7 @@ pub mod bench { let payment_hash = PaymentHash(Sha256::hash(&payment_preimage.0[..]).to_byte_array()); let payment_secret = $node_b.create_inbound_payment_for_hash(payment_hash, None, 7200, None).unwrap(); - $node_a.send_payment(payment_hash, RecipientOnionFields::secret_only(payment_secret), + $node_a.send_payment(payment_hash, RecipientOnionFields::secret_only(payment_secret, 10_000), PaymentId(payment_hash.0), RouteParameters::from_payment_params_and_value(payment_params, 10_000), Retry::Attempts(0)).unwrap(); diff --git a/lightning/src/ln/functional_test_utils.rs b/lightning/src/ln/functional_test_utils.rs index 218779123f6..0e1ada083c6 100644 --- a/lightning/src/ln/functional_test_utils.rs +++ b/lightning/src/ln/functional_test_utils.rs @@ -3412,7 +3412,7 @@ pub fn send_along_route_with_secret<'a, 'b, 'c>( .node .send_payment( our_payment_hash, - RecipientOnionFields::secret_only(our_payment_secret), + RecipientOnionFields::secret_only(our_payment_secret, recv_value), payment_id, route.route_params.unwrap(), Retry::Attempts(0), @@ -3551,7 +3551,7 @@ pub fn do_pass_along_path<'a, 'b, 'c>(args: PassAlongPathArgs) -> Option if is_last_hop && is_probe { do_commitment_signed_dance(node, prev_node, &payment_event.commitment_msg, true, true); - node.node.process_pending_htlc_forwards(); + expect_and_process_pending_htlcs(node, true); check_added_monitors(node, 1); } else { let commitment = &payment_event.commitment_msg; diff --git a/lightning/src/ln/functional_tests.rs b/lightning/src/ln/functional_tests.rs index 21c2af696c1..c2926681c73 100644 --- a/lightning/src/ln/functional_tests.rs +++ b/lightning/src/ln/functional_tests.rs @@ -1986,7 +1986,7 @@ fn do_test_commitment_revoked_fail_backward_exhaustive( // on nodes[2]'s RAA. let (route, fourth_payment_hash, _, fourth_payment_secret) = get_route_and_payment_hash!(nodes[1], nodes[2], 1000000); - let onion = RecipientOnionFields::secret_only(fourth_payment_secret); + let onion = RecipientOnionFields::secret_only(fourth_payment_secret, 1000000); let id = PaymentId(fourth_payment_hash.0); nodes[1].node.send_payment_with_route(route, fourth_payment_hash, onion, id).unwrap(); assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty()); @@ -2209,7 +2209,7 @@ pub fn fail_backward_pending_htlc_upon_channel_failure() { { let (route, payment_hash, _, payment_secret) = get_route_and_payment_hash!(nodes[0], nodes[1], 50_000); - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, 50_000); let id = PaymentId(payment_hash.0); nodes[0].node.send_payment_with_route(route, payment_hash, onion, id).unwrap(); check_added_monitors(&nodes[0], 1); @@ -2227,7 +2227,7 @@ pub fn fail_backward_pending_htlc_upon_channel_failure() { let (route, failed_payment_hash, _, failed_payment_secret) = get_route_and_payment_hash!(nodes[0], nodes[1], 50_000); { - let onion = RecipientOnionFields::secret_only(failed_payment_secret); + let onion = RecipientOnionFields::secret_only(failed_payment_secret, 50_000); let id = PaymentId(failed_payment_hash.0); nodes[0].node.send_payment_with_route(route, failed_payment_hash, onion, id).unwrap(); check_added_monitors(&nodes[0], 0); @@ -2243,7 +2243,7 @@ pub fn fail_backward_pending_htlc_upon_channel_failure() { let secp_ctx = Secp256k1::new(); let session_priv = SecretKey::from_slice(&[42; 32]).unwrap(); let current_height = nodes[1].node.best_block.read().unwrap().height + 1; - let recipient_onion_fields = RecipientOnionFields::secret_only(payment_secret); + let recipient_onion_fields = RecipientOnionFields::secret_only(payment_secret, 50_000); let (onion_payloads, _amount_msat, cltv_expiry) = onion_utils::build_onion_payloads( &route.paths[0], 50_000, @@ -2372,7 +2372,7 @@ pub fn test_force_close_fail_back() { get_route_and_payment_hash!(nodes[0], nodes[2], 1000000); let mut payment_event = { - let onion = RecipientOnionFields::secret_only(our_payment_secret); + let onion = RecipientOnionFields::secret_only(our_payment_secret, 1000000); let id = PaymentId(our_payment_hash.0); nodes[0].node.send_payment_with_route(route, our_payment_hash, onion, id).unwrap(); check_added_monitors(&nodes[0], 1); @@ -2658,7 +2658,7 @@ fn do_test_drop_messages_peer_disconnect(messages_delivered: u8, simulate_broken get_route_and_payment_hash!(nodes[0], nodes[1], 1_000_000); let payment_event = { - let onion = RecipientOnionFields::secret_only(payment_secret_1); + let onion = RecipientOnionFields::secret_only(payment_secret_1, 1_000_000); let id = PaymentId(payment_hash_1.0); nodes[0].node.send_payment_with_route(route, payment_hash_1, onion, id).unwrap(); check_added_monitors(&nodes[0], 1); @@ -3073,7 +3073,7 @@ pub fn test_drop_messages_peer_disconnect_dual_htlc() { // Now try to send a second payment which will fail to send let (route, payment_hash_2, payment_preimage_2, payment_secret_2) = get_route_and_payment_hash!(nodes[0], nodes[1], 1000000); - let onion = RecipientOnionFields::secret_only(payment_secret_2); + let onion = RecipientOnionFields::secret_only(payment_secret_2, 1000000); let id = PaymentId(payment_hash_2.0); nodes[0].node.send_payment_with_route(route, payment_hash_2, onion, id).unwrap(); check_added_monitors(&nodes[0], 1); @@ -3262,7 +3262,7 @@ fn do_test_htlc_timeout(send_partial_mpp: bool) { // indicates there are more HTLCs coming. let cur_height = CHAN_CONFIRM_DEPTH + 1; // route_payment calls send_payment, which adds 1 to the current height. So we do the same here to match. let payment_id = PaymentId([42; 32]); - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, 100000); let session_privs = nodes[0] .node .test_add_new_pending_payment(our_payment_hash, onion, payment_id, &route) @@ -3273,7 +3273,7 @@ fn do_test_htlc_timeout(send_partial_mpp: bool) { .test_send_payment_along_path( &route.paths[0], &our_payment_hash, - RecipientOnionFields::secret_only(payment_secret), + RecipientOnionFields::secret_only(payment_secret, 200_000), 200_000, cur_height, payment_id, @@ -3362,7 +3362,7 @@ fn do_test_holding_cell_htlc_add_timeouts(forwarded_htlc: bool) { // Route a first payment to get the 1 -> 2 channel in awaiting_raa... let (route, first_payment_hash, _, first_payment_secret) = get_route_and_payment_hash!(nodes[1], nodes[2], 100000); - let onion = RecipientOnionFields::secret_only(first_payment_secret); + let onion = RecipientOnionFields::secret_only(first_payment_secret, 100000); let id = PaymentId(first_payment_hash.0); nodes[1].node.send_payment_with_route(route, first_payment_hash, onion, id).unwrap(); assert_eq!(nodes[1].node.get_and_clear_pending_msg_events().len(), 1); @@ -3372,7 +3372,7 @@ fn do_test_holding_cell_htlc_add_timeouts(forwarded_htlc: bool) { let sending_node = if forwarded_htlc { &nodes[0] } else { &nodes[1] }; let (route, second_payment_hash, _, second_payment_secret) = get_route_and_payment_hash!(sending_node, nodes[2], 100000); - let onion = RecipientOnionFields::secret_only(second_payment_secret); + let onion = RecipientOnionFields::secret_only(second_payment_secret, 100000); let id = PaymentId(second_payment_hash.0); sending_node.node.send_payment_with_route(route, second_payment_hash, onion, id).unwrap(); @@ -4986,7 +4986,8 @@ fn do_htlc_claim_current_remote_commitment_only(use_dust: bool) { let (route, payment_hash, _, payment_secret) = get_route_and_payment_hash!(nodes[0], nodes[1], if use_dust { 50000 } else { 3000000 }); - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = + RecipientOnionFields::secret_only(payment_secret, if use_dust { 50000 } else { 3000000 }); let id = PaymentId(payment_hash.0); nodes[0].node.send_payment_with_route(route, payment_hash, onion, id).unwrap(); check_added_monitors(&nodes[0], 1); @@ -5149,7 +5150,7 @@ pub fn test_fail_holding_cell_htlc_upon_free() { get_route_and_payment_hash!(nodes[0], nodes[1], max_can_send); // Send a payment which passes reserve checks but gets stuck in the holding cell. - let onion = RecipientOnionFields::secret_only(our_payment_secret); + let onion = RecipientOnionFields::secret_only(our_payment_secret, max_can_send); let id = PaymentId(our_payment_hash.0); nodes[0].node.send_payment_with_route(route.clone(), our_payment_hash, onion, id).unwrap(); chan_stat = get_channel_value_stat!(nodes[0], nodes[1], chan.2); @@ -5253,14 +5254,14 @@ pub fn test_free_and_fail_holding_cell_htlcs() { get_route_and_payment_hash!(nodes[0], nodes[1], amt_2); // Send 2 payments which pass reserve checks but get stuck in the holding cell. - let onion = RecipientOnionFields::secret_only(payment_secret_1); + let onion = RecipientOnionFields::secret_only(payment_secret_1, amt_1); let id_1 = PaymentId(payment_hash_1.0); nodes[0].node.send_payment_with_route(route_1, payment_hash_1, onion, id_1).unwrap(); chan_stat = get_channel_value_stat!(nodes[0], nodes[1], chan.2); assert_eq!(chan_stat.holding_cell_outbound_amount_msat, amt_1); let id_2 = PaymentId(nodes[0].keys_manager.get_secure_random_bytes()); - let onion = RecipientOnionFields::secret_only(payment_secret_2); + let onion = RecipientOnionFields::secret_only(payment_secret_2, amt_2); nodes[0].node.send_payment_with_route(route_2.clone(), payment_hash_2, onion, id_2).unwrap(); chan_stat = get_channel_value_stat!(nodes[0], nodes[1], chan.2); assert_eq!(chan_stat.holding_cell_outbound_amount_msat, amt_1 + amt_2); @@ -5399,7 +5400,7 @@ pub fn test_fail_holding_cell_htlc_upon_free_multihop() { let (route, our_payment_hash, _, our_payment_secret) = get_route_and_payment_hash!(nodes[0], nodes[2], max_can_send); let payment_event = { - let onion = RecipientOnionFields::secret_only(our_payment_secret); + let onion = RecipientOnionFields::secret_only(our_payment_secret, max_can_send); let id = PaymentId(our_payment_hash.0); nodes[0].node.send_payment_with_route(route, our_payment_hash, onion, id).unwrap(); check_added_monitors(&nodes[0], 1); @@ -5507,7 +5508,7 @@ pub fn test_update_fulfill_htlc_bolt2_after_malformed_htlc_message_must_forward_ //First hop let mut payment_event = { - let onion = RecipientOnionFields::secret_only(our_payment_secret); + let onion = RecipientOnionFields::secret_only(our_payment_secret, 100000); let id = PaymentId(our_payment_hash.0); nodes[0].node.send_payment_with_route(route, our_payment_hash, onion, id).unwrap(); check_added_monitors(&nodes[0], 1); @@ -5620,7 +5621,7 @@ pub fn test_channel_failed_after_message_with_badonion_node_perm_bits_set() { // First hop let mut payment_event = { - let onion = RecipientOnionFields::secret_only(our_payment_secret); + let onion = RecipientOnionFields::secret_only(our_payment_secret, 100_000); let id = PaymentId(our_payment_hash.0); nodes[0].node.send_payment_with_route(route, our_payment_hash, onion, id).unwrap(); check_added_monitors(&nodes[0], 1); @@ -5967,7 +5968,7 @@ pub fn test_check_htlc_underpaying() { .node .create_inbound_payment_for_hash(our_payment_hash, Some(100_000), 7200, None) .unwrap(); - let onion = RecipientOnionFields::secret_only(our_payment_secret); + let onion = RecipientOnionFields::secret_only(our_payment_secret, route.get_total_amount()); let id = PaymentId(our_payment_hash.0); nodes[0].node.send_payment_with_route(route, our_payment_hash, onion, id).unwrap(); check_added_monitors(&nodes[0], 1); @@ -6906,7 +6907,7 @@ pub fn test_onion_value_mpp_set_calculation() { // Send payment let id = PaymentId(nodes[0].keys_manager.backing.get_secure_random_bytes()); - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, total_msat); let onion_session_privs = nodes[0].node.test_add_new_pending_payment(hash, onion.clone(), id, &route).unwrap(); let amt = Some(total_msat); @@ -6939,7 +6940,7 @@ pub fn test_onion_value_mpp_set_calculation() { &route.paths[0], &session_priv, ); - let recipient_onion_fields = RecipientOnionFields::secret_only(payment_secret); + let recipient_onion_fields = RecipientOnionFields::secret_only(payment_secret, 100_000); let (mut onion_payloads, _, _) = onion_utils::build_onion_payloads( &route.paths[0], 100_000, @@ -7044,10 +7045,10 @@ fn do_test_overshoot_mpp(msat_amounts: &[u64], total_msat: u64) { // Send payment with manually set total_msat let id = PaymentId(nodes[src_idx].keys_manager.backing.get_secure_random_bytes()); - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, total_msat); let onion_session_privs = nodes[src_idx].node.test_add_new_pending_payment(hash, onion, id, &route).unwrap(); - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, total_msat); let amt = Some(total_msat); nodes[src_idx] .node @@ -7135,7 +7136,7 @@ pub fn test_preimage_storage() { let (payment_hash, payment_secret) = nodes[1].node.create_inbound_payment(Some(100_000), 7200, None).unwrap(); let (route, _, _, _) = get_route_and_payment_hash!(nodes[0], nodes[1], 100_000); - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, 100_000); let id = PaymentId(payment_hash.0); nodes[0].node.send_payment_with_route(route, payment_hash, onion, id).unwrap(); @@ -7227,20 +7228,20 @@ pub fn test_bad_secret_hash() { let expected_err_data = [0, 0, 0, 0, 0, 1, 0x86, 0xa0, 0, 0, 0, CHAN_CONFIRM_DEPTH as u8]; // Send a payment with the right payment hash but the wrong payment secret - let onion = RecipientOnionFields::secret_only(random_secret); + let onion = RecipientOnionFields::secret_only(random_secret, 100_000); let id = PaymentId(our_payment_hash.0); nodes[0].node.send_payment_with_route(route.clone(), our_payment_hash, onion, id).unwrap(); handle_unknown_invalid_payment_data!(our_payment_hash); expect_payment_failed!(nodes[0], our_payment_hash, true, expected_err_code, expected_err_data); // Send a payment with a random payment hash, but the right payment secret - let onion = RecipientOnionFields::secret_only(our_payment_secret); + let onion = RecipientOnionFields::secret_only(our_payment_secret, 100_000); nodes[0].node.send_payment_with_route(route.clone(), random_hash, onion, id).unwrap(); handle_unknown_invalid_payment_data!(random_hash); expect_payment_failed!(nodes[0], random_hash, true, expected_err_code, expected_err_data); // Send a payment with a random payment hash and random payment secret - let onion = RecipientOnionFields::secret_only(random_secret); + let onion = RecipientOnionFields::secret_only(random_secret, 100_000); nodes[0].node.send_payment_with_route(route, random_hash, onion, id).unwrap(); handle_unknown_invalid_payment_data!(random_hash); expect_payment_failed!(nodes[0], random_hash, true, expected_err_code, expected_err_data); @@ -7467,7 +7468,7 @@ pub fn test_concurrent_monitor_claim() { // Route another payment to generate another update with still previous HTLC pending let (route, payment_hash, _, payment_secret) = get_route_and_payment_hash!(nodes[1], nodes[0], 3000000); - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, 3000000); let id = PaymentId(payment_hash.0); nodes[1].node.send_payment_with_route(route, payment_hash, onion, id).unwrap(); check_added_monitors(&nodes[1], 1); @@ -8203,7 +8204,7 @@ fn do_test_dup_htlc_second_rejected(test_for_second_fail_panic: bool) { get_payment_preimage_hash!(&nodes[1]); { - let onion = RecipientOnionFields::secret_only(our_payment_secret); + let onion = RecipientOnionFields::secret_only(our_payment_secret, 10_000); let id = PaymentId(our_payment_hash.0); nodes[0].node.send_payment_with_route(route.clone(), our_payment_hash, onion, id).unwrap(); check_added_monitors(&nodes[0], 1); @@ -8219,7 +8220,7 @@ fn do_test_dup_htlc_second_rejected(test_for_second_fail_panic: bool) { { // Note that we use a different PaymentId here to allow us to duplicativly pay - let onion = RecipientOnionFields::secret_only(our_payment_secret); + let onion = RecipientOnionFields::secret_only(our_payment_secret, 10_000); let id = PaymentId(our_payment_secret.0); nodes[0].node.send_payment_with_route(route, our_payment_hash, onion, id).unwrap(); check_added_monitors(&nodes[0], 1); @@ -8359,10 +8360,10 @@ pub fn test_inconsistent_mpp_params() { // ultimately have, just not right away. let mut dup_route = route.clone(); dup_route.paths.push(route.paths[1].clone()); - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, 15_000_000); nodes[0].node.test_add_new_pending_payment(hash, onion, id, &dup_route).unwrap() }; - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, 15_000_000); let path_a = &route.paths[0]; let real_amt = 15_000_000; let priv_a = session_privs[0]; @@ -8380,7 +8381,7 @@ pub fn test_inconsistent_mpp_params() { assert!(nodes[3].node.get_and_clear_pending_events().is_empty()); let path_b = &route.paths[1]; - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, 14_000_000); let amt_b = 14_000_000; let priv_b = session_privs[1]; nodes[0] @@ -8440,7 +8441,7 @@ pub fn test_inconsistent_mpp_params() { let conditions = PaymentFailedConditions::new().mpp_parts_remain(); expect_payment_failed_conditions(&nodes[0], hash, true, conditions); - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, real_amt); let path_b = &route.paths[1]; let priv_c = session_privs[2]; nodes[0] @@ -8508,7 +8509,7 @@ pub fn test_double_partial_claim() { pass_failed_payment_back(&nodes[0], paths, false, hash, reason); // nodes[1] now retries one of the two paths... - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, 15_000_000); let id = PaymentId(hash.0); nodes[0].node.send_payment_with_route(route, hash, onion, id).unwrap(); check_added_monitors(&nodes[0], 2); @@ -8740,12 +8741,18 @@ fn do_test_max_dust_htlc_exposure( }; // With default dust exposure: 5000 sats if on_holder_tx { - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only( + payment_secret, + dust_outbound_htlc_on_holder_tx_msat, + ); let id = PaymentId(payment_hash.0); let res = nodes[0].node.send_payment_with_route(route, payment_hash, onion, id); unwrap_send_err!(nodes[0], res, true, APIError::ChannelUnavailable { .. }, {}); } else { - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only( + payment_secret, + dust_htlc_on_counterparty_tx_msat + 1, + ); let id = PaymentId(payment_hash.0); let res = nodes[0].node.send_payment_with_route(route, payment_hash, onion, id); unwrap_send_err!(nodes[0], res, true, APIError::ChannelUnavailable { .. }, {}); @@ -8759,7 +8766,7 @@ fn do_test_max_dust_htlc_exposure( let (route, payment_hash, _, payment_secret) = get_route_and_payment_hash!(nodes[1], nodes[0], amount_msats); - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, amount_msats); let id = PaymentId(payment_hash.0); nodes[1].node.send_payment_with_route(route, payment_hash, onion, id).unwrap(); check_added_monitors(&nodes[1], 1); @@ -8798,7 +8805,7 @@ fn do_test_max_dust_htlc_exposure( // to cross the threshold. for _ in 0..AT_FEE_OUTBOUND_HTLCS { let (_, hash, payment_secret) = get_payment_preimage_hash(&nodes[1], Some(1_000), None); - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, route.get_total_amount()); let id = PaymentId(hash.0); nodes[0].node.send_payment_with_route(route.clone(), hash, onion, id).unwrap(); } @@ -9028,7 +9035,7 @@ pub fn test_nondust_htlc_excess_fees_are_dust() { // Send an additional non-dust htlc from 1 to 0, and check the complaint let (route, payment_hash, _, payment_secret) = get_route_and_payment_hash!(nodes[1], nodes[0], dust_limit * 2); - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, route.get_total_amount()); let id = PaymentId(payment_hash.0); nodes[1].node.send_payment_with_route(route, payment_hash, onion, id).unwrap(); check_added_monitors(&nodes[1], 1); @@ -9064,7 +9071,7 @@ pub fn test_nondust_htlc_excess_fees_are_dust() { assert_eq!(nodes[1].node.list_channels()[0].pending_outbound_htlcs.len(), 0); // Send an additional non-dust htlc from 0 to 1 using the pre-calculated route above, and check the immediate complaint - let onion = RecipientOnionFields::secret_only(payment_secret_0_1); + let onion = RecipientOnionFields::secret_only(payment_secret_0_1, route_0_1.get_total_amount()); let id = PaymentId(payment_hash_0_1.0); let res = nodes[0].node.send_payment_with_route(route_0_1, payment_hash_0_1, onion, id); unwrap_send_err!(nodes[0], res, true, APIError::ChannelUnavailable { .. }, {}); @@ -9082,7 +9089,7 @@ pub fn test_nondust_htlc_excess_fees_are_dust() { create_announced_chan_between_nodes(&nodes, 2, 0); let (route, payment_hash, _, payment_secret) = get_route_and_payment_hash!(nodes[2], nodes[1], dust_limit * 2); - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, route.get_total_amount()); nodes[2].node.send_payment_with_route(route, payment_hash, onion, PaymentId([0; 32])).unwrap(); check_added_monitors(&nodes[2], 1); let send = SendEvent::from_node(&nodes[2]); @@ -9204,7 +9211,7 @@ fn do_test_nondust_htlc_fees_dust_exposure_delta(features: ChannelTypeFeatures) // Send an additional non-dust htlc from 0 to 1, and check the complaint let (route, payment_hash, _, payment_secret) = get_route_and_payment_hash!(nodes[0], nodes[1], NON_DUST_HTLC_MSAT); - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, NON_DUST_HTLC_MSAT); let id = PaymentId(payment_hash.0); nodes[0].node.send_payment_with_route(route, payment_hash, onion, id).unwrap(); check_added_monitors(&nodes[0], 1); @@ -9286,7 +9293,7 @@ fn do_test_nondust_htlc_fees_dust_exposure_delta(features: ChannelTypeFeatures) nodes[1].node.update_partial_channel_config(&node_a_id, &[chan_id], &update).unwrap(); // Send an additional non-dust htlc from 1 to 0 using the pre-calculated route above, and check the immediate complaint - let onion = RecipientOnionFields::secret_only(payment_secret_1_0); + let onion = RecipientOnionFields::secret_only(payment_secret_1_0, NON_DUST_HTLC_MSAT); let id = PaymentId(payment_hash_1_0.0); let res = nodes[1].node.send_payment_with_route(route_1_0, payment_hash_1_0, onion, id); unwrap_send_err!(nodes[1], res, true, APIError::ChannelUnavailable { .. }, {}); @@ -9369,7 +9376,7 @@ fn do_payment_with_custom_min_final_cltv_expiry(valid_delta: bool, use_user_hash (hash, nodes[1].node.get_payment_preimage(hash, payment_secret).unwrap(), payment_secret) }; let route = get_route!(nodes[0], payment_parameters, recv_value).unwrap(); - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, recv_value); nodes[0].node.send_payment_with_route(route, hash, onion, PaymentId(hash.0)).unwrap(); check_added_monitors(&nodes[0], 1); let mut events = nodes[0].node.get_and_clear_pending_msg_events(); @@ -9846,7 +9853,7 @@ fn do_test_multi_post_event_actions(do_reload: bool) { let (route, payment_hash_3, _, payment_secret_3) = get_route_and_payment_hash!(nodes[1], nodes[0], 100_000); let payment_id = PaymentId(payment_hash_3.0); - let onion = RecipientOnionFields::secret_only(payment_secret_3); + let onion = RecipientOnionFields::secret_only(payment_secret_3, 100_000); nodes[1].node.send_payment_with_route(route, payment_hash_3, onion, payment_id).unwrap(); check_added_monitors(&nodes[1], 1); @@ -9957,7 +9964,7 @@ pub fn test_dust_exposure_holding_cell_assertion() { // messages (leaving B waiting on C's RAA) the next HTLC will go into B's holding cell. let (route_bc, payment_hash_bc, _payment_preimage_bc, payment_secret_bc) = get_route_and_payment_hash!(nodes[1], nodes[2], DUST_HTLC_VALUE_MSAT); - let onion_bc = RecipientOnionFields::secret_only(payment_secret_bc); + let onion_bc = RecipientOnionFields::secret_only(payment_secret_bc, DUST_HTLC_VALUE_MSAT); let id = PaymentId(payment_hash_bc.0); nodes[1].node.send_payment_with_route(route_bc, payment_hash_bc, onion_bc, id).unwrap(); check_added_monitors(&nodes[1], 1); @@ -9977,7 +9984,7 @@ pub fn test_dust_exposure_holding_cell_assertion() { .unwrap(); let (route_ac, payment_hash_cell, _, payment_secret_ac) = get_route_and_payment_hash!(nodes[0], nodes[2], payment_params_ac, DUST_HTLC_VALUE_MSAT); - let onion_ac = RecipientOnionFields::secret_only(payment_secret_ac); + let onion_ac = RecipientOnionFields::secret_only(payment_secret_ac, DUST_HTLC_VALUE_MSAT); let id = PaymentId(payment_hash_cell.0); nodes[0].node.send_payment_with_route(route_ac, payment_hash_cell, onion_ac, id).unwrap(); check_added_monitors(&nodes[0], 1); @@ -10000,7 +10007,7 @@ pub fn test_dust_exposure_holding_cell_assertion() { // its holding cell as it would be over-exposed to dust. let (route_cb, payment_hash_cb, payment_preimage_cb, payment_secret_cb) = get_route_and_payment_hash!(nodes[2], nodes[1], DUST_HTLC_VALUE_MSAT); - let onion_cb = RecipientOnionFields::secret_only(payment_secret_cb); + let onion_cb = RecipientOnionFields::secret_only(payment_secret_cb, DUST_HTLC_VALUE_MSAT); let id = PaymentId(payment_hash_cb.0); nodes[2].node.send_payment_with_route(route_cb, payment_hash_cb, onion_cb, id).unwrap(); check_added_monitors(&nodes[2], 1); diff --git a/lightning/src/ln/htlc_reserve_unit_tests.rs b/lightning/src/ln/htlc_reserve_unit_tests.rs index 4c4fbada7dd..5a8b7eb522e 100644 --- a/lightning/src/ln/htlc_reserve_unit_tests.rs +++ b/lightning/src/ln/htlc_reserve_unit_tests.rs @@ -170,7 +170,7 @@ pub fn test_channel_reserve_holding_cell_htlcs() { route.paths[0].hops.last_mut().unwrap().fee_msat += 1; assert!(route.paths[0].hops.iter().rev().skip(1).all(|h| h.fee_msat == feemsat)); - let onion = RecipientOnionFields::secret_only(our_payment_secret); + let onion = RecipientOnionFields::secret_only(our_payment_secret, route.get_total_amount()); let id = PaymentId(our_payment_hash.0); let res = nodes[0].node.send_payment_with_route(route, our_payment_hash, onion, id); unwrap_send_err!(nodes[0], res, true, APIError::ChannelUnavailable { .. }, {}); @@ -246,7 +246,7 @@ pub fn test_channel_reserve_holding_cell_htlcs() { get_route_and_payment_hash!(nodes[0], nodes[2], recv_value_1); let payment_event_1 = { let route = route_1.clone(); - let onion = RecipientOnionFields::secret_only(our_payment_secret_1); + let onion = RecipientOnionFields::secret_only(our_payment_secret_1, recv_value_1); let id = PaymentId(our_payment_hash_1.0); nodes[0].node.send_payment_with_route(route, our_payment_hash_1, onion, id).unwrap(); check_added_monitors(&nodes[0], 1); @@ -267,7 +267,7 @@ pub fn test_channel_reserve_holding_cell_htlcs() { let mut route = route_1.clone(); route.paths[0].hops.last_mut().unwrap().fee_msat = recv_value_2 + 1; let (_, our_payment_hash, our_payment_secret) = get_payment_preimage_hash!(nodes[2]); - let onion = RecipientOnionFields::secret_only(our_payment_secret); + let onion = RecipientOnionFields::secret_only(our_payment_secret, route.get_total_amount()); let id = PaymentId(our_payment_hash.0); let res = nodes[0].node.send_payment_with_route(route, our_payment_hash, onion, id); unwrap_send_err!(nodes[0], res, true, APIError::ChannelUnavailable { .. }, {}); @@ -295,7 +295,7 @@ pub fn test_channel_reserve_holding_cell_htlcs() { let (route_21, our_payment_hash_21, our_payment_preimage_21, our_payment_secret_21) = get_route_and_payment_hash!(nodes[0], nodes[2], recv_value_21); // but this will stuck in the holding cell - let onion = RecipientOnionFields::secret_only(our_payment_secret_21); + let onion = RecipientOnionFields::secret_only(our_payment_secret_21, recv_value_21); let id = PaymentId(our_payment_hash_21.0); nodes[0].node.send_payment_with_route(route_21, our_payment_hash_21, onion, id).unwrap(); check_added_monitors(&nodes[0], 0); @@ -307,7 +307,7 @@ pub fn test_channel_reserve_holding_cell_htlcs() { let (mut route, our_payment_hash, _, our_payment_secret) = get_route_and_payment_hash!(nodes[0], nodes[2], recv_value_22); route.paths[0].hops.last_mut().unwrap().fee_msat += 1; - let onion = RecipientOnionFields::secret_only(our_payment_secret); + let onion = RecipientOnionFields::secret_only(our_payment_secret, route.get_total_amount()); let id = PaymentId(our_payment_hash.0); let res = nodes[0].node.send_payment_with_route(route, our_payment_hash, onion, id); unwrap_send_err!(nodes[0], res, true, APIError::ChannelUnavailable { .. }, {}); @@ -317,7 +317,7 @@ pub fn test_channel_reserve_holding_cell_htlcs() { let (route_22, our_payment_hash_22, our_payment_preimage_22, our_payment_secret_22) = get_route_and_payment_hash!(nodes[0], nodes[2], recv_value_22); // this will also stuck in the holding cell - let onion = RecipientOnionFields::secret_only(our_payment_secret_22); + let onion = RecipientOnionFields::secret_only(our_payment_secret_22, recv_value_22); let id = PaymentId(our_payment_hash_22.0); nodes[0].node.send_payment_with_route(route_22, our_payment_hash_22, onion, id).unwrap(); check_added_monitors(&nodes[0], 0); @@ -493,7 +493,7 @@ pub fn channel_reserve_in_flight_removes() { let (route, payment_hash_3, payment_preimage_3, payment_secret_3) = get_route_and_payment_hash!(nodes[0], nodes[1], 100000); let send_1 = { - let onion = RecipientOnionFields::secret_only(payment_secret_3); + let onion = RecipientOnionFields::secret_only(payment_secret_3, 100000); let id = PaymentId(payment_hash_3.0); nodes[0].node.send_payment_with_route(route, payment_hash_3, onion, id).unwrap(); check_added_monitors(&nodes[0], 1); @@ -570,7 +570,7 @@ pub fn channel_reserve_in_flight_removes() { let (route, payment_hash_4, payment_preimage_4, payment_secret_4) = get_route_and_payment_hash!(nodes[1], nodes[0], 10000); let send_2 = { - let onion = RecipientOnionFields::secret_only(payment_secret_4); + let onion = RecipientOnionFields::secret_only(payment_secret_4, 10000); let id = PaymentId(payment_hash_4.0); nodes[1].node.send_payment_with_route(route, payment_hash_4, onion, id).unwrap(); check_added_monitors(&nodes[1], 1); @@ -637,7 +637,7 @@ pub fn holding_cell_htlc_counting() { for _ in 0..50 { let (route, payment_hash, payment_preimage, payment_secret) = get_route_and_payment_hash!(nodes[1], nodes[2], 100000); - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, 100000); let id = PaymentId(payment_hash.0); nodes[1].node.send_payment_with_route(route, payment_hash, onion, id).unwrap(); payments.push((payment_preimage, payment_hash)); @@ -653,7 +653,7 @@ pub fn holding_cell_htlc_counting() { // the holding cell waiting on B's RAA to send. At this point we should not be able to add // another HTLC. { - let onion = RecipientOnionFields::secret_only(payment_secret_1); + let onion = RecipientOnionFields::secret_only(payment_secret_1, 100000); let id = PaymentId(payment_hash_1.0); let res = nodes[1].node.send_payment_with_route(route, payment_hash_1, onion, id); unwrap_send_err!(nodes[1], res, true, APIError::ChannelUnavailable { .. }, {}); @@ -663,7 +663,7 @@ pub fn holding_cell_htlc_counting() { // This should also be true if we try to forward a payment. let (route, payment_hash_2, _, payment_secret_2) = get_route_and_payment_hash!(nodes[0], nodes[2], 100000); - let onion = RecipientOnionFields::secret_only(payment_secret_2); + let onion = RecipientOnionFields::secret_only(payment_secret_2, 100000); let id = PaymentId(payment_hash_2.0); nodes[0].node.send_payment_with_route(route, payment_hash_2, onion, id).unwrap(); check_added_monitors(&nodes[0], 1); @@ -767,7 +767,7 @@ pub fn test_basic_channel_reserve() { let (mut route, our_payment_hash, _, our_payment_secret) = get_route_and_payment_hash!(nodes[0], nodes[1], max_can_send); route.paths[0].hops.last_mut().unwrap().fee_msat += 1; - let onion = RecipientOnionFields::secret_only(our_payment_secret); + let onion = RecipientOnionFields::secret_only(our_payment_secret, max_can_send + 1); let id = PaymentId(our_payment_hash.0); let err = nodes[0].node.send_payment_with_route(route, our_payment_hash, onion, id); unwrap_send_err!(nodes[0], err, true, APIError::ChannelUnavailable { .. }, {}); @@ -815,7 +815,8 @@ pub fn do_test_fee_spike_buffer(cfg: Option, htlc_fails: bool) { let payment_amt_msat = 3460001; let onion_keys = onion_utils::construct_onion_keys(&secp_ctx, &route.paths[0], &session_priv); - let recipient_onion_fields = RecipientOnionFields::secret_only(payment_secret); + let recipient_onion_fields = + RecipientOnionFields::secret_only(payment_secret, payment_amt_msat); let (onion_payloads, htlc_msat, htlc_cltv) = onion_utils::build_onion_payloads( &route.paths[0], payment_amt_msat, @@ -1014,7 +1015,7 @@ pub fn test_chan_reserve_violation_outbound_htlc_inbound_chan() { } // However one more HTLC should be significantly over the reserve amount and fail. - let onion = RecipientOnionFields::secret_only(our_payment_secret); + let onion = RecipientOnionFields::secret_only(our_payment_secret, 1_000_000); let id = PaymentId(our_payment_hash.0); let res = nodes[1].node.send_payment_with_route(route, our_payment_hash, onion, id); unwrap_send_err!(nodes[1], res, true, APIError::ChannelUnavailable { .. }, {}); @@ -1059,7 +1060,7 @@ pub fn test_chan_reserve_violation_inbound_htlc_outbound_channel() { let session_priv = SecretKey::from_slice(&[42; 32]).unwrap(); let cur_height = nodes[1].node.best_block.read().unwrap().height + 1; let onion_keys = onion_utils::construct_onion_keys(&secp_ctx, &route.paths[0], &session_priv); - let recipient_onion_fields = RecipientOnionFields::secret_only(payment_secret); + let recipient_onion_fields = RecipientOnionFields::secret_only(payment_secret, 700_000); let (onion_payloads, htlc_msat, htlc_cltv) = onion_utils::build_onion_payloads( &route.paths[0], 700_000, @@ -1142,7 +1143,7 @@ pub fn test_chan_reserve_dust_inbound_htlcs_outbound_chan() { let (mut route, our_payment_hash, _, our_payment_secret) = get_route_and_payment_hash!(nodes[1], nodes[0], dust_amt); route.paths[0].hops[0].fee_msat += 1; - let onion = RecipientOnionFields::secret_only(our_payment_secret); + let onion = RecipientOnionFields::secret_only(our_payment_secret, dust_amt + 1); let id = PaymentId(our_payment_hash.0); let res = nodes[1].node.send_payment_with_route(route, our_payment_hash, onion, id); unwrap_send_err!(nodes[1], res, true, APIError::ChannelUnavailable { .. }, {}); @@ -1211,7 +1212,7 @@ pub fn test_chan_reserve_violation_inbound_htlc_inbound_chan() { let (route_1, our_payment_hash_1, _, our_payment_secret_1) = get_route_and_payment_hash!(nodes[0], nodes[2], amt_msat_1); let payment_event_1 = { - let onion = RecipientOnionFields::secret_only(our_payment_secret_1); + let onion = RecipientOnionFields::secret_only(our_payment_secret_1, amt_msat_1); let id = PaymentId(our_payment_hash_1.0); let route = route_1.clone(); nodes[0].node.send_payment_with_route(route, our_payment_hash_1, onion, id).unwrap(); @@ -1240,7 +1241,7 @@ pub fn test_chan_reserve_violation_inbound_htlc_inbound_chan() { let session_priv = SecretKey::from_slice(&[42; 32]).unwrap(); let cur_height = nodes[0].node.best_block.read().unwrap().height + 1; let onion_keys = onion_utils::construct_onion_keys(&secp_ctx, &route_2.paths[0], &session_priv); - let recipient_onion_fields = RecipientOnionFields::spontaneous_empty(); + let recipient_onion_fields = RecipientOnionFields::spontaneous_empty(recv_value_2); let (onion_payloads, htlc_msat, htlc_cltv) = onion_utils::build_onion_payloads( &route_2.paths[0], recv_value_2, @@ -1310,7 +1311,7 @@ pub fn test_payment_route_reaching_same_channel_twice() { route.paths[0].hops.extend_from_slice(&cloned_hops); unwrap_send_err!(nodes[0], nodes[0].node.send_payment_with_route(route, our_payment_hash, - RecipientOnionFields::secret_only(our_payment_secret), PaymentId(our_payment_hash.0) + RecipientOnionFields::secret_only(our_payment_secret, 100000000), PaymentId(our_payment_hash.0) ), false, APIError::InvalidRoute { ref err }, assert_eq!(err, &"Path went through the same channel twice")); assert!(nodes[0].node.list_recent_payments().is_empty()); @@ -1334,7 +1335,7 @@ pub fn test_update_add_htlc_bolt2_sender_value_below_minimum_msat() { get_route_and_payment_hash!(nodes[0], nodes[1], 100000); route.paths[0].hops[0].fee_msat = 100; - let onion = RecipientOnionFields::secret_only(our_payment_secret); + let onion = RecipientOnionFields::secret_only(our_payment_secret, 100); let id = PaymentId(our_payment_hash.0); let res = nodes[0].node.send_payment_with_route(route, our_payment_hash, onion, id); unwrap_send_err!(nodes[0], res, true, APIError::ChannelUnavailable { .. }, {}); @@ -1354,7 +1355,7 @@ pub fn test_update_add_htlc_bolt2_sender_zero_value_msat() { let (mut route, our_payment_hash, _, our_payment_secret) = get_route_and_payment_hash!(nodes[0], nodes[1], 100000); route.paths[0].hops[0].fee_msat = 0; - let onion = RecipientOnionFields::secret_only(our_payment_secret); + let onion = RecipientOnionFields::secret_only(our_payment_secret, 0); let id = PaymentId(our_payment_hash.0); let res = nodes[0].node.send_payment_with_route(route, our_payment_hash, onion, id); unwrap_send_err!(nodes[0], res, @@ -1384,7 +1385,7 @@ pub fn test_update_add_htlc_bolt2_receiver_zero_value_msat() { let (route, our_payment_hash, _, our_payment_secret) = get_route_and_payment_hash!(nodes[0], nodes[1], 100000); - let onion = RecipientOnionFields::secret_only(our_payment_secret); + let onion = RecipientOnionFields::secret_only(our_payment_secret, 100000); let id = PaymentId(our_payment_hash.0); nodes[0].node.send_payment_with_route(route, our_payment_hash, onion, id).unwrap(); check_added_monitors(&nodes[0], 1); @@ -1425,7 +1426,7 @@ pub fn test_update_add_htlc_bolt2_sender_cltv_expiry_too_high() { get_route_and_payment_hash!(nodes[0], nodes[1], payment_params, 100000000); route.paths[0].hops.last_mut().unwrap().cltv_expiry_delta = 500000001; - let onion = RecipientOnionFields::secret_only(our_payment_secret); + let onion = RecipientOnionFields::secret_only(our_payment_secret, 100000000); let id = PaymentId(our_payment_hash.0); let res = nodes[0].node.send_payment_with_route(route, our_payment_hash, onion, id); unwrap_send_err!(nodes[0], res, true, APIError::InvalidRoute { ref err }, @@ -1460,7 +1461,7 @@ pub fn test_update_add_htlc_bolt2_sender_exceed_max_htlc_num_and_htlc_id_increme let (route, our_payment_hash, _, our_payment_secret) = get_route_and_payment_hash!(nodes[0], nodes[1], 100000); let payment_event = { - let onion = RecipientOnionFields::secret_only(our_payment_secret); + let onion = RecipientOnionFields::secret_only(our_payment_secret, 100000); let id = PaymentId(our_payment_hash.0); nodes[0].node.send_payment_with_route(route, our_payment_hash, onion, id).unwrap(); check_added_monitors(&nodes[0], 1); @@ -1486,7 +1487,7 @@ pub fn test_update_add_htlc_bolt2_sender_exceed_max_htlc_num_and_htlc_id_increme expect_and_process_pending_htlcs(&nodes[1], false); expect_payment_claimable!(nodes[1], our_payment_hash, our_payment_secret, 100000); } - let onion = RecipientOnionFields::secret_only(our_payment_secret); + let onion = RecipientOnionFields::secret_only(our_payment_secret, 100000); let id = PaymentId(our_payment_hash.0); let res = nodes[0].node.send_payment_with_route(route, our_payment_hash, onion, id); unwrap_send_err!(nodes[0], res, true, APIError::ChannelUnavailable { .. }, {}); @@ -1514,7 +1515,7 @@ pub fn test_update_add_htlc_bolt2_sender_exceed_max_htlc_value_in_flight() { // Manually create a route over our max in flight (which our router normally automatically // limits us to. route.paths[0].hops[0].fee_msat = max_in_flight + 1; - let onion = RecipientOnionFields::secret_only(our_payment_secret); + let onion = RecipientOnionFields::secret_only(our_payment_secret, max_in_flight + 1); let id = PaymentId(our_payment_hash.0); let res = nodes[0].node.send_payment_with_route(route, our_payment_hash, onion, id); unwrap_send_err!(nodes[0], res, true, APIError::ChannelUnavailable { .. }, {}); @@ -1546,7 +1547,7 @@ pub fn test_update_add_htlc_bolt2_receiver_check_amount_received_more_than_min() let (route, our_payment_hash, _, our_payment_secret) = get_route_and_payment_hash!(nodes[0], nodes[1], htlc_minimum_msat); - let onion = RecipientOnionFields::secret_only(our_payment_secret); + let onion = RecipientOnionFields::secret_only(our_payment_secret, htlc_minimum_msat); let id = PaymentId(our_payment_hash.0); nodes[0].node.send_payment_with_route(route, our_payment_hash, onion, id).unwrap(); check_added_monitors(&nodes[0], 1); @@ -1584,7 +1585,7 @@ pub fn test_update_add_htlc_bolt2_receiver_sender_can_afford_amount_sent() { let max_can_send = 5000000 - channel_reserve - commit_tx_fee_outbound; let (route, our_payment_hash, _, our_payment_secret) = get_route_and_payment_hash!(nodes[0], nodes[1], max_can_send); - let onion = RecipientOnionFields::secret_only(our_payment_secret); + let onion = RecipientOnionFields::secret_only(our_payment_secret, max_can_send); let id = PaymentId(our_payment_hash.0); nodes[0].node.send_payment_with_route(route, our_payment_hash, onion, id).unwrap(); check_added_monitors(&nodes[0], 1); @@ -1628,7 +1629,7 @@ pub fn test_update_add_htlc_bolt2_receiver_check_max_htlc_limit() { &route.paths[0], &session_priv, ); - let recipient_onion_fields = RecipientOnionFields::secret_only(our_payment_secret); + let recipient_onion_fields = RecipientOnionFields::secret_only(our_payment_secret, send_amt); let (onion_payloads, _htlc_msat, htlc_cltv) = onion_utils::build_onion_payloads( &route.paths[0], send_amt, @@ -1688,7 +1689,7 @@ pub fn test_update_add_htlc_bolt2_receiver_check_max_in_flight_msat() { let (route, our_payment_hash, _, our_payment_secret) = get_route_and_payment_hash!(nodes[0], nodes[1], 1000000); - let onion = RecipientOnionFields::secret_only(our_payment_secret); + let onion = RecipientOnionFields::secret_only(our_payment_secret, 1000000); let id = PaymentId(our_payment_hash.0); nodes[0].node.send_payment_with_route(route, our_payment_hash, onion, id).unwrap(); check_added_monitors(&nodes[0], 1); @@ -1722,7 +1723,7 @@ pub fn test_update_add_htlc_bolt2_receiver_check_cltv_expiry() { create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 100000, 95000000); let (route, our_payment_hash, _, our_payment_secret) = get_route_and_payment_hash!(nodes[0], nodes[1], 1000000); - let reason = RecipientOnionFields::secret_only(our_payment_secret); + let reason = RecipientOnionFields::secret_only(our_payment_secret, 1000000); let id = PaymentId(our_payment_hash.0); nodes[0].node.send_payment_with_route(route, our_payment_hash, reason, id).unwrap(); check_added_monitors(&nodes[0], 1); @@ -1754,7 +1755,7 @@ pub fn test_update_add_htlc_bolt2_receiver_check_repeated_id_ignore() { create_announced_chan_between_nodes(&nodes, 0, 1); let (route, our_payment_hash, _, our_payment_secret) = get_route_and_payment_hash!(nodes[0], nodes[1], 1000000); - let onion = RecipientOnionFields::secret_only(our_payment_secret); + let onion = RecipientOnionFields::secret_only(our_payment_secret, 1000000); let id = PaymentId(our_payment_hash.0); nodes[0].node.send_payment_with_route(route, our_payment_hash, onion, id).unwrap(); @@ -1819,7 +1820,7 @@ pub fn test_update_fulfill_htlc_bolt2_update_fulfill_htlc_before_commitment() { let chan = create_announced_chan_between_nodes(&nodes, 0, 1); let (route, our_payment_hash, our_payment_preimage, our_payment_secret) = get_route_and_payment_hash!(nodes[0], nodes[1], 1000000); - let onion = RecipientOnionFields::secret_only(our_payment_secret); + let onion = RecipientOnionFields::secret_only(our_payment_secret, 1000000); let id = PaymentId(our_payment_hash.0); nodes[0].node.send_payment_with_route(route, our_payment_hash, onion, id).unwrap(); @@ -1864,7 +1865,7 @@ pub fn test_update_fulfill_htlc_bolt2_update_fail_htlc_before_commitment() { let (route, our_payment_hash, _, our_payment_secret) = get_route_and_payment_hash!(nodes[0], nodes[1], 1000000); - let onion = RecipientOnionFields::secret_only(our_payment_secret); + let onion = RecipientOnionFields::secret_only(our_payment_secret, 1000000); let id = PaymentId(our_payment_hash.0); nodes[0].node.send_payment_with_route(route, our_payment_hash, onion, id).unwrap(); check_added_monitors(&nodes[0], 1); @@ -1908,7 +1909,7 @@ pub fn test_update_fulfill_htlc_bolt2_update_fail_malformed_htlc_before_commitme let (route, our_payment_hash, _, our_payment_secret) = get_route_and_payment_hash!(nodes[0], nodes[1], 1000000); - let onion = RecipientOnionFields::secret_only(our_payment_secret); + let onion = RecipientOnionFields::secret_only(our_payment_secret, 1000000); let id = PaymentId(our_payment_hash.0); nodes[0].node.send_payment_with_route(route, our_payment_hash, onion, id).unwrap(); check_added_monitors(&nodes[0], 1); @@ -2071,7 +2072,7 @@ pub fn test_update_fulfill_htlc_bolt2_missing_badonion_bit_for_malformed_htlc_me let (route, our_payment_hash, _, our_payment_secret) = get_route_and_payment_hash!(nodes[0], nodes[1], 1000000); - let onion = RecipientOnionFields::secret_only(our_payment_secret); + let onion = RecipientOnionFields::secret_only(our_payment_secret, 1000000); let id = PaymentId(our_payment_hash.0); nodes[0].node.send_payment_with_route(route, our_payment_hash, onion, id).unwrap(); check_added_monitors(&nodes[0], 1); @@ -2234,7 +2235,8 @@ pub fn do_test_dust_limit_fee_accounting(can_afford: bool) { let onion_keys = onion_utils::construct_onion_keys(&secp_ctx, &route_0_1.paths[0], &session_priv); - let recipient_onion_fields = RecipientOnionFields::secret_only(payment_secret_0_1); + let recipient_onion_fields = + RecipientOnionFields::secret_only(payment_secret_0_1, HTLC_AMT_SAT * 1000); let (onion_payloads, amount_msat, cltv_expiry) = onion_utils::build_onion_payloads( &route_0_1.paths[0], HTLC_AMT_SAT * 1000, diff --git a/lightning/src/ln/interception_tests.rs b/lightning/src/ln/interception_tests.rs index c83ef177628..2f5692c2b16 100644 --- a/lightning/src/ln/interception_tests.rs +++ b/lightning/src/ln/interception_tests.rs @@ -123,7 +123,7 @@ fn do_test_htlc_interception_flags( None => {}, } - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, amt_msat); let payment_id = PaymentId(payment_hash.0); nodes[0].node.send_payment_with_route(route, payment_hash, onion, payment_id).unwrap(); check_added_monitors(&nodes[0], 1); diff --git a/lightning/src/ln/invoice_utils.rs b/lightning/src/ln/invoice_utils.rs index 195eb73595a..b1097b6c274 100644 --- a/lightning/src/ln/invoice_utils.rs +++ b/lightning/src/ln/invoice_utils.rs @@ -1283,7 +1283,10 @@ mod test { let payment_hash = invoice.payment_hash(); let id = PaymentId(payment_hash.0); - let onion = RecipientOnionFields::secret_only(*invoice.payment_secret()); + let onion = RecipientOnionFields::secret_only( + *invoice.payment_secret(), + invoice.amount_milli_satoshis().unwrap(), + ); nodes[0].node.send_payment(payment_hash, onion, id, params, Retry::Attempts(0)).unwrap(); check_added_monitors(&nodes[0], 1); diff --git a/lightning/src/ln/max_payment_path_len_tests.rs b/lightning/src/ln/max_payment_path_len_tests.rs index b947273115e..ea78449316c 100644 --- a/lightning/src/ln/max_payment_path_len_tests.rs +++ b/lightning/src/ln/max_payment_path_len_tests.rs @@ -87,6 +87,7 @@ fn large_payment_metadata() { payment_secret: Some(payment_secret), payment_metadata: Some(payment_metadata.clone()), custom_tlvs: Vec::new(), + total_mpp_amount_msat: amt_msat, }; let route_params = route_0_1.route_params.clone().unwrap(); let id = PaymentId(payment_hash.0); @@ -128,6 +129,7 @@ fn large_payment_metadata() { // If our payment_metadata contains 1 additional byte, we'll fail prior to pathfinding. let mut too_large_onion = max_sized_onion.clone(); too_large_onion.payment_metadata.as_mut().map(|mut md| md.push(42)); + too_large_onion.total_mpp_amount_msat = MIN_FINAL_VALUE_ESTIMATE_WITH_OVERPAY; // First confirm we'll fail to create the onion packet directly. let secp_ctx = Secp256k1::signing_only(); @@ -167,6 +169,7 @@ fn large_payment_metadata() { payment_secret: Some(payment_secret_2), payment_metadata: Some(two_hop_metadata.clone()), custom_tlvs: Vec::new(), + total_mpp_amount_msat: amt_msat, }; let mut route_params_0_2 = route_0_2.route_params.clone().unwrap(); route_params_0_2.payment_params.max_path_length = 2; @@ -261,7 +264,7 @@ fn one_hop_blinded_path_with_custom_tlv() { - final_payload_len_without_custom_tlv; // Check that we can send the maximum custom TLV with 1 blinded hop. - let max_sized_onion = RecipientOnionFields::spontaneous_empty().with_custom_tlvs( + let max_sized_onion = RecipientOnionFields::spontaneous_empty(amt_msat).with_custom_tlvs( RecipientCustomTlvs::new(vec![(CUSTOM_TLV_TYPE, vec![42; max_custom_tlv_len])]).unwrap(), ); let id = PaymentId(payment_hash.0); @@ -369,7 +372,7 @@ fn blinded_path_with_custom_tlv() { let reserved_packet_bytes_without_custom_tlv: usize = onion_utils::build_onion_payloads( &route.paths[0], MIN_FINAL_VALUE_ESTIMATE_WITH_OVERPAY, - &RecipientOnionFields::spontaneous_empty(), + &RecipientOnionFields::spontaneous_empty(MIN_FINAL_VALUE_ESTIMATE_WITH_OVERPAY), nodes[0].best_block_info().1 + DEFAULT_MAX_TOTAL_CLTV_EXPIRY_DELTA, &None, None, @@ -387,7 +390,7 @@ fn blinded_path_with_custom_tlv() { - reserved_packet_bytes_without_custom_tlv; // Check that we can send the maximum custom TLV size with 0 intermediate unblinded hops. - let max_sized_onion = RecipientOnionFields::spontaneous_empty().with_custom_tlvs( + let max_sized_onion = RecipientOnionFields::spontaneous_empty(amt_msat).with_custom_tlvs( RecipientCustomTlvs::new(vec![(CUSTOM_TLV_TYPE, vec![42; max_custom_tlv_len])]).unwrap(), ); let no_retry = Retry::Attempts(0); @@ -420,10 +423,12 @@ fn blinded_path_with_custom_tlv() { .unwrap_err(); assert_eq!(err, RetryableSendFailure::OnionPacketSizeExceeded); - // Confirm that we can't construct an onion packet given this too-large custom TLV. + // Confirm that we can't construct an onion packet given this too-large custom TLV (as long as + // we actually use the amount the payment logic uses when validating). let secp_ctx = Secp256k1::signing_only(); route.paths[0].hops[0].fee_msat = MIN_FINAL_VALUE_ESTIMATE_WITH_OVERPAY; route.paths[0].hops[0].cltv_expiry_delta = DEFAULT_MAX_TOTAL_CLTV_EXPIRY_DELTA; + too_large_onion.total_mpp_amount_msat = MIN_FINAL_VALUE_ESTIMATE_WITH_OVERPAY; let err = onion_utils::create_payment_onion( &secp_ctx, &route.paths[0], diff --git a/lightning/src/ln/monitor_tests.rs b/lightning/src/ln/monitor_tests.rs index 097266cf83f..686010bd228 100644 --- a/lightning/src/ln/monitor_tests.rs +++ b/lightning/src/ln/monitor_tests.rs @@ -68,7 +68,7 @@ fn chanmon_fail_from_stale_commitment() { let (route, payment_hash, _, payment_secret) = get_route_and_payment_hash!(nodes[0], nodes[2], 1_000_000); nodes[0].node.send_payment_with_route(route, payment_hash, - RecipientOnionFields::secret_only(payment_secret), PaymentId(payment_hash.0)).unwrap(); + RecipientOnionFields::secret_only(payment_secret, 1_000_000), PaymentId(payment_hash.0)).unwrap(); check_added_monitors(&nodes[0], 1); let bs_txn = get_local_commitment_txn!(nodes[1], chan_id_2); @@ -885,7 +885,7 @@ fn do_test_balances_on_local_commitment_htlcs(keyed_anchors: bool, p2a_anchor: b let (route, payment_hash, _, payment_secret) = get_route_and_payment_hash!(nodes[0], nodes[1], 10_000_000); let htlc_cltv_timeout = nodes[0].best_block_info().1 + TEST_FINAL_CLTV + 1; // Note ChannelManager adds one to CLTV timeouts for safety nodes[0].node.send_payment_with_route(route, payment_hash, - RecipientOnionFields::secret_only(payment_secret), PaymentId(payment_hash.0)).unwrap(); + RecipientOnionFields::secret_only(payment_secret, 10_000_000), PaymentId(payment_hash.0)).unwrap(); check_added_monitors(&nodes[0], 1); let updates = get_htlc_update_msgs(&nodes[0], &nodes[1].node.get_our_node_id()); @@ -897,7 +897,7 @@ fn do_test_balances_on_local_commitment_htlcs(keyed_anchors: bool, p2a_anchor: b let (route_2, payment_hash_2, payment_preimage_2, payment_secret_2) = get_route_and_payment_hash!(nodes[0], nodes[1], 20_000_000); nodes[0].node.send_payment_with_route(route_2, payment_hash_2, - RecipientOnionFields::secret_only(payment_secret_2), PaymentId(payment_hash_2.0)).unwrap(); + RecipientOnionFields::secret_only(payment_secret_2, 20_000_000), PaymentId(payment_hash_2.0)).unwrap(); check_added_monitors(&nodes[0], 1); let updates = get_htlc_update_msgs(&nodes[0], &nodes[1].node.get_our_node_id()); @@ -3643,7 +3643,7 @@ fn do_test_lost_timeout_monitor_events(confirm_tx: CommitmentType, dust_htlcs: b let (route, hash_b, _, payment_secret_b) = get_route_and_payment_hash!(nodes[1], nodes[2], amt); - let onion = RecipientOnionFields::secret_only(payment_secret_b); + let onion = RecipientOnionFields::secret_only(payment_secret_b, amt); nodes[1].node.send_payment_with_route(route, hash_b, onion, PaymentId(hash_b.0)).unwrap(); check_added_monitors(&nodes[1], 1); diff --git a/lightning/src/ln/offers_tests.rs b/lightning/src/ln/offers_tests.rs index 12e631b4042..1c689686ccd 100644 --- a/lightning/src/ln/offers_tests.rs +++ b/lightning/src/ln/offers_tests.rs @@ -2463,7 +2463,7 @@ fn rejects_keysend_to_non_static_invoice_path() { let route_params = RouteParameters::from_payment_params_and_value(pay_params, amt_msat); let keysend_payment_id = PaymentId([2; 32]); let payment_hash = nodes[0].node.send_spontaneous_payment( - Some(payment_preimage), RecipientOnionFields::spontaneous_empty(), keysend_payment_id, + Some(payment_preimage), RecipientOnionFields::spontaneous_empty(amt_msat), keysend_payment_id, route_params, Retry::Attempts(0) ).unwrap(); check_added_monitors(&nodes[0], 1); diff --git a/lightning/src/ln/onion_payment.rs b/lightning/src/ln/onion_payment.rs index 555cc7a87af..d0d50c6a315 100644 --- a/lightning/src/ln/onion_payment.rs +++ b/lightning/src/ln/onion_payment.rs @@ -879,7 +879,7 @@ mod tests { let total_amt_msat = 1000; let cur_height = 1000; let pay_secret = PaymentSecret([99; 32]); - let recipient_onion = RecipientOnionFields::secret_only(pay_secret); + let recipient_onion = RecipientOnionFields::secret_only(pay_secret, total_amt_msat); let preimage_bytes = [43; 32]; let preimage = PaymentPreimage(preimage_bytes); let rhash_bytes = Sha256::hash(&preimage_bytes).to_byte_array(); diff --git a/lightning/src/ln/onion_route_tests.rs b/lightning/src/ln/onion_route_tests.rs index 27e0cfafade..1ee0be2938b 100644 --- a/lightning/src/ln/onion_route_tests.rs +++ b/lightning/src/ln/onion_route_tests.rs @@ -128,7 +128,8 @@ fn run_onion_failure_test_with_fail_intercept( // 0 ~~> 2 send payment let payment_id = PaymentId(nodes[0].keys_manager.backing.get_secure_random_bytes()); - let recipient_onion = RecipientOnionFields::secret_only(*payment_secret); + let recipient_onion = + RecipientOnionFields::secret_only(*payment_secret, route.get_total_amount()); nodes[0] .node .send_payment_with_route(route.clone(), *payment_hash, recipient_onion, payment_id) @@ -399,7 +400,7 @@ fn test_fee_failures() { // positive case let (route, payment_hash_success, payment_preimage_success, payment_secret_success) = get_route_and_payment_hash!(nodes[0], nodes[2], 40_000); - let recipient_onion = RecipientOnionFields::secret_only(payment_secret_success); + let recipient_onion = RecipientOnionFields::secret_only(payment_secret_success, 40_000); let payment_id = PaymentId(payment_hash_success.0); nodes[0] .node @@ -450,7 +451,7 @@ fn test_fee_failures() { let (payment_preimage_success, payment_hash_success, payment_secret_success) = get_payment_preimage_hash!(nodes[2]); - let recipient_onion = RecipientOnionFields::secret_only(payment_secret_success); + let recipient_onion = RecipientOnionFields::secret_only(payment_secret_success, 40_000); let payment_id = PaymentId(payment_hash_success.0); nodes[0] .node @@ -523,7 +524,7 @@ fn test_onion_failure() { let cur_height = nodes[0].best_block_info().1 + 1; let onion_keys = construct_onion_keys(&Secp256k1::new(), &route.paths[0], &session_priv); - let recipient_fields = RecipientOnionFields::spontaneous_empty(); + let recipient_fields = RecipientOnionFields::spontaneous_empty(40000); let path = &route.paths[0]; let (mut onion_payloads, _htlc_msat, _htlc_cltv) = build_onion_payloads(path, 40000, &recipient_fields, cur_height, &None, None, None) @@ -565,7 +566,7 @@ fn test_onion_failure() { let cur_height = nodes[0].best_block_info().1 + 1; let onion_keys = construct_onion_keys(&Secp256k1::new(), &route.paths[0], &session_priv); - let recipient_fields = RecipientOnionFields::spontaneous_empty(); + let recipient_fields = RecipientOnionFields::spontaneous_empty(40000); let path = &route.paths[0]; let (mut onion_payloads, _htlc_msat, _htlc_cltv) = build_onion_payloads(path, 40000, &recipient_fields, cur_height, &None, None, None) @@ -1284,7 +1285,7 @@ fn test_onion_failure() { CLTV_FAR_FAR_AWAY + route.paths[0].hops[0].cltv_expiry_delta + 1; let onion_keys = construct_onion_keys(&Secp256k1::new(), &route.paths[0], &session_priv); - let recipient_fields = RecipientOnionFields::spontaneous_empty(); + let recipient_fields = RecipientOnionFields::spontaneous_empty(40000); let path = &route.paths[0]; let (onion_payloads, _, htlc_cltv) = build_onion_payloads(path, 40000, &recipient_fields, height, &None, None, None) @@ -1542,7 +1543,7 @@ fn test_overshoot_final_cltv() { get_route_and_payment_hash!(nodes[0], nodes[2], 40000); let payment_id = PaymentId(nodes[0].keys_manager.backing.get_secure_random_bytes()); - let recipient_onion = RecipientOnionFields::secret_only(payment_secret); + let recipient_onion = RecipientOnionFields::secret_only(payment_secret, 40000); nodes[0] .node .send_payment_with_route(route, payment_hash, recipient_onion, payment_id) @@ -1837,7 +1838,7 @@ fn test_always_create_tlv_format_onion_payloads() { assert!(!hops[1].node_features.supports_variable_length_onion()); let cur_height = nodes[0].best_block_info().1 + 1; - let recipient_fields = RecipientOnionFields::spontaneous_empty(); + let recipient_fields = RecipientOnionFields::spontaneous_empty(40000); let path = &route.paths[0]; let (onion_payloads, _htlc_msat, _htlc_cltv) = build_onion_payloads(path, 40000, &recipient_fields, cur_height, &None, None, None) @@ -1973,7 +1974,7 @@ fn test_trampoline_onion_payload_assembly_values() { let payment_secret = PaymentSecret( SecretKey::from_slice(&>::from_hex(SECRET_HEX).unwrap()).unwrap().secret_bytes(), ); - let recipient_onion_fields = RecipientOnionFields::secret_only(payment_secret); + let recipient_onion_fields = RecipientOnionFields::secret_only(payment_secret, amt_msat); let (trampoline_payloads, outer_total_msat, outer_starting_htlc_offset) = onion_utils::build_trampoline_onion_payloads( &path.blinded_tail.as_ref().unwrap(), @@ -2038,6 +2039,8 @@ fn test_trampoline_onion_payload_assembly_values() { ) .unwrap(); + let recipient_onion_fields = + RecipientOnionFields::secret_only(payment_secret, outer_total_msat); let (outer_payloads, total_msat, total_htlc_offset) = build_onion_payloads( &path, outer_total_msat, @@ -2072,6 +2075,7 @@ fn test_trampoline_onion_payload_assembly_values() { panic!("Bob payload must be Forward"); } + let recipient_onion_fields = RecipientOnionFields::secret_only(payment_secret, amt_msat); let (_, total_msat_combined, total_htlc_offset_combined) = onion_utils::create_payment_onion( &Secp256k1::new(), &path, @@ -2280,7 +2284,7 @@ fn do_test_fail_htlc_backwards_with_reason(failure_code: FailureCode) { let payment_amount = 100_000; let (route, payment_hash, _, payment_secret) = get_route_and_payment_hash!(nodes[0], nodes[1], payment_amount); - let recipient_onion = RecipientOnionFields::secret_only(payment_secret); + let recipient_onion = RecipientOnionFields::secret_only(payment_secret, payment_amount); nodes[0] .node .send_payment_with_route(route, payment_hash, recipient_onion, PaymentId(payment_hash.0)) @@ -2430,7 +2434,7 @@ fn test_phantom_onion_hmac_failure() { let (route, phantom_scid) = get_phantom_route!(nodes, recv_value_msat, channel); // Route the HTLC through to the destination. - let recipient_onion = RecipientOnionFields::secret_only(payment_secret); + let recipient_onion = RecipientOnionFields::secret_only(payment_secret, recv_value_msat); nodes[0] .node .send_payment_with_route(route, payment_hash, recipient_onion, PaymentId(payment_hash.0)) @@ -2502,7 +2506,7 @@ fn test_phantom_invalid_onion_payload() { // We'll use the session priv later when constructing an invalid onion packet. let session_priv = [3; 32]; *nodes[0].keys_manager.override_random_bytes.lock().unwrap() = Some(session_priv); - let recipient_onion = RecipientOnionFields::secret_only(payment_secret); + let recipient_onion = RecipientOnionFields::secret_only(payment_secret, recv_value_msat); let payment_id = PaymentId(payment_hash.0); nodes[0] .node @@ -2534,7 +2538,8 @@ fn test_phantom_invalid_onion_payload() { let session_priv = SecretKey::from_slice(&session_priv).unwrap(); let mut onion_keys = construct_onion_keys(&Secp256k1::new(), &route.paths[0], &session_priv); - let recipient_onion_fields = RecipientOnionFields::secret_only(payment_secret); + let recipient_onion_fields = + RecipientOnionFields::secret_only(payment_secret, msgs::MAX_VALUE_MSAT + 1); let (mut onion_payloads, _, _) = build_onion_payloads( &route.paths[0], msgs::MAX_VALUE_MSAT + 1, @@ -2602,7 +2607,7 @@ fn test_phantom_final_incorrect_cltv_expiry() { let (route, phantom_scid) = get_phantom_route!(nodes, recv_value_msat, channel); // Route the HTLC through to the destination. - let recipient_onion = RecipientOnionFields::secret_only(payment_secret); + let recipient_onion = RecipientOnionFields::secret_only(payment_secret, recv_value_msat); nodes[0] .node .send_payment_with_route(route, payment_hash, recipient_onion, PaymentId(payment_hash.0)) @@ -2671,7 +2676,7 @@ fn test_phantom_failure_too_low_cltv() { route.paths[0].hops[1].cltv_expiry_delta = 5; // Route the HTLC through to the destination. - let recipient_onion = RecipientOnionFields::secret_only(payment_secret); + let recipient_onion = RecipientOnionFields::secret_only(payment_secret, recv_value_msat); nodes[0] .node .send_payment_with_route(route, payment_hash, recipient_onion, PaymentId(payment_hash.0)) @@ -2724,7 +2729,7 @@ fn test_phantom_failure_modified_cltv() { let (mut route, phantom_scid) = get_phantom_route!(nodes, recv_value_msat, channel); // Route the HTLC through to the destination. - let recipient_onion = RecipientOnionFields::secret_only(payment_secret); + let recipient_onion = RecipientOnionFields::secret_only(payment_secret, recv_value_msat); nodes[0] .node .send_payment_with_route(route, payment_hash, recipient_onion, PaymentId(payment_hash.0)) @@ -2779,7 +2784,7 @@ fn test_phantom_failure_expires_too_soon() { let (mut route, phantom_scid) = get_phantom_route!(nodes, recv_value_msat, channel); // Route the HTLC through to the destination. - let recipient_onion = RecipientOnionFields::secret_only(payment_secret); + let recipient_onion = RecipientOnionFields::secret_only(payment_secret, recv_value_msat); nodes[0] .node .send_payment_with_route(route, payment_hash, recipient_onion, PaymentId(payment_hash.0)) @@ -2829,7 +2834,8 @@ fn test_phantom_failure_too_low_recv_amt() { let (mut route, phantom_scid) = get_phantom_route!(nodes, bad_recv_amt_msat, channel); // Route the HTLC through to the destination. - let recipient_onion = RecipientOnionFields::secret_only(payment_secret); + let recipient_onion = + RecipientOnionFields::secret_only(payment_secret, route.get_total_amount()); nodes[0] .node .send_payment_with_route(route, payment_hash, recipient_onion, PaymentId(payment_hash.0)) @@ -2898,7 +2904,7 @@ fn do_test_phantom_dust_exposure_failure(multiplier_dust_limit: bool) { let (mut route, phantom_scid) = get_phantom_route!(nodes, max_dust_exposure + 1, channel); // Route the HTLC through to the destination. - let recipient_onion = RecipientOnionFields::secret_only(payment_secret); + let recipient_onion = RecipientOnionFields::secret_only(payment_secret, max_dust_exposure + 1); let payment_id = PaymentId(payment_hash.0); nodes[0] .node @@ -2948,7 +2954,7 @@ fn test_phantom_failure_reject_payment() { let (mut route, phantom_scid) = get_phantom_route!(nodes, recv_amt_msat, channel); // Route the HTLC through to the destination. - let recipient_onion = RecipientOnionFields::secret_only(payment_secret); + let recipient_onion = RecipientOnionFields::secret_only(payment_secret, recv_amt_msat); let payment_id = PaymentId(payment_hash.0); nodes[0] .node diff --git a/lightning/src/ln/onion_utils.rs b/lightning/src/ln/onion_utils.rs index d48fcb25179..b5c5088eecb 100644 --- a/lightning/src/ln/onion_utils.rs +++ b/lightning/src/ln/onion_utils.rs @@ -219,6 +219,7 @@ impl<'a, 'b> OnionPayload<'a, 'b> for msgs::OutboundOnionPayload<'a> { recipient_onion: &'a RecipientOnionFields, keysend_preimage: Option, sender_intended_htlc_amt_msat: u64, total_msat: u64, cltv_expiry_height: u32, ) -> Result { + debug_assert_eq!(total_msat, recipient_onion.total_mpp_amount_msat); Ok(Self::Receive { payment_data: recipient_onion .payment_secret @@ -257,6 +258,7 @@ impl<'a, 'b> OnionPayload<'a, 'b> for msgs::OutboundOnionPayload<'a> { total_msat: u64, amt_to_forward: u64, outgoing_cltv_value: u32, recipient_onion: &'a RecipientOnionFields, packet: msgs::TrampolineOnionPacket, ) -> Result { + debug_assert_eq!(total_msat, recipient_onion.total_mpp_amount_msat); Ok(Self::TrampolineEntrypoint { amt_to_forward, outgoing_cltv_value, @@ -443,6 +445,8 @@ pub(super) fn build_onion_payloads<'a>( invoice_request: Option<&'a InvoiceRequest>, trampoline_packet: Option, ) -> Result<(Vec>, u64, u32), APIError> { + debug_assert_eq!(total_msat, recipient_onion.total_mpp_amount_msat); + let mut res: Vec = Vec::with_capacity( path.hops.len() + path.blinded_tail.as_ref().map_or(0, |t| t.hops.len()), ); @@ -514,6 +518,8 @@ where let mut cur_cltv = starting_htlc_offset; let mut last_hop_id = None; + debug_assert_eq!(total_msat, recipient_onion.total_mpp_amount_msat); + for (idx, hop) in hops.rev().enumerate() { // First hop gets special values so that it can check, on receipt, that everything is // exactly as it should be (and the next hop isn't trying to probe to find out if we're @@ -661,11 +667,15 @@ pub(crate) fn set_max_path_length( maybe_announced_channel: false, }; let mut num_reserved_bytes: usize = 0; + // TODO: Find a way to avoid `clone`ing the whole recipient onion without re-adding the + // explicit amount parameter to build_onion_payloads_callback. + let mut recipient_onion_with_excess_value = recipient_onion.clone(); + recipient_onion_with_excess_value.total_mpp_amount_msat = final_value_msat_with_overpay_buffer; let build_payloads_res = build_onion_payloads_callback( core::iter::once(&unblinded_route_hop), blinded_tail_opt, final_value_msat_with_overpay_buffer, - &recipient_onion, + &recipient_onion_with_excess_value, best_block_height, &keysend_preimage, invoice_request, @@ -2623,10 +2633,19 @@ pub(crate) fn create_payment_onion_internal( prng_seed: [u8; 32], trampoline_session_priv_override: Option, trampoline_prng_seed_override: Option<[u8; 32]>, ) -> Result<(msgs::OnionPacket, u64, u32), APIError> { + debug_assert_eq!(total_msat, recipient_onion.total_mpp_amount_msat); + let mut outer_total_msat = total_msat; let mut outer_starting_htlc_offset = cur_block_height; let mut trampoline_packet_option = None; + let mut trampoline_outer_onion = RecipientOnionFields { + payment_secret: recipient_onion.payment_secret, + total_mpp_amount_msat: recipient_onion.total_mpp_amount_msat, + payment_metadata: None, + custom_tlvs: Vec::new(), + }; + let mut outer_onion = recipient_onion; if let Some(blinded_tail) = &path.blinded_tail { if !blinded_tail.trampoline_hops.is_empty() { let trampoline_payloads; @@ -2638,6 +2657,7 @@ pub(crate) fn create_payment_onion_internal( cur_block_height, keysend_preimage, )?; + trampoline_outer_onion.total_mpp_amount_msat = outer_total_msat; let trampoline_session_priv = trampoline_session_priv_override .unwrap_or_else(|| compute_trampoline_session_priv(session_priv)); @@ -2657,13 +2677,14 @@ pub(crate) fn create_payment_onion_internal( })?; trampoline_packet_option = Some(trampoline_packet); + outer_onion = &trampoline_outer_onion; } } let (onion_payloads, htlc_msat, htlc_cltv) = build_onion_payloads( &path, outer_total_msat, - recipient_onion, + outer_onion, outer_starting_htlc_offset, keysend_preimage, invoice_request, @@ -4029,7 +4050,7 @@ mod tests { max_total_routing_fee_msat: Some(u64::MAX), }; route_params.payment_params.max_total_cltv_expiry_delta = u32::MAX; - let recipient_onion = RecipientOnionFields::spontaneous_empty(); + let recipient_onion = RecipientOnionFields::spontaneous_empty(u64::MAX); set_max_path_length(&mut route_params, &recipient_onion, None, None, 42).unwrap(); } diff --git a/lightning/src/ln/outbound_payment.rs b/lightning/src/ln/outbound_payment.rs index 3380e4f0120..6a72601d4a0 100644 --- a/lightning/src/ln/outbound_payment.rs +++ b/lightning/src/ln/outbound_payment.rs @@ -21,6 +21,7 @@ use crate::ln::channelmanager::{ EventCompletionAction, HTLCSource, OptionalBolt11PaymentParams, PaymentCompleteUpdate, PaymentId, }; +use crate::ln::msgs::DecodeError; use crate::ln::onion_utils; use crate::ln::onion_utils::{DecodedOnionFailure, HTLCFailReason}; use crate::offers::invoice::{Bolt12Invoice, DerivedSigningPubkey, InvoiceBuilder}; @@ -44,8 +45,10 @@ use core::fmt::{self, Display, Formatter}; use core::sync::atomic::{AtomicBool, Ordering}; use core::time::Duration; +use crate::io; use crate::prelude::*; use crate::sync::Mutex; +use crate::util::ser; /// The number of ticks of [`ChannelManager::timer_tick_occurred`] until we time-out the idempotency /// of payments by [`PaymentId`]. See [`OutboundPayments::remove_stale_payments`]. @@ -758,21 +761,60 @@ pub struct RecipientOnionFields { pub payment_metadata: Option>, /// See [`Self::custom_tlvs`] for more info. pub(super) custom_tlvs: Vec<(u64, Vec)>, + /// The total payment amount which is being sent. + /// + /// This is communicated to the recipient as an indication that they should delay claiming the + /// payment until they've received multiple payment parts totaling at least this amount. + /// + /// Note that in order to properly communicate this, the recipient must either be paid using + /// blinded paths or a [`Self::payment_secret`] must be set. + pub total_mpp_amount_msat: u64, } -impl_writeable_tlv_based!(RecipientOnionFields, { - (0, payment_secret, option), - (1, custom_tlvs, optional_vec), - (2, payment_metadata, option), -}); +impl ser::Writeable for RecipientOnionFields { + fn write(&self, writer: &mut W) -> Result<(), io::Error> { + write_tlv_fields!(writer, { + (0, self.payment_secret, option), + (1, self.custom_tlvs, optional_vec), + (2, self.payment_metadata, option), + (3, self.total_mpp_amount_msat, required), + }); + Ok(()) + } +} + +impl ser::ReadableArgs for RecipientOnionFields { + fn read( + reader: &mut R, default_total_mpp_amount_msat: u64, + ) -> Result { + _init_and_read_len_prefixed_tlv_fields!(reader, { + (0, payment_secret, option), + (1, custom_tlvs, optional_vec), + (2, payment_metadata, option), + // Added and always written in LDK 0.3 + (3, total_mpp_amount_msat, option), + }); + Ok(Self { + payment_secret, + custom_tlvs: custom_tlvs.unwrap_or(Vec::new()), + payment_metadata, + total_mpp_amount_msat: total_mpp_amount_msat.unwrap_or(default_total_mpp_amount_msat), + }) + } +} impl RecipientOnionFields { /// Creates a [`RecipientOnionFields`] from only a [`PaymentSecret`]. This is the most common /// set of onion fields for today's BOLT11 invoices - most nodes require a [`PaymentSecret`] /// but do not require or provide any further data. #[rustfmt::skip] - pub fn secret_only(payment_secret: PaymentSecret) -> Self { - Self { payment_secret: Some(payment_secret), payment_metadata: None, custom_tlvs: Vec::new() } + pub fn secret_only(payment_secret: PaymentSecret, total_mpp_amount_msat: u64) -> Self { + Self { + payment_secret: Some(payment_secret), + payment_metadata: None, + custom_tlvs: Vec::new(), + total_mpp_amount_msat, + } } /// Creates a new [`RecipientOnionFields`] with no fields. This generally does not create @@ -783,8 +825,13 @@ impl RecipientOnionFields { /// /// [`ChannelManager::send_spontaneous_payment`]: super::channelmanager::ChannelManager::send_spontaneous_payment /// [`RecipientOnionFields::secret_only`]: RecipientOnionFields::secret_only - pub fn spontaneous_empty() -> Self { - Self { payment_secret: None, payment_metadata: None, custom_tlvs: Vec::new() } + pub fn spontaneous_empty(total_mpp_amount_msat: u64) -> Self { + Self { + payment_secret: None, + payment_metadata: None, + custom_tlvs: Vec::new(), + total_mpp_amount_msat, + } } /// Creates a new [`RecipientOnionFields`] from an existing one, adding validated custom TLVs. @@ -837,6 +884,9 @@ impl RecipientOnionFields { pub(super) fn check_merge(&mut self, further_htlc_fields: &mut Self) -> Result<(), ()> { if self.payment_secret != further_htlc_fields.payment_secret { return Err(()); } if self.payment_metadata != further_htlc_fields.payment_metadata { return Err(()); } + if self.total_mpp_amount_msat != further_htlc_fields.total_mpp_amount_msat { + return Err(()); + } let tlvs = &mut self.custom_tlvs; let further_tlvs = &mut further_htlc_fields.custom_tlvs; @@ -984,8 +1034,9 @@ impl OutboundPayments { (None, None) => return Err(Bolt11PaymentError::InvalidAmount), }; - let mut recipient_onion = RecipientOnionFields::secret_only(*invoice.payment_secret()) - .with_custom_tlvs(optional_params.custom_tlvs); + let mut recipient_onion = + RecipientOnionFields::secret_only(*invoice.payment_secret(), amount) + .with_custom_tlvs(optional_params.custom_tlvs); recipient_onion.payment_metadata = invoice.payment_metadata().map(|v| v.clone()); let payment_params = PaymentParameters::from_bolt11_invoice(invoice) @@ -1084,6 +1135,7 @@ impl OutboundPayments { payment_secret: None, payment_metadata: None, custom_tlvs: vec![], + total_mpp_amount_msat: route_params.final_value_msat, }; let route = match self.find_initial_route( payment_id, payment_hash, &recipient_onion, keysend_preimage, invoice_request, @@ -1224,7 +1276,7 @@ impl OutboundPayments { if let Err(()) = onion_utils::set_max_path_length( &mut route_params, - &RecipientOnionFields::spontaneous_empty(), + &RecipientOnionFields::spontaneous_empty(amount_msat), Some(keysend_preimage), Some(invreq), best_block_height, @@ -1620,6 +1672,7 @@ impl OutboundPayments { payment_secret: *payment_secret, payment_metadata: payment_metadata.clone(), custom_tlvs: custom_tlvs.clone(), + total_mpp_amount_msat: total_msat, }; let keysend_preimage = *keysend_preimage; let invoice_request = invoice_request.clone(); @@ -1824,15 +1877,16 @@ impl OutboundPayments { } let route = Route { paths: vec![path], route_params: None }; + let recipient_onion_fields = + RecipientOnionFields::secret_only(payment_secret, route.get_total_amount()); let onion_session_privs = self.add_new_pending_payment(payment_hash, - RecipientOnionFields::secret_only(payment_secret), payment_id, None, &route, None, None, + recipient_onion_fields.clone(), payment_id, None, &route, None, None, entropy_source, best_block_height, None ).map_err(|e| { debug_assert!(matches!(e, PaymentSendFailure::DuplicatePayment)); ProbeSendFailure::DuplicateProbe })?; - let recipient_onion_fields = RecipientOnionFields::spontaneous_empty(); match self.pay_route_internal(&route, payment_hash, &recipient_onion_fields, None, None, None, payment_id, None, &onion_session_privs, false, node_signer, best_block_height, &send_payment_along_path @@ -2847,7 +2901,7 @@ mod tests { #[test] #[rustfmt::skip] fn test_recipient_onion_fields_with_custom_tlvs() { - let onion_fields = RecipientOnionFields::spontaneous_empty(); + let onion_fields = RecipientOnionFields::spontaneous_empty(42); let bad_type_range_tlvs = RecipientCustomTlvs::new(vec![ (0, vec![42]), @@ -2895,7 +2949,7 @@ mod tests { let expired_route_params = RouteParameters::from_payment_params_and_value(payment_params, 0); let pending_events = Mutex::new(VecDeque::new()); if on_retry { - outbound_payments.add_new_pending_payment(PaymentHash([0; 32]), RecipientOnionFields::spontaneous_empty(), + outbound_payments.add_new_pending_payment(PaymentHash([0; 32]), RecipientOnionFields::spontaneous_empty(0), PaymentId([0; 32]), None, &Route { paths: vec![], route_params: None }, Some(Retry::Attempts(1)), Some(expired_route_params.payment_params.clone()), &&keys_manager, 0, None).unwrap(); @@ -2910,7 +2964,7 @@ mod tests { } else { panic!("Unexpected event"); } } else { let err = outbound_payments.send_payment( - PaymentHash([0; 32]), RecipientOnionFields::spontaneous_empty(), PaymentId([0; 32]), + PaymentHash([0; 32]), RecipientOnionFields::spontaneous_empty(0), PaymentId([0; 32]), Retry::Attempts(0), expired_route_params, &&router, vec![], || InFlightHtlcs::new(), &&keys_manager, &&keys_manager, 0, &pending_events, |_| Ok(()), &log).unwrap_err(); if let RetryableSendFailure::PaymentExpired = err { } else { panic!("Unexpected error"); } @@ -2941,7 +2995,7 @@ mod tests { let pending_events = Mutex::new(VecDeque::new()); if on_retry { - outbound_payments.add_new_pending_payment(PaymentHash([0; 32]), RecipientOnionFields::spontaneous_empty(), + outbound_payments.add_new_pending_payment(PaymentHash([0; 32]), RecipientOnionFields::spontaneous_empty(0), PaymentId([0; 32]), None, &Route { paths: vec![], route_params: None }, Some(Retry::Attempts(1)), Some(route_params.payment_params.clone()), &&keys_manager, 0, None).unwrap(); @@ -2954,7 +3008,7 @@ mod tests { if let Event::PaymentFailed { .. } = events[0].0 { } else { panic!("Unexpected event"); } } else { let err = outbound_payments.send_payment( - PaymentHash([0; 32]), RecipientOnionFields::spontaneous_empty(), PaymentId([0; 32]), + PaymentHash([0; 32]), RecipientOnionFields::spontaneous_empty(0), PaymentId([0; 32]), Retry::Attempts(0), route_params, &&router, vec![], || InFlightHtlcs::new(), &&keys_manager, &&keys_manager, 0, &pending_events, |_| Ok(()), &log).unwrap_err(); if let RetryableSendFailure::RouteNotFound = err { @@ -3005,7 +3059,7 @@ mod tests { // PaymentPathFailed event. let pending_events = Mutex::new(VecDeque::new()); outbound_payments.send_payment( - PaymentHash([0; 32]), RecipientOnionFields::spontaneous_empty(), PaymentId([0; 32]), + PaymentHash([0; 32]), RecipientOnionFields::spontaneous_empty(1), PaymentId([0; 32]), Retry::Attempts(0), route_params.clone(), &&router, vec![], || InFlightHtlcs::new(), &&keys_manager, &&keys_manager, 0, &pending_events, |_| Err(APIError::ChannelUnavailable { err: "test".to_owned() }), &log).unwrap(); @@ -3023,7 +3077,7 @@ mod tests { // Ensure that a MonitorUpdateInProgress "error" will not result in a PaymentPathFailed event. outbound_payments.send_payment( - PaymentHash([0; 32]), RecipientOnionFields::spontaneous_empty(), PaymentId([0; 32]), + PaymentHash([0; 32]), RecipientOnionFields::spontaneous_empty(1), PaymentId([0; 32]), Retry::Attempts(0), route_params.clone(), &&router, vec![], || InFlightHtlcs::new(), &&keys_manager, &&keys_manager, 0, &pending_events, |_| Err(APIError::MonitorUpdateInProgress), &log).unwrap(); @@ -3031,7 +3085,7 @@ mod tests { // Ensure that any other error will result in a PaymentPathFailed event but no blamed scid. outbound_payments.send_payment( - PaymentHash([0; 32]), RecipientOnionFields::spontaneous_empty(), PaymentId([1; 32]), + PaymentHash([0; 32]), RecipientOnionFields::spontaneous_empty(1), PaymentId([1; 32]), Retry::Attempts(0), route_params.clone(), &&router, vec![], || InFlightHtlcs::new(), &&keys_manager, &&keys_manager, 0, &pending_events, |_| Err(APIError::APIMisuseError { err: "test".to_owned() }), &log).unwrap(); diff --git a/lightning/src/ln/payment_tests.rs b/lightning/src/ln/payment_tests.rs index e6c3c6eb121..a4eaa1653ca 100644 --- a/lightning/src/ln/payment_tests.rs +++ b/lightning/src/ln/payment_tests.rs @@ -146,7 +146,7 @@ fn mpp_retry() { let mut route_params = route.route_params.clone().unwrap(); nodes[0].router.expect_find_route(route_params.clone(), Ok(route.clone())); - let onion = RecipientOnionFields::secret_only(pay_secret); + let onion = RecipientOnionFields::secret_only(pay_secret, amt_msat * 2); let retry = Retry::Attempts(1); nodes[0].node.send_payment(hash, onion, id, route_params.clone(), retry).unwrap(); check_added_monitors(&nodes[0], 2); // one monitor per path @@ -264,7 +264,7 @@ fn mpp_retry_overpay() { let mut route_params = route.route_params.clone().unwrap(); nodes[0].router.expect_find_route(route_params.clone(), Ok(route.clone())); - let onion = RecipientOnionFields::secret_only(pay_secret); + let onion = RecipientOnionFields::secret_only(pay_secret, amt_msat); let retry = Retry::Attempts(1); nodes[0].node.send_payment(hash, onion, id, route_params.clone(), retry).unwrap(); check_added_monitors(&nodes[0], 2); // one monitor per path @@ -366,7 +366,7 @@ fn do_mpp_receive_timeout(send_partial_mpp: bool) { route.route_params.as_mut().unwrap().final_value_msat *= 2; // Initiate the MPP payment. - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, 200_000); nodes[0].node.send_payment_with_route(route, hash, onion, PaymentId(hash.0)).unwrap(); check_added_monitors(&nodes[0], 2); // one monitor per path let mut events = nodes[0].node.get_and_clear_pending_msg_events(); @@ -461,7 +461,7 @@ fn do_test_keysend_payments(public_node: bool) { { let preimage = Some(PaymentPreimage([42; 32])); - let onion = RecipientOnionFields::spontaneous_empty(); + let onion = RecipientOnionFields::spontaneous_empty(10000); let retry = Retry::Attempts(1); let id = PaymentId([42; 32]); nodes[0].node.send_spontaneous_payment(preimage, onion, id, route_params, retry).unwrap(); @@ -511,7 +511,7 @@ fn test_mpp_keysend() { let preimage = Some(PaymentPreimage([42; 32])); let payment_secret = PaymentSecret([42; 32]); - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, recv_value); let retry = Retry::Attempts(0); let id = PaymentId([42; 32]); let hash = @@ -554,7 +554,7 @@ fn test_fulfill_hold_times() { let preimage = Some(PaymentPreimage([42; 32])); let payment_secret = PaymentSecret([42; 32]); - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, recv_value); let retry = Retry::Attempts(0); let id = PaymentId([42; 32]); let hash = @@ -624,7 +624,7 @@ fn test_reject_mpp_keysend_htlc_mismatching_secret() { let payment_id_0 = PaymentId(nodes[0].keys_manager.backing.get_secure_random_bytes()); nodes[0].router.expect_find_route(route.route_params.clone().unwrap(), Ok(route.clone())); let params = route.route_params.clone().unwrap(); - let onion = RecipientOnionFields::spontaneous_empty(); + let onion = RecipientOnionFields::spontaneous_empty(amount); let retry = Retry::Attempts(0); nodes[0].node.send_spontaneous_payment(preimage, onion, payment_id_0, params, retry).unwrap(); check_added_monitors(&nodes[0], 1); @@ -672,7 +672,7 @@ fn test_reject_mpp_keysend_htlc_mismatching_secret() { let payment_id_1 = PaymentId(nodes[0].keys_manager.backing.get_secure_random_bytes()); nodes[0].router.expect_find_route(route.route_params.clone().unwrap(), Ok(route.clone())); - let onion = RecipientOnionFields::spontaneous_empty(); + let onion = RecipientOnionFields::spontaneous_empty(amount); let params = route.route_params.clone().unwrap(); let retry = Retry::Attempts(0); nodes[0].node.send_spontaneous_payment(preimage, onion, payment_id_1, params, retry).unwrap(); @@ -761,7 +761,7 @@ fn no_pending_leak_on_initial_send_failure() { nodes[0].node.peer_disconnected(node_b_id); nodes[1].node.peer_disconnected(node_a_id); - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, 100_000); let payment_id = PaymentId(payment_hash.0); let res = nodes[0].node.send_payment_with_route(route, payment_hash, onion, payment_id); unwrap_send_err!(nodes[0], res, true, APIError::ChannelUnavailable { ref err }, @@ -809,7 +809,7 @@ fn do_retry_with_no_persist(confirm_before_reload: bool) { send_along_route(&nodes[0], route.clone(), &[&nodes[1], &nodes[2]], 1_000_000); let route_params = route.route_params.unwrap().clone(); - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, amt_msat); let id = PaymentId(payment_hash.0); nodes[0].node.send_payment(payment_hash, onion, id, route_params, Retry::Attempts(1)).unwrap(); check_added_monitors(&nodes[0], 1); @@ -986,7 +986,7 @@ fn do_retry_with_no_persist(confirm_before_reload: bool) { nodes[1].node.timer_tick_occurred(); } - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, 1_000_000); // Check that we cannot retry a fulfilled payment nodes[0] .node @@ -994,7 +994,7 @@ fn do_retry_with_no_persist(confirm_before_reload: bool) { .unwrap_err(); // ...but if we send with a different PaymentId the payment should fly let id = PaymentId(payment_hash.0); - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, 1_000_000); nodes[0].node.send_payment_with_route(new_route.clone(), payment_hash, onion, id).unwrap(); check_added_monitors(&nodes[0], 1); @@ -1164,7 +1164,7 @@ fn do_test_completed_payment_not_retryable_on_reload(use_dust: bool) { // If we attempt to retry prior to the HTLC-Timeout (or commitment transaction, for dust HTLCs) // confirming, we will fail as it's considered still-pending... let (new_route, _, _, _) = get_route_and_payment_hash!(nodes[0], nodes[2], amt); - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, amt); match nodes[0].node.send_payment_with_route(new_route.clone(), hash, onion, payment_id) { Err(RetryableSendFailure::DuplicatePayment) => {}, _ => panic!("Unexpected error"), @@ -1184,7 +1184,7 @@ fn do_test_completed_payment_not_retryable_on_reload(use_dust: bool) { node_a_ser = nodes[0].node.encode(); // After the payment failed, we're free to send it again. - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, amt); nodes[0].node.send_payment_with_route(new_route.clone(), hash, onion, payment_id).unwrap(); assert!(!nodes[0].node.get_and_clear_pending_msg_events().is_empty()); @@ -1201,13 +1201,13 @@ fn do_test_completed_payment_not_retryable_on_reload(use_dust: bool) { // Now resend the payment, delivering the HTLC and actually claiming it this time. This ensures // the payment is not (spuriously) listed as still pending. - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, amt); nodes[0].node.send_payment_with_route(new_route.clone(), hash, onion, payment_id).unwrap(); check_added_monitors(&nodes[0], 1); pass_along_route(&nodes[0], &[&[&nodes[1], &nodes[2]]], amt, hash, payment_secret); claim_payment(&nodes[0], &[&nodes[1], &nodes[2]], payment_preimage); - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, amt); match nodes[0].node.send_payment_with_route(new_route.clone(), hash, onion, payment_id) { Err(RetryableSendFailure::DuplicatePayment) => {}, _ => panic!("Unexpected error"), @@ -1229,7 +1229,7 @@ fn do_test_completed_payment_not_retryable_on_reload(use_dust: bool) { reconnect_nodes(ReconnectArgs::new(&nodes[0], &nodes[1])); - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, amt); match nodes[0].node.send_payment_with_route(new_route, hash, onion, payment_id) { Err(RetryableSendFailure::DuplicatePayment) => {}, _ => panic!("Unexpected error"), @@ -1520,7 +1520,7 @@ fn get_ldk_payment_preimage() { &Default::default(), &random_seed_bytes, ); - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, amt_msat); let id = PaymentId(payment_hash.0); nodes[0].node.send_payment_with_route(route.unwrap(), payment_hash, onion, id).unwrap(); check_added_monitors(&nodes[0], 1); @@ -1873,7 +1873,7 @@ fn claimed_send_payment_idempotent() { () => { // If we try to resend a new payment with a different payment_hash but with the same // payment_id, it should be rejected. - let onion = RecipientOnionFields::secret_only(second_payment_secret); + let onion = RecipientOnionFields::secret_only(second_payment_secret, 100_000); let send_result = nodes[0].node.send_payment_with_route(route.clone(), hash_b, onion, payment_id); match send_result { @@ -1885,7 +1885,7 @@ fn claimed_send_payment_idempotent() { // also be rejected. let send_result = nodes[0].node.send_spontaneous_payment( None, - RecipientOnionFields::spontaneous_empty(), + RecipientOnionFields::spontaneous_empty(100_000), payment_id, route.route_params.clone().unwrap(), Retry::Attempts(0), @@ -1929,7 +1929,7 @@ fn claimed_send_payment_idempotent() { nodes[0].node.timer_tick_occurred(); } - let onion = RecipientOnionFields::secret_only(second_payment_secret); + let onion = RecipientOnionFields::secret_only(second_payment_secret, 100_000); nodes[0].node.send_payment_with_route(route, hash_b, onion, payment_id).unwrap(); check_added_monitors(&nodes[0], 1); pass_along_route(&nodes[0], &[&[&nodes[1]]], 100_000, hash_b, second_payment_secret); @@ -1956,7 +1956,7 @@ fn abandoned_send_payment_idempotent() { () => { // If we try to resend a new payment with a different payment_hash but with the same // payment_id, it should be rejected. - let onion = RecipientOnionFields::secret_only(second_payment_secret); + let onion = RecipientOnionFields::secret_only(second_payment_secret, 100_000); let send_result = nodes[0].node.send_payment_with_route(route.clone(), hash_b, onion, payment_id); match send_result { @@ -1968,7 +1968,7 @@ fn abandoned_send_payment_idempotent() { // also be rejected. let send_result = nodes[0].node.send_spontaneous_payment( None, - RecipientOnionFields::spontaneous_empty(), + RecipientOnionFields::spontaneous_empty(100_000), payment_id, route.route_params.clone().unwrap(), Retry::Attempts(0), @@ -1998,7 +1998,7 @@ fn abandoned_send_payment_idempotent() { // However, we can reuse the PaymentId immediately after we `abandon_payment` upon passing the // failed payment back. - let onion = RecipientOnionFields::secret_only(second_payment_secret); + let onion = RecipientOnionFields::secret_only(second_payment_secret, 100_000); nodes[0].node.send_payment_with_route(route, hash_b, onion, payment_id).unwrap(); check_added_monitors(&nodes[0], 1); pass_along_route(&nodes[0], &[&[&nodes[1]]], 100_000, hash_b, second_payment_secret); @@ -2166,12 +2166,12 @@ fn test_holding_cell_inflight_htlcs() { // Queue up two payments - one will be delivered right away, one immediately goes into the // holding cell as nodes[0] is AwaitingRAA. { - let onion = RecipientOnionFields::secret_only(payment_secret_1); + let onion = RecipientOnionFields::secret_only(payment_secret_1, 1000000); let id = PaymentId(payment_hash_1.0); nodes[0].node.send_payment_with_route(route.clone(), payment_hash_1, onion, id).unwrap(); check_added_monitors(&nodes[0], 1); - let onion = RecipientOnionFields::secret_only(payment_secret_2); + let onion = RecipientOnionFields::secret_only(payment_secret_2, 1000000); let id = PaymentId(payment_hash_2.0); nodes[0].node.send_payment_with_route(route, payment_hash_2, onion, id).unwrap(); check_added_monitors(&nodes[0], 0); @@ -2262,7 +2262,7 @@ fn do_test_intercepted_payment(test: InterceptTest) { let (hash, payment_secret) = nodes[2].node.create_inbound_payment(Some(amt_msat), 60 * 60, None).unwrap(); - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, amt_msat); let id = PaymentId(hash.0); nodes[0].node.send_payment_with_route(route.clone(), hash, onion, id).unwrap(); let payment_event = { @@ -2498,7 +2498,7 @@ fn do_accept_underpaying_htlcs_config(num_mpp_parts: usize) { let (payment_hash, payment_secret) = nodes[2].node.create_inbound_payment(Some(amt_msat), 60 * 60, None).unwrap(); - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, amt_msat); let id = PaymentId(payment_hash.0); nodes[0].node.send_payment(payment_hash, onion, id, route_params, Retry::Attempts(0)).unwrap(); @@ -2710,7 +2710,7 @@ fn do_automatic_retries(test: AutoRetry) { if test == AutoRetry::Success { // Test that we can succeed on the first retry. - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, amt_msat); let id = PaymentId(hash.0); let retry = Retry::Attempts(1); nodes[0].node.send_payment(hash, onion, id, route_params, retry).unwrap(); @@ -2736,7 +2736,7 @@ fn do_automatic_retries(test: AutoRetry) { preimage, )); } else if test == AutoRetry::Spontaneous { - let onion = RecipientOnionFields::spontaneous_empty(); + let onion = RecipientOnionFields::spontaneous_empty(amt_msat); let id = PaymentId(hash.0); nodes[0] .node @@ -2761,7 +2761,7 @@ fn do_automatic_retries(test: AutoRetry) { claim_payment_along_route(ClaimAlongRouteArgs::new(&nodes[0], &[path], preimage)); } else if test == AutoRetry::FailAttempts { // Ensure ChannelManager will not retry a payment if it has run out of payment attempts. - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, amt_msat); let id = PaymentId(hash.0); nodes[0].node.send_payment(hash, onion, id, route_params, Retry::Attempts(1)).unwrap(); pass_failed_attempt_with_retry_along_path!(channel_id_2, true); @@ -2782,7 +2782,7 @@ fn do_automatic_retries(test: AutoRetry) { #[cfg(feature = "std")] { // Ensure ChannelManager will not retry a payment if it times out due to Retry::Timeout. - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, amt_msat); let id = PaymentId(hash.0); let retry = Retry::Timeout(Duration::from_secs(60)); nodes[0].node.send_payment(hash, onion, id, route_params, retry).unwrap(); @@ -2810,7 +2810,7 @@ fn do_automatic_retries(test: AutoRetry) { } else if test == AutoRetry::FailOnRestart { // Ensure ChannelManager will not retry a payment after restart, even if there were retry // attempts remaining prior to restart. - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, amt_msat); let id = PaymentId(hash.0); nodes[0].node.send_payment(hash, onion, id, route_params, Retry::Attempts(2)).unwrap(); pass_failed_attempt_with_retry_along_path!(channel_id_2, true); @@ -2844,7 +2844,7 @@ fn do_automatic_retries(test: AutoRetry) { _ => panic!("Unexpected event"), } } else if test == AutoRetry::FailOnRetry { - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, amt_msat); let id = PaymentId(hash.0); nodes[0].node.send_payment(hash, onion, id, route_params, Retry::Attempts(1)).unwrap(); pass_failed_attempt_with_retry_along_path!(channel_id_2, true); @@ -3004,7 +3004,7 @@ fn auto_retry_partial_failure() { nodes[0].router.expect_find_route(retry_2_params, Ok(retry_2_route)); // Send a payment that will partially fail on send, then partially fail on retry, then succeed. - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, amt_msat); let id = PaymentId(payment_hash.0); nodes[0].node.send_payment(payment_hash, onion, id, route_params, Retry::Attempts(3)).unwrap(); @@ -3164,7 +3164,7 @@ fn auto_retry_zero_attempts_send_error() { }; nodes[0].router.expect_find_route(route_params.clone(), Ok(send_route)); - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, amt_msat); let id = PaymentId(payment_hash.0); nodes[0].node.send_payment(payment_hash, onion, id, route_params, Retry::Attempts(0)).unwrap(); @@ -3212,7 +3212,7 @@ fn fails_paying_after_rejected_by_payee() { .unwrap(); let route_params = RouteParameters::from_payment_params_and_value(payment_params, amt_msat); - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, amt_msat); let id = PaymentId(payment_hash.0); nodes[0].node.send_payment(payment_hash, onion, id, route_params, Retry::Attempts(1)).unwrap(); check_added_monitors(&nodes[0], 1); @@ -3328,7 +3328,9 @@ fn retry_multi_path_single_failed_payment() { scorer.expect_usage(chans[1].short_channel_id.unwrap(), usage); } - let onion = RecipientOnionFields::secret_only(payment_secret); + // Note that while we actaully pay amt_msat + 1, we should really set the onion amount to + // amt_msat as that's what we built a route for. + let onion = RecipientOnionFields::secret_only(payment_secret, amt_msat + 1); let id = PaymentId(payment_hash.0); nodes[0].node.send_payment(payment_hash, onion, id, route_params, Retry::Attempts(1)).unwrap(); let events = nodes[0].node.get_and_clear_pending_events(); @@ -3409,7 +3411,7 @@ fn immediate_retry_on_failure() { route.route_params = Some(retry_params.clone()); nodes[0].router.expect_find_route(retry_params, Ok(route.clone())); - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, amt_msat); let id = PaymentId(payment_hash.0); nodes[0].node.send_payment(payment_hash, onion, id, route_params, Retry::Attempts(1)).unwrap(); let events = nodes[0].node.get_and_clear_pending_events(); @@ -3548,7 +3550,7 @@ fn no_extra_retries_on_back_to_back_fail() { // We can't use the commitment_signed_dance macro helper because in this test we'll be sending // two HTLCs back-to-back on the same channel, and the macro only expects to handle one at a // time. - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, amt_msat); let id = PaymentId(payment_hash.0); nodes[0].node.send_payment(payment_hash, onion, id, route_params, Retry::Attempts(1)).unwrap(); @@ -3793,7 +3795,7 @@ fn test_simple_partial_retry() { // We can't use the commitment_signed_dance macro helper because in this test we'll be sending // two HTLCs back-to-back on the same channel, and the macro only expects to handle one at a // time. - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, amt_msat); let id = PaymentId(payment_hash.0); nodes[0].node.send_payment(payment_hash, onion, id, route_params, Retry::Attempts(1)).unwrap(); let first_htlc = SendEvent::from_node(&nodes[0]); @@ -3995,7 +3997,7 @@ fn test_threaded_payment_retries() { }; nodes[0].router.expect_find_route(route_params.clone(), Ok(route.clone())); - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, amt_msat); let id = PaymentId(payment_hash.0); let retry = Retry::Attempts(0xdeadbeef); nodes[0].node.send_payment(payment_hash, onion, id, route_params.clone(), retry).unwrap(); @@ -4294,7 +4296,7 @@ fn do_claim_from_closed_chan(fail_payment: bool) { let final_cltv = nodes[0].best_block_info().1 + TEST_FINAL_CLTV + 8 + 1; nodes[0].router.expect_find_route(route_params.clone(), Ok(route.clone())); - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, amt_msat); let id = PaymentId(hash.0); nodes[0].node.send_payment(hash, onion, id, route_params, Retry::Attempts(1)).unwrap(); @@ -4463,6 +4465,7 @@ fn do_test_custom_tlvs(spontaneous: bool, even_tlvs: bool, known_tlvs: bool) { payment_secret: if spontaneous { None } else { Some(payment_secret) }, payment_metadata: None, custom_tlvs: custom_tlvs.clone(), + total_mpp_amount_msat: amt_msat, }; if spontaneous { let params = route.route_params.unwrap(); @@ -4543,7 +4546,7 @@ fn test_retry_custom_tlvs() { let mut route_params = route.route_params.clone().unwrap(); let custom_tlvs = vec![((1 << 16) + 1, vec![0x42u8; 16])]; - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, amt_msat); let onion = onion.with_custom_tlvs(RecipientCustomTlvs::new(custom_tlvs.clone()).unwrap()); nodes[0].router.expect_find_route(route_params.clone(), Ok(route.clone())); @@ -4675,6 +4678,7 @@ fn do_test_custom_tlvs_consistency( payment_secret: Some(payment_secret), payment_metadata: None, custom_tlvs: first_tlvs, + total_mpp_amount_msat: amt_msat, }; let session_privs = nodes[0].node.test_add_new_pending_payment(hash, onion.clone(), id, &route).unwrap(); @@ -4700,6 +4704,7 @@ fn do_test_custom_tlvs_consistency( payment_secret: Some(payment_secret), payment_metadata: None, custom_tlvs: second_tlvs, + total_mpp_amount_msat: amt_msat, }; let path_b = &route.paths[1]; let priv_b = session_privs[1]; @@ -4824,6 +4829,7 @@ fn do_test_payment_metadata_consistency(do_reload: bool, do_modify: bool) { payment_secret: Some(payment_secret), payment_metadata: Some(payment_metadata), custom_tlvs: vec![], + total_mpp_amount_msat: amt_msat, }; let retry = Retry::Attempts(1); nodes[0].node.send_payment(payment_hash, onion, payment_id, route_params, retry).unwrap(); @@ -5018,7 +5024,10 @@ fn test_htlc_forward_considers_anchor_outputs_value() { nodes[2], sendable_balance_msat + anchor_outpus_value_msat ); - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only( + payment_secret, + sendable_balance_msat + anchor_outpus_value_msat, + ); let id = PaymentId(payment_hash.0); nodes[0].node.send_payment_with_route(route, payment_hash, onion, id).unwrap(); check_added_monitors(&nodes[0], 1); @@ -5083,7 +5092,7 @@ fn peel_payment_onion_custom_tlvs() { let payment_params = PaymentParameters::for_keysend(node_b_id, TEST_FINAL_CLTV, false); let route_params = RouteParameters::from_payment_params_and_value(payment_params, amt_msat); let route = functional_test_utils::get_route(&nodes[0], &route_params).unwrap(); - let mut recipient_onion = RecipientOnionFields::spontaneous_empty() + let mut recipient_onion = RecipientOnionFields::spontaneous_empty(amt_msat) .with_custom_tlvs(RecipientCustomTlvs::new(vec![(414141, vec![42; 1200])]).unwrap()); let prng_seed = chanmon_cfgs[0].keys_manager.get_secure_random_bytes(); let session_priv = SecretKey::from_slice(&prng_seed[..]).expect("RNG is busted"); @@ -5178,7 +5187,7 @@ fn test_non_strict_forwarding() { for i in 0..4 { let (payment_preimage, payment_hash, payment_secret) = get_payment_preimage_hash(&nodes[2], Some(payment_value), None); - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, payment_value); let id = PaymentId(payment_hash.0); nodes[0].node.send_payment_with_route(route.clone(), payment_hash, onion, id).unwrap(); check_added_monitors(&nodes[0], 1); @@ -5217,7 +5226,7 @@ fn test_non_strict_forwarding() { // Send a 5th payment which will fail. let (_, payment_hash, payment_secret) = get_payment_preimage_hash(&nodes[2], Some(payment_value), None); - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, payment_value); let id = PaymentId(payment_hash.0); nodes[0].node.send_payment_with_route(route.clone(), payment_hash, onion, id).unwrap(); @@ -5278,7 +5287,7 @@ fn remove_pending_outbounds_on_buggy_router() { nodes[0].router.expect_find_route(route_params.clone(), Ok(route.clone())); // Send the payment with one retry allowed, but the payment should still fail - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, amt_msat); let retry = Retry::Attempts(1); nodes[0].node.send_payment(payment_hash, onion, payment_id, route_params, retry).unwrap(); let events = nodes[0].node.get_and_clear_pending_events(); @@ -5354,7 +5363,7 @@ fn pay_route_without_params() { get_route_and_payment_hash!(nodes[0], nodes[1], payment_params, amt_msat); route.route_params.take(); - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, amt_msat); let id = PaymentId(hash.0); nodes[0].node.send_payment_with_route(route, hash, onion, id).unwrap(); diff --git a/lightning/src/ln/priv_short_conf_tests.rs b/lightning/src/ln/priv_short_conf_tests.rs index 2035af15046..664d52a1a33 100644 --- a/lightning/src/ln/priv_short_conf_tests.rs +++ b/lightning/src/ln/priv_short_conf_tests.rs @@ -81,7 +81,7 @@ fn test_priv_forwarding_rejection() { let (route, our_payment_hash, our_payment_preimage, our_payment_secret) = get_route_and_payment_hash!(nodes[0], nodes[2], payment_params, 10_000); - let onion = RecipientOnionFields::secret_only(our_payment_secret); + let onion = RecipientOnionFields::secret_only(our_payment_secret, 10_000); let id = PaymentId(our_payment_hash.0); nodes[0].node.send_payment_with_route(route.clone(), our_payment_hash, onion, id).unwrap(); check_added_monitors(&nodes[0], 1); @@ -164,7 +164,7 @@ fn test_priv_forwarding_rejection() { get_event_msg!(nodes[1], MessageSendEvent::SendChannelUpdate, node_c_id); get_event_msg!(nodes[2], MessageSendEvent::SendChannelUpdate, node_b_id); - let onion = RecipientOnionFields::secret_only(our_payment_secret); + let onion = RecipientOnionFields::secret_only(our_payment_secret, 10_000); let id = PaymentId(our_payment_hash.0); nodes[0].node.send_payment_with_route(route, our_payment_hash, onion, id).unwrap(); check_added_monitors(&nodes[0], 1); @@ -348,7 +348,7 @@ fn test_routed_scid_alias() { get_route_and_payment_hash!(nodes[0], nodes[2], payment_params, 100_000); assert_eq!(route.paths[0].hops[1].short_channel_id, last_hop[0].inbound_scid_alias.unwrap()); - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, 100_000); let id = PaymentId(payment_hash.0); nodes[0].node.send_payment_with_route(route, payment_hash, onion, id).unwrap(); check_added_monitors(&nodes[0], 1); @@ -578,7 +578,7 @@ fn test_inbound_scid_privacy() { get_route_and_payment_hash!(nodes[0], nodes[2], payment_params, 100_000); assert_eq!(route.paths[0].hops[1].short_channel_id, last_hop[0].inbound_scid_alias.unwrap()); - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, 100_000); let id = PaymentId(payment_hash.0); nodes[0].node.send_payment_with_route(route, payment_hash, onion, id).unwrap(); check_added_monitors(&nodes[0], 1); @@ -599,7 +599,7 @@ fn test_inbound_scid_privacy() { get_route_and_payment_hash!(nodes[0], nodes[2], payment_params_2, 100_000); assert_eq!(route_2.paths[0].hops[1].short_channel_id, last_hop[0].short_channel_id.unwrap()); - let onion = RecipientOnionFields::secret_only(payment_secret_2); + let onion = RecipientOnionFields::secret_only(payment_secret_2, 100_000); let id = PaymentId(payment_hash_2.0); nodes[0].node.send_payment_with_route(route_2, payment_hash_2, onion, id).unwrap(); check_added_monitors(&nodes[0], 1); @@ -695,7 +695,7 @@ fn test_scid_alias_returned() { route.paths[0].hops[1].fee_msat = 10_000_000; // Overshoot the last channel's value // Route the HTLC through to the destination. - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, route.get_total_amount()); let id = PaymentId(payment_hash.0); nodes[0].node.send_payment_with_route(route.clone(), payment_hash, onion, id).unwrap(); @@ -732,7 +732,7 @@ fn test_scid_alias_returned() { route.paths[0].hops[0].fee_msat = 0; // But set fee paid to the middle hop to 0 // Route the HTLC through to the destination. - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, 10_000); let id = PaymentId(payment_hash.0); nodes[0].node.send_payment_with_route(route, payment_hash, onion, id).unwrap(); @@ -936,7 +936,7 @@ fn test_0conf_channel_with_async_monitor() { let (route, payment_hash, payment_preimage, payment_secret) = get_route_and_payment_hash!(nodes[0], nodes[2], 1_000_000); - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, 1_000_000); let id = PaymentId(payment_hash.0); nodes[0].node.send_payment_with_route(route, payment_hash, onion, id).unwrap(); check_added_monitors(&nodes[0], 1); @@ -1288,7 +1288,7 @@ fn test_0conf_channel_reorg() { ); claim_payment(&nodes[0], &[&nodes[1], &nodes[2]], payment_preimage); - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, 10_000); let id = PaymentId([0; 32]); nodes[1].node.send_payment_with_route(route, payment_hash, onion.clone(), id).unwrap(); let mut conditions = PaymentFailedConditions::new(); diff --git a/lightning/src/ln/quiescence_tests.rs b/lightning/src/ln/quiescence_tests.rs index d972fb6a5c5..3557b03697e 100644 --- a/lightning/src/ln/quiescence_tests.rs +++ b/lightning/src/ln/quiescence_tests.rs @@ -98,7 +98,7 @@ fn allow_shutdown_while_awaiting_quiescence(local_shutdown: bool) { let payment_amount = 1_000_000; let (route, payment_hash, _, payment_secret) = get_route_and_payment_hash!(local_node, remote_node, payment_amount); - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, payment_amount); let payment_id = PaymentId(payment_hash.0); local_node.node.send_payment_with_route(route, payment_hash, onion, payment_id).unwrap(); check_added_monitors(&local_node, 1); @@ -304,7 +304,7 @@ fn test_quiescence_on_final_revoke_and_ack_pending_monitor_update() { let payment_amount = 1_000_000; let (route, payment_hash, _, payment_secret) = get_route_and_payment_hash!(&nodes[0], &nodes[1], payment_amount); - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, payment_amount); let payment_id = PaymentId(payment_hash.0); nodes[0].node.send_payment_with_route(route, payment_hash, onion, payment_id).unwrap(); check_added_monitors(&nodes[0], 1); @@ -370,7 +370,7 @@ fn quiescence_updates_go_to_holding_cell(fail_htlc: bool) { let (route1, payment_hash1, payment_preimage1, payment_secret1) = get_route_and_payment_hash!(&nodes[1], &nodes[0], payment_amount); - let onion1 = RecipientOnionFields::secret_only(payment_secret1); + let onion1 = RecipientOnionFields::secret_only(payment_secret1, payment_amount); let payment_id1 = PaymentId(payment_hash1.0); nodes[1].node.send_payment_with_route(route1, payment_hash1, onion1, payment_id1).unwrap(); check_added_monitors(&nodes[1], 0); @@ -380,7 +380,7 @@ fn quiescence_updates_go_to_holding_cell(fail_htlc: bool) { // allowed to make updates. let (route2, payment_hash2, payment_preimage2, payment_secret2) = get_route_and_payment_hash!(&nodes[0], &nodes[1], payment_amount); - let onion2 = RecipientOnionFields::secret_only(payment_secret2); + let onion2 = RecipientOnionFields::secret_only(payment_secret2, payment_amount); let payment_id2 = PaymentId(payment_hash2.0); nodes[0].node.send_payment_with_route(route2, payment_hash2, onion2, payment_id2).unwrap(); check_added_monitors(&nodes[0], 1); diff --git a/lightning/src/ln/reload_tests.rs b/lightning/src/ln/reload_tests.rs index c0432051a62..2b1cc1f1395 100644 --- a/lightning/src/ln/reload_tests.rs +++ b/lightning/src/ln/reload_tests.rs @@ -541,7 +541,7 @@ fn do_test_data_loss_protect(reconnect_panicing: bool, substantially_old: bool, // `not_stale` to test the boundary condition. let pay_params = PaymentParameters::for_keysend(nodes[1].node.get_our_node_id(), 100, false); let route_params = RouteParameters::from_payment_params_and_value(pay_params, 40000); - nodes[0].node.send_spontaneous_payment(None, RecipientOnionFields::spontaneous_empty(), PaymentId([0; 32]), route_params, Retry::Attempts(0)).unwrap(); + nodes[0].node.send_spontaneous_payment(None, RecipientOnionFields::spontaneous_empty(40000), PaymentId([0; 32]), route_params, Retry::Attempts(0)).unwrap(); check_added_monitors(&nodes[0], 1); let update_add_commit = SendEvent::from_node(&nodes[0]); @@ -754,7 +754,7 @@ fn do_test_partial_claim_before_restart(persist_both_monitors: bool, double_rest }); nodes[0].node.send_payment_with_route(route, payment_hash, - RecipientOnionFields::secret_only(payment_secret), PaymentId(payment_hash.0)).unwrap(); + RecipientOnionFields::secret_only(payment_secret, 15_000_000), PaymentId(payment_hash.0)).unwrap(); check_added_monitors(&nodes[0], 2); // Send the payment through to nodes[3] *without* clearing the PaymentClaimable event @@ -952,7 +952,7 @@ fn do_forwarded_payment_no_manager_persistence(use_cs_commitment: bool, claim_ht let payment_id = PaymentId(nodes[0].keys_manager.backing.get_secure_random_bytes()); let htlc_expiry = nodes[0].best_block_info().1 + TEST_FINAL_CLTV; nodes[0].node.send_payment_with_route(route, payment_hash, - RecipientOnionFields::secret_only(payment_secret), payment_id).unwrap(); + RecipientOnionFields::secret_only(payment_secret, 1_000_000), payment_id).unwrap(); check_added_monitors(&nodes[0], 1); let payment_event = SendEvent::from_node(&nodes[0]); @@ -1206,7 +1206,7 @@ fn do_manager_persisted_pre_outbound_edge_forward(intercept_htlc: bool) { if intercept_htlc { route.paths[0].hops[1].short_channel_id = nodes[1].node.get_intercept_scid(); } - nodes[0].node.send_payment_with_route(route, payment_hash, RecipientOnionFields::secret_only(payment_secret), PaymentId(payment_hash.0)).unwrap(); + nodes[0].node.send_payment_with_route(route, payment_hash, RecipientOnionFields::secret_only(payment_secret, amt_msat), PaymentId(payment_hash.0)).unwrap(); check_added_monitors(&nodes[0], 1); let updates = get_htlc_update_msgs(&nodes[0], &nodes[1].node.get_our_node_id()); nodes[1].node.handle_update_add_htlc(nodes[0].node.get_our_node_id(), &updates.update_add_htlcs[0]); @@ -1279,7 +1279,7 @@ fn test_manager_persisted_post_outbound_edge_forward() { // Lock in the HTLC from node_a <> node_b. let amt_msat = 5000; let (mut route, payment_hash, payment_preimage, payment_secret) = get_route_and_payment_hash!(nodes[0], nodes[2], amt_msat); - nodes[0].node.send_payment_with_route(route, payment_hash, RecipientOnionFields::secret_only(payment_secret), PaymentId(payment_hash.0)).unwrap(); + nodes[0].node.send_payment_with_route(route, payment_hash, RecipientOnionFields::secret_only(payment_secret, amt_msat), PaymentId(payment_hash.0)).unwrap(); check_added_monitors(&nodes[0], 1); let updates = get_htlc_update_msgs(&nodes[0], &nodes[1].node.get_our_node_id()); nodes[1].node.handle_update_add_htlc(nodes[0].node.get_our_node_id(), &updates.update_add_htlcs[0]); @@ -1409,9 +1409,9 @@ fn test_htlc_localremoved_persistence() { let test_preimage = PaymentPreimage([42; 32]); let mismatch_payment_hash = PaymentHash([43; 32]); let session_privs = nodes[0].node.test_add_new_pending_payment(mismatch_payment_hash, - RecipientOnionFields::spontaneous_empty(), PaymentId(mismatch_payment_hash.0), &route).unwrap(); + RecipientOnionFields::spontaneous_empty(10_000), PaymentId(mismatch_payment_hash.0), &route).unwrap(); nodes[0].node.test_send_payment_internal(&route, mismatch_payment_hash, - RecipientOnionFields::spontaneous_empty(), Some(test_preimage), PaymentId(mismatch_payment_hash.0), None, session_privs).unwrap(); + RecipientOnionFields::spontaneous_empty(10_000), Some(test_preimage), PaymentId(mismatch_payment_hash.0), None, session_privs).unwrap(); check_added_monitors(&nodes[0], 1); let updates = get_htlc_update_msgs(&nodes[0], &nodes[1].node.get_our_node_id()); diff --git a/lightning/src/ln/shutdown_tests.rs b/lightning/src/ln/shutdown_tests.rs index ee037be428e..a0db05f1197 100644 --- a/lightning/src/ln/shutdown_tests.rs +++ b/lightning/src/ln/shutdown_tests.rs @@ -443,12 +443,12 @@ fn updates_shutdown_wait() { ) .unwrap(); - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, 100_000); let id = PaymentId(payment_hash.0); let res = nodes[0].node.send_payment_with_route(route_1, payment_hash, onion, id); unwrap_send_err!(nodes[0], res, true, APIError::ChannelUnavailable { .. }, {}); - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, 100_000); let res = nodes[1].node.send_payment_with_route(route_2, payment_hash, onion, id); unwrap_send_err!(nodes[1], res, true, APIError::ChannelUnavailable { .. }, {}); @@ -544,7 +544,7 @@ fn do_htlc_fail_async_shutdown(blinded_recipient: bool) { amt_msat, ) }; - let onion = RecipientOnionFields::secret_only(our_payment_secret); + let onion = RecipientOnionFields::secret_only(our_payment_secret, amt_msat); let id = PaymentId(our_payment_hash.0); nodes[0] .node @@ -1877,7 +1877,7 @@ fn test_pending_htlcs_arent_lost_on_mon_delay() { // moment `cs_last_raa` is received by B. let (route_b, payment_hash_b, _preimage, payment_secret_b) = get_route_and_payment_hash!(&nodes[0], nodes[2], 900_000); - let onion = RecipientOnionFields::secret_only(payment_secret_b); + let onion = RecipientOnionFields::secret_only(payment_secret_b, 900_000); let id = PaymentId(payment_hash_b.0); nodes[0].node.send_payment_with_route(route_b, payment_hash_b, onion, id).unwrap(); check_added_monitors(&nodes[0], 1); diff --git a/lightning/src/ln/splicing_tests.rs b/lightning/src/ln/splicing_tests.rs index 9c7c8b55eac..a810b8eba9d 100644 --- a/lightning/src/ln/splicing_tests.rs +++ b/lightning/src/ln/splicing_tests.rs @@ -2119,7 +2119,7 @@ fn do_test_splice_with_inflight_htlc_forward_and_resolution(expire_scid_pre_forw let route = get_route(&nodes[0], &route_params).unwrap(); let (_, payment_hash, payment_secret) = get_payment_preimage_hash(&nodes[2], Some(payment_amount), None); - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, payment_amount); let id = PaymentId(payment_hash.0); nodes[0].node.send_payment_with_route(route.clone(), payment_hash, onion, id).unwrap(); check_added_monitors(&nodes[0], 1); diff --git a/lightning/src/ln/update_fee_tests.rs b/lightning/src/ln/update_fee_tests.rs index 67a07325ad6..4b69d5c3766 100644 --- a/lightning/src/ln/update_fee_tests.rs +++ b/lightning/src/ln/update_fee_tests.rs @@ -80,7 +80,7 @@ pub fn test_async_inbound_update_fee() { // ...but before it's delivered, nodes[1] starts to send a payment back to nodes[0]... let (route, our_payment_hash, _, our_payment_secret) = get_route_and_payment_hash!(nodes[1], nodes[0], 40000); - let onion = RecipientOnionFields::secret_only(our_payment_secret); + let onion = RecipientOnionFields::secret_only(our_payment_secret, 40000); let id = PaymentId(our_payment_hash.0); nodes[1].node.send_payment_with_route(route, our_payment_hash, onion, id).unwrap(); check_added_monitors(&nodes[1], 1); @@ -181,7 +181,7 @@ pub fn test_update_fee_unordered_raa() { // ...but before it's delivered, nodes[1] starts to send a payment back to nodes[0]... let (route, our_payment_hash, _, our_payment_secret) = get_route_and_payment_hash!(nodes[1], nodes[0], 40000); - let onion = RecipientOnionFields::secret_only(our_payment_secret); + let onion = RecipientOnionFields::secret_only(our_payment_secret, 40000); let id = PaymentId(our_payment_hash.0); nodes[1].node.send_payment_with_route(route, our_payment_hash, onion, id).unwrap(); check_added_monitors(&nodes[1], 1); @@ -672,7 +672,7 @@ pub fn test_update_fee_with_fundee_update_add_htlc() { get_route_and_payment_hash!(nodes[1], nodes[0], 800000); // nothing happens since node[1] is in AwaitingRemoteRevoke - let onion = RecipientOnionFields::secret_only(our_payment_secret); + let onion = RecipientOnionFields::secret_only(our_payment_secret, 800000); let id = PaymentId(our_payment_hash.0); nodes[1].node.send_payment_with_route(route, our_payment_hash, onion, id).unwrap(); check_added_monitors(&nodes[1], 0); @@ -1094,7 +1094,7 @@ pub fn do_cannot_afford_on_holding_cell_release( let (route, payment_hash, _, payment_secret) = get_route_and_payment_hash!(nodes[1], nodes[0], 5000 * 1000); - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, 5000 * 1000); let id = PaymentId(payment_hash.0); nodes[1].node.send_payment_with_route(route, payment_hash, onion, id).unwrap(); check_added_monitors(&nodes[1], 1); From 8ffb53beae8d60b74a5a21deaf5fd1998681cb60 Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Fri, 6 Feb 2026 17:34:13 +0000 Subject: [PATCH 09/68] f rebase --- lightning/src/ln/reload_tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lightning/src/ln/reload_tests.rs b/lightning/src/ln/reload_tests.rs index 2b1cc1f1395..6bc1eb3ad38 100644 --- a/lightning/src/ln/reload_tests.rs +++ b/lightning/src/ln/reload_tests.rs @@ -1595,7 +1595,7 @@ fn test_hold_completed_inflight_monitor_updates_upon_manager_reload() { let (route, payment_hash, _, payment_secret) = get_route_and_payment_hash!(nodes[0], nodes[1], 1_000_000); let payment_id = PaymentId(payment_hash.0); - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, 1_000_000); nodes[0].node.send_payment_with_route(route, payment_hash, onion, payment_id).unwrap(); check_added_monitors(&nodes[0], 1); From 484e81bade113f397d89ed625af940e565aefb67 Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Mon, 2 Feb 2026 20:52:29 +0000 Subject: [PATCH 10/68] Replace existing MPP-total args with `RecipientOnionFields` In some uses of LDK we need the ability to send HTLCs for only a portion of some larger MPP payment. This allows payers to make single payments which spend funds from multiple wallets, which may be important for ecash wallets holding funds in multiple mints or graduated wallets which hold funds across a trusted wallet and a self-custodial wallet. In the previous commit we added a new field to `RecipientOnionFields` to describe the total value of an MPP payment. Here we start using this field when building onions, dropping existing arguments to onion-building methods. --- lightning/src/ln/blinded_payment_tests.rs | 22 ++--- lightning/src/ln/channelmanager.rs | 16 ++-- lightning/src/ln/functional_tests.rs | 20 ++-- lightning/src/ln/htlc_reserve_unit_tests.rs | 15 +-- .../src/ln/max_payment_path_len_tests.rs | 5 +- lightning/src/ln/onion_payment.rs | 8 +- lightning/src/ln/onion_route_tests.rs | 19 ++-- lightning/src/ln/onion_utils.rs | 91 ++++++++++--------- lightning/src/ln/outbound_payment.rs | 29 +++--- lightning/src/ln/payment_tests.rs | 9 +- lightning/src/ln/reload_tests.rs | 2 +- 11 files changed, 101 insertions(+), 135 deletions(-) diff --git a/lightning/src/ln/blinded_payment_tests.rs b/lightning/src/ln/blinded_payment_tests.rs index b00a4f360d8..3eb1f55d445 100644 --- a/lightning/src/ln/blinded_payment_tests.rs +++ b/lightning/src/ln/blinded_payment_tests.rs @@ -478,8 +478,8 @@ fn do_forward_checks_failure(check: ForwardCheckFail, intro_fails: bool) { let session_priv = SecretKey::from_slice(&[3; 32]).unwrap(); let mut onion_keys = onion_utils::construct_onion_keys(&Secp256k1::new(), &route.paths[0], &session_priv); let cur_height = nodes[0].best_block_info().1; - let (mut onion_payloads, ..) = onion_utils::build_onion_payloads( - &route.paths[0], amt_msat, &recipient_onion_fields, cur_height, &None, None, None).unwrap(); + let (mut onion_payloads, ..) = onion_utils::test_build_onion_payloads( + &route.paths[0], &recipient_onion_fields, cur_height, &None, None, None).unwrap(); // Remove the receive payload so the blinded forward payload is encoded as a final payload // (i.e. next_hop_hmac == [0; 32]) onion_payloads.pop(); @@ -1065,8 +1065,8 @@ fn do_multi_hop_receiver_fail(check: ReceiveCheckFail) { let mut onion_keys = onion_utils::construct_onion_keys(&Secp256k1::new(), &route.paths[0], &session_priv); let cur_height = nodes[0].best_block_info().1; let recipient_onion_fields = RecipientOnionFields::spontaneous_empty(amt_msat); - let (mut onion_payloads, ..) = onion_utils::build_onion_payloads( - &route.paths[0], amt_msat, &recipient_onion_fields, cur_height, &None, None, None).unwrap(); + let (mut onion_payloads, ..) = onion_utils::test_build_onion_payloads( + &route.paths[0], &recipient_onion_fields, cur_height, &None, None, None).unwrap(); let update_add = &mut payment_event_1_2.msgs[0]; onion_payloads.last_mut().map(|p| { @@ -1681,7 +1681,7 @@ fn route_blinding_spec_test_vector() { }), }; let cur_height = 747_000; - let (bob_onion, _, _) = onion_utils::create_payment_onion(&secp_ctx, &path, &session_priv, amt_msat, &RecipientOnionFields::spontaneous_empty(amt_msat), cur_height, &PaymentHash([0; 32]), &None, None, [0; 32]).unwrap(); + let (bob_onion, _, _) = onion_utils::create_payment_onion(&secp_ctx, &path, &session_priv, &RecipientOnionFields::spontaneous_empty(amt_msat), cur_height, &PaymentHash([0; 32]), &None, None, [0; 32]).unwrap(); struct TestEcdhSigner { node_secret: SecretKey, @@ -1905,7 +1905,7 @@ fn test_combined_trampoline_onion_creation_vectors() { let amt_msat = 150_000_000; let cur_height = 800_000; let recipient_onion_fields = RecipientOnionFields::secret_only(payment_secret, amt_msat); - let (bob_onion, htlc_msat, htlc_cltv) = onion_utils::create_payment_onion_internal(&secp_ctx, &path, &outer_session_key, amt_msat, &recipient_onion_fields, cur_height, &associated_data, &None, None, outer_onion_prng_seed, Some(session_priv), Some([0; 32])).unwrap(); + let (bob_onion, htlc_msat, htlc_cltv) = onion_utils::create_payment_onion_internal(&secp_ctx, &path, &outer_session_key, &recipient_onion_fields, cur_height, &associated_data, &None, None, outer_onion_prng_seed, Some(session_priv), Some([0; 32])).unwrap(); let outer_onion_packet_hex = bob_onion.encode().to_lower_hex_string(); assert_eq!(outer_onion_packet_hex, "00025fd60556c134ae97e4baedba220a644037754ee67c54fd05e93bf40c17cbb73362fb9dee96001ff229945595b6edb59437a6bc143406d3f90f749892a84d8d430c6890437d26d5bfc599d565316ef51347521075bbab87c59c57bcf20af7e63d7192b46cf171e4f73cb11f9f603915389105d91ad630224bea95d735e3988add1e24b5bf28f1d7128db64284d90a839ba340d088c74b1fb1bd21136b1809428ec5399c8649e9bdf92d2dcfc694deae5046fa5b2bdf646847aaad73f5e95275763091c90e71031cae1f9a770fdea559642c9c02f424a2a28163dd0957e3874bd28a97bec67d18c0321b0e68bc804aa8345b17cb626e2348ca06c8312a167c989521056b0f25c55559d446507d6c491d50605cb79fa87929ce64b0a9860926eeaec2c431d926a1cadb9a1186e4061cb01671a122fc1f57602cbef06d6c194ec4b715c2e3dd4120baca3172cd81900b49fef857fb6d6afd24c983b608108b0a5ac0c1c6c52011f23b8778059ffadd1bb7cd06e2525417365f485a7fd1d4a9ba3818ede7cdc9e71afee8532252d08e2531ca52538655b7e8d912f7ec6d37bbcce8d7ec690709dbf9321e92c565b78e7fe2c22edf23e0902153d1ca15a112ad32fb19695ec65ce11ddf670da7915f05ad4b86c154fb908cb567315d1124f303f75fa075ebde8ef7bb12e27737ad9e4924439097338ea6d7a6fc3721b88c9b830a34e8d55f4c582b74a3895cc848fe57f4fe29f115dabeb6b3175be15d94408ed6771109cfaf57067ae658201082eae7605d26b1449af4425ae8e8f58cdda5c6265f1fd7a386fc6cea3074e4f25b909b96175883676f7610a00fdf34df9eb6c7b9a4ae89b839c69fd1f285e38cdceb634d782cc6d81179759bc9fd47d7fd060470d0b048287764c6837963274e708314f017ac7dc26d0554d59bfcfd3136225798f65f0b0fea337c6b256ebbb63a90b994c0ab93fd8b1d6bd4c74aebe535d6110014cd3d525394027dfe8faa98b4e9b2bee7949eb1961f1b026791092f84deea63afab66603dbe9b6365a102a1fef2f6b9744bc1bb091a8da9130d34d4d39f25dbad191649cfb67e10246364b7ce0c6ec072f9690cabb459d9fda0c849e17535de4357e9907270c75953fca3c845bb613926ecf73205219c7057a4b6bb244c184362bb4e2f24279dc4e60b94a5b1ec11c34081a628428ba5646c995b9558821053ba9c84a05afbf00dabd60223723096516d2f5668f3ec7e11612b01eb7a3a0506189a2272b88e89807943adb34291a17f6cb5516ffd6f945a1c42a524b21f096d66f350b1dad4db455741ae3d0e023309fbda5ef55fb0dc74f3297041448b2be76c525141963934c6afc53d263fb7836626df502d7c2ee9e79cbbd87afd84bbb8dfbf45248af3cd61ad5fac827e7683ca4f91dfad507a8eb9c17b2c9ac5ec051fe645a4a6cb37136f6f19b611e0ea8da7960af2d779507e55f57305bc74b7568928c5dd5132990fe54c22117df91c257d8c7b61935a018a28c1c3b17bab8e4294fa699161ec21123c9fc4e71079df31f300c2822e1246561e04765d3aab333eafd026c7431ac7616debb0e022746f4538e1c6348b600c988eeb2d051fc60c468dca260a84c79ab3ab8342dc345a764672848ea234e17332bc124799daf7c5fcb2e2358514a7461357e1c19c802c5ee32deccf1776885dd825bedd5f781d459984370a6b7ae885d4483a76ddb19b30f47ed47cd56aa5a079a89793dbcad461c59f2e002067ac98dd5a534e525c9c46c2af730741bf1f8629357ec0bfc0bc9ecb31af96777e507648ff4260dc3673716e098d9111dfd245f1d7c55a6de340deb8bd7a053e5d62d760f184dc70ca8fa255b9023b9b9aedfb6e419a5b5951ba0f83b603793830ee68d442d7b88ee1bbf6bbd1bcd6f68cc1af"); @@ -1996,7 +1996,7 @@ fn test_trampoline_inbound_payment_decoding() { let amt_msat = 150_000_001; let cur_height = 800_001; let recipient_onion_fields = RecipientOnionFields::secret_only(payment_secret, amt_msat); - let (bob_onion, _, _) = onion_utils::create_payment_onion(&secp_ctx, &path, &session_priv, amt_msat, &recipient_onion_fields, cur_height, &PaymentHash([0; 32]), &None, None, [0; 32]).unwrap(); + let (bob_onion, _, _) = onion_utils::create_payment_onion(&secp_ctx, &path, &session_priv, &recipient_onion_fields, cur_height, &PaymentHash([0; 32]), &None, None, [0; 32]).unwrap(); struct TestEcdhSigner { node_secret: SecretKey, @@ -2181,7 +2181,7 @@ fn test_trampoline_forward_payload_encoded_as_receive() { }); let recipient_onion_fields = RecipientOnionFields::spontaneous_empty(amt_msat); - let (mut trampoline_payloads, outer_total_msat, outer_starting_htlc_offset) = onion_utils::build_trampoline_onion_payloads(&blinded_tail, amt_msat, &recipient_onion_fields, 32, &None).unwrap(); + let (mut trampoline_payloads, outer_total_msat, outer_starting_htlc_offset) = onion_utils::build_trampoline_onion_payloads(&blinded_tail, &recipient_onion_fields, 32, &None).unwrap(); // pop the last dummy hop trampoline_payloads.pop(); @@ -2196,7 +2196,7 @@ fn test_trampoline_forward_payload_encoded_as_receive() { ).unwrap(); let recipient_onion_fields = RecipientOnionFields::spontaneous_empty(outer_total_msat); - let (outer_payloads, _, _) = onion_utils::build_onion_payloads(&route.paths[0], outer_total_msat, &recipient_onion_fields, outer_starting_htlc_offset, &None, None, Some(trampoline_packet)).unwrap(); + let (outer_payloads, _, _) = onion_utils::test_build_onion_payloads(&route.paths[0], &recipient_onion_fields, outer_starting_htlc_offset, &None, None, Some(trampoline_packet)).unwrap(); let outer_onion_keys = onion_utils::construct_onion_keys(&secp_ctx, &route.clone().paths[0], &outer_session_priv); let outer_packet = onion_utils::construct_onion_packet( outer_payloads, @@ -2489,7 +2489,6 @@ fn replacement_onion( let (mut trampoline_payloads, outer_total_msat, outer_starting_htlc_offset) = onion_utils::build_trampoline_onion_payloads( &blinded_tail, - original_amt_msat, &recipient_onion_fields, starting_htlc_offset, &None, @@ -2527,9 +2526,8 @@ fn replacement_onion( // Use a different session key to construct the replacement onion packet. Note that the // sender isn't aware of this and won't be able to decode the fulfill hold times. let recipient_onion_fields = RecipientOnionFields::spontaneous_empty(outer_total_msat); - let (mut outer_payloads, _, _) = onion_utils::build_onion_payloads( + let (mut outer_payloads, _, _) = onion_utils::test_build_onion_payloads( &route.paths[0], - outer_total_msat, &recipient_onion_fields, outer_starting_htlc_offset, &None, diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index e3094234417..7afdf97c731 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -5131,15 +5131,14 @@ impl< #[cfg(any(test, feature = "_externalize_tests"))] pub(crate) fn test_send_payment_along_path( &self, path: &Path, payment_hash: &PaymentHash, recipient_onion: RecipientOnionFields, - total_value: u64, cur_height: u32, payment_id: PaymentId, - keysend_preimage: &Option, session_priv_bytes: [u8; 32], + cur_height: u32, payment_id: PaymentId, keysend_preimage: &Option, + session_priv_bytes: [u8; 32], ) -> Result<(), APIError> { let _lck = self.total_consistency_lock.read().unwrap(); self.send_payment_along_path(SendAlongPathArgs { path, payment_hash, recipient_onion: &recipient_onion, - total_value, cur_height, payment_id, keysend_preimage, @@ -5155,7 +5154,6 @@ impl< path, payment_hash, recipient_onion, - total_value, cur_height, payment_id, keysend_preimage, @@ -5180,7 +5178,6 @@ impl< &self.secp_ctx, &path, &session_priv, - total_value, recipient_onion, cur_height, payment_hash, @@ -5399,7 +5396,7 @@ impl< pub(super) fn test_send_payment_internal( &self, route: &Route, payment_hash: PaymentHash, recipient_onion: RecipientOnionFields, keysend_preimage: Option, payment_id: PaymentId, - recv_value_msat: Option, onion_session_privs: Vec<[u8; 32]>, + 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); @@ -5409,7 +5406,6 @@ impl< recipient_onion, keysend_preimage, payment_id, - recv_value_msat, onion_session_privs, &self.node_signer, best_block_height, @@ -19669,7 +19665,7 @@ mod tests { let session_privs = nodes[0].node.test_add_new_pending_payment(our_payment_hash, RecipientOnionFields::secret_only(payment_secret, 200_000), payment_id, &mpp_route).unwrap(); nodes[0].node.test_send_payment_along_path(&mpp_route.paths[0], &our_payment_hash, - RecipientOnionFields::secret_only(payment_secret, 200_000), 200_000, cur_height, payment_id, &None, session_privs[0]).unwrap(); + RecipientOnionFields::secret_only(payment_secret, 200_000), cur_height, payment_id, &None, session_privs[0]).unwrap(); check_added_monitors(&nodes[0], 1); let mut events = nodes[0].node.get_and_clear_pending_msg_events(); assert_eq!(events.len(), 1); @@ -19705,7 +19701,7 @@ mod tests { // Send the second half of the original MPP payment. nodes[0].node.test_send_payment_along_path(&mpp_route.paths[1], &our_payment_hash, - RecipientOnionFields::secret_only(payment_secret, 200_000), 200_000, cur_height, payment_id, &None, session_privs[1]).unwrap(); + RecipientOnionFields::secret_only(payment_secret, 200_000), cur_height, payment_id, &None, session_privs[1]).unwrap(); check_added_monitors(&nodes[0], 1); let mut events = nodes[0].node.get_and_clear_pending_msg_events(); assert_eq!(events.len(), 1); @@ -19953,7 +19949,7 @@ mod tests { let session_privs = nodes[0].node.test_add_new_pending_payment(mismatch_payment_hash, RecipientOnionFields::spontaneous_empty(10_000), PaymentId(mismatch_payment_hash.0), &route).unwrap(); nodes[0].node.test_send_payment_internal(&route, mismatch_payment_hash, - RecipientOnionFields::spontaneous_empty(10_000), Some(test_preimage), PaymentId(mismatch_payment_hash.0), None, session_privs).unwrap(); + RecipientOnionFields::spontaneous_empty(10_000), Some(test_preimage), PaymentId(mismatch_payment_hash.0), session_privs).unwrap(); check_added_monitors(&nodes[0], 1); let updates = get_htlc_update_msgs(&nodes[0], &nodes[1].node.get_our_node_id()); diff --git a/lightning/src/ln/functional_tests.rs b/lightning/src/ln/functional_tests.rs index c2926681c73..ed2665665c7 100644 --- a/lightning/src/ln/functional_tests.rs +++ b/lightning/src/ln/functional_tests.rs @@ -2244,9 +2244,8 @@ pub fn fail_backward_pending_htlc_upon_channel_failure() { let session_priv = SecretKey::from_slice(&[42; 32]).unwrap(); let current_height = nodes[1].node.best_block.read().unwrap().height + 1; let recipient_onion_fields = RecipientOnionFields::secret_only(payment_secret, 50_000); - let (onion_payloads, _amount_msat, cltv_expiry) = onion_utils::build_onion_payloads( + let (onion_payloads, _amount_msat, cltv_expiry) = onion_utils::test_build_onion_payloads( &route.paths[0], - 50_000, &recipient_onion_fields, current_height, &None, @@ -3274,7 +3273,6 @@ fn do_test_htlc_timeout(send_partial_mpp: bool) { &route.paths[0], &our_payment_hash, RecipientOnionFields::secret_only(payment_secret, 200_000), - 200_000, cur_height, payment_id, &None, @@ -6910,10 +6908,9 @@ pub fn test_onion_value_mpp_set_calculation() { let onion = RecipientOnionFields::secret_only(payment_secret, total_msat); let onion_session_privs = nodes[0].node.test_add_new_pending_payment(hash, onion.clone(), id, &route).unwrap(); - let amt = Some(total_msat); nodes[0] .node - .test_send_payment_internal(&route, hash, onion, None, id, amt, onion_session_privs) + .test_send_payment_internal(&route, hash, onion, None, id, onion_session_privs) .unwrap(); check_added_monitors(&nodes[0], expected_paths.len()); @@ -6941,9 +6938,8 @@ pub fn test_onion_value_mpp_set_calculation() { &session_priv, ); let recipient_onion_fields = RecipientOnionFields::secret_only(payment_secret, 100_000); - let (mut onion_payloads, _, _) = onion_utils::build_onion_payloads( + let (mut onion_payloads, _, _) = onion_utils::test_build_onion_payloads( &route.paths[0], - 100_000, &recipient_onion_fields, height + 1, &None, @@ -7049,10 +7045,9 @@ fn do_test_overshoot_mpp(msat_amounts: &[u64], total_msat: u64) { let onion_session_privs = nodes[src_idx].node.test_add_new_pending_payment(hash, onion, id, &route).unwrap(); let onion = RecipientOnionFields::secret_only(payment_secret, total_msat); - let amt = Some(total_msat); nodes[src_idx] .node - .test_send_payment_internal(&route, hash, onion, None, id, amt, onion_session_privs) + .test_send_payment_internal(&route, hash, onion, None, id, onion_session_privs) .unwrap(); check_added_monitors(&nodes[src_idx], expected_paths.len()); @@ -8369,7 +8364,7 @@ pub fn test_inconsistent_mpp_params() { let priv_a = session_privs[0]; nodes[0] .node - .test_send_payment_along_path(path_a, &hash, onion, real_amt, cur_height, id, &None, priv_a) + .test_send_payment_along_path(path_a, &hash, onion, cur_height, id, &None, priv_a) .unwrap(); check_added_monitors(&nodes[0], 1); @@ -8382,11 +8377,10 @@ pub fn test_inconsistent_mpp_params() { let path_b = &route.paths[1]; let onion = RecipientOnionFields::secret_only(payment_secret, 14_000_000); - let amt_b = 14_000_000; let priv_b = session_privs[1]; nodes[0] .node - .test_send_payment_along_path(path_b, &hash, onion, amt_b, cur_height, id, &None, priv_b) + .test_send_payment_along_path(path_b, &hash, onion, cur_height, id, &None, priv_b) .unwrap(); check_added_monitors(&nodes[0], 1); @@ -8446,7 +8440,7 @@ pub fn test_inconsistent_mpp_params() { let priv_c = session_privs[2]; nodes[0] .node - .test_send_payment_along_path(path_b, &hash, onion, real_amt, cur_height, id, &None, priv_c) + .test_send_payment_along_path(path_b, &hash, onion, cur_height, id, &None, priv_c) .unwrap(); check_added_monitors(&nodes[0], 1); diff --git a/lightning/src/ln/htlc_reserve_unit_tests.rs b/lightning/src/ln/htlc_reserve_unit_tests.rs index 5a8b7eb522e..2a1ceadaf5a 100644 --- a/lightning/src/ln/htlc_reserve_unit_tests.rs +++ b/lightning/src/ln/htlc_reserve_unit_tests.rs @@ -817,9 +817,8 @@ pub fn do_test_fee_spike_buffer(cfg: Option, htlc_fails: bool) { let onion_keys = onion_utils::construct_onion_keys(&secp_ctx, &route.paths[0], &session_priv); let recipient_onion_fields = RecipientOnionFields::secret_only(payment_secret, payment_amt_msat); - let (onion_payloads, htlc_msat, htlc_cltv) = onion_utils::build_onion_payloads( + let (onion_payloads, htlc_msat, htlc_cltv) = onion_utils::test_build_onion_payloads( &route.paths[0], - payment_amt_msat, &recipient_onion_fields, cur_height, &None, @@ -1061,9 +1060,8 @@ pub fn test_chan_reserve_violation_inbound_htlc_outbound_channel() { let cur_height = nodes[1].node.best_block.read().unwrap().height + 1; let onion_keys = onion_utils::construct_onion_keys(&secp_ctx, &route.paths[0], &session_priv); let recipient_onion_fields = RecipientOnionFields::secret_only(payment_secret, 700_000); - let (onion_payloads, htlc_msat, htlc_cltv) = onion_utils::build_onion_payloads( + let (onion_payloads, htlc_msat, htlc_cltv) = onion_utils::test_build_onion_payloads( &route.paths[0], - 700_000, &recipient_onion_fields, cur_height, &None, @@ -1242,9 +1240,8 @@ pub fn test_chan_reserve_violation_inbound_htlc_inbound_chan() { let cur_height = nodes[0].node.best_block.read().unwrap().height + 1; let onion_keys = onion_utils::construct_onion_keys(&secp_ctx, &route_2.paths[0], &session_priv); let recipient_onion_fields = RecipientOnionFields::spontaneous_empty(recv_value_2); - let (onion_payloads, htlc_msat, htlc_cltv) = onion_utils::build_onion_payloads( + let (onion_payloads, htlc_msat, htlc_cltv) = onion_utils::test_build_onion_payloads( &route_2.paths[0], - recv_value_2, &recipient_onion_fields, cur_height, &None, @@ -1630,9 +1627,8 @@ pub fn test_update_add_htlc_bolt2_receiver_check_max_htlc_limit() { &session_priv, ); let recipient_onion_fields = RecipientOnionFields::secret_only(our_payment_secret, send_amt); - let (onion_payloads, _htlc_msat, htlc_cltv) = onion_utils::build_onion_payloads( + let (onion_payloads, _htlc_msat, htlc_cltv) = onion_utils::test_build_onion_payloads( &route.paths[0], - send_amt, &recipient_onion_fields, cur_height, &None, @@ -2237,9 +2233,8 @@ pub fn do_test_dust_limit_fee_accounting(can_afford: bool) { onion_utils::construct_onion_keys(&secp_ctx, &route_0_1.paths[0], &session_priv); let recipient_onion_fields = RecipientOnionFields::secret_only(payment_secret_0_1, HTLC_AMT_SAT * 1000); - let (onion_payloads, amount_msat, cltv_expiry) = onion_utils::build_onion_payloads( + let (onion_payloads, amount_msat, cltv_expiry) = onion_utils::test_build_onion_payloads( &route_0_1.paths[0], - HTLC_AMT_SAT * 1000, &recipient_onion_fields, cur_height, &None, diff --git a/lightning/src/ln/max_payment_path_len_tests.rs b/lightning/src/ln/max_payment_path_len_tests.rs index ea78449316c..45640d3486d 100644 --- a/lightning/src/ln/max_payment_path_len_tests.rs +++ b/lightning/src/ln/max_payment_path_len_tests.rs @@ -139,7 +139,6 @@ fn large_payment_metadata() { &secp_ctx, &route_0_1.paths[0], &test_utils::privkey(42), - MIN_FINAL_VALUE_ESTIMATE_WITH_OVERPAY, &too_large_onion, nodes[0].best_block_info().1 + DEFAULT_MAX_TOTAL_CLTV_EXPIRY_DELTA, &payment_hash, @@ -369,9 +368,8 @@ fn blinded_path_with_custom_tlv() { // Calculate the maximum custom TLV value size where a valid onion packet is still possible. const CUSTOM_TLV_TYPE: u64 = 65537; let mut route = get_route(&nodes[1], &route_params).unwrap(); - let reserved_packet_bytes_without_custom_tlv: usize = onion_utils::build_onion_payloads( + let reserved_packet_bytes_without_custom_tlv: usize = onion_utils::test_build_onion_payloads( &route.paths[0], - MIN_FINAL_VALUE_ESTIMATE_WITH_OVERPAY, &RecipientOnionFields::spontaneous_empty(MIN_FINAL_VALUE_ESTIMATE_WITH_OVERPAY), nodes[0].best_block_info().1 + DEFAULT_MAX_TOTAL_CLTV_EXPIRY_DELTA, &None, @@ -433,7 +431,6 @@ fn blinded_path_with_custom_tlv() { &secp_ctx, &route.paths[0], &test_utils::privkey(42), - MIN_FINAL_VALUE_ESTIMATE_WITH_OVERPAY, &too_large_onion, nodes[0].best_block_info().1 + DEFAULT_MAX_TOTAL_CLTV_EXPIRY_DELTA, &payment_hash, diff --git a/lightning/src/ln/onion_payment.rs b/lightning/src/ln/onion_payment.rs index d0d50c6a315..def4a1861c4 100644 --- a/lightning/src/ln/onion_payment.rs +++ b/lightning/src/ln/onion_payment.rs @@ -779,7 +779,7 @@ mod tests { let charlie_pk = PublicKey::from_secret_key(&secp_ctx, &charlie.get_node_secret_key()); let ( - session_priv, total_amt_msat, cur_height, mut recipient_onion, keysend_preimage, payment_hash, + session_priv, _total_amt_msat, cur_height, mut recipient_onion, keysend_preimage, payment_hash, prng_seed, hops, .. ) = payment_onion_args(bob_pk, charlie_pk); @@ -788,8 +788,8 @@ mod tests { let path = Path { hops, blinded_tail: None, }; let onion_keys = super::onion_utils::construct_onion_keys(&secp_ctx, &path, &session_priv); - let (onion_payloads, ..) = super::onion_utils::build_onion_payloads( - &path, total_amt_msat, &recipient_onion, cur_height + 1, &Some(keysend_preimage), None, None + let (onion_payloads, ..) = super::onion_utils::test_build_onion_payloads( + &path, &recipient_onion, cur_height + 1, &Some(keysend_preimage), None, None ).unwrap(); assert!(super::onion_utils::construct_onion_packet( @@ -817,7 +817,7 @@ mod tests { }; let (onion, amount_msat, cltv_expiry) = create_payment_onion( - &secp_ctx, &path, &session_priv, total_amt_msat, &recipient_onion, + &secp_ctx, &path, &session_priv, &recipient_onion, cur_height, &payment_hash, &Some(preimage), None, prng_seed ).unwrap(); diff --git a/lightning/src/ln/onion_route_tests.rs b/lightning/src/ln/onion_route_tests.rs index 1ee0be2938b..f18eead8759 100644 --- a/lightning/src/ln/onion_route_tests.rs +++ b/lightning/src/ln/onion_route_tests.rs @@ -25,7 +25,7 @@ use crate::ln::msgs::{ OutboundOnionPayload, OutboundTrampolinePayload, }; use crate::ln::onion_utils::{ - self, build_onion_payloads, construct_onion_keys, LocalHTLCFailureReason, + self, construct_onion_keys, test_build_onion_payloads, LocalHTLCFailureReason, }; use crate::ln::outbound_payment::RecipientOnionFields; use crate::ln::wire::Encode; @@ -527,7 +527,7 @@ fn test_onion_failure() { let recipient_fields = RecipientOnionFields::spontaneous_empty(40000); let path = &route.paths[0]; let (mut onion_payloads, _htlc_msat, _htlc_cltv) = - build_onion_payloads(path, 40000, &recipient_fields, cur_height, &None, None, None) + test_build_onion_payloads(path, &recipient_fields, cur_height, &None, None, None) .unwrap(); let mut new_payloads = Vec::new(); for payload in onion_payloads.drain(..) { @@ -569,7 +569,7 @@ fn test_onion_failure() { let recipient_fields = RecipientOnionFields::spontaneous_empty(40000); let path = &route.paths[0]; let (mut onion_payloads, _htlc_msat, _htlc_cltv) = - build_onion_payloads(path, 40000, &recipient_fields, cur_height, &None, None, None) + test_build_onion_payloads(path, &recipient_fields, cur_height, &None, None, None) .unwrap(); let mut new_payloads = Vec::new(); for payload in onion_payloads.drain(..) { @@ -1288,7 +1288,7 @@ fn test_onion_failure() { let recipient_fields = RecipientOnionFields::spontaneous_empty(40000); let path = &route.paths[0]; let (onion_payloads, _, htlc_cltv) = - build_onion_payloads(path, 40000, &recipient_fields, height, &None, None, None) + test_build_onion_payloads(path, &recipient_fields, height, &None, None, None) .unwrap(); let onion_packet = onion_utils::construct_onion_packet( onion_payloads, @@ -1841,8 +1841,7 @@ fn test_always_create_tlv_format_onion_payloads() { let recipient_fields = RecipientOnionFields::spontaneous_empty(40000); let path = &route.paths[0]; let (onion_payloads, _htlc_msat, _htlc_cltv) = - build_onion_payloads(path, 40000, &recipient_fields, cur_height, &None, None, None) - .unwrap(); + test_build_onion_payloads(path, &recipient_fields, cur_height, &None, None, None).unwrap(); match onion_payloads[0] { msgs::OutboundOnionPayload::Forward { .. } => {}, @@ -1978,7 +1977,6 @@ fn test_trampoline_onion_payload_assembly_values() { let (trampoline_payloads, outer_total_msat, outer_starting_htlc_offset) = onion_utils::build_trampoline_onion_payloads( &path.blinded_tail.as_ref().unwrap(), - amt_msat, &recipient_onion_fields, cur_height, &None, @@ -2041,9 +2039,8 @@ fn test_trampoline_onion_payload_assembly_values() { let recipient_onion_fields = RecipientOnionFields::secret_only(payment_secret, outer_total_msat); - let (outer_payloads, total_msat, total_htlc_offset) = build_onion_payloads( + let (outer_payloads, total_msat, total_htlc_offset) = test_build_onion_payloads( &path, - outer_total_msat, &recipient_onion_fields, outer_starting_htlc_offset, &None, @@ -2080,7 +2077,6 @@ fn test_trampoline_onion_payload_assembly_values() { &Secp256k1::new(), &path, &session_priv, - amt_msat, &recipient_onion_fields, cur_height, &payment_hash, @@ -2540,9 +2536,8 @@ fn test_phantom_invalid_onion_payload() { construct_onion_keys(&Secp256k1::new(), &route.paths[0], &session_priv); let recipient_onion_fields = RecipientOnionFields::secret_only(payment_secret, msgs::MAX_VALUE_MSAT + 1); - let (mut onion_payloads, _, _) = build_onion_payloads( + let (mut onion_payloads, _, _) = test_build_onion_payloads( &route.paths[0], - msgs::MAX_VALUE_MSAT + 1, &recipient_onion_fields, height + 1, &None, diff --git a/lightning/src/ln/onion_utils.rs b/lightning/src/ln/onion_utils.rs index b5c5088eecb..f9bcbea7e34 100644 --- a/lightning/src/ln/onion_utils.rs +++ b/lightning/src/ln/onion_utils.rs @@ -193,7 +193,7 @@ trait OnionPayload<'a, 'b> { ) -> Self; fn new_receive( recipient_onion: &'a RecipientOnionFields, keysend_preimage: Option, - sender_intended_htlc_amt_msat: u64, total_msat: u64, cltv_expiry_height: u32, + sender_intended_htlc_amt_msat: u64, cltv_expiry_height: u32, ) -> Result; fn new_blinded_forward( encrypted_tlvs: &'a Vec, intro_node_blinding_point: Option, @@ -205,8 +205,8 @@ trait OnionPayload<'a, 'b> { custom_tlvs: &'a Vec<(u64, Vec)>, ) -> Self; fn new_trampoline_entry( - total_msat: u64, amt_to_forward: u64, outgoing_cltv_value: u32, - recipient_onion: &'a RecipientOnionFields, packet: msgs::TrampolineOnionPacket, + amt_to_forward: u64, outgoing_cltv_value: u32, recipient_onion: &'a RecipientOnionFields, + packet: msgs::TrampolineOnionPacket, ) -> Result; } impl<'a, 'b> OnionPayload<'a, 'b> for msgs::OutboundOnionPayload<'a> { @@ -217,13 +217,15 @@ impl<'a, 'b> OnionPayload<'a, 'b> for msgs::OutboundOnionPayload<'a> { } fn new_receive( recipient_onion: &'a RecipientOnionFields, keysend_preimage: Option, - sender_intended_htlc_amt_msat: u64, total_msat: u64, cltv_expiry_height: u32, + sender_intended_htlc_amt_msat: u64, cltv_expiry_height: u32, ) -> Result { - debug_assert_eq!(total_msat, recipient_onion.total_mpp_amount_msat); Ok(Self::Receive { - payment_data: recipient_onion - .payment_secret - .map(|payment_secret| msgs::FinalOnionHopData { payment_secret, total_msat }), + payment_data: recipient_onion.payment_secret.map(|payment_secret| { + msgs::FinalOnionHopData { + payment_secret, + total_msat: recipient_onion.total_mpp_amount_msat, + } + }), payment_metadata: recipient_onion.payment_metadata.as_ref(), keysend_preimage, custom_tlvs: &recipient_onion.custom_tlvs, @@ -255,16 +257,18 @@ impl<'a, 'b> OnionPayload<'a, 'b> for msgs::OutboundOnionPayload<'a> { } fn new_trampoline_entry( - total_msat: u64, amt_to_forward: u64, outgoing_cltv_value: u32, - recipient_onion: &'a RecipientOnionFields, packet: msgs::TrampolineOnionPacket, + amt_to_forward: u64, outgoing_cltv_value: u32, recipient_onion: &'a RecipientOnionFields, + packet: msgs::TrampolineOnionPacket, ) -> Result { - debug_assert_eq!(total_msat, recipient_onion.total_mpp_amount_msat); Ok(Self::TrampolineEntrypoint { amt_to_forward, outgoing_cltv_value, - multipath_trampoline_data: recipient_onion - .payment_secret - .map(|payment_secret| msgs::FinalOnionHopData { payment_secret, total_msat }), + multipath_trampoline_data: recipient_onion.payment_secret.map(|payment_secret| { + msgs::FinalOnionHopData { + payment_secret, + total_msat: recipient_onion.total_mpp_amount_msat, + } + }), trampoline_packet: packet, }) } @@ -279,7 +283,7 @@ impl<'a, 'b> OnionPayload<'a, 'b> for msgs::OutboundTrampolinePayload<'a> { } fn new_receive( _recipient_onion: &'a RecipientOnionFields, _keysend_preimage: Option, - _sender_intended_htlc_amt_msat: u64, _total_msat: u64, _cltv_expiry_height: u32, + _sender_intended_htlc_amt_msat: u64, _cltv_expiry_height: u32, ) -> Result { Err(APIError::InvalidRoute { err: "Unblinded receiving is not supported for Trampoline!".to_string(), @@ -308,7 +312,7 @@ impl<'a, 'b> OnionPayload<'a, 'b> for msgs::OutboundTrampolinePayload<'a> { } fn new_trampoline_entry( - _total_msat: u64, _amt_to_forward: u64, _outgoing_cltv_value: u32, + _amt_to_forward: u64, _outgoing_cltv_value: u32, _recipient_onion: &'a RecipientOnionFields, _packet: msgs::TrampolineOnionPacket, ) -> Result { Err(APIError::InvalidRoute { @@ -410,7 +414,7 @@ pub(super) fn construct_trampoline_onion_keys( } pub(super) fn build_trampoline_onion_payloads<'a>( - blinded_tail: &'a BlindedTail, total_msat: u64, recipient_onion: &'a RecipientOnionFields, + blinded_tail: &'a BlindedTail, recipient_onion: &'a RecipientOnionFields, starting_htlc_offset: u32, keysend_preimage: &Option, ) -> Result<(Vec>, u64, u32), APIError> { let mut res: Vec = @@ -425,7 +429,6 @@ pub(super) fn build_trampoline_onion_payloads<'a>( let (value_msat, cltv) = build_onion_payloads_callback( blinded_tail.trampoline_hops.iter(), Some(blinded_tail_with_hop_iter), - total_msat, recipient_onion, starting_htlc_offset, keysend_preimage, @@ -439,14 +442,28 @@ pub(super) fn build_trampoline_onion_payloads<'a>( } /// returns the hop data, as well as the first-hop value_msat and CLTV value we should send. -pub(super) fn build_onion_payloads<'a>( - path: &'a Path, total_msat: u64, recipient_onion: &'a RecipientOnionFields, - starting_htlc_offset: u32, keysend_preimage: &Option, - invoice_request: Option<&'a InvoiceRequest>, +#[cfg(any(test, feature = "_externalize_tests"))] +pub(crate) fn test_build_onion_payloads<'a>( + path: &'a Path, recipient_onion: &'a RecipientOnionFields, starting_htlc_offset: u32, + keysend_preimage: &Option, invoice_request: Option<&'a InvoiceRequest>, trampoline_packet: Option, ) -> Result<(Vec>, u64, u32), APIError> { - debug_assert_eq!(total_msat, recipient_onion.total_mpp_amount_msat); + build_onion_payloads( + path, + recipient_onion, + starting_htlc_offset, + keysend_preimage, + invoice_request, + trampoline_packet, + ) +} +/// returns the hop data, as well as the first-hop value_msat and CLTV value we should send. +fn build_onion_payloads<'a>( + path: &'a Path, recipient_onion: &'a RecipientOnionFields, starting_htlc_offset: u32, + keysend_preimage: &Option, invoice_request: Option<&'a InvoiceRequest>, + trampoline_packet: Option, +) -> Result<(Vec>, u64, u32), APIError> { let mut res: Vec = Vec::with_capacity( path.hops.len() + path.blinded_tail.as_ref().map_or(0, |t| t.hops.len()), ); @@ -472,7 +489,6 @@ pub(super) fn build_onion_payloads<'a>( let (value_msat, cltv) = build_onion_payloads_callback( path.hops.iter(), blinded_tail_with_hop_iter, - total_msat, recipient_onion, starting_htlc_offset, keysend_preimage, @@ -503,7 +519,7 @@ enum PayloadCallbackAction { PushFront, } fn build_onion_payloads_callback<'a, 'b, H, B, F, OP>( - hops: H, mut blinded_tail: Option>, total_msat: u64, + hops: H, mut blinded_tail: Option>, recipient_onion: &'a RecipientOnionFields, starting_htlc_offset: u32, keysend_preimage: &Option, invoice_request: Option<&'a InvoiceRequest>, mut callback: F, @@ -518,8 +534,6 @@ where let mut cur_cltv = starting_htlc_offset; let mut last_hop_id = None; - debug_assert_eq!(total_msat, recipient_onion.total_mpp_amount_msat); - for (idx, hop) in hops.rev().enumerate() { // First hop gets special values so that it can check, on receipt, that everything is // exactly as it should be (and the next hop isn't trying to probe to find out if we're @@ -548,7 +562,7 @@ where PayloadCallbackAction::PushBack, OP::new_blinded_receive( final_value_msat, - total_msat, + recipient_onion.total_mpp_amount_msat, cur_cltv + excess_final_cltv_expiry_delta, &blinded_hop.encrypted_payload, blinding_point.take(), @@ -576,7 +590,6 @@ where callback( PayloadCallbackAction::PushBack, OP::new_trampoline_entry( - total_msat, final_value_msat + hop.fee_msat(), cur_cltv, &recipient_onion, @@ -587,13 +600,7 @@ where None => { callback( PayloadCallbackAction::PushBack, - OP::new_receive( - &recipient_onion, - *keysend_preimage, - value_msat, - total_msat, - cltv, - )?, + OP::new_receive(&recipient_onion, *keysend_preimage, value_msat, cltv)?, ); }, } @@ -674,7 +681,6 @@ pub(crate) fn set_max_path_length( let build_payloads_res = build_onion_payloads_callback( core::iter::once(&unblinded_route_hop), blinded_tail_opt, - final_value_msat_with_overpay_buffer, &recipient_onion_with_excess_value, best_block_height, &keysend_preimage, @@ -2596,7 +2602,7 @@ pub(super) fn peel_dummy_hop_update_add_htlc( - secp_ctx: &Secp256k1, path: &Path, session_priv: &SecretKey, total_msat: u64, + secp_ctx: &Secp256k1, path: &Path, session_priv: &SecretKey, recipient_onion: &RecipientOnionFields, cur_block_height: u32, payment_hash: &PaymentHash, keysend_preimage: &Option, invoice_request: Option<&InvoiceRequest>, prng_seed: [u8; 32], @@ -2605,7 +2611,6 @@ pub fn create_payment_onion( secp_ctx, path, session_priv, - total_msat, recipient_onion, cur_block_height, payment_hash, @@ -2627,15 +2632,12 @@ pub(super) fn compute_trampoline_session_priv(outer_onion_session_priv: &SecretK /// Build a payment onion, returning the first hop msat and cltv values as well. /// `cur_block_height` should be set to the best known block height + 1. pub(crate) fn create_payment_onion_internal( - secp_ctx: &Secp256k1, path: &Path, session_priv: &SecretKey, total_msat: u64, + secp_ctx: &Secp256k1, path: &Path, session_priv: &SecretKey, recipient_onion: &RecipientOnionFields, cur_block_height: u32, payment_hash: &PaymentHash, keysend_preimage: &Option, invoice_request: Option<&InvoiceRequest>, prng_seed: [u8; 32], trampoline_session_priv_override: Option, trampoline_prng_seed_override: Option<[u8; 32]>, ) -> Result<(msgs::OnionPacket, u64, u32), APIError> { - debug_assert_eq!(total_msat, recipient_onion.total_mpp_amount_msat); - - let mut outer_total_msat = total_msat; let mut outer_starting_htlc_offset = cur_block_height; let mut trampoline_packet_option = None; @@ -2649,10 +2651,10 @@ pub(crate) fn create_payment_onion_internal( if let Some(blinded_tail) = &path.blinded_tail { if !blinded_tail.trampoline_hops.is_empty() { let trampoline_payloads; + let outer_total_msat; (trampoline_payloads, outer_total_msat, outer_starting_htlc_offset) = build_trampoline_onion_payloads( &blinded_tail, - total_msat, recipient_onion, cur_block_height, keysend_preimage, @@ -2683,7 +2685,6 @@ pub(crate) fn create_payment_onion_internal( let (onion_payloads, htlc_msat, htlc_cltv) = build_onion_payloads( &path, - outer_total_msat, outer_onion, outer_starting_htlc_offset, keysend_preimage, diff --git a/lightning/src/ln/outbound_payment.rs b/lightning/src/ln/outbound_payment.rs index 6a72601d4a0..f677d811894 100644 --- a/lightning/src/ln/outbound_payment.rs +++ b/lightning/src/ln/outbound_payment.rs @@ -907,7 +907,6 @@ pub(super) struct SendAlongPathArgs<'a> { pub path: &'a Path, pub payment_hash: &'a PaymentHash, pub recipient_onion: &'a RecipientOnionFields, - pub total_value: u64, pub cur_height: u32, pub payment_id: PaymentId, pub keysend_preimage: &'a Option, @@ -1186,7 +1185,7 @@ impl OutboundPayments { let result = self.pay_route_internal( &route, payment_hash, &recipient_onion, keysend_preimage, invoice_request, Some(&bolt12_invoice), payment_id, - Some(route_params.final_value_msat), &onion_session_privs, hold_htlcs_at_next_hop, node_signer, + &onion_session_privs, hold_htlcs_at_next_hop, node_signer, best_block_height, &send_payment_along_path ); log_info!( @@ -1577,7 +1576,7 @@ impl OutboundPayments { })?; let res = self.pay_route_internal(&route, payment_hash, &recipient_onion, - keysend_preimage, None, None, payment_id, None, &onion_session_privs, false, node_signer, + keysend_preimage, None, None, payment_id, &onion_session_privs, false, node_signer, best_block_height, &send_payment_along_path); log_info!(logger, "Sending payment with id {} and hash {} returned {:?}", payment_id, payment_hash, res); @@ -1644,7 +1643,7 @@ impl OutboundPayments { } } } - let (total_msat, recipient_onion, keysend_preimage, onion_session_privs, invoice_request, bolt12_invoice) = { + let (recipient_onion, keysend_preimage, onion_session_privs, invoice_request, bolt12_invoice) = { let mut outbounds = self.pending_outbound_payments.lock().unwrap(); match outbounds.entry(payment_id) { hash_map::Entry::Occupied(mut payment) => { @@ -1667,12 +1666,11 @@ impl OutboundPayments { return } - let total_msat = *total_msat; let recipient_onion = RecipientOnionFields { payment_secret: *payment_secret, payment_metadata: payment_metadata.clone(), custom_tlvs: custom_tlvs.clone(), - total_mpp_amount_msat: total_msat, + total_mpp_amount_msat: *total_msat, }; let keysend_preimage = *keysend_preimage; let invoice_request = invoice_request.clone(); @@ -1689,7 +1687,7 @@ impl OutboundPayments { payment.get_mut().increment_attempts(); let bolt12_invoice = payment.get().bolt12_invoice(); - (total_msat, recipient_onion, keysend_preimage, onion_session_privs, invoice_request, bolt12_invoice.cloned()) + (recipient_onion, keysend_preimage, onion_session_privs, invoice_request, bolt12_invoice.cloned()) }, PendingOutboundPayment::Legacy { .. } => { log_error!(logger, "Unable to retry payments that were initially sent on LDK versions prior to 0.0.102"); @@ -1729,7 +1727,7 @@ impl OutboundPayments { } }; let res = self.pay_route_internal(&route, payment_hash, &recipient_onion, keysend_preimage, - invoice_request.as_ref(), bolt12_invoice.as_ref(), payment_id, Some(total_msat), + invoice_request.as_ref(), bolt12_invoice.as_ref(), payment_id, &onion_session_privs, false, node_signer, best_block_height, &send_payment_along_path); log_info!(logger, "Result retrying payment id {}: {:?}", &payment_id, res); if let Err(e) = res { @@ -1888,7 +1886,7 @@ impl OutboundPayments { })?; match self.pay_route_internal(&route, payment_hash, &recipient_onion_fields, - None, None, None, payment_id, None, &onion_session_privs, false, node_signer, + None, None, None, payment_id, &onion_session_privs, false, node_signer, best_block_height, &send_payment_along_path ) { Ok(()) => Ok((payment_hash, payment_id)), @@ -2133,7 +2131,7 @@ impl OutboundPayments { fn pay_route_internal( &self, route: &Route, payment_hash: PaymentHash, recipient_onion: &RecipientOnionFields, keysend_preimage: Option, invoice_request: Option<&InvoiceRequest>, bolt12_invoice: Option<&PaidBolt12Invoice>, - payment_id: PaymentId, recv_value_msat: Option, onion_session_privs: &Vec<[u8; 32]>, + payment_id: PaymentId, onion_session_privs: &Vec<[u8; 32]>, hold_htlcs_at_next_hop: bool, node_signer: &NS, best_block_height: u32, send_payment_along_path: &F ) -> Result<(), PaymentSendFailure> where @@ -2147,7 +2145,6 @@ impl OutboundPayments { { return Err(PaymentSendFailure::ParameterError(APIError::APIMisuseError{err: "Payment secret is required for multi-path payments".to_owned()})); } - let mut total_value = 0; let our_node_id = node_signer.get_node_id(Recipient::Node).unwrap(); // TODO no unwrap let mut path_errs = Vec::with_capacity(route.paths.len()); 'path_check: for path in route.paths.iter() { @@ -2170,22 +2167,18 @@ impl OutboundPayments { continue 'path_check; } } - total_value += path.final_value_msat(); path_errs.push(Ok(())); } if path_errs.iter().any(|e| e.is_err()) { return Err(PaymentSendFailure::PathParameterError(path_errs)); } - if let Some(amt_msat) = recv_value_msat { - total_value = amt_msat; - } let cur_height = best_block_height + 1; let mut results = Vec::new(); debug_assert_eq!(route.paths.len(), onion_session_privs.len()); for (path, session_priv_bytes) in route.paths.iter().zip(onion_session_privs.iter()) { let path_res = send_payment_along_path(SendAlongPathArgs { - path: &path, payment_hash: &payment_hash, recipient_onion, total_value, + path: &path, payment_hash: &payment_hash, recipient_onion, cur_height, payment_id, keysend_preimage: &keysend_preimage, invoice_request, bolt12_invoice, hold_htlc_at_next_hop: hold_htlcs_at_next_hop, session_priv_bytes: *session_priv_bytes @@ -2246,7 +2239,7 @@ impl OutboundPayments { #[rustfmt::skip] pub(super) fn test_send_payment_internal( &self, route: &Route, payment_hash: PaymentHash, recipient_onion: RecipientOnionFields, - keysend_preimage: Option, payment_id: PaymentId, recv_value_msat: Option, + keysend_preimage: Option, payment_id: PaymentId, onion_session_privs: Vec<[u8; 32]>, node_signer: &NS, best_block_height: u32, send_payment_along_path: F ) -> Result<(), PaymentSendFailure> @@ -2254,7 +2247,7 @@ impl OutboundPayments { F: Fn(SendAlongPathArgs) -> Result<(), APIError>, { self.pay_route_internal(route, payment_hash, &recipient_onion, - keysend_preimage, None, None, payment_id, recv_value_msat, &onion_session_privs, + keysend_preimage, None, None, payment_id, &onion_session_privs, false, node_signer, best_block_height, &send_payment_along_path) .map_err(|e| { self.remove_outbound_if_all_failed(payment_id, &e); e }) } diff --git a/lightning/src/ln/payment_tests.rs b/lightning/src/ln/payment_tests.rs index a4eaa1653ca..662d3543f8f 100644 --- a/lightning/src/ln/payment_tests.rs +++ b/lightning/src/ln/payment_tests.rs @@ -3328,9 +3328,7 @@ fn retry_multi_path_single_failed_payment() { scorer.expect_usage(chans[1].short_channel_id.unwrap(), usage); } - // Note that while we actaully pay amt_msat + 1, we should really set the onion amount to - // amt_msat as that's what we built a route for. - let onion = RecipientOnionFields::secret_only(payment_secret, amt_msat + 1); + let onion = RecipientOnionFields::secret_only(payment_secret, amt_msat); let id = PaymentId(payment_hash.0); nodes[0].node.send_payment(payment_hash, onion, id, route_params, Retry::Attempts(1)).unwrap(); let events = nodes[0].node.get_and_clear_pending_events(); @@ -4687,7 +4685,7 @@ fn do_test_custom_tlvs_consistency( let priv_a = session_privs[0]; nodes[0] .node - .test_send_payment_along_path(path_a, &hash, onion, amt_msat, cur_height, id, &None, priv_a) + .test_send_payment_along_path(path_a, &hash, onion, cur_height, id, &None, priv_a) .unwrap(); check_added_monitors(&nodes[0], 1); @@ -4710,7 +4708,7 @@ fn do_test_custom_tlvs_consistency( let priv_b = session_privs[1]; nodes[0] .node - .test_send_payment_along_path(path_b, &hash, onion, amt_msat, cur_height, id, &None, priv_b) + .test_send_payment_along_path(path_b, &hash, onion, cur_height, id, &None, priv_b) .unwrap(); check_added_monitors(&nodes[0], 1); @@ -5103,7 +5101,6 @@ fn peel_payment_onion_custom_tlvs() { &secp_ctx, &route.paths[0], &session_priv, - amt_msat, &recipient_onion, nodes[0].best_block_info().1, &payment_hash, diff --git a/lightning/src/ln/reload_tests.rs b/lightning/src/ln/reload_tests.rs index 6bc1eb3ad38..61d11d664b0 100644 --- a/lightning/src/ln/reload_tests.rs +++ b/lightning/src/ln/reload_tests.rs @@ -1411,7 +1411,7 @@ fn test_htlc_localremoved_persistence() { let session_privs = nodes[0].node.test_add_new_pending_payment(mismatch_payment_hash, RecipientOnionFields::spontaneous_empty(10_000), PaymentId(mismatch_payment_hash.0), &route).unwrap(); nodes[0].node.test_send_payment_internal(&route, mismatch_payment_hash, - RecipientOnionFields::spontaneous_empty(10_000), Some(test_preimage), PaymentId(mismatch_payment_hash.0), None, session_privs).unwrap(); + RecipientOnionFields::spontaneous_empty(10_000), Some(test_preimage), PaymentId(mismatch_payment_hash.0), session_privs).unwrap(); check_added_monitors(&nodes[0], 1); let updates = get_htlc_update_msgs(&nodes[0], &nodes[1].node.get_our_node_id()); From 2f4d8d57b184565252508e870bc9ff712ffd5e5a Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Mon, 2 Feb 2026 01:09:57 +0000 Subject: [PATCH 11/68] Add total-MPP-value storage in pending payments In some uses of LDK we need the ability to send HTLCs for only a portion of some larger MPP payment. This allows payers to make single payments which spend funds from multiple wallets, which may be important for ecash wallets holding funds in multiple mints or graduated wallets which hold funds across a trusted wallet and a self-custodial wallet. In the previous commits we moved the total-MPP-value we set in onions from being manually passed through onion-building to passing it via `RecipientOnionFields`. This introduced a subtle bug, though - payments which are retried will get a fresh `RecipientOnionFields` built from the data in `PendingOutboundPayment::Retryable`, losing any custom total-MPP-value settings and causing retries to fail. Here we fix this by storing the total-MPP-value directly in `PendingOutboundPayment::Retryable`. --- lightning/src/ln/outbound_payment.rs | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/lightning/src/ln/outbound_payment.rs b/lightning/src/ln/outbound_payment.rs index f677d811894..a10f1dc9ce1 100644 --- a/lightning/src/ln/outbound_payment.rs +++ b/lightning/src/ln/outbound_payment.rs @@ -133,6 +133,11 @@ pub(crate) enum PendingOutboundPayment { pending_fee_msat: Option, /// The total payment amount across all paths, used to verify that a retry is not overpaying. total_msat: u64, + /// The total payment amount which is set in the onion. + /// + /// This is generally equal to [`Self::Retryable::total_msat`] but may differ when making + /// payments which are sent MPP from different sources. + onion_total_msat: u64, /// Our best known block height at the time this payment was initiated. starting_block_height: u32, remaining_max_total_routing_fee_msat: Option, @@ -1650,7 +1655,7 @@ impl OutboundPayments { match payment.get() { PendingOutboundPayment::Retryable { total_msat, keysend_preimage, payment_secret, payment_metadata, - custom_tlvs, pending_amt_msat, invoice_request, .. + custom_tlvs, pending_amt_msat, invoice_request, onion_total_msat, .. } => { const RETRY_OVERFLOW_PERCENTAGE: u64 = 10; let retry_amt_msat = route.get_total_amount(); @@ -1670,7 +1675,7 @@ impl OutboundPayments { payment_secret: *payment_secret, payment_metadata: payment_metadata.clone(), custom_tlvs: custom_tlvs.clone(), - total_mpp_amount_msat: *total_msat, + total_mpp_amount_msat: *onion_total_msat, }; let keysend_preimage = *keysend_preimage; let invoice_request = invoice_request.clone(); @@ -1986,6 +1991,7 @@ impl OutboundPayments { custom_tlvs: recipient_onion.custom_tlvs, starting_block_height: best_block_height, total_msat: route.get_total_amount(), + onion_total_msat: recipient_onion.total_mpp_amount_msat, remaining_max_total_routing_fee_msat: route.route_params.as_ref().and_then(|p| p.max_total_routing_fee_msat), }; @@ -2693,6 +2699,7 @@ impl OutboundPayments { pending_amt_msat: path_amt, pending_fee_msat: Some(path_fee), total_msat: path_amt, + onion_total_msat: path_amt, starting_block_height: best_block_height, remaining_max_total_routing_fee_msat: None, // only used for retries, and we'll never retry on startup } @@ -2775,6 +2782,21 @@ impl_writeable_tlv_based_enum_upgradable!(PendingOutboundPayment, (9, custom_tlvs, optional_vec), (10, starting_block_height, required), (11, remaining_max_total_routing_fee_msat, option), + (12, onion_total_msat, (custom, u64, + // Once we get here, `total_msat` will have been read (or we'll fail to read) + |read_val: Option| Ok(read_val.unwrap_or(total_msat.0.unwrap())), + |us: &PendingOutboundPayment| { + match us { + PendingOutboundPayment::Retryable { total_msat, onion_total_msat, .. } => { + if total_msat != onion_total_msat { + Some(*onion_total_msat) + } else { + None + } + }, + _ => unreachable!(), + } + })), (13, invoice_request, option), (15, bolt12_invoice, option), (not_written, retry_strategy, (static_value, None)), From 0d47977a4d566f01ae7aa64627aa41f25796fd2d Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Mon, 2 Feb 2026 01:49:34 +0000 Subject: [PATCH 12/68] Allow BOLT 11 payments to be a part of a larger MPP payment In some uses of LDK we need the ability to send HTLCs for only a portion of some larger MPP payment. This allows payers to make single payments which spend funds from multiple wallets, which may be important for ecash wallets holding funds in multiple mints or graduated wallets which hold funds across a trusted wallet and a self-custodial wallet. In the previous few commits we added support for making these kinds of payments when using the payment methods which explicitly accepted a `RecipientOnionFields`. Here we also add support for such payments made via the `pay_for_bolt11_invoice` method, utilizing the new `OptionalBolt11PaymentParams` to hide the parameter from most calls. Test mostly by Claude --- lightning/src/ln/channelmanager.rs | 31 +++++- lightning/src/ln/invoice_utils.rs | 1 + lightning/src/ln/outbound_payment.rs | 22 +++- lightning/src/ln/payment_tests.rs | 157 +++++++++++++++++++++++++++ 4 files changed, 205 insertions(+), 6 deletions(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 7afdf97c731..94776f15655 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -686,6 +686,20 @@ pub struct OptionalBolt11PaymentParams { /// will ultimately fail once all pending paths have failed (generating an /// [`Event::PaymentFailed`]). pub retry_strategy: Retry, + /// If the payment being made from this node is part of a larger MPP payment from multiple + /// nodes (i.e. because a single payment is being made from multiple wallets), you can specify + /// the total amount being paid here. + /// + /// If this is set, it must be at least the [`Bolt11Invoice::amount_milli_satoshis`] for the + /// invoice provided to [`ChannelManager::pay_for_bolt11_invoice`]. Further, if this is set, + /// the `amount_msats` provided to [`ChannelManager::pay_for_bolt11_invoice`] is allowed to be + /// lower than [`Bolt11Invoice::amount_milli_satoshis`] (as the payment we're making may be a + /// small part of the amount needed to meet the invoice's minimum). + /// + /// If this is lower than the `amount_msats` passed to + /// [`ChannelManager::pay_for_bolt11_invoice`] the call will fail with + /// [`Bolt11PaymentError::InvalidAmount`]. + pub declared_total_mpp_value_override: Option, } impl Default for OptionalBolt11PaymentParams { @@ -697,6 +711,7 @@ impl Default for OptionalBolt11PaymentParams { retry_strategy: Retry::Timeout(core::time::Duration::from_secs(2)), #[cfg(not(feature = "std"))] retry_strategy: Retry::Attempts(3), + declared_total_mpp_value_override: None, } } } @@ -5456,10 +5471,18 @@ impl< /// The invoice's `payment_hash().0` serves as a reliable choice for the `payment_id`. /// /// # Handling Invoice Amounts - /// Some invoices include a specific amount, while others require you to specify one. - /// - If the invoice **includes** an amount, user may provide an amount greater or equal to it - /// to allow for overpayments. - /// - If the invoice **doesn't include** an amount, you'll need to specify `amount_msats`. + /// Some invoices require a specific amount (which can be fetched with + /// [`Bolt11Invoice::amount_milli_satoshis`]) while others allow you to pay amount amount. + /// + /// - If the invoice **includes** an amount, `amount_msats` may be `None` to pay exactly + /// [`Bolt11Invoice::amount_milli_satoshis`] or may be `Some` with a value greater than or + /// equal to the [`Bolt11Invoice::amount_milli_satoshis`] to allow for deliberate overpayment + /// (e.g. for "tips"). + /// - If the invoice **doesn't include** an amount, `amount_msats` must be `Some`. + /// + /// In the special case that [`OptionalBolt11PaymentParams::declared_total_mpp_value_override`] + /// is set, `amount_msats` may be `Some` and lower than + /// [`Bolt11Invoice::amount_milli_satoshis`]. See the parameter for more details. /// /// If these conditions aren’t met, the function will return [`Bolt11PaymentError::InvalidAmount`]. /// diff --git a/lightning/src/ln/invoice_utils.rs b/lightning/src/ln/invoice_utils.rs index b1097b6c274..e97d2066d83 100644 --- a/lightning/src/ln/invoice_utils.rs +++ b/lightning/src/ln/invoice_utils.rs @@ -690,6 +690,7 @@ mod test { custom_tlvs: custom_tlvs.clone(), route_params_config: RouteParametersConfig::default(), retry_strategy: Retry::Attempts(0), + declared_total_mpp_value_override: None, }; nodes[0] diff --git a/lightning/src/ln/outbound_payment.rs b/lightning/src/ln/outbound_payment.rs index a10f1dc9ce1..38000b40e74 100644 --- a/lightning/src/ln/outbound_payment.rs +++ b/lightning/src/ln/outbound_payment.rs @@ -627,7 +627,11 @@ pub(crate) enum PaymentSendFailure { #[derive(Debug)] pub enum Bolt11PaymentError { /// Incorrect amount was provided to [`ChannelManager::pay_for_bolt11_invoice`]. - /// This happens when the user-provided amount is less than an amount specified in the [`Bolt11Invoice`]. + /// + /// This happens when the user-provided amount is less than an amount specified in the + /// [`Bolt11Invoice`] or the amount set at + /// [`OptionalBolt11PaymentParams::declared_total_mpp_value_override`] was lower than the + /// explicit amount provided to [`ChannelManager::pay_for_bolt11_invoice`]. /// /// [`Bolt11Invoice`]: lightning_invoice::Bolt11Invoice /// [`ChannelManager::pay_for_bolt11_invoice`]: crate::ln::channelmanager::ChannelManager::pay_for_bolt11_invoice @@ -1031,9 +1035,11 @@ impl OutboundPayments { { let payment_hash = invoice.payment_hash(); + let partial_payment = optional_params.declared_total_mpp_value_override.is_some(); let amount = match (invoice.amount_milli_satoshis(), amount_msats) { (Some(amt), None) | (None, Some(amt)) => amt, - (Some(inv_amt), Some(user_amt)) if user_amt < inv_amt => return Err(Bolt11PaymentError::InvalidAmount), + (Some(inv_amt), Some(user_amt)) if user_amt < inv_amt && !partial_payment => + return Err(Bolt11PaymentError::InvalidAmount), (Some(_), Some(user_amt)) => user_amt, (None, None) => return Err(Bolt11PaymentError::InvalidAmount), }; @@ -1043,6 +1049,18 @@ impl OutboundPayments { .with_custom_tlvs(optional_params.custom_tlvs); recipient_onion.payment_metadata = invoice.payment_metadata().map(|v| v.clone()); + if let Some(mpp_amt) = optional_params.declared_total_mpp_value_override { + if mpp_amt < amount { + return Err(Bolt11PaymentError::InvalidAmount); + } + if let Some(invoice_amount) = invoice.amount_milli_satoshis() { + if mpp_amt < invoice_amount { + return Err(Bolt11PaymentError::InvalidAmount); + } + } + recipient_onion.total_mpp_amount_msat = mpp_amt; + } + let payment_params = PaymentParameters::from_bolt11_invoice(invoice) .with_user_config_ignoring_fee_limit(optional_params.route_params_config); diff --git a/lightning/src/ln/payment_tests.rs b/lightning/src/ln/payment_tests.rs index 662d3543f8f..6d819bb0fe2 100644 --- a/lightning/src/ln/payment_tests.rs +++ b/lightning/src/ln/payment_tests.rs @@ -5423,3 +5423,160 @@ fn max_out_mpp_path() { check_added_monitors(&nodes[0], 2); // one monitor update per MPP part nodes[0].node.get_and_clear_pending_msg_events(); } + +#[test] +fn bolt11_multi_node_mpp() { + // Test that multiple nodes can collaborate to pay a single BOLT 11 invoice, with each node + // paying a portion of the total invoice amount. This is useful for scenarios like: + // - Paying from multiple wallets (e.g., ecash wallets with funds in multiple mints) + // - Graduated wallets (funds split between trusted and self-custodial wallets) + + 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, None, None]); + let nodes = create_network(3, &node_cfgs, &node_chanmgrs); + + // Create channels: A<>C and B<>C + create_announced_chan_between_nodes(&nodes, 0, 2); + create_announced_chan_between_nodes(&nodes, 1, 2); + + // Node C creates a BOLT 11 invoice for 100_000 msat + let invoice_amt_msat = 100_000; + let invoice_params = crate::ln::channelmanager::Bolt11InvoiceParameters { + amount_msats: Some(invoice_amt_msat), + ..Default::default() + }; + let invoice = nodes[2].node.create_bolt11_invoice(invoice_params).unwrap(); + + // Node A pays 60_000 msat (part of the total) + let node_a_payment_amt = 60_000; + let payment_id_a = PaymentId([1; 32]); + let optional_params_a = crate::ln::channelmanager::OptionalBolt11PaymentParams { + declared_total_mpp_value_override: Some(invoice_amt_msat), + ..Default::default() + }; + nodes[0] + .node + .pay_for_bolt11_invoice(&invoice, payment_id_a, Some(node_a_payment_amt), optional_params_a) + .unwrap(); + check_added_monitors(&nodes[0], 1); + + // Node B pays 40_000 msat (the remaining part) + let node_b_payment_amt = 40_000; + let payment_id_b = PaymentId([2; 32]); + let optional_params_b = crate::ln::channelmanager::OptionalBolt11PaymentParams { + declared_total_mpp_value_override: Some(invoice_amt_msat), + ..Default::default() + }; + nodes[1] + .node + .pay_for_bolt11_invoice(&invoice, payment_id_b, Some(node_b_payment_amt), optional_params_b) + .unwrap(); + check_added_monitors(&nodes[1], 1); + + let payment_event_a = SendEvent::from_node(&nodes[0]); + nodes[2].node.handle_update_add_htlc(nodes[0].node.get_our_node_id(), &payment_event_a.msgs[0]); + do_commitment_signed_dance(&nodes[2], &nodes[0], &payment_event_a.commitment_msg, false, false); + + let payment_event_b = SendEvent::from_node(&nodes[1]); + nodes[2].node.handle_update_add_htlc(nodes[1].node.get_our_node_id(), &payment_event_b.msgs[0]); + do_commitment_signed_dance(&nodes[2], &nodes[1], &payment_event_b.commitment_msg, false, false); + + // Process the pending HTLCs on node C and generate the PaymentClaimable event + assert!(nodes[2].node.get_and_clear_pending_events().is_empty()); + expect_and_process_pending_htlcs(&nodes[2], false); + let events = nodes[2].node.get_and_clear_pending_events(); + assert_eq!(events.len(), 1); + let payment_preimage = match &events[0] { + Event::PaymentClaimable { + payment_hash, + amount_msat, + purpose: PaymentPurpose::Bolt11InvoicePayment { payment_preimage, .. }, + .. + } => { + assert_eq!(*payment_hash, invoice.payment_hash()); + assert_eq!(*amount_msat, invoice_amt_msat); + payment_preimage.unwrap() + }, + _ => panic!("Unexpected event: {:?}", events[0]), + }; + + nodes[2].node.claim_funds(payment_preimage); + + expect_payment_claimed!(nodes[2], invoice.payment_hash(), invoice_amt_msat); + check_added_monitors(&nodes[2], 2); + + // Get the fulfill messages from C to both A and B + let mut events_c = nodes[2].node.get_and_clear_pending_msg_events(); + assert_eq!(events_c.len(), 2); + + // Handle fulfill message from C to A + let fulfill_idx_a = events_c + .iter() + .position(|ev| { + if let MessageSendEvent::UpdateHTLCs { node_id, .. } = ev { + *node_id == nodes[0].node.get_our_node_id() + } else { + false + } + }) + .unwrap(); + let fulfill_idx_b = 1 - fulfill_idx_a; + + if let MessageSendEvent::UpdateHTLCs { ref updates, .. } = events_c[fulfill_idx_a] { + nodes[0].node.handle_update_fulfill_htlc( + nodes[2].node.get_our_node_id(), + updates.update_fulfill_htlcs[0].clone(), + ); + do_commitment_signed_dance(&nodes[0], &nodes[2], &updates.commitment_signed, false, false); + } + + let payment_sent = nodes[0].node.get_and_clear_pending_events(); + check_added_monitors(&nodes[0], 1); + + assert_eq!(payment_sent.len(), 2, "{payment_sent:?}"); + if let Event::PaymentSent { payment_id, payment_hash, amount_msat, fee_paid_msat, .. } = + &payment_sent[0] + { + assert_eq!(*payment_id, Some(payment_id_a)); + assert_eq!(*payment_hash, invoice.payment_hash()); + assert_eq!(*amount_msat, Some(node_a_payment_amt)); + assert_eq!(*fee_paid_msat, Some(0)); + } else { + panic!("{payment_sent:?}"); + } + if let Event::PaymentPathSuccessful { payment_id, .. } = &payment_sent[1] { + assert_eq!(*payment_id, payment_id_a); + } else { + panic!("{payment_sent:?}"); + } + + // Handle fulfill message from C to B + if let MessageSendEvent::UpdateHTLCs { ref updates, .. } = events_c[fulfill_idx_b] { + nodes[1].node.handle_update_fulfill_htlc( + nodes[2].node.get_our_node_id(), + updates.update_fulfill_htlcs[0].clone(), + ); + do_commitment_signed_dance(&nodes[1], &nodes[2], &updates.commitment_signed, false, false); + } + + let payment_sent = nodes[1].node.get_and_clear_pending_events(); + check_added_monitors(&nodes[1], 1); + + assert_eq!(payment_sent.len(), 2, "{payment_sent:?}"); + if let Event::PaymentSent { payment_id, payment_hash, amount_msat, fee_paid_msat, .. } = + &payment_sent[0] + { + assert_eq!(*payment_id, Some(payment_id_b)); + assert_eq!(*payment_hash, invoice.payment_hash()); + assert_eq!(*amount_msat, Some(node_b_payment_amt)); + assert_eq!(*fee_paid_msat, Some(0)); + } else { + panic!("{payment_sent:?}"); + } + if let Event::PaymentPathSuccessful { payment_id, .. } = &payment_sent[1] { + assert_eq!(*payment_id, payment_id_b); + } else { + panic!("{payment_sent:?}"); + } +} From af0c57dfa7a4fbefed2fd788aa61efd9084e7654 Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Sun, 8 Feb 2026 23:43:07 +0000 Subject: [PATCH 13/68] Use HTLC CLTV instead of onion CLTV values for payment claim timer When we receive an HTLC as a part of a claim, we validate that the CLTV on the HTLC is >= the CLTV that the sender requested we receive, but then we use the CLTV value that the sender requested we receive as the deadline to claim the HTLC anyway. This isn't generally all that interesting (they're always the same unless the previous-hop node gave us "free CLTV"), but for trampoline payments where we're both a trampoline hop and the blinded intro point and the recipient, it means we end up allowing ourselves less claim time than we actually have. Instead, here, we just use the actual HTLC CLTV deadline. --- lightning/src/ln/blinded_payment_tests.rs | 15 +++++---------- lightning/src/ln/onion_payment.rs | 6 +++--- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/lightning/src/ln/blinded_payment_tests.rs b/lightning/src/ln/blinded_payment_tests.rs index 3eb1f55d445..e5ed0301be2 100644 --- a/lightning/src/ln/blinded_payment_tests.rs +++ b/lightning/src/ln/blinded_payment_tests.rs @@ -981,11 +981,11 @@ fn do_multi_hop_receiver_fail(check: ReceiveCheckFail) { }; let amt_msat = 5000; - let excess_final_cltv_delta_opt = if check == ReceiveCheckFail::ProcessPendingHTLCsCheck { - // Set the final CLTV expiry too low to trigger the failure in process_pending_htlc_forwards. - Some(TEST_FINAL_CLTV as u16 - 2) + let required_final_cltv = if check == ReceiveCheckFail::ProcessPendingHTLCsCheck { + // Set the final CLTV required much too high to trigger the failure in process_pending_htlc_forwards. + Some((TEST_FINAL_CLTV as u16) * 10) } else { None }; - let (_, payment_hash, payment_secret) = get_payment_preimage_hash(&nodes[2], Some(amt_msat), excess_final_cltv_delta_opt); + let (_, payment_hash, payment_secret) = get_payment_preimage_hash(&nodes[2], Some(amt_msat), required_final_cltv); let mut route_params = get_blinded_route_parameters(amt_msat, payment_secret, 1, 1_0000_0000, nodes.iter().skip(1).map(|n| n.node.get_our_node_id()).collect(), &[&chan_upd_1_2], &chanmon_cfgs[2].keys_manager); @@ -993,11 +993,7 @@ fn do_multi_hop_receiver_fail(check: ReceiveCheckFail) { route_params.payment_params.max_path_length = 17; let route = if check == ReceiveCheckFail::ProcessPendingHTLCsCheck { - let mut route = get_route(&nodes[0], &route_params).unwrap(); - // Set the final CLTV expiry too low to trigger the failure in process_pending_htlc_forwards. - route.paths[0].hops.last_mut().map(|h| h.cltv_expiry_delta += excess_final_cltv_delta_opt.unwrap() as u32); - route.paths[0].blinded_tail.as_mut().map(|bt| bt.excess_final_cltv_expiry_delta = excess_final_cltv_delta_opt.unwrap() as u32); - route + get_route(&nodes[0], &route_params).unwrap() } else if check == ReceiveCheckFail::PaymentConstraints { // Create a blinded path where the receiver's encrypted payload has an htlc_minimum_msat that is // violated by `amt_msat`, and stick it in the route_params without changing the corresponding @@ -1115,7 +1111,6 @@ fn do_multi_hop_receiver_fail(check: ReceiveCheckFail) { check_added_monitors(&nodes[2], 1); }, ReceiveCheckFail::ProcessPendingHTLCsCheck => { - assert_eq!(payment_event_1_2.msgs[0].cltv_expiry, nodes[0].best_block_info().1 + 1 + excess_final_cltv_delta_opt.unwrap() as u32 + TEST_FINAL_CLTV); nodes[2].node.handle_update_add_htlc(nodes[1].node.get_our_node_id(), &payment_event_1_2.msgs[0]); check_added_monitors(&nodes[2], 0); do_commitment_signed_dance(&nodes[2], &nodes[1], &payment_event_1_2.commitment_msg, true, true); diff --git a/lightning/src/ln/onion_payment.rs b/lightning/src/ln/onion_payment.rs index def4a1861c4..5111f6982fe 100644 --- a/lightning/src/ln/onion_payment.rs +++ b/lightning/src/ln/onion_payment.rs @@ -438,7 +438,7 @@ pub(super) fn create_recv_pending_htlc_info( payment_data, payment_preimage, payment_metadata, - incoming_cltv_expiry: onion_cltv_expiry, + incoming_cltv_expiry: cltv_expiry, custom_tlvs, requires_blinded_error, has_recipient_created_payment_secret, @@ -450,7 +450,7 @@ pub(super) fn create_recv_pending_htlc_info( payment_data: data, payment_metadata, payment_context, - incoming_cltv_expiry: onion_cltv_expiry, + incoming_cltv_expiry: cltv_expiry, phantom_shared_secret, trampoline_shared_secret, custom_tlvs, @@ -842,7 +842,7 @@ mod tests { PendingHTLCRouting::ReceiveKeysend { payment_preimage, payment_data, incoming_cltv_expiry, .. } => { assert_eq!(payment_preimage, preimage); assert_eq!(peeled2.outgoing_amt_msat, recipient_amount); - assert_eq!(incoming_cltv_expiry, peeled2.outgoing_cltv_value); + assert_eq!(incoming_cltv_expiry, msg.cltv_expiry); let msgs::FinalOnionHopData{total_msat, payment_secret} = payment_data.unwrap(); assert_eq!(total_msat, total_amt_msat); assert_eq!(payment_secret, pay_secret); From 06c0c6d198e84a9f8656f8c69b953d822936f075 Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Sun, 8 Feb 2026 23:45:27 +0000 Subject: [PATCH 14/68] Fix trampoline onion encoding to match doc-declared CLTV rules The docs for `RouteHop::cltv_expiry_delta` claim that it includes any trampoline hops, but the way we actually implemented onion building it did not. Because the docs described a simpler and more backwards-compatible API, we update the onion-building logic to match rather than updating the docs. --- lightning/src/ln/blinded_payment_tests.rs | 63 +++++++++++++---------- lightning/src/ln/functional_test_utils.rs | 15 +++++- lightning/src/ln/onion_route_tests.rs | 9 ++-- lightning/src/ln/onion_utils.rs | 21 +++----- lightning/src/routing/router.rs | 4 +- 5 files changed, 64 insertions(+), 48 deletions(-) diff --git a/lightning/src/ln/blinded_payment_tests.rs b/lightning/src/ln/blinded_payment_tests.rs index e5ed0301be2..b4d3d36b383 100644 --- a/lightning/src/ln/blinded_payment_tests.rs +++ b/lightning/src/ln/blinded_payment_tests.rs @@ -1852,7 +1852,7 @@ fn test_combined_trampoline_onion_creation_vectors() { short_channel_id: (572330 << 40) + (42 << 16) + 2821, channel_features: ChannelFeatures::empty(), fee_msat: 153_000, - cltv_expiry_delta: 0, + cltv_expiry_delta: 24 + 36, maybe_announced_channel: false, }, ], @@ -1947,7 +1947,7 @@ fn test_trampoline_inbound_payment_decoding() { short_channel_id: (572330 << 40) + (42 << 16) + 2821, channel_features: ChannelFeatures::empty(), fee_msat: 150_153_000, - cltv_expiry_delta: 0, + cltv_expiry_delta: 24 + 36, maybe_announced_channel: false, }, ], @@ -2115,7 +2115,7 @@ fn test_trampoline_forward_payload_encoded_as_receive() { blinded_path::utils::construct_blinded_hops( &secp_ctx, path.into_iter(), &trampoline_session_priv, ) - }; + }; let route = Route { paths: vec![Path { @@ -2138,7 +2138,7 @@ fn test_trampoline_forward_payload_encoded_as_receive() { short_channel_id: bob_carol_scid, channel_features: ChannelFeatures::empty(), fee_msat: 0, - cltv_expiry_delta: 48, + cltv_expiry_delta: 24 + 39, maybe_announced_channel: false, } ], @@ -2176,7 +2176,7 @@ fn test_trampoline_forward_payload_encoded_as_receive() { }); let recipient_onion_fields = RecipientOnionFields::spontaneous_empty(amt_msat); - let (mut trampoline_payloads, outer_total_msat, outer_starting_htlc_offset) = onion_utils::build_trampoline_onion_payloads(&blinded_tail, &recipient_onion_fields, 32, &None).unwrap(); + let (mut trampoline_payloads, outer_total_msat) = onion_utils::build_trampoline_onion_payloads(&blinded_tail, &recipient_onion_fields, 32, &None).unwrap(); // pop the last dummy hop trampoline_payloads.pop(); @@ -2191,7 +2191,7 @@ fn test_trampoline_forward_payload_encoded_as_receive() { ).unwrap(); let recipient_onion_fields = RecipientOnionFields::spontaneous_empty(outer_total_msat); - let (outer_payloads, _, _) = onion_utils::test_build_onion_payloads(&route.paths[0], &recipient_onion_fields, outer_starting_htlc_offset, &None, None, Some(trampoline_packet)).unwrap(); + let (outer_payloads, _, _) = onion_utils::test_build_onion_payloads(&route.paths[0], &recipient_onion_fields, 32, &None, None, Some(trampoline_packet)).unwrap(); let outer_onion_keys = onion_utils::construct_onion_keys(&secp_ctx, &route.clone().paths[0], &outer_session_priv); let outer_packet = onion_utils::construct_onion_packet( outer_payloads, @@ -2304,7 +2304,7 @@ fn do_test_trampoline_single_hop_receive(success: bool) { short_channel_id: bob_carol_scid, channel_features: ChannelFeatures::empty(), fee_msat: 0, - cltv_expiry_delta: 48, + cltv_expiry_delta: 104 + 39, maybe_announced_channel: false, } ], @@ -2423,8 +2423,8 @@ fn test_trampoline_blinded_receive() { /// Creates a blinded tail where Carol receives via a blinded path. fn create_blinded_tail( secp_ctx: &Secp256k1, override_random_bytes: [u8; 32], carol_node_id: PublicKey, - carol_auth_key: ReceiveAuthKey, trampoline_cltv_expiry_delta: u32, final_value_msat: u64, - payment_secret: PaymentSecret, + carol_auth_key: ReceiveAuthKey, trampoline_cltv_expiry_delta: u32, + excess_final_cltv_delta: u32, final_value_msat: u64, payment_secret: PaymentSecret, ) -> BlindedTail { let outer_session_priv = SecretKey::from_slice(&override_random_bytes).unwrap(); let trampoline_session_priv = onion_utils::compute_trampoline_session_priv(&outer_session_priv); @@ -2459,7 +2459,7 @@ fn create_blinded_tail( }], hops: carol_blinded_hops, blinding_point: carol_blinding_point, - excess_final_cltv_expiry_delta: 39, + excess_final_cltv_expiry_delta: excess_final_cltv_delta, final_value_msat, } } @@ -2468,8 +2468,9 @@ fn create_blinded_tail( // payloads that send to unblinded receives and invalid payloads. fn replacement_onion( test_case: TrampolineTestCase, secp_ctx: &Secp256k1, override_random_bytes: [u8; 32], - route: Route, original_amt_msat: u64, starting_htlc_offset: u32, original_trampoline_cltv: u32, - payment_hash: PaymentHash, payment_secret: PaymentSecret, blinded: bool, + route: Route, original_amt_msat: u64, starting_htlc_offset: u32, excess_final_cltv: u32, + original_trampoline_cltv: u32, payment_hash: PaymentHash, payment_secret: PaymentSecret, + blinded: bool, ) -> msgs::OnionPacket { let outer_session_priv = SecretKey::from_slice(&override_random_bytes[..]).unwrap(); let trampoline_session_priv = onion_utils::compute_trampoline_session_priv(&outer_session_priv); @@ -2480,8 +2481,8 @@ fn replacement_onion( // Rebuild our trampoline packet from the original route. If we want to test Carol receiving // as an unblinded trampoline hop, we switch out her inner trampoline onion with a direct // receive payload because LDK doesn't support unblinded trampoline receives. - let (trampoline_packet, outer_total_msat, outer_starting_htlc_offset) = { - let (mut trampoline_payloads, outer_total_msat, outer_starting_htlc_offset) = + let (trampoline_packet, outer_total_msat) = { + let (mut trampoline_payloads, outer_total_msat) = onion_utils::build_trampoline_onion_payloads( &blinded_tail, &recipient_onion_fields, @@ -2497,7 +2498,8 @@ fn replacement_onion( total_msat: original_amt_msat, }), sender_intended_htlc_amt_msat: original_amt_msat, - cltv_expiry_height: original_trampoline_cltv + starting_htlc_offset, + cltv_expiry_height: + original_trampoline_cltv + starting_htlc_offset + excess_final_cltv, }]; } @@ -2515,7 +2517,7 @@ fn replacement_onion( ) .unwrap(); - (trampoline_packet, outer_total_msat, outer_starting_htlc_offset) + (trampoline_packet, outer_total_msat) }; // Use a different session key to construct the replacement onion packet. Note that the @@ -2524,7 +2526,7 @@ fn replacement_onion( let (mut outer_payloads, _, _) = onion_utils::test_build_onion_payloads( &route.paths[0], &recipient_onion_fields, - outer_starting_htlc_offset, + starting_htlc_offset, &None, None, Some(trampoline_packet), @@ -2542,7 +2544,7 @@ fn replacement_onion( .. } => { *amt_to_forward = test_case.outer_onion_amt(original_amt_msat); - let outer_cltv = original_trampoline_cltv + starting_htlc_offset; + let outer_cltv = original_trampoline_cltv + starting_htlc_offset + excess_final_cltv; *outgoing_cltv_value = test_case.outer_onion_cltv(outer_cltv); }, _ => panic!("final payload is not trampoline entrypoint"), @@ -2577,11 +2579,10 @@ fn do_test_trampoline_relay(blinded: bool, test_case: TrampolineTestCase) { let alice_bob_chan = create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0); let bob_carol_chan = create_announced_chan_between_nodes_with_value(&nodes, 1, 2, 1_000_000, 0); + let starting_htlc_offset = + (TOTAL_NODE_COUNT as u32) * CHAN_CONFIRM_DEPTH + 1; for i in 0..TOTAL_NODE_COUNT { - connect_blocks( - &nodes[i], - (TOTAL_NODE_COUNT as u32) * CHAN_CONFIRM_DEPTH + 1 - nodes[i].best_block_info().1, - ); + connect_blocks(&nodes[i], starting_htlc_offset - nodes[i].best_block_info().1); } let alice_node_id = nodes[0].node.get_our_node_id(); @@ -2592,8 +2593,11 @@ fn do_test_trampoline_relay(blinded: bool, test_case: TrampolineTestCase) { let bob_carol_scid = get_scid_from_channel_id(&nodes[1], bob_carol_chan.2); let original_amt_msat = 1000; - let original_trampoline_cltv = 72; - let starting_htlc_offset = 32; + // Note that for TrampolineTestCase::OuterCLTVLessThanTrampoline to work properly, + // (starting_htlc_offset + excess_final_cltv) / 2 < (starting_htlc_offset + excess_final_cltv + original_trampoline_cltv) + // otherwise dividing the CLTV value by 2 won't kick us under the outer trampoline CLTV. + let original_trampoline_cltv = 42; + let excess_final_cltv = 70; let (payment_preimage, payment_hash, payment_secret) = get_payment_preimage_hash(&nodes[2], Some(original_amt_msat), None); @@ -2620,7 +2624,7 @@ fn do_test_trampoline_relay(blinded: bool, test_case: TrampolineTestCase) { short_channel_id: bob_carol_scid, channel_features: ChannelFeatures::empty(), fee_msat: 0, - cltv_expiry_delta: 48, + cltv_expiry_delta: original_trampoline_cltv + excess_final_cltv, maybe_announced_channel: false, }, ], @@ -2633,6 +2637,7 @@ fn do_test_trampoline_relay(blinded: bool, test_case: TrampolineTestCase) { carol_node_id, nodes[2].keys_manager.get_receive_auth_key(), original_trampoline_cltv, + excess_final_cltv, original_amt_msat, payment_secret, )), @@ -2675,6 +2680,7 @@ fn do_test_trampoline_relay(blinded: bool, test_case: TrampolineTestCase) { original_amt_msat, starting_htlc_offset, original_trampoline_cltv, + excess_final_cltv, payment_hash, payment_secret, blinded, @@ -2692,7 +2698,7 @@ fn do_test_trampoline_relay(blinded: bool, test_case: TrampolineTestCase) { let amt_bytes = test_case.outer_onion_amt(original_amt_msat).to_be_bytes(); let cltv_bytes = - test_case.outer_onion_cltv(original_trampoline_cltv + starting_htlc_offset).to_be_bytes(); + test_case.outer_onion_cltv(original_trampoline_cltv + starting_htlc_offset + excess_final_cltv).to_be_bytes(); let payment_failure = test_case.payment_failed_conditions(&amt_bytes, &cltv_bytes).map(|p| { if blinded { PaymentFailedConditions::new() @@ -2706,7 +2712,8 @@ fn do_test_trampoline_relay(blinded: bool, test_case: TrampolineTestCase) { .without_claimable_event() .expect_failure(HTLCHandlingFailureType::Receive { payment_hash }) } else { - args.with_payment_secret(payment_secret) + let htlc_cltv = starting_htlc_offset + original_trampoline_cltv + excess_final_cltv; + args.with_payment_secret(payment_secret).with_payment_claimable_cltv(htlc_cltv) }; do_pass_along_path(args); @@ -2792,7 +2799,7 @@ fn test_trampoline_forward_rejection() { short_channel_id: bob_carol_scid, channel_features: ChannelFeatures::empty(), fee_msat: 0, - cltv_expiry_delta: 48, + cltv_expiry_delta: 24 + 24 + 39, maybe_announced_channel: false, } ], diff --git a/lightning/src/ln/functional_test_utils.rs b/lightning/src/ln/functional_test_utils.rs index 0e1ada083c6..d9f99ea1aa6 100644 --- a/lightning/src/ln/functional_test_utils.rs +++ b/lightning/src/ln/functional_test_utils.rs @@ -11,7 +11,7 @@ //! nodes for functional tests. use crate::blinded_path::payment::DummyTlvs; -use crate::chain::channelmonitor::ChannelMonitor; +use crate::chain::channelmonitor::{ChannelMonitor, HTLC_FAIL_BACK_BUFFER}; use crate::chain::transaction::OutPoint; use crate::chain::{BestBlock, ChannelMonitorUpdateStatus, Confirm, Listen, Watch}; use crate::events::bump_transaction::sync::{ @@ -3456,6 +3456,7 @@ pub struct PassAlongPathArgs<'a, 'b, 'c, 'd> { pub custom_tlvs: Vec<(u64, Vec)>, pub payment_metadata: Option>, pub expected_failure: Option, + pub payment_claimable_cltv: Option, } impl<'a, 'b, 'c, 'd> PassAlongPathArgs<'a, 'b, 'c, 'd> { @@ -3478,6 +3479,7 @@ impl<'a, 'b, 'c, 'd> PassAlongPathArgs<'a, 'b, 'c, 'd> { custom_tlvs: Vec::new(), payment_metadata: None, expected_failure: None, + payment_claimable_cltv: None, } } pub fn without_clearing_recipient_events(mut self) -> Self { @@ -3518,6 +3520,10 @@ impl<'a, 'b, 'c, 'd> PassAlongPathArgs<'a, 'b, 'c, 'd> { self.dummy_tlvs = dummy_tlvs.to_vec(); self } + pub fn with_payment_claimable_cltv(mut self, cltv: u32) -> Self { + self.payment_claimable_cltv = Some(cltv); + self + } } pub fn do_pass_along_path<'a, 'b, 'c>(args: PassAlongPathArgs) -> Option { @@ -3536,6 +3542,7 @@ pub fn do_pass_along_path<'a, 'b, 'c>(args: PassAlongPathArgs) -> Option custom_tlvs, payment_metadata, expected_failure, + payment_claimable_cltv, } = args; let mut payment_event = SendEvent::from_event(ev); @@ -3651,6 +3658,12 @@ pub fn do_pass_along_path<'a, 'b, 'c>(args: PassAlongPathArgs) -> Option assert_eq!(*user_chan_id, Some(chan.user_channel_id)); } assert!(claim_deadline.unwrap() > node.best_block_info().1); + if let Some(expected_cltv) = payment_claimable_cltv { + assert_eq!( + claim_deadline.unwrap(), + expected_cltv - HTLC_FAIL_BACK_BUFFER, + ); + } }, _ => panic!("Unexpected event"), } diff --git a/lightning/src/ln/onion_route_tests.rs b/lightning/src/ln/onion_route_tests.rs index f18eead8759..21d7ed0c566 100644 --- a/lightning/src/ln/onion_route_tests.rs +++ b/lightning/src/ln/onion_route_tests.rs @@ -1918,7 +1918,7 @@ fn test_trampoline_onion_payload_assembly_values() { short_channel_id: (572330 << 40) + (42 << 16) + 2821, channel_features: ChannelFeatures::empty(), fee_msat: 153_000, - cltv_expiry_delta: 0, + cltv_expiry_delta: 36 + 24, // Last hop should include the CLTV of the trampoline hops maybe_announced_channel: false, }, ], @@ -1974,7 +1974,7 @@ fn test_trampoline_onion_payload_assembly_values() { SecretKey::from_slice(&>::from_hex(SECRET_HEX).unwrap()).unwrap().secret_bytes(), ); let recipient_onion_fields = RecipientOnionFields::secret_only(payment_secret, amt_msat); - let (trampoline_payloads, outer_total_msat, outer_starting_htlc_offset) = + let (trampoline_payloads, outer_total_msat) = onion_utils::build_trampoline_onion_payloads( &path.blinded_tail.as_ref().unwrap(), &recipient_onion_fields, @@ -1984,7 +1984,6 @@ fn test_trampoline_onion_payload_assembly_values() { .unwrap(); assert_eq!(trampoline_payloads.len(), 3); assert_eq!(outer_total_msat, 150_153_000); - assert_eq!(outer_starting_htlc_offset, 800_060); let trampoline_carol_payload = &trampoline_payloads[0]; let trampoline_dave_payload = &trampoline_payloads[1]; @@ -2042,7 +2041,7 @@ fn test_trampoline_onion_payload_assembly_values() { let (outer_payloads, total_msat, total_htlc_offset) = test_build_onion_payloads( &path, &recipient_onion_fields, - outer_starting_htlc_offset, + cur_height, &None, None, Some(trampoline_packet), @@ -2067,7 +2066,7 @@ fn test_trampoline_onion_payload_assembly_values() { outer_bob_payload { assert_eq!(amt_to_forward, &150_153_000); - assert_eq!(outgoing_cltv_value, &800_084); + assert_eq!(outgoing_cltv_value, &800_060); } else { panic!("Bob payload must be Forward"); } diff --git a/lightning/src/ln/onion_utils.rs b/lightning/src/ln/onion_utils.rs index f9bcbea7e34..5b2e11024e1 100644 --- a/lightning/src/ln/onion_utils.rs +++ b/lightning/src/ln/onion_utils.rs @@ -416,7 +416,7 @@ pub(super) fn construct_trampoline_onion_keys( pub(super) fn build_trampoline_onion_payloads<'a>( blinded_tail: &'a BlindedTail, recipient_onion: &'a RecipientOnionFields, starting_htlc_offset: u32, keysend_preimage: &Option, -) -> Result<(Vec>, u64, u32), APIError> { +) -> Result<(Vec>, u64), APIError> { let mut res: Vec = Vec::with_capacity(blinded_tail.trampoline_hops.len() + blinded_tail.hops.len()); let blinded_tail_with_hop_iter = BlindedTailDetails::DirectEntry { @@ -426,7 +426,7 @@ pub(super) fn build_trampoline_onion_payloads<'a>( excess_final_cltv_expiry_delta: blinded_tail.excess_final_cltv_expiry_delta, }; - let (value_msat, cltv) = build_onion_payloads_callback( + let (value_msat, _) = build_onion_payloads_callback( blinded_tail.trampoline_hops.iter(), Some(blinded_tail_with_hop_iter), recipient_onion, @@ -438,7 +438,7 @@ pub(super) fn build_trampoline_onion_payloads<'a>( PayloadCallbackAction::PushFront => res.insert(0, payload), }, )?; - Ok((res, value_msat, cltv)) + Ok((res, value_msat)) } /// returns the hop data, as well as the first-hop value_msat and CLTV value we should send. @@ -539,11 +539,7 @@ where // exactly as it should be (and the next hop isn't trying to probe to find out if we're // the intended recipient). let value_msat = if cur_value_msat == 0 { hop.fee_msat() } else { cur_value_msat }; - let cltv = if cur_cltv == starting_htlc_offset { - hop.cltv_expiry_delta().saturating_add(starting_htlc_offset) - } else { - cur_cltv - }; + let cltv = hop.cltv_expiry_delta().saturating_add(cur_cltv); if idx == 0 { match blinded_tail.take() { Some(BlindedTailDetails::DirectEntry { @@ -591,7 +587,7 @@ where PayloadCallbackAction::PushBack, OP::new_trampoline_entry( final_value_msat + hop.fee_msat(), - cur_cltv, + cltv, &recipient_onion, trampoline_packet, )?, @@ -610,7 +606,7 @@ where err: "Next hop ID must be known for non-final hops".to_string(), })?, value_msat, - cltv, + cur_cltv, ); callback(PayloadCallbackAction::PushFront, payload); } @@ -2638,7 +2634,6 @@ pub(crate) fn create_payment_onion_internal( prng_seed: [u8; 32], trampoline_session_priv_override: Option, trampoline_prng_seed_override: Option<[u8; 32]>, ) -> Result<(msgs::OnionPacket, u64, u32), APIError> { - let mut outer_starting_htlc_offset = cur_block_height; let mut trampoline_packet_option = None; let mut trampoline_outer_onion = RecipientOnionFields { @@ -2652,7 +2647,7 @@ pub(crate) fn create_payment_onion_internal( if !blinded_tail.trampoline_hops.is_empty() { let trampoline_payloads; let outer_total_msat; - (trampoline_payloads, outer_total_msat, outer_starting_htlc_offset) = + (trampoline_payloads, outer_total_msat) = build_trampoline_onion_payloads( &blinded_tail, recipient_onion, @@ -2686,7 +2681,7 @@ pub(crate) fn create_payment_onion_internal( let (onion_payloads, htlc_msat, htlc_cltv) = build_onion_payloads( &path, outer_onion, - outer_starting_htlc_offset, + cur_block_height, keysend_preimage, invoice_request, trampoline_packet_option, diff --git a/lightning/src/routing/router.rs b/lightning/src/routing/router.rs index 39ed9009194..0af57b7a41d 100644 --- a/lightning/src/routing/router.rs +++ b/lightning/src/routing/router.rs @@ -512,6 +512,7 @@ pub struct RouteHop { /// to reach this node. pub channel_features: ChannelFeatures, /// The fee taken on this hop (for paying for the use of the *next* channel in the path). + /// /// If this is the last hop in [`Path::hops`]: /// * if we're sending to a [`BlindedPaymentPath`], this is the fee paid for use of the entire /// blinded path (including any Trampoline hops) @@ -557,8 +558,9 @@ pub struct TrampolineHop { /// the entire blinded path. pub fee_msat: u64, /// The CLTV delta added for this hop. + /// /// If this is the last Trampoline hop within [`BlindedTail`], this is the CLTV delta for the entire - /// blinded path. + /// blinded path (including the [`BlindedTail::excess_final_cltv_expiry_delta`]). pub cltv_expiry_delta: u32, } From 54cb8dafa2cb66c456e5b19839f142b040bf22e9 Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Mon, 9 Feb 2026 02:00:11 +0000 Subject: [PATCH 15/68] Clarify CLTV value selection in the first blinded hop marginally --- lightning/src/ln/onion_utils.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lightning/src/ln/onion_utils.rs b/lightning/src/ln/onion_utils.rs index 5b2e11024e1..e069e077726 100644 --- a/lightning/src/ln/onion_utils.rs +++ b/lightning/src/ln/onion_utils.rs @@ -559,7 +559,7 @@ where OP::new_blinded_receive( final_value_msat, recipient_onion.total_mpp_amount_msat, - cur_cltv + excess_final_cltv_expiry_delta, + starting_htlc_offset + excess_final_cltv_expiry_delta, &blinded_hop.encrypted_payload, blinding_point.take(), *keysend_preimage, From 921a331b9678a8d5a856375976d99d32c49c8659 Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Mon, 9 Feb 2026 02:00:37 +0000 Subject: [PATCH 16/68] Add a `Path::total_cltv_expiry_delta` accessor --- lightning/src/ln/onion_utils.rs | 1 + lightning/src/routing/router.rs | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/lightning/src/ln/onion_utils.rs b/lightning/src/ln/onion_utils.rs index e069e077726..41c01f8070d 100644 --- a/lightning/src/ln/onion_utils.rs +++ b/lightning/src/ln/onion_utils.rs @@ -2686,6 +2686,7 @@ pub(crate) fn create_payment_onion_internal( invoice_request, trampoline_packet_option, )?; + debug_assert_eq!(htlc_cltv - cur_block_height, path.total_cltv_expiry_delta()); let onion_keys = construct_onion_keys(&secp_ctx, &path, session_priv); let onion_packet = construct_onion_packet(onion_payloads, onion_keys, prng_seed, payment_hash) diff --git a/lightning/src/routing/router.rs b/lightning/src/routing/router.rs index 0af57b7a41d..c5c1f1765ee 100644 --- a/lightning/src/routing/router.rs +++ b/lightning/src/routing/router.rs @@ -644,6 +644,12 @@ impl Path { } } + /// Gets the total CLTV expiry delta which will be added to the current block height (plus some + /// extra headroom) when sending the HTLC + pub fn total_cltv_expiry_delta(&self) -> u32 { + self.hops.iter().map(|hop| hop.cltv_expiry_delta).sum() + } + /// True if this [`Path`] has at least one Trampoline hop. pub fn has_trampoline_hops(&self) -> bool { self.blinded_tail.as_ref().is_some_and(|bt| !bt.trampoline_hops.is_empty()) From 836f16213fad4f064e5078efe385f1cb2cd93831 Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Mon, 9 Feb 2026 02:00:58 +0000 Subject: [PATCH 17/68] Validate CLTV somewhat in `Route::debug_assert_route_meets_params` Now that we've cleaned up trampoline CLTV building and added `Path::total_cltv_expiry_delta`, we can use both to do some basic validation of CLTV values on blinded tails in `Route::debug_assert_route_meets_params` --- lightning/src/routing/router.rs | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/lightning/src/routing/router.rs b/lightning/src/routing/router.rs index c5c1f1765ee..36da891ea7b 100644 --- a/lightning/src/routing/router.rs +++ b/lightning/src/routing/router.rs @@ -739,6 +739,17 @@ impl Route { return Err(()); } + let total_cltv_delta = path.total_cltv_expiry_delta(); + if total_cltv_delta > route_params.payment_params.max_total_cltv_expiry_delta { + let err = format!( + "Path had a total CLTV of {total_cltv_delta} which is greater than the maximum we're allowed {}", + route_params.payment_params.max_total_cltv_expiry_delta, + ); + debug_assert!(false, "{}", err); + log_error!(logger, "{}", err); + return Err(()); + } + if path.hops.len() > route_params.payment_params.max_path_length.into() { let err = format!( "Path had a length of {}, which is greater than the maximum we're allowed ({})", @@ -751,6 +762,21 @@ impl Route { // This is a bug, but there's not a material safety risk to making this // payment, so we don't bother to error here. } + + if let Some(tail) = &path.blinded_tail { + let trampoline_cltv_sum = + tail.trampoline_hops.iter().map(|hop| hop.cltv_expiry_delta).sum(); + let min_cltv = + tail.excess_final_cltv_expiry_delta.saturating_add(trampoline_cltv_sum); + let last_hop_cltv_delta = path.hops.last().unwrap().cltv_expiry_delta; + if min_cltv > last_hop_cltv_delta { + let err = format!( + "Path had a total trampoline and excess blinded path CLTV of {min_cltv}, which is less than the total last-hop CLTV delta of {last_hop_cltv_delta}" + ); + debug_assert!(false, "{}", err); + log_error!(logger, "{}", err); + } + } } } From 5aa50b341273f86c031b43df8ba24f8265bac99c Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Tue, 10 Feb 2026 12:37:10 +0000 Subject: [PATCH 18/68] f rename var --- lightning/src/ln/onion_utils.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/lightning/src/ln/onion_utils.rs b/lightning/src/ln/onion_utils.rs index 41c01f8070d..519974c4bfc 100644 --- a/lightning/src/ln/onion_utils.rs +++ b/lightning/src/ln/onion_utils.rs @@ -539,8 +539,8 @@ where // exactly as it should be (and the next hop isn't trying to probe to find out if we're // the intended recipient). let value_msat = if cur_value_msat == 0 { hop.fee_msat() } else { cur_value_msat }; - let cltv = hop.cltv_expiry_delta().saturating_add(cur_cltv); if idx == 0 { + let declared_incoming_cltv = hop.cltv_expiry_delta().saturating_add(cur_cltv); match blinded_tail.take() { Some(BlindedTailDetails::DirectEntry { blinding_point, @@ -587,7 +587,7 @@ where PayloadCallbackAction::PushBack, OP::new_trampoline_entry( final_value_msat + hop.fee_msat(), - cltv, + declared_incoming_cltv, &recipient_onion, trampoline_packet, )?, @@ -596,7 +596,12 @@ where None => { callback( PayloadCallbackAction::PushBack, - OP::new_receive(&recipient_onion, *keysend_preimage, value_msat, cltv)?, + OP::new_receive( + &recipient_onion, + *keysend_preimage, + value_msat, + declared_incoming_cltv, + )?, ); }, } From 718b7567fdb1fa7e59a4e154a7587c6bc2236a79 Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Tue, 10 Feb 2026 13:07:13 +0000 Subject: [PATCH 19/68] f correct trampoline tail to included blinded path excess cltv --- lightning/src/routing/router.rs | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/lightning/src/routing/router.rs b/lightning/src/routing/router.rs index 36da891ea7b..a14f6218535 100644 --- a/lightning/src/routing/router.rs +++ b/lightning/src/routing/router.rs @@ -764,14 +764,29 @@ impl Route { } if let Some(tail) = &path.blinded_tail { - let trampoline_cltv_sum = + let trampoline_cltv_sum: u32 = tail.trampoline_hops.iter().map(|hop| hop.cltv_expiry_delta).sum(); - let min_cltv = - tail.excess_final_cltv_expiry_delta.saturating_add(trampoline_cltv_sum); let last_hop_cltv_delta = path.hops.last().unwrap().cltv_expiry_delta; - if min_cltv > last_hop_cltv_delta { + if trampoline_cltv_sum > last_hop_cltv_delta { let err = format!( - "Path had a total trampoline and excess blinded path CLTV of {min_cltv}, which is less than the total last-hop CLTV delta of {last_hop_cltv_delta}" + "Path had a total trampoline CLTV of {trampoline_cltv_sum}, which is less than the total last-hop CLTV delta of {last_hop_cltv_delta}" + ); + debug_assert!(false, "{}", err); + log_error!(logger, "{}", err); + } + let last_trampoline_cltv = tail.trampoline_hops.last().map(|h| h.cltv_expiry_delta).unwrap_or(u32::MAX); + if tail.excess_final_cltv_expiry_delta > last_trampoline_cltv { + let err = format!( + "Last trampoline CLTV of {last_trampoline_cltv} is less than the excess blinded path cltv of {}", + tail.excess_final_cltv_expiry_delta + ); + debug_assert!(false, "{}", err); + log_error!(logger, "{}", err); + } + if tail.excess_final_cltv_expiry_delta > last_hop_cltv_delta { + let err = format!( + "Last path hop CLTV of {last_hop_cltv_delta} is less than the excess blinded path cltv of {}", + tail.excess_final_cltv_expiry_delta ); debug_assert!(false, "{}", err); log_error!(logger, "{}", err); From a4798992ed8a5d28c5b33a1027c8a7f050453db7 Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Tue, 10 Feb 2026 13:07:18 +0000 Subject: [PATCH 20/68] f fix broken test --- lightning/src/ln/htlc_reserve_unit_tests.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lightning/src/ln/htlc_reserve_unit_tests.rs b/lightning/src/ln/htlc_reserve_unit_tests.rs index 2a1ceadaf5a..cff438aa8b5 100644 --- a/lightning/src/ln/htlc_reserve_unit_tests.rs +++ b/lightning/src/ln/htlc_reserve_unit_tests.rs @@ -1416,9 +1416,10 @@ pub fn test_update_add_htlc_bolt2_sender_cltv_expiry_too_high() { let _chan = create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1000000, 0); - let payment_params = PaymentParameters::from_node_id(node_b_id, 0) + let mut payment_params = PaymentParameters::from_node_id(node_b_id, 0) .with_bolt11_features(nodes[1].node.bolt11_invoice_features()) .unwrap(); + payment_params.max_total_cltv_expiry_delta = 500000001; let (mut route, our_payment_hash, _, our_payment_secret) = get_route_and_payment_hash!(nodes[0], nodes[1], payment_params, 100000000); route.paths[0].hops.last_mut().unwrap().cltv_expiry_delta = 500000001; From 503dd8547b3a44ab317b31a184cfa12e967aaf3f Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Tue, 10 Feb 2026 12:57:40 +0000 Subject: [PATCH 21/68] Rename `starting_htlc_offset` `cur_block_height` in onion building Now that we are consistently using the `RouteHop::cltv_expiry_delta` as the last hop's starting CLTV rather than summing trampoline hops, `starting_htlc_offset` is a bit confusing - its actually always the current block height. Thus, here we rename it. --- lightning/src/ln/onion_utils.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/lightning/src/ln/onion_utils.rs b/lightning/src/ln/onion_utils.rs index 519974c4bfc..92a720841d7 100644 --- a/lightning/src/ln/onion_utils.rs +++ b/lightning/src/ln/onion_utils.rs @@ -415,7 +415,7 @@ pub(super) fn construct_trampoline_onion_keys( pub(super) fn build_trampoline_onion_payloads<'a>( blinded_tail: &'a BlindedTail, recipient_onion: &'a RecipientOnionFields, - starting_htlc_offset: u32, keysend_preimage: &Option, + cur_block_height: u32, keysend_preimage: &Option, ) -> Result<(Vec>, u64), APIError> { let mut res: Vec = Vec::with_capacity(blinded_tail.trampoline_hops.len() + blinded_tail.hops.len()); @@ -430,7 +430,7 @@ pub(super) fn build_trampoline_onion_payloads<'a>( blinded_tail.trampoline_hops.iter(), Some(blinded_tail_with_hop_iter), recipient_onion, - starting_htlc_offset, + cur_block_height, keysend_preimage, None, |action, payload| match action { @@ -444,14 +444,14 @@ pub(super) fn build_trampoline_onion_payloads<'a>( /// returns the hop data, as well as the first-hop value_msat and CLTV value we should send. #[cfg(any(test, feature = "_externalize_tests"))] pub(crate) fn test_build_onion_payloads<'a>( - path: &'a Path, recipient_onion: &'a RecipientOnionFields, starting_htlc_offset: u32, + path: &'a Path, recipient_onion: &'a RecipientOnionFields, cur_block_height: u32, keysend_preimage: &Option, invoice_request: Option<&'a InvoiceRequest>, trampoline_packet: Option, ) -> Result<(Vec>, u64, u32), APIError> { build_onion_payloads( path, recipient_onion, - starting_htlc_offset, + cur_block_height, keysend_preimage, invoice_request, trampoline_packet, @@ -460,7 +460,7 @@ pub(crate) fn test_build_onion_payloads<'a>( /// returns the hop data, as well as the first-hop value_msat and CLTV value we should send. fn build_onion_payloads<'a>( - path: &'a Path, recipient_onion: &'a RecipientOnionFields, starting_htlc_offset: u32, + path: &'a Path, recipient_onion: &'a RecipientOnionFields, cur_block_height: u32, keysend_preimage: &Option, invoice_request: Option<&'a InvoiceRequest>, trampoline_packet: Option, ) -> Result<(Vec>, u64, u32), APIError> { @@ -490,7 +490,7 @@ fn build_onion_payloads<'a>( path.hops.iter(), blinded_tail_with_hop_iter, recipient_onion, - starting_htlc_offset, + cur_block_height, keysend_preimage, invoice_request, |action, payload| match action { @@ -520,7 +520,7 @@ enum PayloadCallbackAction { } fn build_onion_payloads_callback<'a, 'b, H, B, F, OP>( hops: H, mut blinded_tail: Option>, - recipient_onion: &'a RecipientOnionFields, starting_htlc_offset: u32, + recipient_onion: &'a RecipientOnionFields, cur_block_height: u32, keysend_preimage: &Option, invoice_request: Option<&'a InvoiceRequest>, mut callback: F, ) -> Result<(u64, u32), APIError> @@ -531,7 +531,7 @@ where OP: OnionPayload<'a, 'b, ReceiveType = OP>, { let mut cur_value_msat = 0u64; - let mut cur_cltv = starting_htlc_offset; + let mut cur_cltv = cur_block_height; let mut last_hop_id = None; for (idx, hop) in hops.rev().enumerate() { @@ -559,7 +559,7 @@ where OP::new_blinded_receive( final_value_msat, recipient_onion.total_mpp_amount_msat, - starting_htlc_offset + excess_final_cltv_expiry_delta, + cur_block_height + excess_final_cltv_expiry_delta, &blinded_hop.encrypted_payload, blinding_point.take(), *keysend_preimage, From 2e6169df194b8b0adfd1ffe300ecf56ebc775f37 Mon Sep 17 00:00:00 2001 From: Carla Kirk-Cohen Date: Thu, 12 Feb 2026 12:59:20 +0200 Subject: [PATCH 22/68] [deleteme]: remove failing assertion in test (from upstream) --- lightning/src/routing/router.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lightning/src/routing/router.rs b/lightning/src/routing/router.rs index a14f6218535..d2a89b12e8d 100644 --- a/lightning/src/routing/router.rs +++ b/lightning/src/routing/router.rs @@ -780,7 +780,7 @@ impl Route { "Last trampoline CLTV of {last_trampoline_cltv} is less than the excess blinded path cltv of {}", tail.excess_final_cltv_expiry_delta ); - debug_assert!(false, "{}", err); + //debug_assert!(false, "{}", err); log_error!(logger, "{}", err); } if tail.excess_final_cltv_expiry_delta > last_hop_cltv_delta { From 4597bf2e17d0cf402b542b6fadeebf2e336920c5 Mon Sep 17 00:00:00 2001 From: Carla Kirk-Cohen Date: Tue, 16 Dec 2025 13:37:23 +0200 Subject: [PATCH 23/68] ln/events: multiple htlcs in/out for trampoline PaymentForwarded --- .../tests/lsps2_integration_tests.rs | 8 +- lightning/src/events/mod.rs | 141 ++++++++++-------- lightning/src/ln/blinded_payment_tests.rs | 13 +- lightning/src/ln/channelmanager.rs | 16 +- lightning/src/ln/functional_test_utils.rs | 42 +++--- lightning/src/ln/functional_tests.rs | 26 ++-- lightning/src/ln/onion_route_tests.rs | 15 +- lightning/src/ln/onion_utils.rs | 13 +- lightning/src/routing/router.rs | 6 +- lightning/src/util/ser.rs | 1 + 10 files changed, 157 insertions(+), 124 deletions(-) diff --git a/lightning-liquidity/tests/lsps2_integration_tests.rs b/lightning-liquidity/tests/lsps2_integration_tests.rs index 312199e19ec..7d4bcfb7830 100644 --- a/lightning-liquidity/tests/lsps2_integration_tests.rs +++ b/lightning-liquidity/tests/lsps2_integration_tests.rs @@ -1332,14 +1332,14 @@ fn client_trusts_lsp_end_to_end_test() { let total_fee_msat = match service_events[0].clone() { Event::PaymentForwarded { - prev_node_id, - next_node_id, + ref prev_htlcs, + ref next_htlcs, skimmed_fee_msat, total_fee_earned_msat, .. } => { - assert_eq!(prev_node_id, Some(payer_node_id)); - assert_eq!(next_node_id, Some(client_node_id)); + assert_eq!(prev_htlcs[0].node_id, Some(payer_node_id)); + assert_eq!(next_htlcs[0].node_id, Some(client_node_id)); service_handler.payment_forwarded(channel_id, skimmed_fee_msat.unwrap_or(0)).unwrap(); Some(total_fee_earned_msat.unwrap() - skimmed_fee_msat.unwrap()) }, diff --git a/lightning/src/events/mod.rs b/lightning/src/events/mod.rs index 74f2f34d4d8..580487caaed 100644 --- a/lightning/src/events/mod.rs +++ b/lightning/src/events/mod.rs @@ -45,7 +45,7 @@ use crate::util::ser::{ UpgradableRequired, WithoutLength, Writeable, Writer, }; -use crate::io; +use crate::io::{self, ErrorKind::InvalidData as IOInvalidData}; use crate::sync::Arc; use bitcoin::hashes::sha256::Hash as Sha256; use bitcoin::hashes::Hash; @@ -730,6 +730,25 @@ pub enum InboundChannelFunds { DualFunded, } +/// Identifies the channel and peer committed to a HTLC, used for both incoming and outgoing HTLCs. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct HTLCLocator { + /// The channel that the HTLC was sent or received on. + pub channel_id: ChannelId, + + /// The `user_channel_id` for `channel_id`. + pub user_channel_id: Option, + + /// The public key identity of the node that the HTLC was sent to or received from. + pub node_id: Option, +} + +impl_writeable_tlv_based!(HTLCLocator, { + (1, channel_id, required), + (3, user_channel_id, option), + (5, node_id, option), +}); + /// An Event which you should probably take some action in response to. /// /// Note that while Writeable and Readable are implemented for Event, you probably shouldn't use @@ -1322,38 +1341,22 @@ pub enum Event { /// This event is generated when a payment has been successfully forwarded through us and a /// forwarding fee earned. /// + /// Note that downgrading from 0.3 with pending trampoline forwards that use multipart payments + /// will produce an event that only provides information about the first htlc that was + /// received/dispatched. + /// /// # Failure Behavior and Persistence /// This event will eventually be replayed after failures-to-handle (i.e., the event handler /// returning `Err(ReplayEvent ())`) and will be persisted across restarts. PaymentForwarded { - /// The channel id of the incoming channel between the previous node and us. - /// - /// This is only `None` for events generated or serialized by versions prior to 0.0.107. - prev_channel_id: Option, - /// The channel id of the outgoing channel between the next node and us. - /// - /// This is only `None` for events generated or serialized by versions prior to 0.0.107. - next_channel_id: Option, - /// The `user_channel_id` of the incoming channel between the previous node and us. - /// - /// This is only `None` for events generated or serialized by versions prior to 0.0.122. - prev_user_channel_id: Option, - /// The `user_channel_id` of the outgoing channel between the next node and us. - /// - /// This will be `None` if the payment was settled via an on-chain transaction. See the - /// caveat described for the `total_fee_earned_msat` field. Moreover it will be `None` for - /// events generated or serialized by versions prior to 0.0.122. - next_user_channel_id: Option, - /// The node id of the previous node. - /// - /// This is only `None` for HTLCs received prior to 0.1 or for events serialized by - /// versions prior to 0.1 - prev_node_id: Option, - /// The node id of the next node. - /// - /// This is only `None` for HTLCs received prior to 0.1 or for events serialized by - /// versions prior to 0.1 - next_node_id: Option, + /// The set of HTLCs forwarded to our node that will be claimed by this forward. Contains a + /// single HTLC for source-routed payments, and may contain multiple HTLCs when we acted as + /// a trampoline router, responsible for pathfinding within the route. + prev_htlcs: Vec, + /// The set of HTLCs forwarded by our node that have been claimed by this forward. Contains + /// a single HTLC for regular source-routed payments, and may contain multiple HTLCs when + /// we acted as a trampoline router, responsible for pathfinding within the route. + next_htlcs: Vec, /// The total fee, in milli-satoshis, which was earned as a result of the payment. /// /// Note that if we force-closed the channel over which we forwarded an HTLC while the HTLC @@ -2039,29 +2042,33 @@ impl Writeable for Event { }); }, &Event::PaymentForwarded { - prev_channel_id, - next_channel_id, - prev_user_channel_id, - next_user_channel_id, - prev_node_id, - next_node_id, + ref prev_htlcs, + ref next_htlcs, total_fee_earned_msat, skimmed_fee_msat, claim_from_onchain_tx, outbound_amount_forwarded_msat, } => { 7u8.write(writer)?; + // Fields 1, 3, 9, 11, 13 and 15 are written for backwards compatibility. + let legacy_prev = prev_htlcs.first().ok_or(io::Error::from(IOInvalidData))?; + let legacy_next = next_htlcs.first().ok_or(io::Error::from(IOInvalidData))?; write_tlv_fields!(writer, { (0, total_fee_earned_msat, option), - (1, prev_channel_id, option), + (1, Some(legacy_prev.channel_id), option), (2, claim_from_onchain_tx, required), - (3, next_channel_id, option), + (3, Some(legacy_next.channel_id), option), (5, outbound_amount_forwarded_msat, option), (7, skimmed_fee_msat, option), - (9, prev_user_channel_id, option), - (11, next_user_channel_id, option), - (13, prev_node_id, option), - (15, next_node_id, option), + (9, legacy_prev.user_channel_id, option), + (11, legacy_next.user_channel_id, option), + (13, legacy_prev.node_id, option), + (15, legacy_next.node_id, option), + // HTLCs are written as required, rather than required_vec, so that they can be + // deserialized using default_value to fill in legacy fields which expects + // LengthReadable (required_vec is WithoutLength). + (17, *prev_htlcs, required), + (19, *next_htlcs, required), }); }, &Event::ChannelClosed { @@ -2559,35 +2566,51 @@ impl MaybeReadable for Event { }, 7u8 => { let mut f = || { - let mut prev_channel_id = None; - let mut next_channel_id = None; - let mut prev_user_channel_id = None; - let mut next_user_channel_id = None; - let mut prev_node_id = None; - let mut next_node_id = None; + // Legacy values that have been replaced by prev_htlcs and next_htlcs. + let mut prev_channel_id_legacy = None; + let mut next_channel_id_legacy = None; + let mut prev_user_channel_id_legacy = None; + let mut next_user_channel_id_legacy = None; + let mut prev_node_id_legacy = None; + let mut next_node_id_legacy = None; + let mut total_fee_earned_msat = None; let mut skimmed_fee_msat = None; let mut claim_from_onchain_tx = false; let mut outbound_amount_forwarded_msat = None; + let mut prev_htlcs = vec![]; + let mut next_htlcs = vec![]; read_tlv_fields!(reader, { (0, total_fee_earned_msat, option), - (1, prev_channel_id, option), + (1, prev_channel_id_legacy, option), (2, claim_from_onchain_tx, required), - (3, next_channel_id, option), + (3, next_channel_id_legacy, option), (5, outbound_amount_forwarded_msat, option), (7, skimmed_fee_msat, option), - (9, prev_user_channel_id, option), - (11, next_user_channel_id, option), - (13, prev_node_id, option), - (15, next_node_id, option), + (9, prev_user_channel_id_legacy, option), + (11, next_user_channel_id_legacy, option), + (13, prev_node_id_legacy, option), + (15, next_node_id_legacy, option), + // We can unwrap in the eagerly-evaluated default_value code because we + // always write legacy fields to be backwards compatible, and expect + // this field to be set because the legacy field was only None for versions + // before 0.0.107 and we do not allow upgrades with pending forwards to 0.1 + // for any version before 0.0.123. + (17, prev_htlcs, (default_value, vec![HTLCLocator{ + channel_id: prev_channel_id_legacy.unwrap(), + user_channel_id: prev_user_channel_id_legacy, + node_id: prev_node_id_legacy, + }])), + (19, next_htlcs, (default_value, vec![HTLCLocator{ + channel_id: next_channel_id_legacy.unwrap(), + user_channel_id: next_user_channel_id_legacy, + node_id: next_node_id_legacy, + }])), }); + Ok(Some(Event::PaymentForwarded { - prev_channel_id, - next_channel_id, - prev_user_channel_id, - next_user_channel_id, - prev_node_id, - next_node_id, + prev_htlcs, + next_htlcs, total_fee_earned_msat, skimmed_fee_msat, claim_from_onchain_tx, diff --git a/lightning/src/ln/blinded_payment_tests.rs b/lightning/src/ln/blinded_payment_tests.rs index b4d3d36b383..95f24d59d16 100644 --- a/lightning/src/ln/blinded_payment_tests.rs +++ b/lightning/src/ln/blinded_payment_tests.rs @@ -2498,8 +2498,9 @@ fn replacement_onion( total_msat: original_amt_msat, }), sender_intended_htlc_amt_msat: original_amt_msat, - cltv_expiry_height: - original_trampoline_cltv + starting_htlc_offset + excess_final_cltv, + cltv_expiry_height: original_trampoline_cltv + + starting_htlc_offset + + excess_final_cltv, }]; } @@ -2579,8 +2580,7 @@ fn do_test_trampoline_relay(blinded: bool, test_case: TrampolineTestCase) { let alice_bob_chan = create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0); let bob_carol_chan = create_announced_chan_between_nodes_with_value(&nodes, 1, 2, 1_000_000, 0); - let starting_htlc_offset = - (TOTAL_NODE_COUNT as u32) * CHAN_CONFIRM_DEPTH + 1; + let starting_htlc_offset = (TOTAL_NODE_COUNT as u32) * CHAN_CONFIRM_DEPTH + 1; for i in 0..TOTAL_NODE_COUNT { connect_blocks(&nodes[i], starting_htlc_offset - nodes[i].best_block_info().1); } @@ -2697,8 +2697,9 @@ fn do_test_trampoline_relay(blinded: bool, test_case: TrampolineTestCase) { ); let amt_bytes = test_case.outer_onion_amt(original_amt_msat).to_be_bytes(); - let cltv_bytes = - test_case.outer_onion_cltv(original_trampoline_cltv + starting_htlc_offset + excess_final_cltv).to_be_bytes(); + let cltv_bytes = test_case + .outer_onion_cltv(original_trampoline_cltv + starting_htlc_offset + excess_final_cltv) + .to_be_bytes(); let payment_failure = test_case.payment_failed_conditions(&amt_bytes, &cltv_bytes).map(|p| { if blinded { PaymentFailedConditions::new() diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 94776f15655..6e4a977e4e1 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -9585,12 +9585,16 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ ( Some(MonitorUpdateCompletionAction::EmitEventAndFreeOtherChannel { event: events::Event::PaymentForwarded { - prev_channel_id: Some(prev_channel_id), - next_channel_id: Some(next_channel_id), - prev_user_channel_id, - next_user_channel_id, - prev_node_id, - next_node_id: Some(next_channel_counterparty_node_id), + prev_htlcs: vec![events::HTLCLocator { + channel_id: prev_channel_id, + user_channel_id: prev_user_channel_id, + node_id: prev_node_id, + }], + next_htlcs: vec![events::HTLCLocator { + channel_id: next_channel_id, + user_channel_id: next_user_channel_id, + node_id: Some(next_channel_counterparty_node_id), + }], total_fee_earned_msat, skimmed_fee_msat, claim_from_onchain_tx: from_onchain, diff --git a/lightning/src/ln/functional_test_utils.rs b/lightning/src/ln/functional_test_utils.rs index d9f99ea1aa6..4550f0b1147 100644 --- a/lightning/src/ln/functional_test_utils.rs +++ b/lightning/src/ln/functional_test_utils.rs @@ -3060,17 +3060,16 @@ pub fn expect_payment_forwarded>( ) -> Option { match event { Event::PaymentForwarded { - prev_channel_id, - next_channel_id, - prev_user_channel_id, - next_user_channel_id, - prev_node_id, - next_node_id, + prev_htlcs, + next_htlcs, total_fee_earned_msat, skimmed_fee_msat, claim_from_onchain_tx, .. } => { + assert_eq!(prev_htlcs.len(), 1); + assert_eq!(next_htlcs.len(), 1); + if allow_1_msat_fee_overpay { // Aggregating fees for blinded paths may result in a rounding error, causing slight // overpayment in fees. @@ -3085,33 +3084,36 @@ pub fn expect_payment_forwarded>( // overpaid amount. assert!(skimmed_fee_msat == expected_extra_fees_msat); if !upstream_force_closed { - assert_eq!(prev_node.node().get_our_node_id(), prev_node_id.unwrap()); + let prev_node_id = prev_htlcs[0].node_id.unwrap(); + let prev_channel_id = prev_htlcs[0].channel_id; + let prev_user_channel_id = prev_htlcs[0].user_channel_id.unwrap(); + + assert_eq!(prev_node.node().get_our_node_id(), prev_node_id); // Is the event prev_channel_id in one of the channels between the two nodes? let node_chans = node.node().list_channels(); - assert!(node_chans.iter().any(|x| x.counterparty.node_id == prev_node_id.unwrap() - && x.channel_id == prev_channel_id.unwrap() - && x.user_channel_id == prev_user_channel_id.unwrap())); + assert!(node_chans.iter().any(|x| x.counterparty.node_id == prev_node_id + && x.channel_id == prev_channel_id + && x.user_channel_id == prev_user_channel_id)); } // We check for force closures since a force closed channel is removed from the // node's channel list if !downstream_force_closed { + let next_node_id = next_htlcs[0].node_id.unwrap(); + let next_channel_id = next_htlcs[0].channel_id; + let next_user_channel_id = next_htlcs[0].user_channel_id.unwrap(); // As documented, `next_user_channel_id` will only be `Some` if we didn't settle via an // onchain transaction, just as the `total_fee_earned_msat` field. Rather than // introducing yet another variable, we use the latter's state as a flag to detect // this and only check if it's `Some`. - assert_eq!(next_node.node().get_our_node_id(), next_node_id.unwrap()); + assert_eq!(next_node.node().get_our_node_id(), next_node_id); let node_chans = node.node().list_channels(); if total_fee_earned_msat.is_none() { - assert!(node_chans - .iter() - .any(|x| x.counterparty.node_id == next_node_id.unwrap() - && x.channel_id == next_channel_id.unwrap())); + assert!(node_chans.iter().any(|x| x.counterparty.node_id == next_node_id + && x.channel_id == next_channel_id)); } else { - assert!(node_chans - .iter() - .any(|x| x.counterparty.node_id == next_node_id.unwrap() - && x.channel_id == next_channel_id.unwrap() - && x.user_channel_id == next_user_channel_id.unwrap())); + assert!(node_chans.iter().any(|x| x.counterparty.node_id == next_node_id + && x.channel_id == next_channel_id + && x.user_channel_id == next_user_channel_id)); } } assert_eq!(claim_from_onchain_tx, downstream_force_closed); diff --git a/lightning/src/ln/functional_tests.rs b/lightning/src/ln/functional_tests.rs index ed2665665c7..5b9074d5347 100644 --- a/lightning/src/ln/functional_tests.rs +++ b/lightning/src/ln/functional_tests.rs @@ -1460,37 +1460,37 @@ pub fn test_htlc_on_chain_success() { connect_blocks(&nodes[1], TEST_FINAL_CLTV); // Confirm blocks until the HTLC expires let forwarded_events = nodes[1].node.get_and_clear_pending_events(); assert_eq!(forwarded_events.len(), 3); - let chan_id = Some(chan_1.2); + let chan_id = chan_1.2; match forwarded_events[0] { Event::PaymentForwarded { + ref prev_htlcs, + ref next_htlcs, total_fee_earned_msat, - prev_channel_id, claim_from_onchain_tx, - next_channel_id, outbound_amount_forwarded_msat, .. } => { assert_eq!(total_fee_earned_msat, Some(1000)); - assert_eq!(prev_channel_id, chan_id); + assert_eq!(prev_htlcs[0].channel_id, chan_id); assert_eq!(claim_from_onchain_tx, true); - assert_eq!(next_channel_id, Some(chan_2.2)); + assert_eq!(next_htlcs[0].channel_id, chan_2.2); assert_eq!(outbound_amount_forwarded_msat, Some(3000000)); }, _ => panic!(), } match forwarded_events[1] { Event::PaymentForwarded { + ref prev_htlcs, + ref next_htlcs, total_fee_earned_msat, - prev_channel_id, claim_from_onchain_tx, - next_channel_id, outbound_amount_forwarded_msat, .. } => { assert_eq!(total_fee_earned_msat, Some(1000)); - assert_eq!(prev_channel_id, chan_id); + assert_eq!(prev_htlcs[0].channel_id, chan_id); assert_eq!(claim_from_onchain_tx, true); - assert_eq!(next_channel_id, Some(chan_2.2)); + assert_eq!(next_htlcs[0].channel_id, chan_2.2); assert_eq!(outbound_amount_forwarded_msat, Some(3000000)); }, _ => panic!(), @@ -3965,17 +3965,17 @@ pub fn test_onchain_to_onchain_claim() { assert_eq!(events.len(), 2); match events[0] { Event::PaymentForwarded { + ref prev_htlcs, + ref next_htlcs, total_fee_earned_msat, - prev_channel_id, claim_from_onchain_tx, - next_channel_id, outbound_amount_forwarded_msat, .. } => { assert_eq!(total_fee_earned_msat, Some(1000)); - assert_eq!(prev_channel_id, Some(chan_1.2)); + assert_eq!(prev_htlcs[0].channel_id, chan_1.2); assert_eq!(claim_from_onchain_tx, true); - assert_eq!(next_channel_id, Some(chan_2.2)); + assert_eq!(next_htlcs[0].channel_id, chan_2.2); assert_eq!(outbound_amount_forwarded_msat, Some(3000000)); }, _ => panic!("Unexpected event"), diff --git a/lightning/src/ln/onion_route_tests.rs b/lightning/src/ln/onion_route_tests.rs index 21d7ed0c566..f5743134a49 100644 --- a/lightning/src/ln/onion_route_tests.rs +++ b/lightning/src/ln/onion_route_tests.rs @@ -1974,14 +1974,13 @@ fn test_trampoline_onion_payload_assembly_values() { SecretKey::from_slice(&>::from_hex(SECRET_HEX).unwrap()).unwrap().secret_bytes(), ); let recipient_onion_fields = RecipientOnionFields::secret_only(payment_secret, amt_msat); - let (trampoline_payloads, outer_total_msat) = - onion_utils::build_trampoline_onion_payloads( - &path.blinded_tail.as_ref().unwrap(), - &recipient_onion_fields, - cur_height, - &None, - ) - .unwrap(); + let (trampoline_payloads, outer_total_msat) = onion_utils::build_trampoline_onion_payloads( + &path.blinded_tail.as_ref().unwrap(), + &recipient_onion_fields, + cur_height, + &None, + ) + .unwrap(); assert_eq!(trampoline_payloads.len(), 3); assert_eq!(outer_total_msat, 150_153_000); diff --git a/lightning/src/ln/onion_utils.rs b/lightning/src/ln/onion_utils.rs index 92a720841d7..cc4cb0b2822 100644 --- a/lightning/src/ln/onion_utils.rs +++ b/lightning/src/ln/onion_utils.rs @@ -2652,13 +2652,12 @@ pub(crate) fn create_payment_onion_internal( if !blinded_tail.trampoline_hops.is_empty() { let trampoline_payloads; let outer_total_msat; - (trampoline_payloads, outer_total_msat) = - build_trampoline_onion_payloads( - &blinded_tail, - recipient_onion, - cur_block_height, - keysend_preimage, - )?; + (trampoline_payloads, outer_total_msat) = build_trampoline_onion_payloads( + &blinded_tail, + recipient_onion, + cur_block_height, + keysend_preimage, + )?; trampoline_outer_onion.total_mpp_amount_msat = outer_total_msat; let trampoline_session_priv = trampoline_session_priv_override diff --git a/lightning/src/routing/router.rs b/lightning/src/routing/router.rs index d2a89b12e8d..9376bff758d 100644 --- a/lightning/src/routing/router.rs +++ b/lightning/src/routing/router.rs @@ -774,7 +774,11 @@ impl Route { debug_assert!(false, "{}", err); log_error!(logger, "{}", err); } - let last_trampoline_cltv = tail.trampoline_hops.last().map(|h| h.cltv_expiry_delta).unwrap_or(u32::MAX); + let last_trampoline_cltv = tail + .trampoline_hops + .last() + .map(|h| h.cltv_expiry_delta) + .unwrap_or(u32::MAX); if tail.excess_final_cltv_expiry_delta > last_trampoline_cltv { let err = format!( "Last trampoline CLTV of {last_trampoline_cltv} is less than the excess blinded path cltv of {}", diff --git a/lightning/src/util/ser.rs b/lightning/src/util/ser.rs index f821aa5afc0..249505f2e21 100644 --- a/lightning/src/util/ser.rs +++ b/lightning/src/util/ser.rs @@ -1100,6 +1100,7 @@ impl_for_vec!(crate::routing::router::TrampolineHop); impl_for_vec_with_element_length_prefix!(crate::ln::msgs::UpdateAddHTLC); impl_writeable_for_vec_with_element_length_prefix!(&crate::ln::msgs::UpdateAddHTLC); impl_for_vec!(u32); +impl_for_vec!(crate::events::HTLCLocator); impl Writeable for Vec { #[inline] From d49b112ed7b504910cb128942adb32838bdb41ba Mon Sep 17 00:00:00 2001 From: Carla Kirk-Cohen Date: Tue, 16 Dec 2025 15:14:55 +0200 Subject: [PATCH 24/68] ln: make event optional in EmitEventAndFreeOtherChannel In the commits that follow, we want to be able to free the other channel without emitting an event so that we can emit a single event for trampoline payments with multiple incoming HTLCs. We still want to go through the full claim flow for each incoming HTLC (and persist the EmitEventAndFreeOtherChannel event to be picked up on restart), but do not want multiple events for the same trampoline forward. Changing from upgradable_required to upgradable_option is forwards compatible - old versions of the software will always have written this field, newer versions don't require it to be there but will be able to read it as-is. This change is not backwards compatible, because older versions of the software will expect the field to be present but newer versions may not write it. An alternative would be to add a new event type, but that would need to have an even TLV (because the event must be understood and processed on restart to claim the incoming HTLC), so that option isn't backwards compatible either. --- lightning/src/ln/channelmanager.rs | 15 ++++++++++----- pending_changelog/4304.txt | 3 +++ 2 files changed, 13 insertions(+), 5 deletions(-) create mode 100644 pending_changelog/4304.txt diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 6e4a977e4e1..9bd32c511e8 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -1374,7 +1374,7 @@ pub(crate) enum MonitorUpdateCompletionAction { /// edge completes, we will surface an [`Event::PaymentForwarded`] as well as unblock the /// outbound edge. EmitEventAndFreeOtherChannel { - event: events::Event, + event: Option, downstream_counterparty_and_funding_outpoint: Option, }, /// Indicates we should immediately resume the operation of another channel, unless there is @@ -1409,7 +1409,10 @@ impl_writeable_tlv_based_enum_upgradable!(MonitorUpdateCompletionAction, (5, downstream_channel_id, required), }, (2, EmitEventAndFreeOtherChannel) => { - (0, event, upgradable_required), + // LDK prior to 0.3 required this field. It will not be present for trampoline payments + // with multiple incoming HTLCS, so nodes cannot downgrade while trampoline payments + // are in the process of being resolved. + (0, event, upgradable_option), // LDK prior to 0.0.116 did not have this field as the monitor update application order was // required by clients. If we downgrade to something prior to 0.0.116 this may result in // monitor updates which aren't properly blocked or resumed, however that's fine - we don't @@ -9584,7 +9587,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ ); ( Some(MonitorUpdateCompletionAction::EmitEventAndFreeOtherChannel { - event: events::Event::PaymentForwarded { + event: Some(events::Event::PaymentForwarded { prev_htlcs: vec![events::HTLCLocator { channel_id: prev_channel_id, user_channel_id: prev_user_channel_id, @@ -9599,7 +9602,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ skimmed_fee_msat, claim_from_onchain_tx: from_onchain, outbound_amount_forwarded_msat: forwarded_htlc_value_msat, - }, + }), downstream_counterparty_and_funding_outpoint: chan_to_release, }), None, @@ -9824,7 +9827,9 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ event, downstream_counterparty_and_funding_outpoint, } => { - self.pending_events.lock().unwrap().push_back((event, None)); + if let Some(event) = event { + self.pending_events.lock().unwrap().push_back((event, None)); + } if let Some(unblocked) = downstream_counterparty_and_funding_outpoint { self.handle_monitor_update_release( unblocked.counterparty_node_id, diff --git a/pending_changelog/4304.txt b/pending_changelog/4304.txt new file mode 100644 index 00000000000..8c1580a2f4c --- /dev/null +++ b/pending_changelog/4304.txt @@ -0,0 +1,3 @@ +## Backwards Compatibility + +* Downgrade is not possible while the node has in-flight trampoline forwards. From 3ac0615eb9be32a62cdbfde181984936035d080e Mon Sep 17 00:00:00 2001 From: Carla Kirk-Cohen Date: Wed, 7 Jan 2026 15:36:30 -0500 Subject: [PATCH 25/68] ln/refactor: rename EmitEventAndFreeOtherChannel to note optional event --- lightning/src/ln/channelmanager.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 9bd32c511e8..7d336155ae5 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -1373,7 +1373,7 @@ pub(crate) enum MonitorUpdateCompletionAction { /// completes a monitor update containing the payment preimage. In that case, after the inbound /// edge completes, we will surface an [`Event::PaymentForwarded`] as well as unblock the /// outbound edge. - EmitEventAndFreeOtherChannel { + EmitEventOptionAndFreeOtherChannel { event: Option, downstream_counterparty_and_funding_outpoint: Option, }, @@ -1384,8 +1384,8 @@ pub(crate) enum MonitorUpdateCompletionAction { /// This is usually generated when we've forwarded an HTLC and want to block the outbound edge /// from completing a monitor update which removes the payment preimage until the inbound edge /// completes a monitor update containing the payment preimage. However, we use this variant - /// instead of [`Self::EmitEventAndFreeOtherChannel`] when we discover that the claim was in - /// fact duplicative and we simply want to resume the outbound edge channel immediately. + /// instead of [`Self::EmitEventOptionAndFreeOtherChannel`] when we discover that the claim was + /// in fact duplicative and we simply want to resume the outbound edge channel immediately. /// /// This variant should thus never be written to disk, as it is processed inline rather than /// stored for later processing. @@ -1408,7 +1408,7 @@ impl_writeable_tlv_based_enum_upgradable!(MonitorUpdateCompletionAction, (4, blocking_action, upgradable_required), (5, downstream_channel_id, required), }, - (2, EmitEventAndFreeOtherChannel) => { + (2, EmitEventOptionAndFreeOtherChannel) => { // LDK prior to 0.3 required this field. It will not be present for trampoline payments // with multiple incoming HTLCS, so nodes cannot downgrade while trampoline payments // are in the process of being resolved. @@ -9586,7 +9586,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ "skimmed_fee_msat must always be included in total_fee_earned_msat" ); ( - Some(MonitorUpdateCompletionAction::EmitEventAndFreeOtherChannel { + Some(MonitorUpdateCompletionAction::EmitEventOptionAndFreeOtherChannel { event: Some(events::Event::PaymentForwarded { prev_htlcs: vec![events::HTLCLocator { channel_id: prev_channel_id, @@ -9823,7 +9823,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ } } }, - MonitorUpdateCompletionAction::EmitEventAndFreeOtherChannel { + MonitorUpdateCompletionAction::EmitEventOptionAndFreeOtherChannel { event, downstream_counterparty_and_funding_outpoint, } => { @@ -19062,7 +19062,7 @@ impl< let logger = WithContext::from(&args.logger, Some(node_id), Some(*channel_id), None); for action in actions.iter() { - if let MonitorUpdateCompletionAction::EmitEventAndFreeOtherChannel { + if let MonitorUpdateCompletionAction::EmitEventOptionAndFreeOtherChannel { downstream_counterparty_and_funding_outpoint: Some(EventUnblockedChannel { counterparty_node_id: blocked_node_id, From 72c3086d1aa601e4b6b39a262ccca11e397ac943 Mon Sep 17 00:00:00 2001 From: Carla Kirk-Cohen Date: Wed, 7 Jan 2026 15:05:44 -0500 Subject: [PATCH 26/68] ln+events: allow multiple prev_channel_id in HTLCHandlingFailed In preparation for trampoline failures, allow multiple previous channel ids. We'll only emit a single HTLCHandlingFailed for all of our failed back HTLCs, so we want to be able to express all of them in one event. --- lightning/src/events/mod.rs | 27 ++++++++++++++++++++------- lightning/src/ln/channelmanager.rs | 4 ++-- lightning/src/ln/monitor_tests.rs | 4 ++-- lightning/src/util/ser.rs | 1 + 4 files changed, 25 insertions(+), 11 deletions(-) diff --git a/lightning/src/events/mod.rs b/lightning/src/events/mod.rs index 580487caaed..e69f8d3caaa 100644 --- a/lightning/src/events/mod.rs +++ b/lightning/src/events/mod.rs @@ -1673,12 +1673,17 @@ pub enum Event { /// Indicates that the HTLC was accepted, but could not be processed when or after attempting to /// forward it. /// + /// Note that downgrading from 0.3 with pending trampoline forwards that have incoming multipart + /// payments will produce an event that only provides information about the first htlc that was + /// received/dispatched. + /// /// # Failure Behavior and Persistence /// This event will eventually be replayed after failures-to-handle (i.e., the event handler /// returning `Err(ReplayEvent ())`) and will be persisted across restarts. HTLCHandlingFailed { - /// The channel over which the HTLC was received. - prev_channel_id: ChannelId, + /// The channel(s) over which the HTLC(s) was received. May contain multiple entries for + /// trampoline forwards. + prev_channel_ids: Vec, /// The type of HTLC handling that failed. failure_type: HTLCHandlingFailureType, /// The reason that the HTLC failed. @@ -2216,15 +2221,19 @@ impl Writeable for Event { }) }, &Event::HTLCHandlingFailed { - ref prev_channel_id, + ref prev_channel_ids, ref failure_type, ref failure_reason, } => { 25u8.write(writer)?; + let legacy_chan_id = + prev_channel_ids.first().ok_or(io::Error::from(IOInvalidData))?; write_tlv_fields!(writer, { - (0, prev_channel_id, required), + // Write legacy field to remain backwards compatible. + (0, legacy_chan_id, required), (1, failure_reason, option), (2, failure_type, required), + (3, *prev_channel_ids, required), }) }, &Event::BumpTransaction(ref event) => { @@ -2800,13 +2809,17 @@ impl MaybeReadable for Event { }, 25u8 => { let mut f = || { - let mut prev_channel_id = ChannelId::new_zero(); + let mut prev_channel_id_legacy = ChannelId::new_zero(); let mut failure_reason = None; let mut failure_type_opt = UpgradableRequired(None); + let mut prev_channel_ids = vec![]; read_tlv_fields!(reader, { - (0, prev_channel_id, required), + (0, prev_channel_id_legacy, required), (1, failure_reason, option), (2, failure_type_opt, upgradable_required), + (3, prev_channel_ids, (default_value, vec![ + prev_channel_id_legacy, + ])), }); // If a legacy HTLCHandlingFailureType::UnknownNextHop was written, upgrade @@ -2821,7 +2834,7 @@ impl MaybeReadable for Event { failure_reason = Some(LocalHTLCFailureReason::UnknownNextPeer.into()); } Ok(Some(Event::HTLCHandlingFailed { - prev_channel_id, + prev_channel_ids, failure_type: _init_tlv_based_struct_field!( failure_type_opt, upgradable_required diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 7d336155ae5..89ec42756ca 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -7219,7 +7219,7 @@ impl< .push(failure); self.pending_events.lock().unwrap().push_back(( events::Event::HTLCHandlingFailed { - prev_channel_id: incoming_channel_id, + prev_channel_ids: vec![incoming_channel_id], failure_type, failure_reason: Some(failure_reason), }, @@ -8843,7 +8843,7 @@ impl< let mut pending_events = self.pending_events.lock().unwrap(); pending_events.push_back(( events::Event::HTLCHandlingFailed { - prev_channel_id: *channel_id, + prev_channel_ids: vec![*channel_id], failure_type, failure_reason: Some(onion_error.into()), }, diff --git a/lightning/src/ln/monitor_tests.rs b/lightning/src/ln/monitor_tests.rs index 686010bd228..c065dcf2f28 100644 --- a/lightning/src/ln/monitor_tests.rs +++ b/lightning/src/ln/monitor_tests.rs @@ -3793,8 +3793,8 @@ fn do_test_lost_timeout_monitor_events(confirm_tx: CommitmentType, dust_htlcs: b Event::PaymentFailed { payment_hash, .. } => { assert_eq!(payment_hash, Some(hash_b)); }, - Event::HTLCHandlingFailed { prev_channel_id, .. } => { - assert_eq!(prev_channel_id, chan_a); + Event::HTLCHandlingFailed { prev_channel_ids, .. } => { + assert_eq!(prev_channel_ids[0], chan_a); }, _ => panic!("Wrong event {ev:?}"), } diff --git a/lightning/src/util/ser.rs b/lightning/src/util/ser.rs index 249505f2e21..24e535b70bb 100644 --- a/lightning/src/util/ser.rs +++ b/lightning/src/util/ser.rs @@ -1101,6 +1101,7 @@ impl_for_vec_with_element_length_prefix!(crate::ln::msgs::UpdateAddHTLC); impl_writeable_for_vec_with_element_length_prefix!(&crate::ln::msgs::UpdateAddHTLC); impl_for_vec!(u32); impl_for_vec!(crate::events::HTLCLocator); +impl_for_vec!(crate::ln::types::ChannelId); impl Writeable for Vec { #[inline] From 4b254ff01c72713c75deaa304a098f2bbeea0a23 Mon Sep 17 00:00:00 2001 From: Carla Kirk-Cohen Date: Tue, 6 Jan 2026 15:28:42 -0500 Subject: [PATCH 27/68] events: add TrampolineForward variant to HTLCHandlingFailureType --- lightning/src/events/mod.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lightning/src/events/mod.rs b/lightning/src/events/mod.rs index e69f8d3caaa..c9969f024c6 100644 --- a/lightning/src/events/mod.rs +++ b/lightning/src/events/mod.rs @@ -576,6 +576,10 @@ pub enum HTLCHandlingFailureType { /// The payment hash of the payment we attempted to process. payment_hash: PaymentHash, }, + /// We were responsible for pathfinding and forwarding of a trampoline payment, but failed to + /// do so. An example of such an instance is when we can't find a route to the specified + /// trampoline destination. + TrampolineForward {}, } impl_writeable_tlv_based_enum_upgradable!(HTLCHandlingFailureType, @@ -593,6 +597,7 @@ impl_writeable_tlv_based_enum_upgradable!(HTLCHandlingFailureType, (4, Receive) => { (0, payment_hash, required), }, + (5, TrampolineForward) => {}, ); /// The reason for HTLC failures in [`Event::HTLCHandlingFailed`]. From ae048a130b992d4888bb28d7de5f349d0214212e Mon Sep 17 00:00:00 2001 From: Carla Kirk-Cohen Date: Tue, 2 Dec 2025 10:06:41 -0500 Subject: [PATCH 28/68] ln: add TrampolineForward SendHTLCId variant This commit adds a SendHTLCId for trampoline forwards, identified by their session_priv. As with an OutboundRoute, we can expect our HTLC to be uniquely identified by a randomly generated session_priv. TrampolineForward could also be identified by the set of all previous outbound scid/htlc id pairs that represent its incoming HTLC(s). We choose the 32 byte session_priv to fix the size of this identifier rather than 16 byte scid/id pairs that will grow with the number of incoming htlcs. --- lightning/src/ln/channelmanager.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 89ec42756ca..c99df9c9231 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -757,6 +757,7 @@ impl Default for OptionalOfferPaymentParams { pub(crate) enum SentHTLCId { PreviousHopData { prev_outbound_scid_alias: u64, htlc_id: u64 }, OutboundRoute { session_priv: [u8; SECRET_KEY_SIZE] }, + TrampolineForward { session_priv: [u8; SECRET_KEY_SIZE] }, } impl SentHTLCId { pub(crate) fn from_source(source: &HTLCSource) -> Self { @@ -779,6 +780,9 @@ impl_writeable_tlv_based_enum!(SentHTLCId, (2, OutboundRoute) => { (0, session_priv, required), }, + (4, TrampolineForward) => { + (0, session_priv, required), + }, ); // (src_outbound_scid_alias, src_counterparty_node_id, src_funding_outpoint, src_chan_id, src_user_chan_id) From b2e0e389b2f224a991d7152728e7916d88b35ae0 Mon Sep 17 00:00:00 2001 From: Maurice Date: Fri, 22 Aug 2025 10:37:21 -0400 Subject: [PATCH 29/68] ln: add TrampolineForward variant to HTLCSource enum We only have payment details for HTLCSource::TrampolineForward available once we've dispatched the payment. If we get to the stage where we need a HTLCId for the outbound payment, we expect dispatch details to be present. Co-authored-by: Arik Sosman Co-authored-by: Maurice Poirrier --- lightning/src/chain/channelmonitor.rs | 2 + lightning/src/ln/channelmanager.rs | 77 ++++++++++++++++++++++++++- lightning/src/routing/router.rs | 5 ++ 3 files changed, 83 insertions(+), 1 deletion(-) diff --git a/lightning/src/chain/channelmonitor.rs b/lightning/src/chain/channelmonitor.rs index 741ac2bf4ba..847860ad295 100644 --- a/lightning/src/chain/channelmonitor.rs +++ b/lightning/src/chain/channelmonitor.rs @@ -2794,6 +2794,7 @@ impl ChannelMonitorImpl { let outbound_payment = match source { None => panic!("Outbound HTLCs should have a source"), Some(&HTLCSource::PreviousHopData(_)) => false, + Some(&HTLCSource::TrampolineForward { .. }) => false, Some(&HTLCSource::OutboundRoute { .. }) => true, }; return Some(Balance::MaybeTimeoutClaimableHTLC { @@ -3006,6 +3007,7 @@ impl ChannelMonitor { let outbound_payment = match source { None => panic!("Outbound HTLCs should have a source"), Some(HTLCSource::PreviousHopData(_)) => false, + Some(HTLCSource::TrampolineForward { .. }) => false, Some(HTLCSource::OutboundRoute { .. }) => true, }; if outbound_payment { diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index c99df9c9231..677692a12cc 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -766,6 +766,15 @@ impl SentHTLCId { prev_outbound_scid_alias: hop_data.prev_outbound_scid_alias, htlc_id: hop_data.htlc_id, }, + HTLCSource::TrampolineForward { + ref outbound_payment, + .. + } => Self::TrampolineForward { + session_priv: outbound_payment + .as_ref() + .map(|o| o.session_priv.secret_bytes()) + .expect("trying to identify a trampoline payment that we have no outbound_payment tracked for"), + }, HTLCSource::OutboundRoute { session_priv, .. } => { Self::OutboundRoute { session_priv: session_priv.secret_bytes() } }, @@ -794,11 +803,31 @@ type FailedHTLCForward = (HTLCSource, PaymentHash, HTLCFailReason, HTLCHandlingF mod fuzzy_channelmanager { use super::*; + /// Information about the outgoing payment dispatched to forward to the next trampoline. + #[derive(Clone, Debug, PartialEq, Eq)] + pub struct TrampolineDispatch { + /// The payment ID used for the outbound payment. + pub payment_id: PaymentId, + /// The path used for the outbound payment. + pub path: Path, + /// The session private key used for inter-trampoline outer onions. + pub session_priv: SecretKey, + } + /// Tracks the inbound corresponding to an outbound HTLC #[allow(clippy::derive_hash_xor_eq)] // Our Hash is faithful to the data, we just don't have SecretKey::hash #[derive(Clone, Debug, PartialEq, Eq)] pub enum HTLCSource { PreviousHopData(HTLCPreviousHopData), + TrampolineForward { + /// We might be forwarding an incoming payment that was received over MPP, and therefore + /// need to store the vector of corresponding `HTLCPreviousHopData` values. + previous_hop_data: Vec, + incoming_trampoline_shared_secret: [u8; 32], + /// Track outbound payment details once the payment has been dispatched, will be `None` + /// when waiting for incoming MPP to accumulate. + outbound_payment: Option, + }, OutboundRoute { path: Path, session_priv: SecretKey, @@ -861,6 +890,20 @@ impl core::hash::Hash for HTLCSource { first_hop_htlc_msat.hash(hasher); bolt12_invoice.hash(hasher); }, + HTLCSource::TrampolineForward { + previous_hop_data, + incoming_trampoline_shared_secret, + outbound_payment, + } => { + 2u8.hash(hasher); + previous_hop_data.hash(hasher); + incoming_trampoline_shared_secret.hash(hasher); + if let Some(payment) = outbound_payment { + payment.payment_id.hash(hasher); + payment.path.hash(hasher); + payment.session_priv[..].hash(hasher); + } + }, } } } @@ -8854,6 +8897,7 @@ impl< None, )); }, + HTLCSource::TrampolineForward { .. } => todo!(), } } @@ -9615,6 +9659,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ }, ); }, + HTLCSource::TrampolineForward { .. } => todo!(), } } @@ -16774,6 +16819,12 @@ impl_writeable_tlv_based!(HTLCPreviousHopData, { (13, trampoline_shared_secret, option), }); +impl_writeable_tlv_based!(TrampolineDispatch, { + (1, payment_id, required), + (3, path, required), + (5, session_priv, required), +}); + impl Writeable for ClaimableHTLC { fn write(&self, writer: &mut W) -> Result<(), io::Error> { let (payment_data, keysend_preimage) = match &self.onion_payload { @@ -16892,6 +16943,18 @@ impl Readable for HTLCSource { }) } 1 => Ok(HTLCSource::PreviousHopData(Readable::read(reader)?)), + 2 => { + _init_and_read_len_prefixed_tlv_fields!(reader, { + (1, previous_hop_data, required_vec), + (3, incoming_trampoline_shared_secret, required), + (5, outbound_payment, option), + }); + Ok(HTLCSource::TrampolineForward { + previous_hop_data: _init_tlv_based_struct_field!(previous_hop_data, required_vec), + incoming_trampoline_shared_secret: _init_tlv_based_struct_field!(incoming_trampoline_shared_secret, required), + outbound_payment, + }) + }, _ => Err(DecodeError::UnknownRequiredFeature), } } @@ -16924,6 +16987,18 @@ impl Writeable for HTLCSource { 1u8.write(writer)?; field.write(writer)?; }, + HTLCSource::TrampolineForward { + ref previous_hop_data, + incoming_trampoline_shared_secret, + ref outbound_payment, + } => { + 2u8.write(writer)?; + write_tlv_fields!(writer, { + (1, *previous_hop_data, required_vec), + (3, incoming_trampoline_shared_secret, required), + (5, outbound_payment, option), + }); + }, } Ok(()) } @@ -18730,6 +18805,7 @@ impl< } else { true } }); }, + HTLCSource::TrampolineForward { .. } => todo!(), HTLCSource::OutboundRoute { payment_id, session_priv, @@ -18789,7 +18865,6 @@ impl< // Note that for channels closed pre-0.1, the latest // update_id is `u64::MAX`. *update_id = update_id.saturating_add(1); - pending_background_events.push( BackgroundEvent::MonitorUpdateRegeneratedOnStartup { counterparty_node_id: monitor diff --git a/lightning/src/routing/router.rs b/lightning/src/routing/router.rs index 9376bff758d..407566ee566 100644 --- a/lightning/src/routing/router.rs +++ b/lightning/src/routing/router.rs @@ -656,6 +656,11 @@ impl Path { } } +impl_writeable_tlv_based!(Path,{ + (1, hops, required_vec), + (3, blinded_tail, option), +}); + /// A route directs a payment from the sender (us) to the recipient. If the recipient supports MPP, /// it can take multiple paths. Each path is composed of one or more hops through the network. #[derive(Clone, Debug, Hash, PartialEq, Eq)] From 7601d1b86d73c5c29d4f80c2f155ccded3c3dbe6 Mon Sep 17 00:00:00 2001 From: Carla Kirk-Cohen Date: Wed, 11 Feb 2026 09:48:24 +0200 Subject: [PATCH 30/68] ln: add failure_type helper to HTLCSource for HTLCHandlingFailureType To create the right handling type based on source, add a helper. This is mainly useful for PreviousHopData/TrampolineForward. This helper maps an OutboundRoute to a HTLCHandlingFailureType::Forward. This value isn't actually used once we reach `forward_htlc_backwards_internal`, because we don't emit `HTLCHandlingFailed` events for our own payments. This issue is pre-existing, and could be addressed with an API change to the failure function, which is left out of scope of this work. --- lightning/src/ln/channelmanager.rs | 91 ++++++++++++++++++------------ 1 file changed, 56 insertions(+), 35 deletions(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 677692a12cc..5a36e668e34 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -842,6 +842,26 @@ mod fuzzy_channelmanager { }, } + impl HTLCSource { + pub fn failure_type( + &self, counterparty_node: PublicKey, channel_id: ChannelId, + ) -> HTLCHandlingFailureType { + match self { + // We won't actually emit an event with HTLCHandlingFailure if our source is an + // OutboundRoute, but `fail_htlc_backwards_internal` requires that we provide it. + HTLCSource::PreviousHopData(_) | HTLCSource::OutboundRoute { .. } => { + HTLCHandlingFailureType::Forward { + node_id: Some(counterparty_node), + channel_id, + } + }, + HTLCSource::TrampolineForward { .. } => { + HTLCHandlingFailureType::TrampolineForward {} + }, + } + } + } + /// Tracks the inbound corresponding to an outbound HTLC #[derive(Clone, Debug, Hash, PartialEq, Eq)] pub struct HTLCPreviousHopData { @@ -3984,11 +4004,8 @@ impl< for htlc_source in failed_htlcs.drain(..) { let failure_reason = LocalHTLCFailureReason::ChannelClosed; let reason = HTLCFailReason::from_failure_code(failure_reason); - let receiver = HTLCHandlingFailureType::Forward { - node_id: Some(*counterparty_node_id), - channel_id: *chan_id, - }; let (source, hash) = htlc_source; + let receiver = source.failure_type(*counterparty_node_id, *chan_id); self.fail_htlc_backwards_internal(&source, &hash, &reason, receiver, None); } @@ -4151,10 +4168,7 @@ impl< let (source, payment_hash, counterparty_node_id, channel_id) = htlc_source; let failure_reason = LocalHTLCFailureReason::ChannelClosed; let reason = HTLCFailReason::from_failure_code(failure_reason); - let receiver = HTLCHandlingFailureType::Forward { - node_id: Some(counterparty_node_id), - channel_id, - }; + let receiver = source.failure_type(counterparty_node_id, channel_id); self.fail_htlc_backwards_internal(&source, &payment_hash, &reason, receiver, None); } if let Some((_, funding_txo, _channel_id, monitor_update)) = shutdown_res.monitor_update { @@ -7451,6 +7465,8 @@ impl< }; failed_forwards.push(( + // This can't be a trampoline payment because we don't process them + // as forwards (we're the last/"receiving" onion node). HTLCSource::PreviousHopData(prev_hop), payment_hash, HTLCFailReason::reason(reason, err_data), @@ -8758,11 +8774,14 @@ impl< for (htlc_src, payment_hash) in htlcs_to_fail.drain(..) { let reason = HTLCFailReason::reason(failure_reason, onion_failure_data.clone()); - let receiver = HTLCHandlingFailureType::Forward { - node_id: Some(counterparty_node_id.clone()), - channel_id, - }; - self.fail_htlc_backwards_internal(&htlc_src, &payment_hash, &reason, receiver, None); + let failure_type = htlc_src.failure_type(*counterparty_node_id, channel_id); + self.fail_htlc_backwards_internal( + &htlc_src, + &payment_hash, + &reason, + failure_type, + None, + ); } } @@ -9742,11 +9761,14 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ } self.finalize_claims(finalized_claimed_htlcs); for failure in failed_htlcs { - let receiver = HTLCHandlingFailureType::Forward { - node_id: Some(counterparty_node_id), - channel_id, - }; - self.fail_htlc_backwards_internal(&failure.0, &failure.1, &failure.2, receiver, None); + let failure_type = failure.0.failure_type(counterparty_node_id, channel_id); + self.fail_htlc_backwards_internal( + &failure.0, + &failure.1, + &failure.2, + failure_type, + None, + ); } } @@ -11769,13 +11791,10 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ } } for htlc_source in dropped_htlcs.drain(..) { - let receiver = HTLCHandlingFailureType::Forward { - node_id: Some(counterparty_node_id.clone()), - channel_id: msg.channel_id, - }; - let reason = HTLCFailReason::from_failure_code(LocalHTLCFailureReason::ChannelClosed); let (source, hash) = htlc_source; - self.fail_htlc_backwards_internal(&source, &hash, &reason, receiver, None); + let failure_type = source.failure_type(*counterparty_node_id, msg.channel_id); + let reason = HTLCFailReason::from_failure_code(LocalHTLCFailureReason::ChannelClosed); + self.fail_htlc_backwards_internal(&source, &hash, &reason, failure_type, None); } Ok(()) @@ -12813,10 +12832,8 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ } else { log_trace!(logger, "Failing HTLC from our monitor"); let failure_reason = LocalHTLCFailureReason::OnChainTimeout; - let receiver = HTLCHandlingFailureType::Forward { - node_id: Some(counterparty_node_id), - channel_id, - }; + let failure_type = + htlc_update.source.failure_type(counterparty_node_id, channel_id); let reason = HTLCFailReason::from_failure_code(failure_reason); let completion_update = Some(PaymentCompleteUpdate { counterparty_node_id, @@ -12828,7 +12845,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ &htlc_update.source, &htlc_update.payment_hash, &reason, - receiver, + failure_type, completion_update, ); } @@ -15194,8 +15211,8 @@ impl< for (source, payment_hash) in timed_out_pending_htlcs.drain(..) { let reason = LocalHTLCFailureReason::CLTVExpiryTooSoon; let data = self.get_htlc_inbound_temp_fail_data(reason); - timed_out_htlcs.push((source, payment_hash, HTLCFailReason::reason(reason, data), - HTLCHandlingFailureType::Forward { node_id: Some(funded_channel.context.get_counterparty_node_id()), channel_id: *channel_id })); + let failure_type = source.failure_type(funded_channel.context.get_counterparty_node_id(), *channel_id); + timed_out_htlcs.push((source, payment_hash, HTLCFailReason::reason(reason, data), failure_type)); } let logger = WithChannelContext::from(&self.logger, &funded_channel.context, None); match funding_confirmed_opt { @@ -19606,11 +19623,15 @@ impl< for htlc_source in failed_htlcs { let (source, hash, counterparty_id, channel_id, failure_reason, ev_action) = htlc_source; - let receiver = - HTLCHandlingFailureType::Forward { node_id: Some(counterparty_id), channel_id }; + let failure_type = source.failure_type(counterparty_id, channel_id); let reason = HTLCFailReason::from_failure_code(failure_reason); - channel_manager - .fail_htlc_backwards_internal(&source, &hash, &reason, receiver, ev_action); + channel_manager.fail_htlc_backwards_internal( + &source, + &hash, + &reason, + failure_type, + ev_action, + ); } for ( From 6dd6e8910239cc376ae2ca3eb4700e629af71f2c Mon Sep 17 00:00:00 2001 From: Carla Kirk-Cohen Date: Tue, 16 Dec 2025 15:21:57 +0200 Subject: [PATCH 31/68] ln/refactor: add claim funds for htlc forward helper Will need to share this code when we add trampoline forwarding. This commit exactly moves the logic as-is, in preparation for the next commit that will update to suit trampoline. Co-authored-by: Arik Sosman Co-authored-by: Maurice Poirrier --- lightning/src/ln/channelmanager.rs | 294 ++++++++++++++++------------- 1 file changed, 163 insertions(+), 131 deletions(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 5a36e668e34..e1ac5d715ef 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -9149,6 +9149,157 @@ impl< } } + /// Claims funds for a forwarded HTLC where we are an intermediate hop. + /// + /// Processes attribution data, calculates fees earned, and emits a [`Event::PaymentForwarded`] + /// event upon successful claim. + fn claim_funds_from_htlc_forward_hop( + &self, payment_preimage: PaymentPreimage, forwarded_htlc_value_msat: Option, + skimmed_fee_msat: Option, from_onchain: bool, startup_replay: bool, + next_channel_counterparty_node_id: PublicKey, next_channel_outpoint: OutPoint, + next_channel_id: ChannelId, next_user_channel_id: Option, + hop_data: HTLCPreviousHopData, attribution_data: Option, + send_timestamp: Option, + ) { + let prev_channel_id = hop_data.channel_id; + let prev_user_channel_id = hop_data.user_channel_id; + let prev_node_id = hop_data.counterparty_node_id; + let completed_blocker = RAAMonitorUpdateBlockingAction::from_prev_hop_data(&hop_data); + + // Obtain hold time, if available. + let hold_time = hold_time_since(send_timestamp).unwrap_or(0); + + // If attribution data was received from downstream, we shift it and get it ready for adding our hold + // time. Note that fulfilled HTLCs take a fast path to the incoming side. We don't need to wait for RAA + // to record the hold time like we do for failed HTLCs. + let attribution_data = process_fulfill_attribution_data( + attribution_data, + &hop_data.incoming_packet_shared_secret, + hold_time, + ); + + #[cfg(test)] + let claiming_chan_funding_outpoint = hop_data.outpoint; + self.claim_funds_from_hop( + hop_data, + payment_preimage, + None, + Some(attribution_data), + |htlc_claim_value_msat, definitely_duplicate| { + let chan_to_release = Some(EventUnblockedChannel { + counterparty_node_id: next_channel_counterparty_node_id, + funding_txo: next_channel_outpoint, + channel_id: next_channel_id, + blocking_action: completed_blocker, + }); + + if definitely_duplicate && startup_replay { + // On startup we may get redundant claims which are related to + // monitor updates still in flight. In that case, we shouldn't + // immediately free, but instead let that monitor update complete + // in the background. + #[cfg(test)] + { + let per_peer_state = self.per_peer_state.deadlocking_read(); + // The channel we'd unblock should already be closed, or... + let channel_closed = per_peer_state + .get(&next_channel_counterparty_node_id) + .map(|lck| lck.deadlocking_lock()) + .map(|peer| !peer.channel_by_id.contains_key(&next_channel_id)) + .unwrap_or(true); + let background_events = self.pending_background_events.lock().unwrap(); + // there should be a `BackgroundEvent` pending... + let matching_bg_event = + background_events.iter().any(|ev| { + match ev { + // to apply a monitor update that blocked the claiming channel, + BackgroundEvent::MonitorUpdateRegeneratedOnStartup { + funding_txo, + update, + .. + } => { + if *funding_txo == claiming_chan_funding_outpoint { + assert!( + update.updates.iter().any(|upd| { + if let ChannelMonitorUpdateStep::PaymentPreimage { + payment_preimage: update_preimage, .. + } = upd { + payment_preimage == *update_preimage + } else { false } + }), + "{:?}", + update + ); + true + } else { + false + } + }, + // or the monitor update has completed and will unblock + // immediately once we get going. + BackgroundEvent::MonitorUpdatesComplete { + channel_id, .. + } => *channel_id == prev_channel_id, + } + }); + assert!(channel_closed || matching_bg_event, "{:?}", *background_events); + } + (None, None) + } else if definitely_duplicate { + if let Some(other_chan) = chan_to_release { + ( + Some(MonitorUpdateCompletionAction::FreeOtherChannelImmediately { + downstream_counterparty_node_id: other_chan.counterparty_node_id, + downstream_channel_id: other_chan.channel_id, + blocking_action: other_chan.blocking_action, + }), + None, + ) + } else { + (None, None) + } + } else { + let total_fee_earned_msat = + if let Some(forwarded_htlc_value) = forwarded_htlc_value_msat { + if let Some(claimed_htlc_value) = htlc_claim_value_msat { + Some(claimed_htlc_value - forwarded_htlc_value) + } else { + None + } + } else { + None + }; + debug_assert!( + skimmed_fee_msat <= total_fee_earned_msat, + "skimmed_fee_msat must always be included in total_fee_earned_msat" + ); + ( + Some(MonitorUpdateCompletionAction::EmitEventOptionAndFreeOtherChannel { + event: Some(events::Event::PaymentForwarded { + prev_htlcs: vec![events::HTLCLocator { + channel_id: prev_channel_id, + user_channel_id: prev_user_channel_id, + node_id: prev_node_id, + }], + next_htlcs: vec![events::HTLCLocator { + channel_id: next_channel_id, + user_channel_id: next_user_channel_id, + node_id: Some(next_channel_counterparty_node_id), + }], + total_fee_earned_msat, + skimmed_fee_msat, + claim_from_onchain_tx: from_onchain, + outbound_amount_forwarded_msat: forwarded_htlc_value_msat, + }), + downstream_counterparty_and_funding_outpoint: chan_to_release, + }), + None, + ) + } + }, + ); + } + fn claim_funds_from_hop< ComplFunc: FnOnce( Option, @@ -9544,138 +9695,19 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ } }, HTLCSource::PreviousHopData(hop_data) => { - let prev_channel_id = hop_data.channel_id; - let prev_user_channel_id = hop_data.user_channel_id; - let prev_node_id = hop_data.counterparty_node_id; - let completed_blocker = - RAAMonitorUpdateBlockingAction::from_prev_hop_data(&hop_data); - - // Obtain hold time, if available. - let hold_time = hold_time_since(send_timestamp).unwrap_or(0); - - // If attribution data was received from downstream, we shift it and get it ready for adding our hold - // time. Note that fulfilled HTLCs take a fast path to the incoming side. We don't need to wait for RAA - // to record the hold time like we do for failed HTLCs. - let attribution_data = process_fulfill_attribution_data( - attribution_data, - &hop_data.incoming_packet_shared_secret, - hold_time, - ); - - #[cfg(test)] - let claiming_chan_funding_outpoint = hop_data.outpoint; - self.claim_funds_from_hop( - hop_data, + self.claim_funds_from_htlc_forward_hop( payment_preimage, - None, - Some(attribution_data), - |htlc_claim_value_msat, definitely_duplicate| { - let chan_to_release = Some(EventUnblockedChannel { - counterparty_node_id: next_channel_counterparty_node_id, - funding_txo: next_channel_outpoint, - channel_id: next_channel_id, - blocking_action: completed_blocker, - }); - - if definitely_duplicate && startup_replay { - // On startup we may get redundant claims which are related to - // monitor updates still in flight. In that case, we shouldn't - // immediately free, but instead let that monitor update complete - // in the background. - #[cfg(test)] - { - let per_peer_state = self.per_peer_state.deadlocking_read(); - // The channel we'd unblock should already be closed, or... - let channel_closed = per_peer_state - .get(&next_channel_counterparty_node_id) - .map(|lck| lck.deadlocking_lock()) - .map(|peer| !peer.channel_by_id.contains_key(&next_channel_id)) - .unwrap_or(true); - let background_events = - self.pending_background_events.lock().unwrap(); - // there should be a `BackgroundEvent` pending... - let matching_bg_event = - background_events.iter().any(|ev| { - match ev { - // to apply a monitor update that blocked the claiming channel, - BackgroundEvent::MonitorUpdateRegeneratedOnStartup { - funding_txo, update, .. - } => { - if *funding_txo == claiming_chan_funding_outpoint { - assert!(update.updates.iter().any(|upd| - if let ChannelMonitorUpdateStep::PaymentPreimage { - payment_preimage: update_preimage, .. - } = upd { - payment_preimage == *update_preimage - } else { false } - ), "{:?}", update); - true - } else { false } - }, - // or the monitor update has completed and will unblock - // immediately once we get going. - BackgroundEvent::MonitorUpdatesComplete { - channel_id, .. - } => - *channel_id == prev_channel_id, - } - }); - assert!( - channel_closed || matching_bg_event, - "{:?}", - *background_events - ); - } - (None, None) - } else if definitely_duplicate { - if let Some(other_chan) = chan_to_release { - (Some(MonitorUpdateCompletionAction::FreeOtherChannelImmediately { - downstream_counterparty_node_id: other_chan.counterparty_node_id, - downstream_channel_id: other_chan.channel_id, - blocking_action: other_chan.blocking_action, - }), None) - } else { - (None, None) - } - } else { - let total_fee_earned_msat = - if let Some(forwarded_htlc_value) = forwarded_htlc_value_msat { - if let Some(claimed_htlc_value) = htlc_claim_value_msat { - Some(claimed_htlc_value - forwarded_htlc_value) - } else { - None - } - } else { - None - }; - debug_assert!( - skimmed_fee_msat <= total_fee_earned_msat, - "skimmed_fee_msat must always be included in total_fee_earned_msat" - ); - ( - Some(MonitorUpdateCompletionAction::EmitEventOptionAndFreeOtherChannel { - event: Some(events::Event::PaymentForwarded { - prev_htlcs: vec![events::HTLCLocator { - channel_id: prev_channel_id, - user_channel_id: prev_user_channel_id, - node_id: prev_node_id, - }], - next_htlcs: vec![events::HTLCLocator { - channel_id: next_channel_id, - user_channel_id: next_user_channel_id, - node_id: Some(next_channel_counterparty_node_id), - }], - total_fee_earned_msat, - skimmed_fee_msat, - claim_from_onchain_tx: from_onchain, - outbound_amount_forwarded_msat: forwarded_htlc_value_msat, - }), - downstream_counterparty_and_funding_outpoint: chan_to_release, - }), - None, - ) - } - }, + forwarded_htlc_value_msat, + skimmed_fee_msat, + from_onchain, + startup_replay, + next_channel_counterparty_node_id, + next_channel_outpoint, + next_channel_id, + next_user_channel_id, + hop_data, + attribution_data, + send_timestamp, ); }, HTLCSource::TrampolineForward { .. } => todo!(), From c252eec2d28aa731caa26c31b28213028bdb5ccf Mon Sep 17 00:00:00 2001 From: Carla Kirk-Cohen Date: Tue, 6 Jan 2026 09:09:06 -0500 Subject: [PATCH 32/68] ln/refactor: pass closure to create PaymentForwarded event When we introduce trampoline forwards, we're going to want to provide two external pieces of information to create events: - When to emit an event: we only want to emit one trampoline event, even when we have multiple incoming htlcs. We need to make multiple calls to claim_funds_from_htlc_forward_hop to claim each individual htlc, which are not aware of each other, so we rely on the caller's closure to decide when to emit Some or None. - Forwarding fees: we will not be able to calculate the total fee for a trampoline forward when an individual outgoing htlcs is fulfilled, because there may be other outgoing htlcs that are not accounted for (we only get the htlc_claim_value_msat for the single htlc that was just fulfilled). In future, we'll be able to provide the total fee from the channelmanager's top level view. --- lightning/src/ln/channelmanager.rs | 102 ++++++++++++++++------------- 1 file changed, 57 insertions(+), 45 deletions(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index e1ac5d715ef..855aadf680e 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -882,6 +882,16 @@ mod fuzzy_channelmanager { /// channel remains unconfirmed for too long. pub cltv_expiry: Option, } + + impl From<&HTLCPreviousHopData> for events::HTLCLocator { + fn from(value: &HTLCPreviousHopData) -> Self { + events::HTLCLocator { + channel_id: value.channel_id, + user_channel_id: value.user_channel_id, + node_id: value.counterparty_node_id, + } + } + } } #[cfg(fuzzing)] pub use self::fuzzy_channelmanager::*; @@ -9152,18 +9162,16 @@ impl< /// Claims funds for a forwarded HTLC where we are an intermediate hop. /// /// Processes attribution data, calculates fees earned, and emits a [`Event::PaymentForwarded`] - /// event upon successful claim. + /// event upon successful claim. `make_payment_forwarded_event` is responsible for creating a + /// single [`Event::PaymentForwarded`] event that represents the forward. fn claim_funds_from_htlc_forward_hop( - &self, payment_preimage: PaymentPreimage, forwarded_htlc_value_msat: Option, - skimmed_fee_msat: Option, from_onchain: bool, startup_replay: bool, - next_channel_counterparty_node_id: PublicKey, next_channel_outpoint: OutPoint, - next_channel_id: ChannelId, next_user_channel_id: Option, - hop_data: HTLCPreviousHopData, attribution_data: Option, - send_timestamp: Option, + &self, payment_preimage: PaymentPreimage, + make_payment_forwarded_event: impl Fn(Option) -> Option, + startup_replay: bool, next_channel_counterparty_node_id: PublicKey, + next_channel_outpoint: OutPoint, next_channel_id: ChannelId, hop_data: HTLCPreviousHopData, + attribution_data: Option, send_timestamp: Option, ) { - let prev_channel_id = hop_data.channel_id; - let prev_user_channel_id = hop_data.user_channel_id; - let prev_node_id = hop_data.counterparty_node_id; + let _prev_channel_id = hop_data.channel_id; let completed_blocker = RAAMonitorUpdateBlockingAction::from_prev_hop_data(&hop_data); // Obtain hold time, if available. @@ -9239,7 +9247,7 @@ impl< // immediately once we get going. BackgroundEvent::MonitorUpdatesComplete { channel_id, .. - } => *channel_id == prev_channel_id, + } => *channel_id == _prev_channel_id, } }); assert!(channel_closed || matching_bg_event, "{:?}", *background_events); @@ -9259,38 +9267,16 @@ impl< (None, None) } } else { - let total_fee_earned_msat = - if let Some(forwarded_htlc_value) = forwarded_htlc_value_msat { - if let Some(claimed_htlc_value) = htlc_claim_value_msat { - Some(claimed_htlc_value - forwarded_htlc_value) - } else { - None - } - } else { - None - }; - debug_assert!( - skimmed_fee_msat <= total_fee_earned_msat, - "skimmed_fee_msat must always be included in total_fee_earned_msat" - ); + let event = make_payment_forwarded_event(htlc_claim_value_msat); + if let Some(ref payment_forwarded) = event { + debug_assert!(matches!( + payment_forwarded, + &events::Event::PaymentForwarded { .. } + )); + } ( Some(MonitorUpdateCompletionAction::EmitEventOptionAndFreeOtherChannel { - event: Some(events::Event::PaymentForwarded { - prev_htlcs: vec![events::HTLCLocator { - channel_id: prev_channel_id, - user_channel_id: prev_user_channel_id, - node_id: prev_node_id, - }], - next_htlcs: vec![events::HTLCLocator { - channel_id: next_channel_id, - user_channel_id: next_user_channel_id, - node_id: Some(next_channel_counterparty_node_id), - }], - total_fee_earned_msat, - skimmed_fee_msat, - claim_from_onchain_tx: from_onchain, - outbound_amount_forwarded_msat: forwarded_htlc_value_msat, - }), + event, downstream_counterparty_and_funding_outpoint: chan_to_release, }), None, @@ -9695,16 +9681,42 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ } }, HTLCSource::PreviousHopData(hop_data) => { + let prev_htlcs = vec![events::HTLCLocator::from(&hop_data)]; self.claim_funds_from_htlc_forward_hop( payment_preimage, - forwarded_htlc_value_msat, - skimmed_fee_msat, - from_onchain, + |htlc_claim_value_msat: Option| -> Option { + let total_fee_earned_msat = + if let Some(forwarded_htlc_value) = forwarded_htlc_value_msat { + if let Some(claimed_htlc_value) = htlc_claim_value_msat { + Some(claimed_htlc_value - forwarded_htlc_value) + } else { + None + } + } else { + None + }; + debug_assert!( + skimmed_fee_msat <= total_fee_earned_msat, + "skimmed_fee_msat must always be included in total_fee_earned_msat" + ); + + Some(events::Event::PaymentForwarded { + prev_htlcs: prev_htlcs.clone(), + next_htlcs: vec![events::HTLCLocator { + channel_id: next_channel_id, + user_channel_id: next_user_channel_id, + node_id: Some(next_channel_counterparty_node_id), + }], + total_fee_earned_msat, + skimmed_fee_msat, + claim_from_onchain_tx: from_onchain, + outbound_amount_forwarded_msat: forwarded_htlc_value_msat, + }) + }, startup_replay, next_channel_counterparty_node_id, next_channel_outpoint, next_channel_id, - next_user_channel_id, hop_data, attribution_data, send_timestamp, From cc49d85c5ace3fafa59551ed609934ecc14ba58f Mon Sep 17 00:00:00 2001 From: Carla Kirk-Cohen Date: Tue, 6 Jan 2026 09:42:37 -0500 Subject: [PATCH 33/68] ln: add trampoline routing payment claiming Implement payment claiming for `HTLCSource::TrampolineForward` by iterating through previous hop data and claiming funds for each HTLC. Co-authored-by: Arik Sosman Co-authored-by: Maurice Poirrier --- lightning/src/ln/channelmanager.rs | 42 +++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 855aadf680e..79c17d652a6 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -9722,7 +9722,47 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ send_timestamp, ); }, - HTLCSource::TrampolineForward { .. } => todo!(), + HTLCSource::TrampolineForward { previous_hop_data, .. } => { + // Only emit a single event for trampoline claims. + let prev_htlcs: Vec = + previous_hop_data.iter().map(Into::into).collect(); + for (i, current_previous_hop_data) in previous_hop_data.into_iter().enumerate() { + self.claim_funds_from_htlc_forward_hop( + payment_preimage, + |_: Option| -> Option { + if i == 0 { + Some(events::Event::PaymentForwarded { + prev_htlcs: prev_htlcs.clone(), + // TODO: When trampoline payments are tracked in our + // pending_outbound_payments, we'll be able to provide all the + // outgoing htlcs for this forward. + next_htlcs: vec![events::HTLCLocator { + channel_id: next_channel_id, + user_channel_id: next_user_channel_id, + node_id: Some(next_channel_counterparty_node_id), + }], + // TODO: When trampoline payments are tracked in our + // pending_outbound_payments, we'll be able to lookup our total + // fee earnings. + total_fee_earned_msat: None, + skimmed_fee_msat, + claim_from_onchain_tx: from_onchain, + outbound_amount_forwarded_msat: forwarded_htlc_value_msat, + }) + } else { + None + } + }, + startup_replay, + next_channel_counterparty_node_id, + next_channel_outpoint, + next_channel_id, + current_previous_hop_data, + attribution_data.clone(), + send_timestamp, + ); + } + }, } } From d083341fa0deb5eeb911a3b1d48449186c9bd055 Mon Sep 17 00:00:00 2001 From: Carla Kirk-Cohen Date: Thu, 20 Nov 2025 11:04:59 -0500 Subject: [PATCH 34/68] ln/refactor: add blinded forwarding failure helper function We'll want this extracted when we need to handle trampoline and regular forwards. Co-authored-by: Arik Sosman Co-authored-by: Maurice Poirrier --- lightning/src/ln/channelmanager.rs | 100 ++++++++++++++++++----------- 1 file changed, 62 insertions(+), 38 deletions(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 79c17d652a6..3a3b77cfa03 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -8811,6 +8811,19 @@ impl< debug_assert_ne!(peer.held_by_thread(), LockHeldState::HeldByThread); } + let push_forward_htlcs_failure = + |prev_outbound_scid_alias: u64, failure: HTLCForwardInfo| { + let mut forward_htlcs = self.forward_htlcs.lock().unwrap(); + match forward_htlcs.entry(prev_outbound_scid_alias) { + hash_map::Entry::Occupied(mut entry) => { + entry.get_mut().push(failure); + }, + hash_map::Entry::Vacant(entry) => { + entry.insert(vec![failure]); + }, + } + }; + //TODO: There is a timing attack here where if a node fails an HTLC back to us they can //identify whether we sent it or not based on the (I presume) very different runtime //between the branches here. We should make this async and move it into the forward HTLCs @@ -8877,45 +8890,19 @@ impl< if blinded_failure.is_some() { "blinded " } else { "" }, onion_error ); - // In case of trampoline + phantom we prioritize the trampoline failure over the phantom failure. - // TODO: Correctly wrap the error packet twice if failing back a trampoline + phantom HTLC. - let secondary_shared_secret = trampoline_shared_secret.or(*phantom_shared_secret); - let failure = match blinded_failure { - Some(BlindedFailure::FromIntroductionNode) => { - let blinded_onion_error = HTLCFailReason::reason( - LocalHTLCFailureReason::InvalidOnionBlinding, - vec![0; 32], - ); - let err_packet = blinded_onion_error.get_encrypted_failure_packet( - incoming_packet_shared_secret, - &secondary_shared_secret, - ); - HTLCForwardInfo::FailHTLC { htlc_id: *htlc_id, err_packet } - }, - Some(BlindedFailure::FromBlindedNode) => HTLCForwardInfo::FailMalformedHTLC { - htlc_id: *htlc_id, - failure_code: LocalHTLCFailureReason::InvalidOnionBlinding.failure_code(), - sha256_of_onion: [0; 32], - }, - None => { - let err_packet = onion_error.get_encrypted_failure_packet( - incoming_packet_shared_secret, - &secondary_shared_secret, - ); - HTLCForwardInfo::FailHTLC { htlc_id: *htlc_id, err_packet } - }, - }; - let mut forward_htlcs = self.forward_htlcs.lock().unwrap(); - match forward_htlcs.entry(*prev_outbound_scid_alias) { - hash_map::Entry::Occupied(mut entry) => { - entry.get_mut().push(failure); - }, - hash_map::Entry::Vacant(entry) => { - entry.insert(vec![failure]); - }, - } - mem::drop(forward_htlcs); + push_forward_htlcs_failure( + *prev_outbound_scid_alias, + get_htlc_forward_failure( + blinded_failure, + onion_error, + incoming_packet_shared_secret, + trampoline_shared_secret, + phantom_shared_secret, + *htlc_id, + ), + ); + let mut pending_events = self.pending_events.lock().unwrap(); pending_events.push_back(( events::Event::HTLCHandlingFailed { @@ -13568,6 +13555,43 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ } } +/// Constructs an HTLC forward failure for sending back to the previous hop, converting to a blinded +/// failure where appropriate. +/// +/// When both trampoline and phantom secrets are present, the trampoline secret takes priority +/// for error encryption. +fn get_htlc_forward_failure( + blinded_failure: &Option, onion_error: &HTLCFailReason, + incoming_packet_shared_secret: &[u8; 32], trampoline_shared_secret: &Option<[u8; 32]>, + phantom_shared_secret: &Option<[u8; 32]>, htlc_id: u64, +) -> HTLCForwardInfo { + // TODO: Correctly wrap the error packet twice if failing back a trampoline + phantom HTLC. + let secondary_shared_secret = trampoline_shared_secret.or(*phantom_shared_secret); + match blinded_failure { + Some(BlindedFailure::FromIntroductionNode) => { + let blinded_onion_error = + HTLCFailReason::reason(LocalHTLCFailureReason::InvalidOnionBlinding, vec![0; 32]); + let err_packet = blinded_onion_error.get_encrypted_failure_packet( + incoming_packet_shared_secret, + &secondary_shared_secret, + ); + HTLCForwardInfo::FailHTLC { htlc_id, err_packet } + }, + Some(BlindedFailure::FromBlindedNode) => HTLCForwardInfo::FailMalformedHTLC { + htlc_id, + failure_code: LocalHTLCFailureReason::InvalidOnionBlinding.failure_code(), + sha256_of_onion: [0; 32], + }, + None => { + let err_packet = onion_error.get_encrypted_failure_packet( + incoming_packet_shared_secret, + &secondary_shared_secret, + ); + HTLCForwardInfo::FailHTLC { htlc_id, err_packet } + }, + } +} + /// Parameters used with [`create_bolt11_invoice`]. /// /// [`create_bolt11_invoice`]: ChannelManager::create_bolt11_invoice From 1cae694b47f8888c9312583363f5b36fabd45d92 Mon Sep 17 00:00:00 2001 From: Carla Kirk-Cohen Date: Mon, 1 Dec 2025 15:50:46 -0500 Subject: [PATCH 35/68] ln: add trampoline routing failure handling Implement failure propagation for `HTLCSource::TrampolineForward` by iterating through previous hop data and failing each HTLC with `TemporaryTrampolineFailure`. Note that testing should be implemented when trampoline forward is completed. Co-authored-by: Arik Sosman Co-authored-by: Maurice Poirrier --- lightning/src/ln/channelmanager.rs | 59 +++++++++++++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 3a3b77cfa03..deee41bb58a 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -8913,7 +8913,64 @@ impl< None, )); }, - HTLCSource::TrampolineForward { .. } => todo!(), + HTLCSource::TrampolineForward { + previous_hop_data, + incoming_trampoline_shared_secret, + .. + } => { + // TODO: what do we want to do with this given we do not wish to propagate it directly? + let _decoded_onion_failure = + onion_error.decode_onion_failure(&self.secp_ctx, &self.logger, &source); + let incoming_trampoline_shared_secret = Some(*incoming_trampoline_shared_secret); + + // TODO: when we receive a failure from a single outgoing trampoline HTLC, we don't + // necessarily want to fail all of our incoming HTLCs back yet. We may have other + // outgoing HTLCs that need to resolve first. This will be tracked in our + // pending_outbound_payments in a followup. + for current_hop_data in previous_hop_data { + let incoming_packet_shared_secret = + ¤t_hop_data.incoming_packet_shared_secret; + let channel_id = ¤t_hop_data.channel_id; + let short_channel_id = ¤t_hop_data.prev_outbound_scid_alias; + let htlc_id = ¤t_hop_data.htlc_id; + let blinded_failure = ¤t_hop_data.blinded_failure; + log_trace!( + WithContext::from(&self.logger, None, Some(*channel_id), Some(*payment_hash)), + "Failing {}HTLC with payment_hash {} backwards from us following Trampoline forwarding failure: {:?}", + if blinded_failure.is_some() { "blinded " } else { "" }, &payment_hash, onion_error + ); + let onion_error = HTLCFailReason::reason( + LocalHTLCFailureReason::TemporaryTrampolineFailure, + Vec::new(), + ); + push_forward_htlcs_failure( + *short_channel_id, + get_htlc_forward_failure( + blinded_failure, + &onion_error, + incoming_packet_shared_secret, + &incoming_trampoline_shared_secret, + &None, + *htlc_id, + ), + ); + } + + // We only want to emit a single event for trampoline failures, so we do it once + // we've failed back all of our incoming HTLCs. + let mut pending_events = self.pending_events.lock().unwrap(); + pending_events.push_back(( + events::Event::HTLCHandlingFailed { + prev_channel_ids: previous_hop_data + .iter() + .map(|prev| prev.channel_id) + .collect(), + failure_type, + failure_reason: Some(onion_error.into()), + }, + None, + )); + }, } } From 9e3aa0542a856b917ccf2712dec97f5b372ff0a0 Mon Sep 17 00:00:00 2001 From: Carla Kirk-Cohen Date: Mon, 9 Feb 2026 14:46:19 +0200 Subject: [PATCH 36/68] ln/refactor: extract channelmonitor recovery to external helper Move recovery logic for `HTLCSource::PreviousHopData` into `channel_monitor_recovery_internal` to prepare for trampoline forward reuse. Co-authored-by: Arik Sosman Co-authored-by: Maurice Poirrier --- lightning/src/ln/channelmanager.rs | 100 ++++++++++++++++++----------- 1 file changed, 61 insertions(+), 39 deletions(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index deee41bb58a..bc5b1030a3c 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -18930,10 +18930,6 @@ impl< let htlc_id = SentHTLCId::from_source(&htlc_source); match htlc_source { HTLCSource::PreviousHopData(prev_hop_data) => { - let pending_forward_matches_htlc = |info: &PendingAddHTLCInfo| { - info.prev_funding_outpoint == prev_hop_data.outpoint - && info.prev_htlc_id == prev_hop_data.htlc_id - }; // If `reconstruct_manager_from_monitors` is set, we always add all inbound committed // HTLCs to `decode_update_add_htlcs` in the above loop, but we need to prune from // those added HTLCs if they were already forwarded to the outbound edge. Otherwise, @@ -18950,42 +18946,17 @@ impl< if !is_channel_closed || reconstruct_manager_from_monitors { continue; } - // The ChannelMonitor is now responsible for this HTLC's - // failure/success and will let us know what its outcome is. If we - // still have an entry for this HTLC in `forward_htlcs_legacy`, - // `pending_intercepted_htlcs_legacy`, or - // `decode_update_add_htlcs_legacy`, we were apparently not persisted - // after the monitor was when forwarding the payment. - dedup_decode_update_add_htlcs( + + reconcile_pending_htlcs_with_monitor( + &mut forward_htlcs_legacy, + &mut pending_events_read, + &mut pending_intercepted_htlcs_legacy, &mut decode_update_add_htlcs_legacy, - &prev_hop_data, - "HTLC was forwarded to the closed channel", - &&logger, - ); - forward_htlcs_legacy.retain(|_, forwards| { - forwards.retain(|forward| { - if let HTLCForwardInfo::AddHTLC(htlc_info) = forward { - if pending_forward_matches_htlc(&htlc_info) { - log_info!(logger, "Removing pending to-forward HTLC with hash {} as it was forwarded to the closed channel {}", - &htlc.payment_hash, &monitor.channel_id()); - false - } else { true } - } else { true } - }); - !forwards.is_empty() - }); - pending_intercepted_htlcs_legacy.retain(|intercepted_id, htlc_info| { - if pending_forward_matches_htlc(&htlc_info) { - log_info!(logger, "Removing pending intercepted HTLC with hash {} as it was forwarded to the closed channel {}", - &htlc.payment_hash, &monitor.channel_id()); - pending_events_read.retain(|(event, _)| { - if let Event::HTLCIntercepted { intercept_id: ev_id, .. } = event { - intercepted_id != ev_id - } else { true } - }); - false - } else { true } - }); + prev_hop_data, + &logger, + htlc.payment_hash, + monitor.channel_id(), + ) }, HTLCSource::TrampolineForward { .. } => todo!(), HTLCSource::OutboundRoute { @@ -19835,6 +19806,57 @@ impl< } } +/// Removes pending HTLC entries that the ChannelMonitor has already taken responsibility for, +/// cleaning up state mismatches that can occur during restart. +fn reconcile_pending_htlcs_with_monitor( + forward_htlcs_legacy: &mut HashMap>, + pending_events_read: &mut VecDeque<(Event, Option)>, + pending_intercepted_htlcs_legacy: &mut HashMap, + decode_update_add_htlcs_legacy: &mut HashMap>, + prev_hop_data: HTLCPreviousHopData, logger: &impl Logger, payment_hash: PaymentHash, + channel_id: ChannelId, +) { + let pending_forward_matches_htlc = |info: &PendingAddHTLCInfo| { + info.prev_funding_outpoint == prev_hop_data.outpoint + && info.prev_htlc_id == prev_hop_data.htlc_id + }; + + // The ChannelMonitor is now responsible for this HTLC's failure/success and will let us know + // what its outcome is. If we still have an entry for this HTLC in `forward_htlcs_legacy`, + // `pending_intercepted_htlcs_legacy`, or `decode_update_add_htlcs_legacy`, we were apparently + // not persisted after the monitor was when forwarding the payment. + dedup_decode_update_add_htlcs( + decode_update_add_htlcs_legacy, + &prev_hop_data, + "HTLC was forwarded to the closed channel", + &&logger, + ); + forward_htlcs_legacy.retain(|_, forwards| { + forwards.retain(|forward| { + if let HTLCForwardInfo::AddHTLC(htlc_info) = forward { + if pending_forward_matches_htlc(&htlc_info) { + log_info!(logger, "Removing pending to-forward HTLC with hash {} as it was forwarded to the closed channel {}", + &payment_hash, &channel_id); + false + } else { true } + } else { true } + }); + !forwards.is_empty() + }); + pending_intercepted_htlcs_legacy.retain(|intercepted_id, htlc_info| { + if pending_forward_matches_htlc(&htlc_info) { + log_info!(logger, "Removing pending intercepted HTLC with hash {} as it was forwarded to the closed channel {}", + &payment_hash, &channel_id); + pending_events_read.retain(|(event, _)| { + if let Event::HTLCIntercepted { intercept_id: ev_id, .. } = event { + intercepted_id != ev_id + } else { true } + }); + false + } else { true } + }); +} + #[cfg(test)] mod tests { use crate::events::{ClosureReason, Event, HTLCHandlingFailureType}; From 7ba44d15d87ce92cacf5415d409e50a28fa083e7 Mon Sep 17 00:00:00 2001 From: Carla Kirk-Cohen Date: Mon, 9 Feb 2026 14:56:08 +0200 Subject: [PATCH 37/68] ln: add channel monitor recovery for trampoline forwards Implement channel monitor recovery for trampoline forwards iterating over all hop data and updating pending forwards. --- lightning/src/ln/channelmanager.rs | 34 +++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index bc5b1030a3c..7bcc8e2b325 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -18958,7 +18958,39 @@ impl< monitor.channel_id(), ) }, - HTLCSource::TrampolineForward { .. } => todo!(), + HTLCSource::TrampolineForward { previous_hop_data, .. } => { + // If `reconstruct_manager_from_monitors` is set, we always add all inbound committed + // HTLCs to `decode_update_add_htlcs` in the above loop, but we need to prune from + // those added HTLCs if they were already forwarded to the outbound edge. Otherwise, + // we'll double-forward. + for prev_hop_data in previous_hop_data.iter() { + if reconstruct_manager_from_monitors { + dedup_decode_update_add_htlcs( + &mut decode_update_add_htlcs, + prev_hop_data, + "HTLC already forwarded to the outbound edge", + &&logger, + ); + } + } + + if !is_channel_closed || reconstruct_manager_from_monitors { + continue; + } + + for prev_hop_data in previous_hop_data { + reconcile_pending_htlcs_with_monitor( + &mut forward_htlcs_legacy, + &mut pending_events_read, + &mut pending_intercepted_htlcs_legacy, + &mut decode_update_add_htlcs_legacy, + prev_hop_data, + &logger, + htlc.payment_hash, + monitor.channel_id(), + ); + } + }, HTLCSource::OutboundRoute { payment_id, session_priv, From ce542ae49b978e56bb4a69f0acb496127f8d2c56 Mon Sep 17 00:00:00 2001 From: Carla Kirk-Cohen Date: Thu, 15 Jan 2026 16:27:50 -0500 Subject: [PATCH 38/68] ln/refactor: move outgoing payment replay code into helper function --- lightning/src/ln/channelmanager.rs | 230 +++++++++++++++++------------ 1 file changed, 132 insertions(+), 98 deletions(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 7bcc8e2b325..38bd00cbecd 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -18161,6 +18161,88 @@ fn dedup_decode_update_add_htlcs( } } +/// Checks if a forwarded HTLC claim needs to be replayed on startup, returning None if it doesn't +/// need to be replayed. When the HTLC needs to be claimed, it returns a bool indicating whether +/// deserialization of should be failed due to missing information. +fn prev_hop_needs_claim_replay( + prev_hop: &HTLCPreviousHopData, payment_preimage: PaymentPreimage, + inbound_edge_monitor: Option<&ChannelMonitor>, + short_to_chan_info: &HashMap, logger: &L, +) -> Option { + // Note that for channels which have gone to chain, `get_all_current_outbound_htlcs` is never + // pruned and always returns a constant set until the monitor is removed/archived. Thus, we + // want to skip replaying claims that have definitely been resolved on-chain. + + // If the inbound monitor is not present, we assume it was fully resolved and properly archived, + // implying this payment had plenty of time to get claimed and we can safely skip any further + // attempts to claim it (they wouldn't succeed anyway as we don't have a monitor against which + // to do so). + let inbound_edge_monitor = inbound_edge_monitor?; + + // Second, if the inbound edge of the payment's monitor has been fully claimed we've had at + // least `ANTI_REORG_DELAY` blocks to get any PaymentForwarded event(s) to the user and assume + // that there's no need to try to replay the claim just for that. + let inbound_edge_balances = inbound_edge_monitor.get_claimable_balances(); + if inbound_edge_balances.is_empty() { + return None; + } + + let mut fail_read = false; + if prev_hop.counterparty_node_id.is_none() { + // We no longer support claiming an HTLC where we don't have the counterparty_node_id + // available if the claim has to go to a closed channel. Its possible we can get away with + // it if the channel is not yet closed, but its by no means a guarantee. + + // Thus, in this case we are a bit more aggressive with our pruning - if we have no use for + // the claim (because the inbound edge of the payment's monitor has already claimed the + // HTLC) we skip trying to replay the claim. + let htlc_payment_hash: PaymentHash = payment_preimage.into(); + let balance_could_incl_htlc = |bal| match bal { + &Balance::ClaimableOnChannelClose { .. } => { + // The channel is still open, assume we can still claim against it + true + }, + &Balance::MaybePreimageClaimableHTLC { payment_hash, .. } => { + payment_hash == htlc_payment_hash + }, + _ => false, + }; + let htlc_may_be_in_balances = inbound_edge_balances.iter().any(balance_could_incl_htlc); + if !htlc_may_be_in_balances { + return None; + } + + // First check if we're absolutely going to fail - if we need to replay this claim to get + // the preimage into the inbound edge monitor but the channel is closed (and thus we'll + // immediately panic if we call claim_funds_from_hop). + if short_to_chan_info.get(&prev_hop.prev_outbound_scid_alias).is_none() { + log_error!(logger, + "We need to replay the HTLC claim for payment_hash {} (preimage {}) but cannot do so as the HTLC was forwarded prior to LDK 0.0.124. \ + All HTLCs that were forwarded by LDK 0.0.123 and prior must be resolved prior to upgrading to LDK 0.1", + htlc_payment_hash, + payment_preimage, + ); + fail_read = true; + } + + // At this point we're confident we need the claim, but the inbound edge channel is still + // live. As long as this remains the case, we can conceivably proceed, but we run some risk + // of panicking at runtime. The user ideally should have read the release notes and we + // wouldn't be here, but we go ahead and let things run in the hope that it'll all just + // work out. + log_error!(logger, + "We need to replay the HTLC claim for payment_hash {} (preimage {}) but don't have all the required information to do so reliably. \ + As long as the channel for the inbound edge of the forward remains open, this may work okay, but we may panic at runtime! \ + All HTLCs that were forwarded by LDK 0.0.123 and prior must be resolved prior to upgrading to LDK 0.1. \ + Continuing anyway, though panics may occur!", + htlc_payment_hash, + payment_preimage, + ); + } + + Some(fail_read) +} + // Implement ReadableArgs for an Arc'd ChannelManager to make it a bit easier to work with the // SipmleArcChannelManager type: impl< @@ -19102,112 +19184,64 @@ impl< // preimages from it which may be needed in upstream channels for forwarded // payments. let mut fail_read = false; - let outbound_claimed_htlcs_iter = monitor.get_all_current_outbound_htlcs() + let outbound_claimed_htlcs_iter = monitor + .get_all_current_outbound_htlcs() .into_iter() .filter_map(|(htlc_source, (htlc, preimage_opt))| { - if let HTLCSource::PreviousHopData(prev_hop) = &htlc_source { - if let Some(payment_preimage) = preimage_opt { - let inbound_edge_monitor = args.channel_monitors.get(&prev_hop.channel_id); - // Note that for channels which have gone to chain, - // `get_all_current_outbound_htlcs` is never pruned and always returns - // a constant set until the monitor is removed/archived. Thus, we - // want to skip replaying claims that have definitely been resolved - // on-chain. - - // If the inbound monitor is not present, we assume it was fully - // resolved and properly archived, implying this payment had plenty - // of time to get claimed and we can safely skip any further - // attempts to claim it (they wouldn't succeed anyway as we don't - // have a monitor against which to do so). - let inbound_edge_monitor = if let Some(monitor) = inbound_edge_monitor { - monitor - } else { - return None; - }; - // Second, if the inbound edge of the payment's monitor has been - // fully claimed we've had at least `ANTI_REORG_DELAY` blocks to - // get any PaymentForwarded event(s) to the user and assume that - // there's no need to try to replay the claim just for that. - let inbound_edge_balances = inbound_edge_monitor.get_claimable_balances(); - if inbound_edge_balances.is_empty() { - return None; - } + let payment_preimage = preimage_opt?; - if prev_hop.counterparty_node_id.is_none() { - // We no longer support claiming an HTLC where we don't have - // the counterparty_node_id available if the claim has to go to - // a closed channel. Its possible we can get away with it if - // the channel is not yet closed, but its by no means a - // guarantee. - - // Thus, in this case we are a bit more aggressive with our - // pruning - if we have no use for the claim (because the - // inbound edge of the payment's monitor has already claimed - // the HTLC) we skip trying to replay the claim. - let htlc_payment_hash: PaymentHash = payment_preimage.into(); - let logger = WithChannelMonitor::from( - &args.logger, - monitor, - Some(htlc_payment_hash), - ); - let balance_could_incl_htlc = |bal| match bal { - &Balance::ClaimableOnChannelClose { .. } => { - // The channel is still open, assume we can still - // claim against it - true - }, - &Balance::MaybePreimageClaimableHTLC { payment_hash, .. } => { - payment_hash == htlc_payment_hash - }, - _ => false, - }; - let htlc_may_be_in_balances = - inbound_edge_balances.iter().any(balance_could_incl_htlc); - if !htlc_may_be_in_balances { - return None; - } + let prev_htlcs = match &htlc_source { + HTLCSource::PreviousHopData(prev_hop) => vec![prev_hop], + // If it was an outbound payment, we've handled it above - if a preimage + // came in and we persisted the `ChannelManager` we either handled it + // and are good to go or the channel force-closed - we don't have to + // handle the channel still live case here. + _ => vec![], + }; - // First check if we're absolutely going to fail - if we need - // to replay this claim to get the preimage into the inbound - // edge monitor but the channel is closed (and thus we'll - // immediately panic if we call claim_funds_from_hop). - if short_to_chan_info.get(&prev_hop.prev_outbound_scid_alias).is_none() { - log_error!(logger, - "We need to replay the HTLC claim for payment_hash {} (preimage {}) but cannot do so as the HTLC was forwarded prior to LDK 0.0.124.\ - All HTLCs that were forwarded by LDK 0.0.123 and prior must be resolved prior to upgrading to LDK 0.1", - htlc_payment_hash, - payment_preimage, - ); - fail_read = true; - } + let prev_htlcs_count = prev_htlcs.len(); + if prev_htlcs_count == 0 { + return None; + } - // At this point we're confident we need the claim, but the - // inbound edge channel is still live. As long as this remains - // the case, we can conceivably proceed, but we run some risk - // of panicking at runtime. The user ideally should have read - // the release notes and we wouldn't be here, but we go ahead - // and let things run in the hope that it'll all just work out. - log_error!(logger, - "We need to replay the HTLC claim for payment_hash {} (preimage {}) but don't have all the required information to do so reliably.\ - As long as the channel for the inbound edge of the forward remains open, this may work okay, but we may panic at runtime!\ - All HTLCs that were forwarded by LDK 0.0.123 and prior must be resolved prior to upgrading to LDK 0.1\ - Continuing anyway, though panics may occur!", - htlc_payment_hash, - payment_preimage, - ); + for prev_hop in prev_htlcs { + let inbound_edge_monitor = + args.channel_monitors.get(&prev_hop.channel_id).copied(); + let logger = WithChannelMonitor::from( + &args.logger, + monitor, + Some(payment_preimage.into()), + ); + if let Some(fail_claim_read) = prev_hop_needs_claim_replay( + prev_hop, + payment_preimage, + inbound_edge_monitor, + &short_to_chan_info, + &logger, + ) { + // We can only fail to read from disk for legacy HTLCs that have + // a single prev_htlc. If we could fail_claim_read for multiple + // prev_htlcs, it wouldn't be correct to exit early on our first + // claimable prev_hop (because a subsequent one may + // fail_claim_read). + if fail_claim_read { + debug_assert!(prev_htlcs_count == 1); } - Some((htlc_source, payment_preimage, htlc.amount_msat, - is_channel_closed, monitor.get_counterparty_node_id(), - monitor.get_funding_txo(), monitor.channel_id())) - } else { None } - } else { - // If it was an outbound payment, we've handled it above - if a preimage - // came in and we persisted the `ChannelManager` we either handled it and - // are good to go or the channel force-closed - we don't have to handle the - // channel still live case here. - None + fail_read |= fail_claim_read; + return Some(( + htlc_source, + payment_preimage, + htlc.amount_msat, + is_channel_closed, + monitor.get_counterparty_node_id(), + monitor.get_funding_txo(), + monitor.channel_id(), + )); + } } + + None }); for tuple in outbound_claimed_htlcs_iter { pending_claims_to_replay.push(tuple); From d606b9a2fc1cc9fc49048f6f3753eda42e9654d0 Mon Sep 17 00:00:00 2001 From: Carla Kirk-Cohen Date: Fri, 16 Jan 2026 13:03:55 -0500 Subject: [PATCH 39/68] ln: handle trampoline claims on restart This commit uses the existing outbound payment claims replay logic to restore trampoline claims. If any single previous hop in a htlc source with multiple previous hops requires claim, we represent this with a single outbound claimed htlc because we assume that *all* of the incoming htlcs are represented in the source, and will be appropriately claimed (rather than submitting multiple claims, which will end up being duplicates of each other). This is the case for trampoline payments, where the htlc_source stores all previous hops. --- lightning/src/ln/channelmanager.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 38bd00cbecd..afd31bd1f22 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -19192,6 +19192,9 @@ impl< let prev_htlcs = match &htlc_source { HTLCSource::PreviousHopData(prev_hop) => vec![prev_hop], + HTLCSource::TrampolineForward { previous_hop_data, .. } => { + previous_hop_data.iter().collect() + }, // If it was an outbound payment, we've handled it above - if a preimage // came in and we persisted the `ChannelManager` we either handled it // and are good to go or the channel force-closed - we don't have to @@ -19230,6 +19233,10 @@ impl< fail_read |= fail_claim_read; return Some(( + // When we have multiple prev_htlcs we assume that they all + // share the same htlc_source which contains all previous hops, + // so we can exit on the first claimable prev_hop because this + // will result in all prev_hops being claimed. htlc_source, payment_preimage, htlc.amount_msat, From ca96430a3f216f4d74c86350fbc9d1246e2a9ca1 Mon Sep 17 00:00:00 2001 From: Carla Kirk-Cohen Date: Tue, 27 Jan 2026 13:49:35 -0500 Subject: [PATCH 40/68] ln: store incoming mpp data in PendingHTLCRouting When we receive a trampoline forward, we need to wait for MPP parts to arrive at our node before we can forward the outgoing payment onwards. This commit threads this information through to our pending htlc struct which we'll use to validate the parts we receive. --- lightning/src/ln/channelmanager.rs | 3 +++ lightning/src/ln/onion_payment.rs | 14 +++++++++----- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index afd31bd1f22..99cfa123d44 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -244,6 +244,8 @@ pub enum PendingHTLCRouting { blinded: Option, /// The absolute CLTV of the inbound HTLC incoming_cltv_expiry: u32, + /// MPP data for accumulating incoming HTLCs before dispatching an outbound payment. + incoming_multipath_data: Option, }, /// The onion indicates that this is a payment for an invoice (supposedly) generated by us. /// @@ -16887,6 +16889,7 @@ impl_writeable_tlv_based_enum!(PendingHTLCRouting, (4, blinded, option), (6, node_id, required), (8, incoming_cltv_expiry, required), + (10, incoming_multipath_data, option), } ); diff --git a/lightning/src/ln/onion_payment.rs b/lightning/src/ln/onion_payment.rs index 5111f6982fe..277b0816749 100644 --- a/lightning/src/ln/onion_payment.rs +++ b/lightning/src/ln/onion_payment.rs @@ -111,6 +111,7 @@ enum RoutingInfo { next_hop_hmac: [u8; 32], shared_secret: SharedSecret, current_path_key: Option, + incoming_multipath_data: Option, }, } @@ -167,14 +168,15 @@ pub(super) fn create_fwd_pending_htlc_info( reason: LocalHTLCFailureReason::InvalidOnionPayload, err_data: Vec::new(), }), - onion_utils::Hop::TrampolineForward { next_trampoline_hop_data, next_trampoline_hop_hmac, new_trampoline_packet_bytes, trampoline_shared_secret, .. } => { + onion_utils::Hop::TrampolineForward { outer_hop_data, next_trampoline_hop_data, next_trampoline_hop_hmac, new_trampoline_packet_bytes, trampoline_shared_secret, .. } => { ( RoutingInfo::Trampoline { next_trampoline: next_trampoline_hop_data.next_trampoline, new_packet_bytes: new_trampoline_packet_bytes, next_hop_hmac: next_trampoline_hop_hmac, shared_secret: trampoline_shared_secret, - current_path_key: None + current_path_key: None, + incoming_multipath_data: outer_hop_data.multipath_trampoline_data, }, next_trampoline_hop_data.amt_to_forward, next_trampoline_hop_data.outgoing_cltv_value, @@ -200,7 +202,8 @@ pub(super) fn create_fwd_pending_htlc_info( new_packet_bytes: new_trampoline_packet_bytes, next_hop_hmac: next_trampoline_hop_hmac, shared_secret: trampoline_shared_secret, - current_path_key: outer_hop_data.current_path_key + current_path_key: outer_hop_data.current_path_key, + incoming_multipath_data: outer_hop_data.multipath_trampoline_data, }, amt_to_forward, outgoing_cltv_value, @@ -233,7 +236,7 @@ pub(super) fn create_fwd_pending_htlc_info( }), } } - RoutingInfo::Trampoline { next_trampoline, new_packet_bytes, next_hop_hmac, shared_secret, current_path_key } => { + RoutingInfo::Trampoline { next_trampoline, new_packet_bytes, next_hop_hmac, shared_secret, current_path_key, incoming_multipath_data: multipath_trampoline_data } => { let next_trampoline_packet_pubkey = match next_packet_pubkey_opt { Some(Ok(pubkey)) => pubkey, _ => return Err(InboundHTLCErr { @@ -260,7 +263,8 @@ pub(super) fn create_fwd_pending_htlc_info( failure: intro_node_blinding_point .map(|_| BlindedFailure::FromIntroductionNode) .unwrap_or(BlindedFailure::FromBlindedNode), - }) + }), + incoming_multipath_data: multipath_trampoline_data, } } }; From 0386b02b6b7bd0dcb43306195d45863a0ce6bd12 Mon Sep 17 00:00:00 2001 From: Carla Kirk-Cohen Date: Tue, 10 Feb 2026 08:33:25 +0200 Subject: [PATCH 41/68] ln: store next trampoline amount and cltv in PendingHTLCRouting When we're forwarding a trampoline payment, we need to remember the amount and CLTV that the next trampoline is expecting. --- lightning/src/ln/channelmanager.rs | 6 ++++++ lightning/src/ln/onion_payment.rs | 10 +++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 99cfa123d44..970a5af9bac 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -246,6 +246,10 @@ pub enum PendingHTLCRouting { incoming_cltv_expiry: u32, /// MPP data for accumulating incoming HTLCs before dispatching an outbound payment. incoming_multipath_data: Option, + /// The amount that the next trampoline is expecting to receive. + next_trampoline_amt_msat: u64, + /// The CLTV expiry height that the next trampoline is expecting to receive. + next_trampoline_cltv_expiry: u32, }, /// The onion indicates that this is a payment for an invoice (supposedly) generated by us. /// @@ -16890,6 +16894,8 @@ impl_writeable_tlv_based_enum!(PendingHTLCRouting, (6, node_id, required), (8, incoming_cltv_expiry, required), (10, incoming_multipath_data, option), + (12, next_trampoline_amt_msat, required), + (14, next_trampoline_cltv_expiry, required), } ); diff --git a/lightning/src/ln/onion_payment.rs b/lightning/src/ln/onion_payment.rs index 277b0816749..884439431e2 100644 --- a/lightning/src/ln/onion_payment.rs +++ b/lightning/src/ln/onion_payment.rs @@ -112,6 +112,8 @@ enum RoutingInfo { shared_secret: SharedSecret, current_path_key: Option, incoming_multipath_data: Option, + next_trampoline_amt_msat: u64, + next_trampoline_cltv: u32, }, } @@ -177,6 +179,8 @@ pub(super) fn create_fwd_pending_htlc_info( shared_secret: trampoline_shared_secret, current_path_key: None, incoming_multipath_data: outer_hop_data.multipath_trampoline_data, + next_trampoline_amt_msat: next_trampoline_hop_data.amt_to_forward, + next_trampoline_cltv: next_trampoline_hop_data.outgoing_cltv_value, }, next_trampoline_hop_data.amt_to_forward, next_trampoline_hop_data.outgoing_cltv_value, @@ -204,6 +208,8 @@ pub(super) fn create_fwd_pending_htlc_info( shared_secret: trampoline_shared_secret, current_path_key: outer_hop_data.current_path_key, incoming_multipath_data: outer_hop_data.multipath_trampoline_data, + next_trampoline_amt_msat: next_hop_amount, + next_trampoline_cltv: next_hop_cltv, }, amt_to_forward, outgoing_cltv_value, @@ -236,7 +242,7 @@ pub(super) fn create_fwd_pending_htlc_info( }), } } - RoutingInfo::Trampoline { next_trampoline, new_packet_bytes, next_hop_hmac, shared_secret, current_path_key, incoming_multipath_data: multipath_trampoline_data } => { + RoutingInfo::Trampoline { next_trampoline, new_packet_bytes, next_hop_hmac, shared_secret, current_path_key, incoming_multipath_data: multipath_trampoline_data, next_trampoline_amt_msat: next_hop_amount, next_trampoline_cltv: next_hop_cltv} => { let next_trampoline_packet_pubkey = match next_packet_pubkey_opt { Some(Ok(pubkey)) => pubkey, _ => return Err(InboundHTLCErr { @@ -265,6 +271,8 @@ pub(super) fn create_fwd_pending_htlc_info( .unwrap_or(BlindedFailure::FromBlindedNode), }), incoming_multipath_data: multipath_trampoline_data, + next_trampoline_amt_msat: next_hop_amount, + next_trampoline_cltv_expiry: next_hop_cltv, } } }; From 8cb8247a1b8f9d390001d761f940f3d678f525b5 Mon Sep 17 00:00:00 2001 From: Carla Kirk-Cohen Date: Tue, 10 Feb 2026 09:37:51 +0200 Subject: [PATCH 42/68] ln: use outer onion cltv values in PendingHTLCInfo for trampoline When we are a trampoline node receiving an incoming HTLC (which is not MPP), we need access to our outer onion's amount_to_forward to check that we have been forwarded the correct amount. We can't use the amount in the inner onion, because that contains our fee budget - somebody could forward us less than we were intended to receive, and provided it is within the trampoline fee budget we wouldn't know. In this commit we set our outer onion values in PendingHTLCInfo and track the trampoline values separately so that we can use both where appropriate. --- lightning/src/ln/onion_payment.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lightning/src/ln/onion_payment.rs b/lightning/src/ln/onion_payment.rs index 884439431e2..6a6fa5442fe 100644 --- a/lightning/src/ln/onion_payment.rs +++ b/lightning/src/ln/onion_payment.rs @@ -182,14 +182,14 @@ pub(super) fn create_fwd_pending_htlc_info( next_trampoline_amt_msat: next_trampoline_hop_data.amt_to_forward, next_trampoline_cltv: next_trampoline_hop_data.outgoing_cltv_value, }, - next_trampoline_hop_data.amt_to_forward, - next_trampoline_hop_data.outgoing_cltv_value, + outer_hop_data.amt_to_forward, + outer_hop_data.outgoing_cltv_value, None, None ) }, onion_utils::Hop::TrampolineBlindedForward { outer_hop_data, next_trampoline_hop_data, next_trampoline_hop_hmac, new_trampoline_packet_bytes, trampoline_shared_secret, .. } => { - let (amt_to_forward, outgoing_cltv_value) = check_blinded_forward( + let (next_hop_amount, next_hop_cltv) = check_blinded_forward( msg.amount_msat, msg.cltv_expiry, &next_trampoline_hop_data.payment_relay, &next_trampoline_hop_data.payment_constraints, &next_trampoline_hop_data.features ).map_err(|()| { // We should be returning malformed here if `msg.blinding_point` is set, but this is @@ -211,8 +211,8 @@ pub(super) fn create_fwd_pending_htlc_info( next_trampoline_amt_msat: next_hop_amount, next_trampoline_cltv: next_hop_cltv, }, - amt_to_forward, - outgoing_cltv_value, + outer_hop_data.amt_to_forward, + outer_hop_data.outgoing_cltv_value, next_trampoline_hop_data.intro_node_blinding_point, next_trampoline_hop_data.next_blinding_override ) From 9bdf9437be51b3971ed8f0cf43385222d103af8c Mon Sep 17 00:00:00 2001 From: Carla Kirk-Cohen Date: Thu, 12 Feb 2026 12:34:15 +0200 Subject: [PATCH 43/68] ln: use outer onion values for trampoline NextPacketDetails When we receive trampoline payments, we first want to validate the values in our outer onion to ensure that we've been given the amount/ expiry that the sender was intending us to receive to make sure that forwarding nodes haven't sent us less than they should. --- lightning/src/ln/onion_payment.rs | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/lightning/src/ln/onion_payment.rs b/lightning/src/ln/onion_payment.rs index 6a6fa5442fe..a359d9c3882 100644 --- a/lightning/src/ln/onion_payment.rs +++ b/lightning/src/ln/onion_payment.rs @@ -695,33 +695,24 @@ pub(super) fn decode_incoming_update_add_htlc_onion { + onion_utils::Hop::TrampolineForward { next_trampoline_hop_data: msgs::InboundTrampolineForwardPayload { next_trampoline, .. }, ref outer_hop_data, trampoline_shared_secret, incoming_trampoline_public_key, .. } => { let next_trampoline_packet_pubkey = onion_utils::next_hop_pubkey(secp_ctx, incoming_trampoline_public_key, &trampoline_shared_secret.secret_bytes()); Some(NextPacketDetails { next_packet_pubkey: next_trampoline_packet_pubkey, outgoing_connector: HopConnector::Trampoline(next_trampoline), - outgoing_amt_msat: amt_to_forward, - outgoing_cltv_value, + outgoing_amt_msat: outer_hop_data.amt_to_forward, + outgoing_cltv_value: outer_hop_data.outgoing_cltv_value, }) } - onion_utils::Hop::TrampolineBlindedForward { next_trampoline_hop_data: msgs::InboundTrampolineBlindedForwardPayload { next_trampoline, ref payment_relay, ref payment_constraints, ref features, .. }, outer_shared_secret, trampoline_shared_secret, incoming_trampoline_public_key, .. } => { - let (amt_to_forward, outgoing_cltv_value) = match check_blinded_forward( - msg.amount_msat, msg.cltv_expiry, &payment_relay, &payment_constraints, &features - ) { - Ok((amt, cltv)) => (amt, cltv), - Err(()) => { - return encode_relay_error("Underflow calculating outbound amount or cltv value for blinded trampoline forward", - LocalHTLCFailureReason::InvalidOnionBlinding, outer_shared_secret.secret_bytes(), Some(trampoline_shared_secret.secret_bytes()), &[0; 32]); - } - }; + onion_utils::Hop::TrampolineBlindedForward { next_trampoline_hop_data: msgs::InboundTrampolineBlindedForwardPayload { next_trampoline, .. }, ref outer_hop_data, trampoline_shared_secret, incoming_trampoline_public_key, .. } => { let next_trampoline_packet_pubkey = onion_utils::next_hop_pubkey(secp_ctx, incoming_trampoline_public_key, &trampoline_shared_secret.secret_bytes()); Some(NextPacketDetails { next_packet_pubkey: next_trampoline_packet_pubkey, outgoing_connector: HopConnector::Trampoline(next_trampoline), - outgoing_amt_msat: amt_to_forward, - outgoing_cltv_value, + outgoing_amt_msat: outer_hop_data.amt_to_forward, + outgoing_cltv_value: outer_hop_data.outgoing_cltv_value, }) } _ => None From 301e51d314326a2ffdd4c50c1f06704d84f248ac Mon Sep 17 00:00:00 2001 From: Carla Kirk-Cohen Date: Fri, 16 Jan 2026 14:38:21 -0500 Subject: [PATCH 44/68] ln: add trampoline forward info to PendingOutboundPayment::Retryable Use even persistence value because we can't downgrade with a trampoline payment in flight, we'll fail to claim the appropriate incoming HTLCs. We track previous_hop_data in `TrampolineForwardInfo` so that we have it on hand in our `OutboundPayment::Retryable`to build `HTLCSource` for our retries. --- lightning/src/ln/outbound_payment.rs | 54 +++++++++++++++++++++++++--- 1 file changed, 50 insertions(+), 4 deletions(-) diff --git a/lightning/src/ln/outbound_payment.rs b/lightning/src/ln/outbound_payment.rs index 38000b40e74..cb379b4ef67 100644 --- a/lightning/src/ln/outbound_payment.rs +++ b/lightning/src/ln/outbound_payment.rs @@ -11,17 +11,17 @@ use bitcoin::hashes::sha256::Hash as Sha256; use bitcoin::hashes::Hash; -use bitcoin::secp256k1::{self, Secp256k1, SecretKey}; +use bitcoin::secp256k1::{self, PublicKey, Secp256k1, SecretKey}; use lightning_invoice::Bolt11Invoice; use crate::blinded_path::{IntroductionNode, NodeIdLookUp}; use crate::events::{self, PaidBolt12Invoice, PaymentFailureReason}; use crate::ln::channel_state::ChannelDetails; use crate::ln::channelmanager::{ - EventCompletionAction, HTLCSource, OptionalBolt11PaymentParams, PaymentCompleteUpdate, - PaymentId, + EventCompletionAction, HTLCPreviousHopData, HTLCSource, OptionalBolt11PaymentParams, + PaymentCompleteUpdate, PaymentId, }; -use crate::ln::msgs::DecodeError; +use crate::ln::msgs::{DecodeError, TrampolineOnionPacket}; use crate::ln::onion_utils; use crate::ln::onion_utils::{DecodedOnionFailure, HTLCFailReason}; use crate::offers::invoice::{Bolt12Invoice, DerivedSigningPubkey, InvoiceBuilder}; @@ -127,6 +127,9 @@ pub(crate) enum PendingOutboundPayment { // Storing the BOLT 12 invoice here to allow Proof of Payment after // the payment is made. bolt12_invoice: Option, + // Storing forward information for trampoline payments in order to build next hop info + // or build error or claims to the origin. + trampoline_forward_info: Option, custom_tlvs: Vec<(u64, Vec)>, pending_amt_msat: u64, /// Used to track the fee paid. Present iff the payment was serialized on 0.0.103+. @@ -167,6 +170,46 @@ pub(crate) enum PendingOutboundPayment { }, } +#[derive(Clone)] +pub(crate) struct NextTrampolineHopInfo { + /// The Trampoline packet to include for the next Trampoline hop. + pub(crate) onion_packet: TrampolineOnionPacket, + /// If blinded, the current_path_key to set at the next Trampoline hop. + pub(crate) blinding_point: Option, + /// The amount that the next trampoline is expecting to receive. + pub(crate) amount_msat: u64, + /// The cltv expiry height that the next trampoline is expecting. + pub(crate) cltv_expiry_height: u32, +} + +impl_writeable_tlv_based!(NextTrampolineHopInfo, { + (1, onion_packet, required), + (3, blinding_point, option), + (5, amount_msat, required), + (7, cltv_expiry_height, required), +}); + +#[derive(Clone)] +pub(crate) struct TrampolineForwardInfo { + /// Information necessary to construct the onion packet for the next Trampoline hop. + pub(crate) next_hop_info: NextTrampolineHopInfo, + /// The incoming HTLCs that were forwarded to us, which need to be settled or failed once + /// our outbound payment has been completed. + pub(crate) previous_hop_data: Vec, + /// The shared secret from the incoming trampoline onion, needed for error encryption. + pub(crate) incoming_trampoline_shared_secret: [u8; 32], + /// The forwarding fee charged for this trampoline payment, persisted here so that we don't + /// need to look up the value of all our incoming/outgoing payments to calculate fee. + pub(crate) forwading_fee_msat: u64, +} + +impl_writeable_tlv_based!(TrampolineForwardInfo, { + (1, next_hop_info, required), + (3, previous_hop_data, required_vec), + (5, incoming_trampoline_shared_secret, required), + (7, forwading_fee_msat, required), +}); + #[derive(Clone)] pub(crate) struct RetryableInvoiceRequest { pub(crate) invoice_request: InvoiceRequest, @@ -2006,6 +2049,7 @@ impl OutboundPayments { keysend_preimage, invoice_request, bolt12_invoice, + trampoline_forward_info: None, custom_tlvs: recipient_onion.custom_tlvs, starting_block_height: best_block_height, total_msat: route.get_total_amount(), @@ -2713,6 +2757,7 @@ impl OutboundPayments { keysend_preimage: None, // only used for retries, and we'll never retry on startup invoice_request: None, // only used for retries, and we'll never retry on startup bolt12_invoice: None, // only used for retries, and we'll never retry on startup! + trampoline_forward_info: None, // only used for retries, and we'll never retry on startup custom_tlvs: Vec::new(), // only used for retries, and we'll never retry on startup pending_amt_msat: path_amt, pending_fee_msat: Some(path_fee), @@ -2816,6 +2861,7 @@ impl_writeable_tlv_based_enum_upgradable!(PendingOutboundPayment, } })), (13, invoice_request, option), + (14, trampoline_forward_info, option), (15, bolt12_invoice, option), (not_written, retry_strategy, (static_value, None)), (not_written, attempts, (static_value, PaymentAttempts::new())), From 18bd5232753374ed53f41c4433d71a719eaffb44 Mon Sep 17 00:00:00 2001 From: Carla Kirk-Cohen Date: Tue, 10 Feb 2026 08:46:40 +0200 Subject: [PATCH 45/68] ln: thread trampoline routing information through payment methods --- lightning/src/ln/channelmanager.rs | 2 + lightning/src/ln/outbound_payment.rs | 57 ++++++++++++++++------------ 2 files changed, 34 insertions(+), 25 deletions(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 970a5af9bac..16e489065f4 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -5239,6 +5239,7 @@ impl< keysend_preimage, invoice_request: None, bolt12_invoice: None, + trampoline_forward_info: None, session_priv_bytes, hold_htlc_at_next_hop: false, }) @@ -5256,6 +5257,7 @@ impl< bolt12_invoice, session_priv_bytes, hold_htlc_at_next_hop, + .. } = args; // The top-level caller should hold the total_consistency_lock read lock. debug_assert!(self.total_consistency_lock.try_write().is_err()); diff --git a/lightning/src/ln/outbound_payment.rs b/lightning/src/ln/outbound_payment.rs index cb379b4ef67..772a61fe803 100644 --- a/lightning/src/ln/outbound_payment.rs +++ b/lightning/src/ln/outbound_payment.rs @@ -964,6 +964,7 @@ pub(super) struct SendAlongPathArgs<'a> { pub keysend_preimage: &'a Option, pub invoice_request: Option<&'a InvoiceRequest>, pub bolt12_invoice: Option<&'a PaidBolt12Invoice>, + pub trampoline_forward_info: Option<&'a TrampolineForwardInfo>, pub session_priv_bytes: [u8; 32], pub hold_htlc_at_next_hop: bool, } @@ -1226,7 +1227,7 @@ impl OutboundPayments { hash_map::Entry::Occupied(entry) => match entry.get() { PendingOutboundPayment::InvoiceReceived { .. } => { let (retryable_payment, onion_session_privs) = Self::create_pending_payment( - payment_hash, recipient_onion.clone(), keysend_preimage, None, Some(bolt12_invoice.clone()), &route, + payment_hash, recipient_onion.clone(), keysend_preimage, None, Some(bolt12_invoice.clone()), None, &route, Some(retry_strategy), payment_params, entropy_source, best_block_height, ); *entry.into_mut() = retryable_payment; @@ -1237,8 +1238,8 @@ impl OutboundPayments { invoice_request } else { unreachable!() }; let (retryable_payment, onion_session_privs) = Self::create_pending_payment( - payment_hash, recipient_onion.clone(), keysend_preimage, Some(invreq), Some(bolt12_invoice.clone()), &route, - Some(retry_strategy), payment_params, entropy_source, best_block_height + payment_hash, recipient_onion.clone(), keysend_preimage, Some(invreq), Some(bolt12_invoice.clone()), + None, &route, Some(retry_strategy), payment_params, entropy_source, best_block_height ); outbounds.insert(payment_id, retryable_payment); onion_session_privs @@ -1250,8 +1251,8 @@ impl OutboundPayments { core::mem::drop(outbounds); let result = self.pay_route_internal( - &route, payment_hash, &recipient_onion, keysend_preimage, invoice_request, Some(&bolt12_invoice), payment_id, - &onion_session_privs, hold_htlcs_at_next_hop, node_signer, + &route, payment_hash, &recipient_onion, keysend_preimage, invoice_request, Some(&bolt12_invoice), + None, payment_id, &onion_session_privs, hold_htlcs_at_next_hop, node_signer, best_block_height, &send_payment_along_path ); log_info!( @@ -1634,7 +1635,7 @@ impl OutboundPayments { let onion_session_privs = self.add_new_pending_payment(payment_hash, recipient_onion.clone(), payment_id, keysend_preimage, &route, Some(retry_strategy), - Some(route_params.payment_params.clone()), entropy_source, best_block_height, None) + Some(route_params.payment_params.clone()), entropy_source, best_block_height, None, None) .map_err(|_| { log_error!(logger, "Payment with id {} is already pending. New payment had payment hash {}", payment_id, payment_hash); @@ -1642,7 +1643,7 @@ impl OutboundPayments { })?; let res = self.pay_route_internal(&route, payment_hash, &recipient_onion, - keysend_preimage, None, None, payment_id, &onion_session_privs, false, node_signer, + keysend_preimage, None, None, None, payment_id, &onion_session_privs, false, node_signer, best_block_height, &send_payment_along_path); log_info!(logger, "Sending payment with id {} and hash {} returned {:?}", payment_id, payment_hash, res); @@ -1709,14 +1710,14 @@ impl OutboundPayments { } } } - let (recipient_onion, keysend_preimage, onion_session_privs, invoice_request, bolt12_invoice) = { + let (recipient_onion, keysend_preimage, onion_session_privs, invoice_request, bolt12_invoice, trampoline_forward_info) = { let mut outbounds = self.pending_outbound_payments.lock().unwrap(); match outbounds.entry(payment_id) { hash_map::Entry::Occupied(mut payment) => { match payment.get() { PendingOutboundPayment::Retryable { total_msat, keysend_preimage, payment_secret, payment_metadata, - custom_tlvs, pending_amt_msat, invoice_request, onion_total_msat, .. + custom_tlvs, pending_amt_msat, invoice_request, trampoline_forward_info, onion_total_msat, .. } => { const RETRY_OVERFLOW_PERCENTAGE: u64 = 10; let retry_amt_msat = route.get_total_amount(); @@ -1732,6 +1733,7 @@ impl OutboundPayments { return } + let trampoline_forward_info = trampoline_forward_info.clone(); let recipient_onion = RecipientOnionFields { payment_secret: *payment_secret, payment_metadata: payment_metadata.clone(), @@ -1753,7 +1755,7 @@ impl OutboundPayments { payment.get_mut().increment_attempts(); let bolt12_invoice = payment.get().bolt12_invoice(); - (recipient_onion, keysend_preimage, onion_session_privs, invoice_request, bolt12_invoice.cloned()) + (recipient_onion, keysend_preimage, onion_session_privs, invoice_request, bolt12_invoice.cloned(), trampoline_forward_info) }, PendingOutboundPayment::Legacy { .. } => { log_error!(logger, "Unable to retry payments that were initially sent on LDK versions prior to 0.0.102"); @@ -1793,8 +1795,9 @@ impl OutboundPayments { } }; let res = self.pay_route_internal(&route, payment_hash, &recipient_onion, keysend_preimage, - invoice_request.as_ref(), bolt12_invoice.as_ref(), payment_id, - &onion_session_privs, false, node_signer, best_block_height, &send_payment_along_path); + invoice_request.as_ref(), bolt12_invoice.as_ref(), trampoline_forward_info.as_ref(), + payment_id, &onion_session_privs, false, node_signer, best_block_height, + &send_payment_along_path); log_info!(logger, "Result retrying payment id {}: {:?}", &payment_id, res); if let Err(e) = res { self.handle_pay_route_err( @@ -1945,14 +1948,14 @@ impl OutboundPayments { RecipientOnionFields::secret_only(payment_secret, route.get_total_amount()); let onion_session_privs = self.add_new_pending_payment(payment_hash, recipient_onion_fields.clone(), payment_id, None, &route, None, None, - entropy_source, best_block_height, None + entropy_source, best_block_height, None, None, ).map_err(|e| { debug_assert!(matches!(e, PaymentSendFailure::DuplicatePayment)); ProbeSendFailure::DuplicateProbe })?; match self.pay_route_internal(&route, payment_hash, &recipient_onion_fields, - None, None, None, payment_id, &onion_session_privs, false, node_signer, + None, None, None, None, payment_id, &onion_session_privs, false, node_signer, best_block_height, &send_payment_along_path ) { Ok(()) => Ok((payment_hash, payment_id)), @@ -2000,7 +2003,7 @@ impl OutboundPayments { &self, payment_hash: PaymentHash, recipient_onion: RecipientOnionFields, payment_id: PaymentId, route: &Route, retry_strategy: Option, entropy_source: &ES, best_block_height: u32 ) -> Result, PaymentSendFailure> { - self.add_new_pending_payment(payment_hash, recipient_onion, payment_id, None, route, retry_strategy, None, entropy_source, best_block_height, None) + self.add_new_pending_payment(payment_hash, recipient_onion, payment_id, None, route, retry_strategy, None, entropy_source, best_block_height, None, None) } #[rustfmt::skip] @@ -2008,15 +2011,15 @@ impl OutboundPayments { &self, payment_hash: PaymentHash, recipient_onion: RecipientOnionFields, payment_id: PaymentId, keysend_preimage: Option, route: &Route, retry_strategy: Option, payment_params: Option, entropy_source: &ES, best_block_height: u32, - bolt12_invoice: Option + bolt12_invoice: Option, trampoline_forward_info: Option ) -> Result, PaymentSendFailure> { let mut pending_outbounds = self.pending_outbound_payments.lock().unwrap(); match pending_outbounds.entry(payment_id) { hash_map::Entry::Occupied(_) => Err(PaymentSendFailure::DuplicatePayment), hash_map::Entry::Vacant(entry) => { let (payment, onion_session_privs) = Self::create_pending_payment( - payment_hash, recipient_onion, keysend_preimage, None, bolt12_invoice, route, retry_strategy, - payment_params, entropy_source, best_block_height + payment_hash, recipient_onion, keysend_preimage, None, bolt12_invoice, trampoline_forward_info, + route, retry_strategy, payment_params, entropy_source, best_block_height ); entry.insert(payment); Ok(onion_session_privs) @@ -2028,7 +2031,8 @@ impl OutboundPayments { fn create_pending_payment( payment_hash: PaymentHash, recipient_onion: RecipientOnionFields, keysend_preimage: Option, invoice_request: Option, - bolt12_invoice: Option, route: &Route, retry_strategy: Option, + bolt12_invoice: Option, trampoline_forward_info: Option, + route: &Route, retry_strategy: Option, payment_params: Option, entropy_source: &ES, best_block_height: u32 ) -> (PendingOutboundPayment, Vec<[u8; 32]>) { let mut onion_session_privs = Vec::with_capacity(route.paths.len()); @@ -2049,7 +2053,7 @@ impl OutboundPayments { keysend_preimage, invoice_request, bolt12_invoice, - trampoline_forward_info: None, + trampoline_forward_info, custom_tlvs: recipient_onion.custom_tlvs, starting_block_height: best_block_height, total_msat: route.get_total_amount(), @@ -2199,7 +2203,7 @@ impl OutboundPayments { fn pay_route_internal( &self, route: &Route, payment_hash: PaymentHash, recipient_onion: &RecipientOnionFields, keysend_preimage: Option, invoice_request: Option<&InvoiceRequest>, bolt12_invoice: Option<&PaidBolt12Invoice>, - payment_id: PaymentId, onion_session_privs: &Vec<[u8; 32]>, + trampoline_forward_info: Option<&TrampolineForwardInfo>, payment_id: PaymentId, onion_session_privs: &Vec<[u8; 32]>, hold_htlcs_at_next_hop: bool, node_signer: &NS, best_block_height: u32, send_payment_along_path: &F ) -> Result<(), PaymentSendFailure> where @@ -2213,6 +2217,9 @@ impl OutboundPayments { { return Err(PaymentSendFailure::ParameterError(APIError::APIMisuseError{err: "Payment secret is required for multi-path payments".to_owned()})); } + if trampoline_forward_info.is_some() && keysend_preimage.is_some() { + return Err(PaymentSendFailure::ParameterError(APIError::APIMisuseError{err: "Trampoline forwards cannot include keysend preimage".to_owned()})); + } let our_node_id = node_signer.get_node_id(Recipient::Node).unwrap(); // TODO no unwrap let mut path_errs = Vec::with_capacity(route.paths.len()); 'path_check: for path in route.paths.iter() { @@ -2248,7 +2255,7 @@ impl OutboundPayments { let path_res = send_payment_along_path(SendAlongPathArgs { path: &path, payment_hash: &payment_hash, recipient_onion, cur_height, payment_id, keysend_preimage: &keysend_preimage, invoice_request, - bolt12_invoice, hold_htlc_at_next_hop: hold_htlcs_at_next_hop, + bolt12_invoice, trampoline_forward_info, hold_htlc_at_next_hop: hold_htlcs_at_next_hop, session_priv_bytes: *session_priv_bytes }); results.push(path_res); @@ -2315,7 +2322,7 @@ impl OutboundPayments { F: Fn(SendAlongPathArgs) -> Result<(), APIError>, { self.pay_route_internal(route, payment_hash, &recipient_onion, - keysend_preimage, None, None, payment_id, &onion_session_privs, + keysend_preimage, None, None, None, payment_id, &onion_session_privs, false, node_signer, best_block_height, &send_payment_along_path) .map_err(|e| { self.remove_outbound_if_all_failed(payment_id, &e); e }) } @@ -3031,7 +3038,7 @@ mod tests { outbound_payments.add_new_pending_payment(PaymentHash([0; 32]), RecipientOnionFields::spontaneous_empty(0), PaymentId([0; 32]), None, &Route { paths: vec![], route_params: None }, Some(Retry::Attempts(1)), Some(expired_route_params.payment_params.clone()), - &&keys_manager, 0, None).unwrap(); + &&keys_manager, 0, None, None).unwrap(); outbound_payments.find_route_and_send_payment( PaymentHash([0; 32]), PaymentId([0; 32]), expired_route_params, &&router, vec![], &|| InFlightHtlcs::new(), &&keys_manager, &&keys_manager, 0, &pending_events, @@ -3077,7 +3084,7 @@ mod tests { outbound_payments.add_new_pending_payment(PaymentHash([0; 32]), RecipientOnionFields::spontaneous_empty(0), PaymentId([0; 32]), None, &Route { paths: vec![], route_params: None }, Some(Retry::Attempts(1)), Some(route_params.payment_params.clone()), - &&keys_manager, 0, None).unwrap(); + &&keys_manager, 0, None, None).unwrap(); outbound_payments.find_route_and_send_payment( PaymentHash([0; 32]), PaymentId([0; 32]), route_params, &&router, vec![], &|| InFlightHtlcs::new(), &&keys_manager, &&keys_manager, 0, &pending_events, From b5377ab58e1e6bd804ac3e82c78e7f9960741ed4 Mon Sep 17 00:00:00 2001 From: Carla Kirk-Cohen Date: Tue, 10 Feb 2026 08:53:55 +0200 Subject: [PATCH 46/68] ln: add blinding point to new_trampoline_entry When we are forwading as a trampoline within a blinded path, we need to be able to set a blinding point in the outer onion so that the next blinded trampoline can use it to decrypt its inner onion. This is only used for relaying nodes in the blinded path, because the introduction node's inner onion is encrypted using its node_id (unblinded) pubkey so it can retrieve the path key from inside its trampoline onion. Relaying nodes node_id is unknown to the original sender, so their inner onion is encrypted with their blinded identity. Relaying trampoline nodes therefore have to include the path key in the outer payload so that the inner onion can be decrypted, which in turn contains their blinded data for forwarding. This isn't used for the case where we're the sending node, because all we have to do is include the blinding point for the introduction node. For relaying nodes, we just put their encrypted data inside of their trampoline payload, relying on nodes in the blinded path to pass the blinding point along. --- lightning/src/ln/blinded_payment_tests.rs | 4 +- lightning/src/ln/msgs.rs | 1 - lightning/src/ln/onion_route_tests.rs | 2 +- lightning/src/ln/onion_utils.rs | 51 +++++++++++++++-------- 4 files changed, 37 insertions(+), 21 deletions(-) diff --git a/lightning/src/ln/blinded_payment_tests.rs b/lightning/src/ln/blinded_payment_tests.rs index 95f24d59d16..f854a411dff 100644 --- a/lightning/src/ln/blinded_payment_tests.rs +++ b/lightning/src/ln/blinded_payment_tests.rs @@ -2191,7 +2191,7 @@ fn test_trampoline_forward_payload_encoded_as_receive() { ).unwrap(); let recipient_onion_fields = RecipientOnionFields::spontaneous_empty(outer_total_msat); - let (outer_payloads, _, _) = onion_utils::test_build_onion_payloads(&route.paths[0], &recipient_onion_fields, 32, &None, None, Some(trampoline_packet)).unwrap(); + let (outer_payloads, _, _) = onion_utils::test_build_onion_payloads(&route.paths[0], &recipient_onion_fields, 32, &None, None, Some((trampoline_packet, None))).unwrap(); let outer_onion_keys = onion_utils::construct_onion_keys(&secp_ctx, &route.clone().paths[0], &outer_session_priv); let outer_packet = onion_utils::construct_onion_packet( outer_payloads, @@ -2530,7 +2530,7 @@ fn replacement_onion( starting_htlc_offset, &None, None, - Some(trampoline_packet), + Some((trampoline_packet, None)), ) .unwrap(); assert_eq!(outer_payloads.len(), 2); diff --git a/lightning/src/ln/msgs.rs b/lightning/src/ln/msgs.rs index 67f7807a487..26be79915ef 100644 --- a/lightning/src/ln/msgs.rs +++ b/lightning/src/ln/msgs.rs @@ -2659,7 +2659,6 @@ mod fuzzy_internal_msgs { /// This is used for Trampoline hops that are not the blinded path intro hop. /// We would only ever construct this variant when we are a Trampoline node forwarding a /// payment along a blinded path. - #[allow(unused)] BlindedTrampolineEntrypoint { amt_to_forward: u64, outgoing_cltv_value: u32, diff --git a/lightning/src/ln/onion_route_tests.rs b/lightning/src/ln/onion_route_tests.rs index f5743134a49..e15bd8b74df 100644 --- a/lightning/src/ln/onion_route_tests.rs +++ b/lightning/src/ln/onion_route_tests.rs @@ -2043,7 +2043,7 @@ fn test_trampoline_onion_payload_assembly_values() { cur_height, &None, None, - Some(trampoline_packet), + Some((trampoline_packet, None)), ) .unwrap(); assert_eq!(outer_payloads.len(), 2); diff --git a/lightning/src/ln/onion_utils.rs b/lightning/src/ln/onion_utils.rs index cc4cb0b2822..bcf531a7cb4 100644 --- a/lightning/src/ln/onion_utils.rs +++ b/lightning/src/ln/onion_utils.rs @@ -206,7 +206,7 @@ trait OnionPayload<'a, 'b> { ) -> Self; fn new_trampoline_entry( amt_to_forward: u64, outgoing_cltv_value: u32, recipient_onion: &'a RecipientOnionFields, - packet: msgs::TrampolineOnionPacket, + packet: msgs::TrampolineOnionPacket, blinding_point: Option, ) -> Result; } impl<'a, 'b> OnionPayload<'a, 'b> for msgs::OutboundOnionPayload<'a> { @@ -258,19 +258,29 @@ impl<'a, 'b> OnionPayload<'a, 'b> for msgs::OutboundOnionPayload<'a> { fn new_trampoline_entry( amt_to_forward: u64, outgoing_cltv_value: u32, recipient_onion: &'a RecipientOnionFields, - packet: msgs::TrampolineOnionPacket, + packet: msgs::TrampolineOnionPacket, blinding_point: Option, ) -> Result { - Ok(Self::TrampolineEntrypoint { - amt_to_forward, - outgoing_cltv_value, - multipath_trampoline_data: recipient_onion.payment_secret.map(|payment_secret| { - msgs::FinalOnionHopData { - payment_secret, - total_msat: recipient_onion.total_mpp_amount_msat, - } - }), - trampoline_packet: packet, - }) + let total_msat = recipient_onion.total_mpp_amount_msat; + let multipath_trampoline_data = recipient_onion + .payment_secret + .map(|payment_secret| msgs::FinalOnionHopData { payment_secret, total_msat }); + + if let Some(blinding_point) = blinding_point { + Ok(Self::BlindedTrampolineEntrypoint { + amt_to_forward, + outgoing_cltv_value, + multipath_trampoline_data, + trampoline_packet: packet, + current_path_key: blinding_point, + }) + } else { + Ok(Self::TrampolineEntrypoint { + amt_to_forward, + outgoing_cltv_value, + multipath_trampoline_data, + trampoline_packet: packet, + }) + } } } impl<'a, 'b> OnionPayload<'a, 'b> for msgs::OutboundTrampolinePayload<'a> { @@ -314,6 +324,7 @@ impl<'a, 'b> OnionPayload<'a, 'b> for msgs::OutboundTrampolinePayload<'a> { fn new_trampoline_entry( _amt_to_forward: u64, _outgoing_cltv_value: u32, _recipient_onion: &'a RecipientOnionFields, _packet: msgs::TrampolineOnionPacket, + _blinding_point: Option, ) -> Result { Err(APIError::InvalidRoute { err: "Trampoline onions cannot contain Trampoline entrypoints!".to_string(), @@ -446,7 +457,7 @@ pub(super) fn build_trampoline_onion_payloads<'a>( pub(crate) fn test_build_onion_payloads<'a>( path: &'a Path, recipient_onion: &'a RecipientOnionFields, cur_block_height: u32, keysend_preimage: &Option, invoice_request: Option<&'a InvoiceRequest>, - trampoline_packet: Option, + trampoline_packet: Option<(msgs::TrampolineOnionPacket, Option)>, ) -> Result<(Vec>, u64, u32), APIError> { build_onion_payloads( path, @@ -462,7 +473,7 @@ pub(crate) fn test_build_onion_payloads<'a>( fn build_onion_payloads<'a>( path: &'a Path, recipient_onion: &'a RecipientOnionFields, cur_block_height: u32, keysend_preimage: &Option, invoice_request: Option<&'a InvoiceRequest>, - trampoline_packet: Option, + trampoline_packet: Option<(msgs::TrampolineOnionPacket, Option)>, ) -> Result<(Vec>, u64, u32), APIError> { let mut res: Vec = Vec::with_capacity( path.hops.len() + path.blinded_tail.as_ref().map_or(0, |t| t.hops.len()), @@ -472,10 +483,11 @@ fn build_onion_payloads<'a>( // means that the blinded path needs not be appended to the regular hops, and is only included // among the Trampoline onion payloads. let blinded_tail_with_hop_iter = path.blinded_tail.as_ref().map(|bt| { - if let Some(trampoline_packet) = trampoline_packet { + if let Some((trampoline_packet, blinding_point)) = trampoline_packet { return BlindedTailDetails::TrampolineEntry { trampoline_packet, final_value_msat: bt.final_value_msat, + blinding_point, }; } BlindedTailDetails::DirectEntry { @@ -511,6 +523,9 @@ enum BlindedTailDetails<'a, I: Iterator> { TrampolineEntry { trampoline_packet: msgs::TrampolineOnionPacket, final_value_msat: u64, + // If forwarding a trampoline payment inside of a blinded path, this blinding_point will + // be set for the trampoline to decrypt its inner onion. + blinding_point: Option, }, } @@ -581,6 +596,7 @@ where Some(BlindedTailDetails::TrampolineEntry { trampoline_packet, final_value_msat, + blinding_point, }) => { cur_value_msat += final_value_msat; callback( @@ -590,6 +606,7 @@ where declared_incoming_cltv, &recipient_onion, trampoline_packet, + blinding_point, )?, ); }, @@ -2677,7 +2694,7 @@ pub(crate) fn create_payment_onion_internal( err: "Route size too large considering onion data".to_owned(), })?; - trampoline_packet_option = Some(trampoline_packet); + trampoline_packet_option = Some((trampoline_packet, None)); outer_onion = &trampoline_outer_onion; } } From 4a827cf2b16d6bfd04d3705ad8fec4387f48def7 Mon Sep 17 00:00:00 2001 From: Carla Kirk-Cohen Date: Wed, 28 Jan 2026 15:08:29 -0500 Subject: [PATCH 47/68] ln function to build trampoline forwarding onions --- lightning/src/ln/onion_utils.rs | 45 ++++++++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/lightning/src/ln/onion_utils.rs b/lightning/src/ln/onion_utils.rs index bcf531a7cb4..18ab5f35678 100644 --- a/lightning/src/ln/onion_utils.rs +++ b/lightning/src/ln/onion_utils.rs @@ -18,7 +18,7 @@ use crate::ln::channel::TOTAL_BITCOIN_SUPPLY_SATOSHIS; use crate::ln::channelmanager::HTLCSource; use crate::ln::msgs::{self, DecodeError, InboundOnionDummyPayload, OnionPacket, UpdateAddHTLC}; use crate::ln::onion_payment::{HopConnector, NextPacketDetails}; -use crate::ln::outbound_payment::RecipientOnionFields; +use crate::ln::outbound_payment::{NextTrampolineHopInfo, RecipientOnionFields}; use crate::offers::invoice_request::InvoiceRequest; use crate::routing::gossip::NetworkUpdate; use crate::routing::router::{BlindedTail, Path, RouteHop, RouteParameters, TrampolineHop}; @@ -2647,6 +2647,49 @@ pub(super) fn compute_trampoline_session_priv(outer_onion_session_priv: &SecretK SecretKey::from_slice(&session_priv_hash[..]).expect("You broke SHA-256!") } +/// Builds a payment onion for an inter-trampoline forward. +pub(crate) fn create_trampoline_forward_onion( + secp_ctx: &Secp256k1, path: &Path, session_priv: &SecretKey, payment_hash: &PaymentHash, + recipient_onion: &RecipientOnionFields, keysend_preimage: &Option, + trampoline_forward_info: &NextTrampolineHopInfo, prng_seed: [u8; 32], +) -> Result<(msgs::OnionPacket, u64, u32), APIError> { + // Inter-trampoline payments should always be cleartext because we need to know the node id + // that we need to route to. LDK does not currently support the legacy "trampoline to blinded + // path" approach, where we get a blinded path to pay inside of our trampoline onion. + debug_assert!(path.blinded_tail.is_none(), "trampoline should not be blinded"); + + let mut res: Vec = Vec::with_capacity(path.hops.len()); + + let blinded_tail_with_hop_iter: BlindedTailDetails<'_, core::iter::Empty<&BlindedHop>> = + BlindedTailDetails::TrampolineEntry { + trampoline_packet: trampoline_forward_info.onion_packet.clone(), + final_value_msat: 0, + blinding_point: trampoline_forward_info.blinding_point, + }; + let (value_msat, cltv) = build_onion_payloads_callback( + path.hops.iter(), + Some(blinded_tail_with_hop_iter), + recipient_onion, + // Note that we use the cltv expiry height that the next trampoline is expecting instead + // of the current block height. This is because we need to create an onion that terminates + // at the next trampoline with the cltv we've been told to give them. + trampoline_forward_info.cltv_expiry_height, + keysend_preimage, + None, + |action, payload| match action { + PayloadCallbackAction::PushBack => res.push(payload), + PayloadCallbackAction::PushFront => res.insert(0, payload), + }, + )?; + + let onion_keys = construct_onion_keys(&secp_ctx, &path, session_priv); + let onion_packet = + construct_onion_packet(res, onion_keys, prng_seed, payment_hash).map_err(|_| { + APIError::InvalidRoute { err: "Route size too large considering onion data".to_owned() } + })?; + Ok((onion_packet, value_msat, cltv)) +} + /// Build a payment onion, returning the first hop msat and cltv values as well. /// `cur_block_height` should be set to the best known block height + 1. pub(crate) fn create_payment_onion_internal( From 334a00de75131084adb62bb6a2ed2237a56d10a3 Mon Sep 17 00:00:00 2001 From: Carla Kirk-Cohen Date: Wed, 11 Feb 2026 15:20:15 +0200 Subject: [PATCH 48/68] ln: support trampoline in send_payment_along_path --- lightning/src/ln/channelmanager.rs | 66 +++++++++++++++++++++--------- 1 file changed, 47 insertions(+), 19 deletions(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 16e489065f4..9d86f8c1f2b 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -5255,9 +5255,9 @@ impl< keysend_preimage, invoice_request, bolt12_invoice, + trampoline_forward_info, session_priv_bytes, hold_htlc_at_next_hop, - .. } = args; // The top-level caller should hold the total_consistency_lock read lock. debug_assert!(self.total_consistency_lock.try_write().is_err()); @@ -5271,18 +5271,32 @@ impl< Some(*payment_hash), payment_id, ); - let (onion_packet, htlc_msat, htlc_cltv) = onion_utils::create_payment_onion( - &self.secp_ctx, - &path, - &session_priv, - recipient_onion, - cur_height, - payment_hash, - keysend_preimage, - invoice_request, - prng_seed, - ) - .map_err(|e| { + let onion_result = if let Some(trampoline_forward_info) = trampoline_forward_info { + onion_utils::create_trampoline_forward_onion( + &self.secp_ctx, + &path, + &session_priv, + payment_hash, + recipient_onion, + keysend_preimage, + &trampoline_forward_info.next_hop_info, + prng_seed, + ) + } else { + onion_utils::create_payment_onion( + &self.secp_ctx, + &path, + &session_priv, + recipient_onion, + cur_height, + payment_hash, + keysend_preimage, + invoice_request, + prng_seed, + ) + }; + + let (onion_packet, htlc_msat, htlc_cltv) = onion_result.map_err(|e| { log_error!(logger, "Failed to build an onion for path"); e })?; @@ -5326,12 +5340,26 @@ impl< }); } let funding_txo = chan.funding.get_funding_txo().unwrap(); - let htlc_source = HTLCSource::OutboundRoute { - path: path.clone(), - session_priv: session_priv.clone(), - first_hop_htlc_msat: htlc_msat, - payment_id, - bolt12_invoice: bolt12_invoice.cloned(), + let htlc_source = match trampoline_forward_info { + None => HTLCSource::OutboundRoute { + path: path.clone(), + session_priv: session_priv.clone(), + first_hop_htlc_msat: htlc_msat, + payment_id, + bolt12_invoice: bolt12_invoice.cloned(), + }, + Some(trampoline_forward_info) => HTLCSource::TrampolineForward { + previous_hop_data: trampoline_forward_info + .previous_hop_data + .clone(), + incoming_trampoline_shared_secret: trampoline_forward_info + .incoming_trampoline_shared_secret, + outbound_payment: Some(TrampolineDispatch { + payment_id, + path: path.clone(), + session_priv, + }), + }, }; let send_res = chan.send_htlc_and_commit( htlc_msat, From a49db73dab40db45b65222415afb808698c6261f Mon Sep 17 00:00:00 2001 From: Carla Kirk-Cohen Date: Fri, 16 Jan 2026 14:56:00 -0500 Subject: [PATCH 49/68] ln: add send trampoline payment functionality --- lightning/src/ln/outbound_payment.rs | 114 +++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) diff --git a/lightning/src/ln/outbound_payment.rs b/lightning/src/ln/outbound_payment.rs index 772a61fe803..3f06ad2454b 100644 --- a/lightning/src/ln/outbound_payment.rs +++ b/lightning/src/ln/outbound_payment.rs @@ -1657,6 +1657,120 @@ impl OutboundPayments { Ok(()) } + /// Errors immediately on [`RetryableSendFailure`] error conditions. Otherwise, further errors may + /// be surfaced asynchronously via [`Event::PaymentPathFailed`] and [`Event::PaymentFailed`]. + /// + /// [`Event::PaymentPathFailed`]: crate::events::Event::PaymentPathFailed + /// [`Event::PaymentFailed`]: crate::events::Event::PaymentFailed + pub(super) fn send_payment_for_trampoline_forward< + R: Router, + NS: NodeSigner, + ES: EntropySource, + IH, + SP, + L: Logger, + >( + &self, payment_id: PaymentId, payment_hash: PaymentHash, + trampoline_forward_info: TrampolineForwardInfo, retry_strategy: Retry, + mut route_params: RouteParameters, router: &R, first_hops: Vec, + inflight_htlcs: IH, entropy_source: &ES, node_signer: &NS, best_block_height: u32, + pending_events: &Mutex)>>, + send_payment_along_path: SP, logger: &WithContext, + ) -> Result<(), RetryableSendFailure> + where + IH: Fn() -> InFlightHtlcs, + SP: Fn(SendAlongPathArgs) -> Result<(), APIError>, + { + let inter_trampoline_payment_secret = + PaymentSecret(entropy_source.get_secure_random_bytes()); + let recipient_onion = RecipientOnionFields::secret_only( + inter_trampoline_payment_secret, + trampoline_forward_info.next_hop_info.amount_msat, + ); + + let route = self.find_initial_route( + payment_id, + payment_hash, + &recipient_onion, + None, + None, + &mut route_params, + router, + &first_hops, + &inflight_htlcs, + node_signer, + best_block_height, + logger, + )?; + + let onion_session_privs = self + .add_new_pending_payment( + payment_hash, + recipient_onion.clone(), + payment_id, + None, + &route, + Some(retry_strategy), + Some(route_params.payment_params.clone()), + entropy_source, + best_block_height, + None, + Some(trampoline_forward_info.clone()), + ) + .map_err(|_| { + log_error!( + logger, + "Payment with id {} is already pending. New payment had payment hash {}", + payment_id, + payment_hash + ); + RetryableSendFailure::DuplicatePayment + })?; + + let res = self.pay_route_internal( + &route, + payment_hash, + &recipient_onion, + None, + None, + None, + Some(&trampoline_forward_info), + payment_id, + &onion_session_privs, + false, + node_signer, + best_block_height, + &send_payment_along_path, + ); + log_info!( + logger, + "Sending payment with id {} and hash {} returned {:?}", + payment_id, + payment_hash, + res + ); + if let Err(e) = res { + self.handle_pay_route_err( + e, + payment_id, + payment_hash, + route, + route_params, + onion_session_privs, + router, + first_hops, + &inflight_htlcs, + entropy_source, + node_signer, + best_block_height, + pending_events, + &send_payment_along_path, + logger, + ); + } + Ok(()) + } + #[rustfmt::skip] fn find_route_and_send_payment( &self, payment_hash: PaymentHash, payment_id: PaymentId, route_params: RouteParameters, From 29774b4323c2560eadfab0c4f23dfaf3915ef0d9 Mon Sep 17 00:00:00 2001 From: Carla Kirk-Cohen Date: Thu, 12 Feb 2026 15:59:20 +0200 Subject: [PATCH 50/68] ln/refactor: surface error data in DecodedOnionData for Trampolines When we're a forwarding trampoline and we receive a final error from our route, we want to propagate that failure back to the original sender. Surface the information so that it's available to us. --- lightning/src/ln/onion_utils.rs | 10 ---------- lightning/src/ln/outbound_payment.rs | 14 ++++++++------ 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/lightning/src/ln/onion_utils.rs b/lightning/src/ln/onion_utils.rs index 18ab5f35678..35bc4a5fe58 100644 --- a/lightning/src/ln/onion_utils.rs +++ b/lightning/src/ln/onion_utils.rs @@ -1009,9 +1009,7 @@ mod fuzzy_onion_utils { pub(crate) failed_within_blinded_path: bool, #[allow(dead_code)] pub(crate) hold_times: Vec, - #[cfg(any(test, feature = "_test_utils"))] pub(crate) onion_error_code: Option, - #[cfg(any(test, feature = "_test_utils"))] pub(crate) onion_error_data: Option>, #[cfg(test)] pub(crate) attribution_failed_channel: Option, @@ -1106,9 +1104,7 @@ fn process_onion_failure_inner( payment_failed_permanently: true, failed_within_blinded_path: false, hold_times: Vec::new(), - #[cfg(any(test, feature = "_test_utils"))] onion_error_code: None, - #[cfg(any(test, feature = "_test_utils"))] onion_error_data: None, #[cfg(test)] attribution_failed_channel: None, @@ -1496,9 +1492,7 @@ fn process_onion_failure_inner( payment_failed_permanently, failed_within_blinded_path, hold_times: hop_hold_times, - #[cfg(any(test, feature = "_test_utils"))] onion_error_code: _error_code_ret, - #[cfg(any(test, feature = "_test_utils"))] onion_error_data: _error_packet_ret, #[cfg(test)] attribution_failed_channel, @@ -1519,9 +1513,7 @@ fn process_onion_failure_inner( payment_failed_permanently: is_from_final_non_blinded_node, failed_within_blinded_path: false, hold_times: hop_hold_times, - #[cfg(any(test, feature = "_test_utils"))] onion_error_code: None, - #[cfg(any(test, feature = "_test_utils"))] onion_error_data: None, #[cfg(test)] attribution_failed_channel, @@ -2170,9 +2162,7 @@ impl HTLCFailReason { short_channel_id: Some(path.hops[0].short_channel_id), failed_within_blinded_path: false, hold_times: Vec::new(), - #[cfg(any(test, feature = "_test_utils"))] onion_error_code: Some(*failure_reason), - #[cfg(any(test, feature = "_test_utils"))] onion_error_data: Some(data.clone()), #[cfg(test)] attribution_failed_channel: None, diff --git a/lightning/src/ln/outbound_payment.rs b/lightning/src/ln/outbound_payment.rs index 3f06ad2454b..01e3071892c 100644 --- a/lightning/src/ln/outbound_payment.rs +++ b/lightning/src/ln/outbound_payment.rs @@ -2630,24 +2630,26 @@ impl OutboundPayments { pending_events: &Mutex)>>, completion_action: &mut Option, logger: &WithContext, ) { - #[cfg(any(test, feature = "_test_utils"))] + #[cfg(test)] let DecodedOnionFailure { network_update, short_channel_id, payment_failed_permanently, - onion_error_code, - onion_error_data, failed_within_blinded_path, hold_times, + onion_error_code: _onion_code, + onion_error_data: _onion_data, .. } = onion_error.decode_onion_failure(secp_ctx, &logger, &source); - #[cfg(not(any(test, feature = "_test_utils")))] + #[cfg(not(test))] let DecodedOnionFailure { network_update, short_channel_id, payment_failed_permanently, failed_within_blinded_path, hold_times, + onion_error_code: _onion_code, + onion_error_data: _onion_data, .. } = onion_error.decode_onion_failure(secp_ctx, &logger, &source); @@ -2768,9 +2770,9 @@ impl OutboundPayments { path: path.clone(), short_channel_id, #[cfg(any(test, feature = "_test_utils"))] - error_code: onion_error_code.map(|f| f.failure_code()), + error_code: _onion_code.map(|f| f.failure_code()), #[cfg(any(test, feature = "_test_utils"))] - error_data: onion_error_data, + error_data: _onion_data, hold_times, } } From 8c9aa695bf40ddd57448fefe5e90754a3c8b358b Mon Sep 17 00:00:00 2001 From: Carla Kirk-Cohen Date: Mon, 9 Feb 2026 15:28:53 +0200 Subject: [PATCH 51/68] [wip] ln: add trampoline htlc failure logic to outbound payments - [ ] Check whether we can get away with checking path.hops[0] directly (outbound_payment should always be present?) --- lightning/src/ln/onion_utils.rs | 72 +++++++++++++----- lightning/src/ln/outbound_payment.rs | 110 +++++++++++++++++++++++++++ 2 files changed, 163 insertions(+), 19 deletions(-) diff --git a/lightning/src/ln/onion_utils.rs b/lightning/src/ln/onion_utils.rs index 35bc4a5fe58..c7cef557568 100644 --- a/lightning/src/ln/onion_utils.rs +++ b/lightning/src/ln/onion_utils.rs @@ -1019,12 +1019,32 @@ mod fuzzy_onion_utils { secp_ctx: &Secp256k1, logger: &L, htlc_source: &HTLCSource, encrypted_packet: OnionErrorPacket, ) -> DecodedOnionFailure { - let (path, session_priv) = match htlc_source { - HTLCSource::OutboundRoute { ref path, ref session_priv, .. } => (path, session_priv), + match htlc_source { + HTLCSource::OutboundRoute { ref path, ref session_priv, .. } => { + process_onion_failure_inner( + secp_ctx, + logger, + &path, + &session_priv, + None, + encrypted_packet, + ) + }, + HTLCSource::TrampolineForward { outbound_payment, .. } => { + let dispatch = outbound_payment.as_ref() + .expect("processing trampoline onion failure for forward with no outbound payment details"); + + process_onion_failure_inner( + secp_ctx, + logger, + &dispatch.path, + &dispatch.session_priv, + None, + encrypted_packet, + ) + }, _ => unreachable!(), - }; - - process_onion_failure_inner(secp_ctx, logger, path, &session_priv, None, encrypted_packet) + } } /// Decodes the attribution data that we got back from upstream on a payment we sent. @@ -2144,6 +2164,21 @@ impl HTLCFailReason { pub(super) fn decode_onion_failure( &self, secp_ctx: &Secp256k1, logger: &L, htlc_source: &HTLCSource, ) -> DecodedOnionFailure { + macro_rules! decoded_onion_failure { + ($short_channel_id:expr, $failure_reason:expr, $data:expr) => { + DecodedOnionFailure { + network_update: None, + payment_failed_permanently: false, + short_channel_id: $short_channel_id, + failed_within_blinded_path: false, + hold_times: Vec::new(), + onion_error_code: Some($failure_reason), + onion_error_data: Some($data.clone()), + #[cfg(test)] + attribution_failed_channel: None, + } + }; + } match self.0 { HTLCFailReasonRepr::LightningError { ref err, .. } => { process_onion_failure(secp_ctx, logger, &htlc_source, err.clone()) @@ -2155,20 +2190,19 @@ impl HTLCFailReason { // failures here, but that would be insufficient as find_route // generally ignores its view of our own channels as we provide them via // ChannelDetails. - if let &HTLCSource::OutboundRoute { ref path, .. } = htlc_source { - DecodedOnionFailure { - network_update: None, - payment_failed_permanently: false, - short_channel_id: Some(path.hops[0].short_channel_id), - failed_within_blinded_path: false, - hold_times: Vec::new(), - onion_error_code: Some(*failure_reason), - onion_error_data: Some(data.clone()), - #[cfg(test)] - attribution_failed_channel: None, - } - } else { - unreachable!(); + match htlc_source { + &HTLCSource::OutboundRoute { ref path, .. } => { + decoded_onion_failure!( + (Some(path.hops[0].short_channel_id)), + *failure_reason, + data + ) + }, + &HTLCSource::TrampolineForward { ref outbound_payment, .. } => { + debug_assert!(outbound_payment.is_none()); + decoded_onion_failure!(None, *failure_reason, data) + }, + _ => unreachable!(), } }, } diff --git a/lightning/src/ln/outbound_payment.rs b/lightning/src/ln/outbound_payment.rs index 01e3071892c..73dd8e1d2d6 100644 --- a/lightning/src/ln/outbound_payment.rs +++ b/lightning/src/ln/outbound_payment.rs @@ -2623,6 +2623,116 @@ impl OutboundPayments { }); } + // Reports a failed HTLC that is part of an outgoing trampoline forward. Returns Some() if + // the incoming HTLC(s) associated with the trampoline should be failed back. + pub(super) fn trampoline_htlc_failed( + &self, source: &HTLCSource, payment_hash: &PaymentHash, onion_error: &HTLCFailReason, + secp_ctx: &Secp256k1, logger: &WithContext, + ) -> Option { + #[cfg(any(test, feature = "_test_utils"))] + let decoded_onion = onion_error.decode_onion_failure(secp_ctx, &logger, &source); + + #[cfg(not(any(test, feature = "_test_utils")))] + let decoded_onion = onion_error.decode_onion_failure(secp_ctx, &logger, &source); + + let (payment_id, path, session_priv) = match source { + // TODO: don't unwrap and handle error appropriately + HTLCSource::TrampolineForward { outbound_payment, .. } => { + let outbound_payment = outbound_payment.clone().unwrap(); + (outbound_payment.payment_id, outbound_payment.path, outbound_payment.session_priv) + }, + _ => panic!("can't fail trampoline forward with non-trampoline source"), + }; + + let mut session_priv_bytes = [0; 32]; + session_priv_bytes.copy_from_slice(&session_priv[..]); + let mut outbounds = self.pending_outbound_payments.lock().unwrap(); + + // If any payments already need retry, there's no need to generate a redundant + // `PendingHTLCsForwardable`. + let already_awaiting_retry = outbounds.iter().any(|(_, pmt)| { + let mut awaiting_retry = false; + if pmt.is_auto_retryable_now() { + if let PendingOutboundPayment::Retryable { pending_amt_msat, total_msat, .. } = pmt + { + if pending_amt_msat < total_msat { + awaiting_retry = true; + } + } + } + awaiting_retry + }); + + let attempts_remaining = + if let hash_map::Entry::Occupied(mut payment) = outbounds.entry(payment_id) { + if !payment.get_mut().remove(&session_priv_bytes, Some(&path)) { + log_trace!( + logger, + "Received duplicative fail for HTLC with payment_hash {}", + &payment_hash + ); + return None; + } + if payment.get().is_fulfilled() { + log_trace!( + logger, + "Received failure of HTLC with payment_hash {} after payment completion", + &payment_hash + ); + return None; + } + let mut is_retryable_now = payment.get().is_auto_retryable_now(); + if let Some(scid) = decoded_onion.short_channel_id { + // TODO: If we decided to blame ourselves (or one of our channels) in + // process_onion_failure we should close that channel as it implies our + // next-hop is needlessly blaming us! + payment.get_mut().insert_previously_failed_scid(scid); + } + if decoded_onion.failed_within_blinded_path { + debug_assert!(decoded_onion.short_channel_id.is_none()); + if let Some(bt) = &path.blinded_tail { + payment.get_mut().insert_previously_failed_blinded_path(&bt); + } else { + debug_assert!(false); + } + } + + if !is_retryable_now || decoded_onion.payment_failed_permanently { + let reason = if decoded_onion.payment_failed_permanently { + PaymentFailureReason::RecipientRejected + } else { + PaymentFailureReason::RetriesExhausted + }; + payment.get_mut().mark_abandoned(reason); + is_retryable_now = false; + } + if payment.get().remaining_parts() == 0 { + if let PendingOutboundPayment::Abandoned { .. } = payment.get() { + payment.remove(); + return Some(decoded_onion); + } + } + is_retryable_now + } else { + log_trace!( + logger, + "Received fail for HTLC with payment_hash {} not found.", + &payment_hash + ); + return Some(decoded_onion); + }; + core::mem::drop(outbounds); + log_trace!(logger, "Failing Trampoline forward HTLC with payment_hash {}", &payment_hash); + + // If we miss abandoning the payment above, we *must* generate an event here or else the + // payment will sit in our outbounds forever. + if attempts_remaining && !already_awaiting_retry { + return None; + }; + + return Some(decoded_onion); + } + pub(super) fn fail_htlc( &self, source: &HTLCSource, payment_hash: &PaymentHash, onion_error: &HTLCFailReason, path: &Path, session_priv: &SecretKey, payment_id: &PaymentId, From 08bb8d897bbac6376ea1d7a933475809019821ff Mon Sep 17 00:00:00 2001 From: Carla Kirk-Cohen Date: Wed, 4 Feb 2026 10:02:44 -0500 Subject: [PATCH 52/68] ln: Add utility to lookup forwarded trampoline fee When we settle a trampoline forward, we'd need to look up all of our incoming/outgoing htlc amounts to calculate the fee we've earned. Instead, we just look it up in our outbound payments. Note that here we report the minimum fee that we charged for the forward. It's possible that the rest of the trampoline route was under its allowed budget, and we earned more than our required fee. --- lightning/src/ln/channelmanager.rs | 27 ++++++++++++++++++++++----- lightning/src/ln/outbound_payment.rs | 14 ++++++++++++++ 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 9d86f8c1f2b..cc8eec7ce00 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -9802,7 +9802,27 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ send_timestamp, ); }, - HTLCSource::TrampolineForward { previous_hop_data, .. } => { + HTLCSource::TrampolineForward { previous_hop_data, outbound_payment, .. } => { + let total_fee_earned_msat = match outbound_payment { + Some(trampoline_dispatch) => { + let fee = self + .pending_outbound_payments + .get_trampoline_forwarding_fee(&trampoline_dispatch.payment_id); + debug_assert!( + fee.is_some(), + "Trampoline payment with unknown payment_id: {} settled", + trampoline_dispatch.payment_id + ); + fee + }, + None => { + debug_assert!( + false, + "Trampoline payment settled with no outbound payment dispatched" + ); + None + }, + }; // Only emit a single event for trampoline claims. let prev_htlcs: Vec = previous_hop_data.iter().map(Into::into).collect(); @@ -9821,10 +9841,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ user_channel_id: next_user_channel_id, node_id: Some(next_channel_counterparty_node_id), }], - // TODO: When trampoline payments are tracked in our - // pending_outbound_payments, we'll be able to lookup our total - // fee earnings. - total_fee_earned_msat: None, + total_fee_earned_msat, skimmed_fee_msat, claim_from_onchain_tx: from_onchain, outbound_amount_forwarded_msat: forwarded_htlc_value_msat, diff --git a/lightning/src/ln/outbound_payment.rs b/lightning/src/ln/outbound_payment.rs index 73dd8e1d2d6..8108105cc68 100644 --- a/lightning/src/ln/outbound_payment.rs +++ b/lightning/src/ln/outbound_payment.rs @@ -3034,6 +3034,20 @@ impl OutboundPayments { }, } } + + /// Looks up a trampoline forward by its payment id and returns the forwarding fee our node + /// earned, returning None if the payment is not found or it does not have trampoline forwading + /// information. + pub(crate) fn get_trampoline_forwarding_fee(&self, payment_id: &PaymentId) -> Option { + self.pending_outbound_payments.lock().unwrap().get(payment_id).and_then(|payment| { + match payment { + PendingOutboundPayment::Retryable { trampoline_forward_info, .. } => { + trampoline_forward_info.as_ref().map(|info| info.forwading_fee_msat) + }, + _ => None, + } + }) + } } /// Returns whether a payment with the given [`PaymentHash`] and [`PaymentId`] is, in fact, a From bcb9d8ad572c936b86170370e31aba5acd0ddab0 Mon Sep 17 00:00:00 2001 From: Carla Kirk-Cohen Date: Tue, 10 Feb 2026 09:02:22 +0200 Subject: [PATCH 53/68] ln/refactor: move mpp timeout check into helper function --- lightning/src/ln/channelmanager.rs | 70 +++++++++++++++++++----------- 1 file changed, 44 insertions(+), 26 deletions(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index cc8eec7ce00..71d8afa4d70 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -551,6 +551,14 @@ struct ClaimableHTLC { counterparty_skimmed_fee_msat: Option, } +impl ClaimableHTLC { + // Increments timer ticks and returns a boolean indicating whether HLTC is timed out. + fn mpp_timer_tick(&mut self) -> bool { + self.timer_ticks += 1; + self.timer_ticks >= MPP_TIMEOUT_TICKS + } +} + impl From<&ClaimableHTLC> for events::ClaimedHTLC { fn from(val: &ClaimableHTLC) -> Self { events::ClaimedHTLC { @@ -1205,6 +1213,21 @@ impl ClaimablePayment { } } +/// Increments MPP timeout tick for all HTLCs and returns a boolean indicating whether the HTLC +/// set has hit its MPP timeout. Will return false if the set have reached the sender's intended +/// total, as the MPP has completed in this case. +fn check_mpp_timeout(sender_intended_total: u64, htlcs: &mut Vec) -> bool { + if htlcs.is_empty() { + debug_assert!(false, "check_mpp_timeout called with no HTLCs"); + return false; + } + if htlcs[0].total_msat <= sender_intended_total { + return false; + } + + htlcs.iter_mut().any(|htlc| htlc.mpp_timer_tick()) +} + /// We write the [`ClaimableHTLC`] [`RecipientOnionFields`] separately as they were added sometime /// later. Because [`ClaimableHTLC`] only implements [`ReadableArgs`] and have to add a wrapper /// which reads them without [`RecipientOnionFields::total_mpp_amount_msat`] and then fill them in @@ -8619,42 +8642,37 @@ impl< self.claimable_payments.lock().unwrap().claimable_payments.retain( |payment_hash, payment| { - if payment.htlcs.is_empty() { - // This should be unreachable - debug_assert!(false); - return false; - } if let OnionPayload::Invoice { .. } = payment.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. - // This condition determining whether the MPP is complete here must match - // exactly the condition used in `process_pending_htlc_forwards`. - let htlc_total_msat = + let htlc_total_msat: u64 = payment.htlcs.iter().map(|h| h.sender_intended_value).sum(); - if payment.htlcs[0].total_msat <= htlc_total_msat { - return true; - } else if payment.htlcs.iter_mut().any(|htlc| { - htlc.timer_ticks += 1; - return htlc.timer_ticks >= MPP_TIMEOUT_TICKS; - }) { - let htlcs = payment - .htlcs - .drain(..) - .map(|htlc: ClaimableHTLC| (htlc.prev_hop, *payment_hash)); - timed_out_mpp_htlcs.extend(htlcs); - return false; + let mpp_timeout = check_mpp_timeout(htlc_total_msat, &mut payment.htlcs); + if mpp_timeout { + timed_out_mpp_htlcs.extend(payment.htlcs.drain(..).map(|h| { + ( + HTLCSource::PreviousHopData(h.prev_hop), + *payment_hash, + HTLCHandlingFailureType::Receive { + payment_hash: *payment_hash, + }, + ) + })); } + return !mpp_timeout; } true }, ); - for htlc_source in timed_out_mpp_htlcs.drain(..) { - let source = HTLCSource::PreviousHopData(htlc_source.0.clone()); + for (htlc_source, payment_hash, failure_type) in timed_out_mpp_htlcs.drain(..) { let failure_reason = LocalHTLCFailureReason::MPPTimeout; let reason = HTLCFailReason::from_failure_code(failure_reason); - let receiver = HTLCHandlingFailureType::Receive { payment_hash: htlc_source.1 }; - self.fail_htlc_backwards_internal(&source, &htlc_source.1, &reason, receiver, None); + self.fail_htlc_backwards_internal( + &htlc_source, + &payment_hash, + &reason, + failure_type, + None, + ); } for (err, counterparty_node_id) in handle_errors { From 5750874828bf84a6ae1661a027c358286ce53b86 Mon Sep 17 00:00:00 2001 From: Carla Kirk-Cohen Date: Thu, 22 Jan 2026 12:11:27 -0500 Subject: [PATCH 54/68] ln/refactor: move on chain timeout check into claimable htlc --- lightning/src/ln/channelmanager.rs | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 71d8afa4d70..9c381448d27 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -557,6 +557,12 @@ impl ClaimableHTLC { self.timer_ticks += 1; self.timer_ticks >= MPP_TIMEOUT_TICKS } + + /// Returns a boolean indicating whether the HTLC has timed out on chain, accounting for a buffer + /// that gives us time to resolve it. + fn check_onchain_timeout(&self, height: u32, buffer: u32) -> bool { + height >= self.cltv_expiry - buffer + } } impl From<&ClaimableHTLC> for events::ClaimedHTLC { @@ -15602,14 +15608,16 @@ impl< } if let Some(height) = height_opt { + // If height is approaching the number of blocks we think it takes us to get our + // commitment transaction confirmed before the HTLC expires, plus the number of blocks + // we generally consider it to take to do a commitment update, just give up on it and + // fail the HTLC. self.claimable_payments.lock().unwrap().claimable_payments.retain( |payment_hash, payment| { payment.htlcs.retain(|htlc| { - // If height is approaching the number of blocks we think it takes us to get - // our commitment transaction confirmed before the HTLC expires, plus the - // number of blocks we generally consider it to take to do a commitment update, - // just give up on it and fail the HTLC. - if height >= htlc.cltv_expiry - HTLC_FAIL_BACK_BUFFER { + let htlc_timed_out = + htlc.check_onchain_timeout(height, HTLC_FAIL_BACK_BUFFER); + if htlc_timed_out { let reason = LocalHTLCFailureReason::PaymentClaimBuffer; timed_out_htlcs.push(( HTLCSource::PreviousHopData(htlc.prev_hop.clone()), @@ -15622,10 +15630,8 @@ impl< payment_hash: payment_hash.clone(), }, )); - false - } else { - true } + !htlc_timed_out }); !payment.htlcs.is_empty() // Only retain this entry if htlcs has at least one entry. }, From da7b49ebc4d243879b90201786e6f108489ef958 Mon Sep 17 00:00:00 2001 From: Carla Kirk-Cohen Date: Fri, 23 Jan 2026 10:24:35 -0500 Subject: [PATCH 55/68] [wip]: add Trampoline variant to OnionPayload We're going to need to keep track of our trampoline HLTCs in the same way that we keep track of incoming MPP payment to allow them to accumulate on our incoming channel before forwarding them onwards to the outgoing channel. To do this we'll need to store the payload values we need to remember for forwarding in OnionPayload. - [ ] Readable for ClaimableHTLC is incomplete --- lightning/src/ln/channelmanager.rs | 53 ++++++++++++++++++++++------ lightning/src/ln/outbound_payment.rs | 2 +- 2 files changed, 44 insertions(+), 11 deletions(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 9c381448d27..e741c4c8b49 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -88,9 +88,9 @@ use crate::ln::outbound_payment; #[cfg(any(test, feature = "_externalize_tests"))] use crate::ln::outbound_payment::PaymentSendFailure; use crate::ln::outbound_payment::{ - Bolt11PaymentError, Bolt12PaymentError, OutboundPayments, PendingOutboundPayment, - ProbeSendFailure, RecipientCustomTlvs, RecipientOnionFields, Retry, RetryableInvoiceRequest, - RetryableSendFailure, SendAlongPathArgs, StaleExpiration, + Bolt11PaymentError, Bolt12PaymentError, NextTrampolineHopInfo, OutboundPayments, + PendingOutboundPayment, ProbeSendFailure, RecipientCustomTlvs, RecipientOnionFields, Retry, + RetryableInvoiceRequest, RetryableSendFailure, SendAlongPathArgs, StaleExpiration, }; use crate::ln::types::ChannelId; use crate::offers::async_receive_offer_cache::AsyncReceiveOfferCache; @@ -528,6 +528,8 @@ enum OnionPayload { }, /// Contains the payer-provided preimage. Spontaneous(PaymentPreimage), + /// Indicates that the incoming onion payload is for a trampoline forward. + Trampoline { next_hop_info: NextTrampolineHopInfo, next_trampoline: PublicKey }, } /// HTLCs that are to us and can be failed/claimed by the user @@ -8311,6 +8313,9 @@ impl< }; check_total_value!(purpose); }, + OnionPayload::Trampoline { .. } => { + todo!(); + }, } }, HTLCForwardInfo::FailHTLC { .. } | HTLCForwardInfo::FailMalformedHTLC { .. } => { @@ -17089,10 +17094,17 @@ impl_writeable_tlv_based!(TrampolineDispatch, { impl Writeable for ClaimableHTLC { fn write(&self, writer: &mut W) -> Result<(), io::Error> { - let (payment_data, keysend_preimage) = match &self.onion_payload { - OnionPayload::Invoice { _legacy_hop_data } => (_legacy_hop_data.as_ref(), None), - OnionPayload::Spontaneous(preimage) => (None, Some(preimage)), - }; + let (payment_data, keysend_preimage, trampoline_next_hop, trampoline_next_node) = + match &self.onion_payload { + OnionPayload::Invoice { _legacy_hop_data } => { + (_legacy_hop_data.as_ref(), None, None, None) + }, + OnionPayload::Spontaneous(preimage) => (None, Some(preimage), None, None), + OnionPayload::Trampoline { next_hop_info, next_trampoline } => { + (None, None, Some(next_hop_info), Some(next_trampoline)) + }, + }; + write_tlv_fields!(writer, { (0, self.prev_hop, required), (1, self.total_msat, required), @@ -17103,6 +17115,8 @@ impl Writeable for ClaimableHTLC { (6, self.cltv_expiry, required), (8, keysend_preimage, option), (10, self.counterparty_skimmed_fee_msat, option), + (12, trampoline_next_hop, option), + (14, trampoline_next_node, option) }); Ok(()) } @@ -17121,11 +17135,14 @@ impl Readable for ClaimableHTLC { (6, cltv_expiry, required), (8, keysend_preimage, option), (10, counterparty_skimmed_fee_msat, option), + (12, trampoline_next_hop, option), + (14, trampoline_next_node, option) + }); let payment_data: Option = payment_data_opt; let value = value_ser.0.unwrap(); - let onion_payload = match keysend_preimage { - Some(p) => { + let onion_payload = match (keysend_preimage, trampoline_next_hop) { + (Some(p), None) => { if payment_data.is_some() { return Err(DecodeError::InvalidValue) } @@ -17134,7 +17151,7 @@ impl Readable for ClaimableHTLC { } OnionPayload::Spontaneous(p) }, - None => { + (None, None) => { if total_msat.is_none() { if payment_data.is_none() { return Err(DecodeError::InvalidValue) @@ -17143,6 +17160,21 @@ impl Readable for ClaimableHTLC { } OnionPayload::Invoice { _legacy_hop_data: payment_data } }, + (None, Some(trampoline_next_hop)) => { + // TODO: check which values we'll have for trampoline! + if total_msat.is_none() { + match payment_data { + Some(data) => total_msat = Some(data.total_msat), + None => return Err(DecodeError::InvalidValue), + } + } + + OnionPayload::Trampoline { + next_hop_info: trampoline_next_hop, + next_trampoline: trampoline_next_node.ok_or(DecodeError::InvalidValue)?, + } + }, + _ => return Err(DecodeError::InvalidValue) }; Ok(Self { prev_hop: prev_hop.0.unwrap(), @@ -18024,6 +18056,7 @@ impl<'a, ES: EntropySource, NS: NodeSigner, SP: SignerProvider, L: Logger> OnionPayload::Spontaneous(payment_preimage) => { events::PaymentPurpose::SpontaneousPayment(*payment_preimage) }, + OnionPayload::Trampoline { .. } => todo!(), }; claimable_payments .insert(payment_hash, ClaimablePayment { purpose, htlcs, onion_fields: None }); diff --git a/lightning/src/ln/outbound_payment.rs b/lightning/src/ln/outbound_payment.rs index 8108105cc68..be991dd3cf0 100644 --- a/lightning/src/ln/outbound_payment.rs +++ b/lightning/src/ln/outbound_payment.rs @@ -170,7 +170,7 @@ pub(crate) enum PendingOutboundPayment { }, } -#[derive(Clone)] +#[derive(Clone, Eq, PartialEq)] pub(crate) struct NextTrampolineHopInfo { /// The Trampoline packet to include for the next Trampoline hop. pub(crate) onion_packet: TrampolineOnionPacket, From e65f76567133b5415a242551219331ad80128932 Mon Sep 17 00:00:00 2001 From: Carla Kirk-Cohen Date: Thu, 22 Jan 2026 13:40:35 -0500 Subject: [PATCH 56/68] [wip]: add awaiting_trampoline_forwards to accumulate inbound MPP When we are a trampoline router, we need to accumulate incoming HTLCs (if MPP is used) before forwarding the trampoline-routed outgoing HTLC(s). This commits adds a new map in channel manager, and mimics the handling done for claimable_payments. This map is not placed in claimable_payments because we'll need to be able to lock pending_outbound_payments in the commits that follow while holding a lock on our set of trampoline payments (which is not possible with claimable_payments). - [ ] Need to add persistence of trampoline payments --- lightning/src/ln/channelmanager.rs | 78 ++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index e741c4c8b49..f7353a6ca2f 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -2829,6 +2829,12 @@ pub struct ChannelManager< /// [`ClaimablePayments`]' individual field docs for more info. claimable_payments: Mutex, + /// The sets of trampoline payments which are in the process of being accumulated on inbound + /// channel(s). + /// + /// Note: Not adding ChannelMangaer struct level docs because 4300 removes it. + awaiting_trampoline_forwards: Mutex>, + /// The set of outbound SCID aliases across all our channels, including unconfirmed channels /// and some closed channels which reached a usable state prior to being closed. This is used /// only to avoid duplicates, and is not persisted explicitly to disk, but rebuilt from the @@ -3601,6 +3607,7 @@ impl< forward_htlcs: Mutex::new(new_hash_map()), decode_update_add_htlcs: Mutex::new(new_hash_map()), claimable_payments: Mutex::new(ClaimablePayments { claimable_payments: new_hash_map(), pending_claiming_payments: new_hash_map() }), + awaiting_trampoline_forwards: Mutex::new(new_hash_map()), pending_intercepted_htlcs: Mutex::new(new_hash_map()), short_to_chan_info: FairRwLock::new(new_hash_map()), @@ -8674,6 +8681,37 @@ impl< }, ); + self.awaiting_trampoline_forwards.lock().unwrap().retain(|payment_hash, payment| { + if let OnionPayload::Trampoline { .. } = payment.htlcs[0].onion_payload { + let htlc_total_msat: u64 = + payment.htlcs.iter().map(|h| h.sender_intended_value).sum(); + let mpp_timeout = check_mpp_timeout(htlc_total_msat, &mut payment.htlcs); + if mpp_timeout { + let previous_hop_data = + payment.htlcs.drain(..).map(|claimable| claimable.prev_hop).collect(); + + timed_out_mpp_htlcs.push(( + HTLCSource::TrampolineForward { + previous_hop_data, + incoming_trampoline_shared_secret: payment.htlcs[1] + .prev_hop + .incoming_packet_shared_secret, + outbound_payment: None, + }, + *payment_hash, + HTLCHandlingFailureType::TrampolineForward {}, + )); + } + !mpp_timeout + } else { + debug_assert!( + false, + "awaiting_trampoline_forwards should only contain trampolines" + ); + true + } + }); + for (htlc_source, payment_hash, failure_type) in timed_out_mpp_htlcs.drain(..) { let failure_reason = LocalHTLCFailureReason::MPPTimeout; let reason = HTLCFailReason::from_failure_code(failure_reason); @@ -15642,6 +15680,42 @@ impl< }, ); + self.awaiting_trampoline_forwards.lock().unwrap().retain(|payment_hash, payment| { + if let OnionPayload::Trampoline { .. } = payment.htlcs[0].onion_payload { + let htlc_timed_out = payment + .htlcs + .iter() + .any(|htlc| htlc.check_onchain_timeout(height, HTLC_FAIL_BACK_BUFFER)); + if htlc_timed_out { + let previous_hop_data = + payment.htlcs.drain(..).map(|claimable| claimable.prev_hop).collect(); + + timed_out_htlcs.push(( + HTLCSource::TrampolineForward { + previous_hop_data, + incoming_trampoline_shared_secret: payment.htlcs[0] + .prev_hop + .incoming_packet_shared_secret, + outbound_payment: None, + }, + payment_hash.clone(), + HTLCFailReason::reason( + LocalHTLCFailureReason::CLTVExpiryTooSoon, + vec![], + ), + HTLCHandlingFailureType::TrampolineForward {}, + )); + } + !htlc_timed_out + } else { + debug_assert!( + false, + "awaiting_trampoline_forwards should only contain trampolines" + ); + true + } + }); + let mut intercepted_htlcs = self.pending_intercepted_htlcs.lock().unwrap(); intercepted_htlcs.retain(|_, htlc| { if height >= htlc.forward_info.outgoing_cltv_value - HTLC_FAIL_BACK_BUFFER { @@ -17482,6 +17556,8 @@ impl< htlc_onion_fields.push(&payment.onion_fields); } + // TODO: write pending_trampoline_forwards + let mut monitor_update_blocked_actions_per_peer = None; let mut peer_states = Vec::new(); for (_, peer_state_mutex) in per_peer_state.iter() { @@ -18742,6 +18818,7 @@ impl< peer_state.get_mut().unwrap().latest_features = latest_features; } } + // TODO: pending trampoline forwards? // Post-deserialization processing let mut decode_update_add_htlcs = new_hash_map(); @@ -19662,6 +19739,7 @@ impl< claimable_payments, pending_claiming_payments, }), + awaiting_trampoline_forwards: Mutex::new(new_hash_map()), outbound_scid_aliases: Mutex::new(outbound_scid_aliases), short_to_chan_info: FairRwLock::new(short_to_chan_info), fake_scid_rand_bytes: fake_scid_rand_bytes.unwrap(), From 60c073cdd8a3f0ac97da2f6a53a41128cee163cd Mon Sep 17 00:00:00 2001 From: Carla Kirk-Cohen Date: Wed, 11 Feb 2026 10:35:07 +0200 Subject: [PATCH 57/68] ln/refactor: move checks on incoming mpp accumulation into method We're going to use the same logic for trampoline and for incoming MPP payments, so we pull this out into a separate function. --- lightning/src/ln/channelmanager.rs | 272 +++++++++++++++++------------ 1 file changed, 161 insertions(+), 111 deletions(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index f7353a6ca2f..bfed81cc30d 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -7944,6 +7944,115 @@ impl< } } + // Checks whether an incoming htlc can be added to our [`claimable_payments`], and handles + // MPP accumulation. Returns a boolean that indicates whether we're modified our set of + // claimable_payments, and a result that indicates whether the all the parts of the htlc + // have successfully arrived. + fn check_claimable_incoming_htlc( + &self, purpose: events::PaymentPurpose, receiver_node_id: PublicKey, + claimable_htlc: ClaimableHTLC, mut onion_fields: RecipientOnionFields, + payment_hash: PaymentHash, + new_events: &mut VecDeque<(Event, Option)>, + ) -> (bool, Result) { + let mut committed_to_claimable = false; + let is_keysend = purpose.is_keysend(); + let mut claimable_payments = self.claimable_payments.lock().unwrap(); + if claimable_payments.pending_claiming_payments.contains_key(&payment_hash) { + return (committed_to_claimable, Err(())); + } + + let ref mut claimable_payment = claimable_payments + .claimable_payments + .entry(payment_hash) + // Note that if we insert here we MUST NOT fail_htlc!() + .or_insert_with(|| { + committed_to_claimable = true; + ClaimablePayment { purpose: purpose.clone(), htlcs: Vec::new(), onion_fields: None } + }); + if purpose != claimable_payment.purpose { + let log_keysend = |keysend| if keysend { "keysend" } else { "non-keysend" }; + log_trace!(self.logger, "Failing new {} HTLC with payment_hash {} as we already had an existing {} HTLC with the same payment hash", log_keysend(is_keysend), &payment_hash, log_keysend(!is_keysend)); + return (committed_to_claimable, Err(())); + } + if let Some(earlier_fields) = &mut claimable_payment.onion_fields { + if earlier_fields.check_merge(&mut onion_fields).is_err() { + return (committed_to_claimable, Err(())); + } + } else { + claimable_payment.onion_fields = Some(onion_fields); + } + let mut total_value = claimable_htlc.sender_intended_value; + let mut earliest_expiry = claimable_htlc.cltv_expiry; + for htlc in claimable_payment.htlcs.iter() { + total_value += htlc.sender_intended_value; + earliest_expiry = cmp::min(earliest_expiry, htlc.cltv_expiry); + if htlc.total_msat != claimable_htlc.total_msat { + log_trace!(self.logger, "Failing HTLCs with payment_hash {} as the HTLCs had inconsistent total values (eg {} and {})", + &payment_hash, claimable_htlc.total_msat, htlc.total_msat); + total_value = msgs::MAX_VALUE_MSAT; + } + if total_value >= msgs::MAX_VALUE_MSAT { + break; + } + } + // 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 { + return (committed_to_claimable, Err(())); + } else if total_value - claimable_htlc.sender_intended_value >= claimable_htlc.total_msat { + log_trace!( + self.logger, + "Failing HTLC with payment_hash {} as payment is already claimable", + &payment_hash + ); + return (committed_to_claimable, Err(())); + } else if total_value >= claimable_htlc.total_msat { + #[allow(unused_assignments)] + { + committed_to_claimable = true; + } + claimable_payment.htlcs.push(claimable_htlc); + let amount_msat = claimable_payment.htlcs.iter().map(|htlc| htlc.value).sum(); + claimable_payment + .htlcs + .iter_mut() + .for_each(|htlc| htlc.total_value_received = Some(amount_msat)); + let counterparty_skimmed_fee_msat = claimable_payment + .htlcs + .iter() + .map(|htlc| htlc.counterparty_skimmed_fee_msat.unwrap_or(0)) + .sum(); + debug_assert!(total_value.saturating_sub(amount_msat) <= counterparty_skimmed_fee_msat); + claimable_payment.htlcs.sort(); + let payment_id = claimable_payment.inbound_payment_id(&self.inbound_payment_id_secret); + new_events.push_back(( + events::Event::PaymentClaimable { + receiver_node_id: Some(receiver_node_id), + payment_hash, + purpose, + amount_msat, + counterparty_skimmed_fee_msat, + receiving_channel_ids: claimable_payment.receiving_channel_ids(), + claim_deadline: Some(earliest_expiry - HTLC_FAIL_BACK_BUFFER), + onion_fields: claimable_payment.onion_fields.clone(), + payment_id: Some(payment_id), + }, + None, + )); + (committed_to_claimable, Ok(true)) + } else { + // Nothing to do - we haven't reached the total + // payment value yet, wait until we receive more + // MPP parts. + claimable_payment.htlcs.push(claimable_htlc); + #[allow(unused_assignments)] + { + committed_to_claimable = true; + } + (committed_to_claimable, Ok(false)) + } + } + fn process_receive_htlcs( &self, pending_forwards: &mut Vec, new_events: &mut VecDeque<(Event, Option)>, @@ -7974,7 +8083,7 @@ impl< payment_data, payment_context, phantom_shared_secret, - mut onion_fields, + onion_fields, has_recipient_created_payment_secret, invoice_request_opt, trampoline_shared_secret, @@ -8046,12 +8155,26 @@ impl< panic!("short_channel_id == 0 should imply any pending_forward entries are of type Receive"); }, }; + let htlc_value = incoming_amt_msat.unwrap_or(outgoing_amt_msat); + let htlc_source = HTLCSource::PreviousHopData(HTLCPreviousHopData { + prev_outbound_scid_alias: prev_hop.prev_outbound_scid_alias, + user_channel_id: prev_hop.user_channel_id, + counterparty_node_id: prev_hop.counterparty_node_id, + channel_id: prev_channel_id, + outpoint: prev_funding_outpoint, + htlc_id: prev_hop.htlc_id, + incoming_packet_shared_secret: prev_hop.incoming_packet_shared_secret, + phantom_shared_secret, + trampoline_shared_secret, + blinded_failure, + cltv_expiry: Some(cltv_expiry), + }); let claimable_htlc = ClaimableHTLC { prev_hop, // 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), + value: htlc_value, sender_intended_value: outgoing_amt_msat, timer_ticks: 0, total_value_received: None, @@ -8065,38 +8188,20 @@ impl< counterparty_skimmed_fee_msat: skimmed_fee_msat, }; - let mut committed_to_claimable = false; - macro_rules! fail_htlc { ($htlc: expr, $payment_hash: expr) => { - debug_assert!(!committed_to_claimable); let err_data = invalid_payment_err_data( - $htlc.value, + htlc_value, self.best_block.read().unwrap().height, ); - let counterparty_node_id = $htlc.prev_hop.counterparty_node_id; - let incoming_packet_shared_secret = - $htlc.prev_hop.incoming_packet_shared_secret; - let prev_outbound_scid_alias = $htlc.prev_hop.prev_outbound_scid_alias; failed_forwards.push(( - HTLCSource::PreviousHopData(HTLCPreviousHopData { - prev_outbound_scid_alias, - user_channel_id: $htlc.prev_hop.user_channel_id, - counterparty_node_id, - channel_id: prev_channel_id, - outpoint: prev_funding_outpoint, - htlc_id: $htlc.prev_hop.htlc_id, - incoming_packet_shared_secret, - phantom_shared_secret, - trampoline_shared_secret, - blinded_failure, - cltv_expiry: Some(cltv_expiry), - }), + htlc_source, payment_hash, HTLCFailReason::reason( LocalHTLCFailureReason::IncorrectPaymentDetails, err_data, ), + // TODO: could be trampoline? HTLCHandlingFailureType::Receive { payment_hash: $payment_hash }, )); continue 'next_forwardable_htlc; @@ -8111,94 +8216,25 @@ impl< .expect("Failed to get node_id for phantom node recipient"); } - macro_rules! check_total_value { - ($purpose: expr) => {{ - let mut payment_claimable_generated = false; - let is_keysend = $purpose.is_keysend(); - let mut claimable_payments = self.claimable_payments.lock().unwrap(); - if claimable_payments.pending_claiming_payments.contains_key(&payment_hash) { - fail_htlc!(claimable_htlc, payment_hash); - } - let ref mut claimable_payment = claimable_payments.claimable_payments - .entry(payment_hash) - // Note that if we insert here we MUST NOT fail_htlc!() - .or_insert_with(|| { - committed_to_claimable = true; - ClaimablePayment { - purpose: $purpose.clone(), htlcs: Vec::new(), onion_fields: None, - } - }); - if $purpose != claimable_payment.purpose { - let log_keysend = |keysend| if keysend { "keysend" } else { "non-keysend" }; - log_trace!(self.logger, "Failing new {} HTLC with payment_hash {} as we already had an existing {} HTLC with the same payment hash", log_keysend(is_keysend), &payment_hash, log_keysend(!is_keysend)); - fail_htlc!(claimable_htlc, payment_hash); - } - if let Some(earlier_fields) = &mut claimable_payment.onion_fields { - if earlier_fields.check_merge(&mut onion_fields).is_err() { + macro_rules! handle_incoming_htlc { + ($purpose: expr, $receiver_node_id: expr, $claimable_htlc: expr, $onion_fields: expr, + $payment_hash: expr, $new_events: expr) => {{ + let (committed_to_claimable, res) = self.check_claimable_incoming_htlc( + $purpose, + $receiver_node_id, + $claimable_htlc, + $onion_fields, + $payment_hash, + $new_events, + ); + match res { + Ok(mpp_complete) => mpp_complete, + Err(_) => { + debug_assert!(!committed_to_claimable); fail_htlc!(claimable_htlc, payment_hash); - } - } else { - claimable_payment.onion_fields = Some(onion_fields); - } - let mut total_value = claimable_htlc.sender_intended_value; - let mut earliest_expiry = claimable_htlc.cltv_expiry; - for htlc in claimable_payment.htlcs.iter() { - total_value += htlc.sender_intended_value; - earliest_expiry = cmp::min(earliest_expiry, htlc.cltv_expiry); - if htlc.total_msat != claimable_htlc.total_msat { - log_trace!(self.logger, "Failing HTLCs with payment_hash {} as the HTLCs had inconsistent total values (eg {} and {})", - &payment_hash, claimable_htlc.total_msat, htlc.total_msat); - total_value = msgs::MAX_VALUE_MSAT; - } - if total_value >= msgs::MAX_VALUE_MSAT { break; } - } - // 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.sender_intended_value >= claimable_htlc.total_msat { - log_trace!(self.logger, "Failing HTLC with payment_hash {} as payment is already claimable", - &payment_hash); - fail_htlc!(claimable_htlc, payment_hash); - } else if total_value >= claimable_htlc.total_msat { - #[allow(unused_assignments)] { - committed_to_claimable = true; - } - claimable_payment.htlcs.push(claimable_htlc); - let amount_msat = - claimable_payment.htlcs.iter().map(|htlc| htlc.value).sum(); - claimable_payment.htlcs.iter_mut() - .for_each(|htlc| htlc.total_value_received = Some(amount_msat)); - let counterparty_skimmed_fee_msat = claimable_payment.htlcs.iter() - .map(|htlc| htlc.counterparty_skimmed_fee_msat.unwrap_or(0)).sum(); - debug_assert!(total_value.saturating_sub(amount_msat) <= - counterparty_skimmed_fee_msat); - claimable_payment.htlcs.sort(); - let payment_id = - claimable_payment.inbound_payment_id(&self.inbound_payment_id_secret); - new_events.push_back((events::Event::PaymentClaimable { - receiver_node_id: Some(receiver_node_id), - payment_hash, - purpose: $purpose, - amount_msat, - counterparty_skimmed_fee_msat, - receiving_channel_ids: claimable_payment.receiving_channel_ids(), - claim_deadline: Some(earliest_expiry - HTLC_FAIL_BACK_BUFFER), - onion_fields: claimable_payment.onion_fields.clone(), - payment_id: Some(payment_id), - }, None)); - payment_claimable_generated = true; - } else { - // Nothing to do - we haven't reached the total - // payment value yet, wait until we receive more - // MPP parts. - claimable_payment.htlcs.push(claimable_htlc); - #[allow(unused_assignments)] { - committed_to_claimable = true; - } + }, } - payment_claimable_generated - }} + }}; } // Check that the payment hash and secret are known. Note that we @@ -8254,7 +8290,14 @@ impl< fail_htlc!(claimable_htlc, payment_hash); }, }; - check_total_value!(purpose); + handle_incoming_htlc!( + purpose, + receiver_node_id, + claimable_htlc, + onion_fields, + payment_hash, + new_events + ); }, OnionPayload::Spontaneous(keysend_preimage) => { let purpose = if let Some(PaymentContext::AsyncBolt12Offer( @@ -8318,7 +8361,14 @@ impl< } else { events::PaymentPurpose::SpontaneousPayment(keysend_preimage) }; - check_total_value!(purpose); + handle_incoming_htlc!( + purpose, + receiver_node_id, + claimable_htlc, + onion_fields, + payment_hash, + new_events + ); }, OnionPayload::Trampoline { .. } => { todo!(); From a04e211f11fca0594ab8e733196fdd8b7ea115ba Mon Sep 17 00:00:00 2001 From: Carla Kirk-Cohen Date: Fri, 23 Jan 2026 14:26:35 -0500 Subject: [PATCH 58/68] ln: handle claimable htlcs for payments in dedicated method --- lightning/src/ln/channelmanager.rs | 191 +++++++++++++++-------------- 1 file changed, 99 insertions(+), 92 deletions(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index bfed81cc30d..086c8e3bc52 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -1219,6 +1219,11 @@ impl ClaimablePayment { .map(|htlc| (htlc.prev_hop.channel_id, htlc.prev_hop.user_channel_id)) .collect() } + + /// Returns the total counterparty skimmed fee across all HTLCs. + fn total_counterparty_skimmed_msat(&self) -> u64 { + self.htlcs.iter().map(|htlc| htlc.counterparty_skimmed_fee_msat.unwrap_or(0)).sum() + } } /// Increments MPP timeout tick for all HTLCs and returns a boolean indicating whether the HTLC @@ -7945,47 +7950,22 @@ impl< } // Checks whether an incoming htlc can be added to our [`claimable_payments`], and handles - // MPP accumulation. Returns a boolean that indicates whether we're modified our set of - // claimable_payments, and a result that indicates whether the all the parts of the htlc - // have successfully arrived. + // MPP accumulation. On successful add, returns Ok() with a boolean indicating whether all + // MPP parts have arrrived. Callers *MUST NOT* fail htlcs if Ok(..) is returned. fn check_claimable_incoming_htlc( - &self, purpose: events::PaymentPurpose, receiver_node_id: PublicKey, - claimable_htlc: ClaimableHTLC, mut onion_fields: RecipientOnionFields, - payment_hash: PaymentHash, - new_events: &mut VecDeque<(Event, Option)>, - ) -> (bool, Result) { - let mut committed_to_claimable = false; - let is_keysend = purpose.is_keysend(); - let mut claimable_payments = self.claimable_payments.lock().unwrap(); - if claimable_payments.pending_claiming_payments.contains_key(&payment_hash) { - return (committed_to_claimable, Err(())); - } - - let ref mut claimable_payment = claimable_payments - .claimable_payments - .entry(payment_hash) - // Note that if we insert here we MUST NOT fail_htlc!() - .or_insert_with(|| { - committed_to_claimable = true; - ClaimablePayment { purpose: purpose.clone(), htlcs: Vec::new(), onion_fields: None } - }); - if purpose != claimable_payment.purpose { - let log_keysend = |keysend| if keysend { "keysend" } else { "non-keysend" }; - log_trace!(self.logger, "Failing new {} HTLC with payment_hash {} as we already had an existing {} HTLC with the same payment hash", log_keysend(is_keysend), &payment_hash, log_keysend(!is_keysend)); - return (committed_to_claimable, Err(())); - } + &self, claimable_payment: &mut ClaimablePayment, claimable_htlc: ClaimableHTLC, + mut onion_fields: RecipientOnionFields, payment_hash: PaymentHash, + ) -> Result { if let Some(earlier_fields) = &mut claimable_payment.onion_fields { if earlier_fields.check_merge(&mut onion_fields).is_err() { - return (committed_to_claimable, Err(())); + return Err(()); } } else { claimable_payment.onion_fields = Some(onion_fields); } let mut total_value = claimable_htlc.sender_intended_value; - let mut earliest_expiry = claimable_htlc.cltv_expiry; for htlc in claimable_payment.htlcs.iter() { total_value += htlc.sender_intended_value; - earliest_expiry = cmp::min(earliest_expiry, htlc.cltv_expiry); if htlc.total_msat != claimable_htlc.total_msat { log_trace!(self.logger, "Failing HTLCs with payment_hash {} as the HTLCs had inconsistent total values (eg {} and {})", &payment_hash, claimable_htlc.total_msat, htlc.total_msat); @@ -7998,19 +7978,15 @@ impl< // 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 { - return (committed_to_claimable, Err(())); + return Err(()); } else if total_value - claimable_htlc.sender_intended_value >= claimable_htlc.total_msat { log_trace!( self.logger, "Failing HTLC with payment_hash {} as payment is already claimable", &payment_hash ); - return (committed_to_claimable, Err(())); + return Err(()); } else if total_value >= claimable_htlc.total_msat { - #[allow(unused_assignments)] - { - committed_to_claimable = true; - } claimable_payment.htlcs.push(claimable_htlc); let amount_msat = claimable_payment.htlcs.iter().map(|htlc| htlc.value).sum(); claimable_payment @@ -8024,33 +8000,79 @@ impl< .sum(); debug_assert!(total_value.saturating_sub(amount_msat) <= counterparty_skimmed_fee_msat); claimable_payment.htlcs.sort(); - let payment_id = claimable_payment.inbound_payment_id(&self.inbound_payment_id_secret); + + Ok(true) + } else { + // Nothing to do - we haven't reached the total + // payment value yet, wait until we receive more + // MPP parts. + claimable_payment.htlcs.push(claimable_htlc); + Ok(false) + } + } + + // Handles the addition of a HTLC associated with a payment we're receiving. Err(bool) indicates + // whether we have failed after adding committing to the HTLC - callers should assert that this + // value is false. + fn handle_claimable_htlc( + &self, purpose: events::PaymentPurpose, claimable_htlc: ClaimableHTLC, + onion_fields: RecipientOnionFields, payment_hash: PaymentHash, receiver_node_id: PublicKey, + new_events: &mut VecDeque<(Event, Option)>, + ) -> Result<(), bool> { + let mut committed_to_claimable = false; + + let mut claimable_payments = self.claimable_payments.lock().unwrap(); + if claimable_payments.pending_claiming_payments.contains_key(&payment_hash) { + return Err(committed_to_claimable); + } + + let ref mut claimable_payment = claimable_payments + .claimable_payments + .entry(payment_hash) + // Note that if we insert here we MUST NOT fail_htlc!() + .or_insert_with(|| { + committed_to_claimable = true; + ClaimablePayment { purpose: purpose.clone(), htlcs: Vec::new(), onion_fields: None } + }); + + let is_keysend = purpose.is_keysend(); + if purpose != claimable_payment.purpose { + let log_keysend = |keysend| if keysend { "keysend" } else { "non-keysend" }; + log_trace!(self.logger, "Failing new {} HTLC with payment_hash {} as we already had an existing {} HTLC with the same payment hash", log_keysend(is_keysend), &payment_hash, log_keysend(!is_keysend)); + return Err(committed_to_claimable); + } + + if self + .check_claimable_incoming_htlc( + claimable_payment, + claimable_htlc, + onion_fields, + payment_hash, + ) + .map_err(|_| committed_to_claimable)? + { new_events.push_back(( events::Event::PaymentClaimable { receiver_node_id: Some(receiver_node_id), payment_hash, purpose, - amount_msat, - counterparty_skimmed_fee_msat, + amount_msat: claimable_payment.htlcs.iter().map(|htlc| htlc.value).sum(), + counterparty_skimmed_fee_msat: claimable_payment + .total_counterparty_skimmed_msat(), receiving_channel_ids: claimable_payment.receiving_channel_ids(), - claim_deadline: Some(earliest_expiry - HTLC_FAIL_BACK_BUFFER), + claim_deadline: Some( + claimable_payment.htlcs.iter().map(|h| h.cltv_expiry).min().unwrap() // TODO: don't unwrap + - HTLC_FAIL_BACK_BUFFER, + ), onion_fields: claimable_payment.onion_fields.clone(), - payment_id: Some(payment_id), + payment_id: Some( + claimable_payment.inbound_payment_id(&self.inbound_payment_id_secret), + ), }, None, )); - (committed_to_claimable, Ok(true)) - } else { - // Nothing to do - we haven't reached the total - // payment value yet, wait until we receive more - // MPP parts. - claimable_payment.htlcs.push(claimable_htlc); - #[allow(unused_assignments)] - { - committed_to_claimable = true; - } - (committed_to_claimable, Ok(false)) } + Ok(()) } fn process_receive_htlcs( @@ -8189,7 +8211,8 @@ impl< }; macro_rules! fail_htlc { - ($htlc: expr, $payment_hash: expr) => { + ($htlc: expr, $payment_hash: expr, $committed_to_claimable: expr) => { + debug_assert!(!$committed_to_claimable); let err_data = invalid_payment_err_data( htlc_value, self.best_block.read().unwrap().height, @@ -8216,27 +8239,6 @@ impl< .expect("Failed to get node_id for phantom node recipient"); } - macro_rules! handle_incoming_htlc { - ($purpose: expr, $receiver_node_id: expr, $claimable_htlc: expr, $onion_fields: expr, - $payment_hash: expr, $new_events: expr) => {{ - let (committed_to_claimable, res) = self.check_claimable_incoming_htlc( - $purpose, - $receiver_node_id, - $claimable_htlc, - $onion_fields, - $payment_hash, - $new_events, - ); - match res { - Ok(mpp_complete) => mpp_complete, - Err(_) => { - debug_assert!(!committed_to_claimable); - fail_htlc!(claimable_htlc, payment_hash); - }, - } - }}; - } - // Check that the payment hash and secret are known. Note that we // MUST take care to handle the "unknown payment hash" and // "incorrect payment secret" cases here identically or we'd expose @@ -8256,7 +8258,7 @@ impl< Ok(result) => result, Err(()) => { log_trace!(self.logger, "Failing new HTLC with payment_hash {} as payment verification failed", &payment_hash); - fail_htlc!(claimable_htlc, payment_hash); + fail_htlc!(claimable_htlc, payment_hash, false); }, }; if let Some(min_final_cltv_expiry_delta) = min_final_cltv_expiry_delta { @@ -8266,12 +8268,12 @@ impl< if (cltv_expiry as u64) < expected_min_expiry_height { log_trace!(self.logger, "Failing new HTLC with payment_hash {} as its CLTV expiry was too soon (had {}, earliest expected {})", &payment_hash, cltv_expiry, expected_min_expiry_height); - fail_htlc!(claimable_htlc, payment_hash); + fail_htlc!(claimable_htlc, payment_hash, false); } } payment_preimage } else { - fail_htlc!(claimable_htlc, payment_hash); + fail_htlc!(claimable_htlc, payment_hash, false); } } else { None @@ -8287,17 +8289,20 @@ impl< let purpose = match from_parts_res { Ok(purpose) => purpose, Err(()) => { - fail_htlc!(claimable_htlc, payment_hash); + fail_htlc!(claimable_htlc, payment_hash, false); }, }; - handle_incoming_htlc!( + + if let Err(committed_to_claimable) = self.handle_claimable_htlc( purpose, - receiver_node_id, claimable_htlc, onion_fields, payment_hash, - new_events - ); + receiver_node_id, + new_events, + ) { + fail_htlc!(claimable_htlc, payment_hash, committed_to_claimable); + } }, OnionPayload::Spontaneous(keysend_preimage) => { let purpose = if let Some(PaymentContext::AsyncBolt12Offer( @@ -8311,7 +8316,7 @@ impl< false, "We checked that payment_data is Some above" ); - fail_htlc!(claimable_htlc, payment_hash); + fail_htlc!(claimable_htlc, payment_hash, false); }, }; @@ -8330,13 +8335,13 @@ impl< verified_invreq.amount_msats() { if payment_data.total_msat < invreq_amt_msat { - fail_htlc!(claimable_htlc, payment_hash); + fail_htlc!(claimable_htlc, payment_hash, false); } } verified_invreq }, None => { - fail_htlc!(claimable_htlc, payment_hash); + fail_htlc!(claimable_htlc, payment_hash, false); }, }; let payment_purpose_context = @@ -8352,23 +8357,25 @@ impl< match from_parts_res { Ok(purpose) => purpose, Err(()) => { - fail_htlc!(claimable_htlc, payment_hash); + fail_htlc!(claimable_htlc, payment_hash, false); }, } } else if payment_context.is_some() { log_trace!(self.logger, "Failing new HTLC with payment_hash {}: received a keysend payment to a non-async payments context {:#?}", payment_hash, payment_context); - fail_htlc!(claimable_htlc, payment_hash); + fail_htlc!(claimable_htlc, payment_hash, false); } else { events::PaymentPurpose::SpontaneousPayment(keysend_preimage) }; - handle_incoming_htlc!( + if let Err(committed_to_claimable) = self.handle_claimable_htlc( purpose, - receiver_node_id, claimable_htlc, onion_fields, payment_hash, - new_events - ); + receiver_node_id, + new_events, + ) { + fail_htlc!(claimable_htlc, payment_hash, committed_to_claimable); + } }, OnionPayload::Trampoline { .. } => { todo!(); From c1af8ebef52d888a59607cdda534d0a16d320589 Mon Sep 17 00:00:00 2001 From: Carla Kirk-Cohen Date: Thu, 12 Feb 2026 14:08:43 +0200 Subject: [PATCH 59/68] ln: move receive-specific failures into fail_htlc macro We'll only use this for non-trampoline incoming accumulated htlcs, because we want different source/failure for trampoline. --- lightning/src/ln/channelmanager.rs | 61 ++++++++++++++++-------------- 1 file changed, 33 insertions(+), 28 deletions(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 086c8e3bc52..aeb2562a09f 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -8178,19 +8178,12 @@ impl< }, }; let htlc_value = incoming_amt_msat.unwrap_or(outgoing_amt_msat); - let htlc_source = HTLCSource::PreviousHopData(HTLCPreviousHopData { - prev_outbound_scid_alias: prev_hop.prev_outbound_scid_alias, - user_channel_id: prev_hop.user_channel_id, - counterparty_node_id: prev_hop.counterparty_node_id, - channel_id: prev_channel_id, - outpoint: prev_funding_outpoint, - htlc_id: prev_hop.htlc_id, - incoming_packet_shared_secret: prev_hop.incoming_packet_shared_secret, - phantom_shared_secret, - trampoline_shared_secret, - blinded_failure, - cltv_expiry: Some(cltv_expiry), - }); + let prev_outbound_scid_alias = prev_hop.prev_outbound_scid_alias; + let user_channel_id = prev_hop.user_channel_id; + let counterparty_node_id = prev_hop.counterparty_node_id; + let htlc_id = prev_hop.htlc_id; + let incoming_packet_shared_secret = prev_hop.incoming_packet_shared_secret; + let claimable_htlc = ClaimableHTLC { prev_hop, // We differentiate the received value from the sender intended value @@ -8210,8 +8203,21 @@ impl< counterparty_skimmed_fee_msat: skimmed_fee_msat, }; - macro_rules! fail_htlc { - ($htlc: expr, $payment_hash: expr, $committed_to_claimable: expr) => { + macro_rules! fail_receive_htlc { + ($committed_to_claimable: expr) => { + let htlc_source = HTLCSource::PreviousHopData(HTLCPreviousHopData { + prev_outbound_scid_alias, + user_channel_id, + counterparty_node_id, + channel_id: prev_channel_id, + outpoint: prev_funding_outpoint, + htlc_id, + incoming_packet_shared_secret, + phantom_shared_secret, + trampoline_shared_secret, + blinded_failure, + cltv_expiry: Some(cltv_expiry), + }); debug_assert!(!$committed_to_claimable); let err_data = invalid_payment_err_data( htlc_value, @@ -8224,8 +8230,7 @@ impl< LocalHTLCFailureReason::IncorrectPaymentDetails, err_data, ), - // TODO: could be trampoline? - HTLCHandlingFailureType::Receive { payment_hash: $payment_hash }, + HTLCHandlingFailureType::Receive { payment_hash }, )); continue 'next_forwardable_htlc; }; @@ -8258,7 +8263,7 @@ impl< Ok(result) => result, Err(()) => { log_trace!(self.logger, "Failing new HTLC with payment_hash {} as payment verification failed", &payment_hash); - fail_htlc!(claimable_htlc, payment_hash, false); + fail_receive_htlc!(false); }, }; if let Some(min_final_cltv_expiry_delta) = min_final_cltv_expiry_delta { @@ -8268,12 +8273,12 @@ impl< if (cltv_expiry as u64) < expected_min_expiry_height { log_trace!(self.logger, "Failing new HTLC with payment_hash {} as its CLTV expiry was too soon (had {}, earliest expected {})", &payment_hash, cltv_expiry, expected_min_expiry_height); - fail_htlc!(claimable_htlc, payment_hash, false); + fail_receive_htlc!(false); } } payment_preimage } else { - fail_htlc!(claimable_htlc, payment_hash, false); + fail_receive_htlc!(false); } } else { None @@ -8289,7 +8294,7 @@ impl< let purpose = match from_parts_res { Ok(purpose) => purpose, Err(()) => { - fail_htlc!(claimable_htlc, payment_hash, false); + fail_receive_htlc!(false); }, }; @@ -8301,7 +8306,7 @@ impl< receiver_node_id, new_events, ) { - fail_htlc!(claimable_htlc, payment_hash, committed_to_claimable); + fail_receive_htlc!(committed_to_claimable); } }, OnionPayload::Spontaneous(keysend_preimage) => { @@ -8316,7 +8321,7 @@ impl< false, "We checked that payment_data is Some above" ); - fail_htlc!(claimable_htlc, payment_hash, false); + fail_receive_htlc!(false); }, }; @@ -8335,13 +8340,13 @@ impl< verified_invreq.amount_msats() { if payment_data.total_msat < invreq_amt_msat { - fail_htlc!(claimable_htlc, payment_hash, false); + fail_receive_htlc!(false); } } verified_invreq }, None => { - fail_htlc!(claimable_htlc, payment_hash, false); + fail_receive_htlc!(false); }, }; let payment_purpose_context = @@ -8357,12 +8362,12 @@ impl< match from_parts_res { Ok(purpose) => purpose, Err(()) => { - fail_htlc!(claimable_htlc, payment_hash, false); + fail_receive_htlc!(false); }, } } else if payment_context.is_some() { log_trace!(self.logger, "Failing new HTLC with payment_hash {}: received a keysend payment to a non-async payments context {:#?}", payment_hash, payment_context); - fail_htlc!(claimable_htlc, payment_hash, false); + fail_receive_htlc!(false); } else { events::PaymentPurpose::SpontaneousPayment(keysend_preimage) }; @@ -8374,7 +8379,7 @@ impl< receiver_node_id, new_events, ) { - fail_htlc!(claimable_htlc, payment_hash, committed_to_claimable); + fail_receive_htlc!(committed_to_claimable); } }, OnionPayload::Trampoline { .. } => { From def83f9bec4a215a856ed8f9f9ec91929a081ba5 Mon Sep 17 00:00:00 2001 From: Carla Kirk-Cohen Date: Thu, 12 Feb 2026 09:46:56 +0200 Subject: [PATCH 60/68] [wip]: handling function for trampoline dispatch - [ ] Need to return real errors here - [ ] Need to be able to handle committed_to_claimable if we fail --- lightning/src/events/mod.rs | 13 +- lightning/src/ln/channelmanager.rs | 279 +++++++++++++++++++++- lightning/src/ln/functional_test_utils.rs | 3 + 3 files changed, 288 insertions(+), 7 deletions(-) diff --git a/lightning/src/events/mod.rs b/lightning/src/events/mod.rs index c9969f024c6..2ba6eb1ed3a 100644 --- a/lightning/src/events/mod.rs +++ b/lightning/src/events/mod.rs @@ -163,6 +163,9 @@ pub enum PaymentPurpose { /// Because this is a spontaneous payment, the payer generated their own preimage rather than us /// (the payee) providing a preimage. SpontaneousPayment(PaymentPreimage), + /// HTLCs terminating at our node are intended for forwarding onwards as a trampoline + /// forward. + Trampoline {}, } impl PaymentPurpose { @@ -173,6 +176,7 @@ impl PaymentPurpose { PaymentPurpose::Bolt12OfferPayment { payment_preimage, .. } => *payment_preimage, PaymentPurpose::Bolt12RefundPayment { payment_preimage, .. } => *payment_preimage, PaymentPurpose::SpontaneousPayment(preimage) => Some(*preimage), + PaymentPurpose::Trampoline {} => None, } } @@ -182,6 +186,7 @@ impl PaymentPurpose { PaymentPurpose::Bolt12OfferPayment { .. } => false, PaymentPurpose::Bolt12RefundPayment { .. } => false, PaymentPurpose::SpontaneousPayment(..) => true, + PaymentPurpose::Trampoline {} => false, } } @@ -229,8 +234,9 @@ impl_writeable_tlv_based_enum_legacy!(PaymentPurpose, (2, payment_secret, required), (4, payment_context, required), }, + (3, Trampoline) => {}, ; - (2, SpontaneousPayment) + (2, SpontaneousPayment), ); /// Information about an HTLC that is part of a payment that can be claimed. @@ -1940,6 +1946,11 @@ impl Writeable for Event { PaymentPurpose::SpontaneousPayment(preimage) => { payment_preimage = Some(*preimage); }, + PaymentPurpose::Trampoline {} => { + payment_secret = None; + payment_preimage = None; + payment_context = None; + }, } let skimmed_fee_opt = if counterparty_skimmed_fee_msat == 0 { None diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index aeb2562a09f..698c7ea6274 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -91,6 +91,7 @@ use crate::ln::outbound_payment::{ Bolt11PaymentError, Bolt12PaymentError, NextTrampolineHopInfo, OutboundPayments, PendingOutboundPayment, ProbeSendFailure, RecipientCustomTlvs, RecipientOnionFields, Retry, RetryableInvoiceRequest, RetryableSendFailure, SendAlongPathArgs, StaleExpiration, + TrampolineForwardInfo, }; use crate::ln::types::ChannelId; use crate::offers::async_receive_offer_cache::AsyncReceiveOfferCache; @@ -115,14 +116,14 @@ use crate::onion_message::offers::{OffersMessage, OffersMessageHandler}; use crate::routing::gossip::NodeId; use crate::routing::router::{ BlindedTail, FixedRouter, InFlightHtlcs, Path, Payee, PaymentParameters, Route, - RouteParameters, RouteParametersConfig, Router, + RouteParameters, RouteParametersConfig, Router, DEFAULT_MAX_PATH_COUNT, + MAX_PATH_LENGTH_ESTIMATE, }; use crate::sign::ecdsa::EcdsaChannelSigner; use crate::sign::{EntropySource, NodeSigner, Recipient, SignerProvider}; -#[cfg(any(feature = "_test_utils", test))] -use crate::types::features::Bolt11InvoiceFeatures; use crate::types::features::{ - Bolt12InvoiceFeatures, ChannelFeatures, ChannelTypeFeatures, InitFeatures, NodeFeatures, + Bolt11InvoiceFeatures, Bolt12InvoiceFeatures, ChannelFeatures, ChannelTypeFeatures, + InitFeatures, NodeFeatures, }; use crate::types::payment::{PaymentHash, PaymentPreimage, PaymentSecret}; use crate::types::string::UntrustedString; @@ -8075,6 +8076,198 @@ impl< Ok(()) } + // Handles the addition of a HTLC associated with a trampoline forward that we need to accumulate + // on the incoming link before forwarding onwards. If the HTLC is failed, it returns the source + // and error that should be used to fail the HTLC(s) back. + fn handle_trampoline_htlc( + &self, claimable_htlc: ClaimableHTLC, onion_fields: RecipientOnionFields, + payment_hash: PaymentHash, incoming_trampoline_shared_secret: [u8; 32], + next_hop_info: NextTrampolineHopInfo, next_node_id: PublicKey, + ) -> Result<(), (HTLCSource, HTLCFailReason)> { + let mut trampoline_payments = self.awaiting_trampoline_forwards.lock().unwrap(); + + let mut committed_to_claimable = false; + let claimable_payment = trampoline_payments.entry(payment_hash).or_insert_with(|| { + committed_to_claimable = true; + ClaimablePayment { + purpose: events::PaymentPurpose::Trampoline {}, + htlcs: Vec::new(), + onion_fields: None, + } + }); + + // If MPP hasn't fully arrived yet, return early (saving indentation below). + let prev_hop = claimable_htlc.prev_hop.clone(); + if !self + .check_claimable_incoming_htlc( + claimable_payment, + claimable_htlc, + onion_fields, + payment_hash, + ) + .map_err(|_| { + debug_assert!(!committed_to_claimable); + ( + // When we couldn't add a new HTLC, we just fail back our last received htlc, + // allowing others to wait for more MPP parts to arrive. If this was the first + // htlc we'll eventually clean up the awaiting_trampoline_forwards entry in + // our MPP timeout logic. + HTLCSource::TrampolineForward { + previous_hop_data: vec![prev_hop], + incoming_trampoline_shared_secret, + outbound_payment: None, + }, + HTLCFailReason::reason( + LocalHTLCFailureReason::InvalidTrampolineForward, + vec![], + ), + ) + })? { + return Ok(()); + } + + let incoming_amt_msat: u64 = claimable_payment.htlcs.iter().map(|h| h.value).sum(); + let incoming_cltv_expiry = + claimable_payment.htlcs.iter().map(|h| h.cltv_expiry).min().unwrap(); + + let (forwarding_fee_proportional_millionths, forwarding_fee_base_msat, cltv_delta) = { + let config = self.config.read().unwrap(); + ( + config.channel_config.forwarding_fee_proportional_millionths, + config.channel_config.forwarding_fee_base_msat, + config.channel_config.cltv_expiry_delta as u32, + ) + }; + let proportional_fee = + forwarding_fee_proportional_millionths as u64 * next_hop_info.amount_msat / 1_000_000; + let our_forwarding_fee_msat = proportional_fee + forwarding_fee_base_msat as u64; + + let trampoline_source = || -> HTLCSource { + HTLCSource::TrampolineForward { + previous_hop_data: claimable_payment + .htlcs + .iter() + .map(|htlc| htlc.prev_hop.clone()) + .collect(), + incoming_trampoline_shared_secret, + outbound_payment: None, + } + }; + let trampoline_failure = || -> HTLCFailReason { + let mut err_data = Vec::with_capacity(10); + err_data.extend_from_slice(&forwarding_fee_base_msat.to_be_bytes()); + err_data.extend_from_slice(&forwarding_fee_proportional_millionths.to_be_bytes()); + err_data.extend_from_slice(&(cltv_delta as u16).to_be_bytes()); + HTLCFailReason::reason( + LocalHTLCFailureReason::TrampolineFeeOrExpiryInsufficient, + err_data, + ) + }; + + let max_total_routing_fee_msat = match incoming_amt_msat + .checked_sub(our_forwarding_fee_msat + next_hop_info.amount_msat) + { + Some(amount) => amount, + None => { + return Err((trampoline_source(), trampoline_failure())); + }, + }; + + let max_total_cltv_expiry_delta = + match incoming_cltv_expiry.checked_sub(next_hop_info.cltv_expiry_height + cltv_delta) { + Some(cltv_delta) => cltv_delta, + None => { + return Err((trampoline_source(), trampoline_failure())); + }, + }; + + // Assume any Trampoline node supports MPP + let mut recipient_features = Bolt11InvoiceFeatures::empty(); + recipient_features.set_basic_mpp_optional(); + + let route_parameters = RouteParameters { + payment_params: PaymentParameters { + payee: Payee::Clear { + node_id: next_node_id, // TODO: this can be threaded through from above + route_hints: vec![], + features: Some(recipient_features), + // When sending a trampoline payment, we assume that the original sender has + // baked a final cltv into our instructions. + final_cltv_expiry_delta: 0, + }, + expiry_time: None, + max_total_cltv_expiry_delta, + max_path_count: DEFAULT_MAX_PATH_COUNT, + max_path_length: MAX_PATH_LENGTH_ESTIMATE / 2, + max_channel_saturation_power_of_half: 2, + previously_failed_channels: vec![], + previously_failed_blinded_path_idxs: vec![], + }, + final_value_msat: next_hop_info.amount_msat, + max_total_routing_fee_msat: Some(max_total_routing_fee_msat), + }; + + #[cfg(not(any(test, feature = "_test_utils")))] + let retry_strategy = Retry::Attempts(3); + #[cfg(any(test, feature = "_test_utils"))] + let retry_strategy = Retry::Attempts(0); + + log_debug!( + self.logger, + "Attempting to forward trampoline payment that pays us {} with {} fee budget ({} total, {} cltv max)", + our_forwarding_fee_msat, + max_total_routing_fee_msat, + next_hop_info.amount_msat, + max_total_cltv_expiry_delta, + ); + let result = self.pending_outbound_payments.send_payment_for_trampoline_forward( + PaymentId(payment_hash.0), + payment_hash, + TrampolineForwardInfo { + next_hop_info, + previous_hop_data: claimable_payment + .htlcs + .iter() + .map(|htlc| htlc.prev_hop.clone()) + .collect(), + incoming_trampoline_shared_secret, + forwading_fee_msat: our_forwarding_fee_msat, + }, + retry_strategy, + route_parameters.clone(), + &self.router, + self.list_usable_channels(), + || self.compute_inflight_htlcs(), + &self.entropy_source, + &self.node_signer, + self.current_best_block().height, + &self.pending_events, + |args| self.send_payment_along_path(args), + &WithContext::from(&self.logger, None, None, Some(payment_hash)), + ); + + let source = trampoline_source(); + if trampoline_payments.remove(&payment_hash).is_none() { + log_error!( + &self.logger, + "Dispatched trampoline payment: {} was not present in awaiting inbound", + payment_hash + ); + return Err(( + source, + HTLCFailReason::reason(LocalHTLCFailureReason::TemporaryTrampolineFailure, vec![]), + )); + } + + if let Err(_retryable_send_failure) = result { + return Err(( + source, + HTLCFailReason::reason(LocalHTLCFailureReason::TemporaryTrampolineFailure, vec![]), + )); + }; + Ok(()) + } + fn process_receive_htlcs( &self, pending_forwards: &mut Vec, new_events: &mut VecDeque<(Event, Option)>, @@ -8173,6 +8366,63 @@ impl< None, ) }, + PendingHTLCRouting::TrampolineForward { + incoming_shared_secret: incoming_trampoline_shared_secret, + onion_packet, + node_id: next_trampoline, + blinded, + incoming_cltv_expiry, + incoming_multipath_data, + next_trampoline_amt_msat, + next_trampoline_cltv_expiry, + } => { + // Trampoline forwards only *need* to have MPP data if they're + // multi-part. + let onion_fields = match incoming_multipath_data { + Some(ref final_mpp) => RecipientOnionFields::secret_only( + final_mpp.payment_secret, + final_mpp.total_msat, + ), + None => RecipientOnionFields::spontaneous_empty(outgoing_amt_msat), + }; + ( + incoming_cltv_expiry, + OnionPayload::Trampoline { + next_hop_info: NextTrampolineHopInfo { + onion_packet, + blinding_point: blinded.and_then(|b| { + b.next_blinding_override.or_else(|| { + let encrypted_tlvs_ss = self + .node_signer + .ecdh( + Recipient::Node, + &b.inbound_blinding_point, + None, + ) + .unwrap() + .secret_bytes(); + onion_utils::next_hop_pubkey( + &self.secp_ctx, + b.inbound_blinding_point, + &encrypted_tlvs_ss, + ) + .ok() + }) + }), + amount_msat: next_trampoline_amt_msat, + cltv_expiry_height: next_trampoline_cltv_expiry, + }, + next_trampoline, + }, + incoming_multipath_data, + None, + None, + onion_fields, + false, + None, + Some(incoming_trampoline_shared_secret), + ) + }, _ => { panic!("short_channel_id == 0 should imply any pending_forward entries are of type Receive"); }, @@ -8382,8 +8632,25 @@ impl< fail_receive_htlc!(committed_to_claimable); } }, - OnionPayload::Trampoline { .. } => { - todo!(); + OnionPayload::Trampoline { ref next_hop_info, next_trampoline } => { + let next_hop_info = next_hop_info.clone(); + if let Err((htlc_source, failure_reason)) = self.handle_trampoline_htlc( + claimable_htlc, + onion_fields, + payment_hash, + // Safe to unwrap because we set to Some above. + trampoline_shared_secret.unwrap(), + next_hop_info, + next_trampoline, + ) { + failed_forwards.push(( + htlc_source, + payment_hash, + failure_reason, + HTLCHandlingFailureType::TrampolineForward {}, + )); + continue 'next_forwardable_htlc; + } }, } }, diff --git a/lightning/src/ln/functional_test_utils.rs b/lightning/src/ln/functional_test_utils.rs index 4550f0b1147..1b7a44cb34e 100644 --- a/lightning/src/ln/functional_test_utils.rs +++ b/lightning/src/ln/functional_test_utils.rs @@ -3649,6 +3649,9 @@ pub fn do_pass_along_path<'a, 'b, 'c>(args: PassAlongPathArgs) -> Option onion_fields.as_ref().unwrap().payment_secret ); }, + PaymentPurpose::Trampoline {} => { + panic!("Trampoline should not emit PaymentClaimable"); + }, } assert_eq!(*amount_msat, recv_value); let channels = node.node.list_channels(); From 0756235b137185f0cbdeb423bf20e31a3631586c Mon Sep 17 00:00:00 2001 From: Carla Kirk-Cohen Date: Thu, 12 Feb 2026 15:33:54 +0200 Subject: [PATCH 61/68] ln: only fail trampoline payments backwards when payment state ready --- lightning/src/ln/channelmanager.rs | 87 ++++++++++++++++-------------- lightning/src/ln/onion_utils.rs | 2 +- 2 files changed, 48 insertions(+), 41 deletions(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 698c7ea6274..61b39c53e45 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -81,7 +81,9 @@ use crate::ln::onion_utils::{self}; use crate::ln::onion_utils::{ decode_fulfill_attribution_data, HTLCFailReason, LocalHTLCFailureReason, }; -use crate::ln::onion_utils::{process_fulfill_attribution_data, AttributionData}; +use crate::ln::onion_utils::{ + process_fulfill_attribution_data, AttributionData, DecodedOnionFailure, +}; use crate::ln::our_peer_storage::{EncryptedOurPeerStorage, PeerStorageMonitorHolder}; #[cfg(test)] use crate::ln::outbound_payment; @@ -9345,47 +9347,52 @@ impl< None, )); }, - HTLCSource::TrampolineForward { - previous_hop_data, - incoming_trampoline_shared_secret, - .. - } => { - // TODO: what do we want to do with this given we do not wish to propagate it directly? - let _decoded_onion_failure = - onion_error.decode_onion_failure(&self.secp_ctx, &self.logger, &source); - let incoming_trampoline_shared_secret = Some(*incoming_trampoline_shared_secret); - - // TODO: when we receive a failure from a single outgoing trampoline HTLC, we don't - // necessarily want to fail all of our incoming HTLCs back yet. We may have other - // outgoing HTLCs that need to resolve first. This will be tracked in our - // pending_outbound_payments in a followup. - for current_hop_data in previous_hop_data { - let incoming_packet_shared_secret = - ¤t_hop_data.incoming_packet_shared_secret; - let channel_id = ¤t_hop_data.channel_id; - let short_channel_id = ¤t_hop_data.prev_outbound_scid_alias; - let htlc_id = ¤t_hop_data.htlc_id; - let blinded_failure = ¤t_hop_data.blinded_failure; - log_trace!( - WithContext::from(&self.logger, None, Some(*channel_id), Some(*payment_hash)), - "Failing {}HTLC with payment_hash {} backwards from us following Trampoline forwarding failure: {:?}", - if blinded_failure.is_some() { "blinded " } else { "" }, &payment_hash, onion_error - ); - let onion_error = HTLCFailReason::reason( - LocalHTLCFailureReason::TemporaryTrampolineFailure, - Vec::new(), - ); - push_forward_htlcs_failure( - *short_channel_id, - get_htlc_forward_failure( - blinded_failure, - &onion_error, - incoming_packet_shared_secret, - &incoming_trampoline_shared_secret, - &None, - *htlc_id, + HTLCSource::TrampolineForward { previous_hop_data, .. } => { + if let Some(decoded_error) = self.pending_outbound_payments.trampoline_htlc_failed( + source, + payment_hash, + onion_error, + &self.secp_ctx, + &WithContext::from(&self.logger, None, None, Some(*payment_hash)), + ) { + let onion_error = match decoded_error { + DecodedOnionFailure { + onion_error_code: Some(error_code), + onion_error_data: Some(error_data), + .. + } if error_code.is_recipient_failure() => HTLCFailReason::reason(error_code, error_data), + _ => HTLCFailReason::reason( + LocalHTLCFailureReason::TemporaryTrampolineFailure, + Vec::new(), ), + }; + + for current_hop_data in previous_hop_data { + let incoming_packet_shared_secret = + ¤t_hop_data.incoming_packet_shared_secret; + let channel_id = ¤t_hop_data.channel_id; + let short_channel_id = ¤t_hop_data.prev_outbound_scid_alias; + let htlc_id = ¤t_hop_data.htlc_id; + let blinded_failure = ¤t_hop_data.blinded_failure; + log_trace!( + WithContext::from(&self.logger, None, Some(*channel_id), Some(*payment_hash)), + "Failing {}HTLC with payment_hash {} backwards from us following Trampoline forwarding failure at {}: {:?}", + if blinded_failure.is_some() { "blinded " } else { "" }, &payment_hash, + if decoded_error.payment_failed_permanently { "final node" } else {"intermediate hop"}, + decoded_error.onion_error_code, ); + push_forward_htlcs_failure( + *short_channel_id, + get_htlc_forward_failure( + blinded_failure, + &onion_error, + incoming_packet_shared_secret, + &None, + &None, + *htlc_id, + ), + ); + } } // We only want to emit a single event for trampoline failures, so we do it once diff --git a/lightning/src/ln/onion_utils.rs b/lightning/src/ln/onion_utils.rs index c7cef557568..65938fe6d2a 100644 --- a/lightning/src/ln/onion_utils.rs +++ b/lightning/src/ln/onion_utils.rs @@ -1794,7 +1794,7 @@ impl LocalHTLCFailureReason { /// Returns true if the failure is only sent by the final recipient. Note that this function /// only checks [`LocalHTLCFailureReason`] variants that represent bolt 04 errors directly, /// as it's intended to analyze errors we've received as a sender. - fn is_recipient_failure(&self) -> bool { + pub(super) fn is_recipient_failure(&self) -> bool { self.failure_code() == LocalHTLCFailureReason::IncorrectPaymentDetails.failure_code() || *self == LocalHTLCFailureReason::FinalIncorrectCLTVExpiry || *self == LocalHTLCFailureReason::FinalIncorrectHTLCAmount From 33a8f73b4862d8c7a9fbd5ed4595cb5b0f5f70ef Mon Sep 17 00:00:00 2001 From: Carla Kirk-Cohen Date: Fri, 16 Jan 2026 15:14:57 -0500 Subject: [PATCH 62/68] ln: add handling in malformed error handler - [ ] Not sure what this does, look into it --- lightning/src/ln/channelmanager.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 61b39c53e45..1d53747c5ce 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -12605,14 +12605,14 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ match peer_state.channel_by_id.entry(msg.channel_id) { hash_map::Entry::Occupied(mut chan_entry) => { if (msg.failure_code & 0x8000) == 0 { - let chan_err = ChannelError::close("Got update_fail_malformed_htlc with BADONION not set".to_owned()); - try_channel_entry!(self, peer_state, Err(chan_err), chan_entry); + let chan_err = ChannelError::close("Got update_fail_malformed_htlc with BADONION not set".to_owned()); + try_channel_entry!(self, peer_state, Err(chan_err), chan_entry); } if let Some(chan) = chan_entry.get_mut().as_funded_mut() { - try_channel_entry!(self, peer_state, chan.update_fail_malformed_htlc(&msg, HTLCFailReason::reason(msg.failure_code.into(), msg.sha256_of_onion.to_vec())), chan_entry); + try_channel_entry!(self, peer_state, chan.update_fail_malformed_htlc(&msg, HTLCFailReason::reason(msg.failure_code.into(), msg.sha256_of_onion.to_vec())), chan_entry); } else { - return try_channel_entry!(self, peer_state, Err(ChannelError::close( - "Got an update_fail_malformed_htlc message for an unfunded channel!".into())), chan_entry); + return try_channel_entry!(self, peer_state, Err(ChannelError::close( + "Got an update_fail_malformed_htlc message for an unfunded channel!".into())), chan_entry); } Ok(()) }, From 6946c6e5600b935087a12f3cfcfdabfcc3ca51f8 Mon Sep 17 00:00:00 2001 From: Carla Kirk-Cohen Date: Thu, 12 Feb 2026 13:31:49 +0200 Subject: [PATCH 63/68] ln/refactor: pass minimum delta into check_incoming_htlc_cltv For trampoline payments, we don't want to enforce a minimum cltv delta between our incoming and outer onion outgoing CLTV because we'll calculate our delta from the inner trampoline onion's value. However, we still want to check that we get at least the CLTV that the sending node intended for us and we still want to validate our incoming value. Refactor to allow setting a zero delta, for use for trampoline payments. --- lightning/src/ln/channelmanager.rs | 8 ++++++-- lightning/src/ln/onion_payment.rs | 6 +++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 1d53747c5ce..d9206f90d30 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -5049,8 +5049,12 @@ impl< }; let cur_height = self.best_block.read().unwrap().height + 1; - check_incoming_htlc_cltv(cur_height, next_hop.outgoing_cltv_value, msg.cltv_expiry)?; - + check_incoming_htlc_cltv( + cur_height, + next_hop.outgoing_cltv_value, + msg.cltv_expiry, + MIN_CLTV_EXPIRY_DELTA.into(), + )?; Ok(intercept) } diff --git a/lightning/src/ln/onion_payment.rs b/lightning/src/ln/onion_payment.rs index a359d9c3882..7e052d216fb 100644 --- a/lightning/src/ln/onion_payment.rs +++ b/lightning/src/ln/onion_payment.rs @@ -527,7 +527,7 @@ pub fn peel_payment_onion }; if let Err(reason) = check_incoming_htlc_cltv( - cur_height, outgoing_cltv_value, msg.cltv_expiry, + cur_height, outgoing_cltv_value, msg.cltv_expiry, MIN_CLTV_EXPIRY_DELTA.into(), ) { return Err(InboundHTLCErr { msg: "incoming cltv check failed", @@ -722,9 +722,9 @@ pub(super) fn decode_incoming_update_add_htlc_onion Result<(), LocalHTLCFailureReason> { - if (cltv_expiry as u64) < (outgoing_cltv_value) as u64 + MIN_CLTV_EXPIRY_DELTA as u64 { + if (cltv_expiry as u64) < (outgoing_cltv_value) as u64 + min_cltv_expiry_delta { return Err(LocalHTLCFailureReason::IncorrectCLTVExpiry); } // Theoretically, channel counterparty shouldn't send us a HTLC expiring now, From b1f3a490c4ff70f0ffd4cfdf8f9ec75750622aa7 Mon Sep 17 00:00:00 2001 From: Carla Kirk-Cohen Date: Fri, 16 Jan 2026 15:09:49 -0500 Subject: [PATCH 64/68] ln: process added trampoline htlcs with CLTV validation Remove error that was added to prevent forwarding of trampoline payments during development, and instead process incoming trampoline htlcs. We can't perform proper validation because we don't know the outgoing channel id until we forward the HTLC, so we just perform a basic CLTV check. --- lightning/src/ln/blinded_payment_tests.rs | 120 ---------------------- lightning/src/ln/channelmanager.rs | 20 +++- 2 files changed, 18 insertions(+), 122 deletions(-) diff --git a/lightning/src/ln/blinded_payment_tests.rs b/lightning/src/ln/blinded_payment_tests.rs index f854a411dff..e16efbffa4b 100644 --- a/lightning/src/ln/blinded_payment_tests.rs +++ b/lightning/src/ln/blinded_payment_tests.rs @@ -2751,123 +2751,3 @@ fn do_test_trampoline_relay(blinded: bool, test_case: TrampolineTestCase) { claim_payment(&nodes[0], &[&nodes[1], &nodes[2]], payment_preimage); } } - -#[test] -#[rustfmt::skip] -fn test_trampoline_forward_rejection() { - const TOTAL_NODE_COUNT: usize = 3; - - let chanmon_cfgs = create_chanmon_cfgs(TOTAL_NODE_COUNT); - let node_cfgs = create_node_cfgs(TOTAL_NODE_COUNT, &chanmon_cfgs); - let node_chanmgrs = create_node_chanmgrs(TOTAL_NODE_COUNT, &node_cfgs, &vec![None; TOTAL_NODE_COUNT]); - let mut nodes = create_network(TOTAL_NODE_COUNT, &node_cfgs, &node_chanmgrs); - - let (_, _, chan_id_alice_bob, _) = create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0); - let (_, _, chan_id_bob_carol, _) = create_announced_chan_between_nodes_with_value(&nodes, 1, 2, 1_000_000, 0); - - for i in 0..TOTAL_NODE_COUNT { // connect all nodes' blocks - connect_blocks(&nodes[i], (TOTAL_NODE_COUNT as u32) * CHAN_CONFIRM_DEPTH + 1 - nodes[i].best_block_info().1); - } - - let alice_node_id = nodes[0].node().get_our_node_id(); - let bob_node_id = nodes[1].node().get_our_node_id(); - let carol_node_id = nodes[2].node().get_our_node_id(); - - let alice_bob_scid = nodes[0].node().list_channels().iter().find(|c| c.channel_id == chan_id_alice_bob).unwrap().short_channel_id.unwrap(); - let bob_carol_scid = nodes[1].node().list_channels().iter().find(|c| c.channel_id == chan_id_bob_carol).unwrap().short_channel_id.unwrap(); - - let amt_msat = 1000; - let (payment_preimage, payment_hash, _) = get_payment_preimage_hash(&nodes[2], Some(amt_msat), None); - - let route = Route { - paths: vec![Path { - hops: vec![ - // Bob - RouteHop { - pubkey: bob_node_id, - node_features: NodeFeatures::empty(), - short_channel_id: alice_bob_scid, - channel_features: ChannelFeatures::empty(), - fee_msat: 1000, - cltv_expiry_delta: 48, - maybe_announced_channel: false, - }, - - // Carol - RouteHop { - pubkey: carol_node_id, - node_features: NodeFeatures::empty(), - short_channel_id: bob_carol_scid, - channel_features: ChannelFeatures::empty(), - fee_msat: 0, - cltv_expiry_delta: 24 + 24 + 39, - maybe_announced_channel: false, - } - ], - blinded_tail: Some(BlindedTail { - trampoline_hops: vec![ - // Carol - TrampolineHop { - pubkey: carol_node_id, - node_features: Features::empty(), - fee_msat: amt_msat, - cltv_expiry_delta: 24, - }, - - // Alice (unreachable) - TrampolineHop { - pubkey: alice_node_id, - node_features: Features::empty(), - fee_msat: amt_msat, - cltv_expiry_delta: 24, - }, - ], - hops: vec![BlindedHop{ - // Fake public key - blinded_node_id: alice_node_id, - encrypted_payload: vec![], - }], - blinding_point: alice_node_id, - excess_final_cltv_expiry_delta: 39, - final_value_msat: amt_msat, - }) - }], - route_params: None, - }; - - nodes[0].node.send_payment_with_route(route.clone(), payment_hash, RecipientOnionFields::spontaneous_empty(amt_msat), PaymentId(payment_hash.0)).unwrap(); - - check_added_monitors(&nodes[0], 1); - - let mut events = nodes[0].node.get_and_clear_pending_msg_events(); - assert_eq!(events.len(), 1); - let first_message_event = remove_first_msg_event_to_node(&nodes[1].node.get_our_node_id(), &mut events); - - let route: &[&Node] = &[&nodes[1], &nodes[2]]; - let args = PassAlongPathArgs::new(&nodes[0], route, amt_msat, payment_hash, first_message_event) - .with_payment_preimage(payment_preimage) - .without_claimable_event() - .expect_failure(HTLCHandlingFailureType::Receive { payment_hash }); - do_pass_along_path(args); - - { - let unblinded_node_updates = get_htlc_update_msgs(&nodes[2], &nodes[1].node.get_our_node_id()); - nodes[1].node.handle_update_fail_htlc( - nodes[2].node.get_our_node_id(), &unblinded_node_updates.update_fail_htlcs[0] - ); - do_commitment_signed_dance(&nodes[1], &nodes[2], &unblinded_node_updates.commitment_signed, true, false); - } - { - let unblinded_node_updates = get_htlc_update_msgs(&nodes[1], &nodes[0].node.get_our_node_id()); - nodes[0].node.handle_update_fail_htlc( - nodes[1].node.get_our_node_id(), &unblinded_node_updates.update_fail_htlcs[0] - ); - do_commitment_signed_dance(&nodes[0], &nodes[1], &unblinded_node_updates.commitment_signed, false, false); - } - { - // Expect UnknownNextPeer error while we are unable to route forwarding Trampoline payments. - let payment_failed_conditions = PaymentFailedConditions::new() - .expected_htlc_error_data(LocalHTLCFailureReason::UnknownNextPeer, &[0; 0]); - expect_payment_failed_conditions(&nodes[0], payment_hash, false, payment_failed_conditions); - } -} diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index d9206f90d30..18669e9771a 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -5001,6 +5001,7 @@ impl< fn can_forward_htlc_should_intercept( &self, msg: &msgs::UpdateAddHTLC, next_hop: &NextPacketDetails, ) -> Result { + let cur_height = self.best_block.read().unwrap().height + 1; let outgoing_scid = match next_hop.outgoing_connector { HopConnector::ShortChannelId(scid) => scid, HopConnector::Dummy => { @@ -5008,8 +5009,24 @@ impl< debug_assert!(false, "Dummy hop reached HTLC handling."); return Err(LocalHTLCFailureReason::InvalidOnionPayload); }, + // We can't make forwarding checks on trampoline forwards where we don't know the + // outgoing channel on receipt of the incoming htlc. Our trampoline logic will check + // our required delta and fee later on, so here we just check that the forwarding node + // did not "skim" off some of the sender's intended fee/cltv. HopConnector::Trampoline(_) => { - return Err(LocalHTLCFailureReason::InvalidTrampolineForward); + if msg.amount_msat < next_hop.outgoing_amt_msat { + return Err(LocalHTLCFailureReason::FeeInsufficient); + } + + check_incoming_htlc_cltv( + cur_height, + next_hop.outgoing_cltv_value, + msg.cltv_expiry, + 0, + )?; + + // TODO: what do we do about interception for trampoline? + return Ok(false); }, }; // TODO: We do the fake SCID namespace check a bunch of times here (and indirectly via @@ -5048,7 +5065,6 @@ impl< }, }; - let cur_height = self.best_block.read().unwrap().height + 1; check_incoming_htlc_cltv( cur_height, next_hop.outgoing_cltv_value, From 71c89c8384d74818da2017622693baba89b9617c Mon Sep 17 00:00:00 2001 From: Carla Kirk-Cohen Date: Mon, 2 Feb 2026 13:52:19 -0500 Subject: [PATCH 65/68] ln: use correct blinding point for trampoline payload decodes The blinding point that we pass in is supposed to be the "update add" blinding point equivalent, which in blinded trampoline relay is the one that we get in the outer onion. --- lightning/src/ln/onion_utils.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lightning/src/ln/onion_utils.rs b/lightning/src/ln/onion_utils.rs index 65938fe6d2a..d9b23fb9409 100644 --- a/lightning/src/ln/onion_utils.rs +++ b/lightning/src/ln/onion_utils.rs @@ -2460,7 +2460,10 @@ pub(crate) fn decode_next_payment_hop( &hop_data.trampoline_packet.hop_data, hop_data.trampoline_packet.hmac, Some(payment_hash), - (blinding_point, &node_signer), + // When we have a trampoline packet, the current_path_key in our outer onion + // payload plays the role of the update_add_htlc blinding_point for the inner + // onion. + (hop_data.current_path_key, node_signer), ); match decoded_trampoline_hop { Ok(( From 840970a26a470eff7bde21ded867441b6c99c7ae Mon Sep 17 00:00:00 2001 From: Carla Kirk-Cohen Date: Tue, 10 Feb 2026 09:57:43 +0200 Subject: [PATCH 66/68] ln/test: only use replacement onion in trampoline tests when needed Don't always blindly replace with a manually built test onion when we run trampoline tests (only for unblinded / failure cases where we need to mess with the onion). The we update our replacement onion logic to correctly match our internal behavior which adds one block to the current height when dispatching payments. --- lightning/src/ln/blinded_payment_tests.rs | 44 ++++++++++++----------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/lightning/src/ln/blinded_payment_tests.rs b/lightning/src/ln/blinded_payment_tests.rs index e16efbffa4b..220fc384d6b 100644 --- a/lightning/src/ln/blinded_payment_tests.rs +++ b/lightning/src/ln/blinded_payment_tests.rs @@ -2472,6 +2472,7 @@ fn replacement_onion( original_trampoline_cltv: u32, payment_hash: PaymentHash, payment_secret: PaymentSecret, blinded: bool, ) -> msgs::OnionPacket { + assert!(!blinded || !matches!(test_case, TrampolineTestCase::Success)); let outer_session_priv = SecretKey::from_slice(&override_random_bytes[..]).unwrap(); let trampoline_session_priv = onion_utils::compute_trampoline_session_priv(&outer_session_priv); let recipient_onion_fields = RecipientOnionFields::spontaneous_empty(original_amt_msat); @@ -2671,21 +2672,26 @@ fn do_test_trampoline_relay(blinded: bool, test_case: TrampolineTestCase) { // Replace the onion to test different scenarios: // - If !blinded: Creates a payload sending to an unblinded trampoline // - If blinded: Modifies outer onion to create outer/inner mismatches if testing failures - update_message.map(|msg| { - msg.onion_routing_packet = replacement_onion( - test_case, - &secp_ctx, - override_random_bytes, - route, - original_amt_msat, - starting_htlc_offset, - original_trampoline_cltv, - excess_final_cltv, - payment_hash, - payment_secret, - blinded, - ) - }); + if !blinded || !matches!(test_case, TrampolineTestCase::Success) { + update_message.map(|msg| { + msg.onion_routing_packet = replacement_onion( + test_case, + &secp_ctx, + override_random_bytes, + route, + original_amt_msat, + // Our internal send payment helpers add one block to the current height to + // create our payments. Do the same here so that our replacement onion will have + // the right cltv. + starting_htlc_offset + 1, + original_trampoline_cltv, + excess_final_cltv, + payment_hash, + payment_secret, + blinded, + ) + }); + } let route: &[&Node] = &[&nodes[1], &nodes[2]]; let args = PassAlongPathArgs::new( @@ -2696,10 +2702,9 @@ fn do_test_trampoline_relay(blinded: bool, test_case: TrampolineTestCase) { first_message_event, ); + let final_cltv_height = original_trampoline_cltv + starting_htlc_offset + excess_final_cltv + 1; let amt_bytes = test_case.outer_onion_amt(original_amt_msat).to_be_bytes(); - let cltv_bytes = test_case - .outer_onion_cltv(original_trampoline_cltv + starting_htlc_offset + excess_final_cltv) - .to_be_bytes(); + let cltv_bytes = test_case.outer_onion_cltv(final_cltv_height).to_be_bytes(); let payment_failure = test_case.payment_failed_conditions(&amt_bytes, &cltv_bytes).map(|p| { if blinded { PaymentFailedConditions::new() @@ -2713,8 +2718,7 @@ fn do_test_trampoline_relay(blinded: bool, test_case: TrampolineTestCase) { .without_claimable_event() .expect_failure(HTLCHandlingFailureType::Receive { payment_hash }) } else { - let htlc_cltv = starting_htlc_offset + original_trampoline_cltv + excess_final_cltv; - args.with_payment_secret(payment_secret).with_payment_claimable_cltv(htlc_cltv) + args.with_payment_secret(payment_secret).with_payment_claimable_cltv(final_cltv_height) }; do_pass_along_path(args); From af42a85f2ff9fa0018b1399793b191c7a5f226ed Mon Sep 17 00:00:00 2001 From: Carla Kirk-Cohen Date: Tue, 3 Feb 2026 09:23:11 -0500 Subject: [PATCH 67/68] [deleteme]: remove assertion that fails on unblinded test - [ ] Right now, we assume that the presence of a trampoline means that we're in a blinded route. This fails when we test an unblinded case (which we do to get coverage for forwarding). We likely need to decouple trampoline and blinded tail to allow this to work properly. --- lightning/src/routing/router.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lightning/src/routing/router.rs b/lightning/src/routing/router.rs index 407566ee566..154343ac0e7 100644 --- a/lightning/src/routing/router.rs +++ b/lightning/src/routing/router.rs @@ -1322,7 +1322,7 @@ impl PaymentParameters { found_blinded_tail = true; } } - debug_assert!(found_blinded_tail); + //debug_assert!(found_blinded_tail); } } From e8ac7ab1ea62dc59fe5d3be41b57e7e0d28a0252 Mon Sep 17 00:00:00 2001 From: Carla Kirk-Cohen Date: Tue, 10 Feb 2026 10:08:05 +0200 Subject: [PATCH 68/68] ln/test: add coverage for blinded and unblinded trampoline forwarding --- lightning/src/ln/blinded_payment_tests.rs | 445 ++++++++++++++++------ lightning/src/routing/router.rs | 2 +- 2 files changed, 340 insertions(+), 107 deletions(-) diff --git a/lightning/src/ln/blinded_payment_tests.rs b/lightning/src/ln/blinded_payment_tests.rs index 220fc384d6b..90cd70ae85f 100644 --- a/lightning/src/ln/blinded_payment_tests.rs +++ b/lightning/src/ln/blinded_payment_tests.rs @@ -8,8 +8,9 @@ // licenses. use crate::blinded_path::payment::{ - BlindedPaymentPath, Bolt12RefundContext, DummyTlvs, ForwardTlvs, PaymentConstraints, - PaymentContext, PaymentForwardNode, PaymentRelay, ReceiveTlvs, PAYMENT_PADDING_ROUND_OFF, + compute_aggregated_base_prop_fee, BlindedPaymentPath, Bolt12RefundContext, DummyTlvs, + ForwardTlvs, PaymentConstraints, PaymentContext, PaymentForwardNode, PaymentRelay, ReceiveTlvs, + TrampolineForwardTlvs, PAYMENT_PADDING_ROUND_OFF, }; use crate::blinded_path::utils::is_padded; use crate::blinded_path::{self, BlindedHop}; @@ -29,7 +30,8 @@ use crate::ln::types::ChannelId; use crate::offers::invoice::UnsignedBolt12Invoice; use crate::prelude::*; use crate::routing::router::{ - BlindedTail, Path, Payee, PaymentParameters, Route, RouteHop, RouteParameters, TrampolineHop, + compute_fees_saturating, BlindedTail, Path, Payee, PaymentParameters, Route, RouteHop, + RouteParameters, TrampolineHop, }; use crate::sign::{NodeSigner, PeerStorageKey, ReceiveAuthKey, Recipient}; use crate::types::features::{BlindedHopFeatures, ChannelFeatures, NodeFeatures}; @@ -41,6 +43,7 @@ use bitcoin::hex::DisplayHex; use bitcoin::secp256k1::ecdh::SharedSecret; use bitcoin::secp256k1::ecdsa::{RecoverableSignature, Signature}; use bitcoin::secp256k1::{schnorr, All, PublicKey, Scalar, Secp256k1, SecretKey}; +use bolt11_invoice::RoutingFees; use lightning_invoice::RawBolt11Invoice; use types::features::Features; @@ -2391,16 +2394,16 @@ impl<'a> TrampolineTestCase { } } - fn outer_onion_cltv(&self, outer_cltv: u32) -> u32 { + fn inner_onion_cltv(&self, outer_cltv: u32) -> u32 { if *self == TrampolineTestCase::OuterCLTVLessThanTrampoline { - return outer_cltv / 2; + return outer_cltv * 10; } outer_cltv } - fn outer_onion_amt(&self, original_amt: u64) -> u64 { + fn inner_onion_amt(&self, original_amt: u64) -> u64 { if *self == TrampolineTestCase::Underpayment { - return original_amt / 2; + return original_amt * 10; } original_amt } @@ -2420,28 +2423,53 @@ fn test_trampoline_blinded_receive() { do_test_trampoline_relay(true, TrampolineTestCase::OuterCLTVLessThanTrampoline); } -/// Creates a blinded tail where Carol receives via a blinded path. +/// Creates a blinded tail where Carol is the introduction point, Eve is a blinded trampoline +/// relay and Fred is the final recipient. fn create_blinded_tail( - secp_ctx: &Secp256k1, override_random_bytes: [u8; 32], carol_node_id: PublicKey, - carol_auth_key: ReceiveAuthKey, trampoline_cltv_expiry_delta: u32, - excess_final_cltv_delta: u32, final_value_msat: u64, payment_secret: PaymentSecret, + secp_ctx: &Secp256k1, override_random_bytes: [u8; 32], carol: (PublicKey, &PaymentRelay), + eve: (PublicKey, &PaymentRelay), fred_node_id: PublicKey, fred_auth_key: ReceiveAuthKey, + fred_cltv_final: u32, excess_final_cltv_delta: u32, final_value_msat: u64, + payment_secret: PaymentSecret, ) -> BlindedTail { let outer_session_priv = SecretKey::from_slice(&override_random_bytes).unwrap(); let trampoline_session_priv = onion_utils::compute_trampoline_session_priv(&outer_session_priv); let carol_blinding_point = PublicKey::from_secret_key(&secp_ctx, &trampoline_session_priv); - let carol_blinded_hops = { - let payee_tlvs = ReceiveTlvs { + let blinded_hops = { + let no_payment_constraints = PaymentConstraints { + max_cltv_expiry: u32::max_value(), + htlc_minimum_msat: final_value_msat, + }; + let carol_tlvs = TrampolineForwardTlvs { + next_trampoline: eve.0, + payment_relay: carol.1.clone(), + payment_constraints: no_payment_constraints.clone(), + features: BlindedHopFeatures::empty(), + next_blinding_override: None, + } + .encode(); + + let eve_tlvs = TrampolineForwardTlvs { + next_trampoline: fred_node_id, + payment_relay: eve.1.clone(), + payment_constraints: no_payment_constraints.clone(), + features: BlindedHopFeatures::empty(), + next_blinding_override: None, + } + .encode(); + + let fred_tlvs = ReceiveTlvs { payment_secret, - payment_constraints: PaymentConstraints { - max_cltv_expiry: u32::max_value(), - htlc_minimum_msat: final_value_msat, - }, + payment_constraints: no_payment_constraints, payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext {}), } .encode(); - let path = [((carol_node_id, Some(carol_auth_key)), WithoutLength(&payee_tlvs))]; + let path = [ + ((carol.0, None), WithoutLength(&carol_tlvs)), + ((eve.0, None), WithoutLength(&eve_tlvs)), + ((fred_node_id, Some(fred_auth_key)), WithoutLength(&fred_tlvs)), + ]; blinded_path::utils::construct_blinded_hops( &secp_ctx, @@ -2450,14 +2478,32 @@ fn create_blinded_tail( ) }; + // We have to report the total fees for the blinded path to report to the sender. + let (base_msat, proportional_millionths) = + compute_aggregated_base_prop_fee([&carol.1, &eve.1].iter().map(|relay| RoutingFees { + base_msat: relay.fee_base_msat, + proportional_millionths: relay.fee_proportional_millionths, + })) + .unwrap(); + let total_fees = compute_fees_saturating( + final_value_msat, + RoutingFees { + base_msat: base_msat as u32, + proportional_millionths: proportional_millionths as u32, + }, + ); + BlindedTail { trampoline_hops: vec![TrampolineHop { - pubkey: carol_node_id, + pubkey: carol.0, node_features: Features::empty(), - fee_msat: final_value_msat, - cltv_expiry_delta: trampoline_cltv_expiry_delta, + fee_msat: total_fees, + cltv_expiry_delta: carol.1.cltv_expiry_delta as u32 + + eve.1.cltv_expiry_delta as u32 + + fred_cltv_final + + excess_final_cltv_delta, }], - hops: carol_blinded_hops, + hops: blinded_hops, blinding_point: carol_blinding_point, excess_final_cltv_expiry_delta: excess_final_cltv_delta, final_value_msat, @@ -2468,19 +2514,20 @@ fn create_blinded_tail( // payloads that send to unblinded receives and invalid payloads. fn replacement_onion( test_case: TrampolineTestCase, secp_ctx: &Secp256k1, override_random_bytes: [u8; 32], - route: Route, original_amt_msat: u64, starting_htlc_offset: u32, excess_final_cltv: u32, - original_trampoline_cltv: u32, payment_hash: PaymentHash, payment_secret: PaymentSecret, - blinded: bool, + route: Route, fred_amt_msat: u64, fred_final_cltv: u32, excess_final_cltv_delta: u32, + payment_hash: PaymentHash, payment_secret: PaymentSecret, blinded: bool, + starting_htlc_offset: u32, carol: PublicKey, eve: (PublicKey, &PaymentRelay), fred: PublicKey, ) -> msgs::OnionPacket { assert!(!blinded || !matches!(test_case, TrampolineTestCase::Success)); let outer_session_priv = SecretKey::from_slice(&override_random_bytes[..]).unwrap(); let trampoline_session_priv = onion_utils::compute_trampoline_session_priv(&outer_session_priv); - let recipient_onion_fields = RecipientOnionFields::spontaneous_empty(original_amt_msat); + let recipient_onion_fields = RecipientOnionFields::spontaneous_empty(fred_amt_msat); let blinded_tail = route.paths[0].blinded_tail.clone().unwrap(); - // Rebuild our trampoline packet from the original route. If we want to test Carol receiving - // as an unblinded trampoline hop, we switch out her inner trampoline onion with a direct + // Rebuild our trampoline packet from the original route. If we want to test Fred receiving + // as an unblinded trampoline hop, we switch out the trampoline packets with unblinded ones. + // her inner trampoline onion with a direct // receive payload because LDK doesn't support unblinded trampoline receives. let (trampoline_packet, outer_total_msat) = { let (mut trampoline_payloads, outer_total_msat) = @@ -2493,21 +2540,105 @@ fn replacement_onion( .unwrap(); if !blinded { - trampoline_payloads = vec![msgs::OutboundTrampolinePayload::Receive { - payment_data: Some(msgs::FinalOnionHopData { - payment_secret, - total_msat: original_amt_msat, - }), - sender_intended_htlc_amt_msat: original_amt_msat, - cltv_expiry_height: original_trampoline_cltv - + starting_htlc_offset - + excess_final_cltv, - }]; + let eve_trampoline_fees = compute_fees_saturating( + fred_amt_msat, + RoutingFees { + base_msat: eve.1.fee_base_msat, + proportional_millionths: eve.1.fee_proportional_millionths, + }, + ); + + trampoline_payloads = vec![ + // Carol must forward to Eve with enough fees + CLTV to cover her policy. + msgs::OutboundTrampolinePayload::Forward { + amt_to_forward: fred_amt_msat + eve_trampoline_fees, + outgoing_cltv_value: starting_htlc_offset + + fred_final_cltv + excess_final_cltv_delta + + eve.1.cltv_expiry_delta as u32, + outgoing_node_id: eve.0, + }, + // Eve should forward the final amount to fred, allowing enough CLTV to cover his + // final expiry delta and the excess that the sender added. + msgs::OutboundTrampolinePayload::Forward { + amt_to_forward: fred_amt_msat, + outgoing_cltv_value: starting_htlc_offset + + fred_final_cltv + excess_final_cltv_delta, + outgoing_node_id: fred, + }, + // Fred just needs to receive the amount he's expecting, and since this is an + // unblinded route he'll expect an outgoing cltv that accounts for his final + // expiry delta and excess that the sender added. + msgs::OutboundTrampolinePayload::Receive { + payment_data: Some(msgs::FinalOnionHopData { + payment_secret, + total_msat: fred_amt_msat, + }), + sender_intended_htlc_amt_msat: fred_amt_msat, + cltv_expiry_height: starting_htlc_offset + + fred_final_cltv + excess_final_cltv_delta, + }, + ]; + } + + match trampoline_payloads.last_mut().unwrap() { + msgs::OutboundTrampolinePayload::Receive { + sender_intended_htlc_amt_msat, + cltv_expiry_height, + .. + } => { + *sender_intended_htlc_amt_msat = + test_case.inner_onion_amt(*sender_intended_htlc_amt_msat); + *cltv_expiry_height = test_case.inner_onion_cltv(*cltv_expiry_height); + }, + msgs::OutboundTrampolinePayload::BlindedReceive { + sender_intended_htlc_amt_msat, + cltv_expiry_height, + .. + } => { + *sender_intended_htlc_amt_msat = + test_case.inner_onion_amt(*sender_intended_htlc_amt_msat); + *cltv_expiry_height = test_case.inner_onion_cltv(*cltv_expiry_height); + }, + _ => panic!("unexpected final trampoline payload type"), } + // TODO: clean this up + let key_derivation_tail = if !blinded { + BlindedTail { + // Note: this tail isn't *actually* used in our trampoline key derivation, we just + // have to have one to be able to use the helper function. + trampoline_hops: vec![ + TrampolineHop { + pubkey: carol, + node_features: Features::empty(), + fee_msat: 0, + cltv_expiry_delta: 0, + }, + TrampolineHop { + pubkey: eve.0, + node_features: Features::empty(), + fee_msat: 0, + cltv_expiry_delta: 0, + }, + TrampolineHop { + pubkey: fred, + node_features: Features::empty(), + fee_msat: 0, + cltv_expiry_delta: 0, + }, + ], + hops: vec![], + blinding_point: blinded_tail.blinding_point, + excess_final_cltv_expiry_delta: excess_final_cltv_delta, + final_value_msat: fred_amt_msat, + } + } else { + blinded_tail.clone() + }; + let trampoline_onion_keys = onion_utils::construct_trampoline_onion_keys( &secp_ctx, - &blinded_tail, + &key_derivation_tail, &trampoline_session_priv, ); let trampoline_packet = onion_utils::construct_trampoline_onion_packet( @@ -2536,22 +2667,6 @@ fn replacement_onion( .unwrap(); assert_eq!(outer_payloads.len(), 2); - // If we're trying to test invalid payloads, we modify Carol's *outer* onion to have values - // that are inconsistent with her inner onion. We need to do this manually because we - // (obviously) can't construct an invalid onion with LDK's built in functions. - match &mut outer_payloads[1] { - msgs::OutboundOnionPayload::TrampolineEntrypoint { - amt_to_forward, - outgoing_cltv_value, - .. - } => { - *amt_to_forward = test_case.outer_onion_amt(original_amt_msat); - let outer_cltv = original_trampoline_cltv + starting_htlc_offset + excess_final_cltv; - *outgoing_cltv_value = test_case.outer_onion_cltv(outer_cltv); - }, - _ => panic!("final payload is not trampoline entrypoint"), - } - let outer_onion_keys = onion_utils::construct_onion_keys(&secp_ctx, &route.clone().paths[0], &outer_session_priv); onion_utils::construct_onion_packet( @@ -2569,7 +2684,7 @@ fn replacement_onion( // - To hit validation errors by manipulating the trampoline's outer packet. Without this, we would // have to manually construct the onion. fn do_test_trampoline_relay(blinded: bool, test_case: TrampolineTestCase) { - const TOTAL_NODE_COUNT: usize = 3; + const TOTAL_NODE_COUNT: usize = 6; let secp_ctx = Secp256k1::new(); let chanmon_cfgs = create_chanmon_cfgs(TOTAL_NODE_COUNT); @@ -2580,33 +2695,82 @@ fn do_test_trampoline_relay(blinded: bool, test_case: TrampolineTestCase) { let alice_bob_chan = create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0); let bob_carol_chan = create_announced_chan_between_nodes_with_value(&nodes, 1, 2, 1_000_000, 0); + let carol_dave_chan = + create_announced_chan_between_nodes_with_value(&nodes, 2, 3, 1_000_000, 0); + let dave_eve_chan = create_announced_chan_between_nodes_with_value(&nodes, 3, 4, 1_000_000, 0); + let eve_fred_chan = create_announced_chan_between_nodes_with_value(&nodes, 4, 5, 1_000_000, 0); let starting_htlc_offset = (TOTAL_NODE_COUNT as u32) * CHAN_CONFIRM_DEPTH + 1; for i in 0..TOTAL_NODE_COUNT { connect_blocks(&nodes[i], starting_htlc_offset - nodes[i].best_block_info().1); } - let alice_node_id = nodes[0].node.get_our_node_id(); let bob_node_id = nodes[1].node().get_our_node_id(); let carol_node_id = nodes[2].node().get_our_node_id(); + let dave_node_id = nodes[3].node().get_our_node_id(); + let eve_node_id = nodes[4].node().get_our_node_id(); + let fred_node_id = nodes[5].node().get_our_node_id(); let alice_bob_scid = get_scid_from_channel_id(&nodes[0], alice_bob_chan.2); let bob_carol_scid = get_scid_from_channel_id(&nodes[1], bob_carol_chan.2); - let original_amt_msat = 1000; - // Note that for TrampolineTestCase::OuterCLTVLessThanTrampoline to work properly, - // (starting_htlc_offset + excess_final_cltv) / 2 < (starting_htlc_offset + excess_final_cltv + original_trampoline_cltv) - // otherwise dividing the CLTV value by 2 won't kick us under the outer trampoline CLTV. - let original_trampoline_cltv = 42; + let fred_recv_amt = 1000; + let fred_cltv_final = 72; let excess_final_cltv = 70; + let carol_dave_policy = carol_dave_chan.1.contents; + let dave_eve_policy = dave_eve_chan.1.contents; + let eve_fred_policy = eve_fred_chan.1.contents; + + let carol_trampoline_cltv_delta = + carol_dave_policy.cltv_expiry_delta + dave_eve_policy.cltv_expiry_delta; + let carol_trampoline_fee_prop = + carol_dave_policy.fee_proportional_millionths + dave_eve_policy.fee_proportional_millionths; + let carol_trampoline_fee_base = carol_dave_policy.fee_base_msat + dave_eve_policy.fee_base_msat; + + let eve_trampoline_relay = PaymentRelay { + // Note that we add 1 to eve's required CLTV so that she has a non-zero CLTV budget, because + // our pathfinding doesn't support a zero cltv detla. In reality, we'd include a larger + // margin than a single node's delta for trampoline payments, so we don't worry about it. + cltv_expiry_delta: eve_fred_policy.cltv_expiry_delta + 1, + fee_proportional_millionths: eve_fred_policy.fee_proportional_millionths, + fee_base_msat: eve_fred_policy.fee_base_msat, + }; let (payment_preimage, payment_hash, payment_secret) = - get_payment_preimage_hash(&nodes[2], Some(original_amt_msat), None); + get_payment_preimage_hash(&nodes[5], Some(fred_recv_amt), None); // We need the session priv to replace the onion packet later. let override_random_bytes = [42; 32]; *nodes[0].keys_manager.override_random_bytes.lock().unwrap() = Some(override_random_bytes); + // Create a blinded tail where Carol and Eve are trampoline hops, sending to Fred. In our + // unblinded test cases, we'll override this anyway (with a tail sending to an unblinded + // receive, which LDK doesn't allow). + let blinded_tail = create_blinded_tail( + &secp_ctx, + override_random_bytes, + ( + carol_node_id, + // The policy for a blinded trampoline hop needs to cover all the fees for the path to + // the next trampoline. Here we're using the exact values, but IRL the receiving node + // would probably set more general values. + &PaymentRelay { + cltv_expiry_delta: carol_trampoline_cltv_delta, + fee_proportional_millionths: carol_trampoline_fee_prop, + fee_base_msat: carol_trampoline_fee_base, + }, + ), + (eve_node_id, &eve_trampoline_relay), + fred_node_id, + nodes[5].keys_manager.get_receive_auth_key(), + fred_cltv_final, + excess_final_cltv, + fred_recv_amt, + payment_secret, + ); + assert_eq!(blinded_tail.trampoline_hops.len(), 1); + assert_eq!(blinded_tail.hops.len(), 3); + let route = Route { paths: vec![Path { hops: vec![ @@ -2624,24 +2788,12 @@ fn do_test_trampoline_relay(blinded: bool, test_case: TrampolineTestCase) { node_features: NodeFeatures::empty(), short_channel_id: bob_carol_scid, channel_features: ChannelFeatures::empty(), - fee_msat: 0, - cltv_expiry_delta: original_trampoline_cltv + excess_final_cltv, + fee_msat: blinded_tail.trampoline_hops[0].fee_msat, + cltv_expiry_delta: blinded_tail.trampoline_hops[0].cltv_expiry_delta, maybe_announced_channel: false, }, ], - // Create a blinded tail where Carol is receiving. In our unblinded test cases, we'll - // override this anyway (with a tail sending to an unblinded receive, which LDK doesn't - // allow). - blinded_tail: Some(create_blinded_tail( - &secp_ctx, - override_random_bytes, - carol_node_id, - nodes[2].keys_manager.get_receive_auth_key(), - original_trampoline_cltv, - excess_final_cltv, - original_amt_msat, - payment_secret, - )), + blinded_tail: Some(blinded_tail), }], route_params: None, }; @@ -2651,7 +2803,7 @@ fn do_test_trampoline_relay(blinded: bool, test_case: TrampolineTestCase) { .send_payment_with_route( route.clone(), payment_hash, - RecipientOnionFields::spontaneous_empty(original_amt_msat), + RecipientOnionFields::spontaneous_empty(fred_recv_amt), PaymentId(payment_hash.0), ) .unwrap(); @@ -2679,32 +2831,29 @@ fn do_test_trampoline_relay(blinded: bool, test_case: TrampolineTestCase) { &secp_ctx, override_random_bytes, route, - original_amt_msat, - // Our internal send payment helpers add one block to the current height to - // create our payments. Do the same here so that our replacement onion will have - // the right cltv. - starting_htlc_offset + 1, - original_trampoline_cltv, + fred_recv_amt, + fred_cltv_final, excess_final_cltv, payment_hash, payment_secret, blinded, + // Our internal send payment helpers add one block to the current height to + // create our payments. Do the same here so that our replacement onion will have + // the right cltv. + starting_htlc_offset + 1, + carol_node_id, + (eve_node_id, &eve_trampoline_relay), + fred_node_id, ) }); } - let route: &[&Node] = &[&nodes[1], &nodes[2]]; - let args = PassAlongPathArgs::new( - &nodes[0], - route, - original_amt_msat, - payment_hash, - first_message_event, - ); - - let final_cltv_height = original_trampoline_cltv + starting_htlc_offset + excess_final_cltv + 1; - let amt_bytes = test_case.outer_onion_amt(original_amt_msat).to_be_bytes(); - let cltv_bytes = test_case.outer_onion_cltv(final_cltv_height).to_be_bytes(); + // We add two blocks to the minimum height that fred will accept because we added one block + // extra CLTV for Eve's forwarding CLTV "budget" and our dispatch adds one block to the + // current height. + let final_cltv_height = fred_cltv_final + starting_htlc_offset + excess_final_cltv + 2; + let amt_bytes = fred_recv_amt.to_be_bytes(); + let cltv_bytes = final_cltv_height.to_be_bytes(); let payment_failure = test_case.payment_failed_conditions(&amt_bytes, &cltv_bytes).map(|p| { if blinded { PaymentFailedConditions::new() @@ -2713,6 +2862,11 @@ fn do_test_trampoline_relay(blinded: bool, test_case: TrampolineTestCase) { p } }); + let route: &[&Node] = &[&nodes[1], &nodes[2], &nodes[3], &nodes[4], &nodes[5]]; + + let args = + PassAlongPathArgs::new(&nodes[0], route, fred_recv_amt, payment_hash, first_message_event); + let args = if payment_failure.is_some() { args.with_payment_preimage(payment_preimage) .without_claimable_event() @@ -2724,22 +2878,101 @@ fn do_test_trampoline_relay(blinded: bool, test_case: TrampolineTestCase) { do_pass_along_path(args); if let Some(failure) = payment_failure { - let node_updates = get_htlc_update_msgs(&nodes[2], &bob_node_id); - nodes[1].node.handle_update_fail_htlc(carol_node_id, &node_updates.update_fail_htlcs[0]); + let alice_node_id = nodes[0].node.get_our_node_id(); + + // Fred is a blinded introduction node recipient, so will fail back with fail htlc. + let updates_fred = get_htlc_update_msgs(&nodes[5], &eve_node_id); + assert_eq!(updates_fred.update_fail_htlcs.len(), 1); + nodes[4].node.handle_update_fail_htlc(fred_node_id, &updates_fred.update_fail_htlcs[0]); + do_commitment_signed_dance( + &nodes[4], + &nodes[5], + &updates_fred.commitment_signed, + false, + false, + ); + + // Eve is a relaying blinded trampoline, so will fail back with malformed htlc. + expect_and_process_pending_htlcs_and_htlc_handling_failed( + &nodes[4], + &[HTLCHandlingFailureType::TrampolineForward {}], + ); + check_added_monitors(&nodes[4], 1); + + let updates_eve = get_htlc_update_msgs(&nodes[4], &dave_node_id); + if blinded { + assert_eq!(updates_eve.update_fail_malformed_htlcs.len(), 1); + nodes[3].node.handle_update_fail_malformed_htlc( + eve_node_id, + &updates_eve.update_fail_malformed_htlcs[0], + ); + } else { + assert_eq!(updates_eve.update_fail_htlcs.len(), 1); + nodes[3].node.handle_update_fail_htlc(eve_node_id, &updates_eve.update_fail_htlcs[0]); + } + + do_commitment_signed_dance( + &nodes[3], + &nodes[4], + &updates_eve.commitment_signed, + true, + false, + ); + + // Dave is a regular forwarding node, so will fail back with fail htlc. + let updates_dave = get_htlc_update_msgs(&nodes[3], &carol_node_id); + assert_eq!(updates_dave.update_fail_htlcs.len(), 1); + nodes[2].node.handle_update_fail_htlc(dave_node_id, &updates_dave.update_fail_htlcs[0]); + do_commitment_signed_dance( + &nodes[2], + &nodes[3], + &updates_dave.commitment_signed, + false, + false, + ); + + // Carol is a blinded trampoline introduction node, so will fail back with htlc fail. + expect_and_process_pending_htlcs_and_htlc_handling_failed( + &nodes[2], + &[HTLCHandlingFailureType::TrampolineForward {}], + ); + + check_added_monitors(&nodes[2], 1); + + let updates_carol = get_htlc_update_msgs(&nodes[2], &bob_node_id); + assert_eq!(updates_carol.update_fail_htlcs.len(), 1); + nodes[1].node.handle_update_fail_htlc(carol_node_id, &updates_carol.update_fail_htlcs[0]); + let bob_carol_chan = nodes[1] + .node + .list_channels() + .iter() + .find(|c| c.counterparty.node_id == carol_node_id) + .unwrap() + .channel_id; do_commitment_signed_dance( &nodes[1], &nodes[2], - &node_updates.commitment_signed, - true, + &updates_carol.commitment_signed, + false, false, ); - let node_updates = get_htlc_update_msgs(&nodes[1], &alice_node_id); - nodes[0].node.handle_update_fail_htlc(bob_node_id, &node_updates.update_fail_htlcs[0]); + // Bob is a regular forwarding node, so will fail back with htlc fail. + expect_and_process_pending_htlcs_and_htlc_handling_failed( + &nodes[1], + &[HTLCHandlingFailureType::Forward { + node_id: Some(carol_node_id), + channel_id: bob_carol_chan, + }], + ); + check_added_monitors(&nodes[1], 1); + let updates_bob = get_htlc_update_msgs(&nodes[1], &alice_node_id); + assert_eq!(updates_bob.update_fail_htlcs.len(), 1); + nodes[0].node.handle_update_fail_htlc(bob_node_id, &updates_bob.update_fail_htlcs[0]); do_commitment_signed_dance( &nodes[0], &nodes[1], - &node_updates.commitment_signed, + &updates_bob.commitment_signed, false, false, ); @@ -2749,9 +2982,9 @@ fn do_test_trampoline_relay(blinded: bool, test_case: TrampolineTestCase) { // Because we support blinded paths, we also assert on our expected logs to make sure // that the failure reason hidden by obfuscated blinded errors is as expected. if let Some((module, line, count)) = test_case.expected_log() { - nodes[2].logger.assert_log_contains(module, line, count); + nodes[5].logger.assert_log_contains(module, line, count); } } else { - claim_payment(&nodes[0], &[&nodes[1], &nodes[2]], payment_preimage); + claim_payment(&nodes[0], route, payment_preimage); } } diff --git a/lightning/src/routing/router.rs b/lightning/src/routing/router.rs index 154343ac0e7..509beb9bc3e 100644 --- a/lightning/src/routing/router.rs +++ b/lightning/src/routing/router.rs @@ -2469,7 +2469,7 @@ fn compute_fees(amount_msat: u64, channel_fees: RoutingFees) -> Option { /// Calculate the fees required to route the given amount over a channel with the given fees, /// saturating to [`u64::max_value`]. #[rustfmt::skip] -fn compute_fees_saturating(amount_msat: u64, channel_fees: RoutingFees) -> u64 { +pub(crate) fn compute_fees_saturating(amount_msat: u64, channel_fees: RoutingFees) -> u64 { amount_msat.checked_mul(channel_fees.proportional_millionths as u64) .map(|prop| prop / 1_000_000).unwrap_or(u64::max_value()) .saturating_add(channel_fees.base_msat as u64)