Skip to content

Commit e1aa18a

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 e1aa18a

File tree

6 files changed

+429
-13
lines changed

6 files changed

+429
-13
lines changed

lightning/src/offers/invoice.rs

Lines changed: 391 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,391 @@
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+
let version = match WitnessVersion::try_from(address.version) {
140+
Ok(version) => version,
141+
Err(_) => return None,
142+
};
143+
144+
let program = &address.program;
145+
if program.len() < 2 || program.len() > 40 {
146+
return None;
147+
}
148+
149+
let address = Address {
150+
payload: Payload::WitnessProgram {
151+
version,
152+
program: address.program.clone(),
153+
},
154+
network,
155+
};
156+
157+
if !address.is_standard() && version == WitnessVersion::V0 {
158+
return None;
159+
}
160+
161+
Some(address)
162+
};
163+
164+
self.contents.fields().fallbacks
165+
.as_ref()
166+
.map(|fallbacks| fallbacks.iter().filter_map(to_valid_address).collect())
167+
.unwrap_or_else(Vec::new)
168+
}
169+
170+
fn network(&self) -> Option<Network> {
171+
let chain = self.contents.chain();
172+
if chain == ChainHash::using_genesis_block(Network::Bitcoin) {
173+
Some(Network::Bitcoin)
174+
} else if chain == ChainHash::using_genesis_block(Network::Testnet) {
175+
Some(Network::Testnet)
176+
} else if chain == ChainHash::using_genesis_block(Network::Signet) {
177+
Some(Network::Signet)
178+
} else if chain == ChainHash::using_genesis_block(Network::Regtest) {
179+
Some(Network::Regtest)
180+
} else {
181+
None
182+
}
183+
}
184+
185+
/// Features pertaining to paying an invoice.
186+
pub fn features(&self) -> &Bolt12InvoiceFeatures {
187+
&self.contents.fields().features
188+
}
189+
190+
/// The public key used to sign invoices.
191+
pub fn signing_pubkey(&self) -> PublicKey {
192+
self.contents.fields().signing_pubkey
193+
}
194+
195+
/// Signature of the invoice using [`Invoice::signing_pubkey`].
196+
pub fn signature(&self) -> Signature {
197+
self.signature
198+
}
199+
}
200+
201+
impl InvoiceContents {
202+
fn chain(&self) -> ChainHash {
203+
match self {
204+
InvoiceContents::ForOffer { invoice_request, .. } => invoice_request.chain(),
205+
InvoiceContents::ForRefund { refund, .. } => refund.chain(),
206+
}
207+
}
208+
209+
fn fields(&self) -> &InvoiceFields {
210+
match self {
211+
InvoiceContents::ForOffer { fields, .. } => fields,
212+
InvoiceContents::ForRefund { fields, .. } => fields,
213+
}
214+
}
215+
}
216+
217+
impl Writeable for Invoice {
218+
fn write<W: Writer>(&self, writer: &mut W) -> Result<(), io::Error> {
219+
WithoutLength(&self.bytes).write(writer)
220+
}
221+
}
222+
223+
impl TryFrom<Vec<u8>> for Invoice {
224+
type Error = ParseError;
225+
226+
fn try_from(bytes: Vec<u8>) -> Result<Self, Self::Error> {
227+
let parsed_invoice = ParsedMessage::<FullInvoiceTlvStream>::try_from(bytes)?;
228+
Invoice::try_from(parsed_invoice)
229+
}
230+
}
231+
232+
tlv_stream!(InvoiceTlvStream, InvoiceTlvStreamRef, 160..240, {
233+
(160, paths: (Vec<BlindedPath>, WithoutLength)),
234+
(162, blindedpay: (Vec<BlindedPayInfo>, WithoutLength)),
235+
(164, created_at: (u64, HighZeroBytesDroppedBigSize)),
236+
(166, relative_expiry: (u32, HighZeroBytesDroppedBigSize)),
237+
(168, payment_hash: PaymentHash),
238+
(170, amount: (u64, HighZeroBytesDroppedBigSize)),
239+
(172, fallbacks: (Vec<FallbackAddress>, WithoutLength)),
240+
(174, features: (Bolt12InvoiceFeatures, WithoutLength)),
241+
(176, node_id: PublicKey),
242+
});
243+
244+
/// Information needed to route a payment across a [`BlindedPath`] hop.
245+
#[derive(Debug, PartialEq)]
246+
pub struct BlindedPayInfo {
247+
fee_base_msat: u32,
248+
fee_proportional_millionths: u32,
249+
cltv_expiry_delta: u16,
250+
htlc_minimum_msat: u64,
251+
htlc_maximum_msat: u64,
252+
features: BlindedHopFeatures,
253+
}
254+
255+
impl_writeable!(BlindedPayInfo, {
256+
fee_base_msat,
257+
fee_proportional_millionths,
258+
cltv_expiry_delta,
259+
htlc_minimum_msat,
260+
htlc_maximum_msat,
261+
features
262+
});
263+
264+
/// Wire representation for an on-chain fallback address.
265+
#[derive(Debug, PartialEq)]
266+
pub(super) struct FallbackAddress {
267+
version: u8,
268+
program: Vec<u8>,
269+
}
270+
271+
impl_writeable!(FallbackAddress, { version, program });
272+
273+
type FullInvoiceTlvStream =
274+
(PayerTlvStream, OfferTlvStream, InvoiceRequestTlvStream, InvoiceTlvStream, SignatureTlvStream);
275+
276+
impl SeekReadable for FullInvoiceTlvStream {
277+
fn read<R: io::Read + io::Seek>(r: &mut R) -> Result<Self, DecodeError> {
278+
let payer = SeekReadable::read(r)?;
279+
let offer = SeekReadable::read(r)?;
280+
let invoice_request = SeekReadable::read(r)?;
281+
let invoice = SeekReadable::read(r)?;
282+
let signature = SeekReadable::read(r)?;
283+
284+
Ok((payer, offer, invoice_request, invoice, signature))
285+
}
286+
}
287+
288+
type PartialInvoiceTlvStream =
289+
(PayerTlvStream, OfferTlvStream, InvoiceRequestTlvStream, InvoiceTlvStream);
290+
291+
impl TryFrom<ParsedMessage<FullInvoiceTlvStream>> for Invoice {
292+
type Error = ParseError;
293+
294+
fn try_from(invoice: ParsedMessage<FullInvoiceTlvStream>) -> Result<Self, Self::Error> {
295+
let ParsedMessage { bytes, tlv_stream } = invoice;
296+
let (
297+
payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream,
298+
SignatureTlvStream { signature },
299+
) = tlv_stream;
300+
let contents = InvoiceContents::try_from(
301+
(payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream)
302+
)?;
303+
304+
let signature = match signature {
305+
None => return Err(ParseError::InvalidSemantics(SemanticError::MissingSignature)),
306+
Some(signature) => signature,
307+
};
308+
let pubkey = contents.fields().signing_pubkey;
309+
merkle::verify_signature(&signature, SIGNATURE_TAG, &bytes, pubkey)?;
310+
311+
Ok(Invoice { bytes, contents, signature })
312+
}
313+
}
314+
315+
impl TryFrom<PartialInvoiceTlvStream> for InvoiceContents {
316+
type Error = SemanticError;
317+
318+
fn try_from(tlv_stream: PartialInvoiceTlvStream) -> Result<Self, Self::Error> {
319+
let (
320+
payer_tlv_stream,
321+
offer_tlv_stream,
322+
invoice_request_tlv_stream,
323+
InvoiceTlvStream {
324+
paths, blindedpay, created_at, relative_expiry, payment_hash, amount, fallbacks,
325+
features, node_id,
326+
},
327+
) = tlv_stream;
328+
329+
let payment_paths = match (paths, blindedpay) {
330+
(None, _) => return Err(SemanticError::MissingPaths),
331+
(_, None) => return Err(SemanticError::InvalidPayInfo),
332+
(Some(paths), _) if paths.is_empty() => return Err(SemanticError::MissingPaths),
333+
(Some(paths), Some(blindedpay)) if paths.len() != blindedpay.len() => {
334+
return Err(SemanticError::InvalidPayInfo);
335+
},
336+
(Some(paths), Some(blindedpay)) => {
337+
paths.into_iter().zip(blindedpay.into_iter()).collect::<Vec<_>>()
338+
},
339+
};
340+
341+
let created_at = match created_at {
342+
None => return Err(SemanticError::MissingCreationTime),
343+
Some(timestamp) => Duration::from_secs(timestamp),
344+
};
345+
346+
let relative_expiry = relative_expiry
347+
.map(Into::<u64>::into)
348+
.map(Duration::from_secs);
349+
350+
let payment_hash = match payment_hash {
351+
None => return Err(SemanticError::MissingPaymentHash),
352+
Some(payment_hash) => payment_hash,
353+
};
354+
355+
let amount_msats = match amount {
356+
None => return Err(SemanticError::MissingAmount),
357+
Some(amount) => amount,
358+
};
359+
360+
let features = features.unwrap_or_else(Bolt12InvoiceFeatures::empty);
361+
362+
let signing_pubkey = match node_id {
363+
None => return Err(SemanticError::MissingSigningPubkey),
364+
Some(node_id) => node_id,
365+
};
366+
367+
let fields = InvoiceFields {
368+
payment_paths, created_at, relative_expiry, payment_hash, amount_msats, fallbacks,
369+
features, signing_pubkey,
370+
};
371+
372+
match offer_tlv_stream.node_id {
373+
Some(expected_signing_pubkey) => {
374+
if fields.signing_pubkey != expected_signing_pubkey {
375+
return Err(SemanticError::InvalidSigningPubkey);
376+
}
377+
378+
let invoice_request = InvoiceRequestContents::try_from(
379+
(payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream)
380+
)?;
381+
Ok(InvoiceContents::ForOffer { invoice_request, fields })
382+
},
383+
None => {
384+
let refund = RefundContents::try_from(
385+
(payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream)
386+
)?;
387+
Ok(InvoiceContents::ForRefund { refund, fields })
388+
},
389+
}
390+
}
391+
}

0 commit comments

Comments
 (0)