Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 27 additions & 3 deletions lightning-liquidity/src/lsps2/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,27 @@ pub enum LSPS2ClientEvent {
/// When the invoice is paid, the LSP will open a channel with the previously agreed upon
/// parameters to you.
///
/// **Note: ** This event will *not* be persisted across restarts.
/// ## BOLT11
/// For BOLT11 invoices, use `intercept_scid` and `cltv_expiry_delta` in a route hint
/// pointing to the LSP (`counterparty_node_id`).
///
/// ## BOLT12
/// For BOLT12 invoices, the same parameters are used to construct blinded payment paths
/// through the LSP:
/// - `counterparty_node_id` is the introduction node (LSP) of the blinded payment path
/// - `intercept_scid` is used as `ForwardTlvs::short_channel_id` in the blinded path
/// - `cltv_expiry_delta` is used as `PaymentRelay::cltv_expiry_delta` in the blinded path
/// - Fee parameters should be set to zero (fees are taken via fee skimming in LSPS2)
///
/// Use [`OffersMessageFlow::create_blinded_payment_paths_for_intercept_scid`] to construct
/// the blinded payment paths, and
/// [`OffersMessageFlow::create_invoice_builder_from_invoice_request_with_custom_payment_paths`]
/// to build the BOLT12 invoice with those paths.
///
/// [`OffersMessageFlow::create_blinded_payment_paths_for_intercept_scid`]: lightning::offers::flow::OffersMessageFlow::create_blinded_payment_paths_for_intercept_scid
/// [`OffersMessageFlow::create_invoice_builder_from_invoice_request_with_custom_payment_paths`]: lightning::offers::flow::OffersMessageFlow::create_invoice_builder_from_invoice_request_with_custom_payment_paths
///
/// **Note:** This event will *not* be persisted across restarts.
InvoiceParametersReady {
/// The identifier of the issued bLIP-52 / LSPS2 `buy` request, as returned by
/// [`LSPS2ClientHandler::select_opening_params`].
Expand All @@ -59,10 +79,14 @@ pub enum LSPS2ClientEvent {
/// [`LSPS2ClientHandler::select_opening_params`]: crate::lsps2::client::LSPS2ClientHandler::select_opening_params
request_id: LSPSRequestId,
/// The node id of the LSP.
///
/// For BOLT12, this is used as the introduction node of the blinded payment path.
counterparty_node_id: PublicKey,
/// The intercept short channel id to use in the route hint.
/// The intercept short channel id to use in the route hint (BOLT11) or as the
/// `ForwardTlvs::short_channel_id` in a blinded payment path (BOLT12).
intercept_scid: u64,
/// The `cltv_expiry_delta` to use in the route hint.
/// The `cltv_expiry_delta` to use in the route hint (BOLT11) or as the
/// `PaymentRelay::cltv_expiry_delta` in a blinded payment path (BOLT12).
cltv_expiry_delta: u32,
/// The initial payment size you specified.
payment_size_msat: Option<u64>,
Expand Down
38 changes: 38 additions & 0 deletions lightning/src/events/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1827,6 +1827,38 @@ pub enum Event {
/// [`ChannelManager::respond_to_static_invoice_request`]: crate::ln::channelmanager::ChannelManager::respond_to_static_invoice_request
invoice_request: InvoiceRequest,
},
/// We received a [`Bolt12InvoiceRequest`] and the user has opted to manually handle it via
/// [`UserConfig::manually_handle_bolt12_invoice_requests`].
///
/// The user should construct a [`Bolt12Invoice`] (e.g., using custom blinded payment paths
/// for LSPS2 JIT channels via
/// [`OffersMessageFlow::create_blinded_payment_paths_for_intercept_scid`] and
/// [`OffersMessageFlow::create_invoice_builder_from_invoice_request_with_custom_payment_paths`])
/// and send it back via [`ChannelManager::send_invoice_for_request`].
///
/// # Failure Behavior and Persistence
/// This event will eventually be replayed after failures-to-handle (i.e., the event handler
/// returning `Err(ReplayEvent ())`), but won't be persisted across restarts.
///
/// [`Bolt12InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
/// [`Bolt12Invoice`]: crate::offers::invoice::Bolt12Invoice
/// [`UserConfig::manually_handle_bolt12_invoice_requests`]: crate::util::config::UserConfig::manually_handle_bolt12_invoice_requests
/// [`OffersMessageFlow::create_blinded_payment_paths_for_intercept_scid`]: crate::offers::flow::OffersMessageFlow::create_blinded_payment_paths_for_intercept_scid
/// [`OffersMessageFlow::create_invoice_builder_from_invoice_request_with_custom_payment_paths`]: crate::offers::flow::OffersMessageFlow::create_invoice_builder_from_invoice_request_with_custom_payment_paths
/// [`ChannelManager::send_invoice_for_request`]: crate::ln::channelmanager::ChannelManager::send_invoice_for_request
InvoiceRequestReceived {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We've been discussing for removing these events for awhile now in favor of events in OffersMessageFlow. But maybe we've come full circle on that? See: #3833 (comment). (cc: @TheBlueMatt)

There's definitely a lot of handling logic in ChannelManager's implementation of OffersMessageHandler that wouldn't be great if a custom implementation needed to reproduce.

/// The invoice request received from the payer.
invoice_request: InvoiceRequest,
/// The context from the incoming onion message, needed for verification via
/// [`OffersMessageFlow::verify_invoice_request`].
///
/// [`OffersMessageFlow::verify_invoice_request`]: crate::offers::flow::OffersMessageFlow::verify_invoice_request
context: Option<OffersContext>,
/// Used to send the [`Bolt12Invoice`] response back to the payer.
///
/// [`Bolt12Invoice`]: crate::offers::invoice::Bolt12Invoice
responder: Responder,
},
/// Indicates that a channel funding transaction constructed interactively is ready to be
/// signed. This event will only be triggered if at least one input was contributed.
///
Expand Down Expand Up @@ -2317,6 +2349,10 @@ impl Writeable for Event {
47u8.write(writer)?;
// Never write StaticInvoiceRequested events as buffered onion messages aren't serialized.
},
&Event::InvoiceRequestReceived { .. } => {
51u8.write(writer)?;
// Never write InvoiceRequestReceived events as the responder isn't serialized.
},
&Event::FundingTransactionReadyForSigning { .. } => {
49u8.write(writer)?;
// We never write out FundingTransactionReadyForSigning events as they will be regenerated when
Expand Down Expand Up @@ -2948,6 +2984,8 @@ impl MaybeReadable for Event {
47u8 => Ok(None),
// Note that we do not write a length-prefixed TLV for FundingTransactionReadyForSigning events.
49u8 => Ok(None),
// Note that we do not write a length-prefixed TLV for InvoiceRequestReceived events.
51u8 => Ok(None),
50u8 => {
let mut f = || {
_init_and_read_len_prefixed_tlv_fields!(reader, {
Expand Down
55 changes: 55 additions & 0 deletions lightning/src/ln/channelmanager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ use bitcoin::secp256k1::Secp256k1;
use bitcoin::secp256k1::{PublicKey, SecretKey};
use bitcoin::{secp256k1, Sequence, SignedAmount};

use crate::blinded_path::message::MessageContext;
use crate::blinded_path::message::{
AsyncPaymentsContext, BlindedMessagePath, MessageForwardNode, OffersContext,
};
Expand Down Expand Up @@ -5579,6 +5580,41 @@ impl<
self.flow.enqueue_static_invoice(invoice, responder)
}

/// Sends a [`Bolt12Invoice`] in response to an [`Event::InvoiceRequestReceived`].
///
/// This is used when [`UserConfig::manually_handle_bolt12_invoice_requests`] is set to `true`,
/// allowing the user to construct a custom invoice (e.g., with blinded payment paths through
/// an LSP's intercept SCID for LSPS2 JIT channels) and send it back to the payer.
///
/// The `responder` and `context` should come from the [`Event::InvoiceRequestReceived`]
/// event and the invoice builder (e.g., via
/// [`OffersMessageFlow::create_invoice_builder_from_invoice_request_with_custom_payment_paths`]),
/// respectively.
///
/// [`Event::InvoiceRequestReceived`]: crate::events::Event::InvoiceRequestReceived
/// [`UserConfig::manually_handle_bolt12_invoice_requests`]: crate::util::config::UserConfig::manually_handle_bolt12_invoice_requests
/// [`OffersMessageFlow::create_invoice_builder_from_invoice_request_with_custom_payment_paths`]: crate::offers::flow::OffersMessageFlow::create_invoice_builder_from_invoice_request_with_custom_payment_paths
pub fn send_invoice_for_request(
&self, invoice: Bolt12Invoice, context: MessageContext, responder: Responder,
) {
self.flow.enqueue_invoice_for_request(invoice, context, responder);
}

/// Returns a reference to the [`OffersMessageFlow`], which provides methods for creating
/// BOLT12 offers, invoices, and related blinded paths.
///
/// This is useful when manually handling invoice requests (see
/// [`UserConfig::manually_handle_bolt12_invoice_requests`]) to access methods like
/// [`OffersMessageFlow::create_blinded_payment_paths_for_intercept_scid`] and
/// [`OffersMessageFlow::create_invoice_builder_from_invoice_request_with_custom_payment_paths`].
///
/// [`UserConfig::manually_handle_bolt12_invoice_requests`]: crate::util::config::UserConfig::manually_handle_bolt12_invoice_requests
/// [`OffersMessageFlow::create_blinded_payment_paths_for_intercept_scid`]: crate::offers::flow::OffersMessageFlow::create_blinded_payment_paths_for_intercept_scid
/// [`OffersMessageFlow::create_invoice_builder_from_invoice_request_with_custom_payment_paths`]: crate::offers::flow::OffersMessageFlow::create_invoice_builder_from_invoice_request_with_custom_payment_paths
pub fn offers_handler(&self) -> &OffersMessageFlow<MR, L> {
&self.flow
}

fn initiate_async_payment(
&self, invoice: &StaticInvoice, payment_id: PaymentId,
) -> Result<(), Bolt12PaymentError> {
Expand Down Expand Up @@ -15992,6 +16028,9 @@ impl<
None => return None,
};

let manually_handle = self.config.read().unwrap().manually_handle_bolt12_invoice_requests;
let saved_context = if manually_handle { context.clone() } else { None };

let invoice_request = match self.flow.verify_invoice_request(invoice_request, context) {
Ok(InvreqResponseInstructions::SendInvoice(invoice_request)) => invoice_request,
Ok(InvreqResponseInstructions::SendStaticInvoice { recipient_id, invoice_slot, invoice_request }) => {
Expand All @@ -16004,6 +16043,22 @@ impl<
Err(_) => return None,
};

if manually_handle {
let inner_request = match invoice_request {
InvoiceRequestVerifiedFromOffer::DerivedKeys(ref request) => request.inner.clone(),
InvoiceRequestVerifiedFromOffer::ExplicitKeys(ref request) => request.inner.clone(),
};
self.pending_events.lock().unwrap().push_back((
Event::InvoiceRequestReceived {
invoice_request: inner_request,
context: saved_context,
responder,
},
None,
));
return None;
}

let get_payment_info = |amount_msats, relative_expiry| {
self.create_inbound_payment(
Some(amount_msats),
Expand Down
141 changes: 139 additions & 2 deletions lightning/src/offers/flow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ use crate::blinded_path::message::{
};
use crate::blinded_path::payment::{
AsyncBolt12OfferContext, BlindedPaymentPath, Bolt12OfferContext, Bolt12RefundContext,
PaymentConstraints, PaymentContext, ReceiveTlvs,
ForwardTlvs, PaymentConstraints, PaymentContext, PaymentForwardNode, PaymentRelay, ReceiveTlvs,
};
use crate::chain::channelmonitor::LATENCY_GRACE_PERIOD_BLOCKS;

Expand All @@ -31,7 +31,9 @@ use crate::prelude::*;

use crate::chain::BestBlock;
use crate::ln::channel_state::ChannelDetails;
use crate::ln::channelmanager::{InterceptId, PaymentId, CLTV_FAR_FAR_AWAY};
use crate::ln::channelmanager::{
InterceptId, PaymentId, CLTV_FAR_FAR_AWAY, MIN_FINAL_CLTV_EXPIRY_DELTA,
};
use crate::ln::inbound_payment;
use crate::offers::async_receive_offer_cache::AsyncReceiveOfferCache;
use crate::offers::invoice::{
Expand All @@ -58,6 +60,7 @@ use crate::onion_message::packet::OnionMessageContents;
use crate::routing::router::Router;
use crate::sign::{EntropySource, ReceiveAuthKey};
use crate::sync::{Mutex, RwLock};
use crate::types::features::BlindedHopFeatures;
use crate::types::payment::{PaymentHash, PaymentSecret};
use crate::util::logger::Logger;
use crate::util::ser::Writeable;
Expand Down Expand Up @@ -337,6 +340,79 @@ impl<MR: MessageRouter, L: Logger> OffersMessageFlow<MR, L> {
)
}

/// Creates a [`BlindedPaymentPath`] that goes through an LSP's intercept SCID.
///
/// This is intended for use with LSPS2 (JIT channels) and BOLT12 invoices. The resulting
/// blinded payment path has the LSP as the introduction node, with the `intercept_scid`
/// encoded in the [`ForwardTlvs`] so that the LSP can intercept the HTLC and open a JIT
/// channel to the client.
///
/// Fee parameters in the blinded path are set to zero since LSPS2 takes fees via fee
/// skimming rather than through relay fees.
///
/// The caller is expected to obtain `payment_secret` from
/// [`ChannelManager::create_inbound_payment`] and pass the corresponding `payment_hash`
/// when building the invoice via
/// [`Self::create_invoice_builder_from_invoice_request_with_custom_payment_paths`].
///
/// [`ChannelManager::create_inbound_payment`]: crate::ln::channelmanager::ChannelManager::create_inbound_payment
pub fn create_blinded_payment_paths_for_intercept_scid<ES: EntropySource>(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be a custom Router implementation instead? For message blinded paths, we took the approach of allowing to pass a custom MessageRouter if we want non-standard blinded path creation.

&self, entropy_source: ES, lsp_node_id: PublicKey, intercept_scid: u64,
cltv_expiry_delta: u16, payment_secret: PaymentSecret, payment_context: PaymentContext,
amount_msats: u64, relative_expiry_seconds: u32,
) -> Result<Vec<BlindedPaymentPath>, ()> {
let secp_ctx = &self.secp_ctx;
let receive_auth_key = self.receive_auth_key;
let payee_node_id = self.get_our_node_id();

// Assume shorter than usual block times to avoid spuriously failing payments too early.
const SECONDS_PER_BLOCK: u32 = 9 * 60;
let relative_expiry_blocks = relative_expiry_seconds / SECONDS_PER_BLOCK;
let max_cltv_expiry = core::cmp::max(relative_expiry_blocks, CLTV_FAR_FAR_AWAY)
.saturating_add(LATENCY_GRACE_PERIOD_BLOCKS)
.saturating_add(self.best_block.read().unwrap().height);

let payee_tlvs = ReceiveTlvs {
payment_secret,
payment_constraints: PaymentConstraints { max_cltv_expiry, htlc_minimum_msat: 1 },
payment_context,
};

// Build the forwarding node representing the LSP. The payment constraints for the
// forwarding hop extend the max CLTV expiry by the hop's delta.
let forward_node = PaymentForwardNode {
tlvs: ForwardTlvs {
short_channel_id: intercept_scid,
payment_relay: PaymentRelay {
cltv_expiry_delta,
fee_base_msat: 0,
fee_proportional_millionths: 0,
},
payment_constraints: PaymentConstraints {
max_cltv_expiry: max_cltv_expiry.saturating_add(cltv_expiry_delta as u32),
htlc_minimum_msat: 0,
},
features: BlindedHopFeatures::empty(),
next_blinding_override: None,
},
node_id: lsp_node_id,
htlc_maximum_msat: amount_msats,
};

let path = BlindedPaymentPath::new(
&[forward_node],
payee_node_id,
receive_auth_key,
payee_tlvs,
amount_msats,
MIN_FINAL_CLTV_EXPIRY_DELTA,
entropy_source,
secp_ctx,
)?;

Ok(vec![path])
}

#[cfg(test)]
/// Creates multi-hop blinded payment paths for the given `amount_msats` by delegating to
/// [`Router::create_blinded_payment_paths`].
Expand Down Expand Up @@ -1032,6 +1108,43 @@ impl<MR: MessageRouter, L: Logger> OffersMessageFlow<MR, L> {
Ok((builder, context))
}

/// Creates an [`InvoiceBuilder<DerivedSigningPubkey>`] for the provided
/// [`VerifiedInvoiceRequest<DerivedSigningPubkey>`] using pre-built blinded payment paths.
///
/// This is intended for use with LSPS2 (JIT channels) where the blinded payment paths are
/// constructed via [`Self::create_blinded_payment_paths_for_intercept_scid`] rather than
/// through the [`Router`].
///
/// The caller is expected to:
/// 1. Call [`ChannelManager::create_inbound_payment`] to obtain both a `payment_hash` and
/// `payment_secret`.
/// 2. Use the `payment_secret` when constructing blinded payment paths via
/// [`Self::create_blinded_payment_paths_for_intercept_scid`].
/// 3. Pass the resulting `payment_paths` and `payment_hash` to this method.
///
/// Returns the invoice builder along with a [`MessageContext`] that can later be used to
/// respond to the counterparty.
///
/// [`ChannelManager::create_inbound_payment`]: crate::ln::channelmanager::ChannelManager::create_inbound_payment
pub fn create_invoice_builder_from_invoice_request_with_custom_payment_paths<'a>(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then this wouldn't be needed since you'd just call create_invoice_builder_from_invoice_request_without_keys with a custom Router.

&self, invoice_request: &'a VerifiedInvoiceRequest<DerivedSigningPubkey>,
payment_paths: Vec<BlindedPaymentPath>, payment_hash: PaymentHash,
) -> Result<(InvoiceBuilder<'a, DerivedSigningPubkey>, MessageContext), Bolt12SemanticError> {
#[cfg(feature = "std")]
let builder = invoice_request.respond_using_derived_keys(payment_paths, payment_hash);
#[cfg(not(feature = "std"))]
let builder = invoice_request.respond_using_derived_keys_no_std(
payment_paths,
payment_hash,
Duration::from_secs(self.highest_seen_timestamp.load(Ordering::Acquire) as u64),
);
let builder = builder.map(|b| InvoiceBuilder::from(b).allow_mpp())?;

let context = MessageContext::Offers(OffersContext::InboundPayment { payment_hash });

Ok((builder, context))
}

/// Enqueues the created [`InvoiceRequest`] to be sent to the counterparty.
///
/// # Payment
Expand Down Expand Up @@ -1183,6 +1296,30 @@ impl<MR: MessageRouter, L: Logger> OffersMessageFlow<MR, L> {
pending_offers_messages.push((message, instructions));
}

/// Enqueues a [`Bolt12Invoice`] to be sent back to a payer in response to an
/// [`InvoiceRequest`] that was manually handled.
///
/// This is used when [`UserConfig::manually_handle_bolt12_invoice_requests`] is set to `true`,
/// typically for LSPS2 JIT channels where custom blinded payment paths are needed.
///
/// The `context` should be obtained from
/// [`Self::create_invoice_builder_from_invoice_request_with_custom_payment_paths`], and the
/// `responder` should come from the [`Event::InvoiceRequestReceived`] event.
///
/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
/// [`UserConfig::manually_handle_bolt12_invoice_requests`]: crate::util::config::UserConfig::manually_handle_bolt12_invoice_requests
/// [`Event::InvoiceRequestReceived`]: crate::events::Event::InvoiceRequestReceived
pub fn enqueue_invoice_for_request(
&self, invoice: Bolt12Invoice, context: MessageContext, responder: Responder,
) {
let message = OffersMessage::Invoice(invoice);
let instructions = responder.respond_with_reply_path(context);
self.pending_offers_messages
.lock()
.unwrap()
.push((message, instructions.into_instructions()));
}

/// Enqueues `held_htlc_available` onion messages to be sent to the payee via the reply paths
/// contained within the provided [`StaticInvoice`].
///
Expand Down
Loading