Skip to content

Commit 5b567a1

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 2ffe5e5 commit 5b567a1

File tree

4 files changed

+203
-1
lines changed

4 files changed

+203
-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: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
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 in a burst on
84+
/// each timer tick
85+
/// 2. Once offer paths are received, recipients will build offers and corresponding static
86+
/// invoices, cache the offers as `OfferStatus::Pending`, and send the invoices to the server for
87+
/// persistence
88+
/// 3. While the invoices remain unconfirmed as persisted by the server, the recipient will send a
89+
/// fresh invoice corresponding each pending offer to the server on each timer tick
90+
/// 4. Once an invoice is confirmed as persisted by the server, the recipient will mark the
91+
/// corresponding offer as ready to receive payments (`OfferStatus::Ready`)
92+
/// 5. If a ready-to-receive offer gets returned to the user, the cache will mark that offer as
93+
/// `OfferStatus::Used` and attempt to update the server-persisted invoice corresponding to that
94+
/// offer once per timer tick until the offer expires
95+
/// 6. If a ready-to-receive offer in the cache is several hours old and has not yet been seen by
96+
/// the user, the cache will interactively build a new replacement offer and send the
97+
/// corresponding invoice to the server. This way the cache tries to always have a fresh unused
98+
/// offer ready to go.
99+
///
100+
/// [`OffersMessageFlow`]: crate::offers::flow::OffersMessageFlow
101+
pub struct AsyncReceiveOfferCache {
102+
/// The cache is allocated up-front with a fixed number of slots for offers, where each slot is
103+
/// filled in with an AsyncReceiveOffer as they are interactively built.
104+
///
105+
/// We only want to store a limited number of static invoices with the server, and those stored
106+
/// invoices need to regularly be replaced with new ones. When sending a replacement invoice to
107+
/// the server, we indicate which invoice is being replaced by the invoice's "slot number",
108+
/// see [`ServeStaticInvoice::invoice_slot`]. So rather than internally tracking which cached
109+
/// offer corresponds to what invoice slot number on the server's end, we always set the slot
110+
/// number to the index of the offer in the cache.
111+
///
112+
/// [`ServeStaticInvoice::invoice_slot`]: crate::onion_message::async_payments::ServeStaticInvoice
113+
offers: Vec<Option<AsyncReceiveOffer>>,
114+
/// Used to limit the number of times we request paths for our offer from the static invoice
115+
/// server.
116+
#[allow(unused)] // TODO: remove when we get rid of async payments cfg flag
117+
offer_paths_request_attempts: u8,
118+
/// Used to determine whether enough time has passed since our last request for offer paths that
119+
/// more requests should be allowed to go out.
120+
#[allow(unused)] // TODO: remove when we get rid of async payments cfg flag
121+
last_offer_paths_request_timestamp: Duration,
122+
/// Blinded paths used to request offer paths from the static invoice server.
123+
#[allow(unused)] // TODO: remove when we get rid of async payments cfg flag
124+
paths_to_static_invoice_server: Vec<BlindedMessagePath>,
125+
}
126+
127+
impl AsyncReceiveOfferCache {
128+
/// Creates an empty [`AsyncReceiveOfferCache`] to be passed into [`OffersMessageFlow`].
129+
///
130+
/// [`OffersMessageFlow`]: crate::offers::flow::OffersMessageFlow
131+
pub fn new() -> Self {
132+
Self {
133+
offers: Vec::new(),
134+
offer_paths_request_attempts: 0,
135+
last_offer_paths_request_timestamp: Duration::from_secs(0),
136+
paths_to_static_invoice_server: Vec::new(),
137+
}
138+
}
139+
140+
pub(super) fn paths_to_static_invoice_server(&self) -> Vec<BlindedMessagePath> {
141+
self.paths_to_static_invoice_server.clone()
142+
}
143+
}
144+
145+
impl Writeable for AsyncReceiveOfferCache {
146+
fn write<W: Writer>(&self, w: &mut W) -> Result<(), io::Error> {
147+
write_tlv_fields!(w, {
148+
(0, self.offers, required_vec),
149+
(2, self.paths_to_static_invoice_server, required_vec),
150+
// offer paths request retry info always resets on restart
151+
});
152+
Ok(())
153+
}
154+
}
155+
156+
impl Readable for AsyncReceiveOfferCache {
157+
fn read<R: Read>(r: &mut R) -> Result<Self, DecodeError> {
158+
_init_and_read_len_prefixed_tlv_fields!(r, {
159+
(0, offers, required_vec),
160+
(2, paths_to_static_invoice_server, required_vec),
161+
});
162+
let offers: Vec<Option<AsyncReceiveOffer>> = offers;
163+
Ok(Self {
164+
offers,
165+
offer_paths_request_attempts: 0,
166+
last_offer_paths_request_timestamp: Duration::from_secs(0),
167+
paths_to_static_invoice_server,
168+
})
169+
}
170+
}

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)