Skip to content

Commit 31de292

Browse files
Static invoice encoding and parsing
Define an interface for BOLT 12 static invoice messages. The underlying format consists of the original bytes and the parsed contents. The bytes are later needed for serialization. This is because it must mirror all the offer TLV records, including unknown ones, which aren't represented in the contents. Invoices may be created from an offer.
1 parent 2bc5fd4 commit 31de292

File tree

2 files changed

+360
-0
lines changed

2 files changed

+360
-0
lines changed

lightning/src/offers/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,6 @@ pub mod parse;
2424
mod payer;
2525
pub mod refund;
2626
pub(crate) mod signer;
27+
pub mod static_invoice;
2728
#[cfg(test)]
2829
pub(crate) mod test_utils;
Lines changed: 359 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,359 @@
1+
// This file is Copyright its original authors, visible in version control
2+
// history.
3+
//
4+
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
5+
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
6+
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
7+
// You may not use this file except in accordance with one or both of these
8+
// licenses.
9+
10+
//! Data structures and encoding for static BOLT 12 invoices.
11+
12+
use crate::blinded_path::BlindedPath;
13+
use crate::io;
14+
use crate::ln::features::{Bolt12InvoiceFeatures, OfferFeatures};
15+
use crate::ln::msgs::DecodeError;
16+
use crate::offers::invoice::{
17+
construct_payment_paths, filter_fallbacks, BlindedPathIter, BlindedPayInfo, BlindedPayInfoIter,
18+
FallbackAddress, SIGNATURE_TAG,
19+
};
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};
23+
use crate::offers::parse::{Bolt12ParseError, Bolt12SemanticError, ParsedMessage};
24+
use crate::util::ser::{
25+
HighZeroBytesDroppedBigSize, Iterable, SeekReadable, WithoutLength, Writeable, Writer,
26+
};
27+
use crate::util::string::PrintableString;
28+
use bitcoin::address::Address;
29+
use bitcoin::blockdata::constants::ChainHash;
30+
use bitcoin::secp256k1::schnorr::Signature;
31+
use bitcoin::secp256k1::PublicKey;
32+
use core::time::Duration;
33+
34+
#[cfg(feature = "std")]
35+
use crate::offers::invoice::is_expired;
36+
37+
#[allow(unused_imports)]
38+
use crate::prelude::*;
39+
40+
/// Static invoices default to expiring after 24 hours.
41+
const DEFAULT_RELATIVE_EXPIRY: Duration = Duration::from_secs(3600 * 24);
42+
43+
/// A `StaticInvoice` is a reusable payment request corresponding to an [`Offer`].
44+
///
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.
49+
///
50+
/// [`Offer`]: crate::offers::offer::Offer
51+
/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
52+
/// [`Bolt12Invoice`]: crate::offers::invoice::Bolt12Invoice
53+
#[derive(Clone, Debug)]
54+
pub struct StaticInvoice {
55+
bytes: Vec<u8>,
56+
contents: InvoiceContents,
57+
signature: Signature,
58+
}
59+
60+
/// The contents of a [`StaticInvoice`] for responding to an [`Offer`].
61+
///
62+
/// [`Offer`]: crate::offers::offer::Offer
63+
#[derive(Clone, Debug)]
64+
struct InvoiceContents {
65+
offer: OfferContents,
66+
payment_paths: Vec<(BlindedPayInfo, BlindedPath)>,
67+
created_at: Duration,
68+
relative_expiry: Option<Duration>,
69+
fallbacks: Option<Vec<FallbackAddress>>,
70+
features: Bolt12InvoiceFeatures,
71+
signing_pubkey: PublicKey,
72+
message_paths: Vec<BlindedPath>,
73+
}
74+
75+
macro_rules! invoice_accessors { ($self: ident, $contents: expr) => {
76+
/// The chain that must be used when paying the invoice. [`StaticInvoice`]s currently can only be
77+
/// created from offers that support a single chain.
78+
pub fn chain(&$self) -> ChainHash {
79+
$contents.chain()
80+
}
81+
82+
/// Opaque bytes set by the originating [`Offer::metadata`].
83+
///
84+
/// [`Offer::metadata`]: crate::offers::offer::Offer::metadata
85+
pub fn metadata(&$self) -> Option<&Vec<u8>> {
86+
$contents.metadata()
87+
}
88+
89+
/// The minimum amount required for a successful payment of a single item.
90+
///
91+
/// From [`Offer::amount`].
92+
///
93+
/// [`Offer::amount`]: crate::offers::offer::Offer::amount
94+
pub fn amount(&$self) -> Option<Amount> {
95+
$contents.amount()
96+
}
97+
98+
/// Features pertaining to the originating [`Offer`], from [`Offer::offer_features`].
99+
///
100+
/// [`Offer`]: crate::offers::offer::Offer
101+
/// [`Offer::offer_features`]: crate::offers::offer::Offer::offer_features
102+
pub fn offer_features(&$self) -> &OfferFeatures {
103+
$contents.offer_features()
104+
}
105+
106+
/// A complete description of the purpose of the originating offer, from [`Offer::description`].
107+
///
108+
/// [`Offer::description`]: crate::offers::offer::Offer::description
109+
pub fn description(&$self) -> Option<PrintableString> {
110+
$contents.description()
111+
}
112+
113+
/// Duration since the Unix epoch when an invoice should no longer be requested, from
114+
/// [`Offer::absolute_expiry`].
115+
///
116+
/// [`Offer::absolute_expiry`]: crate::offers::offer::Offer::absolute_expiry
117+
pub fn absolute_expiry(&$self) -> Option<Duration> {
118+
$contents.absolute_expiry()
119+
}
120+
121+
/// The issuer of the offer, from [`Offer::issuer`].
122+
///
123+
/// [`Offer::issuer`]: crate::offers::offer::Offer::issuer
124+
pub fn issuer(&$self) -> Option<PrintableString> {
125+
$contents.issuer()
126+
}
127+
128+
/// Paths to the node that may supply the invoice on the recipient's behalf, originating from
129+
/// publicly reachable nodes. Taken from [`Offer::paths`].
130+
///
131+
/// [`Offer::paths`]: crate::offers::offer::Offer::paths
132+
pub fn offer_message_paths(&$self) -> &[BlindedPath] {
133+
$contents.offer_message_paths()
134+
}
135+
136+
/// Paths to the recipient for indicating that a held HTLC is available to claim when they next
137+
/// come online.
138+
pub fn message_paths(&$self) -> &[BlindedPath] {
139+
$contents.message_paths()
140+
}
141+
142+
/// The quantity of items supported, from [`Offer::supported_quantity`].
143+
///
144+
/// [`Offer::supported_quantity`]: crate::offers::offer::Offer::supported_quantity
145+
pub fn supported_quantity(&$self) -> Quantity {
146+
$contents.supported_quantity()
147+
}
148+
} }
149+
150+
impl StaticInvoice {
151+
invoice_accessors_common!(self, self.contents, StaticInvoice);
152+
invoice_accessors!(self, self.contents);
153+
154+
/// Signature of the invoice verified using [`StaticInvoice::signing_pubkey`].
155+
pub fn signature(&self) -> Signature {
156+
self.signature
157+
}
158+
}
159+
160+
impl InvoiceContents {
161+
fn chain(&self) -> ChainHash {
162+
debug_assert_eq!(self.offer.chains().len(), 1);
163+
self.offer.chains().first().cloned().unwrap_or_else(|| self.offer.implied_chain())
164+
}
165+
166+
fn metadata(&self) -> Option<&Vec<u8>> {
167+
self.offer.metadata()
168+
}
169+
170+
fn amount(&self) -> Option<Amount> {
171+
self.offer.amount()
172+
}
173+
174+
fn offer_features(&self) -> &OfferFeatures {
175+
self.offer.features()
176+
}
177+
178+
fn description(&self) -> Option<PrintableString> {
179+
self.offer.description()
180+
}
181+
182+
fn absolute_expiry(&self) -> Option<Duration> {
183+
self.offer.absolute_expiry()
184+
}
185+
186+
fn issuer(&self) -> Option<PrintableString> {
187+
self.offer.issuer()
188+
}
189+
190+
fn offer_message_paths(&self) -> &[BlindedPath] {
191+
self.offer.paths()
192+
}
193+
194+
fn message_paths(&self) -> &[BlindedPath] {
195+
&self.message_paths[..]
196+
}
197+
198+
fn supported_quantity(&self) -> Quantity {
199+
self.offer.supported_quantity()
200+
}
201+
202+
fn payment_paths(&self) -> &[(BlindedPayInfo, BlindedPath)] {
203+
&self.payment_paths[..]
204+
}
205+
206+
fn created_at(&self) -> Duration {
207+
self.created_at
208+
}
209+
210+
fn relative_expiry(&self) -> Duration {
211+
self.relative_expiry.unwrap_or(DEFAULT_RELATIVE_EXPIRY)
212+
}
213+
214+
#[cfg(feature = "std")]
215+
fn is_expired(&self) -> bool {
216+
is_expired(self.created_at(), self.relative_expiry())
217+
}
218+
219+
fn fallbacks(&self) -> Vec<Address> {
220+
let chain =
221+
self.offer.chains().first().cloned().unwrap_or_else(|| self.offer.implied_chain());
222+
self.fallbacks
223+
.as_ref()
224+
.map(|fallbacks| filter_fallbacks(chain, fallbacks))
225+
.unwrap_or_else(Vec::new)
226+
}
227+
228+
fn features(&self) -> &Bolt12InvoiceFeatures {
229+
&self.features
230+
}
231+
232+
fn signing_pubkey(&self) -> PublicKey {
233+
self.signing_pubkey
234+
}
235+
}
236+
237+
impl Writeable for StaticInvoice {
238+
fn write<W: Writer>(&self, writer: &mut W) -> Result<(), io::Error> {
239+
WithoutLength(&self.bytes).write(writer)
240+
}
241+
}
242+
243+
impl TryFrom<Vec<u8>> for StaticInvoice {
244+
type Error = Bolt12ParseError;
245+
246+
fn try_from(bytes: Vec<u8>) -> Result<Self, Self::Error> {
247+
let parsed_invoice = ParsedMessage::<FullInvoiceTlvStream>::try_from(bytes)?;
248+
StaticInvoice::try_from(parsed_invoice)
249+
}
250+
}
251+
252+
tlv_stream!(InvoiceTlvStream, InvoiceTlvStreamRef, 160..240, {
253+
(160, payment_paths: (Vec<BlindedPath>, WithoutLength, Iterable<'a, BlindedPathIter<'a>, BlindedPath>)),
254+
(162, blindedpay: (Vec<BlindedPayInfo>, WithoutLength, Iterable<'a, BlindedPayInfoIter<'a>, BlindedPayInfo>)),
255+
(164, created_at: (u64, HighZeroBytesDroppedBigSize)),
256+
(166, relative_expiry: (u32, HighZeroBytesDroppedBigSize)),
257+
(172, fallbacks: (Vec<FallbackAddress>, WithoutLength)),
258+
(174, features: (Bolt12InvoiceFeatures, WithoutLength)),
259+
(176, node_id: PublicKey),
260+
(178, message_paths: (Vec<BlindedPath>, WithoutLength)),
261+
});
262+
263+
type FullInvoiceTlvStream = (OfferTlvStream, InvoiceTlvStream, SignatureTlvStream);
264+
265+
impl SeekReadable for FullInvoiceTlvStream {
266+
fn read<R: io::Read + io::Seek>(r: &mut R) -> Result<Self, DecodeError> {
267+
let offer = SeekReadable::read(r)?;
268+
let invoice = SeekReadable::read(r)?;
269+
let signature = SeekReadable::read(r)?;
270+
271+
Ok((offer, invoice, signature))
272+
}
273+
}
274+
275+
impl TryFrom<ParsedMessage<FullInvoiceTlvStream>> for StaticInvoice {
276+
type Error = Bolt12ParseError;
277+
278+
fn try_from(invoice: ParsedMessage<FullInvoiceTlvStream>) -> Result<Self, Self::Error> {
279+
let ParsedMessage { bytes, tlv_stream } = invoice;
280+
let (offer_tlv_stream, invoice_tlv_stream, SignatureTlvStream { signature }) = tlv_stream;
281+
let contents = InvoiceContents::try_from((offer_tlv_stream, invoice_tlv_stream))?;
282+
283+
let signature = match signature {
284+
None => {
285+
return Err(Bolt12ParseError::InvalidSemantics(
286+
Bolt12SemanticError::MissingSignature,
287+
))
288+
},
289+
Some(signature) => signature,
290+
};
291+
let tagged_hash = TaggedHash::from_valid_tlv_stream_bytes(SIGNATURE_TAG, &bytes);
292+
let pubkey = contents.signing_pubkey;
293+
merkle::verify_signature(&signature, &tagged_hash, pubkey)?;
294+
295+
Ok(StaticInvoice { bytes, contents, signature })
296+
}
297+
}
298+
299+
impl TryFrom<(OfferTlvStream, InvoiceTlvStream)> for InvoiceContents {
300+
type Error = Bolt12SemanticError;
301+
302+
fn try_from(tlv_stream: (OfferTlvStream, InvoiceTlvStream)) -> Result<Self, Self::Error> {
303+
let (
304+
offer_tlv_stream,
305+
InvoiceTlvStream {
306+
payment_paths,
307+
blindedpay,
308+
created_at,
309+
relative_expiry,
310+
fallbacks,
311+
features,
312+
node_id,
313+
message_paths,
314+
},
315+
) = tlv_stream;
316+
317+
let payment_paths = construct_payment_paths(blindedpay, payment_paths)?;
318+
let message_paths = message_paths.ok_or(Bolt12SemanticError::MissingPaths)?;
319+
320+
let created_at = match created_at {
321+
None => return Err(Bolt12SemanticError::MissingCreationTime),
322+
Some(timestamp) => Duration::from_secs(timestamp),
323+
};
324+
325+
let relative_expiry = relative_expiry.map(Into::<u64>::into).map(Duration::from_secs);
326+
327+
let features = features.unwrap_or_else(Bolt12InvoiceFeatures::empty);
328+
329+
let signing_pubkey = match node_id {
330+
None => return Err(Bolt12SemanticError::MissingSigningPubkey),
331+
Some(node_id) => {
332+
let offer_node_id =
333+
offer_tlv_stream.node_id.ok_or(Bolt12SemanticError::MissingSigningPubkey)?;
334+
if node_id != offer_node_id {
335+
return Err(Bolt12SemanticError::InvalidSigningPubkey);
336+
}
337+
node_id
338+
},
339+
};
340+
341+
if offer_tlv_stream.paths.is_none() {
342+
return Err(Bolt12SemanticError::MissingPaths);
343+
}
344+
if offer_tlv_stream.chains.as_ref().map_or(0, |chains| chains.len()) > 1 {
345+
return Err(Bolt12SemanticError::UnexpectedChain);
346+
}
347+
348+
Ok(InvoiceContents {
349+
offer: OfferContents::try_from(offer_tlv_stream)?,
350+
payment_paths,
351+
message_paths,
352+
created_at,
353+
relative_expiry,
354+
fallbacks,
355+
features,
356+
signing_pubkey,
357+
})
358+
}
359+
}

0 commit comments

Comments
 (0)