rust-lightning icon indicating copy to clipboard operation
rust-lightning copied to clipboard

Introduce Retry InvoiceRequest Flow

Open shaavan opened this issue 1 year ago • 25 comments
trafficstars

resolves #2836

Description:

  • Add functionality to handle retrying the sending of invoice_request messages on new reply_paths that are still awaiting invoices.

Changes:

  1. Introduced invoice_request as an optional field in the PendingOutboundPayments::AwaitingInvoice variant to accommodate instances without invoice requests.
  2. Refactored logic from pay_for_offer to create invoice request messages into a separate function for reuse with retry message flow.
  3. Implemented the retry_tick_occurred function in ChannelManager to handle generating invoice request messages for AwaitingInvoice payments and enqueueing them.
  4. Added retry_tick_occurred to ln_background_processor with a timer duration of 5 seconds for timely retries without overwhelming the system with too many onion messages.

shaavan avatar Apr 22 '24 12:04 shaavan

Updated from pr3010.01 to pr3010.02 (diff):

Changes:

  1. Introduce Readable impl for InvoiceRequest
  2. Restructure commits.

shaavan avatar Apr 26 '24 17:04 shaavan

Updated from pr3010.02 to pr3010.03 (diff):

Updates:

  1. Rebase on main, to resolve merge conflicts.
  2. Fix ci

shaavan avatar Apr 27 '24 13:04 shaavan

Codecov Report

Attention: Patch coverage is 95.09202% with 8 lines in your changes missing coverage. Please review.

Project coverage is 89.64%. Comparing base (db905e8) to head (b1cd887). Report is 5 commits behind head on main.

Files with missing lines Patch % Lines
lightning/src/ln/channelmanager.rs 87.17% 5 Missing :warning:
lightning/src/offers/invoice_request.rs 50.00% 0 Missing and 2 partials :warning:
lightning/src/ln/outbound_payment.rs 98.07% 1 Missing :warning:
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3010      +/-   ##
==========================================
- Coverage   89.64%   89.64%   -0.01%     
==========================================
  Files         126      126              
  Lines      102251   102383     +132     
  Branches   102251   102383     +132     
==========================================
+ Hits        91666    91781     +115     
- Misses       7863     7874      +11     
- Partials     2722     2728       +6     

:umbrella: View full report in Codecov by Sentry.
:loudspeaker: Have feedback on the report? Share it here.

codecov-commenter avatar Apr 27 '24 13:04 codecov-commenter

Updated from pr3010.03 to pr3010.04 (diff):

Changes:

  1. Introduce retry_tick_occurred test.

shaavan avatar Apr 29 '24 09:04 shaavan

Updated from pr3010.04 to pr3010.05 (diff):

Addressed @TheBlueMatt comments

Updates:

  1. Removed the secondary timer in ln_background_processor.
  2. Moved the retry_tick_occurred to a private function, and renamed it to retry_invoice_request_messages.
  3. Decrease the FRESHNESS_TIMER duration, while introducing a call counter within timer_tick_occurred. This is done so that retry_invoice_request_messages can be called faster than the rest of the code without introducing a secondary timer.
  4. Update the test appropriately.

shaavan avatar May 03 '24 12:05 shaavan

Updated from pr3010.05 to pr3010.06 (diff):

Update:

  1. Rebase on main.
  2. To keep the code consistent with the main, now the create_invoice_reqeust_message, and retry_invoice_request_messages functions return Result<(), Bolt12SemanticError>

shaavan avatar May 03 '24 13:05 shaavan

Updated from pr3010.06 to pr3010.07 (diff):

Update:

  1. Replaced using unsafe with using internal Mutex variable for tracking call counts.

shaavan avatar May 03 '24 13:05 shaavan

Updated from pr3010.07 to pr3010.08 (diff):

Changes:

  1. Rebase on main, to solve merge conflicts.

shaavan avatar May 13 '24 16:05 shaavan

I don't buy that we need a new counter. There's a lot of things that hang on the timer rate, but this is a great opportunity to redefine the constants in seconds and multiply out a rate. Looking at all the things we do in the timer tick, as far as I can tell the only one that doesn't have a counter and a limit constant already is timer_check_closing_negotiation_process, and it should be easy to add there.

TheBlueMatt avatar May 13 '24 20:05 TheBlueMatt

Hi Matt! Thanks a lot for the suggestion!

I think this will definitely be a more maintainable way to handle this change than introducing a counter! Worked out your suggestion in a separate PR since they were quite different from this one: #3065 Let me know if the approach seems sound!

Thanks a lot!

shaavan avatar May 14 '24 13:05 shaavan

Updated from pr3010.08 to pr3010.09 (diff): Addressed @TheBlueMatt's comment

Update:

  1. Rebase on main
  2. Introduced a new approach for retrying invoice request messages.

Details:

  1. This approach triggers InvoiceRequest retries when any ChannelMessageHandler's handler is invoked by any message from any peer.
  2. It ensures that if an InvoiceRequest message fails to send on the first attempt due to network disconnection, it will retry as soon as we are back online and receive messages from our peer.
  3. It guarantees that we only retry once for a given PendingOutboundPayment.
  4. This approach allows retries independent of a sub-minute timer.

shaavan avatar Jun 10 '24 13:06 shaavan

Updated from pr3010.09 to pr3010.10 (diff): Addressed @TheBlueMatt comments

Changes:

  1. Update function name from retry_invoice_requests -> handle_message_received, so that it's more explicit about its calling behavior than specific functionality.
  2. Move the function call to do_handle_message_without_peer_lock so that it can be triggered when any message from any peer for any handler is received.
  3. Updated the code to generate awaiting_invoice_flag from pending_outbound_payments instead of serialising it.

shaavan avatar Jun 12 '24 11:06 shaavan

Updated from pr3010.10 to pr3010.11 (diff):

Changes:

  1. Rebase on main, to resolve merge conflicts.

shaavan avatar Jun 20 '24 13:06 shaavan

