From 6b49af1563ffdd2533900059922bd655c1f90a8d Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Tue, 24 Jan 2023 12:15:40 -0500 Subject: [PATCH] Support spontaneous payment retries in ChannelManager --- lightning/src/ln/channelmanager.rs | 16 +++++++++- lightning/src/ln/outbound_payment.rs | 44 ++++++++++++++++++++++------ lightning/src/ln/payment_tests.rs | 17 +++++++++++ 3 files changed, 67 insertions(+), 10 deletions(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index ea17d2a61..1b8d03a87 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -2558,7 +2558,21 @@ where /// [`send_payment`]: Self::send_payment pub fn send_spontaneous_payment(&self, route: &Route, payment_preimage: Option, payment_id: PaymentId) -> Result { let best_block_height = self.best_block.read().unwrap().height(); - self.pending_outbound_payments.send_spontaneous_payment(route, payment_preimage, payment_id, &self.entropy_source, &self.node_signer, best_block_height, + self.pending_outbound_payments.send_spontaneous_payment_with_route( + route, payment_preimage, payment_id, &self.entropy_source, &self.node_signer, + best_block_height, + |path, payment_params, payment_hash, payment_secret, total_value, cur_height, payment_id, keysend_preimage, session_priv| + self.send_payment_along_path(path, payment_params, payment_hash, payment_secret, total_value, cur_height, payment_id, keysend_preimage, session_priv)) + } + + /// Similar to [`ChannelManager::send_spontaneous_payment`], but will automatically find a route + /// based on `route_params` and retry failed payment paths based on `retry_strategy`. + pub fn send_spontaneous_payment_with_retry(&self, payment_preimage: Option, payment_id: PaymentId, route_params: RouteParameters, retry_strategy: Retry) -> Result { + let best_block_height = self.best_block.read().unwrap().height(); + self.pending_outbound_payments.send_spontaneous_payment(payment_preimage, payment_id, + retry_strategy, route_params, &self.router, self.list_usable_channels(), + self.compute_inflight_htlcs(), &self.entropy_source, &self.node_signer, best_block_height, + &self.logger, |path, payment_params, payment_hash, payment_secret, total_value, cur_height, payment_id, keysend_preimage, session_priv| self.send_payment_along_path(path, payment_params, payment_hash, payment_secret, total_value, cur_height, payment_id, keysend_preimage, session_priv)) } diff --git a/lightning/src/ln/outbound_payment.rs b/lightning/src/ln/outbound_payment.rs index a73bc9162..3a49d7c2b 100644 --- a/lightning/src/ln/outbound_payment.rs +++ b/lightning/src/ln/outbound_payment.rs @@ -416,7 +416,7 @@ impl OutboundPayments { F: Fn(&Vec, &Option, &PaymentHash, &Option, u64, u32, PaymentId, &Option, [u8; 32]) -> Result<(), APIError>, { - self.pay_internal(payment_id, Some((payment_hash, payment_secret, retry_strategy)), + self.pay_internal(payment_id, Some((payment_hash, payment_secret, None, retry_strategy)), route_params, router, first_hops, inflight_htlcs, entropy_source, node_signer, best_block_height, logger, &send_payment_along_path) .map_err(|e| { self.remove_outbound_if_all_failed(payment_id, &e); e }) @@ -439,7 +439,33 @@ impl OutboundPayments { .map_err(|e| { self.remove_outbound_if_all_failed(payment_id, &e); e }) } - pub(super) fn send_spontaneous_payment( + pub(super) fn send_spontaneous_payment( + &self, payment_preimage: Option, payment_id: PaymentId, + retry_strategy: Retry, route_params: RouteParameters, router: &R, + first_hops: Vec, inflight_htlcs: InFlightHtlcs, entropy_source: &ES, + node_signer: &NS, best_block_height: u32, logger: &L, send_payment_along_path: F + ) -> Result + where + R::Target: Router, + ES::Target: EntropySource, + NS::Target: NodeSigner, + L::Target: Logger, + F: Fn(&Vec, &Option, &PaymentHash, &Option, u64, + u32, PaymentId, &Option, [u8; 32]) -> Result<(), APIError>, + { + let preimage = match payment_preimage { + Some(p) => p, + None => PaymentPreimage(entropy_source.get_secure_random_bytes()), + }; + let payment_hash = PaymentHash(Sha256::hash(&preimage.0).into_inner()); + self.pay_internal(payment_id, Some((payment_hash, &None, Some(preimage), retry_strategy)), + route_params, router, first_hops, inflight_htlcs, entropy_source, node_signer, + best_block_height, logger, &send_payment_along_path) + .map(|()| payment_hash) + .map_err(|e| { self.remove_outbound_if_all_failed(payment_id, &e); e }) + } + + pub(super) fn send_spontaneous_payment_with_route( &self, route: &Route, payment_preimage: Option, payment_id: PaymentId, entropy_source: &ES, node_signer: &NS, best_block_height: u32, send_payment_along_path: F ) -> Result @@ -512,7 +538,7 @@ impl OutboundPayments { fn pay_internal( &self, payment_id: PaymentId, - initial_send_info: Option<(PaymentHash, &Option, Retry)>, + initial_send_info: Option<(PaymentHash, &Option, Option, Retry)>, route_params: RouteParameters, router: &R, first_hops: Vec, inflight_htlcs: InFlightHtlcs, entropy_source: &ES, node_signer: &NS, best_block_height: u32, logger: &L, send_payment_along_path: &F, @@ -540,8 +566,8 @@ impl OutboundPayments { err: format!("Failed to find a route for payment {}: {:?}", log_bytes!(payment_id.0), e), // TODO: add APIError::RouteNotFound }))?; - let res = if let Some((payment_hash, payment_secret, retry_strategy)) = initial_send_info { - let onion_session_privs = self.add_new_pending_payment(payment_hash, *payment_secret, payment_id, None, &route, Some(retry_strategy), Some(route_params.payment_params.clone()), entropy_source, best_block_height)?; + let res = if let Some((payment_hash, payment_secret, keysend_preimage, retry_strategy)) = initial_send_info { + let onion_session_privs = self.add_new_pending_payment(payment_hash, *payment_secret, payment_id, keysend_preimage, &route, Some(retry_strategy), Some(route_params.payment_params.clone()), entropy_source, best_block_height)?; self.pay_route_internal(&route, payment_hash, payment_secret, None, payment_id, None, onion_session_privs, node_signer, best_block_height, send_payment_along_path) } else { self.retry_payment_with_route(&route, payment_id, entropy_source, node_signer, best_block_height, send_payment_along_path) @@ -597,13 +623,13 @@ impl OutboundPayments { onion_session_privs.push(entropy_source.get_secure_random_bytes()); } - let (total_msat, payment_hash, payment_secret) = { + let (total_msat, payment_hash, payment_secret, keysend_preimage) = { let mut outbounds = self.pending_outbound_payments.lock().unwrap(); match outbounds.get_mut(&payment_id) { Some(payment) => { let res = match payment { PendingOutboundPayment::Retryable { - total_msat, payment_hash, payment_secret, pending_amt_msat, .. + total_msat, payment_hash, keysend_preimage, payment_secret, pending_amt_msat, .. } => { let retry_amt_msat: u64 = route.paths.iter().map(|path| path.last().unwrap().fee_msat).sum(); if retry_amt_msat + *pending_amt_msat > *total_msat * (100 + RETRY_OVERFLOW_PERCENTAGE) / 100 { @@ -611,7 +637,7 @@ impl OutboundPayments { err: format!("retry_amt_msat of {} will put pending_amt_msat (currently: {}) more than 10% over total_payment_amt_msat of {}", retry_amt_msat, pending_amt_msat, total_msat).to_string() })) } - (*total_msat, *payment_hash, *payment_secret) + (*total_msat, *payment_hash, *payment_secret, *keysend_preimage) }, PendingOutboundPayment::Legacy { .. } => { return Err(PaymentSendFailure::ParameterError(APIError::APIMisuseError { @@ -646,7 +672,7 @@ impl OutboundPayments { })), } }; - self.pay_route_internal(route, payment_hash, &payment_secret, None, payment_id, Some(total_msat), onion_session_privs, node_signer, best_block_height, &send_payment_along_path) + self.pay_route_internal(route, payment_hash, &payment_secret, keysend_preimage, payment_id, Some(total_msat), onion_session_privs, node_signer, best_block_height, &send_payment_along_path) } pub(super) fn send_probe( diff --git a/lightning/src/ln/payment_tests.rs b/lightning/src/ln/payment_tests.rs index 68074f1b5..17061caaa 100644 --- a/lightning/src/ln/payment_tests.rs +++ b/lightning/src/ln/payment_tests.rs @@ -1586,6 +1586,7 @@ fn do_test_intercepted_payment(test: InterceptTest) { #[derive(PartialEq)] enum AutoRetry { Success, + Spontaneous, FailAttempts, FailTimeout, FailOnRestart, @@ -1594,6 +1595,7 @@ enum AutoRetry { #[test] fn automatic_retries() { do_automatic_retries(AutoRetry::Success); + do_automatic_retries(AutoRetry::Spontaneous); do_automatic_retries(AutoRetry::FailAttempts); do_automatic_retries(AutoRetry::FailTimeout); do_automatic_retries(AutoRetry::FailOnRestart); @@ -1692,6 +1694,21 @@ fn do_automatic_retries(test: AutoRetry) { assert_eq!(msg_events.len(), 1); pass_along_path(&nodes[0], &[&nodes[1], &nodes[2]], amt_msat, payment_hash, Some(payment_secret), msg_events.pop().unwrap(), true, None); claim_payment_along_route(&nodes[0], &[&[&nodes[1], &nodes[2]]], false, payment_preimage); + } else if test == AutoRetry::Spontaneous { + nodes[0].node.send_spontaneous_payment_with_retry(Some(payment_preimage), PaymentId(payment_hash.0), route_params, Retry::Attempts(1)).unwrap(); + pass_failed_attempt_with_retry_along_path!(channel_id_2, true); + + // Open a new channel with liquidity on the second hop so we can find a route for the retry + // attempt, since the initial second hop channel will be excluded from pathfinding + create_announced_chan_between_nodes(&nodes, 1, 2); + + // We retry payments in `process_pending_htlc_forwards` + nodes[0].node.process_pending_htlc_forwards(); + check_added_monitors!(nodes[0], 1); + let mut msg_events = nodes[0].node.get_and_clear_pending_msg_events(); + assert_eq!(msg_events.len(), 1); + pass_along_path(&nodes[0], &[&nodes[1], &nodes[2]], amt_msat, payment_hash, None, msg_events.pop().unwrap(), true, Some(payment_preimage)); + claim_payment_along_route(&nodes[0], &[&[&nodes[1], &nodes[2]]], false, payment_preimage); } else if test == AutoRetry::FailAttempts { // Ensure ChannelManager will not retry a payment if it has run out of payment attempts. nodes[0].node.send_payment_with_retry(payment_hash, &Some(payment_secret), PaymentId(payment_hash.0), route_params, Retry::Attempts(1)).unwrap(); -- 2.39.5