Static invoice encoding and parsing
[rust-lightning] / lightning / src / offers / static_invoice.rs
diff --git a/lightning/src/offers/static_invoice.rs b/lightning/src/offers/static_invoice.rs
new file mode 100644 (file)
index 0000000..0cf5e92
--- /dev/null
@@ -0,0 +1,352 @@
+// This file is Copyright its original authors, visible in version control
+// history.
+//
+// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
+// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
+// You may not use this file except in accordance with one or both of these
+// licenses.
+
+//! Data structures and encoding for static BOLT 12 invoices.
+
+use crate::blinded_path::BlindedPath;
+use crate::io;
+use crate::ln::features::{Bolt12InvoiceFeatures, OfferFeatures};
+use crate::ln::msgs::DecodeError;
+use crate::offers::invoice::{
+       check_invoice_signing_pubkey, construct_payment_paths, filter_fallbacks, BlindedPathIter,
+       BlindedPayInfo, BlindedPayInfoIter, FallbackAddress, InvoiceTlvStream, InvoiceTlvStreamRef,
+};
+use crate::offers::invoice_macros::invoice_accessors_common;
+use crate::offers::merkle::{self, SignatureTlvStream, TaggedHash};
+use crate::offers::offer::{Amount, OfferContents, OfferTlvStream, Quantity};
+use crate::offers::parse::{Bolt12ParseError, Bolt12SemanticError, ParsedMessage};
+use crate::util::ser::{
+       HighZeroBytesDroppedBigSize, Iterable, SeekReadable, WithoutLength, Writeable, Writer,
+};
+use crate::util::string::PrintableString;
+use bitcoin::address::Address;
+use bitcoin::blockdata::constants::ChainHash;
+use bitcoin::secp256k1::schnorr::Signature;
+use bitcoin::secp256k1::PublicKey;
+use core::time::Duration;
+
+#[cfg(feature = "std")]
+use crate::offers::invoice::is_expired;
+
+#[allow(unused_imports)]
+use crate::prelude::*;
+
+/// Static invoices default to expiring after 2 weeks.
+const DEFAULT_RELATIVE_EXPIRY: Duration = Duration::from_secs(3600 * 24 * 14);
+
+/// Tag for the hash function used when signing a [`StaticInvoice`]'s merkle root.
+pub const SIGNATURE_TAG: &'static str = concat!("lightning", "static_invoice", "signature");
+
+/// A `StaticInvoice` is a reusable payment request corresponding to an [`Offer`].
+///
+/// A static invoice may be sent in response to an [`InvoiceRequest`] and includes all the
+/// information needed to pay the recipient. However, unlike [`Bolt12Invoice`]s, static invoices do
+/// not provide proof-of-payment. Therefore, [`Bolt12Invoice`]s should be preferred when the
+/// recipient is online to provide one.
+///
+/// [`Offer`]: crate::offers::offer::Offer
+/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
+/// [`Bolt12Invoice`]: crate::offers::invoice::Bolt12Invoice
+#[derive(Clone, Debug)]
+pub struct StaticInvoice {
+       bytes: Vec<u8>,
+       contents: InvoiceContents,
+       signature: Signature,
+}
+
+/// The contents of a [`StaticInvoice`] for responding to an [`Offer`].
+///
+/// [`Offer`]: crate::offers::offer::Offer
+#[derive(Clone, Debug)]
+struct InvoiceContents {
+       offer: OfferContents,
+       payment_paths: Vec<(BlindedPayInfo, BlindedPath)>,
+       created_at: Duration,
+       relative_expiry: Option<Duration>,
+       fallbacks: Option<Vec<FallbackAddress>>,
+       features: Bolt12InvoiceFeatures,
+       signing_pubkey: PublicKey,
+       message_paths: Vec<BlindedPath>,
+}
+
+macro_rules! invoice_accessors { ($self: ident, $contents: expr) => {
+       /// The chain that must be used when paying the invoice. [`StaticInvoice`]s currently can only be
+       /// created from offers that support a single chain.
+       pub fn chain(&$self) -> ChainHash {
+               $contents.chain()
+       }
+
+       /// Opaque bytes set by the originating [`Offer::metadata`].
+       ///
+       /// [`Offer::metadata`]: crate::offers::offer::Offer::metadata
+       pub fn metadata(&$self) -> Option<&Vec<u8>> {
+               $contents.metadata()
+       }
+
+       /// The minimum amount required for a successful payment of a single item.
+       ///
+       /// From [`Offer::amount`].
+       ///
+       /// [`Offer::amount`]: crate::offers::offer::Offer::amount
+       pub fn amount(&$self) -> Option<Amount> {
+               $contents.amount()
+       }
+
+       /// Features pertaining to the originating [`Offer`], from [`Offer::offer_features`].
+       ///
+       /// [`Offer`]: crate::offers::offer::Offer
+       /// [`Offer::offer_features`]: crate::offers::offer::Offer::offer_features
+       pub fn offer_features(&$self) -> &OfferFeatures {
+               $contents.offer_features()
+       }
+
+       /// A complete description of the purpose of the originating offer, from [`Offer::description`].
+       ///
+       /// [`Offer::description`]: crate::offers::offer::Offer::description
+       pub fn description(&$self) -> Option<PrintableString> {
+               $contents.description()
+       }
+
+       /// Duration since the Unix epoch when an invoice should no longer be requested, from
+       /// [`Offer::absolute_expiry`].
+       ///
+       /// [`Offer::absolute_expiry`]: crate::offers::offer::Offer::absolute_expiry
+       pub fn absolute_expiry(&$self) -> Option<Duration> {
+               $contents.absolute_expiry()
+       }
+
+       /// The issuer of the offer, from [`Offer::issuer`].
+       ///
+       /// [`Offer::issuer`]: crate::offers::offer::Offer::issuer
+       pub fn issuer(&$self) -> Option<PrintableString> {
+               $contents.issuer()
+       }
+
+       /// Paths to the node that may supply the invoice on the recipient's behalf, originating from
+       /// publicly reachable nodes. Taken from [`Offer::paths`].
+       ///
+       /// [`Offer::paths`]: crate::offers::offer::Offer::paths
+       pub fn offer_message_paths(&$self) -> &[BlindedPath] {
+               $contents.offer_message_paths()
+       }
+
+       /// Paths to the recipient for indicating that a held HTLC is available to claim when they next
+       /// come online.
+       pub fn message_paths(&$self) -> &[BlindedPath] {
+               $contents.message_paths()
+       }
+
+       /// The quantity of items supported, from [`Offer::supported_quantity`].
+       ///
+       /// [`Offer::supported_quantity`]: crate::offers::offer::Offer::supported_quantity
+       pub fn supported_quantity(&$self) -> Quantity {
+               $contents.supported_quantity()
+       }
+} }
+
+impl StaticInvoice {
+       invoice_accessors_common!(self, self.contents, StaticInvoice);
+       invoice_accessors!(self, self.contents);
+
+       /// Signature of the invoice verified using [`StaticInvoice::signing_pubkey`].
+       pub fn signature(&self) -> Signature {
+               self.signature
+       }
+}
+
+impl InvoiceContents {
+       fn chain(&self) -> ChainHash {
+               debug_assert_eq!(self.offer.chains().len(), 1);
+               self.offer.chains().first().cloned().unwrap_or_else(|| self.offer.implied_chain())
+       }
+
+       fn metadata(&self) -> Option<&Vec<u8>> {
+               self.offer.metadata()
+       }
+
+       fn amount(&self) -> Option<Amount> {
+               self.offer.amount()
+       }
+
+       fn offer_features(&self) -> &OfferFeatures {
+               self.offer.features()
+       }
+
+       fn description(&self) -> Option<PrintableString> {
+               self.offer.description()
+       }
+
+       fn absolute_expiry(&self) -> Option<Duration> {
+               self.offer.absolute_expiry()
+       }
+
+       fn issuer(&self) -> Option<PrintableString> {
+               self.offer.issuer()
+       }
+
+       fn offer_message_paths(&self) -> &[BlindedPath] {
+               self.offer.paths()
+       }
+
+       fn message_paths(&self) -> &[BlindedPath] {
+               &self.message_paths[..]
+       }
+
+       fn supported_quantity(&self) -> Quantity {
+               self.offer.supported_quantity()
+       }
+
+       fn payment_paths(&self) -> &[(BlindedPayInfo, BlindedPath)] {
+               &self.payment_paths[..]
+       }
+
+       fn created_at(&self) -> Duration {
+               self.created_at
+       }
+
+       fn relative_expiry(&self) -> Duration {
+               self.relative_expiry.unwrap_or(DEFAULT_RELATIVE_EXPIRY)
+       }
+
+       #[cfg(feature = "std")]
+       fn is_expired(&self) -> bool {
+               is_expired(self.created_at(), self.relative_expiry())
+       }
+
+       fn fallbacks(&self) -> Vec<Address> {
+               let chain = self.chain();
+               self.fallbacks
+                       .as_ref()
+                       .map(|fallbacks| filter_fallbacks(chain, fallbacks))
+                       .unwrap_or_else(Vec::new)
+       }
+
+       fn features(&self) -> &Bolt12InvoiceFeatures {
+               &self.features
+       }
+
+       fn signing_pubkey(&self) -> PublicKey {
+               self.signing_pubkey
+       }
+}
+
+impl Writeable for StaticInvoice {
+       fn write<W: Writer>(&self, writer: &mut W) -> Result<(), io::Error> {
+               WithoutLength(&self.bytes).write(writer)
+       }
+}
+
+impl TryFrom<Vec<u8>> for StaticInvoice {
+       type Error = Bolt12ParseError;
+
+       fn try_from(bytes: Vec<u8>) -> Result<Self, Self::Error> {
+               let parsed_invoice = ParsedMessage::<FullInvoiceTlvStream>::try_from(bytes)?;
+               StaticInvoice::try_from(parsed_invoice)
+       }
+}
+
+type FullInvoiceTlvStream = (OfferTlvStream, InvoiceTlvStream, SignatureTlvStream);
+
+impl SeekReadable for FullInvoiceTlvStream {
+       fn read<R: io::Read + io::Seek>(r: &mut R) -> Result<Self, DecodeError> {
+               let offer = SeekReadable::read(r)?;
+               let invoice = SeekReadable::read(r)?;
+               let signature = SeekReadable::read(r)?;
+
+               Ok((offer, invoice, signature))
+       }
+}
+
+type PartialInvoiceTlvStream = (OfferTlvStream, InvoiceTlvStream);
+
+impl TryFrom<ParsedMessage<FullInvoiceTlvStream>> for StaticInvoice {
+       type Error = Bolt12ParseError;
+
+       fn try_from(invoice: ParsedMessage<FullInvoiceTlvStream>) -> Result<Self, Self::Error> {
+               let ParsedMessage { bytes, tlv_stream } = invoice;
+               let (offer_tlv_stream, invoice_tlv_stream, SignatureTlvStream { signature }) = tlv_stream;
+               let contents = InvoiceContents::try_from((offer_tlv_stream, invoice_tlv_stream))?;
+
+               let signature = match signature {
+                       None => {
+                               return Err(Bolt12ParseError::InvalidSemantics(
+                                       Bolt12SemanticError::MissingSignature,
+                               ))
+                       },
+                       Some(signature) => signature,
+               };
+               let tagged_hash = TaggedHash::from_valid_tlv_stream_bytes(SIGNATURE_TAG, &bytes);
+               let pubkey = contents.signing_pubkey;
+               merkle::verify_signature(&signature, &tagged_hash, pubkey)?;
+
+               Ok(StaticInvoice { bytes, contents, signature })
+       }
+}
+
+impl TryFrom<PartialInvoiceTlvStream> for InvoiceContents {
+       type Error = Bolt12SemanticError;
+
+       fn try_from(tlv_stream: PartialInvoiceTlvStream) -> Result<Self, Self::Error> {
+               let (
+                       offer_tlv_stream,
+                       InvoiceTlvStream {
+                               paths,
+                               blindedpay,
+                               created_at,
+                               relative_expiry,
+                               fallbacks,
+                               features,
+                               node_id,
+                               message_paths,
+                               payment_hash,
+                               amount,
+                       },
+               ) = tlv_stream;
+
+               if payment_hash.is_some() {
+                       return Err(Bolt12SemanticError::UnexpectedPaymentHash);
+               }
+               if amount.is_some() {
+                       return Err(Bolt12SemanticError::UnexpectedAmount);
+               }
+
+               let payment_paths = construct_payment_paths(blindedpay, paths)?;
+               let message_paths = message_paths.ok_or(Bolt12SemanticError::MissingPaths)?;
+
+               let created_at = match created_at {
+                       None => return Err(Bolt12SemanticError::MissingCreationTime),
+                       Some(timestamp) => Duration::from_secs(timestamp),
+               };
+
+               let relative_expiry = relative_expiry.map(Into::<u64>::into).map(Duration::from_secs);
+
+               let features = features.unwrap_or_else(Bolt12InvoiceFeatures::empty);
+
+               let signing_pubkey = node_id.ok_or(Bolt12SemanticError::MissingSigningPubkey)?;
+               check_invoice_signing_pubkey(&signing_pubkey, &offer_tlv_stream)?;
+
+               if offer_tlv_stream.paths.is_none() {
+                       return Err(Bolt12SemanticError::MissingPaths);
+               }
+               if offer_tlv_stream.chains.as_ref().map_or(0, |chains| chains.len()) > 1 {
+                       return Err(Bolt12SemanticError::UnexpectedChain);
+               }
+
+               Ok(InvoiceContents {
+                       offer: OfferContents::try_from(offer_tlv_stream)?,
+                       payment_paths,
+                       message_paths,
+                       created_at,
+                       relative_expiry,
+                       fallbacks,
+                       features,
+                       signing_pubkey,
+               })
+       }
+}