|
70 | 70 | StaticInvoice, StaticInvoiceBuilder, |
71 | 71 | DEFAULT_RELATIVE_EXPIRY as STATIC_INVOICE_DEFAULT_RELATIVE_EXPIRY, |
72 | 72 | }, |
73 | | - crate::onion_message::async_payments::{HeldHtlcAvailable, OfferPathsRequest}, |
| 73 | + crate::onion_message::async_payments::{ |
| 74 | + HeldHtlcAvailable, OfferPaths, OfferPathsRequest, ServeStaticInvoice, |
| 75 | + }, |
| 76 | + crate::onion_message::messenger::Responder, |
74 | 77 | }; |
75 | 78 |
|
76 | 79 | #[cfg(feature = "dnssec")] |
@@ -477,29 +480,61 @@ where |
477 | 480 | &self, absolute_expiry: Option<Duration>, nonce: Option<Nonce>, |
478 | 481 | peers: Vec<MessageForwardNode>, |
479 | 482 | ) -> Result<OfferBuilder<DerivedMetadata, secp256k1::All>, Bolt12SemanticError> { |
| 483 | + self.create_offer_builder_internal(absolute_expiry, nonce, peers, None) |
| 484 | + .map(|(offer, _nonce)| offer) |
| 485 | + } |
| 486 | + |
| 487 | + /// NB: the offer utils diff in this commit will go away when the offers message flow PR is updated |
| 488 | + pub fn create_async_receive_offer_builder( |
| 489 | + &self, absolute_expiry: Option<Duration>, peers: Vec<MessageForwardNode>, |
| 490 | + message_paths_to_always_online_node: Vec<BlindedMessagePath>, |
| 491 | + ) -> Result<(OfferBuilder<DerivedMetadata, secp256k1::All>, Nonce), Bolt12SemanticError> { |
| 492 | + self.create_offer_builder_internal( |
| 493 | + absolute_expiry, |
| 494 | + None, |
| 495 | + peers, |
| 496 | + Some(message_paths_to_always_online_node), |
| 497 | + ) |
| 498 | + } |
| 499 | + |
| 500 | + fn create_offer_builder_internal( |
| 501 | + &self, absolute_expiry: Option<Duration>, nonce: Option<Nonce>, |
| 502 | + peers: Vec<MessageForwardNode>, |
| 503 | + message_paths_to_always_online_node: Option<Vec<BlindedMessagePath>>, |
| 504 | + ) -> Result<(OfferBuilder<DerivedMetadata, secp256k1::All>, Nonce), Bolt12SemanticError> { |
480 | 505 | let node_id = self.get_our_node_id(); |
481 | 506 | let expanded_key = &self.inbound_payment_key; |
482 | 507 | let entropy = &*self.entropy_source; |
483 | 508 | let secp_ctx = &self.secp_ctx; |
484 | 509 |
|
485 | | - let nonce = nonce.unwrap_or(Nonce::from_entropy_source(entropy)); |
486 | | - let context = OffersContext::InvoiceRequest { nonce }; |
| 510 | + let offer_nonce = nonce.unwrap_or(Nonce::from_entropy_source(entropy)); |
| 511 | + let context = OffersContext::InvoiceRequest { nonce: offer_nonce }; |
487 | 512 |
|
488 | | - let path = self |
489 | | - .create_blinded_paths_using_absolute_expiry(context, absolute_expiry, peers) |
490 | | - .and_then(|paths| paths.into_iter().next().ok_or(())) |
491 | | - .map_err(|_| Bolt12SemanticError::MissingPaths)?; |
| 513 | + let mut builder = |
| 514 | + OfferBuilder::deriving_signing_pubkey(node_id, expanded_key, offer_nonce, secp_ctx) |
| 515 | + .chain_hash(self.chain_hash); |
492 | 516 |
|
493 | | - let builder = OfferBuilder::deriving_signing_pubkey(node_id, expanded_key, nonce, secp_ctx) |
494 | | - .chain_hash(self.chain_hash) |
495 | | - .path(path); |
| 517 | + match message_paths_to_always_online_node { |
| 518 | + Some(paths) => { |
| 519 | + for path in paths { |
| 520 | + builder = builder.path(path); |
| 521 | + } |
| 522 | + }, |
| 523 | + None => { |
| 524 | + let path = self |
| 525 | + .create_blinded_paths_using_absolute_expiry(context, absolute_expiry, peers) |
| 526 | + .and_then(|paths| paths.into_iter().next().ok_or(())) |
| 527 | + .map_err(|_| Bolt12SemanticError::MissingPaths)?; |
| 528 | + builder = builder.path(path); |
| 529 | + }, |
| 530 | + } |
496 | 531 |
|
497 | 532 | let builder = match absolute_expiry { |
498 | 533 | None => builder, |
499 | 534 | Some(absolute_expiry) => builder.absolute_expiry(absolute_expiry), |
500 | 535 | }; |
501 | 536 |
|
502 | | - Ok(builder) |
| 537 | + Ok((builder, offer_nonce)) |
503 | 538 | } |
504 | 539 |
|
505 | 540 | /// Creates a [`RefundBuilder`] such that the [`Refund`] it builds is recognized by the |
@@ -1100,4 +1135,126 @@ where |
1100 | 1135 |
|
1101 | 1136 | Ok(()) |
1102 | 1137 | } |
| 1138 | + |
| 1139 | + /// Handles an incoming [`OfferPaths`] onion message from the static invoice server, sending out |
| 1140 | + /// [`ServeStaticInvoice`] onion messages in response if we want to use the paths we've received |
| 1141 | + /// to build and cache an async receive offer. |
| 1142 | + #[cfg(async_payments)] |
| 1143 | + pub(crate) fn handle_offer_paths( |
| 1144 | + &self, message: OfferPaths, context: AsyncPaymentsContext, responder: Responder, |
| 1145 | + peers: Vec<MessageForwardNode>, usable_channels: Vec<ChannelDetails>, |
| 1146 | + ) -> Option<(ServeStaticInvoice, MessageContext)> { |
| 1147 | + let expanded_key = &self.inbound_payment_key; |
| 1148 | + let duration_since_epoch = self.duration_since_epoch(); |
| 1149 | + |
| 1150 | + match context { |
| 1151 | + AsyncPaymentsContext::OfferPaths { nonce, hmac, path_absolute_expiry } => { |
| 1152 | + if let Err(()) = signer::verify_offer_paths_context(nonce, hmac, expanded_key) { |
| 1153 | + return None; |
| 1154 | + } |
| 1155 | + if duration_since_epoch > path_absolute_expiry { |
| 1156 | + return None; |
| 1157 | + } |
| 1158 | + }, |
| 1159 | + _ => return None, |
| 1160 | + } |
| 1161 | + |
| 1162 | + { |
| 1163 | + // Only respond with ServeStaticInvoice if we actually need a new offer built. |
| 1164 | + let mut cache = self.async_receive_offer_cache.lock().unwrap(); |
| 1165 | + if !cache.should_build_offer_with_paths(&message, duration_since_epoch) { |
| 1166 | + return None; |
| 1167 | + } |
| 1168 | + } |
| 1169 | + |
| 1170 | + let (offer_builder, offer_nonce) = match self.create_async_receive_offer_builder( |
| 1171 | + message.paths_absolute_expiry, |
| 1172 | + peers.clone(), |
| 1173 | + message.paths, |
| 1174 | + ) { |
| 1175 | + Ok((builder, nonce)) => (builder, nonce), |
| 1176 | + Err(_e) => return None, // TODO log error |
| 1177 | + }; |
| 1178 | + let offer = match offer_builder.build() { |
| 1179 | + Ok(offer) => offer, |
| 1180 | + Err(_e) => return None, // TODO log error |
| 1181 | + }; |
| 1182 | + |
| 1183 | + let (serve_invoice_message, reply_path_context) = match self |
| 1184 | + .create_serve_static_invoice_message( |
| 1185 | + offer, |
| 1186 | + offer_nonce, |
| 1187 | + duration_since_epoch, |
| 1188 | + peers, |
| 1189 | + usable_channels, |
| 1190 | + responder, |
| 1191 | + ) { |
| 1192 | + Ok((msg, context)) => (msg, context), |
| 1193 | + Err(()) => return None, |
| 1194 | + }; |
| 1195 | + |
| 1196 | + let context = MessageContext::AsyncPayments(reply_path_context); |
| 1197 | + Some((serve_invoice_message, context)) |
| 1198 | + } |
| 1199 | + |
| 1200 | + /// Creates a [`ServeStaticInvoice`] onion message, including reply path context for the static |
| 1201 | + /// invoice server to respond with [`StaticInvoicePersisted`]. |
| 1202 | + /// |
| 1203 | + /// [`StaticInvoicePersisted`]: crate::onion_message::async_payments::StaticInvoicePersisted |
| 1204 | + #[cfg(async_payments)] |
| 1205 | + fn create_serve_static_invoice_message( |
| 1206 | + &self, offer: Offer, offer_nonce: Nonce, offer_created_at: Duration, |
| 1207 | + peers: Vec<MessageForwardNode>, usable_channels: Vec<ChannelDetails>, |
| 1208 | + update_static_invoice_path: Responder, |
| 1209 | + ) -> Result<(ServeStaticInvoice, AsyncPaymentsContext), ()> { |
| 1210 | + let expanded_key = &self.inbound_payment_key; |
| 1211 | + let entropy = &*self.entropy_source; |
| 1212 | + let duration_since_epoch = self.duration_since_epoch(); |
| 1213 | + let secp_ctx = &self.secp_ctx; |
| 1214 | + const REPLY_PATH_RELATIVE_EXPIRY: Duration = Duration::from_secs(7200); |
| 1215 | + |
| 1216 | + let offer_relative_expiry = offer |
| 1217 | + .absolute_expiry() |
| 1218 | + .unwrap_or_else(|| Duration::from_secs(u64::MAX)) |
| 1219 | + .saturating_sub(duration_since_epoch); |
| 1220 | + |
| 1221 | + // We limit the static invoice lifetime to STATIC_INVOICE_DEFAULT_RELATIVE_EXPIRY, meaning we'll |
| 1222 | + // need to refresh the static invoice using the reply path to the `OfferPaths` message if the |
| 1223 | + // offer expires later than that. |
| 1224 | + let static_invoice_relative_expiry = Duration::from_secs(core::cmp::min( |
| 1225 | + offer_relative_expiry.as_secs(), |
| 1226 | + STATIC_INVOICE_DEFAULT_RELATIVE_EXPIRY.as_secs(), |
| 1227 | + )); |
| 1228 | + |
| 1229 | + let static_invoice = self |
| 1230 | + .create_static_invoice_builder( |
| 1231 | + &offer, |
| 1232 | + offer_nonce, |
| 1233 | + Some(static_invoice_relative_expiry), |
| 1234 | + usable_channels, |
| 1235 | + peers, |
| 1236 | + ) |
| 1237 | + .and_then(|builder| builder.build_and_sign(secp_ctx)) |
| 1238 | + .map_err(|_e| ())?; // TODO: log error |
| 1239 | + |
| 1240 | + let reply_path_context = { |
| 1241 | + let nonce = Nonce::from_entropy_source(entropy); |
| 1242 | + let hmac = signer::hmac_for_static_invoice_persisted_context(nonce, expanded_key); |
| 1243 | + AsyncPaymentsContext::StaticInvoicePersisted { |
| 1244 | + offer, |
| 1245 | + offer_nonce, |
| 1246 | + offer_created_at, |
| 1247 | + update_static_invoice_path, |
| 1248 | + static_invoice_absolute_expiry: static_invoice |
| 1249 | + .created_at() |
| 1250 | + .saturating_add(static_invoice.relative_expiry()), |
| 1251 | + nonce, |
| 1252 | + hmac, |
| 1253 | + path_absolute_expiry: duration_since_epoch |
| 1254 | + .saturating_add(REPLY_PATH_RELATIVE_EXPIRY), |
| 1255 | + } |
| 1256 | + }; |
| 1257 | + |
| 1258 | + Ok((ServeStaticInvoice { invoice: static_invoice }, reply_path_context)) |
| 1259 | + } |
1103 | 1260 | } |
0 commit comments