|
| 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 | +} |
0 commit comments