Skip to content

Commit 8137302

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 8c7f1b2 commit 8137302

File tree

2 files changed

+178
-85
lines changed

2 files changed

+178
-85
lines changed

lightning/src/ln/channelmanager.rs

Lines changed: 75 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -94,10 +94,11 @@ 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;
100-
use crate::offers::invoice_request::InvoiceRequest;
101+
use crate::offers::invoice_request::{InvoiceRequest, InvoiceRequestVerifiedFromOffer};
101102
use crate::offers::nonce::Nonce;
102103
use crate::offers::offer::Offer;
103104
use crate::offers::parse::Bolt12SemanticError;
@@ -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+
InvoiceRequestVerifiedFromOffer::DerivedKeys(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+
InvoiceRequestVerifiedFromOffer::ExplicitKeys(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: 103 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -37,16 +37,16 @@ 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};
4847
use crate::offers::parse::Bolt12SemanticError;
4948
use crate::offers::refund::{Refund, RefundBuilder};
49+
use crate::offers::static_invoice::{StaticInvoice, StaticInvoiceBuilder};
5050
use crate::onion_message::async_payments::{
5151
AsyncPaymentsMessage, HeldHtlcAvailable, OfferPaths, OfferPathsRequest, ServeStaticInvoice,
5252
StaticInvoicePersisted,
@@ -57,9 +57,7 @@ use crate::onion_message::messenger::{
5757
use crate::onion_message::offers::OffersMessage;
5858
use crate::onion_message::packet::OnionMessageContents;
5959
use crate::routing::router::Router;
60-
use crate::sign::{EntropySource, NodeSigner, ReceiveAuthKey};
61-
62-
use crate::offers::static_invoice::{StaticInvoice, StaticInvoiceBuilder};
60+
use crate::sign::{EntropySource, ReceiveAuthKey};
6361
use crate::sync::{Mutex, RwLock};
6462
use crate::types::payment::{PaymentHash, PaymentSecret};
6563
use crate::util::ser::Writeable;
@@ -902,95 +900,124 @@ where
902900
Ok(builder.into())
903901
}
904902

905-
/// Creates a response for the provided [`InvoiceRequestVerifiedFromOffer`].
903+
/// Creates an [`InvoiceBuilder<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.
908+
///
909+
/// Use this method when you want to inspect or modify the [`InvoiceBuilder`]
910+
/// before signing and generating the final [`Bolt12Invoice`].
906911
///
907-
/// A response can be either an [`OffersMessage::Invoice`] with additional [`MessageContext`],
908-
/// or an [`OffersMessage::InvoiceError`], depending on the [`InvoiceRequest`].
912+
/// # Errors
909913
///
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,
914+
/// Returns a [`Bolt12SemanticError`] if:
915+
/// - Valid blinded payment paths could not be generated for the [`Bolt12Invoice`].
916+
/// - The [`InvoiceBuilder`] could not be created from the [`InvoiceRequest`].
917+
pub fn create_invoice_builder_from_invoice_request_with_keys<'a, ES: Deref, R: Deref>(
918+
&'a self, router: &R, entropy_source: ES,
919+
invoice_request: &'a VerifiedInvoiceRequest<DerivedSigningPubkey>, amount_msats: u64,
916920
payment_hash: PaymentHash, payment_secret: PaymentSecret,
917921
usable_channels: Vec<ChannelDetails>,
918-
) -> (OffersMessage, Option<MessageContext>)
922+
) -> Result<(InvoiceBuilder<'a, DerivedSigningPubkey>, MessageContext), Bolt12SemanticError>
919923
where
920924
ES::Target: EntropySource,
921-
NS::Target: NodeSigner,
925+
922926
R::Target: Router,
923927
{
924928
let entropy = &*entropy_source;
925-
let secp_ctx = &self.secp_ctx;
929+
let relative_expiry = DEFAULT_RELATIVE_EXPIRY.as_secs() as u32;
930+
931+
let context = PaymentContext::Bolt12Offer(Bolt12OfferContext {
932+
offer_id: invoice_request.offer_id,
933+
invoice_request: invoice_request.fields(),
934+
});
926935

936+
let payment_paths = self
937+
.create_blinded_payment_paths(
938+
router,
939+
entropy,
940+
usable_channels,
941+
Some(amount_msats),
942+
payment_secret,
943+
context,
944+
relative_expiry,
945+
)
946+
.map_err(|_| Bolt12SemanticError::MissingPaths)?;
947+
948+
#[cfg(feature = "std")]
949+
let builder = invoice_request.respond_using_derived_keys(payment_paths, payment_hash);
950+
#[cfg(not(feature = "std"))]
951+
let builder = invoice_request.respond_using_derived_keys_no_std(
952+
payment_paths,
953+
payment_hash,
954+
Duration::from_secs(self.highest_seen_timestamp.load(Ordering::Acquire) as u64),
955+
);
956+
let builder = builder.map(|b| InvoiceBuilder::from(b).allow_mpp())?;
957+
958+
let context = MessageContext::Offers(OffersContext::InboundPayment { payment_hash });
959+
960+
Ok((builder, context))
961+
}
962+
963+
/// Creates an [`InvoiceBuilder<ExplicitSigningPubkey>`] for the
964+
/// provided [`VerifiedInvoiceRequest<ExplicitSigningPubkey>`].
965+
///
966+
/// Returns the invoice builder along with a [`MessageContext`] that can
967+
/// later be used to respond to the counterparty.
968+
///
969+
/// Use this method when you want to inspect or modify the [`InvoiceBuilder`]
970+
/// before signing and generating the final [`Bolt12Invoice`].
971+
///
972+
/// # Errors
973+
///
974+
/// Returns a [`Bolt12SemanticError`] if:
975+
/// - Valid blinded payment paths could not be generated for the [`Bolt12Invoice`].
976+
/// - The [`InvoiceBuilder`] could not be created from the [`InvoiceRequest`].
977+
pub fn create_invoice_builder_from_invoice_request_without_keys<'a, ES: Deref, R: Deref>(
978+
&'a self, router: &R, entropy_source: ES,
979+
invoice_request: &'a VerifiedInvoiceRequest<ExplicitSigningPubkey>, amount_msats: u64,
980+
payment_hash: PaymentHash, payment_secret: PaymentSecret,
981+
usable_channels: Vec<ChannelDetails>,
982+
) -> Result<(InvoiceBuilder<'a, ExplicitSigningPubkey>, MessageContext), Bolt12SemanticError>
983+
where
984+
ES::Target: EntropySource,
985+
R::Target: Router,
986+
{
987+
let entropy = &*entropy_source;
927988
let relative_expiry = DEFAULT_RELATIVE_EXPIRY.as_secs() as u32;
928989

929990
let context = PaymentContext::Bolt12Offer(Bolt12OfferContext {
930-
offer_id: invoice_request.offer_id(),
991+
offer_id: invoice_request.offer_id,
931992
invoice_request: invoice_request.fields(),
932993
});
933994

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-
};
995+
let payment_paths = self
996+
.create_blinded_payment_paths(
997+
router,
998+
entropy,
999+
usable_channels,
1000+
Some(amount_msats),
1001+
payment_secret,
1002+
context,
1003+
relative_expiry,
1004+
)
1005+
.map_err(|_| Bolt12SemanticError::MissingPaths)?;
9491006

1007+
#[cfg(feature = "std")]
1008+
let builder = invoice_request.respond_with(payment_paths, payment_hash);
9501009
#[cfg(not(feature = "std"))]
951-
let created_at = Duration::from_secs(self.highest_seen_timestamp.load(Ordering::Acquire) as u64);
1010+
let builder = invoice_request.respond_with_no_std(
1011+
payment_paths,
1012+
payment_hash,
1013+
Duration::from_secs(self.highest_seen_timestamp.load(Ordering::Acquire) as u64),
1014+
);
9521015

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-
};
1016+
let builder = builder.map(|b| InvoiceBuilder::from(b).allow_mpp())?;
9841017

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

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

9961023
/// Enqueues the created [`InvoiceRequest`] to be sent to the counterparty.
@@ -1017,6 +1044,7 @@ where
10171044
/// valid reply paths for the counterparty to send back the corresponding [`Bolt12Invoice`]
10181045
/// or [`InvoiceError`].
10191046
///
1047+
/// [`InvoiceError`]: crate::offers::invoice_error::InvoiceError
10201048
/// [`supports_onion_messages`]: crate::types::features::Features::supports_onion_messages
10211049
pub fn enqueue_invoice_request(
10221050
&self, invoice_request: InvoiceRequest, payment_id: PaymentId, nonce: Nonce,
@@ -1062,6 +1090,7 @@ where
10621090
/// reply paths for the counterparty to send back the corresponding [`InvoiceError`] if we fail
10631091
/// to create blinded reply paths
10641092
///
1093+
/// [`InvoiceError`]: crate::offers::invoice_error::InvoiceError
10651094
/// [`supports_onion_messages`]: crate::types::features::Features::supports_onion_messages
10661095
pub fn enqueue_invoice(
10671096
&self, invoice: Bolt12Invoice, refund: &Refund, peers: Vec<MessageForwardNode>,

0 commit comments

Comments
 (0)