Skip to content

Commit b249e9b

Browse files
committed
Invoice encoding and parsing
Define an interface for BOLT 12 `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` and `invoice_request` TLV records, including unknown ones, which aren't represented in the contents. Invoices may be created for an Offer (from an InvoiceRequest) or for a Refund. The primary difference is how the signing pubkey is given -- by the writer of the offer or the reader of the refund.
1 parent d1755c2 commit b249e9b

File tree

7 files changed

+437
-8
lines changed

7 files changed

+437
-8
lines changed

lightning/src/offers/invoice.rs

Lines changed: 392 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,392 @@
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 `invoice` messages.
11+
12+
use bitcoin::blockdata::constants::ChainHash;
13+
use bitcoin::network::constants::Network;
14+
use bitcoin::secp256k1::schnorr::Signature;
15+
use bitcoin::util::address::{Address, Payload, WitnessVersion};
16+
use core::convert::TryFrom;
17+
use core::time::Duration;
18+
use crate::io;
19+
use crate::ln::PaymentHash;
20+
use crate::ln::features::{BlindedHopFeatures, Bolt12InvoiceFeatures};
21+
use crate::ln::msgs::DecodeError;
22+
use crate::offers::invoice_request::{InvoiceRequestContents, InvoiceRequestTlvStream};
23+
use crate::offers::merkle::{SignatureTlvStream, self};
24+
use crate::offers::offer::OfferTlvStream;
25+
use crate::offers::parse::{ParseError, ParsedMessage, SemanticError};
26+
use crate::offers::payer::PayerTlvStream;
27+
use crate::offers::refund::RefundContents;
28+
use crate::onion_message::BlindedPath;
29+
use crate::util::ser::{HighZeroBytesDroppedBigSize, SeekReadable, WithoutLength, Writeable, Writer};
30+
31+
use crate::prelude::*;
32+
33+
#[cfg(feature = "std")]
34+
use std::time::SystemTime;
35+
36+
const DEFAULT_RELATIVE_EXPIRY: Duration = Duration::from_secs(7200);
37+
38+
const SIGNATURE_TAG: &'static str = concat!("lightning", "invoice", "signature");
39+
40+
/// An `Invoice` is a payment request, typically corresponding to an [`Offer`] or a [`Refund`].
41+
///
42+
/// An invoice may be sent in response to an [`InvoiceRequest`] in the case of an offer or sent
43+
/// directly after scanning a refund. It includes all the information need to pay a recipient.
44+
///
45+
/// [`Offer`]: crate::offers::offer::Offer
46+
/// [`Refund`]: crate::offers::refund::Refund
47+
/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
48+
pub struct Invoice {
49+
bytes: Vec<u8>,
50+
contents: InvoiceContents,
51+
signature: Option<Signature>,
52+
}
53+
54+
/// The contents of an [`Invoice`] for responding to either an [`Offer`] or a [`Refund`].
55+
///
56+
/// [`Offer`]: crate::offers::offer::Offer
57+
/// [`Refund`]: crate::offers::refund::Refund
58+
enum InvoiceContents {
59+
/// Contents for an [`Invoice`] corresponding to an [`Offer`].
60+
///
61+
/// [`Offer`]: crate::offers::offer::Offer
62+
ForOffer {
63+
invoice_request: InvoiceRequestContents,
64+
fields: InvoiceFields,
65+
},
66+
/// Contents for an [`Invoice`] corresponding to a [`Refund`].
67+
///
68+
/// [`Refund`]: crate::offers::refund::Refund
69+
ForRefund {
70+
refund: RefundContents,
71+
fields: InvoiceFields,
72+
},
73+
}
74+
75+
/// Invoice-specific fields for an `invoice` message.
76+
struct InvoiceFields {
77+
paths: Vec<BlindedPath>,
78+
payinfo: Vec<BlindedPayInfo>,
79+
created_at: Duration,
80+
relative_expiry: Option<Duration>,
81+
payment_hash: PaymentHash,
82+
amount_msats: u64,
83+
fallbacks: Option<Vec<FallbackAddress>>,
84+
features: Bolt12InvoiceFeatures,
85+
code: Option<String>,
86+
}
87+
88+
impl Invoice {
89+
/// Paths to the recipient originating from publicly reachable nodes. Blinded paths provide
90+
/// recipient privacy by obfuscating its node id.
91+
pub fn paths(&self) -> &[BlindedPath] {
92+
&self.contents.fields().paths[..]
93+
}
94+
95+
/// Information for each hop in [`Invoice::paths`] needed for routing payments across it.
96+
pub fn payinfo(&self) -> &[BlindedPayInfo] {
97+
&self.contents.fields().payinfo[..]
98+
}
99+
100+
/// Duration since the Unix epoch when the invoice was created.
101+
pub fn created_at(&self) -> Duration {
102+
self.contents.fields().created_at
103+
}
104+
105+
/// Duration since [`Invoice::created_at`] when the invoice has expired and therefore should no
106+
/// longer be paid.
107+
pub fn relative_expiry(&self) -> Duration {
108+
self.contents.fields().relative_expiry.unwrap_or(DEFAULT_RELATIVE_EXPIRY)
109+
}
110+
111+
/// Whether the invoice has expired.
112+
#[cfg(feature = "std")]
113+
pub fn is_expired(&self) -> bool {
114+
let absolute_expiry = self.created_at().checked_add(self.relative_expiry());
115+
match absolute_expiry {
116+
Some(seconds_from_epoch) => match SystemTime::UNIX_EPOCH.elapsed() {
117+
Ok(elapsed) => elapsed > seconds_from_epoch,
118+
Err(_) => false,
119+
},
120+
None => false,
121+
}
122+
}
123+
124+
/// SHA256 hash of the payment preimage that will be given in return for paying the invoice.
125+
pub fn payment_hash(&self) -> PaymentHash {
126+
self.contents.fields().payment_hash
127+
}
128+
129+
/// The minimum amount required for a successful payment of the invoice.
130+
pub fn amount_msats(&self) -> u64 {
131+
self.contents.fields().amount_msats
132+
}
133+
134+
/// Fallback addresses for paying the invoice on-chain, in order of most-preferred to
135+
/// least-preferred.
136+
pub fn fallbacks(&self) -> Vec<Address> {
137+
let network = match self.network() {
138+
None => return Vec::new(),
139+
Some(network) => network,
140+
};
141+
142+
let to_valid_address = |address: &FallbackAddress| {
143+
let program = &address.program;
144+
if program.len() < 2 || program.len() > 40 {
145+
return None;
146+
}
147+
148+
let address = Address {
149+
payload: Payload::WitnessProgram {
150+
version: address.version,
151+
program: address.program.clone(),
152+
},
153+
network,
154+
};
155+
156+
if !address.is_standard() {
157+
return None;
158+
}
159+
160+
Some(address)
161+
};
162+
163+
self.contents.fields().fallbacks
164+
.as_ref()
165+
.map(|fallbacks| fallbacks.iter().filter_map(to_valid_address).collect())
166+
.unwrap_or_else(Vec::new)
167+
}
168+
169+
fn network(&self) -> Option<Network> {
170+
let chain = self.contents.chain();
171+
if chain == ChainHash::using_genesis_block(Network::Bitcoin) {
172+
Some(Network::Bitcoin)
173+
} else if chain == ChainHash::using_genesis_block(Network::Testnet) {
174+
Some(Network::Testnet)
175+
} else if chain == ChainHash::using_genesis_block(Network::Signet) {
176+
Some(Network::Signet)
177+
} else if chain == ChainHash::using_genesis_block(Network::Regtest) {
178+
Some(Network::Regtest)
179+
} else {
180+
None
181+
}
182+
}
183+
184+
/// Features pertaining to paying an invoice.
185+
pub fn features(&self) -> &Bolt12InvoiceFeatures {
186+
&self.contents.fields().features
187+
}
188+
189+
/// A short string used for verifying the recipient when issuing a refund. The payer should only
190+
/// pay the invoice after confirming with recipient out-of-band that this code is correct.
191+
pub fn code(&self) -> Option<&str> {
192+
self.contents.fields().code.as_ref().map(|code| code.as_str())
193+
}
194+
195+
/// Signature of the invoice using [`signing_pubkey`]. `None` for invoices paying refunds.
196+
///
197+
/// [`signing_pubkey`]: crate::offers::offer::Offer::signing_pubkey
198+
pub fn signature(&self) -> Option<Signature> {
199+
self.signature
200+
}
201+
}
202+
203+
impl InvoiceContents {
204+
fn chain(&self) -> ChainHash {
205+
match self {
206+
InvoiceContents::ForOffer { invoice_request, .. } => invoice_request.chain(),
207+
InvoiceContents::ForRefund { refund, .. } => refund.chain(),
208+
}
209+
}
210+
211+
fn fields(&self) -> &InvoiceFields {
212+
match self {
213+
InvoiceContents::ForOffer { fields, .. } => fields,
214+
InvoiceContents::ForRefund { fields, .. } => fields,
215+
}
216+
}
217+
}
218+
219+
impl Writeable for Invoice {
220+
fn write<W: Writer>(&self, writer: &mut W) -> Result<(), io::Error> {
221+
WithoutLength(&self.bytes).write(writer)
222+
}
223+
}
224+
225+
impl TryFrom<Vec<u8>> for Invoice {
226+
type Error = ParseError;
227+
228+
fn try_from(bytes: Vec<u8>) -> Result<Self, Self::Error> {
229+
let parsed_invoice = ParsedMessage::<FullInvoiceTlvStream>::try_from(bytes)?;
230+
Invoice::try_from(parsed_invoice)
231+
}
232+
}
233+
234+
tlv_stream!(InvoiceTlvStream, InvoiceTlvStreamRef, 160..240, {
235+
(160, paths: (Vec<BlindedPath>, WithoutLength)),
236+
(162, blindedpay: (Vec<BlindedPayInfo>, WithoutLength)),
237+
(164, created_at: (u64, HighZeroBytesDroppedBigSize)),
238+
(166, relative_expiry: (u32, HighZeroBytesDroppedBigSize)),
239+
(168, payment_hash: PaymentHash),
240+
(170, amount: (u64, HighZeroBytesDroppedBigSize)),
241+
(172, fallbacks: (Vec<FallbackAddress>, WithoutLength)),
242+
(174, features: (Bolt12InvoiceFeatures, WithoutLength)),
243+
(176, code: (String, WithoutLength)),
244+
});
245+
246+
/// Information needed to route a payment across a [`BlindedPath`] hop.
247+
#[derive(Debug, PartialEq)]
248+
pub struct BlindedPayInfo {
249+
fee_base_msat: u32,
250+
fee_proportional_millionths: u32,
251+
cltv_expiry_delta: u16,
252+
htlc_minimum_msat: u64,
253+
htlc_maximum_msat: u64,
254+
features: BlindedHopFeatures,
255+
}
256+
257+
impl_writeable!(BlindedPayInfo, {
258+
fee_base_msat,
259+
fee_proportional_millionths,
260+
cltv_expiry_delta,
261+
htlc_minimum_msat,
262+
htlc_maximum_msat,
263+
features
264+
});
265+
266+
/// Wire representation for an on-chain fallback address.
267+
#[derive(Debug, PartialEq)]
268+
pub(super) struct FallbackAddress {
269+
version: WitnessVersion,
270+
program: Vec<u8>,
271+
}
272+
273+
impl_writeable!(FallbackAddress, { version, program });
274+
275+
type FullInvoiceTlvStream =
276+
(PayerTlvStream, OfferTlvStream, InvoiceRequestTlvStream, InvoiceTlvStream, SignatureTlvStream);
277+
278+
impl SeekReadable for FullInvoiceTlvStream {
279+
fn read<R: io::Read + io::Seek>(r: &mut R) -> Result<Self, DecodeError> {
280+
let payer = SeekReadable::read(r)?;
281+
let offer = SeekReadable::read(r)?;
282+
let invoice_request = SeekReadable::read(r)?;
283+
let invoice = SeekReadable::read(r)?;
284+
let signature = SeekReadable::read(r)?;
285+
286+
Ok((payer, offer, invoice_request, invoice, signature))
287+
}
288+
}
289+
290+
type PartialInvoiceTlvStream =
291+
(PayerTlvStream, OfferTlvStream, InvoiceRequestTlvStream, InvoiceTlvStream);
292+
293+
impl TryFrom<ParsedMessage<FullInvoiceTlvStream>> for Invoice {
294+
type Error = ParseError;
295+
296+
fn try_from(invoice: ParsedMessage<FullInvoiceTlvStream>) -> Result<Self, Self::Error> {
297+
let ParsedMessage { bytes, tlv_stream } = invoice;
298+
let (
299+
payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream,
300+
SignatureTlvStream { signature },
301+
) = tlv_stream;
302+
let contents = InvoiceContents::try_from(
303+
(payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream)
304+
)?;
305+
306+
match &contents {
307+
InvoiceContents::ForOffer { invoice_request, .. } => match &signature {
308+
None => return Err(ParseError::InvalidSemantics(SemanticError::MissingSignature)),
309+
Some(signature) => {
310+
let pubkey = invoice_request.offer.signing_pubkey();
311+
merkle::verify_signature(signature, SIGNATURE_TAG, &bytes, pubkey)?;
312+
},
313+
},
314+
InvoiceContents::ForRefund { .. } => if let Some(_) = &signature {
315+
return Err(ParseError::InvalidSemantics(SemanticError::UnexpectedSignature));
316+
},
317+
}
318+
319+
Ok(Invoice { bytes, contents, signature })
320+
}
321+
}
322+
323+
impl TryFrom<PartialInvoiceTlvStream> for InvoiceContents {
324+
type Error = SemanticError;
325+
326+
fn try_from(tlv_stream: PartialInvoiceTlvStream) -> Result<Self, Self::Error> {
327+
let (
328+
payer_tlv_stream,
329+
offer_tlv_stream,
330+
invoice_request_tlv_stream,
331+
InvoiceTlvStream {
332+
paths, blindedpay, created_at, relative_expiry, payment_hash, amount, fallbacks,
333+
features, code,
334+
},
335+
) = tlv_stream;
336+
337+
let (paths, payinfo) = match (paths, blindedpay) {
338+
(None, _) => return Err(SemanticError::MissingPaths),
339+
(_, None) => return Err(SemanticError::InvalidPayInfo),
340+
(Some(paths), _) if paths.is_empty() => return Err(SemanticError::MissingPaths),
341+
(Some(paths), Some(blindedpay)) if paths.len() != blindedpay.len() => {
342+
return Err(SemanticError::InvalidPayInfo);
343+
},
344+
(Some(paths), Some(blindedpay)) => (paths, blindedpay),
345+
};
346+
347+
let created_at = match created_at {
348+
None => return Err(SemanticError::MissingCreationTime),
349+
Some(timestamp) => Duration::from_secs(timestamp),
350+
};
351+
352+
let relative_expiry = relative_expiry
353+
.map(Into::<u64>::into)
354+
.map(Duration::from_secs);
355+
356+
let payment_hash = match payment_hash {
357+
None => return Err(SemanticError::MissingPaymentHash),
358+
Some(payment_hash) => payment_hash,
359+
};
360+
361+
let amount_msats = match amount {
362+
None => return Err(SemanticError::MissingAmount),
363+
Some(amount) => amount,
364+
};
365+
366+
let features = features.unwrap_or_else(Bolt12InvoiceFeatures::empty);
367+
368+
let fields = InvoiceFields {
369+
paths, payinfo, created_at, relative_expiry, payment_hash, amount_msats, fallbacks,
370+
features, code,
371+
};
372+
373+
match offer_tlv_stream.node_id {
374+
Some(_) => {
375+
if fields.code.is_some() {
376+
return Err(SemanticError::UnexpectedCode);
377+
}
378+
379+
let invoice_request = InvoiceRequestContents::try_from(
380+
(payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream)
381+
)?;
382+
Ok(InvoiceContents::ForOffer { invoice_request, fields })
383+
},
384+
None => {
385+
let refund = RefundContents::try_from(
386+
(payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream)
387+
)?;
388+
Ok(InvoiceContents::ForRefund { refund, fields })
389+
},
390+
}
391+
}
392+
}

0 commit comments

Comments
 (0)