Skip to content

Commit ea6160d

Browse files
Track cached async receive offers in OffersMessageFlow
In future commits, as part of being an async recipient, we will interactively build offers and static invoices with an always-online node that will serve static invoices on our behalf. Once an offer is built and we've requested persistence of the corresponding invoice from the server, we will use the new offer cache added here to save the invoice metadata and the offer in ChannelManager, though the OffersMessageFlow is responsible for keeping the cache updated. We want to cache and persist these offers so we always have them at the ready, we don't want to begin the process of interactively building an offer the moment it is needed. The offers are likely to be long-lived so caching them avoids having to keep interactively rebuilding them after every restart.
1 parent d06d109 commit ea6160d

File tree

4 files changed

+199
-1
lines changed

4 files changed

+199
-1
lines changed

lightning/src/ln/channelmanager.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ use crate::ln::outbound_payment::{
8686
StaleExpiration,
8787
};
8888
use crate::ln::types::ChannelId;
89+
use crate::offers::async_receive_offer_cache::AsyncReceiveOfferCache;
8990
use crate::offers::flow::OffersMessageFlow;
9091
use crate::offers::invoice::{
9192
Bolt12Invoice, DerivedSigningPubkey, InvoiceBuilder, DEFAULT_RELATIVE_EXPIRY,
@@ -14264,6 +14265,7 @@ where
1426414265
(15, self.inbound_payment_id_secret, required),
1426514266
(17, in_flight_monitor_updates, option),
1426614267
(19, peer_storage_dir, optional_vec),
14268+
(21, self.flow.writeable_async_receive_offer_cache(), required),
1426714269
});
1426814270

1426914271
Ok(())
@@ -14843,6 +14845,7 @@ where
1484314845
let mut decode_update_add_htlcs: Option<HashMap<u64, Vec<msgs::UpdateAddHTLC>>> = None;
1484414846
let mut inbound_payment_id_secret = None;
1484514847
let mut peer_storage_dir: Option<Vec<(PublicKey, Vec<u8>)>> = None;
14848+
let mut async_receive_offer_cache: AsyncReceiveOfferCache = AsyncReceiveOfferCache::new();
1484614849
read_tlv_fields!(reader, {
1484714850
(1, pending_outbound_payments_no_retry, option),
1484814851
(2, pending_intercepted_htlcs, option),
@@ -14860,6 +14863,7 @@ where
1486014863
(15, inbound_payment_id_secret, option),
1486114864
(17, in_flight_monitor_updates, option),
1486214865
(19, peer_storage_dir, optional_vec),
14866+
(21, async_receive_offer_cache, (default_value, async_receive_offer_cache)),
1486314867
});
1486414868
let mut decode_update_add_htlcs = decode_update_add_htlcs.unwrap_or_else(|| new_hash_map());
1486514869
let peer_storage_dir: Vec<(PublicKey, Vec<u8>)> = peer_storage_dir.unwrap_or_else(Vec::new);
@@ -15546,7 +15550,7 @@ where
1554615550
chain_hash, best_block, our_network_pubkey,
1554715551
highest_seen_timestamp, expanded_inbound_key,
1554815552
secp_ctx.clone(), args.message_router
15549-
);
15553+
).with_async_payments_offers_cache(async_receive_offer_cache);
1555015554

