Skip to content

Commit b31bf77

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 d58dd05 commit b31bf77

File tree

1 file changed

+205
-27
lines changed

1 file changed

+205
-27
lines changed

lightning/src/offers/static_invoice.rs

Lines changed: 205 additions & 27 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")]
@@ -40,36 +42,74 @@ use crate::prelude::*;
4042
/// Static invoices default to expiring after 24 hours.
4143
const DEFAULT_RELATIVE_EXPIRY: Duration = Duration::from_secs(3600 * 24);
4244

43-
/// A `StaticInvoice` is a reusable payment request corresponding to an [`Offer`].
45+
/// Builds a [`StaticInvoice`] from an [`Offer`].
4446
///
45-
/// A static invoice may be sent in response to an [`InvoiceRequest`] and includes all the
46-
/// information needed to pay the recipient. However, unlike [`Bolt12Invoice`]s, static invoices do
47-
/// not provide proof-of-payment. Therefore, [`Bolt12Invoice`]s should be preferred when the
48-
/// recipient is online to provide one.
47+
/// See [module-level documentation] for usage.
4948
///
50-
/// [`Offer`]: crate::offers::offer::Offer
51-
/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
52-
/// [`Bolt12Invoice`]: crate::offers::invoice::Bolt12Invoice
53-
pub struct StaticInvoice {
49+
/// This is not exported to bindings users as builder patterns don't map outside of move semantics.
50+
pub struct StaticInvoiceBuilder<'a> {
51+
offer_bytes: &'a Vec<u8>,
52+
invoice: InvoiceContents,
53+
keys: KeyPair,
54+
}
55+
56+
impl<'a> StaticInvoiceBuilder<'a> {
57+
/// Initialize a [`StaticInvoiceBuilder`] from the given [`Offer`].
58+
///
59+
/// Unless [`StaticInvoiceBuilder::relative_expiry`] is set, the invoice will expire 24 hours
60+
/// after `created_at`.
61+
pub fn for_offer_using_keys(
62+
offer: &'a Offer, payment_paths: Vec<(BlindedPayInfo, BlindedPath)>, created_at: Duration,
63+
keys: KeyPair,
64+
) -> Result<Self, Bolt12SemanticError> {
65+
let invoice = InvoiceContents::new(offer, payment_paths, created_at, keys.public_key());
66+
if invoice.payment_paths.is_empty() {
67+
return Err(Bolt12SemanticError::MissingPaths);
68+
}
69+
if invoice.offer.chains().len() > 1 {
70+
return Err(Bolt12SemanticError::UnexpectedChain);
71+
}
72+
Ok(Self { offer_bytes: &offer.bytes, invoice, keys })
73+
}
74+
75+
/// Builds a signed [`StaticInvoice`] after checking for valid semantics.
76+
pub fn build_and_sign<T: secp256k1::Signing>(
77+
self, secp_ctx: &Secp256k1<T>,
78+
) -> Result<StaticInvoice, Bolt12SemanticError> {
79+
#[cfg(feature = "std")]
80+
{
81+
if self.invoice.is_offer_expired() {
82+
return Err(Bolt12SemanticError::AlreadyExpired);
83+
}
84+
}
85+
86+
#[cfg(not(feature = "std"))]
87+
{
88+
if self.invoice.is_offer_expired_no_std(self.invoice.created_at()) {
89+
return Err(Bolt12SemanticError::AlreadyExpired);
90+
}
91+
}
92+
93+
let Self { offer_bytes, invoice, keys } = self;
94+
let unsigned_invoice = UnsignedStaticInvoice::new(&offer_bytes, invoice);
95+
let invoice = unsigned_invoice
96+
.sign(|message: &UnsignedStaticInvoice| {
97+
Ok(secp_ctx.sign_schnorr_no_aux_rand(message.tagged_hash.as_digest(), &keys))
98+
})
99+
.unwrap();
100+
Ok(invoice)
101+
}
102+
103+
invoice_builder_methods_common!(self, Self, self.invoice, Self, self, S, StaticInvoice, mut);
104+
}
105+
106+
/// A semantically valid [`StaticInvoice`] that hasn't been signed.
107+
pub struct UnsignedStaticInvoice {
54108
bytes: Vec<u8>,
55109
contents: InvoiceContents,
56-
signature: Signature,
57110
tagged_hash: TaggedHash,
58111
}
59112

