Skip to content

Commit e22c269

Browse files
Builder for creating static invoices from offers
Add a builder for creating static invoices for an offer. Building produces a semantically valid static invoice for the offer, which can then be signed with the key associated with the offer's signing pubkey.
1 parent 1d82d6a commit e22c269

File tree

1 file changed

+211
-6
lines changed

1 file changed

+211
-6
lines changed

lightning/src/offers/static_invoice.rs

Lines changed: 211 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,11 @@ use crate::offers::invoice::{
1717
construct_payment_paths, filter_fallbacks, BlindedPathIter, BlindedPayInfo, BlindedPayInfoIter,
1818
FallbackAddress, SIGNATURE_TAG,
1919
};
20-
use crate::offers::invoice_macros::invoice_accessors_common;
21-
use crate::offers::merkle::{self, SignatureTlvStream, TaggedHash};
22-
use crate::offers::offer::{Amount, OfferContents, OfferTlvStream, Quantity};
20+
use crate::offers::invoice_macros::{invoice_accessors_common, invoice_builder_methods_common};
21+
use crate::offers::merkle::{
22+
self, SignError, SignFn, SignatureTlvStream, SignatureTlvStreamRef, TaggedHash,
23+
};
24+
use crate::offers::offer::{Amount, Offer, OfferContents, OfferTlvStream, Quantity};
2325
use crate::offers::parse::{Bolt12ParseError, Bolt12SemanticError, ParsedMessage};
2426
use crate::util::ser::{
2527
HighZeroBytesDroppedBigSize, Iterable, SeekReadable, WithoutLength, Writeable, Writer,
@@ -28,7 +30,7 @@ use crate::util::string::PrintableString;
2830
use bitcoin::address::Address;
2931
use bitcoin::blockdata::constants::ChainHash;
3032
use bitcoin::secp256k1::schnorr::Signature;
31-
use bitcoin::secp256k1::PublicKey;
33+
use bitcoin::secp256k1::{self, KeyPair, PublicKey, Secp256k1};
3234
use core::time::Duration;
3335

3436
#[cfg(feature = "std")]
@@ -72,6 +74,90 @@ struct InvoiceContents {
7274
message_paths: Vec<BlindedPath>,
7375
}
7476

77+
/// Builds a [`StaticInvoice`] from an [`Offer`].
78+
///
79+
/// See [module-level documentation] for usage.
80+
///
81+
/// [`Offer`]: crate::offers::offer::Offer
82+
/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
83+
/// [`Bolt12Invoice`]: crate::offers::invoice::Bolt12Invoice
84+
/// This is not exported to bindings users as builder patterns don't map outside of move semantics.
85+
pub struct StaticInvoiceBuilder<'a> {
86+
offer_bytes: &'a Vec<u8>,
87+
invoice: InvoiceContents,
88+
keys: KeyPair,
89+
}
90+
91+
impl<'a> StaticInvoiceBuilder<'a> {
92+
/// Initialize a [`StaticInvoiceBuilder`] from the given [`Offer`].
93+
///
94+
/// Unless [`StaticInvoiceBuilder::relative_expiry`] is set, the invoice will expire 24 hours
95+
/// after `created_at`.
96+
pub fn for_offer_using_keys(
97+
offer: &'a Offer, payment_paths: Vec<(BlindedPayInfo, BlindedPath)>,
98+
message_paths: Vec<BlindedPath>, created_at: Duration, keys: KeyPair,
99+
) -> Result<Self, Bolt12SemanticError> {
100+
let offer_signing_pubkey =
101+
offer.signing_pubkey().ok_or(Bolt12SemanticError::MissingSigningPubkey)?;
102+
let signing_pubkey = keys.public_key();
103+
if signing_pubkey != offer_signing_pubkey {
104+
return Err(Bolt12SemanticError::InvalidSigningPubkey);
105+
}
106+
107+
let invoice =
108+
InvoiceContents::new(offer, payment_paths, message_paths, created_at, signing_pubkey);
109+
110+
if invoice.payment_paths.is_empty()
111+
|| invoice.message_paths.is_empty()
112+
|| offer.paths().is_empty()
113+
{
114+
return Err(Bolt12SemanticError::MissingPaths);
115+
}
116+
if invoice.offer.chains().len() > 1 {
117+
return Err(Bolt12SemanticError::UnexpectedChain);
118+
}
119+
120+
Ok(Self { offer_bytes: &offer.bytes, invoice, keys })
121+
}
122+
123+
/// Builds a signed [`StaticInvoice`] after checking for valid semantics.
124+
pub fn build_and_sign<T: secp256k1::Signing>(
125+
self, secp_ctx: &Secp256k1<T>,
126+
) -> Result<StaticInvoice, Bolt12SemanticError> {
127+
#[cfg(feature = "std")]
128+
{
129+
if self.invoice.is_offer_expired() {
130+
return Err(Bolt12SemanticError::AlreadyExpired);
131+
}
132+
}
133+
134+
#[cfg(not(feature = "std"))]
135+
{
136+
if self.invoice.is_offer_expired_no_std(self.invoice.created_at()) {
137+
return Err(Bolt12SemanticError::AlreadyExpired);
138+
}
139+
}
140+
141+
let Self { offer_bytes, invoice, keys } = self;
142+
let unsigned_invoice = UnsignedStaticInvoice::new(&offer_bytes, invoice);
143+
let invoice = unsigned_invoice
144+
.sign(|message: &UnsignedStaticInvoice| {
145+
Ok(secp_ctx.sign_schnorr_no_aux_rand(message.tagged_hash.as_digest(), &keys))
146+
})
147+
.unwrap();
148+
Ok(invoice)
149+
}
150+
151+
invoice_builder_methods_common!(self, Self, self.invoice, Self, self, S, StaticInvoice, mut);
152+
}
153+
154+
/// A semantically valid [`StaticInvoice`] that hasn't been signed.
155+
pub struct UnsignedStaticInvoice {
156+
bytes: Vec<u8>,
157+
contents: InvoiceContents,
158+
tagged_hash: TaggedHash,
159+
}
160+
75161
macro_rules! invoice_accessors { ($self: ident, $contents: expr) => {
76162
/// The chain that must be used when paying the invoice. [`StaticInvoice`]s currently can only be
77163
/// created from offers that support a single chain.
@@ -147,6 +233,64 @@ macro_rules! invoice_accessors { ($self: ident, $contents: expr) => {
147233
}
148234
} }
149235

236+
impl UnsignedStaticInvoice {
237+
fn new(offer_bytes: &Vec<u8>, contents: InvoiceContents) -> Self {
238+
let mut bytes = Vec::new();
239+
WithoutLength(offer_bytes).write(&mut bytes).unwrap();
240+
contents.as_invoice_fields_tlv_stream().write(&mut bytes).unwrap();
241+
242+
let tagged_hash = TaggedHash::from_valid_tlv_stream_bytes(SIGNATURE_TAG, &bytes);
243+
Self { contents, tagged_hash, bytes }
244+
}
245+
246+
/// Signs the [`TaggedHash`] of the invoice using the given function.
247+
///
248+
/// Note: The hash computation may have included unknown, odd TLV records.
249+
pub fn sign<F: SignStaticInvoiceFn>(mut self, sign: F) -> Result<StaticInvoice, SignError> {
250+
let pubkey = self.contents.signing_pubkey;
251+
let signature = merkle::sign_message(sign, &self, pubkey)?;
252+
253+
// Append the signature TLV record to the bytes.
254+
let signature_tlv_stream = SignatureTlvStreamRef { signature: Some(&signature) };
255+
signature_tlv_stream.write(&mut self.bytes).unwrap();
256+
257+
Ok(StaticInvoice { bytes: self.bytes, contents: self.contents, signature })
258+
}
259+
260+
invoice_accessors_common!(self, self.contents, StaticInvoice);
261+
invoice_accessors!(self, self.contents);
262+
}
263+
264+
impl AsRef<TaggedHash> for UnsignedStaticInvoice {
265+
fn as_ref(&self) -> &TaggedHash {
266+
&self.tagged_hash
267+
}
268+
}
269+
270+
/// A function for signing an [`UnsignedStaticInvoice`].
271+
pub trait SignStaticInvoiceFn {
272+
/// Signs a [`TaggedHash`] computed over the merkle root of `message`'s TLV stream.
273+
fn sign_invoice(&self, message: &UnsignedStaticInvoice) -> Result<Signature, ()>;
274+
}
275+
276+
impl<F> SignStaticInvoiceFn for F
277+
where
278+
F: Fn(&UnsignedStaticInvoice) -> Result<Signature, ()>,
279+
{
280+
fn sign_invoice(&self, message: &UnsignedStaticInvoice) -> Result<Signature, ()> {
281+
self(message)
282+
}
283+
}
284+
285+
impl<F> SignFn<UnsignedStaticInvoice> for F
286+
where
287+
F: SignStaticInvoiceFn,
288+
{
289+
fn sign(&self, message: &UnsignedStaticInvoice) -> Result<Signature, ()> {
290+
self.sign_invoice(message)
291+
}
292+
}
293+
150294
impl StaticInvoice {
151295
invoice_accessors_common!(self, self.contents, StaticInvoice);
152296
invoice_accessors!(self, self.contents);
@@ -158,6 +302,53 @@ impl StaticInvoice {
158302
}
159303

160304
impl InvoiceContents {
305+
#[cfg(feature = "std")]
306+
fn is_offer_expired(&self) -> bool {
307+
self.offer.is_expired()
308+
}
309+
310+
#[cfg(not(feature = "std"))]
311+
fn is_offer_expired_no_std(&self, duration_since_epoch: Duration) -> bool {
312+
self.offer.is_expired_no_std(duration_since_epoch)
313+
}
314+
315+
fn new(
316+
offer: &Offer, payment_paths: Vec<(BlindedPayInfo, BlindedPath)>,
317+
message_paths: Vec<BlindedPath>, created_at: Duration, signing_pubkey: PublicKey,
318+
) -> Self {
319+
Self {
320+
offer: offer.contents.clone(),
321+
payment_paths,
322+
message_paths,
323+
created_at,
324+
relative_expiry: None,
325+
fallbacks: None,
326+
features: Bolt12InvoiceFeatures::empty(),
327+
signing_pubkey,
328+
}
329+
}
330+
331+
fn as_invoice_fields_tlv_stream(&self) -> InvoiceTlvStreamRef {
332+
let features = {
333+
if self.features == Bolt12InvoiceFeatures::empty() {
334+
None
335+
} else {
336+
Some(&self.features)
337+
}
338+
};
339+
340+
InvoiceTlvStreamRef {
341+
payment_paths: Some(Iterable(self.payment_paths.iter().map(|(_, path)| path))),
342+
message_paths: Some(self.message_paths.as_ref()),
343+
blindedpay: Some(Iterable(self.payment_paths.iter().map(|(payinfo, _)| payinfo))),
344+
created_at: Some(self.created_at.as_secs()),
345+
relative_expiry: self.relative_expiry.map(|duration| duration.as_secs() as u32),
346+
fallbacks: self.fallbacks.as_ref(),
347+
features,
348+
node_id: Some(&self.signing_pubkey),
349+
}
350+
}
351+
161352
fn chain(&self) -> ChainHash {
162353
debug_assert_eq!(self.offer.chains().len(), 1);
163354
self.offer.chains().first().cloned().unwrap_or_else(|| self.offer.implied_chain())
@@ -292,7 +483,7 @@ impl TryFrom<ParsedMessage<FullInvoiceTlvStream>> for StaticInvoice {
292483
let pubkey = contents.signing_pubkey;
293484
merkle::verify_signature(&signature, &tagged_hash, pubkey)?;
294485

295-
Ok(StaticInvoice { bytes, contents, signature, tagged_hash })
486+
Ok(StaticInvoice { bytes, contents, signature })
296487
}
297488
}
298489

@@ -328,9 +519,23 @@ impl TryFrom<(OfferTlvStream, InvoiceTlvStream)> for InvoiceContents {
328519

329520
let signing_pubkey = match node_id {
330521
None => return Err(Bolt12SemanticError::MissingSigningPubkey),
331-
Some(node_id) => node_id,
522+
Some(node_id) => {
523+
let offer_node_id =
524+
offer_tlv_stream.node_id.ok_or(Bolt12SemanticError::MissingSigningPubkey)?;
525+
if node_id != offer_node_id {
526+
return Err(Bolt12SemanticError::InvalidSigningPubkey);
527+
}
528+
node_id
529+
},
332530
};
333531

532+
if offer_tlv_stream.paths.is_none() {
533+
return Err(Bolt12SemanticError::MissingPaths);
534+
}
535+
if offer_tlv_stream.chains.as_ref().map_or(0, |chains| chains.len()) > 1 {
536+
return Err(Bolt12SemanticError::UnexpectedChain);
537+
}
538+
334539
Ok(InvoiceContents {
335540
offer: OfferContents::try_from(offer_tlv_stream)?,
336541
payment_paths,

0 commit comments

Comments
 (0)