|
65 | 65 | crate::blinded_path::payment::AsyncBolt12OfferContext,
|
66 | 66 | crate::offers::offer::Amount,
|
67 | 67 | crate::offers::signer,
|
68 |
| - crate::offers::static_invoice::{StaticInvoice, StaticInvoiceBuilder}, |
69 |
| - crate::onion_message::async_payments::{HeldHtlcAvailable, OfferPathsRequest}, |
| 68 | + crate::offers::static_invoice::{ |
| 69 | + StaticInvoice, StaticInvoiceBuilder, |
| 70 | + DEFAULT_RELATIVE_EXPIRY as STATIC_INVOICE_DEFAULT_RELATIVE_EXPIRY, |
| 71 | + }, |
| 72 | + crate::onion_message::async_payments::{ |
| 73 | + HeldHtlcAvailable, OfferPaths, OfferPathsRequest, ServeStaticInvoice, |
| 74 | + }, |
| 75 | + crate::onion_message::messenger::Responder, |
70 | 76 | };
|
71 | 77 |
|
72 | 78 | #[cfg(feature = "dnssec")]
|
@@ -1176,6 +1182,161 @@ where
|
1176 | 1182 | Ok(())
|
1177 | 1183 | }
|
1178 | 1184 |
|
| 1185 | + /// Handles an incoming [`OfferPaths`] message from the static invoice server, sending out |
| 1186 | + /// [`ServeStaticInvoice`] onion messages in response if we want to use the paths we've received |
| 1187 | + /// to build and cache an async receive offer. |
| 1188 | + /// |
| 1189 | + /// Returns `None` if we have enough offers cached already, verification of `message` fails, or we |
| 1190 | + /// fail to create blinded paths. |
| 1191 | + #[cfg(async_payments)] |
| 1192 | + pub(crate) fn handle_offer_paths<ES: Deref, R: Deref>( |
| 1193 | + &self, message: OfferPaths, context: AsyncPaymentsContext, responder: Responder, |
| 1194 | + peers: Vec<MessageForwardNode>, usable_channels: Vec<ChannelDetails>, entropy: ES, |
| 1195 | + router: R, |
| 1196 | + ) -> Option<(ServeStaticInvoice, MessageContext)> |
| 1197 | + where |
| 1198 | + ES::Target: EntropySource, |
| 1199 | + R::Target: Router, |
| 1200 | + { |
| 1201 | + let expanded_key = &self.inbound_payment_key; |
| 1202 | + let duration_since_epoch = self.duration_since_epoch(); |
| 1203 | + |
| 1204 | + match context { |
| 1205 | + AsyncPaymentsContext::OfferPaths { nonce, hmac, path_absolute_expiry } => { |
| 1206 | + if let Err(()) = signer::verify_offer_paths_context(nonce, hmac, expanded_key) { |
| 1207 | + return None; |
| 1208 | + } |
| 1209 | + if duration_since_epoch > path_absolute_expiry { |
| 1210 | + return None; |
| 1211 | + } |
| 1212 | + }, |
| 1213 | + _ => return None, |
| 1214 | + } |
| 1215 | + |
| 1216 | + { |
| 1217 | + // Only respond with `ServeStaticInvoice` if we actually need a new offer built. |
| 1218 | + let cache = self.async_receive_offer_cache.lock().unwrap(); |
| 1219 | + if !cache.should_build_offer_with_paths(&message, duration_since_epoch) { |
| 1220 | + return None; |
| 1221 | + } |
| 1222 | + } |
| 1223 | + |
| 1224 | + let (mut offer_builder, offer_nonce) = |
| 1225 | + match self.create_async_receive_offer_builder(&*entropy, message.paths) { |
| 1226 | + Ok((builder, nonce)) => (builder, nonce), |
| 1227 | + Err(_) => return None, // Only reachable if OfferPaths::paths is empty |
| 1228 | + }; |
| 1229 | + if let Some(paths_absolute_expiry) = message.paths_absolute_expiry { |
| 1230 | + offer_builder = offer_builder.absolute_expiry(paths_absolute_expiry); |
| 1231 | + } |
| 1232 | + let offer = match offer_builder.build() { |
| 1233 | + Ok(offer) => offer, |
| 1234 | + Err(_) => { |
| 1235 | + debug_assert!(false); |
| 1236 | + return None; |
| 1237 | + }, |
| 1238 | + }; |
| 1239 | + |
| 1240 | + let (serve_invoice_message, reply_path_context) = match self |
| 1241 | + .create_serve_static_invoice_message( |
| 1242 | + offer, |
| 1243 | + offer_nonce, |
| 1244 | + duration_since_epoch, |
| 1245 | + peers, |
| 1246 | + usable_channels, |
| 1247 | + responder, |
| 1248 | + &*entropy, |
| 1249 | + router, |
| 1250 | + ) { |
| 1251 | + Ok((msg, context)) => (msg, context), |
| 1252 | + Err(()) => return None, |
| 1253 | + }; |
| 1254 | + |
| 1255 | + let context = MessageContext::AsyncPayments(reply_path_context); |
| 1256 | + Some((serve_invoice_message, context)) |
| 1257 | + } |
| 1258 | + |
| 1259 | + /// Creates a [`ServeStaticInvoice`] onion message, including reply path context for the static |
| 1260 | + /// invoice server to respond with [`StaticInvoicePersisted`]. |
| 1261 | + /// |
| 1262 | + /// [`StaticInvoicePersisted`]: crate::onion_message::async_payments::StaticInvoicePersisted |
| 1263 | + #[cfg(async_payments)] |
| 1264 | + fn create_serve_static_invoice_message<ES: Deref, R: Deref>( |
| 1265 | + &self, offer: Offer, offer_nonce: Nonce, offer_created_at: Duration, |
| 1266 | + peers: Vec<MessageForwardNode>, usable_channels: Vec<ChannelDetails>, |
| 1267 | + update_static_invoice_path: Responder, entropy: ES, router: R, |
| 1268 | + ) -> Result<(ServeStaticInvoice, AsyncPaymentsContext), ()> |
| 1269 | + where |
| 1270 | + ES::Target: EntropySource, |
| 1271 | + R::Target: Router, |
| 1272 | + { |
| 1273 | + let expanded_key = &self.inbound_payment_key; |
| 1274 | + let duration_since_epoch = self.duration_since_epoch(); |
| 1275 | + let secp_ctx = &self.secp_ctx; |
| 1276 | + |
| 1277 | + let offer_relative_expiry = offer |
| 1278 | + .absolute_expiry() |
| 1279 | + .map(|exp| exp.saturating_sub(duration_since_epoch)) |
| 1280 | + .unwrap_or_else(|| Duration::from_secs(u64::MAX)); |
| 1281 | + |
| 1282 | + // We limit the static invoice lifetime to STATIC_INVOICE_DEFAULT_RELATIVE_EXPIRY, meaning we'll |
| 1283 | + // need to refresh the static invoice using the reply path to the `OfferPaths` message if the |
| 1284 | + // offer expires later than that. |
| 1285 | + let static_invoice_relative_expiry = core::cmp::min( |
| 1286 | + offer_relative_expiry.as_secs(), |
| 1287 | + STATIC_INVOICE_DEFAULT_RELATIVE_EXPIRY.as_secs(), |
| 1288 | + ) as u32; |
| 1289 | + |
| 1290 | + let payment_secret = inbound_payment::create_for_spontaneous_payment( |
| 1291 | + expanded_key, |
| 1292 | + None, // The async receive offers we create are always amount-less |
| 1293 | + static_invoice_relative_expiry, |
| 1294 | + self.duration_since_epoch().as_secs(), |
| 1295 | + None, |
| 1296 | + )?; |
| 1297 | + |
| 1298 | + let invoice = self |
| 1299 | + .create_static_invoice_builder( |
| 1300 | + &router, |
| 1301 | + &*entropy, |
| 1302 | + &offer, |
| 1303 | + offer_nonce, |
| 1304 | + payment_secret, |
| 1305 | + static_invoice_relative_expiry, |
| 1306 | + usable_channels, |
| 1307 | + peers.clone(), |
| 1308 | + ) |
| 1309 | + .and_then(|builder| builder.build_and_sign(secp_ctx)) |
| 1310 | + .map_err(|_| ())?; |
| 1311 | + |
| 1312 | + let nonce = Nonce::from_entropy_source(&*entropy); |
| 1313 | + let context = MessageContext::Offers(OffersContext::InvoiceRequest { nonce }); |
| 1314 | + let forward_invoice_request_path = self |
| 1315 | + .create_blinded_paths(peers, context) |
| 1316 | + .and_then(|paths| paths.into_iter().next().ok_or(()))?; |
| 1317 | + |
| 1318 | + let reply_path_context = { |
| 1319 | + let nonce = Nonce::from_entropy_source(entropy); |
| 1320 | + let hmac = signer::hmac_for_static_invoice_persisted_context(nonce, expanded_key); |
| 1321 | + let static_invoice_absolute_expiry = |
| 1322 | + invoice.created_at().saturating_add(invoice.relative_expiry()); |
| 1323 | + let path_absolute_expiry = |
| 1324 | + duration_since_epoch.saturating_add(TEMP_REPLY_PATH_RELATIVE_EXPIRY); |
| 1325 | + AsyncPaymentsContext::StaticInvoicePersisted { |
| 1326 | + offer, |
| 1327 | + offer_nonce, |
| 1328 | + offer_created_at, |
| 1329 | + update_static_invoice_path, |
| 1330 | + static_invoice_absolute_expiry, |
| 1331 | + nonce, |
| 1332 | + hmac, |
| 1333 | + path_absolute_expiry, |
| 1334 | + } |
| 1335 | + }; |
| 1336 | + |
| 1337 | + Ok((ServeStaticInvoice { invoice, forward_invoice_request_path }, reply_path_context)) |
| 1338 | + } |
| 1339 | + |
1179 | 1340 | /// Get the `AsyncReceiveOfferCache` for persistence.
|
1180 | 1341 | pub(crate) fn writeable_async_receive_offer_cache(&self) -> impl Writeable + '_ {
|
1181 | 1342 | &self.async_receive_offer_cache
|
|
0 commit comments