Skip to content

Commit c6c7047

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 134b83b commit c6c7047

File tree

2 files changed

+220
-4
lines changed

2 files changed

+220
-4
lines changed

lightning/src/offers/offer.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -664,6 +664,12 @@ impl Offer {
664664
pub fn expects_quantity(&self) -> bool {
665665
self.contents.expects_quantity()
666666
}
667+
668+
pub(super) fn verify<T: secp256k1::Signing>(
669+
&self, key: &ExpandedKey, secp_ctx: &Secp256k1<T>
670+
) -> Result<(OfferId, Option<Keypair>), ()> {
671+
self.contents.verify(&self.bytes, key, secp_ctx)
672+
}
667673
}
668674

669675
macro_rules! request_invoice_derived_payer_id { ($self: ident, $builder: ty) => {

lightning/src/offers/static_invoice.rs

Lines changed: 214 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,20 @@
1212
use crate::blinded_path::BlindedPath;
1313
use crate::io;
1414
use crate::ln::features::{Bolt12InvoiceFeatures, OfferFeatures};
15+
use crate::ln::inbound_payment::ExpandedKey;
1516
use crate::ln::msgs::DecodeError;
1617
use crate::offers::invoice::{
1718
check_invoice_signing_pubkey, construct_payment_paths, filter_fallbacks, BlindedPathIter,
1819
BlindedPayInfo, BlindedPayInfoIter, FallbackAddress, InvoiceTlvStream, InvoiceTlvStreamRef,
1920
SIGNATURE_TAG,
2021
};
21-
use crate::offers::invoice_macros::invoice_accessors_common;
22-
use crate::offers::merkle::{self, SignatureTlvStream, TaggedHash};
23-
use crate::offers::offer::{Amount, OfferContents, OfferTlvStream, Quantity};
22+
use crate::offers::invoice_macros::{invoice_accessors_common, invoice_builder_methods_common};
23+
use crate::offers::merkle::{
24+
self, SignError, SignFn, SignatureTlvStream, SignatureTlvStreamRef, TaggedHash,
25+
};
26+
use crate::offers::offer::{
27+
Amount, Offer, OfferContents, OfferTlvStream, OfferTlvStreamRef, Quantity,
28+
};
2429
use crate::offers::parse::{Bolt12ParseError, Bolt12SemanticError, ParsedMessage};
2530
use crate::util::ser::{
2631
HighZeroBytesDroppedBigSize, Iterable, SeekReadable, WithoutLength, Writeable, Writer,
@@ -29,7 +34,7 @@ use crate::util::string::PrintableString;
2934
use bitcoin::address::Address;
3035
use bitcoin::blockdata::constants::ChainHash;
3136
use bitcoin::secp256k1::schnorr::Signature;
32-
use bitcoin::secp256k1::PublicKey;
37+
use bitcoin::secp256k1::{self, Keypair, PublicKey, Secp256k1};
3338
use core::time::Duration;
3439

3540
#[cfg(feature = "std")]
@@ -73,6 +78,96 @@ struct InvoiceContents {
7378
message_paths: Vec<BlindedPath>,
7479
}
7580

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

246+
impl UnsignedStaticInvoice {
247+
fn new(offer_bytes: &Vec<u8>, contents: InvoiceContents) -> Self {
248+
let (_, invoice_tlv_stream) = contents.as_tlv_stream();
249+
let offer_bytes = WithoutLength(offer_bytes);
250+
let unsigned_tlv_stream = (offer_bytes, invoice_tlv_stream);
251+
252+
let mut bytes = Vec::new();
253+
unsigned_tlv_stream.write(&mut bytes).unwrap();
254+
255+
let tagged_hash = TaggedHash::from_valid_tlv_stream_bytes(SIGNATURE_TAG, &bytes);
256+
257+
Self { contents, tagged_hash, bytes }
258+
}
259+
260+
/// Signs the [`TaggedHash`] of the invoice using the given function.
261+
///
262+
/// Note: The hash computation may have included unknown, odd TLV records.
263+
pub fn sign<F: SignStaticInvoiceFn>(mut self, sign: F) -> Result<StaticInvoice, SignError> {
264+
let pubkey = self.contents.signing_pubkey;
265+
let signature = merkle::sign_message(sign, &self, pubkey)?;
266+
267+
// Append the signature TLV record to the bytes.
268+
let signature_tlv_stream = SignatureTlvStreamRef { signature: Some(&signature) };
269+
signature_tlv_stream.write(&mut self.bytes).unwrap();
270+
271+
Ok(StaticInvoice { bytes: self.bytes, contents: self.contents, signature })
272+
}
273+
274+
invoice_accessors_common!(self, self.contents, StaticInvoice);
275+
invoice_accessors!(self, self.contents);
276+
}
277+
278+
impl AsRef<TaggedHash> for UnsignedStaticInvoice {
279+
fn as_ref(&self) -> &TaggedHash {
280+
&self.tagged_hash
281+
}
282+
}
283+
284+
/// A function for signing an [`UnsignedStaticInvoice`].
285+
pub trait SignStaticInvoiceFn {
286+
/// Signs a [`TaggedHash`] computed over the merkle root of `message`'s TLV stream.
287+
fn sign_invoice(&self, message: &UnsignedStaticInvoice) -> Result<Signature, ()>;
288+
}
289+
290+
impl<F> SignStaticInvoiceFn for F
291+
where
292+
F: Fn(&UnsignedStaticInvoice) -> Result<Signature, ()>,
293+
{
294+
fn sign_invoice(&self, message: &UnsignedStaticInvoice) -> Result<Signature, ()> {
295+
self(message)
296+
}
297+
}
298+
299+
impl<F> SignFn<UnsignedStaticInvoice> for F
300+
where
301+
F: SignStaticInvoiceFn,
302+
{
303+
fn sign(&self, message: &UnsignedStaticInvoice) -> Result<Signature, ()> {
304+
self.sign_invoice(message)
305+
}
306+
}
307+
151308
impl StaticInvoice {
152309
invoice_accessors_common!(self, self.contents, StaticInvoice);
153310
invoice_accessors!(self, self.contents);
@@ -159,6 +316,57 @@ impl StaticInvoice {
159316
}
160317

161318
impl InvoiceContents {
319+
#[cfg(feature = "std")]
320+
fn is_offer_expired(&self) -> bool {
321+
self.offer.is_expired()
322+
}
323+
324+
#[cfg(not(feature = "std"))]
325+
fn is_offer_expired_no_std(&self, duration_since_epoch: Duration) -> bool {
326+
self.offer.is_expired_no_std(duration_since_epoch)
327+
}
328+
329+
fn new(
330+
offer: &Offer, payment_paths: Vec<(BlindedPayInfo, BlindedPath)>,
331+
message_paths: Vec<BlindedPath>, created_at: Duration, signing_pubkey: PublicKey,
332+
) -> Self {
333+
Self {
334+
offer: offer.contents.clone(),
335+
payment_paths,
336+
message_paths,
337+
created_at,
338+
relative_expiry: None,
339+
fallbacks: None,
340+
features: Bolt12InvoiceFeatures::empty(),
341+
signing_pubkey,
342+
}
343+
}
344+
345+
fn as_tlv_stream(&self) -> PartialInvoiceTlvStreamRef {
346+
let features = {
347+
if self.features == Bolt12InvoiceFeatures::empty() {
348+
None
349+
} else {
350+
Some(&self.features)
351+
}
352+
};
353+
354+
let invoice = InvoiceTlvStreamRef {
355+
paths: Some(Iterable(self.payment_paths.iter().map(|(_, path)| path))),
356+
message_paths: Some(self.message_paths.as_ref()),
357+
blindedpay: Some(Iterable(self.payment_paths.iter().map(|(payinfo, _)| payinfo))),
358+
created_at: Some(self.created_at.as_secs()),
359+
relative_expiry: self.relative_expiry.map(|duration| duration.as_secs() as u32),
360+
fallbacks: self.fallbacks.as_ref(),
361+
features,
362+
node_id: Some(&self.signing_pubkey),
363+
amount: None,
364+
payment_hash: None,
365+
};
366+
367+
(self.offer.as_tlv_stream(), invoice)
368+
}
369+
162370
fn chain(&self) -> ChainHash {
163371
debug_assert_eq!(self.offer.chains().len(), 1);
164372
self.offer.chains().first().cloned().unwrap_or_else(|| self.offer.implied_chain())
@@ -264,6 +472,8 @@ impl SeekReadable for FullInvoiceTlvStream {
264472

265473
type PartialInvoiceTlvStream = (OfferTlvStream, InvoiceTlvStream);
266474

475+
type PartialInvoiceTlvStreamRef<'a> = (OfferTlvStreamRef<'a>, InvoiceTlvStreamRef<'a>);
476+
267477
impl TryFrom<ParsedMessage<FullInvoiceTlvStream>> for StaticInvoice {
268478
type Error = Bolt12ParseError;
269479

0 commit comments

Comments
 (0)