]> git.bitcoin.ninja Git - rust-lightning/commitdiff
Offer message interface and data format
authorJeffrey Czyz <jkczyz@gmail.com>
Tue, 9 Aug 2022 22:24:10 +0000 (17:24 -0500)
committerJeffrey Czyz <jkczyz@gmail.com>
Fri, 4 Nov 2022 20:07:01 +0000 (15:07 -0500)
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
lightning/src/offers/mod.rs [new file with mode: 0644]
lightning/src/offers/offer.rs [new file with mode: 0644]
lightning/src/onion_message/blinded_route.rs
lightning/src/onion_message/mod.rs
lightning/src/util/ser.rs

index 25eba1d74f8f87ddb60fd80d610b187f853f81ce..1f3ab47b1ae3f7d2ca8802e46c267d5428e3822c 100644 (file)
@@ -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 (file)
index 0000000..2f961a0
--- /dev/null
@@ -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 <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.
+
+//! 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 (file)
index 0000000..d640911
--- /dev/null
@@ -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 <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 `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<u8>,
+       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<Vec<ChainHash>>,
+       metadata: Option<Vec<u8>>,
+       amount: Option<Amount>,
+       description: String,
+       features: OfferFeatures,
+       absolute_expiry: Option<Duration>,
+       issuer: Option<String>,
+       paths: Option<Vec<BlindedPath>>,
+       quantity_max: Option<u64>,
+       signing_pubkey: Option<PublicKey>,
+}
+
+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<ChainHash> {
+               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<u8>> {
+               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<Duration> {
+               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<PrintableString> {
+               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,
+}
index 82a325a9e046bbd714d2b1fc6a53b89c1808d014..29d78d6ab58017e574544a67253388d6ae8276d8 100644 (file)
@@ -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<BlindedHop>,
+       pub(crate) blinded_hops: Vec<BlindedHop>,
 }
 
 /// 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.
index 3c522e30c51bf367c25311b6987418f3fe802931..89c1545a7df29c59adfe0956bc9faf1f769e9b80 100644 (file)
@@ -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;
index 5ff6dc86a0bf91d20df6fb110f67185f8cd5f355..c5ca3524079ad06bb2752a42e6ee4af7ece0f543 100644 (file)
@@ -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<T>(pub T);
 
 macro_rules! impl_writeable_primitive {