Skip to content

Commit 3dab135

Browse files
committed
Builder for creating invoices for offers
Add a builder for creating invoices for an offer from a given request and required fields. Other settings are optional and duplicative settings will override previous settings. Building produces a semantically valid `invoice` message for the offer, which then can be signed with the key associated with the offer's signing pubkey.
1 parent 6e38b75 commit 3dab135

File tree

4 files changed

+281
-20
lines changed

4 files changed

+281
-20
lines changed

lightning/src/offers/invoice.rs

+229-5
Original file line numberDiff line numberDiff line change
@@ -10,21 +10,24 @@
1010
//! Data structures and encoding for `invoice` messages.
1111
1212
use bitcoin::blockdata::constants::ChainHash;
13+
use bitcoin::hash_types::{WPubkeyHash, WScriptHash};
14+
use bitcoin::hashes::Hash;
1315
use bitcoin::network::constants::Network;
14-
use bitcoin::secp256k1::PublicKey;
16+
use bitcoin::secp256k1::{Message, PublicKey};
1517
use bitcoin::secp256k1::schnorr::Signature;
1618
use bitcoin::util::address::{Address, Payload, WitnessVersion};
19+
use bitcoin::util::schnorr::TweakedPublicKey;
1720
use core::convert::TryFrom;
1821
use core::time::Duration;
1922
use crate::io;
2023
use crate::ln::PaymentHash;
2124
use crate::ln::features::{BlindedHopFeatures, Bolt12InvoiceFeatures};
2225
use crate::ln::msgs::DecodeError;
23-
use crate::offers::invoice_request::{InvoiceRequestContents, InvoiceRequestTlvStream};
24-
use crate::offers::merkle::{SignatureTlvStream, self};
25-
use crate::offers::offer::OfferTlvStream;
26+
use crate::offers::invoice_request::{InvoiceRequest, InvoiceRequestContents, InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef};
27+
use crate::offers::merkle::{SignError, SignatureTlvStream, SignatureTlvStreamRef, self};
28+
use crate::offers::offer::{OfferTlvStream, OfferTlvStreamRef};
2629
use crate::offers::parse::{ParseError, ParsedMessage, SemanticError};
27-
use crate::offers::payer::PayerTlvStream;
30+
use crate::offers::payer::{PayerTlvStream, PayerTlvStreamRef};
2831
use crate::offers::refund::RefundContents;
2932
use crate::onion_message::BlindedPath;
3033
use crate::util::ser::{HighZeroBytesDroppedBigSize, SeekReadable, WithoutLength, Writeable, Writer};
@@ -38,6 +41,164 @@ const DEFAULT_RELATIVE_EXPIRY: Duration = Duration::from_secs(7200);
3841

3942
const SIGNATURE_TAG: &'static str = concat!("lightning", "invoice", "signature");
4043

