Skip to content

Commit edf8bdb

Browse files
committed
Introduce specific InvoiceBuilders in OffersMessageFlow
This change improves type safety and architectural clarity by introducing dedicated `InvoiceBuilder` methods tied to each variant of `VerifiedInvoiceRequestEnum`. With this change, users are now required to match on the enum variant before calling the corresponding builder method. This pushes the responsibility of selecting the correct builder to the user and ensures that invalid builder usage is caught at compile time, rather than relying on runtime checks. The signing logic has also been moved from the builder to the `ChannelManager`. This shift simplifies the builder's role and aligns it with the rest of the API, where builder methods return a configurable object that can be extended before signing. The result is a more consistent and predictable interface that separates concerns cleanly and makes future maintenance easier.
1 parent 20334f5 commit edf8bdb

File tree

2 files changed

+178
-83
lines changed

2 files changed

+178
-83
lines changed

lightning/src/ln/channelmanager.rs

Lines changed: 74 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,8 @@ use crate::ln::types::ChannelId;
9494
use crate::offers::async_receive_offer_cache::AsyncReceiveOfferCache;
9595
use crate::offers::flow::{InvreqResponseInstructions, OffersMessageFlow};
9696
use crate::offers::invoice::{
97-
Bolt12Invoice, DerivedSigningPubkey, InvoiceBuilder, DEFAULT_RELATIVE_EXPIRY,
97+
Bolt12Invoice, DerivedSigningPubkey, InvoiceBuilder, UnsignedBolt12Invoice,
98+
DEFAULT_RELATIVE_EXPIRY,
9899
};
99100
use crate::offers::invoice_error::InvoiceError;
100101
use crate::offers::invoice_request::InvoiceRequest;
@@ -14347,16 +14348,79 @@ where
1434714348
},
1434814349
};
1434914350

