|
66 | 66 | crate::offers::offer::Amount,
|
67 | 67 | crate::offers::signer,
|
68 | 68 | crate::offers::static_invoice::{StaticInvoice, StaticInvoiceBuilder},
|
69 |
| - crate::onion_message::async_payments::{HeldHtlcAvailable, OfferPathsRequest}, |
| 69 | + crate::onion_message::async_payments::{ |
| 70 | + HeldHtlcAvailable, OfferPaths, OfferPathsRequest, ServeStaticInvoice, |
| 71 | + }, |
| 72 | + crate::onion_message::messenger::Responder, |
70 | 73 | };
|
71 | 74 |
|
72 | 75 | #[cfg(feature = "dnssec")]
|
@@ -1189,6 +1192,158 @@ where
|
1189 | 1192 | Ok(())
|
1190 | 1193 | }
|
1191 | 1194 |
|
| 1195 | + /// Handles an incoming [`OfferPaths`] message from the static invoice server, sending out |
| 1196 | + /// [`ServeStaticInvoice`] onion messages in response if we've built a new async receive offer and |
| 1197 | + /// need the corresponding [`StaticInvoice`] to be persisted by the static invoice server. |
| 1198 | + /// |
| 1199 | + /// Returns `None` if we have enough offers cached already, verification of `message` fails, or we |
| 1200 | + /// fail to create blinded paths. |
| 1201 | + #[cfg(async_payments)] |
| 1202 | + pub(crate) fn handle_offer_paths<ES: Deref, R: Deref>( |
| 1203 | + &self, message: OfferPaths, context: AsyncPaymentsContext, responder: Responder, |
| 1204 | + peers: Vec<MessageForwardNode>, usable_channels: Vec<ChannelDetails>, entropy: ES, |
| 1205 | + router: R, |
| 1206 | + ) -> Option<(ServeStaticInvoice, MessageContext)> |
| 1207 | + where |
| 1208 | + ES::Target: EntropySource, |
| 1209 | + R::Target: Router, |
| 1210 | + { |
| 1211 | + let duration_since_epoch = self.duration_since_epoch(); |
| 1212 | + match context { |
| 1213 | + AsyncPaymentsContext::OfferPaths { path_absolute_expiry } => { |
| 1214 | + if duration_since_epoch > path_absolute_expiry { |
| 1215 | + return None; |
| 1216 | + } |
| 1217 | + }, |
| 1218 | + _ => return None, |
| 1219 | + } |
| 1220 | + |
| 1221 | + { |
| 1222 | + // Only respond with `ServeStaticInvoice` if we actually need a new offer built. |
| 1223 | + let mut cache = self.async_receive_offer_cache.lock().unwrap(); |
| 1224 | + cache.prune_expired_offers(duration_since_epoch, false); |
| 1225 | + if !cache.should_build_offer_with_paths( |
| 1226 | + &message.paths[..], |
| 1227 | + message.paths_absolute_expiry, |
| 1228 | + duration_since_epoch, |
| 1229 | + ) { |
| 1230 | + return None; |
| 1231 | + } |
| 1232 | + } |
| 1233 | + |
| 1234 | + let (mut offer_builder, offer_nonce) = |
| 1235 | + match self.create_async_receive_offer_builder(&*entropy, message.paths) { |
| 1236 | + Ok((builder, nonce)) => (builder, nonce), |
| 1237 | + Err(_) => return None, // Only reachable if OfferPaths::paths is empty |
| 1238 | + }; |
| 1239 | + if let Some(paths_absolute_expiry) = message.paths_absolute_expiry { |
| 1240 | + offer_builder = |
| 1241 | + offer_builder.absolute_expiry(Duration::from_secs(paths_absolute_expiry)); |
| 1242 | + } |
| 1243 | + let offer = match offer_builder.build() { |
| 1244 | + Ok(offer) => offer, |
| 1245 | + Err(_) => { |
| 1246 | + debug_assert!(false); |
| 1247 | + return None; |
| 1248 | + }, |
| 1249 | + }; |
| 1250 | + |
| 1251 | + let (invoice, forward_invoice_request_path) = match self.create_static_invoice_for_server( |
| 1252 | + &offer, |
| 1253 | + offer_nonce, |
| 1254 | + peers, |
| 1255 | + usable_channels, |
| 1256 | + &*entropy, |
| 1257 | + router, |
| 1258 | + ) { |
| 1259 | + Ok(res) => res, |
| 1260 | + Err(()) => return None, |
| 1261 | + }; |
| 1262 | + |
| 1263 | + let mut cache = self.async_receive_offer_cache.lock().unwrap(); |
| 1264 | + let cache_offer_res = cache.cache_pending_offer( |
| 1265 | + offer, |
| 1266 | + message.paths_absolute_expiry, |
| 1267 | + offer_nonce, |
| 1268 | + responder, |
| 1269 | + duration_since_epoch, |
| 1270 | + ); |
| 1271 | + core::mem::drop(cache); |
| 1272 | + |
| 1273 | + let invoice_slot = match cache_offer_res { |
| 1274 | + Ok(idx) => idx, |
| 1275 | + Err(()) => return None, |
| 1276 | + }; |
| 1277 | + |
| 1278 | + let reply_path_context = { |
| 1279 | + let path_absolute_expiry = |
| 1280 | + duration_since_epoch.saturating_add(TEMP_REPLY_PATH_RELATIVE_EXPIRY); |
| 1281 | + MessageContext::AsyncPayments(AsyncPaymentsContext::StaticInvoicePersisted { |
| 1282 | + path_absolute_expiry, |
| 1283 | + offer_slot: invoice_slot, |
| 1284 | + }) |
| 1285 | + }; |
| 1286 | + |
| 1287 | + let serve_invoice_message = |
| 1288 | + ServeStaticInvoice { invoice, forward_invoice_request_path, invoice_slot }; |
| 1289 | + Some((serve_invoice_message, reply_path_context)) |
| 1290 | + } |
| 1291 | + |
| 1292 | + /// Creates a [`StaticInvoice`] and a blinded path for the server to forward invoice requests from |
| 1293 | + /// payers to our node. |
| 1294 | + #[cfg(async_payments)] |
| 1295 | + fn create_static_invoice_for_server<ES: Deref, R: Deref>( |
| 1296 | + &self, offer: &Offer, offer_nonce: Nonce, peers: Vec<MessageForwardNode>, |
| 1297 | + usable_channels: Vec<ChannelDetails>, entropy: ES, router: R, |
| 1298 | + ) -> Result<(StaticInvoice, BlindedMessagePath), ()> |
| 1299 | + where |
| 1300 | + ES::Target: EntropySource, |
| 1301 | + R::Target: Router, |
| 1302 | + { |
| 1303 | + let expanded_key = &self.inbound_payment_key; |
| 1304 | + let duration_since_epoch = self.duration_since_epoch(); |
| 1305 | + let secp_ctx = &self.secp_ctx; |
| 1306 | + |
| 1307 | + let offer_relative_expiry = offer |
| 1308 | + .absolute_expiry() |
| 1309 | + .map(|exp| exp.saturating_sub(duration_since_epoch).as_secs()) |
| 1310 | + .map(|exp_u64| exp_u64.try_into().unwrap_or(u32::MAX)) |
| 1311 | + .unwrap_or(u32::MAX); |
| 1312 | + |
| 1313 | + // Set the invoice to expire at the same time as the offer. We aim to update this invoice as |
| 1314 | + // often as possible, so there shouldn't be any reason to have it expire earlier than the |
| 1315 | + // offer. |
| 1316 | + let payment_secret = inbound_payment::create_for_spontaneous_payment( |
| 1317 | + expanded_key, |
| 1318 | + None, // The async receive offers we create are always amount-less |
| 1319 | + offer_relative_expiry, |
| 1320 | + duration_since_epoch.as_secs(), |
| 1321 | + None, |
| 1322 | + )?; |
| 1323 | + |
| 1324 | + let invoice = self |
| 1325 | + .create_static_invoice_builder( |
| 1326 | + &router, |
| 1327 | + &*entropy, |
| 1328 | + &offer, |
| 1329 | + offer_nonce, |
| 1330 | + payment_secret, |
| 1331 | + offer_relative_expiry, |
| 1332 | + usable_channels, |
| 1333 | + peers.clone(), |
| 1334 | + ) |
| 1335 | + .and_then(|builder| builder.build_and_sign(secp_ctx)) |
| 1336 | + .map_err(|_| ())?; |
| 1337 | + |
| 1338 | + let nonce = Nonce::from_entropy_source(&*entropy); |
| 1339 | + let context = MessageContext::Offers(OffersContext::InvoiceRequest { nonce }); |
| 1340 | + let forward_invoice_request_path = self |
| 1341 | + .create_blinded_paths(peers, context) |
| 1342 | + .and_then(|paths| paths.into_iter().next().ok_or(()))?; |
| 1343 | + |
| 1344 | + Ok((invoice, forward_invoice_request_path)) |
| 1345 | + } |
| 1346 | + |
1192 | 1347 | /// Get the `AsyncReceiveOfferCache` for persistence.
|
1193 | 1348 | pub(crate) fn writeable_async_receive_offer_cache(&self) -> impl Writeable + '_ {
|
1194 | 1349 | &self.async_receive_offer_cache
|
|
0 commit comments