From 5b29f3b827ac1cfc67d68b96397d85597a747ff3 Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Thu, 22 Jan 2026 20:14:09 +0000 Subject: [PATCH 1/2] 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 | 149 +++++++++++------------------ 1 file changed, 55 insertions(+), 94 deletions(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index fd5e5d15b9f..7aa56e1ce2b 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 { @@ -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 779fce3e617357b790536c1ec39b401522b884a5 Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Thu, 22 Jan 2026 21:15:22 +0000 Subject: [PATCH 2/2] 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 7aa56e1ce2b..5e734b8e5f7 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 {