44+
/// Builds an [`Invoice`] from either:
45+
/// - an [`InvoiceRequest`] for the "offer to be paid" flow or
46+
/// - a [`Refund`] for the "offer for money" flow.
47+
///
48+
/// See [module-level documentation] for usage.
49+
///
50+
/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
51+
/// [`Refund`]: crate::offers::refund::Refund
52+
/// [module-level documentation]: self
53+
pub struct InvoiceBuilder<'a> {
54+
bytes: &'a Vec<u8>,
55+
invoice: InvoiceContents,
56+
}
57+
58+
impl<'a> InvoiceBuilder<'a> {
59+
pub(super) fn for_offer(
60+
invoice_request: &'a InvoiceRequest, paths: Vec<BlindedPath>, payinfo: Vec<BlindedPayInfo>,
61+
created_at: Duration, payment_hash: PaymentHash
62+
) -> Result<Self, SemanticError> {
63+
if paths.is_empty() {
64+
return Err(SemanticError::MissingPaths);
65+
}
66+
67+
if paths.len() != payinfo.len() {
68+
return Err(SemanticError::InvalidPayInfo);
69+
}
70+
71+
Ok(Self {
72+
bytes: &invoice_request.bytes,
73+
invoice: InvoiceContents::ForOffer {
74+
invoice_request: invoice_request.contents.clone(),
75+
fields: InvoiceFields {
76+
paths, payinfo, created_at, relative_expiry: None, payment_hash,
77+
amount_msats: invoice_request.amount_msats(), fallbacks: None,
78+
features: Bolt12InvoiceFeatures::empty(), code: None,
79+
},
80+
},
81+
})
82+
}
83+
84+
/// Sets the [`Invoice::relative_expiry`] as seconds since [`Invoice::created_at`]. Any expiry
85+
/// that has already passed is valid and can be checked for using [`Invoice::is_expired`].
86+
///
87+
/// Successive calls to this method will override the previous setting.
88+
pub fn relative_expiry(mut self, relative_expiry_secs: u32) -> Self {
89+
let relative_expiry = Duration::from_secs(relative_expiry_secs as u64);
90+
self.invoice.fields_mut().relative_expiry = Some(relative_expiry);
91+
self
92+
}
93+
94+
/// Adds a P2WSH address to [`Invoice::fallbacks`].
95+
///
96+
/// Successive calls to this method will add another address. Caller is responsible for not
97+
/// adding duplicate addresses.
98+
pub fn fallback_v0_p2wsh(mut self, script_hash: &WScriptHash) -> Self {
99+
let address = FallbackAddress {
100+
version: WitnessVersion::V0,
101+
program: Vec::from(&script_hash.into_inner()[..]),
102+
};
103+
self.invoice.fields_mut().fallbacks.get_or_insert_with(Vec::new).push(address);
104+
self
105+
}
106+
107+
/// Adds a P2WPKH address to [`Invoice::fallbacks`].
108+
///
109+
/// Successive calls to this method will add another address. Caller is responsible for not
110+
/// adding duplicate addresses.
111+
pub fn fallback_v0_p2wpkh(mut self, pubkey_hash: &WPubkeyHash) -> Self {
112+
let address = FallbackAddress {
113+
version: WitnessVersion::V0,
114+
program: Vec::from(&pubkey_hash.into_inner()[..]),
115+
};
116+
self.invoice.fields_mut().fallbacks.get_or_insert_with(Vec::new).push(address);
117+
self
118+
}
119+
120+
/// Adds a P2TR address to [`Invoice::fallbacks`].
121+
///
122+
/// Successive calls to this method will add another address. Caller is responsible for not
123+
/// adding duplicate addresses.
124+
pub fn fallback_v1_p2tr_tweaked(mut self, output_key: &TweakedPublicKey) -> Self {
125+
let address = FallbackAddress {
126+
version: WitnessVersion::V1,
127+
program: Vec::from(&output_key.serialize()[..]),
128+
};
129+
self.invoice.fields_mut().fallbacks.get_or_insert_with(Vec::new).push(address);
130+
self
131+
}
132+
133+
/// Sets [`Invoice::features`] to indicate MPP may be used. Otherwise, MPP is disallowed.
134+
///
135+
/// A subsequent call to [`InvoiceBuilder::mpp_required`] will override this setting.
136+
pub fn mpp_optional(mut self) -> Self {
137+
self.invoice.fields_mut().features.set_basic_mpp_optional();
138+
self
139+
}
140+
141+
/// Sets [`Invoice::features`] to indicate MPP should be used. Otherwise, MPP is disallowed.
142+
///
143+
/// A subsequent call to [`InvoiceBuilder::mpp_optional`] will override this setting.
144+
pub fn mpp_required(mut self) -> Self {
145+
self.invoice.fields_mut().features.set_basic_mpp_required();
146+
self
147+
}
148+
149+
/// Builds an unsigned [`Invoice`] after checking for valid semantics. It can be signed by
150+
/// [`UnsignedInvoice::sign`].
151+
pub fn build(self) -> Result<UnsignedInvoice<'a>, SemanticError> {
152+
#[cfg(feature = "std")] {
153+
if self.invoice.is_offer_or_refund_expired() {
154+
return Err(SemanticError::AlreadyExpired);
155+
}
156+
}
157+
158+
let InvoiceBuilder { bytes, invoice } = self;
159+
Ok(UnsignedInvoice { bytes, invoice })
160+
}
161+
}
162+
163+
/// A semantically valid [`Invoice`] that hasn't been signed.
164+
pub struct UnsignedInvoice<'a> {
165+
bytes: &'a Vec<u8>,
166+
invoice: InvoiceContents,
167+
}
168+
169+
impl<'a> UnsignedInvoice<'a> {
170+
/// Signs the invoice using the given function.
171+
pub fn sign<F, E>(self, sign: F) -> Result<Invoice, SignError<E>>
172+
where
173+
F: FnOnce(&Message) -> Result<Signature, E>
174+
{
175+
// Use the invoice_request bytes instead of the invoice_reqeuest TLV stream as the latter
176+
// may have contained unknown TLV records, which are not stored in `InvoiceRequestContents`
177+
// or `RefundContents`.
178+
let (_, _, _, invoice_tlv_stream) = self.invoice.as_tlv_stream();
179+
let invoice_request_bytes = WithoutLength(self.bytes);
180+
let unsigned_tlv_stream = (invoice_request_bytes, invoice_tlv_stream);
181+
182+
let mut bytes = Vec::new();
183+
unsigned_tlv_stream.write(&mut bytes).unwrap();
184+
185+
let pubkey = self.invoice.signing_pubkey();
186+
let signature = merkle::sign_message(sign, SIGNATURE_TAG, &bytes, pubkey)?;
187+
188+
// Append the signature TLV record to the bytes.
189+
let signature_tlv_stream = SignatureTlvStreamRef {
190+
signature: Some(&signature),
191+
};
192+
signature_tlv_stream.write(&mut bytes).unwrap();
193+
194+
Ok(Invoice {
195+
bytes,
196+
contents: self.invoice,
197+
signature: Some(signature),
198+
})
199+
}
200+
}
201+
41202
/// An `Invoice` is a payment request, typically corresponding to an [`Offer`] or a [`Refund`].
42203
///
43204
/// An invoice may be sent in response to an [`InvoiceRequest`] in the case of an offer or sent
@@ -199,19 +360,75 @@ impl Invoice {
199360
}
200361

