From 77d3659d8af798c11750cb5826e45d29691dadd9 Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Thu, 22 Jan 2026 20:14:09 +0000 Subject: [PATCH 1/7] Pass constructed `PendingAddHTLCInfo` to chanman `forward_htlcs` We jump through some hoops in order to pass a small list of objects to `forward_htlcs` on a per-channel basis rather than per-HTLC. Then, `forward_htlcs` builds a `PendingAddHTLCInfo` for each HTLC for insertion. Worse, in some `forward_htlcs` callsites we're actually starting with a `PendingAddHTLCInfo`, converting it to a tuple, then back inside `forward_htlcs`. Instead, here we just pass a list of built `PendingAddHTLCInfo`s to `forward_htlcs`, cleaning up a good bit of code and even avoiding an allocation of the HTLCs vec in many cases. --- lightning/src/ln/channelmanager.rs | 151 +++++++++++------------------ 1 file changed, 56 insertions(+), 95 deletions(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index fd5e5d15b9f..d82db629c68 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -739,10 +739,6 @@ impl_writeable_tlv_based_enum!(SentHTLCId, }, ); -// (src_outbound_scid_alias, src_counterparty_node_id, src_funding_outpoint, src_chan_id, src_user_chan_id) -type PerSourcePendingForward = - (u64, PublicKey, OutPoint, ChannelId, u128, Vec<(PendingHTLCInfo, u64)>); - type FailedHTLCForward = (HTLCSource, PaymentHash, HTLCFailReason, HTLCHandlingFailureType); mod fuzzy_channelmanager { @@ -1375,7 +1371,7 @@ enum PostMonitorUpdateChanResume { counterparty_node_id: PublicKey, unbroadcasted_batch_funding_txid: Option, update_actions: Vec, - htlc_forwards: Option, + htlc_forwards: Vec, decode_update_add_htlcs: Option<(u64, Vec)>, finalized_claimed_htlcs: Vec<(HTLCSource, Option)>, failed_htlcs: Vec<(HTLCSource, PaymentHash, HTLCFailReason)>, @@ -6771,15 +6767,16 @@ where ..payment.forward_info }; - let mut per_source_pending_forward = [( - payment.prev_outbound_scid_alias, - payment.prev_counterparty_node_id, - payment.prev_funding_outpoint, - payment.prev_channel_id, - payment.prev_user_channel_id, - vec![(pending_htlc_info, payment.prev_htlc_id)], - )]; - self.forward_htlcs(&mut per_source_pending_forward); + let forward = [PendingAddHTLCInfo { + prev_outbound_scid_alias: payment.prev_outbound_scid_alias, + prev_htlc_id: payment.prev_htlc_id, + prev_counterparty_node_id: payment.prev_counterparty_node_id, + prev_channel_id: payment.prev_channel_id, + prev_funding_outpoint: payment.prev_funding_outpoint, + prev_user_channel_id: payment.prev_user_channel_id, + forward_info: pending_htlc_info, + }]; + self.forward_htlcs(forward); Ok(()) } @@ -7010,7 +7007,7 @@ where next_packet_details_opt.map(|d| d.next_packet_pubkey), ) { Ok(info) => { - let to_pending_add = |info| PendingAddHTLCInfo { + let pending_add = PendingAddHTLCInfo { prev_outbound_scid_alias: incoming_scid_alias, prev_counterparty_node_id: incoming_counterparty_node_id, prev_funding_outpoint: incoming_funding_txo, @@ -7032,7 +7029,7 @@ where Some(incoming_channel_id), Some(update_add_htlc.payment_hash), ); - if info.routing.should_hold_htlc() { + if pending_add.forward_info.routing.should_hold_htlc() { let mut held_htlcs = self.pending_intercepted_htlcs.lock().unwrap(); let intercept_id = intercept_id(); match held_htlcs.entry(intercept_id) { @@ -7041,7 +7038,6 @@ where logger, "Intercepted held HTLC with id {intercept_id}, holding until the recipient is online" ); - let pending_add = to_pending_add(info); entry.insert(pending_add); }, hash_map::Entry::Occupied(_) => { @@ -7058,7 +7054,6 @@ where self.pending_intercepted_htlcs.lock().unwrap(); match pending_intercepts.entry(intercept_id) { hash_map::Entry::Vacant(entry) => { - let pending_add = to_pending_add(info); if let Ok(intercept_ev) = create_htlc_intercepted_event(intercept_id, &pending_add) { @@ -7098,7 +7093,7 @@ where }, } } else { - htlc_forwards.push((info, update_add_htlc.htlc_id)) + htlc_forwards.push(pending_add); } }, Err(inbound_err) => { @@ -7118,15 +7113,7 @@ where // Process all of the forwards and failures for the channel in which the HTLCs were // proposed to as a batch. - let pending_forwards = ( - incoming_scid_alias, - incoming_counterparty_node_id, - incoming_funding_txo, - incoming_channel_id, - incoming_user_channel_id, - htlc_forwards, - ); - self.forward_htlcs(&mut [pending_forwards]); + self.forward_htlcs(htlc_forwards); for (htlc_fail, failure_type, failure_reason) in htlc_fails.drain(..) { let failure = match htlc_fail { HTLCFailureMsg::Relay(fail_htlc) => HTLCForwardInfo::FailHTLC { @@ -7220,7 +7207,7 @@ where let mut new_events = VecDeque::new(); let mut failed_forwards = Vec::new(); - let mut phantom_receives: Vec = Vec::new(); + let mut phantom_receives: Vec = Vec::new(); let mut forward_htlcs = new_hash_map(); mem::swap(&mut forward_htlcs, &mut self.forward_htlcs.lock().unwrap()); @@ -7266,7 +7253,7 @@ where None, ); } - self.forward_htlcs(&mut phantom_receives); + self.forward_htlcs(phantom_receives); if self.check_free_holding_cells() { should_persist = NotifyOption::DoPersist; @@ -7286,7 +7273,7 @@ where fn forwarding_channel_not_found( &self, forward_infos: impl Iterator, short_chan_id: u64, forwarding_counterparty: Option, failed_forwards: &mut Vec, - phantom_receives: &mut Vec, + phantom_receives: &mut Vec, ) { for forward_info in forward_infos { match forward_info { @@ -7408,14 +7395,15 @@ where current_height, ); match create_res { - Ok(info) => phantom_receives.push(( + Ok(info) => phantom_receives.push(PendingAddHTLCInfo { + forward_info: info, prev_outbound_scid_alias, + prev_htlc_id, prev_counterparty_node_id, - prev_funding_outpoint, prev_channel_id, + prev_funding_outpoint, prev_user_channel_id, - vec![(info, prev_htlc_id)], - )), + }), Err(InboundHTLCErr { reason, err_data, msg }) => { failure_handler( msg, @@ -7467,7 +7455,7 @@ where fn process_forward_htlcs( &self, short_chan_id: u64, pending_forwards: &mut Vec, failed_forwards: &mut Vec, - phantom_receives: &mut Vec, + phantom_receives: &mut Vec, ) { let mut forwarding_counterparty = None; @@ -9503,8 +9491,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ fn post_monitor_update_unlock( &self, channel_id: ChannelId, counterparty_node_id: PublicKey, unbroadcasted_batch_funding_txid: Option, - update_actions: Vec, - htlc_forwards: Option, + update_actions: Vec, htlc_forwards: Vec, decode_update_add_htlcs: Option<(u64, Vec)>, finalized_claimed_htlcs: Vec<(HTLCSource, Option)>, failed_htlcs: Vec<(HTLCSource, PaymentHash, HTLCFailReason)>, @@ -9559,9 +9546,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ self.handle_monitor_update_completion_actions(update_actions); - if let Some(forwards) = htlc_forwards { - self.forward_htlcs(&mut [forwards][..]); - } + self.forward_htlcs(htlc_forwards); if let Some(decode) = decode_update_add_htlcs { self.push_decode_update_add_htlcs(decode); } @@ -10102,7 +10087,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ channel_ready: Option, announcement_sigs: Option, tx_signatures: Option, tx_abort: Option, channel_ready_order: ChannelReadyOrder, - ) -> (Option<(u64, PublicKey, OutPoint, ChannelId, u128, Vec<(PendingHTLCInfo, u64)>)>, Option<(u64, Vec)>) { + ) -> (Vec, Option<(u64, Vec)>) { let logger = WithChannelContext::from(&self.logger, &channel.context, None); log_trace!(logger, "Handling channel resumption with {} RAA, {} commitment update, {} pending forwards, {} pending update_add_htlcs, {}broadcasting funding, {} channel ready, {} announcement, {} tx_signatures, {} tx_abort", if raa.is_some() { "an" } else { "no" }, @@ -10118,13 +10103,19 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ let counterparty_node_id = channel.context.get_counterparty_node_id(); let outbound_scid_alias = channel.context.outbound_scid_alias(); - let mut htlc_forwards = None; + let mut htlc_forwards = Vec::new(); if !pending_forwards.is_empty() { - htlc_forwards = Some(( - outbound_scid_alias, channel.context.get_counterparty_node_id(), - channel.funding.get_funding_txo().unwrap(), channel.context.channel_id(), - channel.context.get_user_id(), pending_forwards - )); + htlc_forwards = pending_forwards.into_iter().map(|(forward_info, prev_htlc_id)| { + PendingAddHTLCInfo { + forward_info, + prev_outbound_scid_alias: outbound_scid_alias, + prev_htlc_id, + prev_counterparty_node_id: channel.context.get_counterparty_node_id(), + prev_channel_id: channel.context.channel_id(), + prev_funding_outpoint: channel.funding.get_funding_txo().unwrap(), + prev_user_channel_id: channel.context.get_user_id(), + } + }).collect(); } let mut decode_update_add_htlcs = None; if !pending_update_adds.is_empty() { @@ -11979,44 +11970,22 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ } #[inline] - fn forward_htlcs(&self, per_source_pending_forwards: &mut [PerSourcePendingForward]) { - for &mut ( - prev_outbound_scid_alias, - prev_counterparty_node_id, - prev_funding_outpoint, - prev_channel_id, - prev_user_channel_id, - ref mut pending_forwards, - ) in per_source_pending_forwards - { - if !pending_forwards.is_empty() { - for (forward_info, prev_htlc_id) in pending_forwards.drain(..) { - let scid = match forward_info.routing { - PendingHTLCRouting::Forward { short_channel_id, .. } => short_channel_id, - PendingHTLCRouting::TrampolineForward { .. } - | PendingHTLCRouting::Receive { .. } - | PendingHTLCRouting::ReceiveKeysend { .. } => 0, - }; - - let pending_add = PendingAddHTLCInfo { - prev_outbound_scid_alias, - prev_counterparty_node_id, - prev_funding_outpoint, - prev_channel_id, - prev_htlc_id, - prev_user_channel_id, - forward_info, - }; + fn forward_htlcs>(&self, pending_forwards: I) { + for htlc in pending_forwards.into_iter() { + let scid = match htlc.forward_info.routing { + PendingHTLCRouting::Forward { short_channel_id, .. } => short_channel_id, + PendingHTLCRouting::TrampolineForward { .. } + | PendingHTLCRouting::Receive { .. } + | PendingHTLCRouting::ReceiveKeysend { .. } => 0, + }; - match self.forward_htlcs.lock().unwrap().entry(scid) { - hash_map::Entry::Occupied(mut entry) => { - entry.get_mut().push(HTLCForwardInfo::AddHTLC(pending_add)); - }, - hash_map::Entry::Vacant(entry) => { - entry.insert(vec![HTLCForwardInfo::AddHTLC(pending_add)]); - }, - } - } + match self.forward_htlcs.lock().unwrap().entry(scid) { + hash_map::Entry::Occupied(mut entry) => { + entry.get_mut().push(HTLCForwardInfo::AddHTLC(htlc)); + }, + hash_map::Entry::Vacant(entry) => { + entry.insert(vec![HTLCForwardInfo::AddHTLC(htlc)]); + }, } } } @@ -12354,7 +12323,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ Vec::new(), Vec::new(), None, responses.channel_ready, responses.announcement_sigs, responses.tx_signatures, responses.tx_abort, responses.channel_ready_order, ); - debug_assert!(htlc_forwards.is_none()); + debug_assert!(htlc_forwards.is_empty()); debug_assert!(decode_update_add_htlcs.is_none()); if let Some(upd) = channel_update { peer_state.pending_msg_events.push(upd); @@ -16388,15 +16357,7 @@ where }, } } else { - let mut per_source_pending_forward = [( - htlc.prev_outbound_scid_alias, - htlc.prev_counterparty_node_id, - htlc.prev_funding_outpoint, - htlc.prev_channel_id, - htlc.prev_user_channel_id, - vec![(htlc.forward_info, htlc.prev_htlc_id)], - )]; - self.forward_htlcs(&mut per_source_pending_forward); + self.forward_htlcs([htlc]); } }, _ => return, From 94710b85fbc7204e7e21f29c729fbb3228fb16f3 Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Sat, 7 Feb 2026 16:49:20 +0000 Subject: [PATCH 2/7] f update comment --- 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 d82db629c68..4b6cb0f834b 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -452,7 +452,7 @@ pub(super) enum PendingHTLCStatus { pub(super) struct PendingAddHTLCInfo { pub(super) forward_info: PendingHTLCInfo, - // These fields are produced in `forward_htlcs()` and consumed in + // These fields are set before calling `forward_htlcs()` and consumed in // `process_pending_htlc_forwards()` for constructing the // `HTLCSource::PreviousHopData` for failed and forwarded // HTLCs. From e767c9792e9bc6faeb22bebf29d124b41dbfdcd8 Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Sat, 7 Feb 2026 16:50:26 +0000 Subject: [PATCH 3/7] f cleanup --- 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 4b6cb0f834b..e0b0cf50842 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -10110,7 +10110,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ forward_info, prev_outbound_scid_alias: outbound_scid_alias, prev_htlc_id, - prev_counterparty_node_id: channel.context.get_counterparty_node_id(), + prev_counterparty_node_id: counterparty_node_id, prev_channel_id: channel.context.channel_id(), prev_funding_outpoint: channel.funding.get_funding_txo().unwrap(), prev_user_channel_id: channel.context.get_user_id(), From eb36f117f1c68ba0197797064bd12651abf854c3 Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Thu, 22 Jan 2026 21:15:22 +0000 Subject: [PATCH 4/7] Allow intercepting HTLCs based on the source channel It may be useful in some situations to select HTLCs for interception based on the source channel in addition to the sink. Here we add the ability to do so by adding new flags to `HTLCInterceptionFlags`. --- lightning/src/ln/channelmanager.rs | 72 ++++++++++---- lightning/src/ln/interception_tests.rs | 125 ++++++++++++++++++++----- lightning/src/util/config.rs | 49 +++++++++- 3 files changed, 203 insertions(+), 43 deletions(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index e0b0cf50842..d3a26c88283 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -4767,7 +4767,9 @@ where } } - fn forward_needs_intercept_to_known_chan(&self, outbound_chan: &FundedChannel) -> bool { + fn forward_needs_intercept_to_known_chan( + &self, prev_chan_public: bool, outbound_chan: &FundedChannel, + ) -> bool { let intercept_flags = self.config.read().unwrap().htlc_interception_flags; if !outbound_chan.context.should_announce() { if outbound_chan.context.is_connected() { @@ -4784,6 +4786,23 @@ where return true; } } + if prev_chan_public { + if outbound_chan.context.should_announce() { + if intercept_flags & (HTLCInterceptionFlags::FromPublicToPublicChannels as u8) != 0 + { + return true; + } + } else { + if intercept_flags & (HTLCInterceptionFlags::FromPublicToPrivateChannels as u8) != 0 + { + return true; + } + } + } else { + if intercept_flags & (HTLCInterceptionFlags::FromPrivateChannels as u8) != 0 { + return true; + } + } false } @@ -4877,7 +4896,7 @@ where } fn can_forward_htlc_should_intercept( - &self, msg: &msgs::UpdateAddHTLC, next_hop: &NextPacketDetails, + &self, msg: &msgs::UpdateAddHTLC, prev_chan_public: bool, next_hop: &NextPacketDetails, ) -> Result { let outgoing_scid = match next_hop.outgoing_connector { HopConnector::ShortChannelId(scid) => scid, @@ -4896,7 +4915,7 @@ where // times we do it. let intercept = match self.do_funded_channel_callback(outgoing_scid, |chan: &mut FundedChannel| { - let intercept = self.forward_needs_intercept_to_known_chan(chan); + let intercept = self.forward_needs_intercept_to_known_chan(prev_chan_public, chan); self.can_forward_htlc_to_outgoing_channel(chan, msg, next_hop, intercept)?; Ok(intercept) }) { @@ -6845,17 +6864,13 @@ where let incoming_channel_details_opt = self.do_funded_channel_callback( incoming_scid_alias, |chan: &mut FundedChannel| { - let counterparty_node_id = chan.context.get_counterparty_node_id(); - let channel_id = chan.context.channel_id(); - let funding_txo = chan.funding.get_funding_txo().unwrap(); - let user_channel_id = chan.context.get_user_id(); - let accept_underpaying_htlcs = chan.context.config().accept_underpaying_htlcs; ( - counterparty_node_id, - channel_id, - funding_txo, - user_channel_id, - accept_underpaying_htlcs, + chan.context.get_counterparty_node_id(), + chan.context.channel_id(), + chan.funding.get_funding_txo().unwrap(), + chan.context.get_user_id(), + chan.context.config().accept_underpaying_htlcs, + chan.context.should_announce(), ) }, ); @@ -6865,6 +6880,7 @@ where incoming_funding_txo, incoming_user_channel_id, incoming_accept_underpaying_htlcs, + incoming_chan_is_public, ) = if let Some(incoming_channel_details) = incoming_channel_details_opt { incoming_channel_details } else { @@ -6989,9 +7005,11 @@ where // Now process the HTLC on the outgoing channel if it's a forward. let mut intercept_forward = false; if let Some(next_packet_details) = next_packet_details_opt.as_ref() { - match self - .can_forward_htlc_should_intercept(&update_add_htlc, next_packet_details) - { + match self.can_forward_htlc_should_intercept( + &update_add_htlc, + incoming_chan_is_public, + next_packet_details, + ) { Err(reason) => { fail_htlc_continue_to_next!(reason); }, @@ -16317,9 +16335,29 @@ where ); log_trace!(logger, "Releasing held htlc with intercept_id {}", intercept_id); + let prev_chan_public = { + let per_peer_state = self.per_peer_state.read().unwrap(); + let peer_state = per_peer_state + .get(&htlc.prev_counterparty_node_id) + .map(|mtx| mtx.lock().unwrap()); + let chan_state = peer_state + .as_ref() + .map(|state| state.channel_by_id.get(&htlc.prev_channel_id)) + .flatten(); + if let Some(chan_state) = chan_state { + chan_state.context().should_announce() + } else { + // If the inbound channel has closed since the HTLC was held, we really + // shouldn't forward it - forwarding it now would result in, at best, + // having to claim the HTLC on chain. Instead, drop the HTLC and let the + // counterparty claim their money on chain. + return; + } + }; + let should_intercept = self .do_funded_channel_callback(next_hop_scid, |chan| { - self.forward_needs_intercept_to_known_chan(chan) + self.forward_needs_intercept_to_known_chan(prev_chan_public, chan) }) .unwrap_or_else(|| self.forward_needs_intercept_to_unknown_chan(next_hop_scid)); diff --git a/lightning/src/ln/interception_tests.rs b/lightning/src/ln/interception_tests.rs index 11b5de166f6..2122e86a3e0 100644 --- a/lightning/src/ln/interception_tests.rs +++ b/lightning/src/ln/interception_tests.rs @@ -50,7 +50,16 @@ fn do_test_htlc_interception_flags( let node_chanmgrs = create_node_chanmgrs(3, &node_cfgs, &[None, Some(intercept_config), None]); let nodes = create_network(3, &node_cfgs, &node_chanmgrs); - create_announced_chan_between_nodes(&nodes, 0, 1); + let inbound_private = match flag { + Flag::FromPrivateChannels => { + create_unannounced_chan_between_nodes_with_value(&nodes, 0, 1, 100000, 0); + true + }, + _ => { + create_announced_chan_between_nodes(&nodes, 0, 1); + false + }, + }; let node_0_id = nodes[0].node.get_our_node_id(); let node_1_id = nodes[1].node.get_our_node_id(); @@ -58,29 +67,31 @@ fn do_test_htlc_interception_flags( // First open the right type of channel (and get it in the right state) for the bit we're // testing. - let (target_scid, target_chan_id) = match flag { - Flag::ToOfflinePrivateChannels | Flag::ToOnlinePrivateChannels => { + let (target_scid, target_chan_id, outbound_private_for_known_scids) = match flag { + Flag::ToOfflinePrivateChannels + | Flag::ToOnlinePrivateChannels + | Flag::FromPublicToPrivateChannels => { create_unannounced_chan_between_nodes_with_value(&nodes, 1, 2, 100000, 0); let chan_id = nodes[2].node.list_channels()[0].channel_id; let scid = nodes[2].node.list_channels()[0].short_channel_id.unwrap(); if flag == Flag::ToOfflinePrivateChannels { nodes[1].node.peer_disconnected(node_2_id); nodes[2].node.peer_disconnected(node_1_id); - } else { - assert_eq!(flag, Flag::ToOnlinePrivateChannels); } - (scid, chan_id) + (scid, chan_id, Some(true)) }, - Flag::ToInterceptSCIDs | Flag::ToPublicChannels | Flag::ToUnknownSCIDs => { + Flag::ToInterceptSCIDs + | Flag::ToPublicChannels + | Flag::FromPrivateChannels + | Flag::FromPublicToPublicChannels + | Flag::ToUnknownSCIDs => { let (chan_upd, _, chan_id, _) = create_announced_chan_between_nodes(&nodes, 1, 2); if flag == Flag::ToInterceptSCIDs { - (nodes[1].node.get_intercept_scid(), chan_id) - } else if flag == Flag::ToPublicChannels { - (chan_upd.contents.short_channel_id, chan_id) + (nodes[1].node.get_intercept_scid(), chan_id, None) } else if flag == Flag::ToUnknownSCIDs { - (42424242, chan_id) + (42424242, chan_id, None) } else { - panic!(); + (chan_upd.contents.short_channel_id, chan_id, Some(false)) } }, _ => panic!("Combined flags aren't allowed"), @@ -100,19 +111,51 @@ fn do_test_htlc_interception_flags( get_route_and_payment_hash!(nodes[0], nodes[2], pay_params, amt_msat); route.paths[0].hops[1].short_channel_id = target_scid; - let interception_bit_match = (flags_bitmask & (flag as u8)) != 0; + let mut should_intercept = false; + for a_flag in ALL_FLAGS { + if flags_bitmask & (a_flag as u8) != 0 { + match a_flag { + Flag::ToInterceptSCIDs => { + should_intercept |= flag == Flag::ToInterceptSCIDs; + }, + Flag::ToOfflinePrivateChannels => { + should_intercept |= flag == Flag::ToOfflinePrivateChannels; + }, + Flag::ToOnlinePrivateChannels => { + should_intercept |= flag != Flag::ToOfflinePrivateChannels + && outbound_private_for_known_scids == Some(true); + }, + Flag::ToPublicChannels => { + should_intercept |= outbound_private_for_known_scids == Some(false); + }, + Flag::ToUnknownSCIDs => { + should_intercept |= flag == Flag::ToUnknownSCIDs; + }, + Flag::FromPrivateChannels => { + should_intercept |= inbound_private; + }, + Flag::FromPublicToPrivateChannels => { + should_intercept |= + !inbound_private && outbound_private_for_known_scids == Some(true); + }, + Flag::FromPublicToPublicChannels => { + should_intercept |= + !inbound_private && outbound_private_for_known_scids == Some(false); + }, + _ => panic!("Combined flags aren't allowed"), + } + } + } + match modification { Some(ForwardingMod::FeeTooLow) => { - assert!( - interception_bit_match, - "No reason to test failing if we aren't trying to intercept", - ); + assert!(should_intercept, "No reason to test failing if we aren't trying to intercept"); route.paths[0].hops[0].fee_msat = 500; }, Some(ForwardingMod::CLTVBelowConfig) => { route.paths[0].hops[0].cltv_expiry_delta = 6 * 12; assert!( - interception_bit_match, + should_intercept, "No reason to test failing if we aren't trying to intercept", ); }, @@ -132,7 +175,7 @@ fn do_test_htlc_interception_flags( do_commitment_signed_dance(&nodes[1], &nodes[0], &payment_event.commitment_msg, false, true); expect_and_process_pending_htlcs(&nodes[1], false); - if interception_bit_match && modification.is_none() { + if should_intercept && modification.is_none() { // If we were set to intercept, check that we got an interception event then // forward the HTLC on to nodes[2] and claim the payment. let intercept_id; @@ -171,7 +214,14 @@ fn do_test_htlc_interception_flags( // If we were not set to intercept, check that the HTLC either failed or was // automatically forwarded as appropriate. match (modification, flag) { - (None, Flag::ToOnlinePrivateChannels | Flag::ToPublicChannels) => { + ( + None, + Flag::ToOnlinePrivateChannels + | Flag::ToPublicChannels + | Flag::FromPrivateChannels + | Flag::FromPublicToPrivateChannels + | Flag::FromPublicToPublicChannels, + ) => { check_added_monitors(&nodes[1], 1); let forward_ev = SendEvent::from_node(&nodes[1]); @@ -240,31 +290,55 @@ fn do_test_htlc_interception_flags( } const MAX_BITMASK: u8 = HTLCInterceptionFlags::AllValidHTLCs as u8; -const ALL_FLAGS: [HTLCInterceptionFlags; 5] = [ +const ALL_FLAGS: [HTLCInterceptionFlags; 8] = [ HTLCInterceptionFlags::ToInterceptSCIDs, HTLCInterceptionFlags::ToOfflinePrivateChannels, HTLCInterceptionFlags::ToOnlinePrivateChannels, HTLCInterceptionFlags::ToPublicChannels, HTLCInterceptionFlags::ToUnknownSCIDs, + HTLCInterceptionFlags::FromPrivateChannels, + HTLCInterceptionFlags::FromPublicToPrivateChannels, + HTLCInterceptionFlags::FromPublicToPublicChannels, ]; - #[test] -fn test_htlc_interception_flags() { +fn check_all_flags() { let mut all_flag_bits = 0; for flag in ALL_FLAGS { all_flag_bits |= flag as isize; } assert_eq!(all_flag_bits, MAX_BITMASK as isize, "all flags must test all bits"); +} +fn test_htlc_interception_flags_subrange>(r: I) { // Test all 2^5 = 32 combinations of the HTLCInterceptionFlags bitmask // For each combination, test 5 different HTLC forwards and verify correct interception behavior - for flags_bitmask in 0..=MAX_BITMASK { + for flags_bitmask in r { for flag in ALL_FLAGS { do_test_htlc_interception_flags(flags_bitmask, flag, None); } } } +#[test] +fn test_htlc_interception_flags_a() { + test_htlc_interception_flags_subrange(0..MAX_BITMASK / 4); +} + +#[test] +fn test_htlc_interception_flags_b() { + test_htlc_interception_flags_subrange(MAX_BITMASK / 4..MAX_BITMASK / 2); +} + +#[test] +fn test_htlc_interception_flags_c() { + test_htlc_interception_flags_subrange(MAX_BITMASK / 2..MAX_BITMASK / 4 * 3); +} + +#[test] +fn test_htlc_interception_flags_d() { + test_htlc_interception_flags_subrange(MAX_BITMASK / 4 * 3..=MAX_BITMASK); +} + #[test] fn test_htlc_bad_for_chan_config() { // Test that interception won't be done if an HTLC fails to meet the target channel's channel @@ -273,6 +347,9 @@ fn test_htlc_bad_for_chan_config() { HTLCInterceptionFlags::ToOfflinePrivateChannels, HTLCInterceptionFlags::ToOnlinePrivateChannels, HTLCInterceptionFlags::ToPublicChannels, + HTLCInterceptionFlags::FromPrivateChannels, + HTLCInterceptionFlags::FromPublicToPrivateChannels, + HTLCInterceptionFlags::FromPublicToPublicChannels, ]; for flag in have_chan_flags { do_test_htlc_interception_flags(flag as u8, flag, Some(ForwardingMod::FeeTooLow)); diff --git a/lightning/src/util/config.rs b/lightning/src/util/config.rs index feb326cfad6..aa9dd667204 100644 --- a/lightning/src/util/config.rs +++ b/lightning/src/util/config.rs @@ -930,6 +930,51 @@ pub enum HTLCInterceptionFlags { | Self::ToOfflinePrivateChannels as isize | Self::ToOnlinePrivateChannels as isize | Self::ToPublicChannels as isize, + /// If this flag is set, any attempts to forward a payment from a private channel (to anywhere) + /// will instead generate an [`Event::HTLCIntercepted`] which must be handled the same as any + /// other intercepted HTLC. + /// + /// This is useful for an LSP that may wish to apply a higher fee policy on their channels when + /// the HTLC comes from a private channel client. Note that HTLCs which do not pay the + /// configured fee rate or do not meet the [`ChannelConfig::cltv_expiry_delta`] will fail. + /// Thus, this cannot be used to allow forwarding for less than the public fees. + /// + /// Note that no HTLCs to unknown channels will be intercepted by this flag. For that, use + /// [`Self::ToUnknownSCIDs`]. + /// + /// [`Event::HTLCIntercepted`]: crate::events::Event::HTLCIntercepted + FromPrivateChannels = 1 << 4, + /// If this flag is set, any attempts to forward a payment from a public channel to a private + /// channel will instead generate an [`Event::HTLCIntercepted`] which must be handled the same + /// as any other intercepted HTLC. + /// + /// This is useful for an LSP that may wish to take an additional fee on any HTLCs which are + /// forwarded to a private channel client but wishes to avoid taking that fee when forwarding + /// an HTLC from a private channel client to another private channel client. + /// + /// Note that HTLCs which do not pay the configured fee rate or do not meet the + /// [`ChannelConfig::cltv_expiry_delta`] will fail and not be intercepted. + /// + /// Note that no HTLCs to unknown channels will be intercepted by this flag. For that, use + /// [`Self::ToUnknownSCIDs`]. + /// + /// [`Event::HTLCIntercepted`]: crate::events::Event::HTLCIntercepted + FromPublicToPrivateChannels = 1 << 5, + /// If this flag is set, any attempts to forward a payment from a public channel to another + /// public channel will instead generate an [`Event::HTLCIntercepted`] which must be handled + /// the same as any other intercepted HTLC. + /// + /// This primarily exists for completeness, and generally interception of of HTLCs between + /// public channels is *strongly* discouraged. + /// + /// Note that HTLCs which do not pay the configured fee rate or do not meet the + /// [`ChannelConfig::cltv_expiry_delta`] will fail and not be intercepted. + /// + /// Note that no HTLCs to unknown channels will be intercepted by this flag. For that, use + /// [`Self::ToUnknownSCIDs`]. + /// + /// [`Event::HTLCIntercepted`]: crate::events::Event::HTLCIntercepted + FromPublicToPublicChannels = 1 << 6, /// If this flag is set, any attempts to forward a payment to an unknown short channel id will /// instead generate an [`Event::HTLCIntercepted`] which must be handled the same as any other /// intercepted HTLC. @@ -941,7 +986,7 @@ pub enum HTLCInterceptionFlags { /// delta meets your requirements before forwarding the HTLC. /// /// [`Event::HTLCIntercepted`]: crate::events::Event::HTLCIntercepted - ToUnknownSCIDs = 1 << 4, + ToUnknownSCIDs = 1 << 7, /// If these flags are set, all HTLCs being forwarded over this node will instead generate an /// [`Event::HTLCIntercepted`] which must be handled the same as any other intercepted HTLC. /// @@ -951,7 +996,7 @@ pub enum HTLCInterceptionFlags { /// validate the fee and CLTV delta meets your requirements before forwarding the HTLC. /// /// [`Event::HTLCIntercepted`]: crate::events::Event::HTLCIntercepted - AllValidHTLCs = Self::ToAllKnownSCIDs as isize | Self::ToUnknownSCIDs as isize, + AllValidHTLCs = 0xff, } impl Into for HTLCInterceptionFlags { From 9f2632c6414c61be6eab6ce648620b889febfb93 Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Sat, 7 Feb 2026 16:50:22 +0000 Subject: [PATCH 5/7] f cleanup --- lightning/src/ln/channelmanager.rs | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index d3a26c88283..c536bc6affd 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -6861,7 +6861,14 @@ where 'outer_loop: for (incoming_scid_alias, update_add_htlcs) in decode_update_add_htlcs { // If any decoded update_add_htlcs were processed, we need to persist. should_persist = true; - let incoming_channel_details_opt = self.do_funded_channel_callback( + let ( + incoming_counterparty_node_id, + incoming_channel_id, + incoming_funding_txo, + incoming_user_channel_id, + incoming_accept_underpaying_htlcs, + incoming_chan_is_public, + ) = match self.do_funded_channel_callback( incoming_scid_alias, |chan: &mut FundedChannel| { ( @@ -6873,19 +6880,10 @@ where chan.context.should_announce(), ) }, - ); - let ( - incoming_counterparty_node_id, - incoming_channel_id, - incoming_funding_txo, - incoming_user_channel_id, - incoming_accept_underpaying_htlcs, - incoming_chan_is_public, - ) = if let Some(incoming_channel_details) = incoming_channel_details_opt { - incoming_channel_details - } else { + ) { + Some(incoming_channel_details) => incoming_channel_details, // The incoming channel no longer exists, HTLCs should be resolved onchain instead. - continue; + None => continue, }; let mut htlc_forwards = Vec::new(); From b758ce1f2e59ed6b7937f0205b470d54614ed4f4 Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Sat, 7 Feb 2026 16:50:30 +0000 Subject: [PATCH 6/7] f rustfmt --- lightning/src/ln/interception_tests.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/lightning/src/ln/interception_tests.rs b/lightning/src/ln/interception_tests.rs index 2122e86a3e0..34d3f39e05e 100644 --- a/lightning/src/ln/interception_tests.rs +++ b/lightning/src/ln/interception_tests.rs @@ -154,10 +154,7 @@ fn do_test_htlc_interception_flags( }, Some(ForwardingMod::CLTVBelowConfig) => { route.paths[0].hops[0].cltv_expiry_delta = 6 * 12; - assert!( - should_intercept, - "No reason to test failing if we aren't trying to intercept", - ); + assert!(should_intercept, "No reason to test failing if we aren't trying to intercept"); }, Some(ForwardingMod::CLTVBelowMin) => { route.paths[0].hops[0].cltv_expiry_delta = 6; From f6ecc6104c1c7ee78008374dbf34e7522d0f6d60 Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Sat, 7 Feb 2026 16:50:35 +0000 Subject: [PATCH 7/7] f sp --- lightning/src/util/config.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lightning/src/util/config.rs b/lightning/src/util/config.rs index aa9dd667204..56294614da7 100644 --- a/lightning/src/util/config.rs +++ b/lightning/src/util/config.rs @@ -964,8 +964,8 @@ pub enum HTLCInterceptionFlags { /// public channel will instead generate an [`Event::HTLCIntercepted`] which must be handled /// the same as any other intercepted HTLC. /// - /// This primarily exists for completeness, and generally interception of of HTLCs between - /// public channels is *strongly* discouraged. + /// This primarily exists for completeness, and generally interception of HTLCs between public + /// channels is *strongly* discouraged. /// /// Note that HTLCs which do not pay the configured fee rate or do not meet the /// [`ChannelConfig::cltv_expiry_delta`] will fail and not be intercepted.