Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a separate PaymentSendFailure for idempotency violation #1826

Merged
merged 2 commits into from
Nov 16, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion fuzz/src/chanmon_consistency.rs
Original file line number Diff line number Diff line change
Expand Up @@ -283,12 +283,13 @@ fn check_payment_err(send_err: PaymentSendFailure) {
PaymentSendFailure::PathParameterError(per_path_results) => {
for res in per_path_results { if let Err(api_err) = res { check_api_err(api_err); } }
},
PaymentSendFailure::AllFailedRetrySafe(per_path_results) => {
PaymentSendFailure::AllFailedResendSafe(per_path_results) => {
for api_err in per_path_results { check_api_err(api_err); }
},
PaymentSendFailure::PartialFailure { results, .. } => {
for res in results { if let Err(api_err) = res { check_api_err(api_err); } }
},
PaymentSendFailure::DuplicatePayment => panic!(),
}
}

Expand Down
9 changes: 7 additions & 2 deletions lightning-invoice/src/payment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -543,7 +543,8 @@ where
Err(e) => match e {
PaymentSendFailure::ParameterError(_) => Err(e),
PaymentSendFailure::PathParameterError(_) => Err(e),
PaymentSendFailure::AllFailedRetrySafe(_) => {
PaymentSendFailure::DuplicatePayment => Err(e),
PaymentSendFailure::AllFailedResendSafe(_) => {
let mut payment_cache = self.payment_cache.lock().unwrap();
let payment_info = payment_cache.get_mut(&payment_hash).unwrap();
payment_info.attempts.count += 1;
Expand Down Expand Up @@ -655,9 +656,13 @@ where
log_trace!(self.logger, "Failed to retry for payment {} due to bogus route/payment data, not retrying.", log_bytes!(payment_hash.0));
Err(())
},
Err(PaymentSendFailure::AllFailedRetrySafe(_)) => {
Err(PaymentSendFailure::AllFailedResendSafe(_)) => {
self.retry_payment(payment_id, payment_hash, params)
},
Err(PaymentSendFailure::DuplicatePayment) => {
log_error!(self.logger, "Got a DuplicatePayment error when attempting to retry a payment, this shouldn't happen.");
Err(())
}
Err(PaymentSendFailure::PartialFailure { failed_paths_retry, results, .. }) => {
// If a `PartialFailure` error contains a result that is an `Ok()`, it means that
// part of our payment is retried. When we receive `MonitorUpdateInProgress`, it
Expand Down
40 changes: 27 additions & 13 deletions lightning/src/ln/channelmanager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1188,24 +1188,40 @@ impl ChannelDetails {
#[derive(Clone, Debug)]
pub enum PaymentSendFailure {
/// A parameter which was passed to send_payment was invalid, preventing us from attempting to
/// send the payment at all. No channel state has been changed or messages sent to peers, and
/// once you've changed the parameter at error, you can freely retry the payment in full.
/// send the payment at all.
///
/// You can freely resend the payment in full (with the parameter error fixed).
///
/// Because the payment failed outright, no payment tracking is done, you do not need to call
/// [`ChannelManager::abandon_payment`] and [`ChannelManager::retry_payment`] will *not* work
/// for this payment.
ParameterError(APIError),
/// A parameter in a single path which was passed to send_payment was invalid, preventing us
/// from attempting to send the payment at all. No channel state has been changed or messages
/// sent to peers, and once you've changed the parameter at error, you can freely retry the
/// payment in full.
/// from attempting to send the payment at all.
///
/// You can freely resend the payment in full (with the parameter error fixed).
///
/// The results here are ordered the same as the paths in the route object which was passed to
/// send_payment.
///
/// Because the payment failed outright, no payment tracking is done, you do not need to call
/// [`ChannelManager::abandon_payment`] and [`ChannelManager::retry_payment`] will *not* work
/// for this payment.
PathParameterError(Vec<Result<(), APIError>>),
/// All paths which were attempted failed to send, with no channel state change taking place.
/// You can freely retry the payment in full (though you probably want to do so over different
/// You can freely resend the payment in full (though you probably want to do so over different
TheBlueMatt marked this conversation as resolved.
Show resolved Hide resolved
/// paths than the ones selected).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// paths than the ones selected).
/// paths than the ones previously selected).

Copy link
Collaborator Author

@TheBlueMatt TheBlueMatt Nov 16, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would rather replace selected with attempted, but not sure its worth it - I don't think anyone is gonna be confused here.

///
/// [`ChannelManager::abandon_payment`] does *not* need to be called for this payment and
/// [`ChannelManager::retry_payment`] will *not* work for this payment.
AllFailedRetrySafe(Vec<APIError>),
/// Because the payment failed outright, no payment tracking is done, you do not need to call
/// [`ChannelManager::abandon_payment`] and [`ChannelManager::retry_payment`] will *not* work
/// for this payment.
AllFailedResendSafe(Vec<APIError>),
/// Indicates that a payment for the provided [`PaymentId`] is already in-flight and has not
/// yet completed (i.e. generated an [`Event::PaymentSent`]) or been abandoned (via
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// yet completed (i.e. generated an [`Event::PaymentSent`]) or been abandoned (via
/// yet completed (i.e. generated an [`Event::PaymentSent`]) or has been abandoned (via

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No - "not yet completed or been abandoned" has the opposite meaning from "not yet completed or has been abandoned" :).

/// [`ChannelManager::abandon_payment`]).
///
/// [`Event::PaymentSent`]: events::Event::PaymentSent
DuplicatePayment,
/// Some paths which were attempted failed to send, though possibly not all. At least some
/// paths have irrevocably committed to the HTLC and retrying the payment in full would result
/// in over-/re-payment.
Expand Down Expand Up @@ -2610,9 +2626,7 @@ impl<M: Deref, T: Deref, K: Deref, F: Deref, L: Deref> ChannelManager<M, T, K, F

let mut pending_outbounds = self.pending_outbound_payments.lock().unwrap();
match pending_outbounds.entry(payment_id) {
hash_map::Entry::Occupied(_) => Err(PaymentSendFailure::ParameterError(APIError::RouteError {
err: "Payment already in progress"
})),
hash_map::Entry::Occupied(_) => Err(PaymentSendFailure::DuplicatePayment),
hash_map::Entry::Vacant(entry) => {
let payment = entry.insert(PendingOutboundPayment::Retryable {
session_privs: HashSet::new(),
Expand Down Expand Up @@ -2726,7 +2740,7 @@ impl<M: Deref, T: Deref, K: Deref, F: Deref, L: Deref> ChannelManager<M, T, K, F
// `pending_outbound_payments` map, as the user isn't expected to `abandon_payment`.
let removed = self.pending_outbound_payments.lock().unwrap().remove(&payment_id).is_some();
debug_assert!(removed, "We should always have a pending payment to remove here");
Err(PaymentSendFailure::AllFailedRetrySafe(results.drain(..).map(|r| r.unwrap_err()).collect()))
Err(PaymentSendFailure::AllFailedResendSafe(results.drain(..).map(|r| r.unwrap_err()).collect()))
} else {
Ok(())
}
Expand Down
2 changes: 1 addition & 1 deletion lightning/src/ln/functional_test_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -587,7 +587,7 @@ macro_rules! get_local_commitment_txn {
macro_rules! unwrap_send_err {
($res: expr, $all_failed: expr, $type: pat, $check: expr) => {
match &$res {
&Err(PaymentSendFailure::AllFailedRetrySafe(ref fails)) if $all_failed => {
&Err(PaymentSendFailure::AllFailedResendSafe(ref fails)) if $all_failed => {
assert_eq!(fails.len(), 1);
match fails[0] {
$type => { $check },
Expand Down
2 changes: 1 addition & 1 deletion lightning/src/ln/functional_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1333,7 +1333,7 @@ fn test_basic_channel_reserve() {
let (route, our_payment_hash, _, our_payment_secret) = get_route_and_payment_hash!(nodes[0], nodes[1], max_can_send + 1);
let err = nodes[0].node.send_payment(&route, our_payment_hash, &Some(our_payment_secret), PaymentId(our_payment_hash.0)).err().unwrap();
match err {
PaymentSendFailure::AllFailedRetrySafe(ref fails) => {
PaymentSendFailure::AllFailedResendSafe(ref fails) => {
match &fails[0] {
&APIError::ChannelUnavailable{ref err} =>
assert!(regex::Regex::new(r"Cannot send value that would put our balance under counterparty-announced channel reserve value \(\d+\)").unwrap().is_match(err)),
Expand Down
8 changes: 4 additions & 4 deletions lightning/src/ln/payment_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1275,15 +1275,15 @@ fn claimed_send_payment_idempotent() {
// payment_id, it should be rejected.
let send_result = nodes[0].node.send_payment(&route, second_payment_hash, &Some(second_payment_secret), payment_id);
match send_result {
Err(PaymentSendFailure::ParameterError(APIError::RouteError { err: "Payment already in progress" })) => {},
Err(PaymentSendFailure::DuplicatePayment) => {},
_ => panic!("Unexpected send result: {:?}", send_result),
}

// Further, if we try to send a spontaneous payment with the same payment_id it should
// also be rejected.
let send_result = nodes[0].node.send_spontaneous_payment(&route, None, payment_id);
match send_result {
Err(PaymentSendFailure::ParameterError(APIError::RouteError { err: "Payment already in progress" })) => {},
Err(PaymentSendFailure::DuplicatePayment) => {},
_ => panic!("Unexpected send result: {:?}", send_result),
}
}
Expand Down Expand Up @@ -1347,15 +1347,15 @@ fn abandoned_send_payment_idempotent() {
// payment_id, it should be rejected.
let send_result = nodes[0].node.send_payment(&route, second_payment_hash, &Some(second_payment_secret), payment_id);
match send_result {
Err(PaymentSendFailure::ParameterError(APIError::RouteError { err: "Payment already in progress" })) => {},
Err(PaymentSendFailure::DuplicatePayment) => {},
_ => panic!("Unexpected send result: {:?}", send_result),
}

// Further, if we try to send a spontaneous payment with the same payment_id it should
// also be rejected.
let send_result = nodes[0].node.send_spontaneous_payment(&route, None, payment_id);
match send_result {
Err(PaymentSendFailure::ParameterError(APIError::RouteError { err: "Payment already in progress" })) => {},
Err(PaymentSendFailure::DuplicatePayment) => {},
_ => panic!("Unexpected send result: {:?}", send_result),
}
}
Expand Down