Skip to content

Commit c4ffb17

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 243f448 commit c4ffb17

File tree

7 files changed

+442
-13
lines changed

7 files changed

+442
-13
lines changed

lightning/src/offers/invoice.rs

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

0 commit comments

Comments
 (0)