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