201362
impl InvoiceContents {
363+
/// Whether the original offer or refund has expired.
364+
#[cfg(feature = "std")]
365+
fn is_offer_or_refund_expired(&self) -> bool {
366+
match self {
367+
InvoiceContents::ForOffer { invoice_request, .. } => invoice_request.offer.is_expired(),
368+
InvoiceContents::ForRefund { refund, .. } => refund.is_expired(),
369+
}
370+
}
371+
202372
fn chain(&self) -> ChainHash {
203373
match self {
204374
InvoiceContents::ForOffer { invoice_request, .. } => invoice_request.chain(),
205375
InvoiceContents::ForRefund { refund, .. } => refund.chain(),
206376
}
207377
}
208378

379+
fn signing_pubkey(&self) -> PublicKey {
380+
match self {
381+
InvoiceContents::ForOffer { invoice_request, .. } => {
382+
invoice_request.offer.signing_pubkey()
383+
},
384+
InvoiceContents::ForRefund { .. } => unreachable!(),
385+
}
386+
}
387+
209388
fn fields(&self) -> &InvoiceFields {
210389
match self {
211390
InvoiceContents::ForOffer { fields, .. } => fields,
212391
InvoiceContents::ForRefund { fields, .. } => fields,
213392
}
214393
}
394+
395+
fn fields_mut(&mut self) -> &mut InvoiceFields {
396+
match self {
397+
InvoiceContents::ForOffer { fields, .. } => fields,
398+
InvoiceContents::ForRefund { fields, .. } => fields,
399+
}
400+
}
401+
402+
fn as_tlv_stream(&self) -> PartialInvoiceTlvStreamRef {
403+
let (payer, offer, invoice_request) = match self {
404+
InvoiceContents::ForOffer { invoice_request, .. } => invoice_request.as_tlv_stream(),
405+
InvoiceContents::ForRefund { refund, .. } => refund.as_tlv_stream(),
406+
};
407+
let invoice = self.fields().as_tlv_stream();
408+
409+
(payer, offer, invoice_request, invoice)
410+
}
411+
}
412+
413+
impl InvoiceFields {
414+
fn as_tlv_stream(&self) -> InvoiceTlvStreamRef {
415+
let features = {
416+
if self.features == Bolt12InvoiceFeatures::empty() { None }
417+
else { Some(&self.features) }
418+
};
419+
420+
InvoiceTlvStreamRef {
421+
paths: Some(&self.paths),
422+
blindedpay: Some(&self.payinfo),
423+
created_at: Some(self.created_at.as_secs()),
424+
relative_expiry: self.relative_expiry.map(|duration| duration.as_secs() as u32),
425+
payment_hash: Some(&self.payment_hash),
426+
amount: Some(self.amount_msats),
427+
fallbacks: self.fallbacks.as_ref(),
428+
features,
429+
code: self.code.as_ref(),
430+
}
431+
}
215432
}
216433

