Skip to content

Commit da645f3

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 31de292 commit da645f3

File tree

1 file changed

+193
-4
lines changed

1 file changed

+193
-4
lines changed

lightning/src/offers/static_invoice.rs

Lines changed: 193 additions & 4 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,88 @@ 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+
if offer.chains().len() > 1 {
101+
return Err(Bolt12SemanticError::UnexpectedChain);
102+
}
103+
104+
if payment_paths.is_empty() || message_paths.is_empty() || offer.paths().is_empty() {
105+
return Err(Bolt12SemanticError::MissingPaths);
106+
}
107+
108+
let offer_signing_pubkey =
109+
offer.signing_pubkey().ok_or(Bolt12SemanticError::MissingSigningPubkey)?;
110+
let signing_pubkey = keys.public_key();
111+
if signing_pubkey != offer_signing_pubkey {
112+
return Err(Bolt12SemanticError::InvalidSigningPubkey);
113+
}
114+
115+
let invoice =
116+
InvoiceContents::new(offer, payment_paths, message_paths, created_at, signing_pubkey);
117+
118+
Ok(Self { offer_bytes: &offer.bytes, invoice, keys })
119+
}
120+
121+
/// Builds a signed [`StaticInvoice`] after checking for valid semantics.
122+
pub fn build_and_sign<T: secp256k1::Signing>(
123+
self, secp_ctx: &Secp256k1<T>,
124+
) -> Result<StaticInvoice, Bolt12SemanticError> {
125+
#[cfg(feature = "std")]
126+
{
127+
if self.invoice.is_offer_expired() {
128+
return Err(Bolt12SemanticError::AlreadyExpired);
129+
}
130+
}
131+
132+
#[cfg(not(feature = "std"))]
133+
{
134+
if self.invoice.is_offer_expired_no_std(self.invoice.created_at()) {
135+
return Err(Bolt12SemanticError::AlreadyExpired);
136+
}
137+
}
138+
139+
let Self { offer_bytes, invoice, keys } = self;
140+
let unsigned_invoice = UnsignedStaticInvoice::new(&offer_bytes, invoice);
141+
let invoice = unsigned_invoice
142+
.sign(|message: &UnsignedStaticInvoice| {
143+
Ok(secp_ctx.sign_schnorr_no_aux_rand(message.tagged_hash.as_digest(), &keys))
144+
})
145+
.unwrap();
146+
Ok(invoice)
147+
}
148+
149+
invoice_builder_methods_common!(self, Self, self.invoice, Self, self, S, StaticInvoice, mut);
150+
}
151+
152+
/// A semantically valid [`StaticInvoice`] that hasn't been signed.
153+
pub struct UnsignedStaticInvoice {
154+
bytes: Vec<u8>,
155+
contents: InvoiceContents,
156+
tagged_hash: TaggedHash,
157+
}
158+
75159
macro_rules! invoice_accessors { ($self: ident, $contents: expr) => {
76160
/// The chain that must be used when paying the invoice. [`StaticInvoice`]s currently can only be
77161
/// created from offers that support a single chain.
@@ -147,6 +231,64 @@ macro_rules! invoice_accessors { ($self: ident, $contents: expr) => {
147231
}
148232
} }
149233

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

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

0 commit comments

Comments
 (0)