Introduce specific InvoiceBuilders in OffersMessageFlow
What changed, and why it matters
This commit is a code cleanup and API redesign in the Lightning Dev Kit library. It splits one large internal method into two smaller, type-specific methods and moves invoice signing from a helper into the main ChannelManager. The change is described by the authors as improving compile-time safety and making the API more consistent. There is no indication in the commit that it fixes a security bug or vulnerability.
No security action required. Treat as a normal API refactor. Review downstream consumers for compile errors due to the changed method signatures.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The patch refactors OffersMessageFlow::create_response_for_invoice_request into create_invoice_builder_from_invoice_request_with_keys and create_invoice_builder_from_invoice_request_without_keys. It requires callers to match on VerifiedInvoiceRequest
Changed components
lightning/src/ln/channelmanager.rslightning/src/offers/flow.rsInspect captured patch +178 / −85
diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs
index ca10609..7f6e963 100644
--- a/lightning/src/ln/channelmanager.rs
+++ b/lightning/src/ln/channelmanager.rs
@@ -93,10 +93,11 @@ use crate::ln::types::ChannelId;
use crate::offers::async_receive_offer_cache::AsyncReceiveOfferCache;
use crate::offers::flow::{HeldHtlcReplyPath, InvreqResponseInstructions, OffersMessageFlow};
use crate::offers::invoice::{
- Bolt12Invoice, DerivedSigningPubkey, InvoiceBuilder, DEFAULT_RELATIVE_EXPIRY,
+ Bolt12Invoice, DerivedSigningPubkey, InvoiceBuilder, UnsignedBolt12Invoice,
+ DEFAULT_RELATIVE_EXPIRY,
};
use crate::offers::invoice_error::InvoiceError;
-use crate::offers::invoice_request::InvoiceRequest;
+use crate::offers::invoice_request::{InvoiceRequest, InvoiceRequestVerifiedFromOffer};
use crate::offers::nonce::Nonce;
use crate::offers::offer::{Offer, OfferFromHrn};
use crate::offers::parse::Bolt12SemanticError;
@@ -14893,16 +14894,79 @@ where
},
};
- let entropy = &*self.entropy_source;
- let (response, context) = self.flow.create_response_for_invoice_request(
- &self.node_signer, &self.router, entropy, invoice_request, amount_msats,
- payment_hash, payment_secret, self.list_usable_channels()
- );
+ let (result, context) = match invoice_request {
+ InvoiceRequestVerifiedFromOffer::DerivedKeys(request) => {
+ let result = self.flow.create_invoice_builder_from_invoice_request_with_keys(
+ &self.router,
+ &*self.entropy_source,
+ &request,
+ amount_msats,
+ payment_hash,
+ payment_secret,
+ self.list_usable_channels(),
+ );
- match context {
- Some(context) => Some((response, responder.respond_with_reply_path(context))),
- None => Some((response, responder.respond()))
- }
+ match result {
+ Ok((builder, context)) => {
+ let res = builder
+ .build_and_sign(&self.secp_ctx)
+ .map_err(InvoiceError::from);
+
+ (res, context)
+ },
+ Err(error) => {
+ return Some((
+ OffersMessage::InvoiceError(InvoiceError::from(error)),
+ responder.respond(),
+ ));
+ },
+ }
+ },
+ InvoiceRequestVerifiedFromOffer::ExplicitKeys(request) => {
+ let result = self.flow.create_invoice_builder_from_invoice_request_without_keys(
+ &self.router,
+ &*self.entropy_source,
+ &request,
+ amount_msats,
+ payment_hash,
+ payment_secret,
+ self.list_usable_channels(),
+ );
+
+ match result {
+ Ok((builder, context)) => {
+ let res = builder
+ .build()
+ .map_err(InvoiceError::from)
+ .and_then(|invoice| {
+ #[cfg(c_bindings)]
+ let mut invoice = invoice;
+ invoice
+ .sign(|invoice: &UnsignedBolt12Invoice| self.node_signer.sign_bolt12_invoice(invoice))
+ .map_err(InvoiceError::from)
+ });
+ (res, context)
+ },
+ Err(error) => {
+ return Some((
+ OffersMessage::InvoiceError(InvoiceError::from(error)),
+ responder.respond(),
+ ));
+ },
+ }
+ }
+ };
+
+ Some(match result {
+ Ok(invoice) => (
+ OffersMessage::Invoice(invoice),
+ responder.respond_with_reply_path(context),
+ ),
+ Err(error) => (
+ OffersMessage::InvoiceError(error),
+ responder.respond(),
+ ),
+ })
},
OffersMessage::Invoice(invoice) => {
let payment_id = match self.flow.verify_bolt12_invoice(&invoice, context.as_ref()) {
diff --git a/lightning/src/offers/flow.rs b/lightning/src/offers/flow.rs
index 5074459..417a13c 100644
--- a/lightning/src/offers/flow.rs
+++ b/lightning/src/offers/flow.rs
@@ -37,16 +37,16 @@ use crate::ln::inbound_payment;
use crate::offers::async_receive_offer_cache::AsyncReceiveOfferCache;
use crate::offers::invoice::{
Bolt12Invoice, DerivedSigningPubkey, ExplicitSigningPubkey, InvoiceBuilder,
- UnsignedBolt12Invoice, DEFAULT_RELATIVE_EXPIRY,
+ DEFAULT_RELATIVE_EXPIRY,
};
-use crate::offers::invoice_error::InvoiceError;
use crate::offers::invoice_request::{
- InvoiceRequest, InvoiceRequestBuilder, InvoiceRequestVerifiedFromOffer,
+ InvoiceRequest, InvoiceRequestBuilder, InvoiceRequestVerifiedFromOffer, VerifiedInvoiceRequest,
};
use crate::offers::nonce::Nonce;
use crate::offers::offer::{Amount, DerivedMetadata, Offer, OfferBuilder};
use crate::offers::parse::Bolt12SemanticError;
use crate::offers::refund::{Refund, RefundBuilder};
+use crate::offers::static_invoice::{StaticInvoice, StaticInvoiceBuilder};
use crate::onion_message::async_payments::{
AsyncPaymentsMessage, HeldHtlcAvailable, OfferPaths, OfferPathsRequest, ServeStaticInvoice,
StaticInvoicePersisted,
@@ -57,9 +57,7 @@ use crate::onion_message::messenger::{
use crate::onion_message::offers::OffersMessage;
use crate::onion_message::packet::OnionMessageContents;
use crate::routing::router::Router;
-use crate::sign::{EntropySource, NodeSigner, ReceiveAuthKey};
-
-use crate::offers::static_invoice::{StaticInvoice, StaticInvoiceBuilder};
+use crate::sign::{EntropySource, ReceiveAuthKey};
use crate::sync::{Mutex, RwLock};
use crate::types::payment::{PaymentHash, PaymentSecret};
use crate::util::logger::Logger;
@@ -939,95 +937,124 @@ where
Ok(builder.into())
}
- /// Creates a response for the provided [`InvoiceRequestVerifiedFromOffer`].
+ /// Creates an [`InvoiceBuilder<DerivedSigningPubkey>`] for the
+ /// provided [`VerifiedInvoiceRequest<DerivedSigningPubkey>`].
+ ///
+ /// Returns the invoice builder along with a [`MessageContext`] that can
+ /// later be used to respond to the counterparty.
+ ///
+ /// Use this method when you want to inspect or modify the [`InvoiceBuilder`]
+ /// before signing and generating the final [`Bolt12Invoice`].
///
- /// A response can be either an [`OffersMessage::Invoice`] with additional [`MessageContext`],
- /// or an [`OffersMessage::InvoiceError`], depending on the [`InvoiceRequest`].
+ /// # Errors
///
- /// An [`OffersMessage::InvoiceError`] will be generated if:
- /// - We fail to generate valid payment paths to include in the [`Bolt12Invoice`].
- /// - We fail to generate a valid signed [`Bolt12Invoice`] for the [`InvoiceRequest`].
- pub fn create_response_for_invoice_request<ES: Deref, NS: Deref, R: Deref>(
- &self, signer: &NS, router: &R, entropy_source: ES,
- invoice_request: InvoiceRequestVerifiedFromOffer, amount_msats: u64,
+ /// Returns a [`Bolt12SemanticError`] if:
+ /// - Valid blinded payment paths could not be generated for the [`Bolt12Invoice`].
+ /// - The [`InvoiceBuilder`] could not be created from the [`InvoiceRequest`].
+ pub fn create_invoice_builder_from_invoice_request_with_keys<'a, ES: Deref, R: Deref>(
+ &self, router: &R, entropy_source: ES,
+ invoice_request: &'a VerifiedInvoiceRequest<DerivedSigningPubkey>, amount_msats: u64,
payment_hash: PaymentHash, payment_secret: PaymentSecret,
usable_channels: Vec<ChannelDetails>,
- ) -> (OffersMessage, Option<MessageContext>)
+ ) -> Result<(InvoiceBuilder<'a, DerivedSigningPubkey>, MessageContext), Bolt12SemanticError>
where
ES::Target: EntropySource,
- NS::Target: NodeSigner,
+
R::Target: Router,
{
let entropy = &*entropy_source;
- let secp_ctx = &self.secp_ctx;
+ let relative_expiry = DEFAULT_RELATIVE_EXPIRY.as_secs() as u32;
+
+ let context = PaymentContext::Bolt12Offer(Bolt12OfferContext {
+ offer_id: invoice_request.offer_id,
+ invoice_request: invoice_request.fields(),
+ });
+
+ let payment_paths = self
+ .create_blinded_payment_paths(
+ router,
+ entropy,
+ usable_channels,
+ Some(amount_msats),
+ payment_secret,
+ context,
+ relative_expiry,
+ )
+ .map_err(|_| Bolt12SemanticError::MissingPaths)?;
+
+ #[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))
+ }
+
+ /// Creates an [`InvoiceBuilder<ExplicitSigningPubkey>`] for the
+ /// provided [`VerifiedInvoiceRequest<ExplicitSigningPubkey>`].
+ ///
+ /// Returns the invoice builder along with a [`MessageContext`] that can
+ /// later be used to respond to the counterparty.
+ ///
+ /// Use this method when you want to inspect or modify the [`InvoiceBuilder`]
+ /// before signing and generating the final [`Bolt12Invoice`].
+ ///
+ /// # Errors
+ ///
+ /// Returns a [`Bolt12SemanticError`] if:
+ /// - Valid blinded payment paths could not be generated for the [`Bolt12Invoice`].
+ /// - The [`InvoiceBuilder`] could not be created from the [`InvoiceRequest`].
+ pub fn create_invoice_builder_from_invoice_request_without_keys<'a, ES: Deref, R: Deref>(
+ &self, router: &R, entropy_source: ES,
+ invoice_request: &'a VerifiedInvoiceRequest<ExplicitSigningPubkey>, amount_msats: u64,
+ payment_hash: PaymentHash, payment_secret: PaymentSecret,
+ usable_channels: Vec<ChannelDetails>,
+ ) -> Result<(InvoiceBuilder<'a, ExplicitSigningPubkey>, MessageContext), Bolt12SemanticError>
+ where
+ ES::Target: EntropySource,
+ R::Target: Router,
+ {
+ let entropy = &*entropy_source;
let relative_expiry = DEFAULT_RELATIVE_EXPIRY.as_secs() as u32;
let context = PaymentContext::Bolt12Offer(Bolt12OfferContext {
- offer_id: invoice_request.offer_id(),
+ offer_id: invoice_request.offer_id,
invoice_request: invoice_request.fields(),
});
- let payment_paths = match self.create_blinded_payment_paths(
- router,
- entropy,
- usable_channels,
- Some(amount_msats),
- payment_secret,
- context,
- relative_expiry,
- ) {
- Ok(paths) => paths,
- Err(_) => {
- let error = InvoiceError::from(Bolt12SemanticError::MissingPaths);
- return (OffersMessage::InvoiceError(error.into()), None);
- },
- };
+ let payment_paths = self
+ .create_blinded_payment_paths(
+ router,
+ entropy,
+ usable_channels,
+ Some(amount_msats),
+ payment_secret,
+ context,
+ relative_expiry,
+ )
+ .map_err(|_| Bolt12SemanticError::MissingPaths)?;
+ #[cfg(feature = "std")]
+ let builder = invoice_request.respond_with(payment_paths, payment_hash);
#[cfg(not(feature = "std"))]
- let created_at = Duration::from_secs(self.highest_seen_timestamp.load(Ordering::Acquire) as u64);
+ let builder = invoice_request.respond_with_no_std(
+ payment_paths,
+ payment_hash,
+ Duration::from_secs(self.highest_seen_timestamp.load(Ordering::Acquire) as u64),
+ );
- let response = match invoice_request {
- InvoiceRequestVerifiedFromOffer::DerivedKeys(request) => {
- #[cfg(feature = "std")]
- let builder = request.respond_using_derived_keys(payment_paths, payment_hash);
- #[cfg(not(feature = "std"))]
- let builder = request.respond_using_derived_keys_no_std(payment_paths, payment_hash, created_at);
- builder
- .map(InvoiceBuilder::<DerivedSigningPubkey>::from)
- .and_then(|builder| builder.allow_mpp().build_and_sign(secp_ctx))
- .map_err(InvoiceError::from)
- },
- InvoiceRequestVerifiedFromOffer::ExplicitKeys(request) => {
- #[cfg(feature = "std")]
- let builder = request.respond_with(payment_paths, payment_hash);
- #[cfg(not(feature = "std"))]
- let builder = request.respond_with_no_std(payment_paths, payment_hash, created_at);
- builder
- .map(InvoiceBuilder::<ExplicitSigningPubkey>::from)
- .and_then(|builder| builder.allow_mpp().build())
- .map_err(InvoiceError::from)
- .and_then(|invoice| {
- #[cfg(c_bindings)]
- let mut invoice = invoice;
- invoice
- .sign(|invoice: &UnsignedBolt12Invoice| {
- signer.sign_bolt12_invoice(invoice)
- })
- .map_err(InvoiceError::from)
- })
- },
- };
+ let builder = builder.map(|b| InvoiceBuilder::from(b).allow_mpp())?;
- match response {
- Ok(invoice) => {
- let context =
- MessageContext::Offers(OffersContext::InboundPayment { payment_hash });
+ let context = MessageContext::Offers(OffersContext::InboundPayment { payment_hash });
- (OffersMessage::Invoice(invoice), Some(context))
- },
- Err(error) => (OffersMessage::InvoiceError(error.into()), None),
- }
+ Ok((builder, context))
}
/// Enqueues the created [`InvoiceRequest`] to be sent to the counterparty.
@@ -1054,6 +1081,7 @@ where
/// valid reply paths for the counterparty to send back the corresponding [`Bolt12Invoice`]
/// or [`InvoiceError`].
///
+ /// [`InvoiceError`]: crate::offers::invoice_error::InvoiceError
/// [`supports_onion_messages`]: crate::types::features::Features::supports_onion_messages
pub fn enqueue_invoice_request(
&self, invoice_request: InvoiceRequest, payment_id: PaymentId, nonce: Nonce,
@@ -1099,6 +1127,7 @@ where
/// reply paths for the counterparty to send back the corresponding [`InvoiceError`] if we fail
/// to create blinded reply paths
///
+ /// [`InvoiceError`]: crate::offers::invoice_error::InvoiceError
/// [`supports_onion_messages`]: crate::types::features::Features::supports_onion_messages
pub fn enqueue_invoice(
&self, invoice: Bolt12Invoice, refund: &Refund, peers: Vec<MessageForwardNode>,
Why this scored 12/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.