217434
impl Writeable for Invoice {
@@ -288,6 +505,13 @@ impl SeekReadable for FullInvoiceTlvStream {
288505
type PartialInvoiceTlvStream =
289506
(PayerTlvStream, OfferTlvStream, InvoiceRequestTlvStream, InvoiceTlvStream);
290507

508+
type PartialInvoiceTlvStreamRef<'a> = (
509+
PayerTlvStreamRef<'a>,
510+
OfferTlvStreamRef<'a>,
511+
InvoiceRequestTlvStreamRef<'a>,
512+
InvoiceTlvStreamRef<'a>,
513+
);
514+
291515
impl TryFrom<ParsedMessage<FullInvoiceTlvStream>> for Invoice {
292516
type Error = ParseError;
293517

lightning/src/offers/invoice_request.rs

+28-1
Original file line numberDiff line numberDiff line change
@@ -56,13 +56,17 @@ use bitcoin::network::constants::Network;
5656
use bitcoin::secp256k1::{Message, PublicKey};
5757
use bitcoin::secp256k1::schnorr::Signature;
5858
use core::convert::TryFrom;
59+
use core::time::Duration;
5960
use crate::io;
61+
use crate::ln::PaymentHash;
6062
use crate::ln::features::InvoiceRequestFeatures;
6163
use crate::ln::msgs::DecodeError;
64+
use crate::offers::invoice::{BlindedPayInfo, InvoiceBuilder};
6265
use crate::offers::merkle::{SignError, SignatureTlvStream, SignatureTlvStreamRef, self};
6366
use crate::offers::offer::{Offer, OfferContents, OfferTlvStream, OfferTlvStreamRef};
6467
use crate::offers::parse::{ParseError, ParsedMessage, SemanticError};
6568
use crate::offers::payer::{PayerContents, PayerTlvStream, PayerTlvStreamRef};
69+
use crate::onion_message::BlindedPath;
6670
use crate::util::ser::{HighZeroBytesDroppedBigSize, SeekReadable, WithoutLength, Writeable, Writer};
6771
use crate::util::string::PrintableString;
6872

@@ -249,7 +253,7 @@ impl<'a> UnsignedInvoiceRequest<'a> {
249253
#[derive(Clone, Debug)]
250254
pub struct InvoiceRequest {
251255
pub(super) bytes: Vec<u8>,
252-
contents: InvoiceRequestContents,
256+
pub(super) contents: InvoiceRequestContents,
253257
signature: Signature,
254258
}
255259

@@ -317,6 +321,29 @@ impl InvoiceRequest {
317321
self.signature
318322
}
319323

324+
/// Creates an [`Invoice`] for the request with the given required fields.
325+
///
326+
/// Unless [`InvoiceBuilder::relative_expiry`] is set, the invoice will expire two hours after
327+
/// `created_at`. The caller is expected to remember the preimage of `payment_hash` in order to
328+
/// claim a payment for the invoice.
329+
///
330+
/// The `paths` and `payinfo` parameters are useful for maintaining the payment recipient's
331+
/// privacy. They must contain one or more elements and be of equal length.
332+
///
333+
/// Errors if the request contains unknown required features.
334+
///
335+
/// [`Invoice`]: crate::offers::invoice::Invoice
336+
pub fn respond_with(
337+
&self, paths: Vec<BlindedPath>, payinfo: Vec<BlindedPayInfo>, created_at: Duration,
338+
payment_hash: PaymentHash
339+
) -> Result<InvoiceBuilder, SemanticError> {
340+
if self.features().requires_unknown_bits() {
341+
return Err(SemanticError::UnknownRequiredFeatures);
342+
}
343+
344+
InvoiceBuilder::for_offer(self, paths, payinfo, created_at, payment_hash)
345+
}
346+
320347
#[cfg(test)]
321348
fn as_tlv_stream(&self) -> FullInvoiceRequestTlvStreamRef {
322349
let (payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream) =

lightning/src/offers/offer.rs

+12-7
Original file line numberDiff line numberDiff line change
@@ -321,13 +321,7 @@ impl Offer {
321321
/// Whether the offer has expired.
322322
#[cfg(feature = "std")]
323323
pub fn is_expired(&self) -> bool {
324-
match self.absolute_expiry() {
325-
Some(seconds_from_epoch) => match SystemTime::UNIX_EPOCH.elapsed() {
326-
Ok(elapsed) => elapsed > seconds_from_epoch,
327-
Err(_) => false,
328-
},
329-
None => false,
330-
}
324+
self.contents.is_expired()
331325
}
332326

333327
/// The issuer of the offer, possibly beginning with `user@domain` or `domain`. Intended to be
@@ -412,6 +406,17 @@ impl OfferContents {
412406
self.chains().contains(&chain)
413407
}
414408

409+
#[cfg(feature = "std")]
410+
pub(super) fn is_expired(&self) -> bool {
411+
match self.absolute_expiry {
412+
Some(seconds_from_epoch) => match SystemTime::UNIX_EPOCH.elapsed() {
413+
Ok(elapsed) => elapsed > seconds_from_epoch,
414+
Err(_) => false,
415+
},
416+
None => false,
417+
}
418+
}
419+
415420
pub fn amount(&self) -> Option<&Amount> {
416421
self.amount.as_ref()
417422
}

0 commit comments

Comments
 (0)