Skip to content

Commit 1ae3151

Browse files
Static invoice server: persist invoices once built
As part of serving static invoices to payers on behalf of often-offline recipients, the recipient will send us the final static invoice once it's done being interactively built. We will then persist this invoice and confirm to them that the corresponding offer is ready to be used for async payments. Surface an event once the invoice is received and expose an API to tell the recipient that it's ready for payments.
1 parent cc9c671 commit 1ae3151

File tree

3 files changed

+137
-0
lines changed

3 files changed

+137
-0
lines changed

lightning/src/events/mod.rs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1582,6 +1582,47 @@ pub enum Event {
15821582
/// onion messages.
15831583
peer_node_id: PublicKey,
15841584
},
1585+
/// As a static invoice server, we received a [`StaticInvoice`] from an async recipient that wants
1586+
/// us to serve the invoice to payers on their behalf when they are offline. This event will only
1587+
/// be generated if we previously created paths using
1588+
/// [`ChannelManager::blinded_paths_for_async_recipient`] and the recipient was configured with
1589+
/// them via [`ChannelManager::set_paths_to_static_invoice_server`].
1590+
///
1591+
/// [`ChannelManager::blinded_paths_for_async_recipient`]: crate::ln::channelmanager::ChannelManager::blinded_paths_for_async_recipient
1592+
/// [`ChannelManager::set_paths_to_static_invoice_server`]: crate::ln::channelmanager::ChannelManager::set_paths_to_static_invoice_server
1593+
#[cfg(async_payments)]
1594+
PersistStaticInvoice {
1595+
/// The invoice that should be persisted and later provided to payers when handling a future
1596+
/// `Event::StaticInvoiceRequested`.
1597+
invoice: StaticInvoice,
1598+
/// Useful for the recipient to replace a specific invoice stored by us as the static invoice
1599+
/// server.
1600+
///
1601+
/// When this invoice and its metadata are persisted, this slot number should be included so if
1602+
/// we receive another [`Event::PersistStaticInvoice`] containing the same slot number we can
1603+
/// swap the existing invoice out for the new one.
1604+
invoice_slot: u16,
1605+
/// An identifier for the recipient, originally provided to
1606+
/// [`ChannelManager::blinded_paths_for_async_recipient`].
1607+
///
1608+
/// When an `Event::StaticInvoiceRequested` comes in for the invoice, this id will be surfaced
1609+
/// and can be used alongside the `invoice_id` to retrieve the invoice from the database.
1610+
recipient_id: Vec<u8>,
1611+
/// A random identifier for the invoice. When an `Event::StaticInvoiceRequested` comes in for
1612+
/// the invoice, this id will be surfaced and can be used alongside the `recipient_id` to
1613+
/// retrieve the invoice from the database.
1614+
///
1615+
/// Note that this id will remain the same for all invoice updates corresponding to a particular
1616+
/// offer that the recipient has cached.
1617+
invoice_id: u128,
1618+
/// Once the [`StaticInvoice`], `invoice_slot` and `invoice_id` are persisted,
1619+
/// [`ChannelManager::static_invoice_persisted`] should be called with this responder to confirm
1620+
/// to the recipient that their [`Offer`] is ready to be used for async payments.
1621+
///
1622+
/// [`ChannelManager::static_invoice_persisted`]: crate::ln::channelmanager::ChannelManager::static_invoice_persisted
1623+
/// [`Offer`]: crate::offers::offer::Offer
1624+
invoice_persisted_path: Responder,
1625+
},
15851626
}
15861627

15871628
impl Writeable for Event {
@@ -2012,6 +2053,12 @@ impl Writeable for Event {
20122053
(8, former_temporary_channel_id, required),
20132054
});
20142055
},
2056+
#[cfg(async_payments)]
2057+
&Event::PersistStaticInvoice { .. } => {
2058+
45u8.write(writer)?;
2059+
// No need to write these events because we can just restart the static invoice negotiation
2060+
// on startup.
2061+
},
20152062
// Note that, going forward, all new events must only write data inside of
20162063
// `write_tlv_fields`. Versions 0.0.101+ will ignore odd-numbered events that write
20172064
// data via `write_tlv_fields`.
@@ -2583,6 +2630,9 @@ impl MaybeReadable for Event {
25832630
former_temporary_channel_id: former_temporary_channel_id.0.unwrap(),
25842631
}))
25852632
},
2633+
// Note that we do not write a length-prefixed TLV for PersistStaticInvoice events.
2634+
#[cfg(async_payments)]
2635+
45u8 => Ok(None),
25862636
// Versions prior to 0.0.100 did not ignore odd types, instead returning InvalidValue.
25872637
// Version 0.0.100 failed to properly ignore odd types, possibly resulting in corrupt
25882638
// reads.

lightning/src/ln/channelmanager.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5272,6 +5272,13 @@ where
52725272
}
52735273
}
52745274