@TheBlueMatt A gentle ping! :)

shaavan avatar Jun 20 '24 13:06 shaavan

Updated from pr3010.11 to pr3010.12 (diff): Addressed @TheBlueMatt comments

Changes:

  1. Updated the handle_message_received documentation to be more generic, removing references to BOLT12.
  2. Removed the return Result type for handle_message_received as the result was unused in the codebase.
  3. Changed the TLV number of invoice_request to odd so that downgrade support is maintained.

shaavan avatar Jun 27 '24 13:06 shaavan

Updated from pr3010.12 to pr3010.13 (diff): Addressed @TheBlueMatt comments

Updates:

  1. Change the memory ordering from SeqCst to Acquire and Release.
  2. Moved the storing of awaiting_invoice_flag from pay_for_offer to add_new_awaiting_invoice.

shaavan avatar Jul 10 '24 12:07 shaavan

Updated from pr3010.13 to pr3010.14 (diff):

Updates:

  1. Rebase on main, to fix ci.
  2. Update get_invoice_request_awaiting_invoice to return vector of (payment_id, invoice_request). This is done so that channelmanager's handle_message_received can recreate the message context using the payment_id when retrying the invoice_request message.

shaavan avatar Jul 10 '24 14:07 shaavan

Updated from pr3010.14 to pr3010.15 (diff): Addressed @jkczyz comments

Updates:

  1. Refactor the Outbound Payment Constructor to make the lock, and flag private members.
  2. Moved the create_invoice_request_message below pay_for_offer for more logical ordering.
  3. Introduce a log message in handle_message_received, and replace return with continue to not quit the function on the first failure.
  4. Update the test. Introduce the with_retry as a separate test instead of modifying the current one.

shaavan avatar Jul 11 '24 15:07 shaavan

Updated from pr3010.15 to pr3010.16 (diff): Addressed @jkczyz comments

  1. Calculate reply_path prior to locking the pending_offers_message mutex in handle_message_received.

shaavan avatar Jul 11 '24 15:07 shaavan

Updated from pr3010.16 to pr3010.17 (diff): Addressed @jkczyz comments

Updates:

  1. Renames function and variable name to be more apt to their usage.
  2. Update test to remove improve comment naming and remove redundant checks.
  3. Used new_hash_map() instead of HashMap::new().

shaavan avatar Jul 12 '24 09:07 shaavan

Updated from pr3010.17 to pr3010.18 (diff): Addressed @jkczyz comments

Changes:

  1. Squashed commits together.
  2. Some code cleanups.

shaavan avatar Jul 25 '24 09:07 shaavan

Updated from pr3010.18 to pr3010.19 (diff): Addressed @jkczyz comments

Changes:

With the merge of #3139, the Invoice Request message now needs both the Invoice Request as well as the original context to authenticate the corresponding Invoice. So post-rebase, this leads to some significant changes to this PR.

  1. Updated the AwaitingInvoice to contain both the InvoiceRequest and context.
  2. Update release_invoice_request_awaiting_invoice to return the Vec<(OffersContext, InvoiceRequest)>, instead of Vec<(PaymentId, InvoiceRequest)>.
Range Diff
 git range-diff 6035c83a1d797133b723a084abe6a55bfb4b9f62..0f1c4920e157bf4c67ac8f02b32e25a8ee5f855f a76ec06d23801191f12688245584e12057bd3b10..45ecb2a46e4b982877a8eb791ffb6bb768544c3e
