diff --git a/lightning-dns-resolver/src/lib.rs b/lightning-dns-resolver/src/lib.rs index 471c7562702..ac8028649ef 100644 --- a/lightning-dns-resolver/src/lib.rs +++ b/lightning-dns-resolver/src/lib.rs @@ -175,6 +175,7 @@ mod test { use lightning::onion_message::messenger::{ AOnionMessenger, Destination, MessageRouter, OnionMessagePath, OnionMessenger, }; + use lightning::routing::router::DEFAULT_PAYMENT_DUMMY_HOPS; use lightning::sign::{KeysManager, NodeSigner, ReceiveAuthKey, Recipient}; use lightning::types::features::InitFeatures; use lightning::types::payment::PaymentHash; @@ -419,6 +420,11 @@ mod test { let updates = get_htlc_update_msgs(&nodes[0], &payee_id); nodes[1].node.handle_update_add_htlc(payer_id, &updates.update_add_htlcs[0]); do_commitment_signed_dance(&nodes[1], &nodes[0], &updates.commitment_signed, false, false); + + for _ in 0..DEFAULT_PAYMENT_DUMMY_HOPS { + nodes[1].node.process_pending_htlc_forwards(); + } + expect_and_process_pending_htlcs(&nodes[1], false); let claimable_events = nodes[1].node.get_and_clear_pending_events(); diff --git a/lightning/src/blinded_path/payment.rs b/lightning/src/blinded_path/payment.rs index 13ade222f5b..c5673c64283 100644 --- a/lightning/src/blinded_path/payment.rs +++ b/lightning/src/blinded_path/payment.rs @@ -12,6 +12,7 @@ use bitcoin::secp256k1::ecdh::SharedSecret; use bitcoin::secp256k1::{self, PublicKey, Secp256k1, SecretKey}; +use crate::blinded_path::message::MAX_DUMMY_HOPS_COUNT; use crate::blinded_path::utils::{self, BlindedPathWithPadding}; use crate::blinded_path::{BlindedHop, BlindedPath, IntroductionNode, NodeIdLookUp}; use crate::crypto::streams::ChaChaDualPolyReadAdapter; @@ -121,6 +122,32 @@ impl BlindedPaymentPath { local_node_receive_key: ReceiveAuthKey, payee_tlvs: ReceiveTlvs, htlc_maximum_msat: u64, min_final_cltv_expiry_delta: u16, entropy_source: ES, secp_ctx: &Secp256k1, ) -> Result + where + ES::Target: EntropySource, + { + BlindedPaymentPath::new_with_dummy_hops( + intermediate_nodes, + payee_node_id, + 0, + local_node_receive_key, + payee_tlvs, + htlc_maximum_msat, + min_final_cltv_expiry_delta, + entropy_source, + secp_ctx, + ) + } + + /// Same as [`BlindedPaymentPath::new`], but allows specifying a number of dummy hops. + /// + /// Note: + /// At most [`MAX_DUMMY_HOPS_COUNT`] dummy hops can be added to the blinded path. + pub fn new_with_dummy_hops( + intermediate_nodes: &[PaymentForwardNode], payee_node_id: PublicKey, + dummy_hop_count: usize, local_node_receive_key: ReceiveAuthKey, payee_tlvs: ReceiveTlvs, + htlc_maximum_msat: u64, min_final_cltv_expiry_delta: u16, entropy_source: ES, + secp_ctx: &Secp256k1, + ) -> Result where ES::Target: EntropySource, { @@ -145,6 +172,7 @@ impl BlindedPaymentPath { secp_ctx, intermediate_nodes, payee_node_id, + dummy_hop_count, payee_tlvs, &blinding_secret, local_node_receive_key, @@ -211,6 +239,19 @@ impl BlindedPaymentPath { self.inner_path.blinded_hops.remove(0); Ok(()) }, + Ok((BlindedPaymentTlvs::Dummy, control_tlvs_ss)) => { + let next_node_id = node_signer.get_node_id(Recipient::Node)?; + let mut new_blinding_point = onion_utils::next_hop_pubkey( + secp_ctx, + self.inner_path.blinding_point, + control_tlvs_ss.as_ref(), + ) + .map_err(|_| ())?; + mem::swap(&mut self.inner_path.blinding_point, &mut new_blinding_point); + self.inner_path.introduction_node = IntroductionNode::NodeId(next_node_id); + self.inner_path.blinded_hops.remove(0); + Ok(()) + }, _ => Err(()), } } @@ -234,9 +275,9 @@ impl BlindedPaymentPath { .map_err(|_| ())?; match (&readable, used_aad) { - (BlindedPaymentTlvs::Forward(_), false) | (BlindedPaymentTlvs::Receive(_), true) => { - Ok((readable, control_tlvs_ss)) - }, + (BlindedPaymentTlvs::Forward(_), false) + | (BlindedPaymentTlvs::Dummy, true) + | (BlindedPaymentTlvs::Receive(_), true) => Ok((readable, control_tlvs_ss)), _ => Err(()), } } @@ -346,6 +387,8 @@ pub struct ReceiveTlvs { pub(crate) enum BlindedPaymentTlvs { /// This blinded payment data is for a forwarding node. Forward(ForwardTlvs), + /// This blinded payment data is dummy and is to be peeled by receiving node. + Dummy, /// This blinded payment data is for the receiving node. Receive(ReceiveTlvs), } @@ -363,6 +406,7 @@ pub(crate) enum BlindedTrampolineTlvs { // Used to include forward and receive TLVs in the same iterator for encoding. enum BlindedPaymentTlvsRef<'a> { Forward(&'a ForwardTlvs), + Dummy, Receive(&'a ReceiveTlvs), } @@ -532,6 +576,11 @@ impl<'a> Writeable for BlindedPaymentTlvsRef<'a> { fn write(&self, w: &mut W) -> Result<(), io::Error> { match self { Self::Forward(tlvs) => tlvs.write(w)?, + Self::Dummy => { + encode_tlv_stream!(w, { + (65539, (), required), + }) + }, Self::Receive(tlvs) => tlvs.write(w)?, } Ok(()) @@ -548,32 +597,48 @@ impl Readable for BlindedPaymentTlvs { (2, scid, option), (8, next_blinding_override, option), (10, payment_relay, option), - (12, payment_constraints, required), + (12, payment_constraints, option), (14, features, (option, encoding: (BlindedHopFeatures, WithoutLength))), (65536, payment_secret, option), (65537, payment_context, option), + (65539, is_dummy, option) }); - if let Some(short_channel_id) = scid { - if payment_secret.is_some() { - return Err(DecodeError::InvalidValue); - } - Ok(BlindedPaymentTlvs::Forward(ForwardTlvs { + match ( + scid, + next_blinding_override, + payment_relay, + payment_constraints, + features, + payment_secret, + payment_context, + is_dummy, + ) { + ( + Some(short_channel_id), + next_override, + Some(relay), + Some(constraints), + features, + None, + None, + None, + ) => Ok(BlindedPaymentTlvs::Forward(ForwardTlvs { short_channel_id, - payment_relay: payment_relay.ok_or(DecodeError::InvalidValue)?, - payment_constraints: payment_constraints.0.unwrap(), - next_blinding_override, + payment_relay: relay, + payment_constraints: constraints, + next_blinding_override: next_override, features: features.unwrap_or_else(BlindedHopFeatures::empty), - })) - } else { - if payment_relay.is_some() || features.is_some() { - return Err(DecodeError::InvalidValue); - } - Ok(BlindedPaymentTlvs::Receive(ReceiveTlvs { - payment_secret: payment_secret.ok_or(DecodeError::InvalidValue)?, - payment_constraints: payment_constraints.0.unwrap(), - payment_context: payment_context.ok_or(DecodeError::InvalidValue)?, - })) + })), + (None, None, None, Some(constraints), None, Some(secret), Some(context), None) => { + Ok(BlindedPaymentTlvs::Receive(ReceiveTlvs { + payment_secret: secret, + payment_constraints: constraints, + payment_context: context, + })) + }, + (None, None, None, None, None, None, None, Some(())) => Ok(BlindedPaymentTlvs::Dummy), + _ => return Err(DecodeError::InvalidValue), } } } @@ -620,22 +685,43 @@ pub(crate) const PAYMENT_PADDING_ROUND_OFF: usize = 30; /// Construct blinded payment hops for the given `intermediate_nodes` and payee info. pub(super) fn blinded_hops( secp_ctx: &Secp256k1, intermediate_nodes: &[PaymentForwardNode], payee_node_id: PublicKey, - payee_tlvs: ReceiveTlvs, session_priv: &SecretKey, local_node_receive_key: ReceiveAuthKey, + dummy_hop_count: usize, payee_tlvs: ReceiveTlvs, session_priv: &SecretKey, + local_node_receive_key: ReceiveAuthKey, ) -> Vec { + let dummy_count = core::cmp::min(dummy_hop_count, MAX_DUMMY_HOPS_COUNT); let pks = intermediate_nodes .iter() .map(|node| (node.node_id, None)) + .chain(core::iter::repeat((payee_node_id, Some(local_node_receive_key))).take(dummy_count)) .chain(core::iter::once((payee_node_id, Some(local_node_receive_key)))); let tlvs = intermediate_nodes .iter() .map(|node| BlindedPaymentTlvsRef::Forward(&node.tlvs)) + .chain((0..dummy_count).map(|_| BlindedPaymentTlvsRef::Dummy)) .chain(core::iter::once(BlindedPaymentTlvsRef::Receive(&payee_tlvs))); - let path = pks.zip( - tlvs.map(|tlv| BlindedPathWithPadding { tlvs: tlv, round_off: PAYMENT_PADDING_ROUND_OFF }), - ); + let path: Vec<_> = pks + .zip( + tlvs.map(|tlv| BlindedPathWithPadding { + tlvs: tlv, + round_off: PAYMENT_PADDING_ROUND_OFF, + }), + ) + .collect(); + + // Debug invariant: all non-final hops must have identical serialized size. + #[cfg(debug_assertions)] + if let Some((_, first)) = path.first() { + let expected = first.serialized_length(); + for (_, hop) in &path[..path.len() - 1] { + debug_assert!( + hop.serialized_length() == expected, + "All intermediate blinded hops must have identical serialized size" + ); + } + } - utils::construct_blinded_hops(secp_ctx, path, session_priv) + utils::construct_blinded_hops(secp_ctx, path.into_iter(), session_priv) } /// `None` if underflow occurs. diff --git a/lightning/src/ln/async_payments_tests.rs b/lightning/src/ln/async_payments_tests.rs index 8e7fbdf94fd..01abf21f6eb 100644 --- a/lightning/src/ln/async_payments_tests.rs +++ b/lightning/src/ln/async_payments_tests.rs @@ -55,7 +55,7 @@ use crate::onion_message::messenger::{ use crate::onion_message::offers::OffersMessage; use crate::onion_message::packet::ParsedOnionMessageContents; use crate::prelude::*; -use crate::routing::router::{Payee, PaymentParameters}; +use crate::routing::router::{Payee, PaymentParameters, DEFAULT_PAYMENT_DUMMY_HOPS}; use crate::sign::NodeSigner; use crate::sync::Mutex; use crate::types::features::Bolt12InvoiceFeatures; @@ -1858,6 +1858,13 @@ fn expired_static_invoice_payment_path() { blinded_path .advance_path_by_one(&nodes[1].keys_manager, &nodes[1].node, &secp_ctx) .unwrap(); + + for _ in 0..DEFAULT_PAYMENT_DUMMY_HOPS { + blinded_path + .advance_path_by_one(&nodes[2].keys_manager, &nodes[2].node, &secp_ctx) + .unwrap(); + } + match blinded_path.decrypt_intro_payload(&nodes[2].keys_manager).unwrap().0 { BlindedPaymentTlvs::Receive(tlvs) => tlvs.payment_constraints.max_cltv_expiry, _ => panic!(), diff --git a/lightning/src/ln/blinded_payment_tests.rs b/lightning/src/ln/blinded_payment_tests.rs index 5bf015e7c81..4514adbb53c 100644 --- a/lightning/src/ln/blinded_payment_tests.rs +++ b/lightning/src/ln/blinded_payment_tests.rs @@ -190,6 +190,56 @@ fn do_one_hop_blinded_path(success: bool) { } } +#[test] +fn one_hop_blinded_path_with_dummy_hops() { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + let chan_upd = create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0).0.contents; + + let amt_msat = 5000; + let (payment_preimage, payment_hash, payment_secret) = get_payment_preimage_hash(&nodes[1], Some(amt_msat), None); + let payee_tlvs = ReceiveTlvs { + payment_secret, + payment_constraints: PaymentConstraints { + max_cltv_expiry: u32::max_value(), + htlc_minimum_msat: chan_upd.htlc_minimum_msat, + }, + payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext {}), + }; + let receive_auth_key = chanmon_cfgs[1].keys_manager.get_receive_auth_key(); + let dummy_hops = 1; + + let mut secp_ctx = Secp256k1::new(); + let blinded_path = BlindedPaymentPath::new_with_dummy_hops( + &[], nodes[1].node.get_our_node_id(), dummy_hops, receive_auth_key, + payee_tlvs, u64::MAX, TEST_FINAL_CLTV as u16, + &chanmon_cfgs[1].keys_manager, &secp_ctx + ).unwrap(); + + let route_params = RouteParameters::from_payment_params_and_value( + PaymentParameters::blinded(vec![blinded_path]), + amt_msat, + ); + nodes[0].node.send_payment(payment_hash, RecipientOnionFields::spontaneous_empty(), + PaymentId(payment_hash.0), route_params, Retry::Attempts(0)).unwrap(); + check_added_monitors(&nodes[0], 1); + + let mut events = nodes[0].node.get_and_clear_pending_msg_events(); + assert_eq!(events.len(), 1); + let ev = remove_first_msg_event_to_node(&nodes[1].node.get_our_node_id(), &mut events); + + let path = &[&nodes[1]]; + let args = + PassAlongPathArgs::new(&nodes[0], path, amt_msat, payment_hash, ev) + .with_dummy_override(dummy_hops) + .with_payment_secret(payment_secret); + + do_pass_along_path(args); + claim_payment(&nodes[0], &[&nodes[1]], payment_preimage); +} + #[test] fn mpp_to_one_hop_blinded_path() { let chanmon_cfgs = create_chanmon_cfgs(4); @@ -1663,8 +1713,9 @@ fn route_blinding_spec_test_vector() { hop_data: carol_packet_bytes, hmac: carol_hmac, }; + let carol_forward_info = carol_packet_details.forward_info.unwrap(); let carol_update_add = update_add_msg( - carol_packet_details.outgoing_amt_msat, carol_packet_details.outgoing_cltv_value, + carol_forward_info.outgoing_amt_msat, carol_forward_info.outgoing_cltv_value, Some(pubkey_from_hex("034e09f450a80c3d252b258aba0a61215bf60dda3b0dc78ffb0736ea1259dfd8a0")), carol_onion ); @@ -1697,8 +1748,9 @@ fn route_blinding_spec_test_vector() { hop_data: dave_packet_bytes, hmac: dave_hmac, }; + let dave_forward_info = dave_packet_details.forward_info.unwrap(); let dave_update_add = update_add_msg( - dave_packet_details.outgoing_amt_msat, dave_packet_details.outgoing_cltv_value, + dave_forward_info.outgoing_amt_msat, dave_forward_info.outgoing_cltv_value, Some(pubkey_from_hex("031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f")), dave_onion ); @@ -1731,8 +1783,9 @@ fn route_blinding_spec_test_vector() { hop_data: eve_packet_bytes, hmac: eve_hmac, }; + let eve_forward_info = eve_packet_details.forward_info.unwrap(); let eve_update_add = update_add_msg( - eve_packet_details.outgoing_amt_msat, eve_packet_details.outgoing_cltv_value, + eve_forward_info.outgoing_amt_msat, eve_forward_info.outgoing_cltv_value, Some(pubkey_from_hex("03e09038ee76e50f444b19abf0a555e8697e035f62937168b80adf0931b31ce52a")), eve_onion ); @@ -1963,7 +2016,8 @@ fn test_trampoline_inbound_payment_decoding() { hop_data: carol_packet_bytes, hmac: carol_hmac, }; - let carol_update_add = update_add_msg(carol_packet_details.outgoing_amt_msat, carol_packet_details.outgoing_cltv_value, None, carol_onion); + let carol_forward_info = carol_packet_details.forward_info.unwrap(); + let carol_update_add = update_add_msg(carol_forward_info.outgoing_amt_msat, carol_forward_info.outgoing_cltv_value, None, carol_onion); let carol_node_signer = TestEcdhSigner { node_secret: carol_secret }; let (carol_peeled_onion, _) = onion_payment::decode_incoming_update_add_htlc_onion( diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index ea9b14211c5..bb97bd95445 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -72,9 +72,9 @@ use crate::ln::msgs::{ MessageSendEvent, }; use crate::ln::onion_payment::{ - check_incoming_htlc_cltv, create_fwd_pending_htlc_info, create_recv_pending_htlc_info, - decode_incoming_update_add_htlc_onion, invalid_payment_err_data, HopConnector, InboundHTLCErr, - NextPacketDetails, + check_incoming_htlc_cltv, create_fwd_pending_htlc_info, create_new_update_add_htlc, + create_recv_pending_htlc_info, decode_incoming_update_add_htlc_onion, invalid_payment_err_data, + ForwardInfo, HopConnector, InboundHTLCErr, NextPacketDetails, }; use crate::ln::onion_utils::{self}; use crate::ln::onion_utils::{ @@ -4888,7 +4888,7 @@ where #[rustfmt::skip] fn can_forward_htlc_to_outgoing_channel( - &self, chan: &mut FundedChannel, msg: &msgs::UpdateAddHTLC, next_packet: &NextPacketDetails + &self, chan: &mut FundedChannel, msg: &msgs::UpdateAddHTLC, forward_info: &ForwardInfo ) -> Result<(), LocalHTLCFailureReason> { if !chan.context.should_announce() && !self.config.read().unwrap().accept_forwards_to_priv_channels @@ -4898,7 +4898,7 @@ where // we don't allow forwards outbound over them. return Err(LocalHTLCFailureReason::PrivateChannelForward); } - if let HopConnector::ShortChannelId(outgoing_scid) = next_packet.outgoing_connector { + if let HopConnector::ShortChannelId(outgoing_scid) = forward_info.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 // "refuse to forward unless the SCID alias was used", so we pretend @@ -4921,10 +4921,10 @@ where return Err(LocalHTLCFailureReason::ChannelNotReady); } } - if next_packet.outgoing_amt_msat < chan.context.get_counterparty_htlc_minimum_msat() { + if forward_info.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)?; + chan.htlc_satisfies_config(msg, forward_info.outgoing_amt_msat, forward_info.outgoing_cltv_value)?; Ok(()) } @@ -4956,14 +4956,20 @@ where fn can_forward_htlc( &self, msg: &msgs::UpdateAddHTLC, next_packet_details: &NextPacketDetails ) -> Result<(), LocalHTLCFailureReason> { - let outgoing_scid = match next_packet_details.outgoing_connector { + let forward_info = next_packet_details + .forward_info + .as_ref() + .ok_or(LocalHTLCFailureReason::InvalidOnionPayload)?; + + let outgoing_scid = match forward_info.outgoing_connector { HopConnector::ShortChannelId(scid) => scid, HopConnector::Trampoline(_) => { return Err(LocalHTLCFailureReason::InvalidTrampolineForward); } }; + match self.do_funded_channel_callback(outgoing_scid, |chan: &mut FundedChannel| { - self.can_forward_htlc_to_outgoing_channel(chan, msg, next_packet_details) + self.can_forward_htlc_to_outgoing_channel(chan, msg, forward_info) }) { Some(Ok(())) => {}, Some(Err(e)) => return Err(e), @@ -4980,7 +4986,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, forward_info.outgoing_cltv_value, msg.cltv_expiry)?; Ok(()) } @@ -5089,6 +5095,14 @@ where onion_utils::Hop::Forward { .. } | onion_utils::Hop::BlindedForward { .. } => { create_fwd_pending_htlc_info(msg, decoded_hop, shared_secret, next_packet_pubkey_opt) }, + onion_utils::Hop::Dummy { .. } => { + debug_assert!(false, "Shouldn't be triggered."); + return Err(InboundHTLCErr { + msg: "Failed to decode update add htlc onion", + reason: LocalHTLCFailureReason::InvalidOnionPayload, + err_data: Vec::new(), + }) + }, onion_utils::Hop::TrampolineForward { .. } | onion_utils::Hop::TrampolineBlindedForward { .. } => { create_fwd_pending_htlc_info(msg, decoded_hop, shared_secret, next_packet_pubkey_opt) }, @@ -6837,6 +6851,7 @@ where pub(crate) fn process_pending_update_add_htlcs(&self) -> bool { let mut should_persist = false; let mut decode_update_add_htlcs = new_hash_map(); + let mut dummy_update_add_htlcs = new_hash_map(); mem::swap(&mut decode_update_add_htlcs, &mut self.decode_update_add_htlcs.lock().unwrap()); let get_htlc_failure_type = |outgoing_scid_opt: Option, payment_hash: PaymentHash| { @@ -6900,7 +6915,39 @@ where &*self.logger, &self.secp_ctx, ) { - Ok(decoded_onion) => decoded_onion, + Ok(decoded_onion) => match decoded_onion { + ( + onion_utils::Hop::Dummy { + intro_node_blinding_point, + next_hop_hmac, + new_packet_bytes, + .. + }, + Some(NextPacketDetails { next_packet_pubkey, forward_info }), + ) => { + debug_assert!( + forward_info.is_none(), + "Dummy hops must not contain any forward info, since they are not actually forwarded." + ); + let new_update_add_htlc = create_new_update_add_htlc( + update_add_htlc.clone(), + &*self.node_signer, + &self.secp_ctx, + intro_node_blinding_point, + next_packet_pubkey, + next_hop_hmac, + new_packet_bytes, + ); + + dummy_update_add_htlcs + .entry(incoming_scid_alias) + .or_insert_with(Vec::new) + .push(new_update_add_htlc); + + continue; + }, + _ => decoded_onion, + }, Err((htlc_fail, reason)) => { let failure_type = HTLCHandlingFailureType::InvalidOnion; @@ -6910,11 +6957,12 @@ where }; let is_intro_node_blinded_forward = next_hop.is_intro_node_blinded_forward(); - let outgoing_scid_opt = - next_packet_details_opt.as_ref().and_then(|d| match d.outgoing_connector { + let outgoing_scid_opt = next_packet_details_opt.as_ref().and_then(|d| { + d.forward_info.as_ref().and_then(|f| match f.outgoing_connector { HopConnector::ShortChannelId(scid) => Some(scid), HopConnector::Trampoline(_) => None, - }); + }) + }); let shared_secret = next_hop.shared_secret().secret_bytes(); // Nodes shouldn't expect us to hold HTLCs for them if we don't advertise htlc_hold feature @@ -7056,6 +7104,11 @@ where )); } } + + // Replace the decode queue with the peeled dummy HTLCs so they can be processed in the next iteration. + let mut decode_update_add_htlc_source = self.decode_update_add_htlcs.lock().unwrap(); + mem::swap(&mut *decode_update_add_htlc_source, &mut dummy_update_add_htlcs); + should_persist } diff --git a/lightning/src/ln/functional_test_utils.rs b/lightning/src/ln/functional_test_utils.rs index e31630a4926..383cafb211b 100644 --- a/lightning/src/ln/functional_test_utils.rs +++ b/lightning/src/ln/functional_test_utils.rs @@ -39,7 +39,9 @@ use crate::ln::peer_handler::IgnoringMessageHandler; use crate::ln::types::ChannelId; use crate::onion_message::messenger::OnionMessenger; use crate::routing::gossip::{NetworkGraph, NetworkUpdate, P2PGossipSync}; -use crate::routing::router::{self, PaymentParameters, Route, RouteParameters}; +use crate::routing::router::{ + self, PaymentParameters, Route, RouteParameters, DEFAULT_PAYMENT_DUMMY_HOPS, +}; use crate::sign::{EntropySource, RandomBytes}; use crate::types::features::ChannelTypeFeatures; use crate::types::features::InitFeatures; @@ -3435,6 +3437,7 @@ fn fail_payment_along_path<'a, 'b, 'c>(expected_path: &[&Node<'a, 'b, 'c>]) { pub struct PassAlongPathArgs<'a, 'b, 'c, 'd> { pub origin_node: &'a Node<'b, 'c, 'd>, pub expected_path: &'a [&'a Node<'b, 'c, 'd>], + pub dummy_hop_override: Option, pub recv_value: u64, pub payment_hash: PaymentHash, pub payment_secret: Option, @@ -3456,6 +3459,7 @@ impl<'a, 'b, 'c, 'd> PassAlongPathArgs<'a, 'b, 'c, 'd> { Self { origin_node, expected_path, + dummy_hop_override: None, recv_value, payment_hash, payment_secret: None, @@ -3503,12 +3507,17 @@ impl<'a, 'b, 'c, 'd> PassAlongPathArgs<'a, 'b, 'c, 'd> { self.expected_failure = Some(failure); self } + pub fn with_dummy_override(mut self, dummy_override: usize) -> Self { + self.dummy_hop_override = Some(dummy_override); + self + } } pub fn do_pass_along_path<'a, 'b, 'c>(args: PassAlongPathArgs) -> Option { let PassAlongPathArgs { origin_node, expected_path, + dummy_hop_override, recv_value, payment_hash: our_payment_hash, payment_secret: our_payment_secret, @@ -3543,6 +3552,15 @@ pub fn do_pass_along_path<'a, 'b, 'c>(args: PassAlongPathArgs) -> Option node.node.process_pending_htlc_forwards(); } + if is_last_hop { + // At the final hop, the incoming packet contains N dummy-hop layers + // before the real HTLC. Each call to `process_pending_htlc_forwards` + // strips exactly one dummy layer, so we call it N times. + for _ in 0..dummy_hop_override.unwrap_or(DEFAULT_PAYMENT_DUMMY_HOPS) { + node.node.process_pending_htlc_forwards(); + } + } + if is_last_hop && clear_recipient_events { let events_2 = node.node.get_and_clear_pending_events(); if payment_claimable_expected { @@ -3755,6 +3773,29 @@ pub struct ClaimAlongRouteArgs<'a, 'b, 'c, 'd> { pub origin_node: &'a Node<'b, 'c, 'd>, pub expected_paths: &'a [&'a [&'a Node<'b, 'c, 'd>]], pub expected_extra_fees: Vec, + /// A one-off adjustment used only in tests to account for an existing + /// fee-handling trade-off in LDK. + /// + /// When the payer is the introduction node of a blinded path, LDK does not + /// subtract the forward fee for the `payer -> next_hop` channel + /// (see [`BlindedPaymentPath::advance_path_by_one`]). This keeps the fee + /// logic simpler at the cost of a small, intentional overpayment. + /// + /// In the simple two-hop case (payer as introduction node → payee), + /// this overpayment has historically been avoided by simply not charging + /// the payer the forward fee, since the payer knows there is only + /// a single hop after them. + /// + /// However, with the introduction of dummy hops in LDK v3.0, even a + /// two-node real path (payer as introduction node → payee) may appear as a + /// multi-hop blinded path. This makes the existing overpayment surface in + /// tests. + /// + /// Until the fee-handling trade-off is revisited, this field allows tests + /// to compensate for that expected difference. + /// + /// [`BlindedPaymentPath::advance_path_by_one`]: crate::blinded_path::payment::BlindedPaymentPath::advance_path_by_one + pub expected_extra_total_fees_msat: u64, pub expected_min_htlc_overpay: Vec, pub skip_last: bool, pub payment_preimage: PaymentPreimage, @@ -3778,6 +3819,7 @@ impl<'a, 'b, 'c, 'd> ClaimAlongRouteArgs<'a, 'b, 'c, 'd> { origin_node, expected_paths, expected_extra_fees: vec![0; expected_paths.len()], + expected_extra_total_fees_msat: 0, expected_min_htlc_overpay: vec![0; expected_paths.len()], skip_last: false, payment_preimage, @@ -3793,6 +3835,10 @@ impl<'a, 'b, 'c, 'd> ClaimAlongRouteArgs<'a, 'b, 'c, 'd> { self.expected_extra_fees = extra_fees; self } + pub fn with_expected_extra_total_fees_msat(mut self, extra_total_fees: u64) -> Self { + self.expected_extra_total_fees_msat = extra_total_fees; + self + } pub fn with_expected_min_htlc_overpay(mut self, extra_fees: Vec) -> Self { self.expected_min_htlc_overpay = extra_fees; self @@ -3817,6 +3863,7 @@ pub fn pass_claimed_payment_along_route(args: ClaimAlongRouteArgs) -> u64 { payment_preimage: our_payment_preimage, allow_1_msat_fee_overpay, custom_tlvs, + .. } = args; let claim_event = expected_paths[0].last().unwrap().node.get_and_clear_pending_events(); assert_eq!(claim_event.len(), 1); @@ -4052,10 +4099,17 @@ pub fn pass_claimed_payment_along_route(args: ClaimAlongRouteArgs) -> u64 { pub fn claim_payment_along_route( args: ClaimAlongRouteArgs, ) -> (Option, Vec) { - let origin_node = args.origin_node; - let payment_preimage = args.payment_preimage; - let skip_last = args.skip_last; - let expected_total_fee_msat = do_claim_payment_along_route(args); + let ClaimAlongRouteArgs { + origin_node, + payment_preimage, + skip_last, + expected_extra_total_fees_msat, + .. + } = args; + + let expected_total_fee_msat = + do_claim_payment_along_route(args) + expected_extra_total_fees_msat; + if !skip_last { expect_payment_sent!(origin_node, payment_preimage, Some(expected_total_fee_msat)) } else { diff --git a/lightning/src/ln/msgs.rs b/lightning/src/ln/msgs.rs index 8e230fab1d9..ec8301a894e 100644 --- a/lightning/src/ln/msgs.rs +++ b/lightning/src/ln/msgs.rs @@ -2355,6 +2355,7 @@ mod fuzzy_internal_msgs { Receive(InboundOnionReceivePayload), BlindedForward(InboundOnionBlindedForwardPayload), BlindedReceive(InboundOnionBlindedReceivePayload), + Dummy { intro_node_blinding_point: Option }, } pub struct InboundTrampolineForwardPayload { @@ -3694,6 +3695,17 @@ where next_blinding_override, })) }, + ChaChaDualPolyReadAdapter { readable: BlindedPaymentTlvs::Dummy, used_aad } => { + if amt.is_some() + || cltv_value.is_some() || total_msat.is_some() + || keysend_preimage.is_some() + || invoice_request.is_some() + || !used_aad + { + return Err(DecodeError::InvalidValue); + } + Ok(Self::Dummy { intro_node_blinding_point }) + }, ChaChaDualPolyReadAdapter { readable: BlindedPaymentTlvs::Receive(receive_tlvs), used_aad, diff --git a/lightning/src/ln/offers_tests.rs b/lightning/src/ln/offers_tests.rs index 4c53aefe58d..4b0e08acbdd 100644 --- a/lightning/src/ln/offers_tests.rs +++ b/lightning/src/ln/offers_tests.rs @@ -1410,7 +1410,41 @@ fn creates_offer_with_blinded_path_using_unannounced_introduction_node() { route_bolt12_payment(bob, &[alice], &invoice); expect_recent_payment!(bob, RecentPaymentDetails::Pending, payment_id); - claim_bolt12_payment(bob, &[alice], payment_context, &invoice); + // Extract PaymentClaimable, verify context, and obtain the payment preimage. + let recipient = &alice; + let payment_purpose = match get_event!(recipient, Event::PaymentClaimable) { + Event::PaymentClaimable { purpose, .. } => purpose, + _ => panic!("No Event::PaymentClaimable"), + }; + + let payment_preimage = payment_purpose + .preimage() + .expect("No preimage in Event::PaymentClaimable"); + + match payment_purpose { + PaymentPurpose::Bolt12OfferPayment { payment_context: ctx, .. } => { + assert_eq!(PaymentContext::Bolt12Offer(ctx), payment_context); + }, + PaymentPurpose::Bolt12RefundPayment { payment_context: ctx, .. } => { + assert_eq!(PaymentContext::Bolt12Refund(ctx), payment_context); + }, + _ => panic!("Unexpected payment purpose: {:?}", payment_purpose), + }; + + // Build ClaimAlongRouteArgs and compensate for the expected overpayment + // caused by the payer being the introduction node of the blinded path with + // dummy hops. + let expected_paths: &[&[&Node]] = &[&[alice]]; + let args = ClaimAlongRouteArgs::new( + bob, + expected_paths, + payment_preimage, + ).with_expected_extra_total_fees_msat(1000); + + // Execute the claim and verify the invoice was fulfilled. + let (inv, _events) = claim_payment_along_route(args); + assert_eq!(inv, Some(PaidBolt12Invoice::Bolt12Invoice(invoice.clone()))); + expect_recent_payment!(bob, RecentPaymentDetails::Fulfilled, payment_id); } @@ -2425,9 +2459,9 @@ fn rejects_keysend_to_non_static_invoice_path() { .expect_failure(HTLCHandlingFailureType::Receive { payment_hash }); do_pass_along_path(args); let mut updates = get_htlc_update_msgs(&nodes[1], &nodes[0].node.get_our_node_id()); - nodes[0].node.handle_update_fail_htlc(nodes[1].node.get_our_node_id(), &updates.update_fail_htlcs[0]); + nodes[0].node.handle_update_fail_malformed_htlc(nodes[1].node.get_our_node_id(), &updates.update_fail_malformed_htlcs[0]); do_commitment_signed_dance(&nodes[0], &nodes[1], &updates.commitment_signed, false, false); - expect_payment_failed_conditions(&nodes[0], payment_hash, true, PaymentFailedConditions::new()); + expect_payment_failed_conditions(&nodes[0], payment_hash, false, PaymentFailedConditions::new()); } #[test] diff --git a/lightning/src/ln/onion_payment.rs b/lightning/src/ln/onion_payment.rs index f52a2d56e85..6ff43e3d86e 100644 --- a/lightning/src/ln/onion_payment.rs +++ b/lightning/src/ln/onion_payment.rs @@ -15,7 +15,7 @@ use crate::ln::channelmanager::{ BlindedFailure, BlindedForward, HTLCFailureMsg, PendingHTLCInfo, PendingHTLCRouting, CLTV_FAR_FAR_AWAY, MIN_CLTV_EXPIRY_DELTA, }; -use crate::ln::msgs; +use crate::ln::msgs::{self, OnionPacket, UpdateAddHTLC}; use crate::ln::onion_utils; use crate::ln::onion_utils::{HTLCFailReason, LocalHTLCFailureReason, ONION_DATA_LEN}; use crate::sign::{NodeSigner, Recipient}; @@ -123,6 +123,14 @@ pub(super) fn create_fwd_pending_htlc_info( (RoutingInfo::Direct { short_channel_id, new_packet_bytes, next_hop_hmac }, amt_to_forward, outgoing_cltv_value, intro_node_blinding_point, next_blinding_override) }, + onion_utils::Hop::Dummy { .. } => { + debug_assert!(false, "This case shall not be triggered"); + return Err(InboundHTLCErr { + msg: "Dummy Hop OnionHopData provided for us as an intermediary node", + reason: LocalHTLCFailureReason::InvalidOnionPayload, + err_data: Vec::new(), + }) + }, onion_utils::Hop::Receive { .. } | onion_utils::Hop::BlindedReceive { .. } => return Err(InboundHTLCErr { msg: "Final Node OnionHopData provided for us as an intermediary node", @@ -327,6 +335,14 @@ pub(super) fn create_recv_pending_htlc_info( msg: "Got blinded non final data with an HMAC of 0", }) }, + onion_utils::Hop::Dummy { .. } => { + debug_assert!(false, "This case shall not be triggered."); + return Err(InboundHTLCErr { + reason: LocalHTLCFailureReason::InvalidOnionBlinding, + err_data: vec![0; 32], + msg: "Got blinded non final data with an HMAC of 0", + }) + } onion_utils::Hop::TrampolineForward { .. } | onion_utils::Hop::TrampolineBlindedForward { .. } => { return Err(InboundHTLCErr { reason: LocalHTLCFailureReason::InvalidOnionPayload, @@ -453,16 +469,19 @@ where Ok(match hop { onion_utils::Hop::Forward { shared_secret, .. } | onion_utils::Hop::BlindedForward { shared_secret, .. } => { - let NextPacketDetails { - next_packet_pubkey, outgoing_amt_msat: _, outgoing_connector: _, outgoing_cltv_value - } = match next_packet_details_opt { - Some(next_packet_details) => next_packet_details, + let (next_packet_pubkey, outgoing_cltv_value) = match next_packet_details_opt { + Some(NextPacketDetails { + next_packet_pubkey, + forward_info: Some(ForwardInfo { outgoing_cltv_value, .. }), + }) => (next_packet_pubkey, outgoing_cltv_value), // Forward should always include the next hop details - None => return Err(InboundHTLCErr { - msg: "Failed to decode update add htlc onion", - reason: LocalHTLCFailureReason::InvalidOnionPayload, - err_data: Vec::new(), - }), + _ => { + return Err(InboundHTLCErr { + msg: "Failed to decode update add htlc onion", + reason: LocalHTLCFailureReason::InvalidOnionPayload, + err_data: Vec::new(), + }); + } }; if let Err(reason) = check_incoming_htlc_cltv( @@ -499,6 +518,10 @@ pub(super) enum HopConnector { pub(super) struct NextPacketDetails { pub(super) next_packet_pubkey: Result, + pub(super) forward_info: Option, +} + +pub(super) struct ForwardInfo { pub(super) outgoing_connector: HopConnector, pub(super) outgoing_amt_msat: u64, pub(super) outgoing_cltv_value: u32, @@ -575,8 +598,12 @@ where let next_packet_pubkey = onion_utils::next_hop_pubkey(secp_ctx, msg.onion_routing_packet.public_key.unwrap(), &shared_secret.secret_bytes()); Some(NextPacketDetails { - next_packet_pubkey, outgoing_connector: HopConnector::ShortChannelId(short_channel_id), - outgoing_amt_msat: amt_to_forward, outgoing_cltv_value + next_packet_pubkey, + forward_info: Some(ForwardInfo { + outgoing_connector: HopConnector::ShortChannelId(short_channel_id), + outgoing_amt_msat: amt_to_forward, + outgoing_cltv_value, + }), }) } onion_utils::Hop::BlindedForward { next_hop_data: msgs::InboundOnionBlindedForwardPayload { short_channel_id, ref payment_relay, ref payment_constraints, ref features, .. }, shared_secret, .. } => { @@ -592,8 +619,12 @@ where let next_packet_pubkey = onion_utils::next_hop_pubkey(&secp_ctx, msg.onion_routing_packet.public_key.unwrap(), &shared_secret.secret_bytes()); Some(NextPacketDetails { - next_packet_pubkey, outgoing_connector: HopConnector::ShortChannelId(short_channel_id), outgoing_amt_msat: amt_to_forward, - outgoing_cltv_value + next_packet_pubkey, + forward_info: Some(ForwardInfo { + outgoing_connector: HopConnector::ShortChannelId(short_channel_id), + outgoing_amt_msat: amt_to_forward, + outgoing_cltv_value, + }), }) } onion_utils::Hop::TrampolineForward { next_trampoline_hop_data: msgs::InboundTrampolineForwardPayload { amt_to_forward, outgoing_cltv_value, next_trampoline }, trampoline_shared_secret, incoming_trampoline_public_key, .. } => { @@ -601,10 +632,18 @@ where incoming_trampoline_public_key, &trampoline_shared_secret.secret_bytes()); Some(NextPacketDetails { next_packet_pubkey: next_trampoline_packet_pubkey, - outgoing_connector: HopConnector::Trampoline(next_trampoline), - outgoing_amt_msat: amt_to_forward, - outgoing_cltv_value, + forward_info: Some(ForwardInfo { + outgoing_connector: HopConnector::Trampoline(next_trampoline), + outgoing_amt_msat: amt_to_forward, + outgoing_cltv_value, + }), }) + }, + onion_utils::Hop::Dummy { shared_secret, .. } => { + let next_packet_pubkey = onion_utils::next_hop_pubkey(secp_ctx, + msg.onion_routing_packet.public_key.unwrap(), &shared_secret.secret_bytes()); + + Some(NextPacketDetails { next_packet_pubkey, forward_info: None }) } _ => None }; @@ -612,6 +651,32 @@ where Ok((next_hop, next_packet_details)) } +pub(super) fn create_new_update_add_htlc( + msg: msgs::UpdateAddHTLC, node_signer: NS, secp_ctx: &Secp256k1, + intro_node_blinding_point: Option, + new_packet_pubkey: Result, next_hop_hmac: [u8; 32], + new_packet_bytes: [u8; 1300], +) -> UpdateAddHTLC +where + NS::Target: NodeSigner, +{ + let new_packet = OnionPacket { + version: 0, + public_key: new_packet_pubkey, + hop_data: new_packet_bytes, + hmac: next_hop_hmac, + }; + + let next_blinding_point = + intro_node_blinding_point.or(msg.blinding_point).and_then(|blinding_point| { + let encrypted_tlvs_ss = + node_signer.ecdh(Recipient::Node, &blinding_point, None).unwrap().secret_bytes(); + onion_utils::next_hop_pubkey(&secp_ctx, blinding_point, &encrypted_tlvs_ss).ok() + }); + + UpdateAddHTLC { onion_routing_packet: new_packet, blinding_point: next_blinding_point, ..msg } +} + pub(super) fn check_incoming_htlc_cltv( cur_height: u32, outgoing_cltv_value: u32, cltv_expiry: u32, ) -> Result<(), LocalHTLCFailureReason> { diff --git a/lightning/src/ln/onion_utils.rs b/lightning/src/ln/onion_utils.rs index e32b39775fe..832ff8b4ece 100644 --- a/lightning/src/ln/onion_utils.rs +++ b/lightning/src/ln/onion_utils.rs @@ -2202,6 +2202,17 @@ pub(crate) enum Hop { /// Bytes of the onion packet we're forwarding. new_packet_bytes: [u8; ONION_DATA_LEN], }, + /// This onion payload is dummy, and needs to be peeled by us. + Dummy { + /// Onion payload data used in interpreting the dummy hop + intro_node_blinding_point: Option, + /// Shared secret that was used to decrypt next_hop_data. + shared_secret: SharedSecret, + /// HMAC of the next hop's onion packet. + next_hop_hmac: [u8; 32], + /// Bytes of the onion packet we're forwarding. + new_packet_bytes: [u8; ONION_DATA_LEN], + }, /// This onion payload was for us, not for forwarding to a next-hop. Contains information for /// verifying the incoming payment. Receive { @@ -2256,6 +2267,7 @@ impl Hop { match self { Hop::Forward { shared_secret, .. } => shared_secret, Hop::BlindedForward { shared_secret, .. } => shared_secret, + Hop::Dummy { shared_secret, .. } => shared_secret, Hop::TrampolineForward { outer_shared_secret, .. } => outer_shared_secret, Hop::TrampolineBlindedForward { outer_shared_secret, .. } => outer_shared_secret, Hop::Receive { shared_secret, .. } => shared_secret, @@ -2323,6 +2335,12 @@ where new_packet_bytes, }) }, + msgs::InboundOnionPayload::Dummy { intro_node_blinding_point } => Ok(Hop::Dummy { + intro_node_blinding_point, + shared_secret, + next_hop_hmac, + new_packet_bytes, + }), _ => { if blinding_point.is_some() { return Err(OnionDecodeErr::Malformed { diff --git a/lightning/src/routing/router.rs b/lightning/src/routing/router.rs index 371c5232511..bbcf7a320b3 100644 --- a/lightning/src/routing/router.rs +++ b/lightning/src/routing/router.rs @@ -74,6 +74,9 @@ pub struct DefaultRouter< score_params: SP, } +/// Default number of Dummy Hops +pub const DEFAULT_PAYMENT_DUMMY_HOPS: usize = 3; + impl< G: Deref>, L: Deref, @@ -198,9 +201,9 @@ where }) }) .map(|forward_node| { - BlindedPaymentPath::new( - &[forward_node], recipient, local_node_receive_key, tlvs.clone(), u64::MAX, MIN_FINAL_CLTV_EXPIRY_DELTA, - &*self.entropy_source, secp_ctx + BlindedPaymentPath::new_with_dummy_hops( + &[forward_node], recipient, DEFAULT_PAYMENT_DUMMY_HOPS, local_node_receive_key, tlvs.clone(), u64::MAX, + MIN_FINAL_CLTV_EXPIRY_DELTA, &*self.entropy_source, secp_ctx ) }) .take(MAX_PAYMENT_PATHS) @@ -210,9 +213,9 @@ where Ok(paths) if !paths.is_empty() => Ok(paths), _ => { if network_graph.nodes().contains_key(&NodeId::from_pubkey(&recipient)) { - BlindedPaymentPath::new( - &[], recipient, local_node_receive_key, tlvs, u64::MAX, MIN_FINAL_CLTV_EXPIRY_DELTA, &*self.entropy_source, - secp_ctx + BlindedPaymentPath::new_with_dummy_hops( + &[], recipient, DEFAULT_PAYMENT_DUMMY_HOPS, local_node_receive_key, tlvs, u64::MAX, + MIN_FINAL_CLTV_EXPIRY_DELTA, &*self.entropy_source, secp_ctx ).map(|path| vec![path]) } else { Err(())