Skip to content

Commit e5079a6

Browse files
Add static invoice server messages and boilerplate
Because async recipients are not online to respond to invoice requests, the plan is for another node on the network that is always-online to serve static invoices on their behalf. The protocol is as follows: - Recipient is configured with blinded message paths to reach the static invoice server - On startup, recipient requests blinded message paths for inclusion in their offer from the static invoice server over the configured paths - Server replies with offer paths for the recipient - Recipient builds their offer using these paths and the corresponding static invoice and replies with the invoice - Server persists the invoice and confirms that they've persisted it, causing the recipient to cache the interactively built offer for use At pay-time, the payer sends an invoice request to the static invoice server, who replies with the static invoice after forwarding the invreq to the recipient (to give them a chance to provide a fresh invoice in case they're online). Here we add the requisite trait methods and onion messages to support this protocol. An alterate design could be for the async recipient to publish static invoices directly without a preceding offer, e.g. on their website. Some drawbacks of this design include: 1) No fallback to regular BOLT 12 in the case that the recipient happens to be online at pay-time. Falling back to regular BOLT 12 allows the recipient to provide a fresh invoice and regain the proof-of-payment property 2) Static invoices don't fit in a QR code 3) No automatic rotation of the static invoice, which is useful in the case that payment paths become outdated due to changing fees, etc
1 parent 80509b5 commit e5079a6

File tree

6 files changed

+303
-6
lines changed

6 files changed

+303
-6
lines changed

