X-Git-Url: http://git.bitcoin.ninja/index.cgi?a=blobdiff_plain;f=lightning%2Fsrc%2Fln%2Foutbound_payment.rs;h=3a49d7c2be6403da603369a63aa2876e4b5163f1;hb=6b49af1563ffdd2533900059922bd655c1f90a8d;hp=0ff7a33e9d88599f0b2ed9bba219abb595c2808c;hpb=ddde63ee12c8b10c0597c50940006d4cd53cfbc3;p=rust-lightning diff --git a/lightning/src/ln/outbound_payment.rs b/lightning/src/ln/outbound_payment.rs index 0ff7a33e..3a49d7c2 100644 --- a/lightning/src/ln/outbound_payment.rs +++ b/lightning/src/ln/outbound_payment.rs @@ -16,6 +16,7 @@ use bitcoin::secp256k1::{self, Secp256k1, SecretKey}; use crate::chain::keysinterface::{EntropySource, NodeSigner, Recipient}; use crate::ln::{PaymentHash, PaymentPreimage, PaymentSecret}; use crate::ln::channelmanager::{ChannelDetails, HTLCSource, IDEMPOTENCY_TIMEOUT_TICKS, MIN_HTLC_RELAY_HOLDING_CELL_MILLIS, PaymentId}; +use crate::ln::channelmanager::MIN_FINAL_CLTV_EXPIRY_DELTA as LDK_DEFAULT_MIN_FINAL_CLTV_EXPIRY_DELTA; use crate::ln::msgs::DecodeError; use crate::ln::onion_utils::HTLCFailReason; use crate::routing::router::{InFlightHtlcs, PaymentParameters, Route, RouteHop, RouteParameters, RoutePath, Router}; @@ -41,12 +42,13 @@ pub(crate) enum PendingOutboundPayment { session_privs: HashSet<[u8; 32]>, }, Retryable { - retry_strategy: Retry, + retry_strategy: Option, attempts: PaymentAttempts, - route_params: Option, + payment_params: Option, 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, @@ -82,15 +84,37 @@ impl PendingOutboundPayment { attempts.count += 1; } } + fn is_auto_retryable_now(&self) -> bool { + match self { + PendingOutboundPayment::Retryable { retry_strategy: Some(strategy), attempts, .. } => { + strategy.is_retryable_now(&attempts) + }, + _ => false, + } + } fn is_retryable_now(&self) -> bool { - if let PendingOutboundPayment::Retryable { retry_strategy, attempts, .. } = self { - return retry_strategy.is_retryable_now(&attempts) + match self { + PendingOutboundPayment::Retryable { retry_strategy: None, .. } => { + // We're handling retries manually, we can always retry. + true + }, + PendingOutboundPayment::Retryable { retry_strategy: Some(strategy), attempts, .. } => { + strategy.is_retryable_now(&attempts) + }, + _ => false, + } + } + fn payment_parameters(&mut self) -> Option<&mut PaymentParameters> { + match self { + PendingOutboundPayment::Retryable { payment_params: Some(ref mut params), .. } => { + Some(params) + }, + _ => None, } - false } pub fn insert_previously_failed_scid(&mut self, scid: u64) { - if let PendingOutboundPayment::Retryable { route_params: Some(params), .. } = self { - params.payment_params.previously_failed_channels.push(scid); + if let PendingOutboundPayment::Retryable { payment_params: Some(params), .. } = self { + params.previously_failed_channels.push(scid); } } pub(super) fn is_fulfilled(&self) -> bool { @@ -212,9 +236,9 @@ impl PendingOutboundPayment { pub enum Retry { /// Max number of attempts to retry payment. /// - /// Note that this is the number of *path* failures, not full payment retries. For multi-path - /// payments, if this is less than the total number of paths, we will never even retry all of the - /// payment's paths. + /// Each attempt may be multiple HTLCs along multiple paths if the router decides to split up a + /// retry, and may retry multiple failed HTLCs at once if they failed around the same time and + /// were retried along a route from a single call to [`Router::find_route`]. Attempts(usize), #[cfg(not(feature = "no-std"))] /// Time elapsed before abandoning retries for a payment. @@ -392,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 }) @@ -409,13 +433,39 @@ 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, Retry::Attempts(0), 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 = 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 @@ -430,7 +480,7 @@ impl OutboundPayments { None => 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, Retry::Attempts(0), 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), @@ -459,11 +509,19 @@ impl OutboundPayments { let mut outbounds = self.pending_outbound_payments.lock().unwrap(); let mut retry_id_route_params = None; for (pmt_id, pmt) in outbounds.iter_mut() { - if pmt.is_retryable_now() { - if let PendingOutboundPayment::Retryable { pending_amt_msat, total_msat, route_params: Some(params), .. } = pmt { + if pmt.is_auto_retryable_now() { + if let PendingOutboundPayment::Retryable { pending_amt_msat, total_msat, payment_params: Some(params), .. } = pmt { if pending_amt_msat < total_msat { - retry_id_route_params = Some((*pmt_id, params.clone())); - pmt.increment_attempts(); + retry_id_route_params = Some((*pmt_id, RouteParameters { + final_value_msat: *total_msat - *pending_amt_msat, + final_cltv_expiry_delta: + if let Some(delta) = params.final_cltv_expiry_delta { delta } + else { + debug_assert!(false, "We always set the final_cltv_expiry_delta when a path fails"); + LDK_DEFAULT_MIN_FINAL_CLTV_EXPIRY_DELTA.into() + }, + payment_params: params.clone(), + })); break } } @@ -480,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, @@ -508,36 +566,22 @@ 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, retry_strategy, Some(route_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) }; match res { Err(PaymentSendFailure::AllFailedResendSafe(_)) => { - let mut outbounds = self.pending_outbound_payments.lock().unwrap(); - if let Some(payment) = outbounds.get_mut(&payment_id) { - let retryable = payment.is_retryable_now(); - if retryable { - payment.increment_attempts(); - } else { return res } - } else { return res } - core::mem::drop(outbounds); let retry_res = self.pay_internal(payment_id, None, route_params, router, first_hops, inflight_htlcs, entropy_source, node_signer, best_block_height, logger, send_payment_along_path); log_info!(logger, "Result retrying payment id {}: {:?}", log_bytes!(payment_id.0), retry_res); + if let Err(PaymentSendFailure::ParameterError(APIError::APIMisuseError { err })) = &retry_res { + if err.starts_with("Retries exhausted ") { return res; } + } retry_res }, - Err(PaymentSendFailure::PartialFailure { failed_paths_retry: Some(retry), results, .. }) => { - let mut outbounds = self.pending_outbound_payments.lock().unwrap(); - if let Some(payment) = outbounds.get_mut(&payment_id) { - let retryable = payment.is_retryable_now(); - if retryable { - payment.increment_attempts(); - } else { return Err(PaymentSendFailure::PartialFailure { failed_paths_retry: Some(retry), results, payment_id }) } - } else { return Err(PaymentSendFailure::PartialFailure { failed_paths_retry: Some(retry), results, payment_id }) } - core::mem::drop(outbounds); - + Err(PaymentSendFailure::PartialFailure { failed_paths_retry: Some(retry), .. }) => { // Some paths were sent, even if we failed to send the full MPP value our recipient may // misbehave and claim the funds, at which point we have to consider the payment sent, so // return `Ok()` here, ignoring any retry errors. @@ -579,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 { @@ -593,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 { @@ -611,6 +655,12 @@ impl OutboundPayments { })); }, }; + if !payment.is_retryable_now() { + return Err(PaymentSendFailure::ParameterError(APIError::APIMisuseError { + err: format!("Retries exhausted for payment id {}", log_bytes!(payment_id.0)), + })) + } + payment.increment_attempts(); for (path, session_priv_bytes) in route.paths.iter().zip(onion_session_privs.iter()) { assert!(payment.insert(*session_priv_bytes, path)); } @@ -622,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( @@ -646,7 +696,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, Retry::Attempts(0), 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)), @@ -660,15 +710,15 @@ impl OutboundPayments { #[cfg(test)] pub(super) fn test_add_new_pending_payment( &self, payment_hash: PaymentHash, payment_secret: Option, payment_id: PaymentId, - route: &Route, retry_strategy: Retry, entropy_source: &ES, best_block_height: u32 + 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: Retry, route_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() { @@ -682,12 +732,13 @@ impl OutboundPayments { let payment = entry.insert(PendingOutboundPayment::Retryable { retry_strategy, attempts: PaymentAttempts::new(), - route_params, + payment_params, session_privs: HashSet::new(), pending_amt_msat: 0, pending_fee_msat: Some(0), payment_hash, payment_secret, + keysend_preimage, starting_block_height: best_block_height, total_msat: route.get_total_amount(), }); @@ -960,6 +1011,7 @@ impl OutboundPayments { let mut all_paths_failed = false; let mut full_failure_ev = None; let mut pending_retry_ev = None; + let mut retry = None; let attempts_remaining = if let hash_map::Entry::Occupied(mut payment) = outbounds.entry(*payment_id) { if !payment.get_mut().remove(&session_priv_bytes, Some(&path)) { log_trace!(logger, "Received duplicative fail for HTLC with payment_hash {}", log_bytes!(payment_hash.0)); @@ -969,10 +1021,37 @@ impl OutboundPayments { log_trace!(logger, "Received failure of HTLC with payment_hash {} after payment completion", log_bytes!(payment_hash.0)); return } - let is_retryable_now = payment.get().is_retryable_now(); + let is_retryable_now = payment.get().is_auto_retryable_now(); if let Some(scid) = short_channel_id { payment.get_mut().insert_previously_failed_scid(scid); } + + // We want to move towards only using the `PaymentParameters` in the outbound payments + // map. However, for backwards-compatibility, we still need to support passing the + // `PaymentParameters` data that was shoved in the HTLC (and given to us via + // `payment_params`) back to the user. + let path_last_hop = path.last().expect("Outbound payments must have had a valid path"); + if let Some(params) = payment.get_mut().payment_parameters() { + if params.final_cltv_expiry_delta.is_none() { + // This should be rare, but a user could provide None for the payment data, and + // we need it when we go to retry the payment, so fill it in. + params.final_cltv_expiry_delta = Some(path_last_hop.cltv_expiry_delta); + } + retry = Some(RouteParameters { + payment_params: params.clone(), + final_value_msat: path_last_hop.fee_msat, + final_cltv_expiry_delta: params.final_cltv_expiry_delta.unwrap(), + }); + } else if let Some(params) = payment_params { + retry = Some(RouteParameters { + payment_params: params.clone(), + final_value_msat: path_last_hop.fee_msat, + final_cltv_expiry_delta: + if let Some(delta) = params.final_cltv_expiry_delta { delta } + else { path_last_hop.cltv_expiry_delta }, + }); + } + if payment.get().remaining_parts() == 0 { all_paths_failed = true; if payment.get().abandoned() { @@ -989,16 +1068,6 @@ impl OutboundPayments { return }; core::mem::drop(outbounds); - let mut retry = if let Some(payment_params_data) = payment_params { - let path_last_hop = path.last().expect("Outbound payments must have had a valid path"); - Some(RouteParameters { - payment_params: payment_params_data.clone(), - final_value_msat: path_last_hop.fee_msat, - final_cltv_expiry_delta: - if let Some(delta) = payment_params_data.final_cltv_expiry_delta { delta } - else { path_last_hop.cltv_expiry_delta }, - }) - } else { None }; log_trace!(logger, "Failing outbound payment HTLC with payment_hash {}", log_bytes!(payment_hash.0)); let path_failure = { @@ -1110,13 +1179,14 @@ impl_writeable_tlv_based_enum_upgradable!(PendingOutboundPayment, (0, session_privs, required), (1, pending_fee_msat, option), (2, payment_hash, required), - (not_written, retry_strategy, (static_value, Retry::Attempts(0))), + (3, payment_params, option), (4, payment_secret, option), - (not_written, attempts, (static_value, PaymentAttempts::new())), + (5, keysend_preimage, option), (6, total_msat, required), - (not_written, route_params, (static_value, None)), (8, pending_amt_msat, required), (10, starting_block_height, required), + (not_written, retry_strategy, (static_value, None)), + (not_written, attempts, (static_value, PaymentAttempts::new())), }, (3, Abandoned) => { (0, session_privs, required), @@ -1206,9 +1276,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 }, Retry::Attempts(1), Some(route_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()