diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index f2ee0860e67..1b8d03a87a0 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)) } @@ -7367,6 +7381,7 @@ where session_privs: [session_priv_bytes].iter().map(|a| *a).collect(), payment_hash: htlc.payment_hash, payment_secret, + keysend_preimage: None, // only used for retries, and we'll never retry on startup pending_amt_msat: path_amt, pending_fee_msat: Some(path_fee), total_msat: path_amt, diff --git a/lightning/src/ln/outbound_payment.rs b/lightning/src/ln/outbound_payment.rs index f745363036b..a9ced49f647 100644 --- a/lightning/src/ln/outbound_payment.rs +++ b/lightning/src/ln/outbound_payment.rs @@ -48,6 +48,7 @@ pub(crate) enum PendingOutboundPayment { session_privs: HashSet<[u8; 32]>, payment_hash: PaymentHash, payment_secret: Option, + keysend_preimage: Option, pending_amt_msat: u64, /// Used to track the fee paid. Only present if the payment was serialized on 0.0.103+. pending_fee_msat: Option, @@ -415,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 }) @@ -432,13 +433,37 @@ impl OutboundPayments { F: Fn(&Vec, &Option, &PaymentHash, &Option, u64, u32, PaymentId, &Option, [u8; 32]) -> Result<(), APIError> { - let onion_session_privs = self.add_new_pending_payment(payment_hash, *payment_secret, payment_id, route, None, None, entropy_source, best_block_height)?; + let onion_session_privs = self.add_new_pending_payment(payment_hash, *payment_secret, payment_id, None, route, None, None, 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) .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 = payment_preimage + .unwrap_or_else(|| 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 @@ -448,12 +473,10 @@ impl OutboundPayments { 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 preimage = payment_preimage + .unwrap_or_else(|| PaymentPreimage(entropy_source.get_secure_random_bytes())); let payment_hash = PaymentHash(Sha256::hash(&preimage.0).into_inner()); - let onion_session_privs = self.add_new_pending_payment(payment_hash, None, payment_id, &route, None, None, entropy_source, best_block_height)?; + let onion_session_privs = self.add_new_pending_payment(payment_hash, None, payment_id, Some(preimage), &route, None, None, entropy_source, best_block_height)?; match self.pay_route_internal(route, payment_hash, &None, Some(preimage), payment_id, None, onion_session_privs, node_signer, best_block_height, &send_payment_along_path) { Ok(()) => Ok(payment_hash), @@ -511,7 +534,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, @@ -539,8 +562,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, &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) @@ -596,13 +619,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 { @@ -610,7 +633,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 { @@ -645,7 +668,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( @@ -669,7 +692,7 @@ impl OutboundPayments { } let route = Route { paths: vec![hops], payment_params: None }; - let onion_session_privs = self.add_new_pending_payment(payment_hash, None, payment_id, &route, None, None, entropy_source, best_block_height)?; + let onion_session_privs = self.add_new_pending_payment(payment_hash, None, payment_id, None, &route, None, None, entropy_source, best_block_height)?; match self.pay_route_internal(&route, payment_hash, &None, None, payment_id, None, onion_session_privs, node_signer, best_block_height, &send_payment_along_path) { Ok(()) => Ok((payment_hash, payment_id)), @@ -685,13 +708,13 @@ impl OutboundPayments { &self, payment_hash: PaymentHash, payment_secret: Option, payment_id: PaymentId, route: &Route, retry_strategy: Option, entropy_source: &ES, best_block_height: u32 ) -> Result, PaymentSendFailure> where ES::Target: EntropySource { - self.add_new_pending_payment(payment_hash, payment_secret, payment_id, route, retry_strategy, None, entropy_source, best_block_height) + self.add_new_pending_payment(payment_hash, payment_secret, payment_id, None, route, retry_strategy, None, entropy_source, best_block_height) } pub(super) fn add_new_pending_payment( &self, payment_hash: PaymentHash, payment_secret: Option, payment_id: PaymentId, - route: &Route, retry_strategy: Option, payment_params: Option, - entropy_source: &ES, best_block_height: u32 + keysend_preimage: Option, route: &Route, retry_strategy: Option, + payment_params: Option, entropy_source: &ES, best_block_height: u32 ) -> Result, PaymentSendFailure> where ES::Target: EntropySource { let mut onion_session_privs = Vec::with_capacity(route.paths.len()); for _ in 0..route.paths.len() { @@ -711,6 +734,7 @@ impl OutboundPayments { pending_fee_msat: Some(0), payment_hash, payment_secret, + keysend_preimage, starting_block_height: best_block_height, total_msat: route.get_total_amount(), }); @@ -1153,6 +1177,7 @@ impl_writeable_tlv_based_enum_upgradable!(PendingOutboundPayment, (2, payment_hash, required), (3, payment_params, option), (4, payment_secret, option), + (5, keysend_preimage, option), (6, total_msat, required), (8, pending_amt_msat, required), (10, starting_block_height, required), @@ -1247,9 +1272,9 @@ mod tests { Err(LightningError { err: String::new(), action: ErrorAction::IgnoreError })); let err = if on_retry { - outbound_payments.add_new_pending_payment(PaymentHash([0; 32]), None, PaymentId([0; 32]), - &Route { paths: vec![], payment_params: None }, Some(Retry::Attempts(1)), Some(route_params.payment_params.clone()), - &&keys_manager, 0).unwrap(); + outbound_payments.add_new_pending_payment(PaymentHash([0; 32]), None, PaymentId([0; 32]), None, + &Route { paths: vec![], payment_params: None }, Some(Retry::Attempts(1)), + Some(route_params.payment_params.clone()), &&keys_manager, 0).unwrap(); outbound_payments.pay_internal( PaymentId([0; 32]), None, route_params, &&router, vec![], InFlightHtlcs::new(), &&keys_manager, &&keys_manager, 0, &&logger, &|_, _, _, _, _, _, _, _, _| Ok(())).unwrap_err() diff --git a/lightning/src/ln/payment_tests.rs b/lightning/src/ln/payment_tests.rs index 68074f1b59b..17061caaa64 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();