From 98e6efc8893a0df809d65b95ec959180bb568f4b Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Tue, 9 Aug 2022 17:24:10 -0500 Subject: [PATCH] Offer message interface and data format Define an interface for BOLT 12 `offer` messages. The underlying format consists of the original bytes and the parsed contents. The bytes are later needed when constructing an `invoice_request` message. This is because it must mirror all the `offer` TLV records, including unknown ones, which aren't represented in the contents. The contents will be used in `invoice_request` messages to avoid duplication. Some fields while required in a typical user-pays-merchant flow may not be necessary in the merchant-pays-user flow (i.e., refund). --- lightning/src/lib.rs | 1 + lightning/src/offers/mod.rs | 15 ++ lightning/src/offers/offer.rs | 152 +++++++++++++++++++ lightning/src/onion_message/blinded_route.rs | 6 +- lightning/src/onion_message/mod.rs | 1 + lightning/src/util/ser.rs | 3 +- 6 files changed, 175 insertions(+), 3 deletions(-) create mode 100644 lightning/src/offers/mod.rs create mode 100644 lightning/src/offers/offer.rs diff --git a/lightning/src/lib.rs b/lightning/src/lib.rs index 045a8f73c..290e2ed24 100644 --- a/lightning/src/lib.rs +++ b/lightning/src/lib.rs @@ -78,6 +78,7 @@ extern crate core; pub mod util; pub mod chain; pub mod ln; +pub mod offers; pub mod routing; pub mod onion_message; diff --git a/lightning/src/offers/mod.rs b/lightning/src/offers/mod.rs new file mode 100644 index 000000000..2f961a0bb --- /dev/null +++ b/lightning/src/offers/mod.rs @@ -0,0 +1,15 @@ +// 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. + +//! Implementation of Lightning Offers +//! ([BOLT 12](https://github.com/lightning/bolts/blob/master/12-offer-encoding.md)). +//! +//! Offers are a flexible protocol for Lightning payments. + +pub mod offer; diff --git a/lightning/src/offers/offer.rs b/lightning/src/offers/offer.rs new file mode 100644 index 000000000..ec22528b9 --- /dev/null +++ b/lightning/src/offers/offer.rs @@ -0,0 +1,152 @@ +// 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 `offer` messages. + +use bitcoin::blockdata::constants::ChainHash; +use bitcoin::network::constants::Network; +use bitcoin::secp256k1::PublicKey; +use core::time::Duration; +use ln::features::OfferFeatures; +use onion_message::BlindedPath; + +use prelude::*; + +#[cfg(feature = "std")] +use std::time::SystemTime; + +/// An `Offer` is a potentially long-lived proposal for payment of a good or service. +/// +/// An offer is precursor to an `InvoiceRequest`. A merchant publishes an offer from which a +/// customer may request an `Invoice` for a specific quantity and using a specific amount. Offers +/// may be denominated in currency other than bitcoin but are ultimately paid using the latter. +/// +/// Through the use of [`BlindedPath`]s, offers provide recipient privacy. +#[derive(Clone, Debug)] +pub struct Offer { + // The serialized offer. Needed when creating an `InvoiceRequest` if the offer contains unknown + // fields. + bytes: Vec, + contents: OfferContents, +} + +/// The contents of an [`Offer`], which may be shared with an `InvoiceRequest` or an `Invoice`. +#[derive(Clone, Debug)] +pub(crate) struct OfferContents { + chains: Option>, + metadata: Option>, + amount: Option, + description: String, + features: OfferFeatures, + absolute_expiry: Option, + issuer: Option, + paths: Option>, + quantity_min: Option, + quantity_max: Option, + signing_pubkey: Option, +} + +impl Offer { + // TODO: Return a slice once ChainHash has constants. + // - https://github.com/rust-bitcoin/rust-bitcoin/pull/1283 + // - https://github.com/rust-bitcoin/rust-bitcoin/pull/1286 + /// The chains that may be used when paying a requested invoice. + pub fn chains(&self) -> Vec { + self.contents.chains + .as_ref() + .cloned() + .unwrap_or_else(|| vec![ChainHash::using_genesis_block(Network::Bitcoin)]) + } + + /// Metadata set by the originator. Useful for authentication and validating fields. + pub fn metadata(&self) -> Option<&Vec> { + self.contents.metadata.as_ref() + } + + /// The minimum amount required for a successful payment. + pub fn amount(&self) -> Option<&Amount> { + self.contents.amount.as_ref() + } + + /// A complete description of the purpose of the payment. + pub fn description(&self) -> &str { + &self.contents.description + } + + /// Features for paying the invoice. + pub fn features(&self) -> &OfferFeatures { + &self.contents.features + } + + /// Duration since the Unix epoch when an invoice should no longer be requested. + /// + /// If `None`, the offer does not expire. + pub fn absolute_expiry(&self) -> Option { + self.contents.absolute_expiry + } + + /// Whether the offer has expired. + #[cfg(feature = "std")] + pub fn is_expired(&self) -> bool { + match self.absolute_expiry() { + Some(seconds_from_epoch) => match SystemTime::UNIX_EPOCH.elapsed() { + Ok(elapsed) => elapsed > seconds_from_epoch, + Err(_) => false, + }, + None => false, + } + } + + /// The issuer of the offer, possibly beginning with `user@domain` or `domain`. + pub fn issuer(&self) -> Option<&str> { + self.contents.issuer.as_ref().map(|issuer| issuer.as_str()) + } + + /// Paths to the recipient originating from publicly reachable nodes. + pub fn paths(&self) -> &[BlindedPath] { + self.contents.paths.as_ref().map(|paths| paths.as_slice()).unwrap_or(&[]) + } + + /// The minimum quantity of items supported. + pub fn quantity_min(&self) -> u64 { + self.contents.quantity_min.unwrap_or(1) + } + + /// The maximum quantity of items supported. + pub fn quantity_max(&self) -> u64 { + self.contents.quantity_max.unwrap_or_else(|| + self.contents.quantity_min.map_or(1, |_| u64::max_value())) + } + + /// The public key used by the recipient to sign invoices. + pub fn signing_pubkey(&self) -> PublicKey { + self.contents.signing_pubkey.unwrap() + } +} + +/// The minimum amount required for an item in an [`Offer`], denominated in either bitcoin or +/// another currency. +#[derive(Clone, Debug)] +pub enum Amount { + /// An amount of bitcoin. + Bitcoin { + /// The amount in millisatoshi. + amount_msats: u64, + }, + /// An amount of currency specified using ISO 4712. + Currency { + /// The currency that the amount is denominated in. + iso4217_code: CurrencyCode, + /// The amount in the currency unit adjusted by the ISO 4712 exponent (e.g., USD cents). + amount: u64, + }, +} + +/// An ISO 4712 three-letter currency code (e.g., USD). +pub type CurrencyCode = [u8; 3]; diff --git a/lightning/src/onion_message/blinded_route.rs b/lightning/src/onion_message/blinded_route.rs index e47c77de3..a3824e42e 100644 --- a/lightning/src/onion_message/blinded_route.rs +++ b/lightning/src/onion_message/blinded_route.rs @@ -22,6 +22,7 @@ use prelude::*; /// Onion messages can be sent and received to blinded routes, which serve to hide the identity of /// the recipient. +#[derive(Clone, Debug)] pub struct BlindedRoute { /// To send to a blinded route, the sender first finds a route to the unblinded /// `introduction_node_id`, which can unblind its [`encrypted_payload`] to find out the onion @@ -35,14 +36,15 @@ pub struct BlindedRoute { /// [`encrypted_payload`]: BlindedHop::encrypted_payload pub(super) blinding_point: PublicKey, /// The hops composing the blinded route. - pub(super) blinded_hops: Vec, + pub(crate) blinded_hops: Vec, } /// Used to construct the blinded hops portion of a blinded route. These hops cannot be identified /// by outside observers and thus can be used to hide the identity of the recipient. +#[derive(Clone, Debug)] pub struct BlindedHop { /// The blinded node id of this hop in a blinded route. - pub(super) blinded_node_id: PublicKey, + pub(crate) blinded_node_id: PublicKey, /// The encrypted payload intended for this hop in a blinded route. // The node sending to this blinded route will later encode this payload into the onion packet for // this hop. diff --git a/lightning/src/onion_message/mod.rs b/lightning/src/onion_message/mod.rs index 2e23b76ad..d39125cd5 100644 --- a/lightning/src/onion_message/mod.rs +++ b/lightning/src/onion_message/mod.rs @@ -30,4 +30,5 @@ mod functional_tests; // Re-export structs so they can be imported with just the `onion_message::` module prefix. pub use self::blinded_route::{BlindedRoute, BlindedHop}; pub use self::messenger::{Destination, OnionMessenger, SendError, SimpleArcOnionMessenger, SimpleRefOnionMessenger}; +pub use self::blinded_route::BlindedRoute as BlindedPath; pub(crate) use self::packet::Packet; diff --git a/lightning/src/util/ser.rs b/lightning/src/util/ser.rs index 852aa8f15..8aafeda13 100644 --- a/lightning/src/util/ser.rs +++ b/lightning/src/util/ser.rs @@ -399,7 +399,8 @@ impl Readable for BigSize { /// In TLV we occasionally send fields which only consist of, or potentially end with, a /// variable-length integer which is simply truncated by skipping high zero bytes. This type /// encapsulates such integers implementing Readable/Writeable for them. -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(test, derive(PartialEq))] +#[derive(Clone, Debug)] pub(crate) struct HighZeroBytesDroppedBigSize(pub T); macro_rules! impl_writeable_primitive { -- 2.39.5