Support generic HTLC interception
What changed, and why it matters
This commit expands an existing Lightning payment interception feature in LDK. Previously, developers could only intercept payments routed through special fake channel IDs. Now they can intercept a wider range of forwarded payments (private channels, public channels, unknown channel IDs) using a configurable bitfield. The change is a feature addition, not a fix for a known vulnerability, but it touches sensitive payment-forwarding logic and changes a public configuration option from a boolean to a bitfield.
Review the new interception paths and bitfield handling for correctness, especially the interaction with channel liveness checks and the requirement that intercepted HTLCs must be handled within seconds. Ensure serialization backward compatibility is preserved for the renamed config field. Consider whether the new unknown-SCID interception path could be abused to delay or stall payments if not handled promptly.
Security signals we found
Sensitive payment-forwarding logic modified
Public API/config field changed (accept_intercept_htlcs -> htlc_interception_flags)
New interception surface for unknown SCIDs and public channels
Documentation warns against holding intercepted HTLCs for more than a few seconds to avoid DoS
Extensive new test coverage added for interception behavior
Evidence from the diff
The commit replaces UserConfig::accept_intercept_htlcs (bool) with UserConfig::htlc_interception_flags (u8 bitfield) and introduces the HTLCInterceptionFlags enum. It refactors ChannelManager::forward_needs_intercept into two helpers (forward_needs_intercept_to_known_chan and forward_needs_intercept_to_unknown_chan), updates can_forward_htlc_to_outgoing_channel and can_forward_htlc_should_intercept to apply the new flags, and adds extensive tests. The change allows interception of HTLCs destined for intercept SCIDs, offline/online private channels, public channels, and unknown SCIDs. It also tightens documentation to require timely handling of intercepted HTLCs.
Changed components
lightning/src/ln/channelmanager.rslightning/src/util/config.rslightning/src/events/mod.rslightning/src/ln/interception_tests.rsInspect captured patch +504 / −73
diff --git a/lightning-liquidity/tests/lsps2_integration_tests.rs b/lightning-liquidity/tests/lsps2_integration_tests.rs
index 2e469d1..45c2891 100644
--- a/lightning-liquidity/tests/lsps2_integration_tests.rs
+++ b/lightning-liquidity/tests/lsps2_integration_tests.rs
@@ -38,6 +38,7 @@ use lightning::ln::peer_handler::CustomMessageHandler;
use lightning::log_error;
use lightning::routing::router::{RouteHint, RouteHintHop};
use lightning::sign::NodeSigner;
+use lightning::util::config::HTLCInterceptionFlags;
use lightning::util::errors::APIError;
use lightning::util::logger::Logger;
use lightning::util::test_utils::{TestBroadcaster, TestStore};
@@ -1157,7 +1158,7 @@ fn client_trusts_lsp_end_to_end_test() {
let chanmon_cfgs = create_chanmon_cfgs(3);
let node_cfgs = create_node_cfgs(3, &chanmon_cfgs);
let mut service_node_config = test_default_channel_config();
- service_node_config.accept_intercept_htlcs = true;
+ service_node_config.htlc_interception_flags = HTLCInterceptionFlags::ToInterceptSCIDs as u8;
let mut client_node_config = test_default_channel_config();
client_node_config.manually_accept_inbound_channels = true;
@@ -1630,7 +1631,7 @@ fn late_payment_forwarded_and_safe_after_force_close_does_not_broadcast() {
let chanmon_cfgs = create_chanmon_cfgs(3);
let node_cfgs = create_node_cfgs(3, &chanmon_cfgs);
let mut service_node_config = test_default_channel_config();
- service_node_config.accept_intercept_htlcs = true;
+ service_node_config.htlc_interception_flags = HTLCInterceptionFlags::ToInterceptSCIDs as u8;
let mut client_node_config = test_default_channel_config();
client_node_config.manually_accept_inbound_channels = true;
@@ -1821,7 +1822,7 @@ fn htlc_timeout_before_client_claim_results_in_handling_failed() {
let chanmon_cfgs = create_chanmon_cfgs(3);
let node_cfgs = create_node_cfgs(3, &chanmon_cfgs);
let mut service_node_config = test_default_channel_config();
- service_node_config.accept_intercept_htlcs = true;
+ service_node_config.htlc_interception_flags = HTLCInterceptionFlags::ToInterceptSCIDs as u8;
let mut client_node_config = test_default_channel_config();
client_node_config.manually_accept_inbound_channels = true;
@@ -2157,7 +2158,7 @@ fn client_trusts_lsp_partial_fee_does_not_trigger_broadcast() {
let chanmon_cfgs = create_chanmon_cfgs(3);
let node_cfgs = create_node_cfgs(3, &chanmon_cfgs);
let mut service_node_config = test_default_channel_config();
- service_node_config.accept_intercept_htlcs = true;
+ service_node_config.htlc_interception_flags = HTLCInterceptionFlags::ToInterceptSCIDs as u8;
let mut client_node_config = test_default_channel_config();
client_node_config.manually_accept_inbound_channels = true;
diff --git a/lightning/src/events/mod.rs b/lightning/src/events/mod.rs
index d97ae60..277ce61 100644
--- a/lightning/src/events/mod.rs
+++ b/lightning/src/events/mod.rs
@@ -1250,28 +1250,29 @@ pub enum Event {
short_channel_id: Option<u64>,
},
/// Used to indicate that we've intercepted an HTLC forward. This event will only be generated if
- /// you've encoded an intercept scid in the receiver's invoice route hints using
- /// [`ChannelManager::get_intercept_scid`] and have set [`UserConfig::accept_intercept_htlcs`].
+ /// you've set some flags on [`UserConfig::htlc_interception_flags`].
///
/// [`ChannelManager::forward_intercepted_htlc`] or
- /// [`ChannelManager::fail_intercepted_htlc`] MUST be called in response to this event. See
- /// their docs for more information.
+ /// [`ChannelManager::fail_intercepted_htlc`] MUST be called in response to this event in a
+ /// timely manner (i.e. within some number of seconds, not minutes). See their docs for more
+ /// information.
///
/// # 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.
///
- /// [`ChannelManager::get_intercept_scid`]: crate::ln::channelmanager::ChannelManager::get_intercept_scid
- /// [`UserConfig::accept_intercept_htlcs`]: crate::util::config::UserConfig::accept_intercept_htlcs
+ /// [`UserConfig::htlc_interception_flags`]: crate::util::config::UserConfig::htlc_interception_flags
/// [`ChannelManager::forward_intercepted_htlc`]: crate::ln::channelmanager::ChannelManager::forward_intercepted_htlc
/// [`ChannelManager::fail_intercepted_htlc`]: crate::ln::channelmanager::ChannelManager::fail_intercepted_htlc
HTLCIntercepted {
/// An id to help LDK identify which HTLC is being forwarded or failed.
intercept_id: InterceptId,
- /// The fake scid that was programmed as the next hop's scid, generated using
- /// [`ChannelManager::get_intercept_scid`].
+ /// The SCID which was selected by the sender as the next hop. It may point to one of our
+ /// channels, an intercept SCID generated with [`ChannelManager::get_intercept_scid`], or
+ /// an unknown SCID if [`HTLCInterceptionFlags::ToUnknownSCIDs`] was selected.
///
/// [`ChannelManager::get_intercept_scid`]: crate::ln::channelmanager::ChannelManager::get_intercept_scid
+ /// [`HTLCInterceptionFlags::ToUnknownSCIDs`]: crate::util::config::HTLCInterceptionFlags::ToUnknownSCIDs
requested_next_hop_scid: u64,
/// The payment hash used for this HTLC.
payment_hash: PaymentHash,
@@ -1282,7 +1283,8 @@ pub enum Event {
/// Forwarding less than this amount may break compatibility with LDK versions prior to 0.0.116.
///
/// Note that LDK will NOT check that expected fees were factored into this value. You MUST
- /// check that whatever fee you want has been included here or subtract it as required. Further,
+ /// check that whatever fee you want has been included here (by comparing with
+ /// [`Self::HTLCIntercepted::inbound_amount_msat`]) or subtract it as required. Further,
/// LDK will not stop you from forwarding more than you received.
expected_outbound_amount_msat: u64,
},
diff --git a/lightning/src/ln/async_payments_tests.rs b/lightning/src/ln/async_payments_tests.rs
index 0b26529..b8d2321 100644
--- a/lightning/src/ln/async_payments_tests.rs
+++ b/lightning/src/ln/async_payments_tests.rs
@@ -60,7 +60,7 @@ use crate::sign::NodeSigner;
use crate::sync::Mutex;
use crate::types::features::Bolt12InvoiceFeatures;
use crate::types::payment::{PaymentHash, PaymentPreimage, PaymentSecret};
-use crate::util::config::UserConfig;
+use crate::util::config::{HTLCInterceptionFlags, UserConfig};
use crate::util::ser::Writeable;
use bitcoin::constants::ChainHash;
use bitcoin::network::Network;
@@ -3063,7 +3063,7 @@ fn intercepted_hold_htlc() {
recipient_cfg.channel_handshake_limits.force_announced_channel_preference = false;
let mut lsp_cfg = test_default_channel_config();
- lsp_cfg.accept_intercept_htlcs = true;
+ lsp_cfg.htlc_interception_flags = HTLCInterceptionFlags::ToInterceptSCIDs as u8;
lsp_cfg.accept_forwards_to_priv_channels = true;
lsp_cfg.enable_htlc_hold = true;
diff --git a/lightning/src/ln/blinded_payment_tests.rs b/lightning/src/ln/blinded_payment_tests.rs
index 74981ea..5a7c326 100644
--- a/lightning/src/ln/blinded_payment_tests.rs
+++ b/lightning/src/ln/blinded_payment_tests.rs
@@ -32,7 +32,7 @@ use crate::routing::router::{
use crate::sign::{NodeSigner, PeerStorageKey, ReceiveAuthKey, Recipient};
use crate::types::features::{BlindedHopFeatures, ChannelFeatures, NodeFeatures};
use crate::types::payment::{PaymentHash, PaymentSecret};
-use crate::util::config::UserConfig;
+use crate::util::config::{HTLCInterceptionFlags, UserConfig};
use crate::util::ser::{WithoutLength, Writeable};
use crate::util::test_utils::{self, bytes_from_hex, pubkey_from_hex, secret_from_hex};
use bitcoin::hex::DisplayHex;
@@ -769,7 +769,8 @@ fn do_blinded_intercept_payment(intercept_node_fails: bool) {
let chanmon_cfgs = create_chanmon_cfgs(3);
let node_cfgs = create_node_cfgs(3, &chanmon_cfgs);
let mut intercept_forwards_config = test_default_channel_config();
- intercept_forwards_config.accept_intercept_htlcs = true;
+ intercept_forwards_config.htlc_interception_flags =
+ HTLCInterceptionFlags::ToInterceptSCIDs as u8;
let node_chanmgrs = create_node_chanmgrs(3, &node_cfgs, &[None, Some(intercept_forwards_config), None]);
let nodes = create_network(3, &node_cfgs, &node_chanmgrs);
create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0);
diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs
index 8bef968..eeb5a53 100644
--- a/lightning/src/ln/channelmanager.rs
+++ b/lightning/src/ln/channelmanager.rs
@@ -122,7 +122,9 @@ use crate::types::features::{
};
use crate::types::payment::{PaymentHash, PaymentPreimage, PaymentSecret};
use crate::types::string::UntrustedString;
-use crate::util::config::{ChannelConfig, ChannelConfigOverrides, ChannelConfigUpdate, UserConfig};
+use crate::util::config::{
+ ChannelConfig, ChannelConfigOverrides, ChannelConfigUpdate, HTLCInterceptionFlags, UserConfig,
+};
use crate::util::errors::APIError;
use crate::util::logger::{Level, Logger, WithContext};
use crate::util::scid_utils::fake_scid;
@@ -4768,27 +4770,53 @@ where
}
}
- fn forward_needs_intercept(
- &self, outbound_chan: Option<&FundedChannel<SP>>, outgoing_scid: u64,
- ) -> bool {
- if outbound_chan.is_none() {
- if fake_scid::is_valid_intercept(
- &self.fake_scid_rand_bytes,
- outgoing_scid,
- &self.chain_hash,
- ) {
- if self.config.read().unwrap().accept_intercept_htlcs {
+ fn forward_needs_intercept_to_known_chan(&self, outbound_chan: &FundedChannel<SP>) -> bool {
+ let intercept_flags = self.config.read().unwrap().htlc_interception_flags;
+ if !outbound_chan.context.should_announce() {
+ if outbound_chan.context.is_connected() {
+ if intercept_flags & (HTLCInterceptionFlags::ToOnlinePrivateChannels as u8) != 0 {
+ return true;
+ }
+ } else {
+ if intercept_flags & (HTLCInterceptionFlags::ToOfflinePrivateChannels as u8) != 0 {
return true;
}
}
+ } else {
+ if intercept_flags & (HTLCInterceptionFlags::ToPublicChannels as u8) != 0 {
+ return true;
+ }
+ }
+ false
+ }
+
+ fn forward_needs_intercept_to_unknown_chan(&self, outgoing_scid: u64) -> bool {
+ let intercept_flags = self.config.read().unwrap().htlc_interception_flags;
+ if fake_scid::is_valid_intercept(
+ &self.fake_scid_rand_bytes,
+ outgoing_scid,
+ &self.chain_hash,
+ ) {
+ if intercept_flags & (HTLCInterceptionFlags::ToInterceptSCIDs as u8) != 0 {
+ return true;
+ }
+ } else if fake_scid::is_valid_phantom(
+ &self.fake_scid_rand_bytes,
+ outgoing_scid,
+ &self.chain_hash,
+ ) {
+ // Handled as a normal forward
+ } else if intercept_flags & (HTLCInterceptionFlags::ToUnknownSCIDs as u8) != 0 {
+ return true;
}
false
}
#[rustfmt::skip]
fn can_forward_htlc_to_outgoing_channel(
- &self, chan: &mut FundedChannel<SP>, msg: &msgs::UpdateAddHTLC, next_packet: &NextPacketDetails
- ) -> Result<bool, LocalHTLCFailureReason> {
+ &self, chan: &mut FundedChannel<SP>, msg: &msgs::UpdateAddHTLC,
+ next_packet: &NextPacketDetails, will_intercept: bool,
+ ) -> Result<(), LocalHTLCFailureReason> {
if !chan.context.should_announce()
&& !self.config.read().unwrap().accept_forwards_to_priv_channels
{
@@ -4797,7 +4825,6 @@ where
// we don't allow forwards outbound over them.
return Err(LocalHTLCFailureReason::PrivateChannelForward);
}
- let intercepted;
if let HopConnector::ShortChannelId(outgoing_scid) = next_packet.outgoing_connector {
if chan.funding.get_channel_type().supports_scid_privacy() && outgoing_scid != chan.context.outbound_scid_alias() {
// `option_scid_alias` (referred to in LDK as `scid_privacy`) means
@@ -4805,7 +4832,6 @@ where
// we don't have the channel here.
return Err(LocalHTLCFailureReason::RealSCIDForward);
}
- intercepted = self.forward_needs_intercept(Some(chan), outgoing_scid);
} else {
return Err(LocalHTLCFailureReason::InvalidTrampolineForward);
}
@@ -4815,7 +4841,7 @@ where
// around to doing the actual forward, but better to fail early if we can and
// hopefully an attacker trying to path-trace payments cannot make this occur
// on a small/per-node/per-channel scale.
- if !intercepted && !chan.context.is_live() {
+ if !will_intercept && !chan.context.is_live() {
if !chan.context.is_enabled() {
return Err(LocalHTLCFailureReason::ChannelDisabled);
} else if !chan.context.is_connected() {
@@ -4827,9 +4853,7 @@ where
if next_packet.outgoing_amt_msat < chan.context.get_counterparty_htlc_minimum_msat() {
return Err(LocalHTLCFailureReason::AmountBelowMinimum);
}
- chan.htlc_satisfies_config(msg, next_packet.outgoing_amt_msat, next_packet.outgoing_cltv_value)?;
-
- Ok(intercepted)
+ chan.htlc_satisfies_config(msg, next_packet.outgoing_amt_msat, next_packet.outgoing_cltv_value)
}
/// Executes a callback `C` that returns some value `X` on the channel found with the given
@@ -4855,10 +4879,10 @@ where
}
}
- fn can_forward_htlc_intercepted(
- &self, msg: &msgs::UpdateAddHTLC, next_packet_details: &NextPacketDetails,
+ fn can_forward_htlc_should_intercept(
+ &self, msg: &msgs::UpdateAddHTLC, next_hop: &NextPacketDetails,
) -> Result<bool, LocalHTLCFailureReason> {
- let outgoing_scid = match next_packet_details.outgoing_connector {
+ let outgoing_scid = match next_hop.outgoing_connector {
HopConnector::ShortChannelId(scid) => scid,
HopConnector::Dummy => {
// Dummy hops are only used for path padding and must not reach HTLC processing.
@@ -4870,22 +4894,23 @@ where
},
};
// TODO: We do the fake SCID namespace check a bunch of times here (and indirectly via
- // `forward_needs_intercept`, including as called in
+ // `forward_needs_intercept_*`, including as called in
// `can_forward_htlc_to_outgoing_channel`), we should find a way to reduce the number of
// times we do it.
let intercept =
match self.do_funded_channel_callback(outgoing_scid, |chan: &mut FundedChannel<SP>| {
- self.can_forward_htlc_to_outgoing_channel(chan, msg, next_packet_details)
+ let intercept = self.forward_needs_intercept_to_known_chan(chan);
+ self.can_forward_htlc_to_outgoing_channel(chan, msg, next_hop, intercept)?;
+ Ok(intercept)
}) {
Some(Ok(intercept)) => intercept,
Some(Err(e)) => return Err(e),
None => {
// Perform basic sanity checks on the amounts and CLTV being forwarded
- if next_packet_details.outgoing_amt_msat > msg.amount_msat {
+ if next_hop.outgoing_amt_msat > msg.amount_msat {
return Err(LocalHTLCFailureReason::FeeInsufficient);
}
- let cltv_delta =
- msg.cltv_expiry.saturating_sub(next_packet_details.outgoing_cltv_value);
+ let cltv_delta = msg.cltv_expiry.saturating_sub(next_hop.outgoing_cltv_value);
if cltv_delta < MIN_CLTV_EXPIRY_DELTA.into() {
return Err(LocalHTLCFailureReason::IncorrectCLTVExpiry);
}
@@ -4896,7 +4921,7 @@ where
&self.chain_hash,
) {
false
- } else if self.forward_needs_intercept(None, outgoing_scid) {
+ } else if self.forward_needs_intercept_to_unknown_chan(outgoing_scid) {
true
} else {
return Err(LocalHTLCFailureReason::UnknownNextPeer);
@@ -4905,11 +4930,7 @@ where
};
let cur_height = self.best_block.read().unwrap().height + 1;
- check_incoming_htlc_cltv(
- cur_height,
- next_packet_details.outgoing_cltv_value,
- msg.cltv_expiry,
- )?;
+ check_incoming_htlc_cltv(cur_height, next_hop.outgoing_cltv_value, msg.cltv_expiry)?;
Ok(intercept)
}
@@ -6641,11 +6662,8 @@ where
/// Intercepted HTLCs can be useful for Lightning Service Providers (LSPs) to open a just-in-time
/// channel to a receiving node if the node lacks sufficient inbound liquidity.
///
- /// To make use of intercepted HTLCs, set [`UserConfig::accept_intercept_htlcs`] and use
- /// [`ChannelManager::get_intercept_scid`] to generate short channel id(s) to put in the
- /// receiver's invoice route hints. These route hints will signal to LDK to generate an
- /// [`HTLCIntercepted`] event when it receives the forwarded HTLC, and this method or
- /// [`ChannelManager::fail_intercepted_htlc`] MUST be called in response to the event.
+ /// To make use of intercepted HTLCs, set [`UserConfig::htlc_interception_flags`] must have a
+ /// non-0 value.
///
/// Note that LDK does not enforce fee requirements in `amt_to_forward_msat`, and will not stop
/// you from forwarding more than you received. See
@@ -6655,7 +6673,7 @@ where
/// Errors if the event was not handled in time, in which case the HTLC was automatically failed
/// backwards.
///
- /// [`UserConfig::accept_intercept_htlcs`]: crate::util::config::UserConfig::accept_intercept_htlcs
+ /// [`UserConfig::htlc_interception_flags`]: crate::util::config::UserConfig::htlc_interception_flags
/// [`HTLCIntercepted`]: events::Event::HTLCIntercepted
/// [`HTLCIntercepted::expected_outbound_amount_msat`]: events::Event::HTLCIntercepted::expected_outbound_amount_msat
// TODO: when we move to deciding the best outbound channel at forward time, only take
@@ -6973,7 +6991,9 @@ 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_intercepted(&update_add_htlc, next_packet_details) {
+ match self
+ .can_forward_htlc_should_intercept(&update_add_htlc, next_packet_details)
+ {
Err(reason) => {
fail_htlc_continue_to_next!(reason);
},
@@ -16329,9 +16349,9 @@ where
let should_intercept = self
.do_funded_channel_callback(next_hop_scid, |chan| {
- self.forward_needs_intercept(Some(chan), next_hop_scid)
+ self.forward_needs_intercept_to_known_chan(chan)
})
- .unwrap_or_else(|| self.forward_needs_intercept(None, next_hop_scid));
+ .unwrap_or_else(|| self.forward_needs_intercept_to_unknown_chan(next_hop_scid));
if should_intercept {
let intercept_id = InterceptId::from_htlc_id_and_chan_id(
diff --git a/lightning/src/ln/interception_tests.rs b/lightning/src/ln/interception_tests.rs
new file mode 100644
index 0000000..11b5de1
--- /dev/null
+++ b/lightning/src/ln/interception_tests.rs
@@ -0,0 +1,290 @@
+// This file is Copyright its original authors, visible in version control
+// history.
+//
+// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
+// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
+// You may not use this file except in accordance with one or both of these
+// licenses.
+
+//! Tests that test standing up a network of ChannelManagers, creating channels, sending
+//! payments/messages between them, and often checking the resulting ChannelMonitors are able to
+//! claim outputs on-chain.
+
+use crate::events::{Event, HTLCHandlingFailureReason, HTLCHandlingFailureType};
+use crate::ln::channelmanager::{PaymentId, RecipientOnionFields};
+use crate::ln::msgs::{BaseMessageHandler, ChannelMessageHandler};
+use crate::ln::onion_utils::LocalHTLCFailureReason;
+use crate::routing::router::PaymentParameters;
+use crate::util::config::HTLCInterceptionFlags;
+
+use crate::prelude::*;
+
+use crate::ln::functional_test_utils::*;
+
+#[derive(Clone, Copy, PartialEq, Eq)]
+enum ForwardingMod {
+ FeeTooLow,
+ CLTVBelowConfig,
+ CLTVBelowMin,
+}
+
+fn do_test_htlc_interception_flags(
+ flags_bitmask: u8, flag: HTLCInterceptionFlags, modification: Option<ForwardingMod>,
+) {
+ use HTLCInterceptionFlags as Flag;
+
+ assert_eq!((flag as isize).count_ones(), 1, "We can only test one type of HTLC at once");
+
+ // Tests that the `htlc_interception_flags` bitmask given by `flags_bitmask` correctly
+ // intercepts (or doesn't intercept) an HTLC which is of type `flag`
+ let chanmon_cfgs = create_chanmon_cfgs(3);
+ let node_cfgs = create_node_cfgs(3, &chanmon_cfgs);
+
+ let mut intercept_config = test_default_channel_config();
+ intercept_config.htlc_interception_flags = flags_bitmask;
+ intercept_config.channel_config.forwarding_fee_base_msat = 1000;
+ intercept_config.channel_config.cltv_expiry_delta = 6 * 24;
+ intercept_config.accept_forwards_to_priv_channels = true;
+
+ 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 node_0_id = nodes[0].node.get_our_node_id();
+ let node_1_id = nodes[1].node.get_our_node_id();
+ let node_2_id = nodes[2].node.get_our_node_id();
+
+ // 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 => {
+ 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)
+ },
+ Flag::ToInterceptSCIDs | Flag::ToPublicChannels | 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)
+ } else if flag == Flag::ToUnknownSCIDs {
+ (42424242, chan_id)
+ } else {
+ panic!();
+ }
+ },
+ _ => panic!("Combined flags aren't allowed"),
+ };
+
+ // Start every node on the same block height to ensure we don't hit spurious CLTV issues
+ connect_blocks(&nodes[0], 2 * CHAN_CONFIRM_DEPTH + 1 - nodes[0].best_block_info().1);
+ connect_blocks(&nodes[1], 2 * CHAN_CONFIRM_DEPTH + 1 - nodes[1].best_block_info().1);
+ connect_blocks(&nodes[2], 2 * CHAN_CONFIRM_DEPTH + 1 - nodes[2].best_block_info().1);
+
+ // Send the HTLC from nodes[0] to nodes[1] and process it to generate the interception (if
+ // we're set to intercept it).
+ let amt_msat = 100_000;
+ let bolt11 = nodes[2].node.create_bolt11_invoice(Default::default()).unwrap();
+ let pay_params = PaymentParameters::from_bolt11_invoice(&bolt11);
+ let (mut route, payment_hash, payment_preimage, payment_secret) =
+ 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;
+ match modification {
+ Some(ForwardingMod::FeeTooLow) => {
+ assert!(
+ interception_bit_match,
+ "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,
+ "No reason to test failing if we aren't trying to intercept",
+ );
+ },
+ Some(ForwardingMod::CLTVBelowMin) => {
+ route.paths[0].hops[0].cltv_expiry_delta = 6;
+ },
+ None => {},
+ }
+
+ let onion = RecipientOnionFields::secret_only(payment_secret);
+ 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);
+
+ let payment_event = SendEvent::from_node(&nodes[0]);
+ nodes[1].node.handle_update_add_htlc(node_0_id, &payment_event.msgs[0]);
+ 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 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;
+ let events = nodes[1].node.get_and_clear_pending_events();
+ assert_eq!(events.len(), 1, "{events:?}");
+ if let Event::HTLCIntercepted { intercept_id: id, requested_next_hop_scid, .. } = &events[0]
+ {
+ assert_eq!(*requested_next_hop_scid, target_scid,
+ "Bitmask {flags_bitmask:#x}: Expected interception for bit {flag:?} to target SCID {target_scid}");
+ intercept_id = *id;
+ } else {
+ panic!("{events:?}");
+ }
+
+ if flag == Flag::ToOfflinePrivateChannels {
+ let mut reconnect_args = ReconnectArgs::new(&nodes[1], &nodes[2]);
+ reconnect_args.send_channel_ready = (true, true);
+ reconnect_nodes(reconnect_args);
+ }
+
+ nodes[1]
+ .node
+ .forward_intercepted_htlc(intercept_id, &target_chan_id, node_2_id, amt_msat)
+ .unwrap();
+ expect_and_process_pending_htlcs(&nodes[1], false);
+ check_added_monitors(&nodes[1], 1);
+
+ let forward_ev = SendEvent::from_node(&nodes[1]);
+ nodes[2].node.handle_update_add_htlc(node_1_id, &forward_ev.msgs[0]);
+ do_commitment_signed_dance(&nodes[2], &nodes[1], &forward_ev.commitment_msg, false, true);
+
+ nodes[2].node.process_pending_htlc_forwards();
+ expect_payment_claimable!(nodes[2], payment_hash, payment_secret, amt_msat);
+ claim_payment(&nodes[0], &[&nodes[1], &nodes[2]], payment_preimage);
+ } else {
+ // 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) => {
+ check_added_monitors(&nodes[1], 1);
+
+ let forward_ev = SendEvent::from_node(&nodes[1]);
+ assert_eq!(forward_ev.node_id, node_2_id);
+ nodes[2].node.handle_update_add_htlc(node_1_id, &forward_ev.msgs[0]);
+ let commitment = &forward_ev.commitment_msg;
+ do_commitment_signed_dance(&nodes[2], &nodes[1], commitment, false, true);
+
+ nodes[2].node.process_pending_htlc_forwards();
+ expect_payment_claimable!(nodes[2], payment_hash, payment_secret, amt_msat);
+ claim_payment(&nodes[0], &[&nodes[1], &nodes[2]], payment_preimage);
+ },
+ _ => {
+ let events = nodes[1].node.get_and_clear_pending_events();
+ let reason_from_mod = match modification {
+ Some(ForwardingMod::FeeTooLow) => Some(LocalHTLCFailureReason::FeeInsufficient),
+ Some(ForwardingMod::CLTVBelowConfig) => {
+ Some(LocalHTLCFailureReason::IncorrectCLTVExpiry)
+ },
+ Some(ForwardingMod::CLTVBelowMin) => {
+ Some(LocalHTLCFailureReason::IncorrectCLTVExpiry)
+ },
+ None => None,
+ };
+ let (expected_failure_type, reason);
+ if flag == Flag::ToOfflinePrivateChannels {
+ expected_failure_type = HTLCHandlingFailureType::Forward {
+ node_id: Some(node_2_id),
+ channel_id: target_chan_id,
+ };
+ reason = reason_from_mod.unwrap_or(LocalHTLCFailureReason::PeerOffline);
+ } else if flag == Flag::ToInterceptSCIDs {
+ expected_failure_type = HTLCHandlingFailureType::InvalidForward {
+ requested_forward_scid: target_scid,
+ };
+ reason = reason_from_mod.unwrap_or(LocalHTLCFailureReason::UnknownNextPeer);
+ } else if flag == Flag::ToUnknownSCIDs {
+ expected_failure_type = HTLCHandlingFailureType::InvalidForward {
+ requested_forward_scid: target_scid,
+ };
+ reason = reason_from_mod.unwrap_or(LocalHTLCFailureReason::UnknownNextPeer);
+ } else {
+ expected_failure_type = HTLCHandlingFailureType::Forward {
+ node_id: Some(node_2_id),
+ channel_id: target_chan_id,
+ };
+ reason = reason_from_mod
+ .expect("We should only fail because of a mod or unknown next-hop");
+ }
+ if let Event::HTLCHandlingFailed { failure_reason, failure_type, .. } = &events[0] {
+ assert_eq!(*failure_reason, Some(HTLCHandlingFailureReason::Local { reason }));
+ assert_eq!(*failure_type, expected_failure_type);
+ } else {
+ panic!("{events:?}");
+ }
+
+ check_added_monitors(&nodes[1], 1);
+ let fail_msgs = get_htlc_update_msgs(&nodes[1], &node_0_id);
+ nodes[0].node.handle_update_fail_htlc(node_1_id, &fail_msgs.update_fail_htlcs[0]);
+ let commitment = fail_msgs.commitment_signed;
+ do_commitment_signed_dance(&nodes[0], &nodes[1], &commitment, true, true);
+ expect_payment_failed!(nodes[0], payment_hash, false);
+ },
+ }
+ }
+}
+
+const MAX_BITMASK: u8 = HTLCInterceptionFlags::AllValidHTLCs as u8;
+const ALL_FLAGS: [HTLCInterceptionFlags; 5] = [
+ HTLCInterceptionFlags::ToInterceptSCIDs,
+ HTLCInterceptionFlags::ToOfflinePrivateChannels,
+ HTLCInterceptionFlags::ToOnlinePrivateChannels,
+ HTLCInterceptionFlags::ToPublicChannels,
+ HTLCInterceptionFlags::ToUnknownSCIDs,
+];
+
+#[test]
+fn test_htlc_interception_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");
+
+ // 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 flag in ALL_FLAGS {
+ do_test_htlc_interception_flags(flags_bitmask, flag, None);
+ }
+ }
+}
+
+#[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
+ // config.
+ let have_chan_flags = [
+ HTLCInterceptionFlags::ToOfflinePrivateChannels,
+ HTLCInterceptionFlags::ToOnlinePrivateChannels,
+ HTLCInterceptionFlags::ToPublicChannels,
+ ];
+ for flag in have_chan_flags {
+ do_test_htlc_interception_flags(flag as u8, flag, Some(ForwardingMod::FeeTooLow));
+ do_test_htlc_interception_flags(flag as u8, flag, Some(ForwardingMod::CLTVBelowConfig));
+ }
+}
+
+#[test]
+fn test_htlc_bad_no_chan() {
+ // Test that setting the CLTV below the hard-coded minimum fails whether we're intercepting for
+ // a channel or not.
+ for flag in ALL_FLAGS {
+ do_test_htlc_interception_flags(flag as u8, flag, Some(ForwardingMod::CLTVBelowMin));
+ }
+}
diff --git a/lightning/src/ln/mod.rs b/lightning/src/ln/mod.rs
index e782fee..b077c98 100644
--- a/lightning/src/ln/mod.rs
+++ b/lightning/src/ln/mod.rs
@@ -84,6 +84,9 @@ pub mod functional_tests;
#[cfg(any(test, feature = "_externalize_tests"))]
#[allow(unused_mut)]
pub mod htlc_reserve_unit_tests;
+#[cfg(any(test, feature = "_externalize_tests"))]
+#[allow(unused_mut)]
+pub mod interception_tests;
#[cfg(test)]
#[allow(unused_mut)]
mod max_payment_path_len_tests;
diff --git a/lightning/src/ln/payment_tests.rs b/lightning/src/ln/payment_tests.rs
index 8f209c8..8ac87fb 100644
--- a/lightning/src/ln/payment_tests.rs
+++ b/lightning/src/ln/payment_tests.rs
@@ -45,6 +45,7 @@ use crate::sign::EntropySource;
use crate::types::features::{Bolt11InvoiceFeatures, ChannelTypeFeatures};
use crate::types::payment::{PaymentHash, PaymentPreimage, PaymentSecret};
use crate::types::string::UntrustedString;
+use crate::util::config::HTLCInterceptionFlags;
use crate::util::errors::APIError;
use crate::util::ser::Writeable;
use crate::util::test_utils;
@@ -2210,7 +2211,8 @@ fn do_test_intercepted_payment(test: InterceptTest) {
let mut zero_conf_chan_config = test_default_channel_config();
zero_conf_chan_config.manually_accept_inbound_channels = true;
let mut intercept_forwards_config = test_default_channel_config();
- intercept_forwards_config.accept_intercept_htlcs = true;
+ intercept_forwards_config.htlc_interception_flags =
+ HTLCInterceptionFlags::ToInterceptSCIDs as u8;
let configs = [None, Some(intercept_forwards_config), Some(zero_conf_chan_config)];
let node_chanmgrs = create_node_chanmgrs(3, &node_cfgs, &configs);
@@ -2435,7 +2437,8 @@ fn do_accept_underpaying_htlcs_config(num_mpp_parts: usize) {
let max_in_flight_percent = 10;
let mut intercept_forwards_config = test_default_channel_config();
- intercept_forwards_config.accept_intercept_htlcs = true;
+ intercept_forwards_config.htlc_interception_flags =
+ HTLCInterceptionFlags::ToInterceptSCIDs as u8;
intercept_forwards_config
.channel_handshake_config
.max_inbound_htlc_value_in_flight_percent_of_channel = max_in_flight_percent;
diff --git a/lightning/src/ln/reload_tests.rs b/lightning/src/ln/reload_tests.rs
index a38262e..4fb2753 100644
--- a/lightning/src/ln/reload_tests.rs
+++ b/lightning/src/ln/reload_tests.rs
@@ -26,7 +26,7 @@ use crate::util::test_channel_signer::TestChannelSigner;
use crate::util::test_utils;
use crate::util::errors::APIError;
use crate::util::ser::{Writeable, ReadableArgs};
-use crate::util::config::UserConfig;
+use crate::util::config::{HTLCInterceptionFlags, UserConfig};
use bitcoin::hashes::Hash;
use bitcoin::hash_types::BlockHash;
@@ -931,7 +931,8 @@ fn do_forwarded_payment_no_manager_persistence(use_cs_commitment: bool, claim_ht
let new_chain_monitor;
let mut intercept_forwards_config = test_default_channel_config();
- intercept_forwards_config.accept_intercept_htlcs = true;
+ intercept_forwards_config.htlc_interception_flags =
+ HTLCInterceptionFlags::ToInterceptSCIDs as u8;
let node_chanmgrs = create_node_chanmgrs(3, &node_cfgs, &[None, Some(intercept_forwards_config), None]);
let nodes_1_deserialized;
@@ -1189,7 +1190,8 @@ fn do_manager_persisted_pre_outbound_edge_forward(intercept_htlc: bool) {
let persister;
let new_chain_monitor;
let mut intercept_forwards_config = test_default_channel_config();
- intercept_forwards_config.accept_intercept_htlcs = true;
+ intercept_forwards_config.htlc_interception_flags =
+ HTLCInterceptionFlags::ToInterceptSCIDs as u8;
let node_chanmgrs = create_node_chanmgrs(3, &node_cfgs, &[None, Some(intercept_forwards_config), None]);
let nodes_1_deserialized;
let mut nodes = create_network(3, &node_cfgs, &node_chanmgrs);
diff --git a/lightning/src/util/config.rs b/lightning/src/util/config.rs
index dd1aaa4..feb326c 100644
--- a/lightning/src/util/config.rs
+++ b/lightning/src/util/config.rs
@@ -855,6 +855,111 @@ impl crate::util::ser::Readable for LegacyChannelConfig {
}
}
+/// Flags which can be set on [`UserConfig::htlc_interception_flags`]. Each flag selects some set
+/// of HTLCs which are forwarded across this node to be intercepted instead, generating an
+/// [`Event::HTLCIntercepted`] instead of automatically forwarding the HTLC and allowing it to be
+/// forwarded or rejected manually.
+///
+/// [`Event::HTLCIntercepted`]: crate::events::Event::HTLCIntercepted
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum HTLCInterceptionFlags {
+ /// If this flag is set, LDK will intercept HTLCs that are attempting to be forwarded over fake
+ /// short channel ids generated via [`ChannelManager::get_intercept_scid`]. This allows you to
+ /// only intercept HTLCs which are specifically marked for interception by the invoice being
+ /// paid.
+ ///
+ /// Note that because LDK is not aware of which channel the HTLC will be forwarded over at the
+ /// time of interception, only basic checks to ensure the fee the HTLC intends to pay is not
+ /// negative and a minimum CLTV delta between the incoming and outgoing HTLC edge are performed
+ /// before the [`Event::HTLCIntercepted`] is generated. You must validate the fee and CLTV
+ /// delta meets your requirements before forwarding the HTLC.
+ ///
+ /// [`ChannelManager::get_intercept_scid`]: crate::ln::channelmanager::ChannelManager::get_intercept_scid
+ /// [`Event::HTLCIntercepted`]: crate::events::Event::HTLCIntercepted
+ ToInterceptSCIDs = 1 << 0,
+ /// If this flag is set, any attempts to forward a payment to a private channel while the
+ /// channel counterparty is offline will instead generate an [`Event::HTLCIntercepted`] which
+ /// must be handled the same as any other intercepted HTLC.
+ ///
+ /// This is useful for LSPs that may need to wake the recipient node (e.g. via a mobile push
+ /// notification). Note that in this case you must ensure that you set a quick timeout to fail
+ /// the HTLC if the recipient node fails to come online (e.g. within 10 seconds).
+ ///
+ /// Before interception, the HTLC is validated against the forwarding config of the outbound
+ /// channel to ensure it pays sufficient fee and meets the
+ /// [`ChannelConfig::cltv_expiry_delta`].
+ ///
+ /// [`Event::HTLCIntercepted`]: crate::events::Event::HTLCIntercepted
+ ToOfflinePrivateChannels = 1 << 1,
+ /// If this flag is set, any attempts to forward a payment to a private channel while the
+ /// channel counterparty is online will instead generate an [`Event::HTLCIntercepted`] which
+ /// must be handled the same as any other intercepted HTLC.
+ ///
+ /// This is the complement to [`Self::ToOfflinePrivateChannels`] and, together, they allow
+ /// intercepting all HTLCs destined for private channels. This may be useful for LSPs that wish
+ /// to take an additional fee paid by the recipient on all forwards to clients.
+ ///
+ /// Before interception, the HTLC is validated against the forwarding config of the outbound
+ /// channel to ensure it pays sufficient fee and meets the
+ /// [`ChannelConfig::cltv_expiry_delta`].
+ ///
+ /// [`Event::HTLCIntercepted`]: crate::events::Event::HTLCIntercepted
+ ToOnlinePrivateChannels = 1 << 2,
+ /// If this flag is set, any attempts to forward a payment to a publicly announced channel will
+ /// instead generate an [`Event::HTLCIntercepted`] which must be handled the same as any other
+ /// intercepted HTLC.
+ ///
+ /// Before interception, the HTLC is validated against the forwarding config of the outbound
+ /// channel to ensure it pays sufficient fee and meets the
+ /// [`ChannelConfig::cltv_expiry_delta`].
+ ///
+ /// [`Event::HTLCIntercepted`]: crate::events::Event::HTLCIntercepted
+ ToPublicChannels = 1 << 3,
+ /// If these flags are set, any attempts to forward a payment to a channel of ours or a fake
+ /// short channel id generated via [`ChannelManager::get_intercept_scid`] will instead generate
+ /// an [`Event::HTLCIntercepted`] which must be handled the same as any other intercepted HTLC.
+ ///
+ /// In the case of intercept SCIDs, only basic checks to ensure the fee the HTLC intends to pay
+ /// is not negative and a minimum CLTV delta between the incoming and outgoing HTLC edge are
+ /// performed before the [`Event::HTLCIntercepted`] is generated. You must validate the fee and
+ /// CLTV delta meets your requirements before forwarding the HTLC.
+ ///
+ /// [`ChannelManager::get_intercept_scid`]: crate::ln::channelmanager::ChannelManager::get_intercept_scid
+ /// [`Event::HTLCIntercepted`]: crate::events::Event::HTLCIntercepted
+ ToAllKnownSCIDs = Self::ToInterceptSCIDs as isize
+ | Self::ToOfflinePrivateChannels as isize
+ | Self::ToOnlinePrivateChannels as isize
+ | Self::ToPublicChannels as isize,
+ /// 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.
+ ///
+ /// Note that because LDK is not aware of which channel the HTLC will be forwarded over at the
+ /// time of interception, only basic checks to ensure the fee the HTLC intends to pay is not
+ /// negative and a minimum CLTV delta between the incoming and outgoing HTLC edge are performed
+ /// before the [`Event::HTLCIntercepted`] is generated. You must validate the fee and CLTV
+ /// delta meets your requirements before forwarding the HTLC.
+ ///
+ /// [`Event::HTLCIntercepted`]: crate::events::Event::HTLCIntercepted
+ ToUnknownSCIDs = 1 << 4,
+ /// 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.
+ ///
+ /// In the case of intercept or unknown SCIDs, only basic checks to ensure the fee the HTLC
+ /// intends to pay is not negative and a minimum CLTV delta between the incoming and outgoing
+ /// HTLC edge are performed before the [`Event::HTLCIntercepted`] is generated. You must
+ /// 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,
+}
+
+impl Into<u8> for HTLCInterceptionFlags {
+ fn into(self) -> u8 {
+ self as u8
+ }
+}
+
/// Top-level config which holds ChannelHandshakeLimits and ChannelConfig.
///
/// `Default::default()` provides sane defaults for most configurations
@@ -907,17 +1012,21 @@ pub struct UserConfig {
/// [`msgs::OpenChannel`]: crate::ln::msgs::OpenChannel
/// [`msgs::AcceptChannel`]: crate::ln::msgs::AcceptChannel
pub manually_accept_inbound_channels: bool,
- /// If this is set to `true`, LDK will intercept HTLCs that are attempting to be forwarded over
- /// fake short channel ids generated via [`ChannelManager::get_intercept_scid`]. Upon HTLC
- /// intercept, LDK will generate an [`Event::HTLCIntercepted`] which MUST be handled by the user.
+ /// Flags consisting of OR'd values from [`HTLCInterceptionFlags`] which describe HTLCs
+ /// forwarded over this node to intercept. Any HTLCs which are intercepted will generate an
+ /// [`Event::HTLCIntercepted`] event which must be handled to forward or fail the HTLC.
///
- /// Setting this to `true` may break backwards compatibility with LDK versions < 0.0.113.
+ /// Do NOT hold on to intercepted HTLCs for more than a few seconds, they must always be
+ /// forwarded or failed nearly immediately to avoid performing accidental denial of service
+ /// attacks against other lightning nodes and being punished appropriately by other nodes.
///
- /// Default value: `false`
+ /// To ensure efficiency and reliable HTLC latency you should ensure you only intercept types
+ /// of HTLCs which you need to manually forward or reject.
+ ///
+ /// Default value: `0` (indicating no HTLCs will be intercepted).
///
- /// [`ChannelManager::get_intercept_scid`]: crate::ln::channelmanager::ChannelManager::get_intercept_scid
/// [`Event::HTLCIntercepted`]: crate::events::Event::HTLCIntercepted
- pub accept_intercept_htlcs: bool,
+ pub htlc_interception_flags: u8,
/// If this is set to `true`, the user needs to manually pay [`Bolt12Invoice`]s when received.
///
/// When set to `true`, [`Event::InvoiceReceived`] will be generated for each received
@@ -984,7 +1093,7 @@ impl Default for UserConfig {
accept_forwards_to_priv_channels: false,
accept_inbound_channels: true,
manually_accept_inbound_channels: false,
- accept_intercept_htlcs: false,
+ htlc_interception_flags: 0,
manually_handle_bolt12_invoices: false,
enable_dual_funded_channels: false,
enable_htlc_hold: false,
@@ -1007,7 +1116,7 @@ impl Readable for UserConfig {
accept_forwards_to_priv_channels: Readable::read(reader)?,
accept_inbound_channels: Readable::read(reader)?,
manually_accept_inbound_channels: Readable::read(reader)?,
- accept_intercept_htlcs: Readable::read(reader)?,
+ htlc_interception_flags: Readable::read(reader)?,
manually_handle_bolt12_invoices: Readable::read(reader)?,
enable_dual_funded_channels: Readable::read(reader)?,
hold_outbound_htlcs_at_next_hop: Readable::read(reader)?,
Why this scored 37/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.