1555115555
let channel_manager = ChannelManager {
1555215556
chain_hash,
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
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 methods for caching offers that we interactively build with a static invoice
11+
//! server as an async recipient. The static invoice server will serve the resulting invoices to
12+
//! payers on our behalf when we're offline.
13+
14+
use crate::blinded_path::message::BlindedMessagePath;
15+
use crate::io;
16+
use crate::io::Read;
17+
use crate::ln::msgs::DecodeError;
18+
use crate::offers::nonce::Nonce;
19+
use crate::offers::offer::Offer;
20+
use crate::onion_message::messenger::Responder;
21+
use crate::prelude::*;
22+
use crate::util::ser::{Readable, Writeable, Writer};
23+
use core::time::Duration;
24+
25+
/// The status of this offer in the cache.
26+
enum OfferStatus {
27+
/// This offer has been returned to the user from the cache, so it needs to be stored until it
28+
/// expires and its invoice needs to be kept updated.
29+
Used,
30+
/// This offer has not yet been returned to the user, and is safe to replace to ensure we always
31+
/// have a maximally fresh offer. We always want to have at least 1 offer in this state,
32+
/// preferably a few so we can respond to user requests for new offers without returning the same
33+
/// one multiple times. Returning a new offer each time is better for privacy.
34+
Ready {
35+
/// If this offer's invoice has been persisted for some time, it's safe to replace to ensure we
36+
/// always have the freshest possible offer available when the user goes to pull an offer from
37+
/// the cache.
38+
invoice_confirmed_persisted_at: Duration,
39+
},
40+
/// This offer's invoice is not yet confirmed as persisted by the static invoice server, so it is
41+
/// not yet ready to receive payments.
42+
Pending,
43+
}
44+
45+
struct AsyncReceiveOffer {
46+
offer: Offer,
47+
/// Whether this offer is used, ready for use, or pending invoice persistence with the static
48+
/// invoice server.
49+
status: OfferStatus,
50+
51+
/// The below fields are used to generate and persist a new static invoice with the invoice
52+
/// server. We support automatically rotating the invoice for long-lived offers so users don't
53+
/// have to update the offer they've posted on e.g. their website if fees change or the invoices'
54+
/// payment paths become otherwise outdated.
55+
offer_nonce: Nonce,
56+
update_static_invoice_path: Responder,
57+
}
58+
59+
impl_writeable_tlv_based_enum!(OfferStatus,
60+
(0, Used) => {},
61+
(1, Ready) => {
62+
(0, invoice_confirmed_persisted_at, required),
63+
},
64+
(2, Pending) => {},
65+
);
66+
67+
impl_writeable_tlv_based!(AsyncReceiveOffer, {
68+
(0, offer, required),
69+
(2, offer_nonce, required),
70+
(4, status, required),
71+
(6, update_static_invoice_path, required),
72+
});
73+
74+
/// If we are an often-offline recipient, we'll want to interactively build offers and static
75+
/// invoices with an always-online node that will serve those static invoices to payers on our
76+
/// behalf when we are offline.
77+
///
78+
/// This struct is used to cache those interactively built offers, and should be passed into
79+
/// [`OffersMessageFlow`] on startup as well as persisted whenever an offer or invoice is updated.
80+
///
81+
/// ## Lifecycle of a cached offer
82+
///
83+
/// 1. On initial startup, recipients will request offer paths from the static invoice server
84+
/// 2. Once a set of offer paths is received, recipients will build an offer and corresponding
85+
/// static invoice, cache the offer as pending, and send the invoice to the server for
86+
/// persistence
87+
/// 3. Once the invoice is confirmed as persisted by the server, the recipient will mark the
88+
/// corresponding offer as ready to receive payments
89+
/// 4. If the offer is later returned to the user, it will be kept cached and its invoice will be
90+
/// kept up-to-date until the offer expires
91+
/// 5. If the offer does not get returned to the user within a certain timeframe, it will be
92+
/// replaced with a new one using fresh offer paths requested from the static invoice server
93+
///
94+
/// ## Staying in sync with the Static Invoice Server
95+
///
96+
/// * Pending offers: for a given cached offer where a corresponding invoice is not yet confirmed as
97+
/// persisted by the static invoice server, we will retry persisting an invoice for that offer until
98+
/// it succeeds, once per timer tick
99+
/// * Confirmed offers that have not yet been returned to the user: we will periodically replace an
100+
/// unused confirmed offer with a new one, to try to always have a fresh offer available. We wait
101+
/// several hours in between replacements to ensure the new offer replacement doesn't conflict with
102+
/// the old one
103+
/// * Confirmed offers that have been returned to the user: we will send the server a fresh invoice
104+
/// corresponding to each used offer once per timer tick until the offer expires
105+
///
106+
/// [`OffersMessageFlow`]: crate::offers::flow::OffersMessageFlow
107+
pub struct AsyncReceiveOfferCache {
108+
/// The cache is allocated up-front with a fixed number of slots for offers, where each slot is
109+
/// filled in with an AsyncReceiveOffer as they are interactively built.
110+
///
111+
/// We only want to store a limited number of static invoices with the server, and those stored
112+
/// invoices need to regularly be replaced with new ones. When sending a replacement invoice to
113+
/// the server, we indicate which invoice is being replaced by the invoice's "slot number",
114+
/// see [`ServeStaticInvoice::invoice_slot`]. So rather than internally tracking which cached
115+
/// offer corresponds to what invoice slot number on the server's end, we always set the slot
116+
/// number to the index of the offer in the cache.
117+
///
118+
/// [`ServeStaticInvoice::invoice_slot`]: crate::onion_message::async_payments::ServeStaticInvoice
119+
offers: Vec<Option<AsyncReceiveOffer>>,
120+
/// Used to limit the number of times we request paths for our offer from the static invoice
121+
/// server.
122+
#[allow(unused)] // TODO: remove when we get rid of async payments cfg flag
123+
offer_paths_request_attempts: u8,
124+
/// Blinded paths used to request offer paths from the static invoice server.
125+
#[allow(unused)] // TODO: remove when we get rid of async payments cfg flag
126+
paths_to_static_invoice_server: Vec<BlindedMessagePath>,
127+
}
128+
129+
impl AsyncReceiveOfferCache {
130+
/// Creates an empty [`AsyncReceiveOfferCache`] to be passed into [`OffersMessageFlow`].
131+
///
132+
/// [`OffersMessageFlow`]: crate::offers::flow::OffersMessageFlow
133+
pub fn new() -> Self {
134+
Self {
135+
offers: Vec::new(),
136+
offer_paths_request_attempts: 0,
137+
paths_to_static_invoice_server: Vec::new(),
138+
}
139+
}
140+
141+
pub(super) fn paths_to_static_invoice_server(&self) -> Vec<BlindedMessagePath> {
142+
self.paths_to_static_invoice_server.clone()
143+
}
144+
}
145+
146+
impl Writeable for AsyncReceiveOfferCache {
147+
fn write<W: Writer>(&self, w: &mut W) -> Result<(), io::Error> {
148+
write_tlv_fields!(w, {
149+
(0, self.offers, required_vec),
150+
(2, self.paths_to_static_invoice_server, required_vec),
151+
// offer paths request retry info always resets on restart
152+
});
153+
Ok(())
154+
}
155+
}
156+
157+
impl Readable for AsyncReceiveOfferCache {
158+
fn read<R: Read>(r: &mut R) -> Result<Self, DecodeError> {
159+
_init_and_read_len_prefixed_tlv_fields!(r, {
160+
(0, offers, required_vec),
161+
(2, paths_to_static_invoice_server, required_vec),
162+
});
163+
let offers: Vec<Option<AsyncReceiveOffer>> = offers;
164+
Ok(Self { offers, offer_paths_request_attempts: 0, paths_to_static_invoice_server })
165+
}
166+
}

lightning/src/offers/flow.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ use crate::ln::channelmanager::{
3636
Verification, {PaymentId, CLTV_FAR_FAR_AWAY, MAX_SHORT_LIVED_RELATIVE_EXPIRY},
3737
};
3838
use crate::ln::inbound_payment;
39+
use crate::offers::async_receive_offer_cache::AsyncReceiveOfferCache;
3940
use crate::offers::invoice::{
4041
Bolt12Invoice, DerivedSigningPubkey, ExplicitSigningPubkey, InvoiceBuilder,
4142
UnsignedBolt12Invoice, DEFAULT_RELATIVE_EXPIRY,
@@ -56,6 +57,7 @@ use crate::routing::router::Router;
5657
use crate::sign::{EntropySource, NodeSigner};
5758
use crate::sync::{Mutex, RwLock};
5859
use crate::types::payment::{PaymentHash, PaymentSecret};
60+
use crate::util::ser::Writeable;
5961

6062
#[cfg(async_payments)]
6163
use {
@@ -98,6 +100,10 @@ where
98100
pub(crate) pending_offers_messages: Mutex<Vec<(OffersMessage, MessageSendInstructions)>>,
99101

100102
pending_async_payments_messages: Mutex<Vec<(AsyncPaymentsMessage, MessageSendInstructions)>>,
103+
async_receive_offer_cache: Mutex<AsyncReceiveOfferCache>,
104+
/// Blinded paths used to request offer paths from the static invoice server, if we are an async
105+
/// recipient.
106+
paths_to_static_invoice_server: Mutex<Vec<BlindedMessagePath>>,
101107

102108
#[cfg(feature = "dnssec")]
103109
pub(crate) hrn_resolver: OMNameResolver,
@@ -133,9 +139,25 @@ where
133139
hrn_resolver: OMNameResolver::new(current_timestamp, best_block.height),
134140
#[cfg(feature = "dnssec")]
135141
pending_dns_onion_messages: Mutex::new(Vec::new()),
142+
143+
async_receive_offer_cache: Mutex::new(AsyncReceiveOfferCache::new()),
144+
paths_to_static_invoice_server: Mutex::new(Vec::new()),
136145
}
137146
}
138147

148+
/// If we are an async recipient, on startup we'll interactively build offers and static invoices
149+
/// with an always-online node that will serve static invoices on our behalf. Once the offer is
150+
/// built and the static invoice is confirmed as persisted by the server, the underlying
151+
/// [`AsyncReceiveOfferCache`] should be persisted so we remember the offers we've built.
152+
pub(crate) fn with_async_payments_offers_cache(
153+
mut self, async_receive_offer_cache: AsyncReceiveOfferCache,
154+
) -> Self {
155+
self.paths_to_static_invoice_server =
156+
Mutex::new(async_receive_offer_cache.paths_to_static_invoice_server());
157+
self.async_receive_offer_cache = Mutex::new(async_receive_offer_cache);
158+
self
159+
}
160+
139161
/// Gets the node_id held by this [`OffersMessageFlow`]`
140162
fn get_our_node_id(&self) -> PublicKey {
141163
self.our_network_pubkey
@@ -1082,4 +1104,9 @@ where
10821104
) -> Vec<(DNSResolverMessage, MessageSendInstructions)> {
10831105
core::mem::take(&mut self.pending_dns_onion_messages.lock().unwrap())
10841106
}
1107+
1108+
/// Get the `AsyncReceiveOfferCache` for persistence.
1109+
pub(crate) fn writeable_async_receive_offer_cache(&self) -> impl Writeable + '_ {
1110+
&self.async_receive_offer_cache
1111+
}
10851112
}

lightning/src/offers/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
pub mod offer;
1717
pub mod flow;
1818

19+
pub(crate) mod async_receive_offer_cache;
1920
pub mod invoice;
2021
pub mod invoice_error;
2122
mod invoice_macros;

0 commit comments

Comments
 (0)