diff --git a/lightning/src/blinded_path/message.rs b/lightning/src/blinded_path/message.rs index 218b2282141..0d4f9461315 100644 --- a/lightning/src/blinded_path/message.rs +++ b/lightning/src/blinded_path/message.rs @@ -524,6 +524,15 @@ pub enum AsyncPaymentsContext { /// [`Offer`]: crate::offers::offer::Offer payment_id: PaymentId, }, + /// Context contained within the reply [`BlindedMessagePath`] we put in outbound + /// [`HeldHtlcAvailable`] messages, provided back to us in corresponding [`ReleaseHeldHtlc`] + /// messages. + OutboundHTLC { + /// Incoming channel id of the HTLC that is being held. + chan_id: u64, + /// The HTLC id of the held HTLC on the incoming channel. + htlc_id: u64, + }, /// Context contained within the [`BlindedMessagePath`]s we put in static invoices, provided back /// to us in corresponding [`HeldHtlcAvailable`] messages. /// @@ -599,6 +608,10 @@ impl_writeable_tlv_based_enum!(AsyncPaymentsContext, (2, invoice_id, required), (4, path_absolute_expiry, required), }, + (6, OutboundHTLC) => { + (0, chan_id, required), + (2, htlc_id, required), + }, ); /// Contains a simple nonce for use in a blinded path's context. diff --git a/lightning/src/ln/blinded_payment_tests.rs b/lightning/src/ln/blinded_payment_tests.rs index 675d628ef2c..efcd16089a6 100644 --- a/lightning/src/ln/blinded_payment_tests.rs +++ b/lightning/src/ln/blinded_payment_tests.rs @@ -1522,6 +1522,7 @@ fn update_add_msg( onion_routing_packet, skimmed_fee_msat: None, blinding_point, + hold_htlc: None, } } diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index b5b77972a6c..54eadfa3dca 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -425,6 +425,7 @@ struct OutboundHTLCOutput { blinding_point: Option, skimmed_fee_msat: Option, send_timestamp: Option, + hold_htlc: bool, } impl OutboundHTLCOutput { @@ -459,6 +460,7 @@ enum HTLCUpdateAwaitingACK { // The extra fee we're skimming off the top of this HTLC. skimmed_fee_msat: Option, blinding_point: Option, + hold_htlc: bool, }, ClaimHTLC { payment_preimage: PaymentPreimage, @@ -7231,6 +7233,7 @@ where ref onion_routing_packet, skimmed_fee_msat, blinding_point, + hold_htlc, .. } => { match self.send_htlc( @@ -7242,6 +7245,7 @@ where false, skimmed_fee_msat, blinding_point, + hold_htlc, fee_estimator, logger, ) { @@ -8362,6 +8366,7 @@ where onion_routing_packet: (**onion_packet).clone(), skimmed_fee_msat: htlc.skimmed_fee_msat, blinding_point: htlc.blinding_point, + hold_htlc: htlc.hold_htlc.then(|| ()), }); } } @@ -10586,7 +10591,8 @@ where pub fn queue_add_htlc( &mut self, amount_msat: u64, payment_hash: PaymentHash, cltv_expiry: u32, source: HTLCSource, onion_routing_packet: msgs::OnionPacket, skimmed_fee_msat: Option, - blinding_point: Option, fee_estimator: &LowerBoundedFeeEstimator, logger: &L, + hold_htlc: bool, blinding_point: Option, + fee_estimator: &LowerBoundedFeeEstimator, logger: &L, ) -> Result<(), (LocalHTLCFailureReason, String)> where F::Target: FeeEstimator, @@ -10601,6 +10607,7 @@ where true, skimmed_fee_msat, blinding_point, + hold_htlc, fee_estimator, logger, ) @@ -10631,7 +10638,7 @@ where fn send_htlc( &mut self, amount_msat: u64, payment_hash: PaymentHash, cltv_expiry: u32, source: HTLCSource, onion_routing_packet: msgs::OnionPacket, mut force_holding_cell: bool, - skimmed_fee_msat: Option, blinding_point: Option, + skimmed_fee_msat: Option, blinding_point: Option, hold_htlc: bool, fee_estimator: &LowerBoundedFeeEstimator, logger: &L, ) -> Result where @@ -10713,6 +10720,7 @@ where onion_routing_packet, skimmed_fee_msat, blinding_point, + hold_htlc, }); return Ok(false); } @@ -10734,6 +10742,7 @@ where blinding_point, skimmed_fee_msat, send_timestamp, + hold_htlc, }); self.context.next_holder_htlc_id += 1; @@ -10977,7 +10986,7 @@ where pub fn send_htlc_and_commit( &mut self, amount_msat: u64, payment_hash: PaymentHash, cltv_expiry: u32, source: HTLCSource, onion_routing_packet: msgs::OnionPacket, skimmed_fee_msat: Option, - fee_estimator: &LowerBoundedFeeEstimator, logger: &L, + hold_htlc: bool, fee_estimator: &LowerBoundedFeeEstimator, logger: &L, ) -> Result, ChannelError> where F::Target: FeeEstimator, @@ -10992,6 +11001,7 @@ where false, skimmed_fee_msat, None, + hold_htlc, fee_estimator, logger, ); @@ -12629,6 +12639,7 @@ where ref onion_routing_packet, blinding_point, skimmed_fee_msat, + .. } => { 0u8.write(writer)?; amount_msat.write(writer)?; @@ -13032,6 +13043,7 @@ where skimmed_fee_msat: None, blinding_point: None, send_timestamp: None, + hold_htlc: false, // TODO: Persistence }); } @@ -13050,6 +13062,7 @@ where onion_routing_packet: Readable::read(reader)?, skimmed_fee_msat: None, blinding_point: None, + hold_htlc: false, // TODO: Persistence }, 1 => HTLCUpdateAwaitingACK::ClaimHTLC { payment_preimage: Readable::read(reader)?, @@ -13944,6 +13957,7 @@ mod tests { skimmed_fee_msat: None, blinding_point: None, send_timestamp: None, + hold_htlc: false, }); // Make sure when Node A calculates their local commitment transaction, none of the HTLCs pass @@ -14398,6 +14412,7 @@ mod tests { skimmed_fee_msat: None, blinding_point: None, send_timestamp: None, + hold_htlc: false, }; let mut pending_outbound_htlcs = vec![dummy_outbound_output.clone(); 10]; for (idx, htlc) in pending_outbound_htlcs.iter_mut().enumerate() { @@ -14423,6 +14438,7 @@ mod tests { }, skimmed_fee_msat: None, blinding_point: None, + hold_htlc: false, }; let dummy_holding_cell_claim_htlc = |attribution_data| HTLCUpdateAwaitingACK::ClaimHTLC { payment_preimage: PaymentPreimage([42; 32]), diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 8bac6c2fa3a..f5e631f7be2 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -235,6 +235,8 @@ pub enum PendingHTLCRouting { blinded: Option, /// The absolute CLTV of the inbound HTLC incoming_cltv_expiry: Option, + /// Set if the HTLC needs to be held until the recipients signals release. + hold_htlc: bool, }, /// An HTLC which should be forwarded on to another Trampoline node. TrampolineForward { @@ -2557,6 +2559,9 @@ pub struct ChannelManager< /// See `ChannelManager` struct-level documentation for lock order requirements. pending_intercepted_htlcs: Mutex>, + #[cfg(async_payments)] + pending_held_htlcs: Mutex>, + /// SCID/SCID Alias -> pending `update_add_htlc`s to decode. /// /// Note that because we may have an SCID Alias as the key we can have two entries per channel, @@ -3741,6 +3746,8 @@ where decode_update_add_htlcs: Mutex::new(new_hash_map()), claimable_payments: Mutex::new(ClaimablePayments { claimable_payments: new_hash_map(), pending_claiming_payments: new_hash_map() }), pending_intercepted_htlcs: Mutex::new(new_hash_map()), + #[cfg(async_payments)] + pending_held_htlcs: Mutex::new(new_hash_map()), short_to_chan_info: FairRwLock::new(new_hash_map()), our_network_pubkey, @@ -4915,6 +4922,7 @@ where &chan.context, Some(*payment_hash), ); + let hold_htlc = true; // TODO: Take from invoice? let htlc_source = HTLCSource::OutboundRoute { path: path.clone(), session_priv: session_priv.clone(), @@ -4929,6 +4937,7 @@ where htlc_source, onion_packet, None, + hold_htlc, &self.fee_estimator, &&logger, ); @@ -6109,6 +6118,7 @@ where blinded, incoming_cltv_expiry, short_channel_id: next_hop_scid, + hold_htlc: false, // Do not hold intercepted HTLCs. } }, _ => unreachable!(), // Only `PendingHTLCRouting::Forward`s are intercepted @@ -6134,6 +6144,41 @@ where Ok(()) } + #[cfg(async_payments)] + fn forward_held_htlc(&self, short_channel_id: u64, htlc_id: u64) -> Result<(), APIError> { + let _persistence_guard = PersistenceNotifierGuard::notify_on_drop(self); + + let held_htlc_id = (short_channel_id, htlc_id); + let mut htlc = + self.pending_held_htlcs.lock().unwrap().remove(&held_htlc_id).ok_or_else(|| { + APIError::APIMisuseError { + err: format!("Held htlc {}:{} not found", short_channel_id, htlc_id), + } + })?; + + if let PendingHTLCRouting::Forward { ref mut hold_htlc, .. } = htlc.forward_info.routing { + // Clear hold flag. + *hold_htlc = false; + } else { + return Err(APIError::APIMisuseError { + err: "Only PendingHTLCRouting::Forward HTLCs can be held".to_owned(), + }); + } + + let mut per_source_pending_forward = [( + htlc.prev_short_channel_id, + htlc.prev_counterparty_node_id, + htlc.prev_funding_outpoint, + htlc.prev_channel_id, + htlc.prev_user_channel_id, + vec![(htlc.forward_info, htlc.prev_htlc_id)], + )]; + + // Re-forward this time without the hold flag. + self.forward_htlcs(&mut per_source_pending_forward); + Ok(()) + } + /// Fails the intercepted HTLC indicated by intercept_id. Should only be called in response to /// an [`HTLCIntercepted`] event. See [`ChannelManager::forward_intercepted_htlc`]. /// @@ -6309,6 +6354,9 @@ where incoming_accept_underpaying_htlcs, next_packet_details_opt.map(|d| d.next_packet_pubkey), ) { + // if let PendingHTLCRouting::Forward { hold_htlc, .. } = info.routing { + // debug_assert!(hold_htlc, "Expected HTLC to be held"); + // } Ok(info) => htlc_forwards.push((info, update_add_htlc.htlc_id)), Err(inbound_err) => { let failure_type = @@ -6823,6 +6871,7 @@ where htlc_source.clone(), onion_packet.clone(), skimmed_fee_msat, + false, // Never signal hold on forwarded HTLCs. next_blinding_point, &self.fee_estimator, &&logger, @@ -10358,6 +10407,46 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ let mut failed_intercept_forwards = Vec::new(); if !pending_forwards.is_empty() { for (forward_info, prev_htlc_id) in pending_forwards.drain(..) { + // If this HTLC needs to be held for release by an offline recipient, we'll move it to a dedicated + // hash map for held HTLCs. + #[cfg(async_payments)] + if let PendingHTLCRouting::Forward { hold_htlc: true, .. } = + forward_info.routing + { + let mut held_htlcs = self.pending_held_htlcs.lock().unwrap(); + held_htlcs.insert( + (prev_short_channel_id, prev_htlc_id), + PendingAddHTLCInfo { + prev_short_channel_id, + prev_counterparty_node_id, + prev_funding_outpoint, + prev_channel_id, + prev_htlc_id, + prev_user_channel_id, + forward_info, + }, + ); + + // TODO: Where to get message paths from?!? + let message_paths = []; + + self.flow.enqueue_held_forward_htlc_available( + prev_short_channel_id, + prev_htlc_id, + self.get_peers_for_blinded_path(), + &message_paths, + ); + + log_debug!( + self.logger, + "Holding HTLC {}:{} for release by an offline recipient", + prev_short_channel_id, + prev_htlc_id + ); + + continue; + } + let scid = match forward_info.routing { PendingHTLCRouting::Forward { short_channel_id, .. } => short_channel_id, PendingHTLCRouting::TrampolineForward { .. } => 0, @@ -14420,19 +14509,30 @@ where fn handle_release_held_htlc(&self, _message: ReleaseHeldHtlc, _context: AsyncPaymentsContext) { #[cfg(async_payments)] { - let payment_id = match _context { - AsyncPaymentsContext::OutboundPayment { payment_id } => payment_id, + match _context { + AsyncPaymentsContext::OutboundPayment { payment_id } => { + if let Err(e) = self.send_payment_for_static_invoice(payment_id) { + log_trace!( + self.logger, + "Failed to release held HTLC with payment id {}: {:?}", + payment_id, + e + ); + } + }, + AsyncPaymentsContext::OutboundHTLC { chan_id, htlc_id } => { + if let Err(e) = self.forward_held_htlc(chan_id, htlc_id) { + log_trace!( + self.logger, + "Failed to release held forward HTLC {}:{} {:?}", + chan_id, + htlc_id, + e + ); + } + }, _ => return, }; - - if let Err(e) = self.send_payment_for_static_invoice(payment_id) { - log_trace!( - self.logger, - "Failed to release held HTLC with payment id {}: {:?}", - payment_id, - e - ); - } } } @@ -14640,6 +14740,7 @@ impl_writeable_tlv_based_enum!(PendingHTLCRouting, (1, blinded, option), (2, short_channel_id, required), (3, incoming_cltv_expiry, option), + (4, hold_htlc, (default_value, false)) }, (1, Receive) => { (0, payment_data, required), @@ -16804,6 +16905,8 @@ where inbound_payment_key: expanded_inbound_key, pending_outbound_payments: pending_outbounds, pending_intercepted_htlcs: Mutex::new(pending_intercepted_htlcs.unwrap()), + #[cfg(async_payments)] + pending_held_htlcs: Mutex::new(new_hash_map()), // TODO: Persistence. forward_htlcs: Mutex::new(forward_htlcs), decode_update_add_htlcs: Mutex::new(decode_update_add_htlcs), diff --git a/lightning/src/ln/functional_tests.rs b/lightning/src/ln/functional_tests.rs index f6c92d77fbf..256ea7fd9c5 100644 --- a/lightning/src/ln/functional_tests.rs +++ b/lightning/src/ln/functional_tests.rs @@ -2683,6 +2683,7 @@ pub fn fail_backward_pending_htlc_upon_channel_failure() { onion_routing_packet, skimmed_fee_msat: None, blinding_point: None, + hold_htlc: None, }; nodes[0].node.handle_update_add_htlc(node_b_id, &update_add_htlc); } diff --git a/lightning/src/ln/htlc_reserve_unit_tests.rs b/lightning/src/ln/htlc_reserve_unit_tests.rs index 2f9d1d3bf7d..6cf8bee1d62 100644 --- a/lightning/src/ln/htlc_reserve_unit_tests.rs +++ b/lightning/src/ln/htlc_reserve_unit_tests.rs @@ -835,6 +835,7 @@ pub fn do_test_fee_spike_buffer(cfg: Option, htlc_fails: bool) { onion_routing_packet: onion_packet, skimmed_fee_msat: None, blinding_point: None, + hold_htlc: None, }; nodes[1].node.handle_update_add_htlc(node_a_id, &msg); @@ -1072,6 +1073,7 @@ pub fn test_chan_reserve_violation_inbound_htlc_outbound_channel() { onion_routing_packet: onion_packet, skimmed_fee_msat: None, blinding_point: None, + hold_htlc: None, }; nodes[0].node.handle_update_add_htlc(node_b_id, &msg); @@ -1255,6 +1257,7 @@ pub fn test_chan_reserve_violation_inbound_htlc_inbound_chan() { onion_routing_packet: onion_packet, skimmed_fee_msat: None, blinding_point: None, + hold_htlc: None, }; nodes[1].node.handle_update_add_htlc(node_a_id, &msg); @@ -1637,6 +1640,7 @@ pub fn test_update_add_htlc_bolt2_receiver_check_max_htlc_limit() { onion_routing_packet: onion_packet.clone(), skimmed_fee_msat: None, blinding_point: None, + hold_htlc: None, }; for i in 0..50 { @@ -2242,6 +2246,7 @@ pub fn do_test_dust_limit_fee_accounting(can_afford: bool) { onion_routing_packet, skimmed_fee_msat: None, blinding_point: None, + hold_htlc: None, }; nodes[1].node.handle_update_add_htlc(node_a_id, &msg); diff --git a/lightning/src/ln/msgs.rs b/lightning/src/ln/msgs.rs index e0219a5523f..dacd0859458 100644 --- a/lightning/src/ln/msgs.rs +++ b/lightning/src/ln/msgs.rs @@ -766,6 +766,10 @@ pub struct UpdateAddHTLC { /// Provided if we are relaying or receiving a payment within a blinded path, to decrypt the onion /// routing packet and the recipient-provided encrypted payload within. pub blinding_point: Option, + /// When set, this HTLC is held and a [`HeldHtlcAvailable`] message will be sent out. + /// + /// [`HeldHtlcAvailable`]: crate::onion_message::async_payments::HeldHtlcAvailable + pub hold_htlc: Option<()>, } /// An onion message to be sent to or received from a peer. @@ -3227,6 +3231,7 @@ impl_writeable_msg!(UpdateAddHTLC, { onion_routing_packet, }, { (0, blinding_point, option), + (2, hold_htlc, option), (65537, skimmed_fee_msat, option) }); @@ -5663,6 +5668,7 @@ mod tests { onion_routing_packet, skimmed_fee_msat: None, blinding_point: None, + hold_htlc: None, }; let encoded_value = update_add_htlc.encode(); let target_value = >::from_hex("020202020202020202020202020202020202020202020202020202020202020200083a840000034d32144668701144760101010101010101010101010101010101010101010101010101010101010101000c89d4ff031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010202020202020202020202020202020202020202020202020202020202020202").unwrap(); diff --git a/lightning/src/ln/onion_payment.rs b/lightning/src/ln/onion_payment.rs index 79952faca9a..21c5b0d0a9e 100644 --- a/lightning/src/ln/onion_payment.rs +++ b/lightning/src/ln/onion_payment.rs @@ -198,6 +198,7 @@ pub(super) fn create_fwd_pending_htlc_info( .map(|_| BlindedFailure::FromIntroductionNode) .unwrap_or(BlindedFailure::FromBlindedNode), }), + hold_htlc: msg.hold_htlc.is_some(), } } RoutingInfo::Trampoline { next_trampoline, new_packet_bytes, next_hop_hmac, shared_secret, current_path_key } => { @@ -753,6 +754,7 @@ mod tests { onion_routing_packet, skimmed_fee_msat: None, blinding_point: None, + hold_htlc: None, } } diff --git a/lightning/src/ln/payment_tests.rs b/lightning/src/ln/payment_tests.rs index b11294f1158..00b885ecb96 100644 --- a/lightning/src/ln/payment_tests.rs +++ b/lightning/src/ln/payment_tests.rs @@ -5015,6 +5015,7 @@ fn peel_payment_onion_custom_tlvs() { skimmed_fee_msat: None, onion_routing_packet, blinding_point: None, + hold_htlc: None, }; let peeled_onion = crate::ln::onion_payment::peel_payment_onion( &update_add, diff --git a/lightning/src/offers/flow.rs b/lightning/src/offers/flow.rs index 951d33de319..d2ae791edb0 100644 --- a/lightning/src/offers/flow.rs +++ b/lightning/src/offers/flow.rs @@ -1169,6 +1169,33 @@ where Ok(()) } + /// Enqueues `held_htlc_available` onion messages to be sent to the payee for a held forward HTLC. + #[cfg(async_payments)] + pub fn enqueue_held_forward_htlc_available( + &self, chan_id: u64, htlc_id: u64, peers: Vec, + message_paths: &[BlindedMessagePath], + ) -> Result<(), Bolt12SemanticError> { + let context = + MessageContext::AsyncPayments(AsyncPaymentsContext::OutboundHTLC { chan_id, htlc_id }); + + let reply_paths = self + .create_blinded_paths(peers, context) + .map_err(|_| Bolt12SemanticError::MissingPaths)?; + + let mut pending_async_payments_messages = + self.pending_async_payments_messages.lock().unwrap(); + + let message = AsyncPaymentsMessage::HeldHtlcAvailable(HeldHtlcAvailable {}); + enqueue_onion_message_with_reply_paths( + message, + message_paths, + reply_paths, + &mut pending_async_payments_messages, + ); + + Ok(()) + } + /// Enqueues the created [`DNSSECQuery`] to be sent to the counterparty. /// /// # Peers