From: Jeffrey Czyz Date: Tue, 9 Aug 2022 22:24:10 +0000 (-0500) Subject: Offer message interface and data format X-Git-Tag: v0.0.113~52^2~3 X-Git-Url: http://git.bitcoin.ninja/?a=commitdiff_plain;h=24b63de10c6b951263e7d262e739c55e01e650b3;p=rust-lightning 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). --- diff --git a/lightning/src/lib.rs b/lightning/src/lib.rs index 25eba1d74..1f3ab47b1 100644 --- a/lightning/src/lib.rs +++ b/lightning/src/lib.rs @@ -78,6 +78,8 @@ extern crate core; pub mod util; pub mod chain; pub mod ln; +#[allow(unused)] +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..d64091155 --- /dev/null +++ b/lightning/src/offers/offer.rs @@ -0,0 +1,169 @@ +// 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::num::NonZeroU64; +use core::time::Duration; +use crate::ln::features::OfferFeatures; +use crate::onion_message::BlindedPath; +use crate::util::string::PrintableString; + +use crate::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 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 +/// 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 +/// 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_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 (e.g., bitcoin mainnet). + /// Payments must be denominated in units of the minimal lightning-payable unit (e.g., msats) + /// for the selected chain. + pub fn chains(&self) -> Vec { + self.contents.chains + .as_ref() + .cloned() + .unwrap_or_else(|| vec![ChainHash::using_genesis_block(Network::Bitcoin)]) + } + + // TODO: Link to corresponding method in `InvoiceRequest`. + /// Opaque bytes set by the originator. Useful for authentication and validating fields since it + /// is reflected in `invoice_request` messages along with all the other fields from the `offer`. + pub fn metadata(&self) -> Option<&Vec> { + self.contents.metadata.as_ref() + } + + /// The minimum amount required for a successful payment of a single item. + pub fn amount(&self) -> Option<&Amount> { + self.contents.amount.as_ref() + } + + /// A complete description of the purpose of the payment. Intended to be displayed to the user + /// but with the caveat that it has not been verified in any way. + pub fn description(&self) -> PrintableString { + PrintableString(&self.contents.description) + } + + /// Features pertaining to the offer. + 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`. Intended to be + /// displayed to the user but with the caveat that it has not been verified in any way. + pub fn issuer(&self) -> Option { + self.contents.issuer.as_ref().map(|issuer| PrintableString(issuer.as_str())) + } + + /// Paths to the recipient originating from publicly reachable nodes. Blinded paths provide + /// recipient privacy by obfuscating its node id. + pub fn paths(&self) -> &[BlindedPath] { + self.contents.paths.as_ref().map(|paths| paths.as_slice()).unwrap_or(&[]) + } + + /// The quantity of items supported. + pub fn supported_quantity(&self) -> Quantity { + match self.contents.quantity_max { + Some(0) => Quantity::Unbounded, + Some(n) => Quantity::Bounded(NonZeroU64::new(n).unwrap()), + None => Quantity::Bounded(NonZeroU64::new(1).unwrap()), + } + } + + /// 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]; + +/// Quantity of items supported by an [`Offer`]. +pub enum Quantity { + /// Up to a specific number of items (inclusive). + Bounded(NonZeroU64), + /// One or more items. + Unbounded, +} diff --git a/lightning/src/onion_message/blinded_route.rs b/lightning/src/onion_message/blinded_route.rs index 82a325a9e..29d78d6ab 100644 --- a/lightning/src/onion_message/blinded_route.rs +++ b/lightning/src/onion_message/blinded_route.rs @@ -28,6 +28,7 @@ use crate::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 @@ -41,14 +42,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 3c522e30c..89c1545a7 100644 --- a/lightning/src/onion_message/mod.rs +++ b/lightning/src/onion_message/mod.rs @@ -28,6 +28,6 @@ mod utils; 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::blinded_route::{BlindedRoute, BlindedRoute as BlindedPath, BlindedHop}; pub use self::messenger::{CustomOnionMessageContents, CustomOnionMessageHandler, Destination, OnionMessageContents, OnionMessenger, SendError, SimpleArcOnionMessenger, SimpleRefOnionMessenger}; pub(crate) use self::packet::Packet; diff --git a/lightning/src/util/ser.rs b/lightning/src/util/ser.rs index 5ff6dc86a..c5ca35240 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, Eq, Debug))] +#[cfg_attr(test, derive(PartialEq, Eq))] +#[derive(Clone, Debug)] pub(crate) struct HighZeroBytesDroppedBigSize(pub T); macro_rules! impl_writeable_primitive {