60-
/// The contents of a [`StaticInvoice`] for responding to an [`Offer`].
61-
///
62-
/// [`Offer`]: crate::offers::offer::Offer
63-
struct InvoiceContents {
64-
offer: OfferContents,
65-
payment_paths: Vec<(BlindedPayInfo, BlindedPath)>,
66-
created_at: Duration,
67-
relative_expiry: Option<Duration>,
68-
fallbacks: Option<Vec<FallbackAddress>>,
69-
features: Bolt12InvoiceFeatures,
70-
signing_pubkey: PublicKey,
71-
}
72-
73113
macro_rules! invoice_accessors { ($self: ident, $contents: expr) => {
74114
/// The chain that must be used when paying the invoice. [`StaticInvoice`]s currently can only be
75115
/// created from offers that support a single chain.
@@ -138,6 +178,99 @@ macro_rules! invoice_accessors { ($self: ident, $contents: expr) => {
138178
}
139179
} }
140180

181+
impl UnsignedStaticInvoice {
182+
fn new(offer_bytes: &Vec<u8>, contents: InvoiceContents) -> Self {
183+
let mut bytes = Vec::new();
184+
WithoutLength(offer_bytes).write(&mut bytes).unwrap();
185+
contents.as_invoice_fields_tlv_stream().write(&mut bytes).unwrap();
186+
187+
let tagged_hash = TaggedHash::from_valid_tlv_stream_bytes(SIGNATURE_TAG, &bytes);
188+
Self { contents, tagged_hash, bytes }
189+
}
190+
191+
/// Signs the [`TaggedHash`] of the invoice using the given function.
192+
///
193+
/// Note: The hash computation may have included unknown, odd TLV records.
194+
pub fn sign<F: SignStaticInvoiceFn>(mut self, sign: F) -> Result<StaticInvoice, SignError> {
195+
let pubkey = self.contents.signing_pubkey;
196+
let signature = merkle::sign_message(sign, &self, pubkey)?;
197+
198+
// Append the signature TLV record to the bytes.
199+
let signature_tlv_stream = SignatureTlvStreamRef { signature: Some(&signature) };
200+
signature_tlv_stream.write(&mut self.bytes).unwrap();
201+
202+
Ok(StaticInvoice {
203+
bytes: self.bytes,
204+
contents: self.contents,
205+
signature,
206+
tagged_hash: self.tagged_hash,
207+
})
208+
}
209+
210+
invoice_accessors_common!(self, self.contents, StaticInvoice);
211+
invoice_accessors!(self, self.contents);
212+
}
213+
214+
impl AsRef<TaggedHash> for UnsignedStaticInvoice {
215+
fn as_ref(&self) -> &TaggedHash {
216+
&self.tagged_hash
217+
}
218+
}
219+
220+
/// A function for signing an [`UnsignedStaticInvoice`].
221+
pub trait SignStaticInvoiceFn {
222+
/// Signs a [`TaggedHash`] computed over the merkle root of `message`'s TLV stream.
223+
fn sign_invoice(&self, message: &UnsignedStaticInvoice) -> Result<Signature, ()>;
224+
}
225+
226+
impl<F> SignStaticInvoiceFn for F
227+
where
228+
F: Fn(&UnsignedStaticInvoice) -> Result<Signature, ()>,
229+
{
230+
fn sign_invoice(&self, message: &UnsignedStaticInvoice) -> Result<Signature, ()> {
231+
self(message)
232+
}
233+
}
234+
235+
impl<F> SignFn<UnsignedStaticInvoice> for F
236+
where
237+
F: SignStaticInvoiceFn,
238+
{
239+
fn sign(&self, message: &UnsignedStaticInvoice) -> Result<Signature, ()> {
240+
self.sign_invoice(message)
241+
}
242+
}
243+
244+
/// A `StaticInvoice` is a reusable payment request corresponding to an [`Offer`].
245+
///
246+
/// A static invoice may be sent in response to an [`InvoiceRequest`] and includes all the
247+
/// information needed to pay the recipient. However, unlike [`Bolt12Invoice`]s, static invoices do
248+
/// not provide proof-of-payment. Therefore, [`Bolt12Invoice`]s should be preferred when the
249+
/// recipient is online to provide one.
250+
///
251+
/// [`Offer`]: crate::offers::offer::Offer
252+
/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
253+
/// [`Bolt12Invoice`]: crate::offers::invoice::Bolt12Invoice
254+
pub struct StaticInvoice {
255+
bytes: Vec<u8>,
256+
contents: InvoiceContents,
257+
signature: Signature,
258+
tagged_hash: TaggedHash,
259+
}
260+
261+
/// The contents of a [`StaticInvoice`] for responding to an [`Offer`].
262+
///
263+
/// [`Offer`]: crate::offers::offer::Offer
264+
struct InvoiceContents {
265+
offer: OfferContents,
266+
payment_paths: Vec<(BlindedPayInfo, BlindedPath)>,
267+
created_at: Duration,
268+
relative_expiry: Option<Duration>,
269+
fallbacks: Option<Vec<FallbackAddress>>,
270+
features: Bolt12InvoiceFeatures,
271+
signing_pubkey: PublicKey,
272+
}
273+
141274
impl StaticInvoice {
142275
invoice_accessors_common!(self, self.contents, StaticInvoice);
143276
invoice_accessors!(self, self.contents);
@@ -154,6 +287,51 @@ impl StaticInvoice {
154287
}
155288

156289
impl InvoiceContents {
290+
#[cfg(feature = "std")]
291+
fn is_offer_expired(&self) -> bool {
292+
self.offer.is_expired()
293+
}
294+
295+
#[cfg(not(feature = "std"))]
296+
fn is_offer_expired_no_std(&self, duration_since_epoch: Duration) -> bool {
297+
self.offer.is_expired_no_std(duration_since_epoch)
298+
}
299+
300+
fn new(
301+
offer: &Offer, payment_paths: Vec<(BlindedPayInfo, BlindedPath)>, created_at: Duration,
302+
signing_pubkey: PublicKey,
303+
) -> Self {
304+
Self {
305+
offer: offer.contents.clone(),
306+
payment_paths,
307+
created_at,
308+
relative_expiry: None,
309+
fallbacks: None,
310+
features: Bolt12InvoiceFeatures::empty(),
311+
signing_pubkey,
312+
}
313+
}
314+
315+
fn as_invoice_fields_tlv_stream(&self) -> InvoiceTlvStreamRef {
316+
let features = {
317+
if self.features == Bolt12InvoiceFeatures::empty() {
318+
None
319+
} else {
320+
Some(&self.features)
321+
}
322+
};
323+
324+
InvoiceTlvStreamRef {
325+
paths: Some(Iterable(self.payment_paths.iter().map(|(_, path)| path))),
326+
blindedpay: Some(Iterable(self.payment_paths.iter().map(|(payinfo, _)| payinfo))),
327+
created_at: Some(self.created_at.as_secs()),
328+
relative_expiry: self.relative_expiry.map(|duration| duration.as_secs() as u32),
329+
fallbacks: self.fallbacks.as_ref(),
330+
features,
331+
node_id: Some(&self.signing_pubkey),
332+
}
333+
}
334+
157335
fn chain(&self) -> ChainHash {
158336
debug_assert_eq!(self.offer.chains().len(), 1);
159337
self.offer.chains().first().cloned().unwrap_or_else(|| self.offer.implied_chain())

0 commit comments

Comments
 (0)