Ok(())
}
+ /// Attempts to forward an intercepted HTLC over the provided channel id and with the provided
+ /// amount to forward. Should only be called in response to an [`HTLCIntercepted`] event.
+ ///
+ /// 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, 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.
+ ///
+ /// Note that LDK does not enforce fee requirements in `amt_to_forward_msat`, and will not stop
+ /// you from forwarding more than you received.
+ ///
+ /// [`HTLCIntercepted`]: events::Event::HTLCIntercepted
+ // TODO: when we move to deciding the best outbound channel at forward time, only take
+ // `next_node_id` and not `next_hop_channel_id`
+ pub fn forward_intercepted_htlc(&self, intercept_id: InterceptId, next_hop_channel_id: &[u8; 32], _next_node_id: PublicKey, amt_to_forward_msat: u64) -> Result<(), APIError> {
+ let _persistence_guard = PersistenceNotifierGuard::notify_on_drop(&self.total_consistency_lock, &self.persistence_notifier);
+
+ let next_hop_scid = match self.channel_state.lock().unwrap().by_id.get(next_hop_channel_id) {
+ Some(chan) => chan.get_short_channel_id().unwrap_or(chan.outbound_scid_alias()),
+ None => return Err(APIError::APIMisuseError {
+ err: format!("Channel with id {:?} not found", next_hop_channel_id)
+ })
+ };
+
+ let payment = self.pending_intercepted_htlcs.lock().unwrap().remove(&intercept_id)
+ .ok_or_else(|| APIError::APIMisuseError {
+ err: format!("Payment with intercept id {:?} not found", intercept_id.0)
+ })?;
+
+ let routing = match payment.forward_info.routing {
+ PendingHTLCRouting::Forward { onion_packet, .. } => {
+ PendingHTLCRouting::Forward { onion_packet, short_channel_id: next_hop_scid }
+ },
+ _ => unreachable!() // Only `PendingHTLCRouting::Forward`s are intercepted
+ };
+ let pending_htlc_info = PendingHTLCInfo {
+ outgoing_amt_msat: amt_to_forward_msat, routing, ..payment.forward_info
+ };
+
+ let mut per_source_pending_forward = [(
+ payment.prev_short_channel_id,
+ payment.prev_funding_outpoint,
+ payment.prev_user_channel_id,
+ vec![(pending_htlc_info, payment.prev_htlc_id)]
+ )];
+ self.forward_htlcs(&mut per_source_pending_forward);
+ Ok(())
+ }
+
/// Processes HTLCs which are pending waiting on random forward delay.
///
/// Should only really ever be called in response to a PendingHTLCsForwardable event.
}
}
+ /// Gets a fake short channel id for use in receiving intercepted payments. These fake scids are
+ /// used when constructing the route hints for HTLCs intended to be intercepted. See
+ /// [`ChannelManager::forward_intercepted_htlc`].
+ ///
+ /// Note that this method is not guaranteed to return unique values, you may need to call it a few
+ /// times to get a unique scid.
+ pub fn get_intercept_scid(&self) -> u64 {
+ let best_block_height = self.best_block.read().unwrap().height();
+ let short_to_chan_info = self.short_to_chan_info.read().unwrap();
+ loop {
+ let scid_candidate = fake_scid::Namespace::Intercept.get_fake_scid(best_block_height, &self.genesis_hash, &self.fake_scid_rand_bytes, &self.keys_manager);
+ // Ensure the generated scid doesn't conflict with a real channel.
+ if short_to_chan_info.contains_key(&scid_candidate) { continue }
+ return scid_candidate
+ }
+ }
+
/// Gets inflight HTLC information by processing pending outbound payments that are in
/// our channels. May be used during pathfinding to account for in-use channel liquidity.
pub fn compute_inflight_htlcs(&self) -> InFlightHtlcs {
use crate::chain::transaction::OutPoint;
use crate::chain::keysinterface::KeysInterface;
use crate::ln::channel::EXPIRE_PREV_CONFIG_TICKS;
-use crate::ln::channelmanager::{self, BREAKDOWN_TIMEOUT, ChannelManager, MPP_TIMEOUT_TICKS, MIN_CLTV_EXPIRY_DELTA, PaymentId, PaymentSendFailure, IDEMPOTENCY_TIMEOUT_TICKS};
+use crate::ln::channelmanager::{self, BREAKDOWN_TIMEOUT, ChannelManager, InterceptId, MPP_TIMEOUT_TICKS, MIN_CLTV_EXPIRY_DELTA, PaymentId, PaymentSendFailure, IDEMPOTENCY_TIMEOUT_TICKS};
use crate::ln::msgs;
use crate::ln::msgs::ChannelMessageHandler;
-use crate::routing::router::{PaymentParameters, get_route};
+use crate::routing::gossip::RoutingFees;
+use crate::routing::router::{find_route, get_route, PaymentParameters, RouteHint, RouteHintHop, RouteParameters};
use crate::util::events::{ClosureReason, Event, HTLCDestination, MessageSendEvent, MessageSendEventsProvider};
use crate::util::test_utils;
use crate::util::errors::APIError;
// Clear pending events so test doesn't throw a "Had excess message on node..." error
nodes[0].node.get_and_clear_pending_msg_events();
}
+
+#[test]
+fn forward_intercepted_payment() {
+ // Test that detecting an intercept scid on payment forward will signal LDK to generate an
+ // intercept event, which the LSP can then use to open a JIT channel to forward the payment.
+ let chanmon_cfgs = create_chanmon_cfgs(3);
+ let node_cfgs = create_node_cfgs(3, &chanmon_cfgs);
+ let mut chan_config = test_default_channel_config();
+ chan_config.manually_accept_inbound_channels = true;
+ let node_chanmgrs = create_node_chanmgrs(3, &node_cfgs, &[None, None, Some(chan_config)]);
+ let nodes = create_network(3, &node_cfgs, &node_chanmgrs);
+ let scorer = test_utils::TestScorer::with_penalty(0);
+ let random_seed_bytes = chanmon_cfgs[0].keys_manager.get_secure_random_bytes();
+
+ let _ = create_announced_chan_between_nodes(&nodes, 0, 1, channelmanager::provided_init_features(), channelmanager::provided_init_features()).2;
+
+ let amt_msat = 100_000;
+ let intercept_scid = nodes[1].node.get_intercept_scid();
+ let payment_params = PaymentParameters::from_node_id(nodes[2].node.get_our_node_id())
+ .with_route_hints(vec![
+ RouteHint(vec![RouteHintHop {
+ src_node_id: nodes[1].node.get_our_node_id(),
+ short_channel_id: intercept_scid,
+ fees: RoutingFees {
+ base_msat: 1000,
+ proportional_millionths: 0,
+ },
+ cltv_expiry_delta: MIN_CLTV_EXPIRY_DELTA,
+ htlc_minimum_msat: None,
+ htlc_maximum_msat: None,
+ }])
+ ])
+ .with_features(channelmanager::provided_invoice_features());
+ let route_params = RouteParameters {
+ payment_params,
+ final_value_msat: amt_msat,
+ final_cltv_expiry_delta: TEST_FINAL_CLTV,
+ };
+ let route = find_route(
+ &nodes[0].node.get_our_node_id(), &route_params, &nodes[0].network_graph, None, nodes[0].logger,
+ &scorer, &random_seed_bytes
+ ).unwrap();
+
+ let (payment_hash, payment_secret) = nodes[2].node.create_inbound_payment(Some(amt_msat), 60 * 60).unwrap();
+ nodes[0].node.send_payment(&route, payment_hash, &Some(payment_secret), PaymentId(payment_hash.0)).unwrap();
+ let payment_event = {
+ {
+ let mut added_monitors = nodes[0].chain_monitor.added_monitors.lock().unwrap();
+ assert_eq!(added_monitors.len(), 1);
+ added_monitors.clear();
+ }
+ let mut events = nodes[0].node.get_and_clear_pending_msg_events();
+ assert_eq!(events.len(), 1);
+ SendEvent::from_event(events.remove(0))
+ };
+ nodes[1].node.handle_update_add_htlc(&nodes[0].node.get_our_node_id(), &payment_event.msgs[0]);
+ commitment_signed_dance!(nodes[1], nodes[0], &payment_event.commitment_msg, false, true);
+
+ // Check that we generate the PaymentIntercepted event when an intercept forward is detected.
+ let events = nodes[1].node.get_and_clear_pending_events();
+ assert_eq!(events.len(), 1);
+ let (intercept_id, expected_outbound_amount_msat) = match events[0] {
+ crate::util::events::Event::HTLCIntercepted {
+ intercept_id, expected_outbound_amount_msat, payment_hash: pmt_hash, inbound_amount_msat, requested_next_hop_scid: short_channel_id
+ } => {
+ assert_eq!(pmt_hash, payment_hash);
+ assert_eq!(inbound_amount_msat, route.get_total_amount() + route.get_total_fees());
+ assert_eq!(short_channel_id, intercept_scid);
+ (intercept_id, expected_outbound_amount_msat)
+ },
+ _ => panic!()
+ };
+
+ // Check for unknown channel id error.
+ let unknown_chan_id_err = nodes[1].node.forward_intercepted_htlc(intercept_id, &[42; 32], nodes[2].node.get_our_node_id(), expected_outbound_amount_msat).unwrap_err();
+ assert_eq!(unknown_chan_id_err , APIError::APIMisuseError { err: format!("Channel with id {:?} not found", [42; 32]) });
+
+ // Open the just-in-time channel so the payment can then be forwarded.
+ let (_, channel_id) = open_zero_conf_channel(&nodes[1], &nodes[2], None);
+
+ // Check for unknown intercept id error.
+ let unknown_intercept_id = InterceptId([42; 32]);
+ let unknown_intercept_id_err = nodes[1].node.forward_intercepted_htlc(unknown_intercept_id, &channel_id, nodes[2].node.get_our_node_id(), expected_outbound_amount_msat).unwrap_err();
+ assert_eq!(unknown_intercept_id_err , APIError::APIMisuseError { err: format!("Payment with intercept id {:?} not found", unknown_intercept_id.0) });
+
+ // Finally, forward the intercepted payment through and claim it.
+ nodes[1].node.forward_intercepted_htlc(intercept_id, &channel_id, nodes[2].node.get_our_node_id(), expected_outbound_amount_msat).unwrap();
+ expect_pending_htlcs_forwardable!(nodes[1]);
+
+ let payment_event = {
+ {
+ let mut added_monitors = nodes[1].chain_monitor.added_monitors.lock().unwrap();
+ assert_eq!(added_monitors.len(), 1);
+ added_monitors.clear();
+ }
+ let mut events = nodes[1].node.get_and_clear_pending_msg_events();
+ assert_eq!(events.len(), 1);
+ SendEvent::from_event(events.remove(0))
+ };
+ nodes[2].node.handle_update_add_htlc(&nodes[1].node.get_our_node_id(), &payment_event.msgs[0]);
+ commitment_signed_dance!(nodes[2], nodes[1], &payment_event.commitment_msg, false, true);
+ expect_pending_htlcs_forwardable!(nodes[2]);
+
+ let payment_preimage = nodes[2].node.get_payment_preimage(payment_hash, payment_secret).unwrap();
+ expect_payment_received!(&nodes[2], payment_hash, payment_secret, amt_msat, Some(payment_preimage), nodes[2].node.get_our_node_id());
+ do_claim_payment_along_route(&nodes[0], &vec!(&vec!(&nodes[1], &nodes[2])[..]), false, payment_preimage);
+ let events = nodes[0].node.get_and_clear_pending_events();
+ assert_eq!(events.len(), 2);
+ match events[0] {
+ Event::PaymentSent { payment_preimage: ref ev_preimage, payment_hash: ref ev_hash, ref fee_paid_msat, .. } => {
+ assert_eq!(payment_preimage, *ev_preimage);
+ assert_eq!(payment_hash, *ev_hash);
+ assert_eq!(fee_paid_msat, &Some(1000));
+ },
+ _ => panic!("Unexpected event")
+ }
+ match events[1] {
+ Event::PaymentPathSuccessful { payment_hash: hash, .. } => {
+ assert_eq!(hash, Some(payment_hash));
+ },
+ _ => panic!("Unexpected event")
+ }
+}