1:  2da46cb0 ! 1:  115fab41 Add InvoiceRequest field and awaiting_invoice_flag
    @@ Metadata
     Author: shaavan <[email protected]>
     
      ## Commit message ##
    -    Add InvoiceRequest field and awaiting_invoice_flag
    +    Add InvoiceRequest and Context Fields in AwaitingInvoice
     
    -    - Introduce InvoiceRequest as a field in AwaitingInvoice.
    -    - Use this field to recreate InvoiceRequest messages for
    -      retrying payments still awaiting an invoice.
    -    - Introduce awaiting_invoice_flag to track if there are any
    -      PendingOutboundPayments::AwaitingInvoice with the corresponding
    -      InvoiceRequest.
    -    - This flag helps determine the state of pending_outbound_payments
    -      without explicitly locking its mutex.
    +    - Introduced `InvoiceRequest` and `context` fields in the `AwaitingInvoice`
    +      to enable recreation of the `InvoiceRequest` message for retries if the
    +      corresponding invoice is not received in time.
    +    - Added `awaiting_invoice` flag to track pending outbound payments with
    +      invoice requests without locking the `pending_outbound_payment` mutex.
     
      ## lightning/src/ln/channelmanager.rs ##
     @@ lightning/src/ln/channelmanager.rs: where
    @@ lightning/src/ln/channelmanager.rs: where
                        forward_htlcs: Mutex::new(new_hash_map()),
                        decode_update_add_htlcs: Mutex::new(new_hash_map()),
                        claimable_payments: Mutex::new(ClaimablePayments { claimable_payments: new_hash_map(), pending_claiming_payments: new_hash_map() }),
    +@@ lightning/src/ln/channelmanager.rs: macro_rules! create_refund_builder { ($self: ident, $builder: ty) => {
    + 
    +           let nonce = Nonce::from_entropy_source(entropy);
    +           let context = OffersContext::OutboundPayment { payment_id, nonce };
    +-          let path = $self.create_blinded_paths_using_absolute_expiry(context, Some(absolute_expiry))
    ++          let path = $self.create_blinded_paths_using_absolute_expiry(context.clone(), Some(absolute_expiry))
    +                   .and_then(|paths| paths.into_iter().next().ok_or(()))
    +                   .map_err(|_| Bolt12SemanticError::MissingPaths)?;
    + 
     @@ lightning/src/ln/channelmanager.rs: macro_rules! create_refund_builder { ($self: ident, $builder: ty) => {
                let expiration = StaleExpiration::AbsoluteTimeout(absolute_expiry);
                $self.pending_outbound_payments
                        .add_new_awaiting_invoice(
     -                          payment_id, expiration, retry_strategy, max_total_routing_fee_msat,
    -+                          payment_id, expiration, retry_strategy, max_total_routing_fee_msat, None
    ++                          payment_id, expiration, retry_strategy, max_total_routing_fee_msat, None, context
                        )
                        .map_err(|_| Bolt12SemanticError::DuplicatePaymentId)?;
      
    +@@ lightning/src/ln/channelmanager.rs: where
    +           let invoice_request = builder.build_and_sign()?;
    + 
    +           let context = OffersContext::OutboundPayment { payment_id, nonce };
    +-          let reply_paths = self.create_blinded_paths(context)
    ++          let reply_paths = self.create_blinded_paths(context.clone())
    +                   .map_err(|_| Bolt12SemanticError::MissingPaths)?;
    + 
    +           let _persistence_guard = PersistenceNotifierGuard::notify_on_drop(self);
     @@ lightning/src/ln/channelmanager.rs: where
                let expiration = StaleExpiration::TimerTicks(1);
                self.pending_outbound_payments
                        .add_new_awaiting_invoice(
     -                          payment_id, expiration, retry_strategy, max_total_routing_fee_msat
     +                          payment_id, expiration, retry_strategy, max_total_routing_fee_msat,
    -+                          Some(invoice_request.clone())
    ++                          Some(invoice_request.clone()), context,
                        )
                        .map_err(|_| Bolt12SemanticError::DuplicatePaymentId)?;
      
    @@ lightning/src/ln/channelmanager.rs: where
                // each `ChannelMonitorUpdate` in `in_flight_monitor_updates`. After doing so, we have to
     
      ## lightning/src/ln/outbound_payment.rs ##
    +@@ lightning/src/ln/outbound_payment.rs: use bitcoin::hashes::Hash;
    + use bitcoin::hashes::sha256::Hash as Sha256;
    + use bitcoin::secp256k1::{self, Secp256k1, SecretKey};
    + 
    ++use crate::blinded_path::message::OffersContext;
    + use crate::blinded_path::{IntroductionNode, NodeIdLookUp};
    + use crate::blinded_path::payment::advance_path_by_one;
    + use crate::events::{self, PaymentFailureReason};
     @@ lightning/src/ln/outbound_payment.rs: use crate::ln::channelmanager::{EventCompletionAction, HTLCSource, PaymentId};
      use crate::ln::onion_utils;
      use crate::ln::onion_utils::{DecodedOnionFailure, HTLCFailReason};
    @@ lightning/src/ln/outbound_payment.rs: pub(crate) enum PendingOutboundPayment {
                retry_strategy: Retry,
                max_total_routing_fee_msat: Option<u64>,
     +          invoice_request: Option<InvoiceRequest>,
    ++          context: OffersContext,
        },
        InvoiceReceived {
                payment_hash: PaymentHash,
    @@ lightning/src/ln/outbound_payment.rs: impl OutboundPayments {
        pub(super) fn add_new_awaiting_invoice(
                &self, payment_id: PaymentId, expiration: StaleExpiration, retry_strategy: Retry,
     -          max_total_routing_fee_msat: Option<u64>
    -+          max_total_routing_fee_msat: Option<u64>, invoice_request: Option<InvoiceRequest>
    ++          max_total_routing_fee_msat: Option<u64>, invoice_request: Option<InvoiceRequest>,
    ++          context: OffersContext,
        ) -> Result<(), ()> {
                let mut pending_outbounds = self.pending_outbound_payments.lock().unwrap();
                match pending_outbounds.entry(payment_id) {
    @@ lightning/src/ln/outbound_payment.rs: impl OutboundPayments {
                                        retry_strategy,
                                        max_total_routing_fee_msat,
     +                                  invoice_request,
    ++                                  context,
                                });
     +                          self.awaiting_invoice.store(true, Ordering::Release);
      
    @@ lightning/src/ln/outbound_payment.rs: impl OutboundPayments {
                self.pending_outbound_payments.lock().unwrap().clear()
        }
     +
    -+  pub fn release_invoice_request_awaiting_invoice(&self) -> Vec<(PaymentId, InvoiceRequest)> {
    ++  pub fn release_invoice_request_awaiting_invoice(&self) -> Vec<(OffersContext, InvoiceRequest)> {
     +          if !self.awaiting_invoice.load(Ordering::Acquire) {
     +                  return vec![];
     +          }
    -+          
    ++
     +          let mut pending_outbound_payments = self.pending_outbound_payments.lock().unwrap();
     +          let invoice_requests = pending_outbound_payments.iter_mut()
    -+                  .filter_map(|(payment_id, payment)| match payment {
    -+                          PendingOutboundPayment::AwaitingInvoice { invoice_request, .. } => {
    -+                                  invoice_request.take().map(|req| (*payment_id, req))
    ++                  .filter_map(|(_, payment)| match payment {
    ++                          PendingOutboundPayment::AwaitingInvoice { invoice_request, context, ..} => {
    ++                                  invoice_request.take().map(|req| (context.clone(), req))
     +                          }
     +                          _ => None,
     +                  })
     +                  .collect();
    -+  
    ++
     +          self.awaiting_invoice.store(false, Ordering::Release);
     +          invoice_requests
     +  }
    @@ lightning/src/ln/outbound_payment.rs: impl_writeable_tlv_based_enum_upgradable!(
                (2, retry_strategy, required),
                (4, max_total_routing_fee_msat, option),
     +          (5, invoice_request, option),
    ++          (6, context, required),
        },
        (7, InvoiceReceived) => {
                (0, payment_hash, required),
     @@ lightning/src/ln/outbound_payment.rs: mod tests {
    + 
    +   use core::time::Duration;
    + 
    ++  use crate::blinded_path::message::OffersContext;
    +   use crate::blinded_path::EmptyNodeIdLookUp;
    +   use crate::events::{Event, PathFailure, PaymentFailureReason};
    +   use crate::ln::types::PaymentHash;
    +@@ lightning/src/ln/outbound_payment.rs: mod tests {
        use crate::routing::router::{InFlightHtlcs, Path, PaymentParameters, Route, RouteHop, RouteParameters};
        use crate::sync::{Arc, Mutex, RwLock};
        use crate::util::errors::APIError;
    @@ lightning/src/ln/outbound_payment.rs: mod tests {
                assert!(
                        outbound_payments.add_new_awaiting_invoice(
     -                          payment_id, expiration, Retry::Attempts(0), None
    -+                          payment_id, expiration, Retry::Attempts(0), None, None
    ++                          payment_id, expiration, Retry::Attempts(0), None, None, OffersContext::Unknown {}
                        ).is_ok()
                );
                assert!(outbound_payments.has_pending_payments());
    @@ lightning/src/ln/outbound_payment.rs: mod tests {
                assert!(
                        outbound_payments.add_new_awaiting_invoice(
     -                          payment_id, expiration, Retry::Attempts(0), None
    -+                          payment_id, expiration, Retry::Attempts(0), None, None
    ++                          payment_id, expiration, Retry::Attempts(0), None, None, OffersContext::Unknown {}
                        ).is_ok()
                );
                assert!(outbound_payments.has_pending_payments());
    @@ lightning/src/ln/outbound_payment.rs: mod tests {
                assert!(
                        outbound_payments.add_new_awaiting_invoice(
     -                          payment_id, expiration, Retry::Attempts(0), None
    -+                          payment_id, expiration, Retry::Attempts(0), None, None
    ++                          payment_id, expiration, Retry::Attempts(0), None, None, OffersContext::Unknown {}
                        ).is_err()
                );
        }
    @@ lightning/src/ln/outbound_payment.rs: mod tests {
                assert!(
                        outbound_payments.add_new_awaiting_invoice(
     -                          payment_id, expiration, Retry::Attempts(0), None
    -+                          payment_id, expiration, Retry::Attempts(0), None, None
    ++                          payment_id, expiration, Retry::Attempts(0), None, None, OffersContext::Unknown {}
                        ).is_ok()
                );
                assert!(outbound_payments.has_pending_payments());
    @@ lightning/src/ln/outbound_payment.rs: mod tests {
                assert!(
                        outbound_payments.add_new_awaiting_invoice(
     -                          payment_id, expiration, Retry::Attempts(0), None
    -+                          payment_id, expiration, Retry::Attempts(0), None, None
    ++                          payment_id, expiration, Retry::Attempts(0), None, None, OffersContext::Unknown {}
                        ).is_ok()
                );
                assert!(outbound_payments.has_pending_payments());
    @@ lightning/src/ln/outbound_payment.rs: mod tests {
                assert!(
                        outbound_payments.add_new_awaiting_invoice(
     -                          payment_id, expiration, Retry::Attempts(0), None
    -+                          payment_id, expiration, Retry::Attempts(0), None, None
    ++                          payment_id, expiration, Retry::Attempts(0), None, None, OffersContext::Unknown {}
                        ).is_err()
                );
        }
    @@ lightning/src/ln/outbound_payment.rs: mod tests {
                assert!(
                        outbound_payments.add_new_awaiting_invoice(
     -                          payment_id, expiration, Retry::Attempts(0), None
    -+                          payment_id, expiration, Retry::Attempts(0), None, None
    ++                          payment_id, expiration, Retry::Attempts(0), None, None, OffersContext::Unknown {}
                        ).is_ok()
                );
                assert!(outbound_payments.has_pending_payments());
    @@ lightning/src/ln/outbound_payment.rs: mod tests {
                assert!(
                        outbound_payments.add_new_awaiting_invoice(
     -                          payment_id, expiration, Retry::Attempts(0), None
    -+                          payment_id, expiration, Retry::Attempts(0), None, None
    ++                          payment_id, expiration, Retry::Attempts(0), None, None, OffersContext::Unknown {}
                        ).is_ok()
                );
                assert!(outbound_payments.has_pending_payments());
    @@ lightning/src/ln/outbound_payment.rs: mod tests {
                        outbound_payments.add_new_awaiting_invoice(
                                payment_id, expiration, Retry::Attempts(0),
     -                          Some(invoice.amount_msats() / 100 + 50_000)
    -+                          Some(invoice.amount_msats() / 100 + 50_000), None
    ++                          Some(invoice.amount_msats() / 100 + 50_000), None, OffersContext::Unknown {}
                        ).is_ok()
                );
                assert!(outbound_payments.has_pending_payments());
    @@ lightning/src/ln/outbound_payment.rs: mod tests {
                assert!(
                        outbound_payments.add_new_awaiting_invoice(
     -                          payment_id, expiration, Retry::Attempts(0), Some(1234)
    -+                          payment_id, expiration, Retry::Attempts(0), Some(1234), None
    ++                          payment_id, expiration, Retry::Attempts(0), Some(1234), None, OffersContext::Unknown {}
                        ).is_ok()
                );
                assert!(outbound_payments.has_pending_payments());
2:  522b2a6e < -:  -------- Add create_invoice_request_messages function
-:  -------- > 2:  261268bd Introduce enqueue_invoice_request Function
3:  ce851f1c ! 3:  dba6d91b Add handle_message_received function in ChannelMessageHandler
    @@ Metadata
     Author: shaavan <[email protected]>
     
      ## Commit message ##
    -    Add handle_message_received function in ChannelMessageHandler
    +    Introduce handle_message_received in ChannelMessageHandler
     
    -    - Handle behavior triggered when any message from any peer is received.
    -    - Manage retry mechanism for InvoiceRequest messages in ChannelManager
    -      related to PendingOutboundPayments.
    -    - Retrieve the InvoiceRequest, use a new reply path, and enqueue the
    -      messages for retry.
    -    - Ensure retries of InvoiceRequest messages if initial attempts fail
    -      due to connection loss, retrying when back online and receiving messages
    -      from peers.
    +    - Introduce the `handle_message_received` function to manage the
    +      behavior when a message is received from any peer.
    +    - This function is used within `ChannelManager` to retry `InvoiceRequest`
    +      messages if we haven't received the corresponding invoice yet.
    +    - This change makes the offer communication more robust against sudden
    +      connection drops where the initial attempt to send the message might
    +      have failed.
     
      ## lightning-net-tokio/src/lib.rs ##
     @@ lightning-net-tokio/src/lib.rs: mod tests {
    @@ lightning/src/ln/channelmanager.rs: where
        }
     +
     +  fn handle_message_received(&self) {
    -+          let invoice_requests = self.pending_outbound_payments.release_invoice_request_awaiting_invoice();
    -+          if invoice_requests.is_empty() {
    -+                  return;
    -+          }
    -+
    -+          let invoice_requests_reply_paths = invoice_requests
    -+                  .into_iter()
    -+                  .filter_map(|(payment_id, invoice_request)| {
    -+                          let context = OffersContext::OutboundPayment { payment_id };
    -+                          match self.create_blinded_path(context) {
    -+                                  Ok(reply_path) => Some((invoice_request, reply_path)),
    ++          for (context, invoice_request) in self
    ++                  .pending_outbound_payments
    ++                  .release_invoice_request_awaiting_invoice()
    ++          {
    ++                  match self.create_blinded_paths(context) {
    ++                          Ok(reply_paths) => match self.enqueue_invoice_request(invoice_request, reply_paths) {
    ++                                  Ok(_) => {}
     +                                  Err(_) => {
    -+                                          log_info!(
    -+                                                  self.logger,
    -+                                                  "Retry failed for invoice request with payment id: {}. \
    ++                                          log_info!(self.logger, "Retry failed for an invoice request.");
    ++                                  }
    ++                          },
    ++                          Err(_) => {
    ++                                  log_info!(self.logger,
    ++                                          "Retry failed for an invoice request. \
     +                                                  Reason: router could not find a blinded path to include as the reply path",
    -+                                                  payment_id
    -+                                          );
    -+                                          None
    -+                                  },
    ++                                  );
     +                          }
    -+                  });
    -+
    -+          let mut pending_offers_messages = self.pending_offers_messages.lock().unwrap();
    -+          for (invoice_request, reply_path) in invoice_requests_reply_paths {
    -+                  if let Ok(messages) = self.create_invoice_request_messages(
    -+                          invoice_request, reply_path
    -+                  ) {
    -+                          pending_offers_messages.extend(messages);
     +                  }
     +          }
     +  }
4:  0f1c4920 ! 4:  45ecb2a4 Introduce handle_message_received test
    @@ lightning/src/ln/offers_tests.rs: fn creates_and_pays_for_offer_using_one_hop_bl
     +          None => {},
     +  };
     +
    -+  let invoice = extract_invoice(bob, &onion_message);
    ++  let (invoice, _) = extract_invoice(bob, &onion_message);
     +  assert_eq!(invoice.amount_msats(), 10_000_000);
     +  assert_ne!(invoice.signing_pubkey(), alice_id);
     +  assert!(!invoice.payment_paths().is_empty());

shaavan avatar Jul 26 '24 12:07 shaavan

Updated from pr3010.19 to pr3010.20 (diff): Addressed @jkczyz comments

Changes:

  1. Include Nonce as the field in AwaitingInvoice instead of Context, since Nonce is sufficient for context recreation, and safeguards against potentially passing the wrong context to AwaitingInvoice.

shaavan avatar Aug 01 '24 15:08 shaavan

Updated from pr3010.20 to pr3010.21 (diff): Addressed @jkczyz comments

Changes:

  1. Removed redundant clone.
  2. Make nonce and Option, instead of required to support downgrading.

shaavan avatar Aug 13 '24 15:08 shaavan

Updated from pr3010.21 to pr3010.22 (diff): Addressed @jkczyz comments

Changes:

  1. Fixed tlv number of nonce, to follow the "it's okay to be odd" rule.
  2. Fixed release pending onion message to avoid unnecessary panic.

shaavan avatar Aug 14 '24 08:08 shaavan

Updated from pr3010.22 to pr3010.23 (diff):

Changes:

  1. Rebase on main to resolve merge conflicts.
  2. Squash fixup commit
  3. No changes to the approach in this version.
Range-Diff:
1:  115fab41 ! 1:  8abd02fd Add InvoiceRequest and Context Fields in AwaitingInvoice
    @@ Metadata
     Author: shaavan <[email protected]>
     
      ## Commit message ##
    -    Add InvoiceRequest and Context Fields in AwaitingInvoice
    +    Add InvoiceRequest and Nonce Fields in AwaitingInvoice
     
    -    - Introduced `InvoiceRequest` and `context` fields in the `AwaitingInvoice`
    +    - Introduced `InvoiceRequest` and `nonce` fields in the `AwaitingInvoice`
           to enable recreation of the `InvoiceRequest` message for retries if the
           corresponding invoice is not received in time.
         - Added `awaiting_invoice` flag to track pending outbound payments with
    @@ lightning/src/ln/channelmanager.rs: where
     @@ lightning/src/ln/channelmanager.rs: macro_rules! create_refund_builder { ($self: ident, $builder: ty) => {
      
      		let nonce = Nonce::from_entropy_source(entropy);
    - 		let context = OffersContext::OutboundPayment { payment_id, nonce };
    + 		let context = OffersContext::OutboundPayment { payment_id, nonce, hmac: None };
     -		let path = $self.create_blinded_paths_using_absolute_expiry(context, Some(absolute_expiry))
     +		let path = $self.create_blinded_paths_using_absolute_expiry(context.clone(), Some(absolute_expiry))
      			.and_then(|paths| paths.into_iter().next().ok_or(()))
    @@ lightning/src/ln/channelmanager.rs: macro_rules! create_refund_builder { ($self:
      		$self.pending_outbound_payments
      			.add_new_awaiting_invoice(
     -				payment_id, expiration, retry_strategy, max_total_routing_fee_msat,
    -+				payment_id, expiration, retry_strategy, max_total_routing_fee_msat, None, context
    ++				payment_id, expiration, retry_strategy, max_total_routing_fee_msat, None, nonce
      			)
      			.map_err(|_| Bolt12SemanticError::DuplicatePaymentId)?;
      
    -@@ lightning/src/ln/channelmanager.rs: where
    - 		let invoice_request = builder.build_and_sign()?;
    - 
    - 		let context = OffersContext::OutboundPayment { payment_id, nonce };
    --		let reply_paths = self.create_blinded_paths(context)
    -+		let reply_paths = self.create_blinded_paths(context.clone())
    - 			.map_err(|_| Bolt12SemanticError::MissingPaths)?;
    - 
    - 		let _persistence_guard = PersistenceNotifierGuard::notify_on_drop(self);
     @@ lightning/src/ln/channelmanager.rs: where
      		let expiration = StaleExpiration::TimerTicks(1);
      		self.pending_outbound_payments
      			.add_new_awaiting_invoice(
     -				payment_id, expiration, retry_strategy, max_total_routing_fee_msat
     +				payment_id, expiration, retry_strategy, max_total_routing_fee_msat,
    -+				Some(invoice_request.clone()), context,
    ++				Some(invoice_request.clone()), nonce,
      			)
      			.map_err(|_| Bolt12SemanticError::DuplicatePaymentId)?;
      
    @@ lightning/src/ln/outbound_payment.rs: use bitcoin::hashes::Hash;
      
     +use crate::blinded_path::message::OffersContext;
      use crate::blinded_path::{IntroductionNode, NodeIdLookUp};
    - use crate::blinded_path::payment::advance_path_by_one;
      use crate::events::{self, PaymentFailureReason};
    -@@ lightning/src/ln/outbound_payment.rs: use crate::ln::channelmanager::{EventCompletionAction, HTLCSource, PaymentId};
    + use crate::ln::types::{PaymentHash, PaymentPreimage, PaymentSecret};
    +@@ lightning/src/ln/outbound_payment.rs: use crate::ln::features::Bolt12InvoiceFeatures;
      use crate::ln::onion_utils;
      use crate::ln::onion_utils::{DecodedOnionFailure, HTLCFailReason};
      use crate::offers::invoice::Bolt12Invoice;
     +use crate::offers::invoice_request::InvoiceRequest;
    ++use crate::offers::nonce::Nonce;
      use crate::routing::router::{BlindedTail, InFlightHtlcs, Path, PaymentParameters, Route, RouteParameters, Router};
      use crate::sign::{EntropySource, NodeSigner, Recipient};
      use crate::util::errors::APIError;
    @@ lightning/src/ln/outbound_payment.rs: pub(crate) enum PendingOutboundPayment {
      		retry_strategy: Retry,
      		max_total_routing_fee_msat: Option<u64>,
     +		invoice_request: Option<InvoiceRequest>,
    -+		context: OffersContext,
    ++		nonce: Option<Nonce>,
      	},
      	InvoiceReceived {
      		payment_hash: PaymentHash,
    @@ lightning/src/ln/outbound_payment.rs: impl OutboundPayments {
      		&self, payment_id: PaymentId, expiration: StaleExpiration, retry_strategy: Retry,
     -		max_total_routing_fee_msat: Option<u64>
     +		max_total_routing_fee_msat: Option<u64>, invoice_request: Option<InvoiceRequest>,
    -+		context: OffersContext,
    ++		nonce: Nonce,
      	) -> Result<(), ()> {
      		let mut pending_outbounds = self.pending_outbound_payments.lock().unwrap();
      		match pending_outbounds.entry(payment_id) {
    @@ lightning/src/ln/outbound_payment.rs: impl OutboundPayments {
      					retry_strategy,
      					max_total_routing_fee_msat,
     +					invoice_request,
    -+					context,
    ++					nonce: Some(nonce),
      				});
     +				self.awaiting_invoice.store(true, Ordering::Release);
      
    @@ lightning/src/ln/outbound_payment.rs: impl OutboundPayments {
     +		}
     +
     +		let mut pending_outbound_payments = self.pending_outbound_payments.lock().unwrap();
    -+		let invoice_requests = pending_outbound_payments.iter_mut()
    -+			.filter_map(|(_, payment)| match payment {
    -+				PendingOutboundPayment::AwaitingInvoice { invoice_request, context, ..} => {
    -+					invoice_request.take().map(|req| (context.clone(), req))
    ++		let invoice_requests = pending_outbound_payments
    ++			.iter_mut()
    ++			.filter_map(|(payment_id, payment)| {
    ++				if let PendingOutboundPayment::AwaitingInvoice {
    ++					invoice_request, nonce: Some(nonce), ..
    ++				} = payment {
    ++					let context = OffersContext::OutboundPayment {
    ++						payment_id: *payment_id,
    ++						nonce: *nonce,
    ++						// TODO: hmac to be incorporated.
    ++						hmac: None,
    ++					};
    ++					invoice_request.take().map(|req| (context, req))
    ++				} else {
    ++					None
     +				}
    -+				_ => None,
     +			})
     +			.collect();
     +
    @@ lightning/src/ln/outbound_payment.rs: impl_writeable_tlv_based_enum_upgradable!(
      		(2, retry_strategy, required),
      		(4, max_total_routing_fee_msat, option),
     +		(5, invoice_request, option),
    -+		(6, context, required),
    ++		(7, nonce, option),
      	},
      	(7, InvoiceReceived) => {
      		(0, payment_hash, required),
     @@ lightning/src/ln/outbound_payment.rs: mod tests {
    - 
    - 	use core::time::Duration;
    - 
    -+	use crate::blinded_path::message::OffersContext;
    - 	use crate::blinded_path::EmptyNodeIdLookUp;
    - 	use crate::events::{Event, PathFailure, PaymentFailureReason};
    - 	use crate::ln::types::PaymentHash;
    -@@ lightning/src/ln/outbound_payment.rs: mod tests {
    + 	use crate::ln::outbound_payment::{Bolt12PaymentError, OutboundPayments, Retry, RetryableSendFailure, StaleExpiration};
    + 	#[cfg(feature = "std")]
    + 	use crate::offers::invoice::DEFAULT_RELATIVE_EXPIRY;
    ++	use crate::offers::nonce::Nonce;
    + 	use crate::offers::offer::OfferBuilder;
    + 	use crate::offers::test_utils::*;
    + 	use crate::routing::gossip::NetworkGraph;
      	use crate::routing::router::{InFlightHtlcs, Path, PaymentParameters, Route, RouteHop, RouteParameters};
      	use crate::sync::{Arc, Mutex, RwLock};
      	use crate::util::errors::APIError;
    @@ lightning/src/ln/outbound_payment.rs: mod tests {
      		assert!(
      			outbound_payments.add_new_awaiting_invoice(
     -				payment_id, expiration, Retry::Attempts(0), None
    -+				payment_id, expiration, Retry::Attempts(0), None, None, OffersContext::Unknown {}
    ++				payment_id, expiration, Retry::Attempts(0), None, None, Nonce::try_from(&[0u8; 16][..]).unwrap()
      			).is_ok()
      		);
      		assert!(outbound_payments.has_pending_payments());
    @@ lightning/src/ln/outbound_payment.rs: mod tests {
      		assert!(
      			outbound_payments.add_new_awaiting_invoice(
     -				payment_id, expiration, Retry::Attempts(0), None
    -+				payment_id, expiration, Retry::Attempts(0), None, None, OffersContext::Unknown {}
    ++				payment_id, expiration, Retry::Attempts(0), None, None, Nonce::try_from(&[0u8; 16][..]).unwrap()
      			).is_ok()
      		);
      		assert!(outbound_payments.has_pending_payments());
    @@ lightning/src/ln/outbound_payment.rs: mod tests {
      		assert!(
      			outbound_payments.add_new_awaiting_invoice(
     -				payment_id, expiration, Retry::Attempts(0), None
    -+				payment_id, expiration, Retry::Attempts(0), None, None, OffersContext::Unknown {}
    ++				payment_id, expiration, Retry::Attempts(0), None, None, Nonce::try_from(&[0u8; 16][..]).unwrap()
      			).is_err()
      		);
      	}
    @@ lightning/src/ln/outbound_payment.rs: mod tests {
      		assert!(
      			outbound_payments.add_new_awaiting_invoice(
     -				payment_id, expiration, Retry::Attempts(0), None
    -+				payment_id, expiration, Retry::Attempts(0), None, None, OffersContext::Unknown {}
    ++				payment_id, expiration, Retry::Attempts(0), None, None, Nonce::try_from(&[0u8; 16][..]).unwrap()
      			).is_ok()
      		);
      		assert!(outbound_payments.has_pending_payments());
    @@ lightning/src/ln/outbound_payment.rs: mod tests {
      		assert!(
      			outbound_payments.add_new_awaiting_invoice(
     -				payment_id, expiration, Retry::Attempts(0), None
    -+				payment_id, expiration, Retry::Attempts(0), None, None, OffersContext::Unknown {}
    ++				payment_id, expiration, Retry::Attempts(0), None, None, Nonce::try_from(&[0u8; 16][..]).unwrap()
      			).is_ok()
      		);
      		assert!(outbound_payments.has_pending_payments());
    @@ lightning/src/ln/outbound_payment.rs: mod tests {
      		assert!(
      			outbound_payments.add_new_awaiting_invoice(
     -				payment_id, expiration, Retry::Attempts(0), None
    -+				payment_id, expiration, Retry::Attempts(0), None, None, OffersContext::Unknown {}
    ++				payment_id, expiration, Retry::Attempts(0), None, None, Nonce::try_from(&[0u8; 16][..]).unwrap()
      			).is_err()
      		);
      	}
    @@ lightning/src/ln/outbound_payment.rs: mod tests {
      		assert!(
      			outbound_payments.add_new_awaiting_invoice(
     -				payment_id, expiration, Retry::Attempts(0), None
    -+				payment_id, expiration, Retry::Attempts(0), None, None, OffersContext::Unknown {}
    ++				payment_id, expiration, Retry::Attempts(0), None, None, Nonce::try_from(&[0u8; 16][..]).unwrap()
      			).is_ok()
      		);
      		assert!(outbound_payments.has_pending_payments());
    @@ lightning/src/ln/outbound_payment.rs: mod tests {
      		assert!(
      			outbound_payments.add_new_awaiting_invoice(
     -				payment_id, expiration, Retry::Attempts(0), None
    -+				payment_id, expiration, Retry::Attempts(0), None, None, OffersContext::Unknown {}
    ++				payment_id, expiration, Retry::Attempts(0), None, None, Nonce::try_from(&[0u8; 16][..]).unwrap()
      			).is_ok()
      		);
      		assert!(outbound_payments.has_pending_payments());
    @@ lightning/src/ln/outbound_payment.rs: mod tests {
      			outbound_payments.add_new_awaiting_invoice(
      				payment_id, expiration, Retry::Attempts(0),
     -				Some(invoice.amount_msats() / 100 + 50_000)
    -+				Some(invoice.amount_msats() / 100 + 50_000), None, OffersContext::Unknown {}
    ++				Some(invoice.amount_msats() / 100 + 50_000), None, Nonce::try_from(&[0u8; 16][..]).unwrap()
      			).is_ok()
      		);
      		assert!(outbound_payments.has_pending_payments());
    @@ lightning/src/ln/outbound_payment.rs: mod tests {
      		assert!(
      			outbound_payments.add_new_awaiting_invoice(
     -				payment_id, expiration, Retry::Attempts(0), Some(1234)
    -+				payment_id, expiration, Retry::Attempts(0), Some(1234), None, OffersContext::Unknown {}
    ++				payment_id, expiration, Retry::Attempts(0), Some(1234), None, Nonce::try_from(&[0u8; 16][..]).unwrap()
      			).is_ok()
      		);
      		assert!(outbound_payments.has_pending_payments());
2:  bf1d03b9 < -:  -------- f: Include Nonce in AwaitingInvoice for context recreation
3:  9b1e056c ! 2:  1cc365ce Introduce enqueue_invoice_request Function
    @@ Commit message
      ## lightning/src/ln/channelmanager.rs ##
     @@ lightning/src/ln/channelmanager.rs: use crate::ln::outbound_payment::{OutboundPayments, PaymentAttempts, PendingOutb
      use crate::ln::wire::Encode;
    - use crate::offers::invoice::{BlindedPayInfo, Bolt12Invoice, DEFAULT_RELATIVE_EXPIRY, DerivedSigningPubkey, ExplicitSigningPubkey, InvoiceBuilder, UnsignedBolt12Invoice};
    + use crate::offers::invoice::{Bolt12Invoice, DEFAULT_RELATIVE_EXPIRY, DerivedSigningPubkey, ExplicitSigningPubkey, InvoiceBuilder, UnsignedBolt12Invoice};
      use crate::offers::invoice_error::InvoiceError;
     -use crate::offers::invoice_request::{DerivedPayerId, InvoiceRequestBuilder};
     +use crate::offers::invoice_request::{DerivedPayerId, InvoiceRequest, InvoiceRequestBuilder};
    @@ lightning/src/ln/channelmanager.rs: where
     +	fn enqueue_invoice_request(
     +		&self,
     +		invoice_request: InvoiceRequest,
    -+		reply_paths: Vec<BlindedPath>,
    ++		reply_paths: Vec<BlindedMessagePath>,
     +	) -> Result<(), Bolt12SemanticError> {
      		let mut pending_offers_messages = self.pending_offers_messages.lock().unwrap();
     -		if !offer.paths().is_empty() {
4:  e9fbabc9 = 3:  8a17319a Introduce handle_message_received in ChannelMessageHandler
5:  656dd95d ! 4:  6a9e9134 Introduce handle_message_received test
    @@ lightning/src/ln/offers_tests.rs: fn creates_and_pays_for_offer_using_one_hop_bl
     +	assert_ne!(offer.signing_pubkey(), Some(alice_id));
     +	assert!(!offer.paths().is_empty());
     +	for path in offer.paths() {
    -+		assert_eq!(path.introduction_node, IntroductionNode::NodeId(alice_id));
    ++		assert_eq!(path.introduction_node(), &IntroductionNode::NodeId(alice_id));
     +	}
     +	let payment_id = PaymentId([1; 32]);
     +	bob.node.pay_for_offer(&offer, None, None, None, payment_id, Retry::Attempts(0), None).unwrap();
    @@ lightning/src/ln/offers_tests.rs: fn creates_and_pays_for_offer_using_one_hop_bl
     +	});
     +	assert_eq!(invoice_request.amount_msats(), None);
     +	assert_ne!(invoice_request.payer_id(), bob_id);
    -+	assert_eq!(reply_path.introduction_node, IntroductionNode::NodeId(bob_id));
    ++	assert_eq!(reply_path.introduction_node(), &IntroductionNode::NodeId(bob_id));
     +	let onion_message = alice.onion_messenger.next_onion_message_for_peer(bob_id).unwrap();
     +	bob.onion_messenger.handle_onion_message(&alice_id, &onion_message);
     +
    @@ lightning/src/ln/offers_tests.rs: fn creates_and_pays_for_offer_using_one_hop_bl
     +	assert_eq!(invoice.amount_msats(), 10_000_000);
     +	assert_ne!(invoice.signing_pubkey(), alice_id);
     +	assert!(!invoice.payment_paths().is_empty());
    -+	for (_, path) in invoice.payment_paths() {
    -+		assert_eq!(path.introduction_node, IntroductionNode::NodeId(alice_id));
    ++	for path in invoice.payment_paths() {
    ++		assert_eq!(path.introduction_node(), &IntroductionNode::NodeId(alice_id));
     +	}
     +	route_bolt12_payment(bob, &[alice], &invoice);
     +	expect_recent_payment!(bob, RecentPaymentDetails::Pending, payment_id);

shaavan avatar Aug 20 '24 07:08 shaavan

Updated from pr3010.23 to pr3010.24 (diff): Addressed @vincenzopalazzo, and @jkczyz comments

Changes:

  1. Introduce RetryableInvoiceRequest struct, which contains all the data required to recreate InvoiceRequest Messages.
  2. Upgrade logging level from info to warn for invoice request retry failure case, and improve logging.

shaavan avatar Aug 21 '24 09:08 shaavan

Updated from pr3010.24 to pr3010.25 (diff): Addressed @jkczyz comments

Changes:

  1. Remove a redundant clone.
  2. Rename handle_message_received -> message_received, and update docs to align with the function's purpose.
  3. Move the message_received call to handle_message so that it is called before the "message handling" for any kind of message received.
  4. Simplify a check in test.

shaavan avatar Aug 22 '24 12:08 shaavan

Updated from pr3010.25 to pr3010.26 (diff): Addressed @TheBlueMatt comments

  1. Update add_new_awaiting_invoice to only update the awaiting_invoice flag if there is some retryable_invoice_request.
  2. Update message_received docs.

shaavan avatar Sep 09 '24 12:09 shaavan

Updated from pr3010.26 to pr3010.27 (diff): Addressed @jkczyz comment

Changes:

  1. Use a if conditional, instead of map for a more idiomatic construct for retryable_invoice_request.

shaavan avatar Sep 10 '24 09:09 shaavan