Skip to content

Commit a0723ad

Browse files
committed
Support generic HTLC interception
At various points we've had requests to support more generic HTLC interception in LDK. In most cases, full HTLC interception was not, in fact, the right way to accomplish what the developer wanted, but there have been various times when it might have been. Here, we finally add full HTLC interception support, doing so with a configurable bitfield to allow developers to intercept only certain classes of HTLCs. Specifically, we currently support intercepting HTLCs: * which were to be forwarded to intercept SCIDs (as was already supported), * which were to be forwarded to offline private channels (for LSPs to accept HTLCs for offline clients so that they can attempt to wake them before failing the HTLC), * which were to be forwarded to online private channels (for LSPs to take additional fees or enforce certain policies), * which were to be forwarded over public channels (for general forwarding policy enforcement), * which were to be forwarded to unknown SCIDs (for everything else).
1 parent 04351fd commit a0723ad

10 files changed

Lines changed: 504 additions & 73 deletions

File tree

lightning-liquidity/tests/lsps2_integration_tests.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ use lightning::ln::peer_handler::CustomMessageHandler;
3838
use lightning::log_error;
3939
use lightning::routing::router::{RouteHint, RouteHintHop};
4040
use lightning::sign::NodeSigner;
41+
use lightning::util::config::HTLCInterceptionFlags;
4142
use lightning::util::errors::APIError;
4243
use lightning::util::logger::Logger;
4344
use lightning::util::test_utils::{TestBroadcaster, TestStore};
@@ -1157,7 +1158,7 @@ fn client_trusts_lsp_end_to_end_test() {
11571158
let chanmon_cfgs = create_chanmon_cfgs(3);
11581159
let node_cfgs = create_node_cfgs(3, &chanmon_cfgs);
11591160
let mut service_node_config = test_default_channel_config();
1160-
service_node_config.accept_intercept_htlcs = true;
1161+
service_node_config.htlc_interception_flags = HTLCInterceptionFlags::ToInterceptSCIDs as u8;
11611162

11621163
let mut client_node_config = test_default_channel_config();
11631164
client_node_config.manually_accept_inbound_channels = true;
@@ -1630,7 +1631,7 @@ fn late_payment_forwarded_and_safe_after_force_close_does_not_broadcast() {
16301631
let chanmon_cfgs = create_chanmon_cfgs(3);
16311632
let node_cfgs = create_node_cfgs(3, &chanmon_cfgs);
16321633
let mut service_node_config = test_default_channel_config();
1633-
service_node_config.accept_intercept_htlcs = true;
1634+
service_node_config.htlc_interception_flags = HTLCInterceptionFlags::ToInterceptSCIDs as u8;
16341635

16351636
let mut client_node_config = test_default_channel_config();
16361637
client_node_config.manually_accept_inbound_channels = true;
@@ -1821,7 +1822,7 @@ fn htlc_timeout_before_client_claim_results_in_handling_failed() {
18211822
let chanmon_cfgs = create_chanmon_cfgs(3);
18221823
let node_cfgs = create_node_cfgs(3, &chanmon_cfgs);
18231824
let mut service_node_config = test_default_channel_config();
1824-
service_node_config.accept_intercept_htlcs = true;
1825+
service_node_config.htlc_interception_flags = HTLCInterceptionFlags::ToInterceptSCIDs as u8;
18251826

18261827
let mut client_node_config = test_default_channel_config();
18271828
client_node_config.manually_accept_inbound_channels = true;
@@ -2157,7 +2158,7 @@ fn client_trusts_lsp_partial_fee_does_not_trigger_broadcast() {
21572158
let chanmon_cfgs = create_chanmon_cfgs(3);
21582159
let node_cfgs = create_node_cfgs(3, &chanmon_cfgs);
21592160
let mut service_node_config = test_default_channel_config();
2160-
service_node_config.accept_intercept_htlcs = true;
2161+
service_node_config.htlc_interception_flags = HTLCInterceptionFlags::ToInterceptSCIDs as u8;
21612162

21622163
let mut client_node_config = test_default_channel_config();
21632164
client_node_config.manually_accept_inbound_channels = true;

lightning/src/events/mod.rs

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1250,28 +1250,29 @@ pub enum Event {
12501250
short_channel_id: Option<u64>,
12511251
},
12521252
/// Used to indicate that we've intercepted an HTLC forward. This event will only be generated if
1253-
/// you've encoded an intercept scid in the receiver's invoice route hints using
1254-
/// [`ChannelManager::get_intercept_scid`] and have set [`UserConfig::accept_intercept_htlcs`].
1253+
/// you've set some flags on [`UserConfig::htlc_interception_flags`].
12551254
///
12561255
/// [`ChannelManager::forward_intercepted_htlc`] or
1257-
/// [`ChannelManager::fail_intercepted_htlc`] MUST be called in response to this event. See
1258-
/// their docs for more information.
1256+
/// [`ChannelManager::fail_intercepted_htlc`] MUST be called in response to this event in a
1257+
/// timely manner (i.e. within some number of seconds, not minutes). See their docs for more
1258+
/// information.
12591259
///
12601260
/// # Failure Behavior and Persistence
12611261
/// This event will eventually be replayed after failures-to-handle (i.e., the event handler
12621262
/// returning `Err(ReplayEvent ())`) and will be persisted across restarts.
12631263
///
1264-
/// [`ChannelManager::get_intercept_scid`]: crate::ln::channelmanager::ChannelManager::get_intercept_scid
1265-
/// [`UserConfig::accept_intercept_htlcs`]: crate::util::config::UserConfig::accept_intercept_htlcs
1264+
/// [`UserConfig::htlc_interception_flags`]: crate::util::config::UserConfig::htlc_interception_flags
12661265
/// [`ChannelManager::forward_intercepted_htlc`]: crate::ln::channelmanager::ChannelManager::forward_intercepted_htlc
12671266
/// [`ChannelManager::fail_intercepted_htlc`]: crate::ln::channelmanager::ChannelManager::fail_intercepted_htlc
12681267
HTLCIntercepted {
12691268
/// An id to help LDK identify which HTLC is being forwarded or failed.
12701269
intercept_id: InterceptId,
1271-
/// The fake scid that was programmed as the next hop's scid, generated using
1272-
/// [`ChannelManager::get_intercept_scid`].
1270+
/// The SCID which was selected by the sender as the next hop. It may point to one of our
1271+
/// channels, an intercept SCID generated with [`ChannelManager::get_intercept_scid`], or
1272+
/// an unknown SCID if [`HTLCInterceptionFlags::ToUnknownSCIDs`] was selected.
12731273
///
12741274
/// [`ChannelManager::get_intercept_scid`]: crate::ln::channelmanager::ChannelManager::get_intercept_scid
1275+
/// [`HTLCInterceptionFlags::ToUnknownSCIDs`]: crate::util::config::HTLCInterceptionFlags::ToUnknownSCIDs
12751276
requested_next_hop_scid: u64,
12761277
/// The payment hash used for this HTLC.
12771278
payment_hash: PaymentHash,
@@ -1282,7 +1283,8 @@ pub enum Event {
12821283
/// Forwarding less than this amount may break compatibility with LDK versions prior to 0.0.116.
12831284
///
12841285
/// Note that LDK will NOT check that expected fees were factored into this value. You MUST
1285-
/// check that whatever fee you want has been included here or subtract it as required. Further,
1286+
/// check that whatever fee you want has been included here (by comparing with
1287+
/// [`Self::HTLCIntercepted::inbound_amount_msat`]) or subtract it as required. Further,
12861288
/// LDK will not stop you from forwarding more than you received.
12871289
expected_outbound_amount_msat: u64,
12881290
},

lightning/src/ln/async_payments_tests.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ use crate::sign::NodeSigner;
6060
use crate::sync::Mutex;
6161
use crate::types::features::Bolt12InvoiceFeatures;
6262
use crate::types::payment::{PaymentHash, PaymentPreimage, PaymentSecret};
63-
use crate::util::config::UserConfig;
63+
use crate::util::config::{HTLCInterceptionFlags, UserConfig};
6464
use crate::util::ser::Writeable;
6565
use bitcoin::constants::ChainHash;
6666
use bitcoin::network::Network;
@@ -3063,7 +3063,7 @@ fn intercepted_hold_htlc() {
30633063
recipient_cfg.channel_handshake_limits.force_announced_channel_preference = false;
30643064

30653065
let mut lsp_cfg = test_default_channel_config();
3066-
lsp_cfg.accept_intercept_htlcs = true;
3066+
lsp_cfg.htlc_interception_flags = HTLCInterceptionFlags::ToInterceptSCIDs as u8;
30673067
lsp_cfg.accept_forwards_to_priv_channels = true;
30683068
lsp_cfg.enable_htlc_hold = true;
30693069

lightning/src/ln/blinded_payment_tests.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ use crate::routing::router::{
3232
use crate::sign::{NodeSigner, PeerStorageKey, ReceiveAuthKey, Recipient};
3333
use crate::types::features::{BlindedHopFeatures, ChannelFeatures, NodeFeatures};
3434
use crate::types::payment::{PaymentHash, PaymentSecret};
35-
use crate::util::config::UserConfig;
35+
use crate::util::config::{HTLCInterceptionFlags, UserConfig};
3636
use crate::util::ser::{WithoutLength, Writeable};
3737
use crate::util::test_utils::{self, bytes_from_hex, pubkey_from_hex, secret_from_hex};
3838
use bitcoin::hex::DisplayHex;
@@ -769,7 +769,8 @@ fn do_blinded_intercept_payment(intercept_node_fails: bool) {
769769
let chanmon_cfgs = create_chanmon_cfgs(3);
770770
let node_cfgs = create_node_cfgs(3, &chanmon_cfgs);
771771
let mut intercept_forwards_config = test_default_channel_config();
772-
intercept_forwards_config.accept_intercept_htlcs = true;
772+
intercept_forwards_config.htlc_interception_flags =
773+
HTLCInterceptionFlags::ToInterceptSCIDs as u8;
773774
let node_chanmgrs = create_node_chanmgrs(3, &node_cfgs, &[None, Some(intercept_forwards_config), None]);
774775
let nodes = create_network(3, &node_cfgs, &node_chanmgrs);
775776
create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0);

lightning/src/ln/channelmanager.rs

Lines changed: 62 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,9 @@ use crate::types::features::{
122122
};
123123
use crate::types::payment::{PaymentHash, PaymentPreimage, PaymentSecret};
124124
use crate::types::string::UntrustedString;
125-
use crate::util::config::{ChannelConfig, ChannelConfigOverrides, ChannelConfigUpdate, UserConfig};
125+
use crate::util::config::{
126+
ChannelConfig, ChannelConfigOverrides, ChannelConfigUpdate, HTLCInterceptionFlags, UserConfig,
127+
};
126128
use crate::util::errors::APIError;
127129
use crate::util::logger::{Level, Logger, WithContext};
128130
use crate::util::scid_utils::fake_scid;
@@ -4768,27 +4770,53 @@ where
47684770
}
47694771
}
47704772

4771-
fn forward_needs_intercept(
4772-
&self, outbound_chan: Option<&FundedChannel<SP>>, outgoing_scid: u64,
4773-
) -> bool {
4774-
if outbound_chan.is_none() {
4775-
if fake_scid::is_valid_intercept(
4776-
&self.fake_scid_rand_bytes,
4777-
outgoing_scid,
4778-
&self.chain_hash,
4779-
) {
4780-
if self.config.read().unwrap().accept_intercept_htlcs {
4773+
fn forward_needs_intercept_to_known_chan(&self, outbound_chan: &FundedChannel<SP>) -> bool {
4774+
let intercept_flags = self.config.read().unwrap().htlc_interception_flags;
4775+
if !outbound_chan.context.should_announce() {
4776+
if outbound_chan.context.is_connected() {
4777+
if intercept_flags & (HTLCInterceptionFlags::ToOnlinePrivateChannels as u8) != 0 {
4778+
return true;
4779+
}
4780+
} else {
4781+
if intercept_flags & (HTLCInterceptionFlags::ToOfflinePrivateChannels as u8) != 0 {
47814782
return true;
47824783
}
47834784
}
4785+
} else {
4786+
if intercept_flags & (HTLCInterceptionFlags::ToPublicChannels as u8) != 0 {
4787+
return true;
4788+
}
4789+
}
4790+
false
4791+
}
4792+
4793+
fn forward_needs_intercept_to_unknown_chan(&self, outgoing_scid: u64) -> bool {
4794+
let intercept_flags = self.config.read().unwrap().htlc_interception_flags;
4795+
if fake_scid::is_valid_intercept(
4796+
&self.fake_scid_rand_bytes,
4797+
outgoing_scid,
4798+
&self.chain_hash,
4799+
) {
4800+
if intercept_flags & (HTLCInterceptionFlags::ToInterceptSCIDs as u8) != 0 {
4801+
return true;
4802+
}
4803+
} else if fake_scid::is_valid_phantom(
4804+
&self.fake_scid_rand_bytes,
4805+
outgoing_scid,
4806+
&self.chain_hash,
4807+
) {
4808+
// Handled as a normal forward
4809+
} else if intercept_flags & (HTLCInterceptionFlags::ToUnknownSCIDs as u8) != 0 {
4810+
return true;
47844811
}
47854812
false
47864813
}
47874814

47884815
#[rustfmt::skip]
47894816
fn can_forward_htlc_to_outgoing_channel(
4790-
&self, chan: &mut FundedChannel<SP>, msg: &msgs::UpdateAddHTLC, next_packet: &NextPacketDetails
4791-
) -> Result<bool, LocalHTLCFailureReason> {
4817+
&self, chan: &mut FundedChannel<SP>, msg: &msgs::UpdateAddHTLC,
4818+
next_packet: &NextPacketDetails, will_intercept: bool,
4819+
) -> Result<(), LocalHTLCFailureReason> {
47924820
if !chan.context.should_announce()
47934821
&& !self.config.read().unwrap().accept_forwards_to_priv_channels
47944822
{
@@ -4797,15 +4825,13 @@ where
47974825
// we don't allow forwards outbound over them.
47984826
return Err(LocalHTLCFailureReason::PrivateChannelForward);
47994827
}
4800-
let intercepted;
48014828
if let HopConnector::ShortChannelId(outgoing_scid) = next_packet.outgoing_connector {
48024829
if chan.funding.get_channel_type().supports_scid_privacy() && outgoing_scid != chan.context.outbound_scid_alias() {
48034830
// `option_scid_alias` (referred to in LDK as `scid_privacy`) means
48044831
// "refuse to forward unless the SCID alias was used", so we pretend
48054832
// we don't have the channel here.
48064833
return Err(LocalHTLCFailureReason::RealSCIDForward);
48074834
}
4808-
intercepted = self.forward_needs_intercept(Some(chan), outgoing_scid);
48094835
} else {
48104836
return Err(LocalHTLCFailureReason::InvalidTrampolineForward);
48114837
}
@@ -4815,7 +4841,7 @@ where
48154841
// around to doing the actual forward, but better to fail early if we can and
48164842
// hopefully an attacker trying to path-trace payments cannot make this occur
48174843
// on a small/per-node/per-channel scale.
4818-
if !intercepted && !chan.context.is_live() {
4844+
if !will_intercept && !chan.context.is_live() {
48194845
if !chan.context.is_enabled() {
48204846
return Err(LocalHTLCFailureReason::ChannelDisabled);
48214847
} else if !chan.context.is_connected() {
@@ -4827,9 +4853,7 @@ where
48274853
if next_packet.outgoing_amt_msat < chan.context.get_counterparty_htlc_minimum_msat() {
48284854
return Err(LocalHTLCFailureReason::AmountBelowMinimum);
48294855
}
4830-
chan.htlc_satisfies_config(msg, next_packet.outgoing_amt_msat, next_packet.outgoing_cltv_value)?;
4831-
4832-
Ok(intercepted)
4856+
chan.htlc_satisfies_config(msg, next_packet.outgoing_amt_msat, next_packet.outgoing_cltv_value)
48334857
}
48344858

48354859
/// Executes a callback `C` that returns some value `X` on the channel found with the given
@@ -4855,10 +4879,10 @@ where
48554879
}
48564880
}
48574881

4858-
fn can_forward_htlc_intercepted(
4859-
&self, msg: &msgs::UpdateAddHTLC, next_packet_details: &NextPacketDetails,
4882+
fn can_forward_htlc_should_intercept(
4883+
&self, msg: &msgs::UpdateAddHTLC, next_hop: &NextPacketDetails,
48604884
) -> Result<bool, LocalHTLCFailureReason> {
4861-
let outgoing_scid = match next_packet_details.outgoing_connector {
4885+
let outgoing_scid = match next_hop.outgoing_connector {
48624886
HopConnector::ShortChannelId(scid) => scid,
48634887
HopConnector::Dummy => {
48644888
// Dummy hops are only used for path padding and must not reach HTLC processing.
@@ -4870,22 +4894,23 @@ where
48704894
},
48714895
};
48724896
// TODO: We do the fake SCID namespace check a bunch of times here (and indirectly via
4873-
// `forward_needs_intercept`, including as called in
4897+
// `forward_needs_intercept_*`, including as called in
48744898
// `can_forward_htlc_to_outgoing_channel`), we should find a way to reduce the number of
48754899
// times we do it.
48764900
let intercept =
48774901
match self.do_funded_channel_callback(outgoing_scid, |chan: &mut FundedChannel<SP>| {
4878-
self.can_forward_htlc_to_outgoing_channel(chan, msg, next_packet_details)
4902+
let intercept = self.forward_needs_intercept_to_known_chan(chan);
4903+
self.can_forward_htlc_to_outgoing_channel(chan, msg, next_hop, intercept)?;
4904+
Ok(intercept)
48794905
}) {
48804906
Some(Ok(intercept)) => intercept,
48814907
Some(Err(e)) => return Err(e),
48824908
None => {
48834909
// Perform basic sanity checks on the amounts and CLTV being forwarded
4884-
if next_packet_details.outgoing_amt_msat > msg.amount_msat {
4910+
if next_hop.outgoing_amt_msat > msg.amount_msat {
48854911
return Err(LocalHTLCFailureReason::FeeInsufficient);
48864912
}
4887-
let cltv_delta =
4888-
msg.cltv_expiry.saturating_sub(next_packet_details.outgoing_cltv_value);
4913+
let cltv_delta = msg.cltv_expiry.saturating_sub(next_hop.outgoing_cltv_value);
48894914
if cltv_delta < MIN_CLTV_EXPIRY_DELTA.into() {
48904915
return Err(LocalHTLCFailureReason::IncorrectCLTVExpiry);
48914916
}
@@ -4896,7 +4921,7 @@ where
48964921
&self.chain_hash,
48974922
) {
48984923
false
4899-
} else if self.forward_needs_intercept(None, outgoing_scid) {
4924+
} else if self.forward_needs_intercept_to_unknown_chan(outgoing_scid) {
49004925
true
49014926
} else {
49024927
return Err(LocalHTLCFailureReason::UnknownNextPeer);
@@ -4905,11 +4930,7 @@ where
49054930
};
49064931

49074932
let cur_height = self.best_block.read().unwrap().height + 1;
4908-
check_incoming_htlc_cltv(
4909-
cur_height,
4910-
next_packet_details.outgoing_cltv_value,
4911-
msg.cltv_expiry,
4912-
)?;
4933+
check_incoming_htlc_cltv(cur_height, next_hop.outgoing_cltv_value, msg.cltv_expiry)?;
49134934

49144935
Ok(intercept)
49154936
}
@@ -6641,11 +6662,8 @@ where
66416662
/// Intercepted HTLCs can be useful for Lightning Service Providers (LSPs) to open a just-in-time
66426663
/// channel to a receiving node if the node lacks sufficient inbound liquidity.
66436664
///
6644-
/// To make use of intercepted HTLCs, set [`UserConfig::accept_intercept_htlcs`] and use
6645-
/// [`ChannelManager::get_intercept_scid`] to generate short channel id(s) to put in the
6646-
/// receiver's invoice route hints. These route hints will signal to LDK to generate an
6647-
/// [`HTLCIntercepted`] event when it receives the forwarded HTLC, and this method or
6648-
/// [`ChannelManager::fail_intercepted_htlc`] MUST be called in response to the event.
6665+
/// To make use of intercepted HTLCs, set [`UserConfig::htlc_interception_flags`] must have a
6666+
/// non-0 value.
66496667
///
66506668
/// Note that LDK does not enforce fee requirements in `amt_to_forward_msat`, and will not stop
66516669
/// you from forwarding more than you received. See
@@ -6655,7 +6673,7 @@ where
66556673
/// Errors if the event was not handled in time, in which case the HTLC was automatically failed
66566674
/// backwards.
66576675
///
6658-
/// [`UserConfig::accept_intercept_htlcs`]: crate::util::config::UserConfig::accept_intercept_htlcs
6676+
/// [`UserConfig::htlc_interception_flags`]: crate::util::config::UserConfig::htlc_interception_flags
66596677
/// [`HTLCIntercepted`]: events::Event::HTLCIntercepted
66606678
/// [`HTLCIntercepted::expected_outbound_amount_msat`]: events::Event::HTLCIntercepted::expected_outbound_amount_msat
66616679
// TODO: when we move to deciding the best outbound channel at forward time, only take
@@ -6973,7 +6991,9 @@ where
69736991
// Now process the HTLC on the outgoing channel if it's a forward.
69746992
let mut intercept_forward = false;
69756993
if let Some(next_packet_details) = next_packet_details_opt.as_ref() {
6976-
match self.can_forward_htlc_intercepted(&update_add_htlc, next_packet_details) {
6994+
match self
6995+
.can_forward_htlc_should_intercept(&update_add_htlc, next_packet_details)
6996+
{
69776997
Err(reason) => {
69786998
fail_htlc_continue_to_next!(reason);
69796999
},
@@ -16329,9 +16349,9 @@ where
1632916349

1633016350
let should_intercept = self
1633116351
.do_funded_channel_callback(next_hop_scid, |chan| {
16332-
self.forward_needs_intercept(Some(chan), next_hop_scid)
16352+
self.forward_needs_intercept_to_known_chan(chan)
1633316353
})
16334-
.unwrap_or_else(|| self.forward_needs_intercept(None, next_hop_scid));
16354+
.unwrap_or_else(|| self.forward_needs_intercept_to_unknown_chan(next_hop_scid));
1633516355

1633616356
if should_intercept {
1633716357
let intercept_id = InterceptId::from_htlc_id_and_chan_id(

0 commit comments

Comments
 (0)