Skip to content

Commit 24b63de

Browse files
committed
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).
1 parent 48bb9ed commit 24b63de

File tree

6 files changed

+193
-4
lines changed

6 files changed

+193
-4
lines changed

lightning/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,8 @@ extern crate core;
7878
pub mod util;
7979
pub mod chain;
8080
pub mod ln;
81+
#[allow(unused)]
82+
mod offers;
8183
pub mod routing;
8284
pub mod onion_message;
8385

lightning/src/offers/mod.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// This file is Copyright its original authors, visible in version control
2+
// history.
3+
//
4+
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
5+
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
6+
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
7+
// You may not use this file except in accordance with one or both of these
8+
// licenses.
9+
10+
//! Implementation of Lightning Offers
11+
//! ([BOLT 12](https://github.com/lightning/bolts/blob/master/12-offer-encoding.md)).
12+
//!
13+
//! Offers are a flexible protocol for Lightning payments.
14+
15+
pub mod offer;

lightning/src/offers/offer.rs

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
// This file is Copyright its original authors, visible in version control
2+
// history.
3+
//
4+
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
5+
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
6+
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
7+
// You may not use this file except in accordance with one or both of these
8+
// licenses.
9+
10+
//! Data structures and encoding for `offer` messages.
11+
12+
use bitcoin::blockdata::constants::ChainHash;
13+
use bitcoin::network::constants::Network;
14+
use bitcoin::secp256k1::PublicKey;
15+
use core::num::NonZeroU64;
16+
use core::time::Duration;
17+
use crate::ln::features::OfferFeatures;
18+
use crate::onion_message::BlindedPath;
19+
use crate::util::string::PrintableString;
20+
21+
use crate::prelude::*;
22+
23+
#[cfg(feature = "std")]
24+
use std::time::SystemTime;
25+
26+
/// An `Offer` is a potentially long-lived proposal for payment of a good or service.
27+
///
28+
/// An offer is a precursor to an `InvoiceRequest`. A merchant publishes an offer from which a
29+
/// customer may request an `Invoice` for a specific quantity and using an amount sufficient to
30+
/// cover that quantity (i.e., at least `quantity * amount`). See [`Offer::amount`].
31+
///
32+
/// Offers may be denominated in currency other than bitcoin but are ultimately paid using the
33+
/// latter.
34+
///
35+
/// Through the use of [`BlindedPath`]s, offers provide recipient privacy.
36+
#[derive(Clone, Debug)]
37+
pub struct Offer {
38+
// The serialized offer. Needed when creating an `InvoiceRequest` if the offer contains unknown
39+
// fields.
40+
bytes: Vec<u8>,
41+
contents: OfferContents,
42+
}
43+
44+
/// The contents of an [`Offer`], which may be shared with an `InvoiceRequest` or an `Invoice`.
45+
#[derive(Clone, Debug)]
46+
pub(crate) struct OfferContents {
47+
chains: Option<Vec<ChainHash>>,
48+
metadata: Option<Vec<u8>>,
49+
amount: Option<Amount>,
50+
description: String,
51+
features: OfferFeatures,
52+
absolute_expiry: Option<Duration>,
53+
issuer: Option<String>,
54+
paths: Option<Vec<BlindedPath>>,
55+
quantity_max: Option<u64>,
56+
signing_pubkey: Option<PublicKey>,
57+
}
58+
59+
impl Offer {
60+
// TODO: Return a slice once ChainHash has constants.
61+
// - https://github.com/rust-bitcoin/rust-bitcoin/pull/1283
62+
// - https://github.com/rust-bitcoin/rust-bitcoin/pull/1286
63+
/// The chains that may be used when paying a requested invoice (e.g., bitcoin mainnet).
64+
/// Payments must be denominated in units of the minimal lightning-payable unit (e.g., msats)
65+
/// for the selected chain.
66+
pub fn chains(&self) -> Vec<ChainHash> {
67+
self.contents.chains
68+
.as_ref()
69+
.cloned()
70+
.unwrap_or_else(|| vec![ChainHash::using_genesis_block(Network::Bitcoin)])
71+
}
72+
73+
// TODO: Link to corresponding method in `InvoiceRequest`.
74+
/// Opaque bytes set by the originator. Useful for authentication and validating fields since it
75+
/// is reflected in `invoice_request` messages along with all the other fields from the `offer`.
76+
pub fn metadata(&self) -> Option<&Vec<u8>> {
77+
self.contents.metadata.as_ref()
78+
}
79+
80+
/// The minimum amount required for a successful payment of a single item.
81+
pub fn amount(&self) -> Option<&Amount> {
82+
self.contents.amount.as_ref()
83+
}
84+
85+
/// A complete description of the purpose of the payment. Intended to be displayed to the user
86+
/// but with the caveat that it has not been verified in any way.
87+
pub fn description(&self) -> PrintableString {
88+
PrintableString(&self.contents.description)
89+
}
90+
91+
/// Features pertaining to the offer.
92+
pub fn features(&self) -> &OfferFeatures {
93+
&self.contents.features
94+
}
95+
96+
/// Duration since the Unix epoch when an invoice should no longer be requested.
97+
///
98+
/// If `None`, the offer does not expire.
99+
pub fn absolute_expiry(&self) -> Option<Duration> {
100+
self.contents.absolute_expiry
101+
}
102+
103+
/// Whether the offer has expired.
104+
#[cfg(feature = "std")]
105+
pub fn is_expired(&self) -> bool {
106+
match self.absolute_expiry() {
107+
Some(seconds_from_epoch) => match SystemTime::UNIX_EPOCH.elapsed() {
108+
Ok(elapsed) => elapsed > seconds_from_epoch,
109+
Err(_) => false,
110+
},
111+
None => false,
112+
}
113+
}
114+
115+
/// The issuer of the offer, possibly beginning with `user@domain` or `domain`. Intended to be
116+
/// displayed to the user but with the caveat that it has not been verified in any way.
117+
pub fn issuer(&self) -> Option<PrintableString> {
118+
self.contents.issuer.as_ref().map(|issuer| PrintableString(issuer.as_str()))
119+
}
120+
121+
/// Paths to the recipient originating from publicly reachable nodes. Blinded paths provide
122+
/// recipient privacy by obfuscating its node id.
123+
pub fn paths(&self) -> &[BlindedPath] {
124+
self.contents.paths.as_ref().map(|paths| paths.as_slice()).unwrap_or(&[])
125+
}
126+
127+
/// The quantity of items supported.
128+
pub fn supported_quantity(&self) -> Quantity {
129+
match self.contents.quantity_max {
130+
Some(0) => Quantity::Unbounded,
131+
Some(n) => Quantity::Bounded(NonZeroU64::new(n).unwrap()),
132+
None => Quantity::Bounded(NonZeroU64::new(1).unwrap()),
133+
}
134+
}
135+
136+
/// The public key used by the recipient to sign invoices.
137+
pub fn signing_pubkey(&self) -> PublicKey {
138+
self.contents.signing_pubkey.unwrap()
139+
}
140+
}
141+
142+
/// The minimum amount required for an item in an [`Offer`], denominated in either bitcoin or
143+
/// another currency.
144+
#[derive(Clone, Debug)]
145+
pub enum Amount {
146+
/// An amount of bitcoin.
147+
Bitcoin {
148+
/// The amount in millisatoshi.
149+
amount_msats: u64,
150+
},
151+
/// An amount of currency specified using ISO 4712.
152+
Currency {
153+
/// The currency that the amount is denominated in.
154+
iso4217_code: CurrencyCode,
155+
/// The amount in the currency unit adjusted by the ISO 4712 exponent (e.g., USD cents).
156+
amount: u64,
157+
},
158+
}
159+
160+
/// An ISO 4712 three-letter currency code (e.g., USD).
161+
pub type CurrencyCode = [u8; 3];
162+
163+
/// Quantity of items supported by an [`Offer`].
164+
pub enum Quantity {
165+
/// Up to a specific number of items (inclusive).
166+
Bounded(NonZeroU64),
167+
/// One or more items.
168+
Unbounded,
169+
}