fuzz/src/onion_message.rs

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ use lightning::ln::peer_handler::IgnoringMessageHandler;
1515
use lightning::ln::script::ShutdownScript;
1616
use lightning::offers::invoice::UnsignedBolt12Invoice;
1717
use lightning::onion_message::async_payments::{
18-
AsyncPaymentsMessageHandler, HeldHtlcAvailable, ReleaseHeldHtlc,
18+
AsyncPaymentsMessageHandler, HeldHtlcAvailable, OfferPaths, OfferPathsRequest, ReleaseHeldHtlc,
19+
ServeStaticInvoice, StaticInvoicePersisted,
1920
};
2021
use lightning::onion_message::messenger::{
2122
CustomOnionMessageHandler, Destination, MessageRouter, MessageSendInstructions,
@@ -124,6 +125,30 @@ impl OffersMessageHandler for TestOffersMessageHandler {
124125
struct TestAsyncPaymentsMessageHandler {}
125126

126127
impl AsyncPaymentsMessageHandler for TestAsyncPaymentsMessageHandler {
128+
fn handle_offer_paths_request(
129+
&self, _message: OfferPathsRequest, _context: AsyncPaymentsContext,
130+
responder: Option<Responder>,
131+
) -> Option<(OfferPaths, ResponseInstruction)> {
132+
let responder = match responder {
133+
Some(resp) => resp,
134+
None => return None,
135+
};
136+
Some((OfferPaths { paths: Vec::new(), paths_absolute_expiry: None }, responder.respond()))
137+
}
138+
fn handle_offer_paths(
139+
&self, _message: OfferPaths, _context: AsyncPaymentsContext, _responder: Option<Responder>,
140+
) -> Option<(ServeStaticInvoice, ResponseInstruction)> {
141+
None
142+
}
143+
fn handle_serve_static_invoice(
144+
&self, _message: ServeStaticInvoice, _context: AsyncPaymentsContext,
145+
_responder: Option<Responder>,
146+
) {
147+
}
148+
fn handle_static_invoice_persisted(
149+
&self, _message: StaticInvoicePersisted, _context: AsyncPaymentsContext,
150+
) {
151+
}
127152
fn handle_held_htlc_available(
128153
&self, _message: HeldHtlcAvailable, _context: AsyncPaymentsContext,
129154
responder: Option<Responder>,

lightning/src/ln/channelmanager.rs

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,8 @@ use crate::offers::parse::Bolt12SemanticError;
9898
use crate::offers::refund::Refund;
9999
use crate::offers::signer;
100100
use crate::onion_message::async_payments::{
101-
AsyncPaymentsMessage, AsyncPaymentsMessageHandler, HeldHtlcAvailable, ReleaseHeldHtlc,
101+
AsyncPaymentsMessage, AsyncPaymentsMessageHandler, HeldHtlcAvailable, OfferPaths,
102+
OfferPathsRequest, ReleaseHeldHtlc, ServeStaticInvoice, StaticInvoicePersisted,
102103
};
103104
use crate::onion_message::dns_resolution::HumanReadableName;
104105
use crate::onion_message::messenger::{
@@ -12995,6 +12996,30 @@ where
1299512996
MR::Target: MessageRouter,
1299612997
L::Target: Logger,
1299712998
{
12999+
fn handle_offer_paths_request(
13000+
&self, _message: OfferPathsRequest, _context: AsyncPaymentsContext,
13001+
_responder: Option<Responder>,
13002+
) -> Option<(OfferPaths, ResponseInstruction)> {
13003+
None
13004+
}
13005+
13006+
fn handle_offer_paths(
13007+
&self, _message: OfferPaths, _context: AsyncPaymentsContext, _responder: Option<Responder>,
13008+
) -> Option<(ServeStaticInvoice, ResponseInstruction)> {
13009+
None
13010+
}
13011+
13012+
fn handle_serve_static_invoice(
13013+
&self, _message: ServeStaticInvoice, _context: AsyncPaymentsContext,
13014+
_responder: Option<Responder>,
13015+
) {
13016+
}
13017+
13018+
fn handle_static_invoice_persisted(
13019+
&self, _message: StaticInvoicePersisted, _context: AsyncPaymentsContext,
13020+
) {
13021+
}
13022+
1299813023
fn handle_held_htlc_available(
1299913024
&self, _message: HeldHtlcAvailable, _context: AsyncPaymentsContext,
1300013025
_responder: Option<Responder>,

lightning/src/ln/peer_handler.rs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ use crate::util::ser::{VecWriter, Writeable, Writer};
3030
use crate::ln::peer_channel_encryptor::{PeerChannelEncryptor, NextNoiseStep, MessageBuf, MSG_BUF_ALLOC_SIZE};
3131
use crate::ln::wire;
3232
use crate::ln::wire::{Encode, Type};
33-
use crate::onion_message::async_payments::{AsyncPaymentsMessageHandler, HeldHtlcAvailable, ReleaseHeldHtlc};
33+
use crate::onion_message::async_payments::{AsyncPaymentsMessageHandler, HeldHtlcAvailable, OfferPaths, OfferPathsRequest, ServeStaticInvoice, ReleaseHeldHtlc, StaticInvoicePersisted};
3434
use crate::onion_message::dns_resolution::{DNSResolverMessageHandler, DNSResolverMessage, DNSSECProof, DNSSECQuery};
3535
use crate::onion_message::messenger::{CustomOnionMessageHandler, Responder, ResponseInstruction, MessageSendInstructions};
3636
use crate::onion_message::offers::{OffersMessage, OffersMessageHandler};
@@ -148,6 +148,23 @@ impl OffersMessageHandler for IgnoringMessageHandler {
148148
}
149149
}
150150
impl AsyncPaymentsMessageHandler for IgnoringMessageHandler {
151+
fn handle_offer_paths_request(
152+
&self, _message: OfferPathsRequest, _context: AsyncPaymentsContext, _responder: Option<Responder>,
153+
) -> Option<(OfferPaths, ResponseInstruction)> {
154+
None
155+
}
156+
fn handle_offer_paths(
157+
&self, _message: OfferPaths, _context: AsyncPaymentsContext, _responder: Option<Responder>,
158+
) -> Option<(ServeStaticInvoice, ResponseInstruction)> {
159+
None
160+
}
161+
fn handle_serve_static_invoice(
162+
&self, _message: ServeStaticInvoice, _context: AsyncPaymentsContext,
163+
_responder: Option<Responder>,
164+
) {}
165+
fn handle_static_invoice_persisted(
166+
&self, _message: StaticInvoicePersisted, _context: AsyncPaymentsContext,
167+
) {}
151168
fn handle_held_htlc_available(
152169
&self, _message: HeldHtlcAvailable, _context: AsyncPaymentsContext,
153170
_responder: Option<Responder>,

lightning/src/onion_message/async_payments.rs

Lines changed: 187 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,22 +9,66 @@
99

1010
//! Message handling for async payments.
1111
12-
use crate::blinded_path::message::AsyncPaymentsContext;
12+
use crate::blinded_path::message::{AsyncPaymentsContext, BlindedMessagePath};
1313
use crate::io;
1414
use crate::ln::msgs::DecodeError;
15+
use crate::offers::static_invoice::StaticInvoice;
1516
use crate::onion_message::messenger::{MessageSendInstructions, Responder, ResponseInstruction};
1617
use crate::onion_message::packet::OnionMessageContents;
1718
use crate::prelude::*;
1819
use crate::util::ser::{Readable, ReadableArgs, Writeable, Writer};
1920

21+
use core::time::Duration;
22+
2023
// TLV record types for the `onionmsg_tlv` TLV stream as defined in BOLT 4.
24+
const OFFER_PATHS_REQ_TLV_TYPE: u64 = 65538;
25+
const OFFER_PATHS_TLV_TYPE: u64 = 65540;
26+
const SERVE_INVOICE_TLV_TYPE: u64 = 65542;
27+
const INVOICE_PERSISTED_TLV_TYPE: u64 = 65544;
2128
const HELD_HTLC_AVAILABLE_TLV_TYPE: u64 = 72;
2229
const RELEASE_HELD_HTLC_TLV_TYPE: u64 = 74;
2330

2431
/// A handler for an [`OnionMessage`] containing an async payments message as its payload.
2532
///
2633
/// [`OnionMessage`]: crate::ln::msgs::OnionMessage
2734
pub trait AsyncPaymentsMessageHandler {
35+
/// Handle an [`OfferPathsRequest`] message. If we are a static invoice server and the message was
36+
/// sent over paths that we previously provided to an async recipient via
37+
/// [`UserConfig::paths_to_static_invoice_server`], an [`OfferPaths`] message should be returned.
38+
///
39+
/// [`UserConfig::paths_to_static_invoice_server`]: crate::util::config::UserConfig::paths_to_static_invoice_server
40+
fn handle_offer_paths_request(
41+
&self, message: OfferPathsRequest, context: AsyncPaymentsContext,
42+
responder: Option<Responder>,
43+
) -> Option<(OfferPaths, ResponseInstruction)>;
44+
45+
/// Handle an [`OfferPaths`] message. If this is in response to an [`OfferPathsRequest`] that
46+
/// we previously sent as an async recipient, we should build an [`Offer`] containing the
47+
/// included [`OfferPaths::paths`] and a corresponding [`StaticInvoice`], and reply with
48+
/// [`ServeStaticInvoice`].
49+
///
50+
/// [`Offer`]: crate::offers::offer::Offer
51+
/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
52+
fn handle_offer_paths(
53+
&self, message: OfferPaths, context: AsyncPaymentsContext, responder: Option<Responder>,
54+
) -> Option<(ServeStaticInvoice, ResponseInstruction)>;
55+
56+
/// Handle a [`ServeStaticInvoice`] message. If this is in response to an [`OfferPaths`] message
57+
/// we previously sent as a static invoice server, a [`StaticInvoicePersisted`] message should be
58+
/// sent once the message is handled.
59+
fn handle_serve_static_invoice(
60+
&self, message: ServeStaticInvoice, context: AsyncPaymentsContext,
61+
responder: Option<Responder>,
62+
);
63+
64+
/// Handle a [`StaticInvoicePersisted`] message. If this is in response to a
65+
/// [`ServeStaticInvoice`] message we previously sent as an async recipient, then the offer we
66+
/// generated on receipt of a previous [`OfferPaths`] message is now ready to be used for async
67+
/// payments.
68+
fn handle_static_invoice_persisted(
69+
&self, message: StaticInvoicePersisted, context: AsyncPaymentsContext,
70+
);
71+
2872
/// Handle a [`HeldHtlcAvailable`] message. A [`ReleaseHeldHtlc`] should be returned to release
2973
/// the held funds.
3074
fn handle_held_htlc_available(
@@ -50,6 +94,29 @@ pub trait AsyncPaymentsMessageHandler {
5094
/// [`OnionMessage`]: crate::ln::msgs::OnionMessage
5195
#[derive(Clone, Debug)]
5296
pub enum AsyncPaymentsMessage {
97+
/// A request from an async recipient for [`BlindedMessagePath`]s, sent to a static invoice
98+
/// server.
99+
OfferPathsRequest(OfferPathsRequest),
100+
101+
/// [`BlindedMessagePath`]s to be included in an async recipient's [`Offer::paths`], sent by a
102+
/// static invoice server in response to an [`OfferPathsRequest`].
103+
///
104+
/// [`Offer::paths`]: crate::offers::offer::Offer::paths
105+
OfferPaths(OfferPaths),
106+
107+
/// A request from an async recipient to a static invoice server that a [`StaticInvoice`] be
108+
/// provided in response to [`InvoiceRequest`]s from payers.
109+
///
110+
/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
111+
ServeStaticInvoice(ServeStaticInvoice),
112+
113+
/// Confirmation from a static invoice server that a [`StaticInvoice`] was persisted and the
114+
/// corresponding [`Offer`] is ready to be used to receive async payments. Sent to an async
115+
/// recipient in response to a [`ServeStaticInvoice`] message.
116+
///
117+
/// [`Offer`]: crate::offers::offer::Offer
118+
StaticInvoicePersisted(StaticInvoicePersisted),
119+
53120
/// An HTLC is being held upstream for the often-offline recipient, to be released via
54121
/// [`ReleaseHeldHtlc`].
55122
HeldHtlcAvailable(HeldHtlcAvailable),
@@ -58,6 +125,57 @@ pub enum AsyncPaymentsMessage {
58125
ReleaseHeldHtlc(ReleaseHeldHtlc),
59126
}
60127

128+
/// A request from an async recipient for [`BlindedMessagePath`]s from a static invoice server.
129+
/// These paths will be used in the async recipient's [`Offer::paths`], so payers can request
130+
/// [`StaticInvoice`]s from the static invoice server.
131+
///
132+
/// [`Offer::paths`]: crate::offers::offer::Offer::paths
133+
#[derive(Clone, Debug)]
134+
pub struct OfferPathsRequest {}
135+
136+
/// [`BlindedMessagePath`]s to be included in an async recipient's [`Offer::paths`], sent by a
137+
/// static invoice server in response to an [`OfferPathsRequest`].
138+
///
139+
/// [`Offer::paths`]: crate::offers::offer::Offer::paths
140+
#[derive(Clone, Debug)]
141+
pub struct OfferPaths {
142+
/// The paths that should be included in the async recipient's [`Offer::paths`].
143+
///
144+
/// [`Offer::paths`]: crate::offers::offer::Offer::paths
145+
pub paths: Vec<BlindedMessagePath>,
146+
/// The time as duration since the Unix epoch at which the [`Self::paths`] expire.
147+
pub paths_absolute_expiry: Option<Duration>,
148+
}
149+
150+
/// A request from an async recipient to a static invoice server that a [`StaticInvoice`] be
151+
/// provided in response to [`InvoiceRequest`]s from payers.
152+
///
153+
/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
154+
#[derive(Clone, Debug)]
155+
pub struct ServeStaticInvoice {
156+
/// The invoice that should be served by the static invoice server. Once this invoice has been
157+
/// persisted, the [`Responder`] accompanying this message should be used to send
158+
/// [`StaticInvoicePersisted`] to the recipient to confirm that the offer corresponding to the
159+
/// invoice is ready to receive async payments.
160+
pub invoice: StaticInvoice,
161+
/// If a static invoice server receives an [`InvoiceRequest`] for a [`StaticInvoice`], they should
162+
/// also forward the [`InvoiceRequest`] to the async recipient so they can respond with a fresh
163+
/// [`Bolt12Invoice`] if the recipient is online at the time. Use this path to forward the
164+
/// [`InvoiceRequest`] to the async recipient.
165+
///
166+
/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
167+
/// [`Bolt12Invoice`]: crate::offers::invoice::Bolt12Invoice
168+
pub forward_invoice_request_path: BlindedMessagePath,
169+
}
170+
171+
/// Confirmation from a static invoice server that a [`StaticInvoice`] was persisted and the
172+
/// corresponding [`Offer`] is ready to be used to receive async payments. Sent to an async
173+
/// recipient in response to a [`ServeStaticInvoice`] message.
174+
///
175+
/// [`Offer`]: crate::offers::offer::Offer
176+
#[derive(Clone, Debug)]
177+
pub struct StaticInvoicePersisted {}
178+
61179
/// An HTLC destined for the recipient of this message is being held upstream. The reply path
62180
/// accompanying this onion message should be used to send a [`ReleaseHeldHtlc`] response, which
63181
/// will cause the upstream HTLC to be released.
@@ -68,6 +186,34 @@ pub struct HeldHtlcAvailable {}
68186
#[derive(Clone, Debug)]
69187
pub struct ReleaseHeldHtlc {}
70188

189+
impl OnionMessageContents for OfferPaths {
190+
fn tlv_type(&self) -> u64 {
191+
OFFER_PATHS_TLV_TYPE
192+
}
193+
#[cfg(c_bindings)]
194+
fn msg_type(&self) -> String {
195+
"Offer Paths".to_string()
196+
}
197+
#[cfg(not(c_bindings))]
198+
fn msg_type(&self) -> &'static str {
199+
"Offer Paths"
200+
}
201+
}
202+
203+
impl OnionMessageContents for ServeStaticInvoice {
204+
fn tlv_type(&self) -> u64 {
205+
SERVE_INVOICE_TLV_TYPE
206+
}
207+
#[cfg(c_bindings)]
208+
fn msg_type(&self) -> String {
209+
"Serve Static Invoice".to_string()
210+
}
211+
#[cfg(not(c_bindings))]
212+
fn msg_type(&self) -> &'static str {
213+
"Serve Static Invoice"
214+
}
215+
}
216+
71217
impl OnionMessageContents for ReleaseHeldHtlc {
72218
fn tlv_type(&self) -> u64 {
73219
RELEASE_HELD_HTLC_TLV_TYPE
@@ -82,6 +228,20 @@ impl OnionMessageContents for ReleaseHeldHtlc {
82228
}
83229
}
84230

231+
impl_writeable_tlv_based!(OfferPathsRequest, {});
232+
233+
impl_writeable_tlv_based!(OfferPaths, {
234+
(0, paths, required_vec),
235+
(2, paths_absolute_expiry, option),
236+
});
237+
238+
impl_writeable_tlv_based!(ServeStaticInvoice, {
239+
(0, invoice, required),
240+
(2, forward_invoice_request_path, required),
241+
});
242+
243+
impl_writeable_tlv_based!(StaticInvoicePersisted, {});
244+
85245
impl_writeable_tlv_based!(HeldHtlcAvailable, {});
86246

87247
impl_writeable_tlv_based!(ReleaseHeldHtlc, {});
@@ -90,7 +250,12 @@ impl AsyncPaymentsMessage {
90250
/// Returns whether `tlv_type` corresponds to a TLV record for async payment messages.
91251
pub fn is_known_type(tlv_type: u64) -> bool {
92252
match tlv_type {
93-
HELD_HTLC_AVAILABLE_TLV_TYPE | RELEASE_HELD_HTLC_TLV_TYPE => true,
253+
OFFER_PATHS_REQ_TLV_TYPE
254+
| OFFER_PATHS_TLV_TYPE
255+
| SERVE_INVOICE_TLV_TYPE
256+
| INVOICE_PERSISTED_TLV_TYPE
257+
| HELD_HTLC_AVAILABLE_TLV_TYPE
258+
| RELEASE_HELD_HTLC_TLV_TYPE => true,
94259
_ => false,
95260
}
96261
}
@@ -99,20 +264,32 @@ impl AsyncPaymentsMessage {
99264
impl OnionMessageContents for AsyncPaymentsMessage {
100265
fn tlv_type(&self) -> u64 {
101266
match self {
267+
Self::OfferPathsRequest(_) => OFFER_PATHS_REQ_TLV_TYPE,
268+
Self::OfferPaths(msg) => msg.tlv_type(),
269+
Self::ServeStaticInvoice(msg) => msg.tlv_type(),
270+
Self::StaticInvoicePersisted(_) => INVOICE_PERSISTED_TLV_TYPE,
102271
Self::HeldHtlcAvailable(_) => HELD_HTLC_AVAILABLE_TLV_TYPE,
103272
Self::ReleaseHeldHtlc(msg) => msg.tlv_type(),
104273
}
105274
}
106275
#[cfg(c_bindings)]
107276
fn msg_type(&self) -> String {
108277
match &self {
278+
Self::OfferPathsRequest(_) => "Offer Paths Request".to_string(),
279+
Self::OfferPaths(msg) => msg.msg_type(),
280+
Self::ServeStaticInvoice(msg) => msg.msg_type(),
281+
Self::StaticInvoicePersisted(_) => "Static Invoice Persisted".to_string(),
109282
Self::HeldHtlcAvailable(_) => "Held HTLC Available".to_string(),
110283
Self::ReleaseHeldHtlc(msg) => msg.msg_type(),
111284
}
112285
}
113286
#[cfg(not(c_bindings))]
114287
fn msg_type(&self) -> &'static str {
115288
match &self {
289+
Self::OfferPathsRequest(_) => "Offer Paths Request",
290+
Self::OfferPaths(msg) => msg.msg_type(),
291+
Self::ServeStaticInvoice(msg) => msg.msg_type(),
292+
Self::StaticInvoicePersisted(_) => "Static Invoice Persisted",
116293
Self::HeldHtlcAvailable(_) => "Held HTLC Available",
117294
Self::ReleaseHeldHtlc(msg) => msg.msg_type(),
118295
}
@@ -122,6 +299,10 @@ impl OnionMessageContents for AsyncPaymentsMessage {
122299
impl Writeable for AsyncPaymentsMessage {
123300
fn write<W: Writer>(&self, w: &mut W) -> Result<(), io::Error> {
124301
match self {
302+
Self::OfferPathsRequest(message) => message.write(w),
303+
Self::OfferPaths(message) => message.write(w),
304+
Self::ServeStaticInvoice(message) => message.write(w),
305+
Self::StaticInvoicePersisted(message) => message.write(w),
125306
Self::HeldHtlcAvailable(message) => message.write(w),
126307
Self::ReleaseHeldHtlc(message) => message.write(w),
127308
}
@@ -131,6 +312,10 @@ impl Writeable for AsyncPaymentsMessage {
131312
impl ReadableArgs<u64> for AsyncPaymentsMessage {
132313
fn read<R: io::Read>(r: &mut R, tlv_type: u64) -> Result<Self, DecodeError> {
133314
match tlv_type {
315+
OFFER_PATHS_REQ_TLV_TYPE => Ok(Self::OfferPathsRequest(Readable::read(r)?)),
316+
OFFER_PATHS_TLV_TYPE => Ok(Self::OfferPaths(Readable::read(r)?)),
317+
SERVE_INVOICE_TLV_TYPE => Ok(Self::ServeStaticInvoice(Readable::read(r)?)),
318+
INVOICE_PERSISTED_TLV_TYPE => Ok(Self::StaticInvoicePersisted(Readable::read(r)?)),
134319
HELD_HTLC_AVAILABLE_TLV_TYPE => Ok(Self::HeldHtlcAvailable(Readable::read(r)?)),
135320
RELEASE_HELD_HTLC_TLV_TYPE => Ok(Self::ReleaseHeldHtlc(Readable::read(r)?)),
136321
_ => Err(DecodeError::InvalidValue),

0 commit comments

Comments
 (0)