Skip to content

Commit 79d65b3

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 1532913 commit 79d65b3

File tree

6 files changed

+307
-6
lines changed

6 files changed

+307
-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: 191 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,22 +9,64 @@
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

2021
// TLV record types for the `onionmsg_tlv` TLV stream as defined in BOLT 4.
22+
const OFFER_PATHS_REQ_TLV_TYPE: u64 = 65538;
23+
const OFFER_PATHS_TLV_TYPE: u64 = 65540;
24+
const SERVE_INVOICE_TLV_TYPE: u64 = 65542;
25+
const INVOICE_PERSISTED_TLV_TYPE: u64 = 65544;
2126
const HELD_HTLC_AVAILABLE_TLV_TYPE: u64 = 72;
2227
const RELEASE_HELD_HTLC_TLV_TYPE: u64 = 74;
2328

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

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

192+
impl OnionMessageContents for OfferPaths {
193+
fn tlv_type(&self) -> u64 {
194+
OFFER_PATHS_TLV_TYPE
195+
}
196+
#[cfg(c_bindings)]
197+
fn msg_type(&self) -> String {
198+
"Offer Paths".to_string()
199+
}
200+
#[cfg(not(c_bindings))]
201+
fn msg_type(&self) -> &'static str {
202+
"Offer Paths"
203+
}
204+
}
205+
206+
impl OnionMessageContents for ServeStaticInvoice {
207+
fn tlv_type(&self) -> u64 {
208+
SERVE_INVOICE_TLV_TYPE
209+
}
210+
#[cfg(c_bindings)]
211+
fn msg_type(&self) -> String {
212+
"Serve Static Invoice".to_string()
213+
}
214+
#[cfg(not(c_bindings))]
215+
fn msg_type(&self) -> &'static str {
216+
"Serve Static Invoice"
217+
}
218+
}
219+
71220
impl OnionMessageContents for ReleaseHeldHtlc {
72221
fn tlv_type(&self) -> u64 {
73222
RELEASE_HELD_HTLC_TLV_TYPE
@@ -82,6 +231,21 @@ impl OnionMessageContents for ReleaseHeldHtlc {
82231
}
83232
}
84233

234+
impl_writeable_tlv_based!(OfferPathsRequest, {});
235+
236+
impl_writeable_tlv_based!(OfferPaths, {
237+
(0, paths, required_vec),
238+
(2, paths_absolute_expiry, option),
239+
});
240+
241+
impl_writeable_tlv_based!(ServeStaticInvoice, {
242+
(0, invoice, required),
243+
(2, forward_invoice_request_path, required),
244+
(4, invoice_slot, required),
245+
});
246+
247+
impl_writeable_tlv_based!(StaticInvoicePersisted, {});
248+
85249
impl_writeable_tlv_based!(HeldHtlcAvailable, {});
86250

87251
impl_writeable_tlv_based!(ReleaseHeldHtlc, {});
@@ -90,7 +254,12 @@ impl AsyncPaymentsMessage {
90254
/// Returns whether `tlv_type` corresponds to a TLV record for async payment messages.
91255
pub fn is_known_type(tlv_type: u64) -> bool {
92256
match tlv_type {
93-
HELD_HTLC_AVAILABLE_TLV_TYPE | RELEASE_HELD_HTLC_TLV_TYPE => true,
257+
OFFER_PATHS_REQ_TLV_TYPE
258+
| OFFER_PATHS_TLV_TYPE
259+
| SERVE_INVOICE_TLV_TYPE
260+
| INVOICE_PERSISTED_TLV_TYPE
261+
| HELD_HTLC_AVAILABLE_TLV_TYPE
262+
| RELEASE_HELD_HTLC_TLV_TYPE => true,
94263
_ => false,
95264
}
96265
}
@@ -99,20 +268,32 @@ impl AsyncPaymentsMessage {
99268
impl OnionMessageContents for AsyncPaymentsMessage {
100269
fn tlv_type(&self) -> u64 {
101270
match self {
271+
Self::OfferPathsRequest(_) => OFFER_PATHS_REQ_TLV_TYPE,
272+
Self::OfferPaths(msg) => msg.tlv_type(),
273+
Self::ServeStaticInvoice(msg) => msg.tlv_type(),
274+
Self::StaticInvoicePersisted(_) => INVOICE_PERSISTED_TLV_TYPE,
102275
Self::HeldHtlcAvailable(_) => HELD_HTLC_AVAILABLE_TLV_TYPE,
103276
Self::ReleaseHeldHtlc(msg) => msg.tlv_type(),
104277
}
105278
}
106279
#[cfg(c_bindings)]
107280
fn msg_type(&self) -> String {
108281
match &self {
282+
Self::OfferPathsRequest(_) => "Offer Paths Request".to_string(),
283+
Self::OfferPaths(msg) => msg.msg_type(),
284+
Self::ServeStaticInvoice(msg) => msg.msg_type(),
285+
Self::StaticInvoicePersisted(_) => "Static Invoice Persisted".to_string(),
109286
Self::HeldHtlcAvailable(_) => "Held HTLC Available".to_string(),
110287
Self::ReleaseHeldHtlc(msg) => msg.msg_type(),
111288
}
112289
}
113290
#[cfg(not(c_bindings))]
114291
fn msg_type(&self) -> &'static str {
115292
match &self {
293+
Self::OfferPathsRequest(_) => "Offer Paths Request",
294+
Self::OfferPaths(msg) => msg.msg_type(),
295+
Self::ServeStaticInvoice(msg) => msg.msg_type(),
296+
Self::StaticInvoicePersisted(_) => "Static Invoice Persisted",
116297
Self::HeldHtlcAvailable(_) => "Held HTLC Available",
117298
Self::ReleaseHeldHtlc(msg) => msg.msg_type(),
118299
}
@@ -122,6 +303,10 @@ impl OnionMessageContents for AsyncPaymentsMessage {
122303
impl Writeable for AsyncPaymentsMessage {
123304
fn write<W: Writer>(&self, w: &mut W) -> Result<(), io::Error> {
124305
match self {
306+
Self::OfferPathsRequest(message) => message.write(w),
307+
Self::OfferPaths(message) => message.write(w),
308+
Self::ServeStaticInvoice(message) => message.write(w),
309+
Self::StaticInvoicePersisted(message) => message.write(w),
125310
Self::HeldHtlcAvailable(message) => message.write(w),
126311
Self::ReleaseHeldHtlc(message) => message.write(w),
127312
}
@@ -131,6 +316,10 @@ impl Writeable for AsyncPaymentsMessage {
131316
impl ReadableArgs<u64> for AsyncPaymentsMessage {
132317
fn read<R: io::Read>(r: &mut R, tlv_type: u64) -> Result<Self, DecodeError> {
133318
match tlv_type {
319+
OFFER_PATHS_REQ_TLV_TYPE => Ok(Self::OfferPathsRequest(Readable::read(r)?)),
320+
OFFER_PATHS_TLV_TYPE => Ok(Self::OfferPaths(Readable::read(r)?)),
321+
SERVE_INVOICE_TLV_TYPE => Ok(Self::ServeStaticInvoice(Readable::read(r)?)),
322+
INVOICE_PERSISTED_TLV_TYPE => Ok(Self::StaticInvoicePersisted(Readable::read(r)?)),
134323
HELD_HTLC_AVAILABLE_TLV_TYPE => Ok(Self::HeldHtlcAvailable(Readable::read(r)?)),
135324
RELEASE_HELD_HTLC_TLV_TYPE => Ok(Self::ReleaseHeldHtlc(Readable::read(r)?)),
136325
_ => Err(DecodeError::InvalidValue),

0 commit comments

Comments
 (0)