lightning/src/onion_message/blinded_route.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ use crate::prelude::*;
2828

2929
/// Onion messages can be sent and received to blinded routes, which serve to hide the identity of
3030
/// the recipient.
31+
#[derive(Clone, Debug)]
3132
pub struct BlindedRoute {
3233
/// To send to a blinded route, the sender first finds a route to the unblinded
3334
/// `introduction_node_id`, which can unblind its [`encrypted_payload`] to find out the onion
@@ -41,14 +42,15 @@ pub struct BlindedRoute {
4142
/// [`encrypted_payload`]: BlindedHop::encrypted_payload
4243
pub(super) blinding_point: PublicKey,
4344
/// The hops composing the blinded route.
44-
pub(super) blinded_hops: Vec<BlindedHop>,
45+
pub(crate) blinded_hops: Vec<BlindedHop>,
4546
}
4647

4748
/// Used to construct the blinded hops portion of a blinded route. These hops cannot be identified
4849
/// by outside observers and thus can be used to hide the identity of the recipient.
50+
#[derive(Clone, Debug)]
4951
pub struct BlindedHop {
5052
/// The blinded node id of this hop in a blinded route.
51-
pub(super) blinded_node_id: PublicKey,
53+
pub(crate) blinded_node_id: PublicKey,
5254
/// The encrypted payload intended for this hop in a blinded route.
5355
// The node sending to this blinded route will later encode this payload into the onion packet for
5456
// this hop.

lightning/src/onion_message/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,6 @@ mod utils;
2828
mod functional_tests;
2929

3030
// Re-export structs so they can be imported with just the `onion_message::` module prefix.
31-
pub use self::blinded_route::{BlindedRoute, BlindedHop};
31+
pub use self::blinded_route::{BlindedRoute, BlindedRoute as BlindedPath, BlindedHop};
3232
pub use self::messenger::{CustomOnionMessageContents, CustomOnionMessageHandler, Destination, OnionMessageContents, OnionMessenger, SendError, SimpleArcOnionMessenger, SimpleRefOnionMessenger};
3333
pub(crate) use self::packet::Packet;

lightning/src/util/ser.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -399,7 +399,8 @@ impl Readable for BigSize {
399399
/// In TLV we occasionally send fields which only consist of, or potentially end with, a
400400
/// variable-length integer which is simply truncated by skipping high zero bytes. This type
401401
/// encapsulates such integers implementing Readable/Writeable for them.
402-
#[cfg_attr(test, derive(PartialEq, Eq, Debug))]
402+
#[cfg_attr(test, derive(PartialEq, Eq))]
403+
#[derive(Clone, Debug)]
403404
pub(crate) struct HighZeroBytesDroppedBigSize<T>(pub T);
404405

405406
macro_rules! impl_writeable_primitive {

0 commit comments

Comments
 (0)