14350-
let entropy = &*self.entropy_source;
14351-
let (response, context) = self.flow.create_response_for_invoice_request(
14352-
&self.node_signer, &self.router, entropy, invoice_request, amount_msats,
14353-
payment_hash, payment_secret, self.list_usable_channels()
14354-
);
14351+
let (result, context) = match invoice_request {
14352+
VerifiedInvoiceRequestEnum::WithKeys(request) => {
14353+
let result = self.flow.create_invoice_builder_from_invoice_request_with_keys(
14354+
&self.router,
14355+
&*self.entropy_source,
14356+
&request,
14357+
amount_msats,
14358+
payment_hash,
14359+
payment_secret,
14360+
self.list_usable_channels(),
14361+
);
1435514362

14356-
match context {
14357-
Some(context) => Some((response, responder.respond_with_reply_path(context))),
14358-
None => Some((response, responder.respond()))
14359-
}
14363+
match result {
14364+
Ok((builder, context)) => {
14365+
let res = builder
14366+
.build_and_sign(&self.secp_ctx)
14367+
.map_err(InvoiceError::from);
14368+
14369+
(res, context)
14370+
},
14371+
Err(error) => {
14372+
return Some((
14373+
OffersMessage::InvoiceError(InvoiceError::from(error)),
14374+
responder.respond(),
14375+
));
14376+
},
14377+
}
14378+
},
14379+
VerifiedInvoiceRequestEnum::WithoutKeys(request) => {
14380+
let result = self.flow.create_invoice_builder_from_invoice_request_without_keys(
14381+
&self.router,
14382+
&*self.entropy_source,
14383+
&request,
14384+
amount_msats,
14385+
payment_hash,
14386+
payment_secret,
14387+
self.list_usable_channels(),
14388+
);
14389+
14390+
match result {
14391+
Ok((builder, context)) => {
14392+
let res = builder
14393+
.build()
14394+
.map_err(InvoiceError::from)
14395+
.and_then(|invoice| {
14396+
#[cfg(c_bindings)]
14397+
let mut invoice = invoice;
14398+
invoice
14399+
.sign(|invoice: &UnsignedBolt12Invoice| self.node_signer.sign_bolt12_invoice(invoice))
14400+
.map_err(InvoiceError::from)
14401+
});
14402+
(res, context)
14403+
},
14404+
Err(error) => {
14405+
return Some((
14406+
OffersMessage::InvoiceError(InvoiceError::from(error)),
14407+
responder.respond(),
14408+
));
14409+
},
14410+
}
14411+
}
14412+
};
14413+
14414+
Some(match result {
14415+
Ok(invoice) => (
14416+
OffersMessage::Invoice(invoice),
14417+
responder.respond_with_reply_path(context),
14418+
),
14419+
Err(error) => (
14420+
OffersMessage::InvoiceError(error),
14421+
responder.respond(),
14422+
),
14423+
})
1436014424
},
1436114425
OffersMessage::Invoice(invoice) => {
1436214426
let payment_id = match self.flow.verify_bolt12_invoice(&invoice, context.as_ref()) {

lightning/src/offers/flow.rs

Lines changed: 104 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,10 @@ use crate::ln::inbound_payment;
3737
use crate::offers::async_receive_offer_cache::AsyncReceiveOfferCache;
3838
use crate::offers::invoice::{
3939
Bolt12Invoice, DerivedSigningPubkey, ExplicitSigningPubkey, InvoiceBuilder,
40-
UnsignedBolt12Invoice, DEFAULT_RELATIVE_EXPIRY,
40+
DEFAULT_RELATIVE_EXPIRY,
4141
};
42-
use crate::offers::invoice_error::InvoiceError;
4342
use crate::offers::invoice_request::{
44-
InvoiceRequest, InvoiceRequestBuilder, InvoiceRequestVerifiedFromOffer,
43+
InvoiceRequest, InvoiceRequestBuilder, InvoiceRequestVerifiedFromOffer, VerifiedInvoiceRequest,
4544
};
4645
use crate::offers::nonce::Nonce;
4746
use crate::offers::offer::{Amount, DerivedMetadata, Offer, OfferBuilder};
@@ -57,8 +56,7 @@ use crate::onion_message::messenger::{
5756
use crate::onion_message::offers::OffersMessage;
5857
use crate::onion_message::packet::OnionMessageContents;
5958
use crate::routing::router::Router;
60-
use crate::sign::{EntropySource, NodeSigner, ReceiveAuthKey};
61-
59+
use crate::sign::{EntropySource, ReceiveAuthKey};
6260
use crate::offers::static_invoice::{StaticInvoice, StaticInvoiceBuilder};
6361
use crate::sync::{Mutex, RwLock};
6462
use crate::types::payment::{PaymentHash, PaymentSecret};
@@ -902,95 +900,126 @@ where
902900
Ok(builder.into())
903901
}
904902

905-
/// Creates a response for the provided [`InvoiceRequestVerifiedFromOffer`].
903+
/// Creates an [`InvoiceBuilder`] with [`DerivedSigningPubkey`] for the
904+
/// provided [`VerifiedInvoiceRequest<DerivedSigningPubkey>`].
905+
///
906+
/// Returns the invoice builder along with a [`MessageContext`] that can
907+
/// later be used to respond to the counterparty.
906908
///
907-
/// A response can be either an [`OffersMessage::Invoice`] with additional [`MessageContext`],
908-
/// or an [`OffersMessage::InvoiceError`], depending on the [`InvoiceRequest`].
909+
/// Use this method when you want to inspect or modify the [`InvoiceBuilder`]
910+
/// before signing and generating the final [`Bolt12Invoice`].
909911
///
910-
/// An [`OffersMessage::InvoiceError`] will be generated if:
911-
/// - We fail to generate valid payment paths to include in the [`Bolt12Invoice`].
912-
/// - We fail to generate a valid signed [`Bolt12Invoice`] for the [`InvoiceRequest`].
913-
pub fn create_response_for_invoice_request<ES: Deref, NS: Deref, R: Deref>(
914-
&self, signer: &NS, router: &R, entropy_source: ES,
915-
invoice_request: InvoiceRequestVerifiedFromOffer, amount_msats: u64,
912+
/// # Errors
913+
///
914+
/// Returns a [`Bolt12SemanticError`] if:
915+
/// - User call the function with [`VerifiedInvoiceRequest<ExplicitSigningPubkey>`].
916+
/// - Valid blinded payment paths could not be generated for the [`Bolt12Invoice`].
917+
/// - The [`InvoiceBuilder`] could not be created from the [`InvoiceRequest`].
918+
pub fn create_invoice_builder_from_invoice_request_with_keys<'a, ES: Deref, R: Deref>(
919+
&'a self, router: &R, entropy_source: ES,
920+
invoice_request: &'a VerifiedInvoiceRequest<DerivedSigningPubkey>, amount_msats: u64,
916921
payment_hash: PaymentHash, payment_secret: PaymentSecret,
917922
usable_channels: Vec<ChannelDetails>,
918-
) -> (OffersMessage, Option<MessageContext>)
923+
) -> Result<(InvoiceBuilder<'a, DerivedSigningPubkey>, MessageContext), Bolt12SemanticError>
919924
where
920925
ES::Target: EntropySource,
921-
NS::Target: NodeSigner,
926+
922927
R::Target: Router,
923928
{
924929
let entropy = &*entropy_source;
925-
let secp_ctx = &self.secp_ctx;
930+
let relative_expiry = DEFAULT_RELATIVE_EXPIRY.as_secs() as u32;
931+
932+
let context = PaymentContext::Bolt12Offer(Bolt12OfferContext {
933+
offer_id: invoice_request.offer_id,
934+
invoice_request: invoice_request.fields(),
935+
});
936+
937+
let payment_paths = self
938+
.create_blinded_payment_paths(
939+
router,
940+
entropy,
941+
usable_channels,
942+
Some(amount_msats),
943+
payment_secret,
944+
context,
945+
relative_expiry,
946+
)
947+
.map_err(|_| Bolt12SemanticError::MissingPaths)?;
948+
949+
#[cfg(feature = "std")]
950+
let builder = invoice_request.respond_using_derived_keys(payment_paths, payment_hash);
951+
#[cfg(not(feature = "std"))]
952+
let builder = invoice_request.respond_using_derived_keys_no_std(
953+
payment_paths,
954+
payment_hash,
955+
Duration::from_secs(self.highest_seen_timestamp.load(Ordering::Acquire) as u64),
956+
);
957+
let builder = builder.map(|b| InvoiceBuilder::from(b).allow_mpp())?;
926958

959+
let context = MessageContext::Offers(OffersContext::InboundPayment { payment_hash });
960+
961+
Ok((builder, context))
962+
}
963+
964+
/// Creates an [`InvoiceBuilder`] with [`ExplicitSigningPubkey`] for the
965+
/// provided [`VerifiedInvoiceRequest<ExplicitSigningPubkey>`].
966+
///
967+
/// Returns the invoice builder along with a [`MessageContext`] that can
968+
/// later be used to respond to the counterparty.
969+
///
970+
/// Use this method when you want to inspect or modify the [`InvoiceBuilder`]
971+
/// before signing and generating the final [`Bolt12Invoice`].
972+
///
973+
/// # Errors
974+
///
975+
/// Returns a [`Bolt12SemanticError`] if:
976+
/// - User call the function with [`VerifiedInvoiceRequest<DerivedSigningPubkey>`].
977+
/// - Valid blinded payment paths could not be generated for the [`Bolt12Invoice`].
978+
/// - The [`InvoiceBuilder`] could not be created from the [`InvoiceRequest`].
979+
pub fn create_invoice_builder_from_invoice_request_without_keys<'a, ES: Deref, R: Deref>(
980+
&'a self, router: &R, entropy_source: ES,
981+
invoice_request: &'a VerifiedInvoiceRequest<ExplicitSigningPubkey>, amount_msats: u64,
982+
payment_hash: PaymentHash, payment_secret: PaymentSecret,
983+
usable_channels: Vec<ChannelDetails>,
984+
) -> Result<(InvoiceBuilder<'a, ExplicitSigningPubkey>, MessageContext), Bolt12SemanticError>
985+
where
986+
ES::Target: EntropySource,
987+
R::Target: Router,
988+
{
989+
let entropy = &*entropy_source;
927990
let relative_expiry = DEFAULT_RELATIVE_EXPIRY.as_secs() as u32;
928991

929992
let context = PaymentContext::Bolt12Offer(Bolt12OfferContext {
930-
offer_id: invoice_request.offer_id(),
993+
offer_id: invoice_request.offer_id,
931994
invoice_request: invoice_request.fields(),
932995
});
933996

934-
let payment_paths = match self.create_blinded_payment_paths(
935-
router,
936-
entropy,
937-
usable_channels,
938-
Some(amount_msats),
939-
payment_secret,
940-
context,
941-
relative_expiry,
942-
) {
943-
Ok(paths) => paths,
944-
Err(_) => {
945-
let error = InvoiceError::from(Bolt12SemanticError::MissingPaths);
946-
return (OffersMessage::InvoiceError(error.into()), None);
947-
},
948-
};
997+
let payment_paths = self
998+
.create_blinded_payment_paths(
999+
router,
1000+
entropy,
1001+
usable_channels,
1002+
Some(amount_msats),
1003+
payment_secret,
1004+
context,
1005+
relative_expiry,
1006+
)
1007+
.map_err(|_| Bolt12SemanticError::MissingPaths)?;
9491008

1009+
#[cfg(feature = "std")]
1010+
let builder = invoice_request.respond_with(payment_paths, payment_hash);
9501011
#[cfg(not(feature = "std"))]
951-
let created_at = Duration::from_secs(self.highest_seen_timestamp.load(Ordering::Acquire) as u64);
1012+
let builder = invoice_request.respond_with_no_std(
1013+
payment_paths,
1014+
payment_hash,
1015+
Duration::from_secs(self.highest_seen_timestamp.load(Ordering::Acquire) as u64),
1016+
);
9521017

953-
let response = match invoice_request {
954-
InvoiceRequestVerifiedFromOffer::DerivedKeys(request) => {
955-
#[cfg(feature = "std")]
956-
let builder = request.respond_using_derived_keys(payment_paths, payment_hash);
957-
#[cfg(not(feature = "std"))]
958-
let builder = request.respond_using_derived_keys_no_std(payment_paths, payment_hash, created_at);
959-
builder
960-
.map(InvoiceBuilder::<DerivedSigningPubkey>::from)
961-
.and_then(|builder| builder.allow_mpp().build_and_sign(secp_ctx))
962-
.map_err(InvoiceError::from)
963-
},
964-
InvoiceRequestVerifiedFromOffer::ExplicitKeys(request) => {
965-
#[cfg(feature = "std")]
966-
let builder = request.respond_with(payment_paths, payment_hash);
967-
#[cfg(not(feature = "std"))]
968-
let builder = request.respond_with_no_std(payment_paths, payment_hash, created_at);
969-
builder
970-
.map(InvoiceBuilder::<ExplicitSigningPubkey>::from)
971-
.and_then(|builder| builder.allow_mpp().build())
972-
.map_err(InvoiceError::from)
973-
.and_then(|invoice| {
974-
#[cfg(c_bindings)]
975-
let mut invoice = invoice;
976-
invoice
977-
.sign(|invoice: &UnsignedBolt12Invoice| {
978-
signer.sign_bolt12_invoice(invoice)
979-
})
980-
.map_err(InvoiceError::from)
981-
})
982-
},
983-
};
1018+
let builder = builder.map(|b| InvoiceBuilder::from(b).allow_mpp())?;
9841019

985-
match response {
986-
Ok(invoice) => {
987-
let context =
988-
MessageContext::Offers(OffersContext::InboundPayment { payment_hash });
1020+
let context = MessageContext::Offers(OffersContext::InboundPayment { payment_hash });
9891021

990-
(OffersMessage::Invoice(invoice), Some(context))
991-
},
992-
Err(error) => (OffersMessage::InvoiceError(error.into()), None),
993-
}
1022+
Ok((builder, context))
9941023
}
9951024

9961025
/// Enqueues the created [`InvoiceRequest`] to be sent to the counterparty.
@@ -1017,6 +1046,7 @@ where
10171046
/// valid reply paths for the counterparty to send back the corresponding [`Bolt12Invoice`]
10181047
/// or [`InvoiceError`].
10191048
///
1049+
/// [`InvoiceError`]: crate::offers::invoice_error::InvoiceError
10201050
/// [`supports_onion_messages`]: crate::types::features::Features::supports_onion_messages
10211051
pub fn enqueue_invoice_request(
10221052
&self, invoice_request: InvoiceRequest, payment_id: PaymentId, nonce: Nonce,
@@ -1062,6 +1092,7 @@ where
10621092
/// reply paths for the counterparty to send back the corresponding [`InvoiceError`] if we fail
10631093
/// to create blinded reply paths
10641094
///
1095+
/// [`InvoiceError`]: crate::offers::invoice_error::InvoiceError
10651096
/// [`supports_onion_messages`]: crate::types::features::Features::supports_onion_messages
10661097
pub fn enqueue_invoice(
10671098
&self, invoice: Bolt12Invoice, refund: &Refund, peers: Vec<MessageForwardNode>,

0 commit comments

Comments
 (0)