Support creating reply_path for HeldHtlcAvailable
What changed, and why it matters
This commit adds a helper to build a private return-address (a 'reply path') for a new type of Lightning message used when an often-offline sender asks a recipient to release a held payment. The change itself is plumbing for an upcoming feature; it does not appear to fix a known vulnerability or introduce an obvious one, but it touches sensitive payment-handling code and is part of a larger async-payments design whose security depends on details not shown here.
Review the subsequent commit that consumes this helper to ensure the reply path is only used for held HTLCs the local node legitimately controls, that InterceptId collisions cannot be exploited, and that the blinded path padding and context authentication prevent reply-path hijacking or release-message replay.
Security signals we found
Adds new blinded reply-path context for async payment release messages
Derives InterceptId from HTLC and channel identifiers for release authorization
Uses PADDED_PATH_LENGTH dummy hops to pad the blinded path
Part of a multi-commit async-payments feature; security relevance depends on follow-up usage
No explicit security claim, CVE, or bug fix language in commit message
Evidence from the diff
The patch introduces AsyncPaymentsContext::ReleaseHeldHtlc and a method path_for_release_held_htlc that constructs a BlindedMessagePath with dummy hops so a ReleaseHeldHtlc onion message reply is routed to the sender’s always-online channel counterparty. It adds serialization support for the new context variant and wires ChannelManager to derive an InterceptId from htlc_id, channel_id, and counterparty_node_id. The actual use of this reply path is deferred to a later commit. No security bug or fix is directly visible in the diff.
Changed components
lightning/src/blinded_path/message.rslightning/src/ln/channelmanager.rslightning/src/offers/flow.rsInspect captured patch +57 / −4
diff --git a/lightning/src/blinded_path/message.rs b/lightning/src/blinded_path/message.rs
index 7d721cd..e291c83 100644
--- a/lightning/src/blinded_path/message.rs
+++ b/lightning/src/blinded_path/message.rs
@@ -19,7 +19,7 @@ use crate::blinded_path::{BlindedHop, BlindedPath, Direction, IntroductionNode,
use crate::crypto::streams::ChaChaPolyReadAdapter;
use crate::io;
use crate::io::Cursor;
-use crate::ln::channelmanager::PaymentId;
+use crate::ln::channelmanager::{InterceptId, PaymentId};
use crate::ln::msgs::DecodeError;
use crate::ln::onion_utils;
use crate::offers::nonce::Nonce;
@@ -556,7 +556,7 @@ pub enum AsyncPaymentsContext {
},
/// Context contained within the reply [`BlindedMessagePath`] we put in outbound
/// [`HeldHtlcAvailable`] messages, provided back to us in corresponding [`ReleaseHeldHtlc`]
- /// messages.
+ /// messages if we are an always-online sender paying an async recipient.
///
/// [`HeldHtlcAvailable`]: crate::onion_message::async_payments::HeldHtlcAvailable
/// [`ReleaseHeldHtlc`]: crate::onion_message::async_payments::ReleaseHeldHtlc
@@ -577,6 +577,17 @@ pub enum AsyncPaymentsContext {
/// able to trivially ask if we're online forever.
path_absolute_expiry: core::time::Duration,
},
+ /// Context contained within the reply [`BlindedMessagePath`] put in outbound
+ /// [`HeldHtlcAvailable`] messages, provided back to the async sender's always-online counterparty
+ /// in corresponding [`ReleaseHeldHtlc`] messages.
+ ///
+ /// [`HeldHtlcAvailable`]: crate::onion_message::async_payments::HeldHtlcAvailable
+ /// [`ReleaseHeldHtlc`]: crate::onion_message::async_payments::ReleaseHeldHtlc
+ ReleaseHeldHtlc {
+ /// An identifier for the HTLC that should be released by us as the sender's always-online
+ /// channel counterparty to the often-offline recipient.
+ intercept_id: InterceptId,
+ },
}
impl_writeable_tlv_based_enum!(MessageContext,
@@ -632,6 +643,9 @@ impl_writeable_tlv_based_enum!(AsyncPaymentsContext,
(2, invoice_slot, required),
(4, path_absolute_expiry, required),
},
+ (6, ReleaseHeldHtlc) => {
+ (0, intercept_id, required),
+ },
);
/// Contains a simple nonce for use in a blinded path's context.
diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs
index ee2de6c..4848500 100644
--- a/lightning/src/ln/channelmanager.rs
+++ b/lightning/src/ln/channelmanager.rs
@@ -5467,6 +5467,18 @@ where
res
}
+ /// If we are holding an HTLC on behalf of an often-offline sender, this method allows us to
+ /// create a path for the sender to use as the reply path when they send the recipient a
+ /// [`HeldHtlcAvailable`] onion message, so the recipient's [`ReleaseHeldHtlc`] response will be
+ /// received to our node.
+ fn path_for_release_held_htlc(
+ &self, htlc_id: u64, channel_id: &ChannelId, counterparty_node_id: &PublicKey,
+ ) -> BlindedMessagePath {
+ let intercept_id =
+ InterceptId::from_htlc_id_and_chan_id(htlc_id, channel_id, counterparty_node_id);
+ self.flow.path_for_release_held_htlc(intercept_id, &*self.entropy_source)
+ }
+
/// Signals that no further attempts for the given payment should occur. Useful if you have a
/// pending outbound payment with retries remaining, but wish to stop retrying the payment before
/// retries are exhausted.
diff --git a/lightning/src/offers/flow.rs b/lightning/src/offers/flow.rs
index 9040f78..9025239 100644
--- a/lightning/src/offers/flow.rs
+++ b/lightning/src/offers/flow.rs
@@ -32,7 +32,7 @@ use crate::prelude::*;
use crate::chain::BestBlock;
use crate::ln::channel_state::ChannelDetails;
-use crate::ln::channelmanager::{PaymentId, CLTV_FAR_FAR_AWAY};
+use crate::ln::channelmanager::{InterceptId, PaymentId, CLTV_FAR_FAR_AWAY};
use crate::ln::inbound_payment;
use crate::offers::async_receive_offer_cache::AsyncReceiveOfferCache;
use crate::offers::invoice::{
@@ -52,7 +52,7 @@ use crate::onion_message::async_payments::{
StaticInvoicePersisted,
};
use crate::onion_message::messenger::{
- Destination, MessageRouter, MessageSendInstructions, Responder,
+ Destination, MessageRouter, MessageSendInstructions, Responder, PADDED_PATH_LENGTH,
};
use crate::onion_message::offers::OffersMessage;
use crate::onion_message::packet::OnionMessageContents;
@@ -1163,6 +1163,33 @@ where
Ok(())
}
+ /// If we are holding an HTLC on behalf of an often-offline sender, this method allows us to
+ /// create a path for the sender to use as the reply path when they send the recipient a
+ /// [`HeldHtlcAvailable`] onion message, so the recipient's [`ReleaseHeldHtlc`] response will be
+ /// received to our node.
+ ///
+ /// [`ReleaseHeldHtlc`]: crate::onion_message::async_payments::ReleaseHeldHtlc
+ pub fn path_for_release_held_htlc<ES: Deref>(
+ &self, intercept_id: InterceptId, entropy: ES,
+ ) -> BlindedMessagePath
+ where
+ ES::Target: EntropySource,
+ {
+ // In the future, we should support multi-hop paths here.
+ let context =
+ MessageContext::AsyncPayments(AsyncPaymentsContext::ReleaseHeldHtlc { intercept_id });
+ let num_dummy_hops = PADDED_PATH_LENGTH.saturating_sub(1);
+ BlindedMessagePath::new_with_dummy_hops(
+ &[],
+ self.get_our_node_id(),
+ num_dummy_hops,
+ self.receive_auth_key,
+ context,
+ &*entropy,
+ &self.secp_ctx,
+ )
+ }
+
/// Enqueues the created [`DNSSECQuery`] to be sent to the counterparty.
///
/// # Peers
Why this scored 22/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.