5275+
/// Should be called after handling an [`Event::PersistStaticInvoice`], where the `Responder`
5276+
/// comes from [`Event::PersistStaticInvoice::invoice_persisted_path`].
5277+
#[cfg(async_payments)]
5278+
pub fn static_invoice_persisted(&self, invoice_persisted_path: Responder) {
5279+
self.flow.static_invoice_persisted(invoice_persisted_path);
5280+
}
5281+
52755282
#[cfg(async_payments)]
52765283
fn initiate_async_payment(
52775284
&self, invoice: &StaticInvoice, payment_id: PaymentId,
@@ -13535,6 +13542,31 @@ where
1353513542
&self, _message: ServeStaticInvoice, _context: AsyncPaymentsContext,
1353613543
_responder: Option<Responder>,
1353713544
) {
13545+
#[cfg(async_payments)]
13546+
{
13547+
let responder = match _responder {
13548+
Some(resp) => resp,
13549+
None => return,
13550+
};
13551+
13552+
let (recipient_id, invoice_id) =
13553+
match self.flow.verify_serve_static_invoice_message(&_message, _context) {
13554+
Ok((recipient_id, inv_id)) => (recipient_id, inv_id),
13555+
Err(()) => return,
13556+
};
13557+
13558+
let mut pending_events = self.pending_events.lock().unwrap();
13559+
pending_events.push_back((
13560+
Event::PersistStaticInvoice {
13561+
invoice: _message.invoice,
13562+
invoice_slot: _message.invoice_slot,
13563+
recipient_id,
13564+
invoice_id,
13565+
invoice_persisted_path: responder,
13566+
},
13567+
None,
13568+
));
13569+
}
1353813570
}
1353913571

1354013572
fn handle_static_invoice_persisted(

lightning/src/offers/flow.rs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ use {
6868
crate::offers::static_invoice::{StaticInvoice, StaticInvoiceBuilder},
6969
crate::onion_message::async_payments::{
7070
HeldHtlcAvailable, OfferPaths, OfferPathsRequest, ServeStaticInvoice,
71+
StaticInvoicePersisted,
7172
},
7273
crate::onion_message::messenger::Responder,
7374
};
@@ -234,6 +235,11 @@ where
234235
}
235236
}
236237

238+
/// The maximum size of a received [`StaticInvoice`] before we'll fail verification in
239+
/// [`OffersMessageFlow::verify_serve_static_invoice_message].
240+
#[cfg(async_payments)]
241+
pub const MAX_STATIC_INVOICE_SIZE_BYTES: usize = 5 * 1024;
242+
237243
/// Defines the maximum number of [`OffersMessage`] including different reply paths to be sent
238244
/// along different paths.
239245
/// Sending multiple requests increases the chances of successful delivery in case some
@@ -1532,6 +1538,55 @@ where
15321538
Ok((invoice, forward_invoice_request_path))
15331539
}
15341540

1541+
/// Verifies an incoming [`ServeStaticInvoice`] onion message from an often-offline recipient who
1542+
/// wants us as a static invoice server to serve the [`ServeStaticInvoice::invoice`] to payers on
1543+
/// their behalf.
1544+
///
1545+
/// On success, returns `(recipient_id, invoice_id)` for use in persisting and later retrieving
1546+
/// the static invoice from the database.
1547+
///
1548+
/// Errors if the [`ServeStaticInvoice::invoice`] is expired or larger than
1549+
/// [`MAX_STATIC_INVOICE_SIZE_BYTES`], or if blinded path verification fails.
1550+
///
1551+
/// [`ServeStaticInvoice::invoice`]: crate::onion_message::async_payments::ServeStaticInvoice::invoice
1552+
#[cfg(async_payments)]
1553+
pub fn verify_serve_static_invoice_message(
1554+
&self, message: &ServeStaticInvoice, context: AsyncPaymentsContext,
1555+
) -> Result<(Vec<u8>, u128), ()> {
1556+
if message.invoice.is_expired_no_std(self.duration_since_epoch()) {
1557+
return Err(());
1558+
}
1559+
if message.invoice.serialized_length() > MAX_STATIC_INVOICE_SIZE_BYTES {
1560+
return Err(());
1561+
}
1562+
match context {
1563+
AsyncPaymentsContext::ServeStaticInvoice {
1564+
recipient_id,
1565+
invoice_id,
1566+
path_absolute_expiry,
1567+
} => {
1568+
if self.duration_since_epoch() > path_absolute_expiry {
1569+
return Err(());
1570+
}
1571+
1572+
return Ok((recipient_id, invoice_id));
1573+
},
1574+
_ => return Err(()),
1575+
};
1576+
}
1577+
1578+
/// Indicates that a [`ServeStaticInvoice::invoice`] has been persisted and is ready to be served
1579+
/// to payers on behalf of an often-offline recipient. This method must be called after persisting
1580+
/// a [`StaticInvoice`] to confirm to the recipient that their corresponding [`Offer`] is ready to
1581+
/// receive async payments.
1582+
#[cfg(async_payments)]
1583+
pub fn static_invoice_persisted(&self, responder: Responder) {
1584+
let mut pending_async_payments_messages =
1585+
self.pending_async_payments_messages.lock().unwrap();
1586+
let message = AsyncPaymentsMessage::StaticInvoicePersisted(StaticInvoicePersisted {});
1587+
pending_async_payments_messages.push((message, responder.respond().into_instructions()));
1588+
}
1589+
15351590
/// Handles an incoming [`StaticInvoicePersisted`] onion message from the static invoice server.
15361591
/// Returns a bool indicating whether the async receive offer cache needs to be re-persisted.
15371592
///

0 commit comments

Comments
 (0)