From: Jeffrey Czyz Date: Mon, 12 Sep 2022 14:30:06 +0000 (-0500) Subject: Invoice encoding and parsing X-Git-Tag: v0.0.114-beta~49^2~7 X-Git-Url: http://git.bitcoin.ninja/?a=commitdiff_plain;h=e1aa18aed800d575da555df1d84a468cd585f3f3;p=rust-lightning 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. --- diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs new file mode 100644 index 000000000..8264aa15f --- /dev/null +++ b/lightning/src/offers/invoice.rs @@ -0,0 +1,391 @@ +// This file is Copyright its original authors, visible in version control +// history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +//! Data structures and encoding for `invoice` messages. + +use bitcoin::blockdata::constants::ChainHash; +use bitcoin::network::constants::Network; +use bitcoin::secp256k1::PublicKey; +use bitcoin::secp256k1::schnorr::Signature; +use bitcoin::util::address::{Address, Payload, WitnessVersion}; +use core::convert::TryFrom; +use core::time::Duration; +use crate::io; +use crate::ln::PaymentHash; +use crate::ln::features::{BlindedHopFeatures, Bolt12InvoiceFeatures}; +use crate::ln::msgs::DecodeError; +use crate::offers::invoice_request::{InvoiceRequestContents, InvoiceRequestTlvStream}; +use crate::offers::merkle::{SignatureTlvStream, self}; +use crate::offers::offer::OfferTlvStream; +use crate::offers::parse::{ParseError, ParsedMessage, SemanticError}; +use crate::offers::payer::PayerTlvStream; +use crate::offers::refund::RefundContents; +use crate::onion_message::BlindedPath; +use crate::util::ser::{HighZeroBytesDroppedBigSize, SeekReadable, WithoutLength, Writeable, Writer}; + +use crate::prelude::*; + +#[cfg(feature = "std")] +use std::time::SystemTime; + +const DEFAULT_RELATIVE_EXPIRY: Duration = Duration::from_secs(7200); + +const SIGNATURE_TAG: &'static str = concat!("lightning", "invoice", "signature"); + +/// An `Invoice` is a payment request, typically corresponding to an [`Offer`] or a [`Refund`]. +/// +/// An invoice may be sent in response to an [`InvoiceRequest`] in the case of an offer or sent +/// directly after scanning a refund. It includes all the information needed to pay a recipient. +/// +/// [`Offer`]: crate::offers::offer::Offer +/// [`Refund`]: crate::offers::refund::Refund +/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest +pub struct Invoice { + bytes: Vec, + contents: InvoiceContents, + signature: Signature, +} + +/// The contents of an [`Invoice`] for responding to either an [`Offer`] or a [`Refund`]. +/// +/// [`Offer`]: crate::offers::offer::Offer +/// [`Refund`]: crate::offers::refund::Refund +enum InvoiceContents { + /// Contents for an [`Invoice`] corresponding to an [`Offer`]. + /// + /// [`Offer`]: crate::offers::offer::Offer + ForOffer { + invoice_request: InvoiceRequestContents, + fields: InvoiceFields, + }, + /// Contents for an [`Invoice`] corresponding to a [`Refund`]. + /// + /// [`Refund`]: crate::offers::refund::Refund + ForRefund { + refund: RefundContents, + fields: InvoiceFields, + }, +} + +/// Invoice-specific fields for an `invoice` message. +struct InvoiceFields { + payment_paths: Vec<(BlindedPath, BlindedPayInfo)>, + created_at: Duration, + relative_expiry: Option, + payment_hash: PaymentHash, + amount_msats: u64, + fallbacks: Option>, + features: Bolt12InvoiceFeatures, + signing_pubkey: PublicKey, +} + +impl Invoice { + /// Paths to the recipient originating from publicly reachable nodes, including information + /// needed for routing payments across them. Blinded paths provide recipient privacy by + /// obfuscating its node id. + pub fn payment_paths(&self) -> &[(BlindedPath, BlindedPayInfo)] { + &self.contents.fields().payment_paths[..] + } + + /// Duration since the Unix epoch when the invoice was created. + pub fn created_at(&self) -> Duration { + self.contents.fields().created_at + } + + /// Duration since [`Invoice::created_at`] when the invoice has expired and therefore should no + /// longer be paid. + pub fn relative_expiry(&self) -> Duration { + self.contents.fields().relative_expiry.unwrap_or(DEFAULT_RELATIVE_EXPIRY) + } + + /// Whether the invoice has expired. + #[cfg(feature = "std")] + pub fn is_expired(&self) -> bool { + let absolute_expiry = self.created_at().checked_add(self.relative_expiry()); + match absolute_expiry { + Some(seconds_from_epoch) => match SystemTime::UNIX_EPOCH.elapsed() { + Ok(elapsed) => elapsed > seconds_from_epoch, + Err(_) => false, + }, + None => false, + } + } + + /// SHA256 hash of the payment preimage that will be given in return for paying the invoice. + pub fn payment_hash(&self) -> PaymentHash { + self.contents.fields().payment_hash + } + + /// The minimum amount required for a successful payment of the invoice. + pub fn amount_msats(&self) -> u64 { + self.contents.fields().amount_msats + } + + /// Fallback addresses for paying the invoice on-chain, in order of most-preferred to + /// least-preferred. + pub fn fallbacks(&self) -> Vec
{ + let network = match self.network() { + None => return Vec::new(), + Some(network) => network, + }; + + let to_valid_address = |address: &FallbackAddress| { + let version = match WitnessVersion::try_from(address.version) { + Ok(version) => version, + Err(_) => return None, + }; + + let program = &address.program; + if program.len() < 2 || program.len() > 40 { + return None; + } + + let address = Address { + payload: Payload::WitnessProgram { + version, + program: address.program.clone(), + }, + network, + }; + + if !address.is_standard() && version == WitnessVersion::V0 { + return None; + } + + Some(address) + }; + + self.contents.fields().fallbacks + .as_ref() + .map(|fallbacks| fallbacks.iter().filter_map(to_valid_address).collect()) + .unwrap_or_else(Vec::new) + } + + fn network(&self) -> Option { + let chain = self.contents.chain(); + if chain == ChainHash::using_genesis_block(Network::Bitcoin) { + Some(Network::Bitcoin) + } else if chain == ChainHash::using_genesis_block(Network::Testnet) { + Some(Network::Testnet) + } else if chain == ChainHash::using_genesis_block(Network::Signet) { + Some(Network::Signet) + } else if chain == ChainHash::using_genesis_block(Network::Regtest) { + Some(Network::Regtest) + } else { + None + } + } + + /// Features pertaining to paying an invoice. + pub fn features(&self) -> &Bolt12InvoiceFeatures { + &self.contents.fields().features + } + + /// The public key used to sign invoices. + pub fn signing_pubkey(&self) -> PublicKey { + self.contents.fields().signing_pubkey + } + + /// Signature of the invoice using [`Invoice::signing_pubkey`]. + pub fn signature(&self) -> Signature { + self.signature + } +} + +impl InvoiceContents { + fn chain(&self) -> ChainHash { + match self { + InvoiceContents::ForOffer { invoice_request, .. } => invoice_request.chain(), + InvoiceContents::ForRefund { refund, .. } => refund.chain(), + } + } + + fn fields(&self) -> &InvoiceFields { + match self { + InvoiceContents::ForOffer { fields, .. } => fields, + InvoiceContents::ForRefund { fields, .. } => fields, + } + } +} + +impl Writeable for Invoice { + fn write(&self, writer: &mut W) -> Result<(), io::Error> { + WithoutLength(&self.bytes).write(writer) + } +} + +impl TryFrom> for Invoice { + type Error = ParseError; + + fn try_from(bytes: Vec) -> Result { + let parsed_invoice = ParsedMessage::::try_from(bytes)?; + Invoice::try_from(parsed_invoice) + } +} + +tlv_stream!(InvoiceTlvStream, InvoiceTlvStreamRef, 160..240, { + (160, paths: (Vec, WithoutLength)), + (162, blindedpay: (Vec, WithoutLength)), + (164, created_at: (u64, HighZeroBytesDroppedBigSize)), + (166, relative_expiry: (u32, HighZeroBytesDroppedBigSize)), + (168, payment_hash: PaymentHash), + (170, amount: (u64, HighZeroBytesDroppedBigSize)), + (172, fallbacks: (Vec, WithoutLength)), + (174, features: (Bolt12InvoiceFeatures, WithoutLength)), + (176, node_id: PublicKey), +}); + +/// Information needed to route a payment across a [`BlindedPath`] hop. +#[derive(Debug, PartialEq)] +pub struct BlindedPayInfo { + fee_base_msat: u32, + fee_proportional_millionths: u32, + cltv_expiry_delta: u16, + htlc_minimum_msat: u64, + htlc_maximum_msat: u64, + features: BlindedHopFeatures, +} + +impl_writeable!(BlindedPayInfo, { + fee_base_msat, + fee_proportional_millionths, + cltv_expiry_delta, + htlc_minimum_msat, + htlc_maximum_msat, + features +}); + +/// Wire representation for an on-chain fallback address. +#[derive(Debug, PartialEq)] +pub(super) struct FallbackAddress { + version: u8, + program: Vec, +} + +impl_writeable!(FallbackAddress, { version, program }); + +type FullInvoiceTlvStream = + (PayerTlvStream, OfferTlvStream, InvoiceRequestTlvStream, InvoiceTlvStream, SignatureTlvStream); + +impl SeekReadable for FullInvoiceTlvStream { + fn read(r: &mut R) -> Result { + let payer = SeekReadable::read(r)?; + let offer = SeekReadable::read(r)?; + let invoice_request = SeekReadable::read(r)?; + let invoice = SeekReadable::read(r)?; + let signature = SeekReadable::read(r)?; + + Ok((payer, offer, invoice_request, invoice, signature)) + } +} + +type PartialInvoiceTlvStream = + (PayerTlvStream, OfferTlvStream, InvoiceRequestTlvStream, InvoiceTlvStream); + +impl TryFrom> for Invoice { + type Error = ParseError; + + fn try_from(invoice: ParsedMessage) -> Result { + let ParsedMessage { bytes, tlv_stream } = invoice; + let ( + payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream, + SignatureTlvStream { signature }, + ) = tlv_stream; + let contents = InvoiceContents::try_from( + (payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream) + )?; + + let signature = match signature { + None => return Err(ParseError::InvalidSemantics(SemanticError::MissingSignature)), + Some(signature) => signature, + }; + let pubkey = contents.fields().signing_pubkey; + merkle::verify_signature(&signature, SIGNATURE_TAG, &bytes, pubkey)?; + + Ok(Invoice { bytes, contents, signature }) + } +} + +impl TryFrom for InvoiceContents { + type Error = SemanticError; + + fn try_from(tlv_stream: PartialInvoiceTlvStream) -> Result { + let ( + payer_tlv_stream, + offer_tlv_stream, + invoice_request_tlv_stream, + InvoiceTlvStream { + paths, blindedpay, created_at, relative_expiry, payment_hash, amount, fallbacks, + features, node_id, + }, + ) = tlv_stream; + + let payment_paths = match (paths, blindedpay) { + (None, _) => return Err(SemanticError::MissingPaths), + (_, None) => return Err(SemanticError::InvalidPayInfo), + (Some(paths), _) if paths.is_empty() => return Err(SemanticError::MissingPaths), + (Some(paths), Some(blindedpay)) if paths.len() != blindedpay.len() => { + return Err(SemanticError::InvalidPayInfo); + }, + (Some(paths), Some(blindedpay)) => { + paths.into_iter().zip(blindedpay.into_iter()).collect::>() + }, + }; + + let created_at = match created_at { + None => return Err(SemanticError::MissingCreationTime), + Some(timestamp) => Duration::from_secs(timestamp), + }; + + let relative_expiry = relative_expiry + .map(Into::::into) + .map(Duration::from_secs); + + let payment_hash = match payment_hash { + None => return Err(SemanticError::MissingPaymentHash), + Some(payment_hash) => payment_hash, + }; + + let amount_msats = match amount { + None => return Err(SemanticError::MissingAmount), + Some(amount) => amount, + }; + + let features = features.unwrap_or_else(Bolt12InvoiceFeatures::empty); + + let signing_pubkey = match node_id { + None => return Err(SemanticError::MissingSigningPubkey), + Some(node_id) => node_id, + }; + + let fields = InvoiceFields { + payment_paths, created_at, relative_expiry, payment_hash, amount_msats, fallbacks, + features, signing_pubkey, + }; + + match offer_tlv_stream.node_id { + Some(expected_signing_pubkey) => { + if fields.signing_pubkey != expected_signing_pubkey { + return Err(SemanticError::InvalidSigningPubkey); + } + + let invoice_request = InvoiceRequestContents::try_from( + (payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream) + )?; + Ok(InvoiceContents::ForOffer { invoice_request, fields }) + }, + None => { + let refund = RefundContents::try_from( + (payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream) + )?; + Ok(InvoiceContents::ForRefund { refund, fields }) + }, + } + } +} diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs index fd5ecda55..21c11bcbf 100644 --- a/lightning/src/offers/invoice_request.rs +++ b/lightning/src/offers/invoice_request.rs @@ -11,11 +11,12 @@ //! //! An [`InvoiceRequest`] can be built from a parsed [`Offer`] as an "offer to be paid". It is //! typically constructed by a customer and sent to the merchant who had published the corresponding -//! offer. The recipient of the request responds with an `Invoice`. +//! offer. The recipient of the request responds with an [`Invoice`]. //! //! For an "offer for money" (e.g., refund, ATM withdrawal), where an offer doesn't exist as a //! precursor, see [`Refund`]. //! +//! [`Invoice`]: crate::offers::invoice::Invoice //! [`Refund`]: crate::offers::refund::Refund //! //! ```ignore @@ -239,11 +240,12 @@ impl<'a> UnsignedInvoiceRequest<'a> { } } -/// An `InvoiceRequest` is a request for an `Invoice` formulated from an [`Offer`]. +/// An `InvoiceRequest` is a request for an [`Invoice`] formulated from an [`Offer`]. /// /// An offer may provide choices such as quantity, amount, chain, features, etc. An invoice request /// specifies these such that its recipient can send an invoice for payment. /// +/// [`Invoice`]: crate::offers::invoice::Invoice /// [`Offer`]: crate::offers::offer::Offer #[derive(Clone, Debug)] pub struct InvoiceRequest { @@ -252,11 +254,13 @@ pub struct InvoiceRequest { signature: Signature, } -/// The contents of an [`InvoiceRequest`], which may be shared with an `Invoice`. +/// The contents of an [`InvoiceRequest`], which may be shared with an [`Invoice`]. +/// +/// [`Invoice`]: crate::offers::invoice::Invoice #[derive(Clone, Debug)] pub(super) struct InvoiceRequestContents { payer: PayerContents, - offer: OfferContents, + pub(super) offer: OfferContents, chain: Option, amount_msats: Option, features: InvoiceRequestFeatures, @@ -327,7 +331,7 @@ impl InvoiceRequest { } impl InvoiceRequestContents { - fn chain(&self) -> ChainHash { + pub(super) fn chain(&self) -> ChainHash { self.chain.unwrap_or_else(|| self.offer.implied_chain()) } diff --git a/lightning/src/offers/mod.rs b/lightning/src/offers/mod.rs index 11df5ca1f..2da6fac08 100644 --- a/lightning/src/offers/mod.rs +++ b/lightning/src/offers/mod.rs @@ -12,6 +12,7 @@ //! //! Offers are a flexible protocol for Lightning payments. +pub mod invoice; pub mod invoice_request; mod merkle; pub mod offer; diff --git a/lightning/src/offers/offer.rs b/lightning/src/offers/offer.rs index e65500446..d28a0bdd9 100644 --- a/lightning/src/offers/offer.rs +++ b/lightning/src/offers/offer.rs @@ -232,7 +232,7 @@ impl OfferBuilder { /// An `Offer` is a potentially long-lived proposal for payment of a good or service. /// /// An offer is a precursor to an [`InvoiceRequest`]. A merchant publishes an offer from which a -/// customer may request an `Invoice` for a specific quantity and using an amount sufficient to +/// customer may request an [`Invoice`] for a specific quantity and using an amount sufficient to /// cover that quantity (i.e., at least `quantity * amount`). See [`Offer::amount`]. /// /// Offers may be denominated in currency other than bitcoin but are ultimately paid using the @@ -241,6 +241,7 @@ impl OfferBuilder { /// Through the use of [`BlindedPath`]s, offers provide recipient privacy. /// /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest +/// [`Invoice`]: crate::offers::invoice::Invoice #[derive(Clone, Debug)] pub struct Offer { // The serialized offer. Needed when creating an `InvoiceRequest` if the offer contains unknown @@ -249,9 +250,10 @@ pub struct Offer { pub(super) contents: OfferContents, } -/// The contents of an [`Offer`], which may be shared with an [`InvoiceRequest`] or an `Invoice`. +/// The contents of an [`Offer`], which may be shared with an [`InvoiceRequest`] or an [`Invoice`]. /// /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest +/// [`Invoice`]: crate::offers::invoice::Invoice #[derive(Clone, Debug)] pub(super) struct OfferContents { chains: Option>, @@ -359,7 +361,7 @@ impl Offer { /// The public key used by the recipient to sign invoices. pub fn signing_pubkey(&self) -> PublicKey { - self.contents.signing_pubkey + self.contents.signing_pubkey() } /// Creates an [`InvoiceRequest`] for the offer with the given `metadata` and `payer_id`, which @@ -473,6 +475,10 @@ impl OfferContents { } } + pub(super) fn signing_pubkey(&self) -> PublicKey { + self.signing_pubkey + } + pub(super) fn as_tlv_stream(&self) -> OfferTlvStreamRef { let (currency, amount) = match &self.amount { None => (None, None), diff --git a/lightning/src/offers/parse.rs b/lightning/src/offers/parse.rs index deada66b0..a7d13e570 100644 --- a/lightning/src/offers/parse.rs +++ b/lightning/src/offers/parse.rs @@ -147,6 +147,8 @@ pub enum SemanticError { MissingDescription, /// A signing pubkey was not provided. MissingSigningPubkey, + /// A signing pubkey was provided but a different one was expected. + InvalidSigningPubkey, /// A signing pubkey was provided but was not expected. UnexpectedSigningPubkey, /// A quantity was expected but was missing. @@ -159,6 +161,14 @@ pub enum SemanticError { MissingPayerMetadata, /// A payer id was expected but was missing. MissingPayerId, + /// Blinded paths were expected but were missing. + MissingPaths, + /// The blinded payinfo given does not match the number of blinded path hops. + InvalidPayInfo, + /// An invoice creation time was expected but was missing. + MissingCreationTime, + /// An invoice payment hash was expected but was missing. + MissingPaymentHash, /// A signature was expected but was missing. MissingSignature, } diff --git a/lightning/src/offers/refund.rs b/lightning/src/offers/refund.rs index d3798463b..f864e73a3 100644 --- a/lightning/src/offers/refund.rs +++ b/lightning/src/offers/refund.rs @@ -10,10 +10,11 @@ //! Data structures and encoding for refunds. //! //! A [`Refund`] is an "offer for money" and is typically constructed by a merchant and presented -//! directly to the customer. The recipient responds with an `Invoice` to be paid. +//! directly to the customer. The recipient responds with an [`Invoice`] to be paid. //! //! This is an [`InvoiceRequest`] produced *not* in response to an [`Offer`]. //! +//! [`Invoice`]: crate::offers::invoice::Invoice //! [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest //! [`Offer`]: crate::offers::offer::Offer //! @@ -191,12 +192,13 @@ impl RefundBuilder { } } -/// A `Refund` is a request to send an `Invoice` without a preceding [`Offer`]. +/// A `Refund` is a request to send an [`Invoice`] without a preceding [`Offer`]. /// /// Typically, after an invoice is paid, the recipient may publish a refund allowing the sender to /// recoup their funds. A refund may be used more generally as an "offer for money", such as with a /// bitcoin ATM. /// +/// [`Invoice`]: crate::offers::invoice::Invoice /// [`Offer`]: crate::offers::offer::Offer #[derive(Clone, Debug)] pub struct Refund { @@ -204,9 +206,11 @@ pub struct Refund { contents: RefundContents, } -/// The contents of a [`Refund`], which may be shared with an `Invoice`. +/// The contents of a [`Refund`], which may be shared with an [`Invoice`]. +/// +/// [`Invoice`]: crate::offers::invoice::Invoice #[derive(Clone, Debug)] -struct RefundContents { +pub(super) struct RefundContents { payer: PayerContents, // offer fields metadata: Option>, @@ -311,7 +315,7 @@ impl AsRef<[u8]> for Refund { } impl RefundContents { - fn chain(&self) -> ChainHash { + pub(super) fn chain(&self) -> ChainHash { self.chain.unwrap_or_else(|| self.implied_chain()) }