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

Support auto-retrying keysend payments in ChannelManager #2002

Merged
Show file tree
Hide file tree
Changes from 2 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
17 changes: 16 additions & 1 deletion lightning/src/ln/channelmanager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2558,7 +2558,21 @@ where
/// [`send_payment`]: Self::send_payment
pub fn send_spontaneous_payment(&self, route: &Route, payment_preimage: Option<PaymentPreimage>, payment_id: PaymentId) -> Result<PaymentHash, PaymentSendFailure> {
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<PaymentPreimage>, payment_id: PaymentId, route_params: RouteParameters, retry_strategy: Retry) -> Result<PaymentHash, PaymentSendFailure> {
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))
}
Expand Down Expand Up @@ -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,
Expand Down
65 changes: 47 additions & 18 deletions lightning/src/ln/outbound_payment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ pub(crate) enum PendingOutboundPayment {
session_privs: HashSet<[u8; 32]>,
payment_hash: PaymentHash,
payment_secret: Option<PaymentSecret>,
keysend_preimage: Option<PaymentPreimage>,
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<u64>,
Expand Down Expand Up @@ -415,7 +416,7 @@ impl OutboundPayments {
F: Fn(&Vec<RouteHop>, &Option<PaymentParameters>, &PaymentHash, &Option<PaymentSecret>, u64,
u32, PaymentId, &Option<PaymentPreimage>, [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 })
Expand All @@ -432,13 +433,39 @@ impl OutboundPayments {
F: Fn(&Vec<RouteHop>, &Option<PaymentParameters>, &PaymentHash, &Option<PaymentSecret>, u64,
u32, PaymentId, &Option<PaymentPreimage>, [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<ES: Deref, NS: Deref, F>(
pub(super) fn send_spontaneous_payment<R: Deref, ES: Deref, NS: Deref, F, L: Deref>(
&self, payment_preimage: Option<PaymentPreimage>, payment_id: PaymentId,
retry_strategy: Retry, route_params: RouteParameters, router: &R,
first_hops: Vec<ChannelDetails>, inflight_htlcs: InFlightHtlcs, entropy_source: &ES,
node_signer: &NS, best_block_height: u32, logger: &L, send_payment_along_path: F
Comment on lines +445 to +446
Copy link
Contributor

Choose a reason for hiding this comment

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

Unrelated this PR, but may want to consider using the Payer abstraction here and throughout in the future to reduce the number of parameters. Could possibly simplify the tests by using a custom function for send_payment_along_path instead of needing to setup ChannelManager... or maybe not yet if retries are still triggered in ChannelManager?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Retries upon payment failure are triggered in ChannelManager but they could always be triggered manually for unit testing. Using Payer to reduce the number of params sgtm 👍

) -> Result<PaymentHash, PaymentSendFailure>
where
R::Target: Router,
ES::Target: EntropySource,
NS::Target: NodeSigner,
L::Target: Logger,
F: Fn(&Vec<RouteHop>, &Option<PaymentParameters>, &PaymentHash, &Option<PaymentSecret>, u64,
u32, PaymentId, &Option<PaymentPreimage>, [u8; 32]) -> Result<(), APIError>,
{
let preimage = match payment_preimage {
Some(p) => p,
None => PaymentPreimage(entropy_source.get_secure_random_bytes()),
};
valentinewallace marked this conversation as resolved.
Show resolved Hide resolved
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<ES: Deref, NS: Deref, F>(
&self, route: &Route, payment_preimage: Option<PaymentPreimage>, payment_id: PaymentId,
entropy_source: &ES, node_signer: &NS, best_block_height: u32, send_payment_along_path: F
) -> Result<PaymentHash, PaymentSendFailure>
Expand All @@ -453,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, 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),
Expand Down Expand Up @@ -511,7 +538,7 @@ impl OutboundPayments {

fn pay_internal<R: Deref, NS: Deref, ES: Deref, F, L: Deref>(
&self, payment_id: PaymentId,
initial_send_info: Option<(PaymentHash, &Option<PaymentSecret>, Retry)>,
initial_send_info: Option<(PaymentHash, &Option<PaymentSecret>, Option<PaymentPreimage>, Retry)>,
route_params: RouteParameters, router: &R, first_hops: Vec<ChannelDetails>,
inflight_htlcs: InFlightHtlcs, entropy_source: &ES, node_signer: &NS, best_block_height: u32,
logger: &L, send_payment_along_path: &F,
Expand Down Expand Up @@ -539,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, &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)
Expand Down Expand Up @@ -596,21 +623,21 @@ 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 {
return Err(PaymentSendFailure::ParameterError(APIError::APIMisuseError {
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)
Comment on lines +622 to +636
Copy link
Contributor

Choose a reason for hiding this comment

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

Possible follow-up opportunity to improve code cleanliness here. The first pattern is the only one that doesn't return early. So some of the indentations can be reduced by getting rid of the res local variable (and thus moving up the match) and also the outer (total_msat, payment_hash, payment_secret, keysend_preimage) variables, though you'd have to drop the lock before calling pay_route_internal.

},
PendingOutboundPayment::Legacy { .. } => {
return Err(PaymentSendFailure::ParameterError(APIError::APIMisuseError {
Expand Down Expand Up @@ -645,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<ES: Deref, NS: Deref, F>(
Expand All @@ -669,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, 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)),
Expand All @@ -685,13 +712,13 @@ impl OutboundPayments {
&self, payment_hash: PaymentHash, payment_secret: Option<PaymentSecret>, payment_id: PaymentId,
route: &Route, retry_strategy: Option<Retry>, entropy_source: &ES, best_block_height: u32
) -> Result<Vec<[u8; 32]>, 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<ES: Deref>(
&self, payment_hash: PaymentHash, payment_secret: Option<PaymentSecret>, payment_id: PaymentId,
route: &Route, retry_strategy: Option<Retry>, payment_params: Option<PaymentParameters>,
entropy_source: &ES, best_block_height: u32
keysend_preimage: Option<PaymentPreimage>, route: &Route, retry_strategy: Option<Retry>,
payment_params: Option<PaymentParameters>, entropy_source: &ES, best_block_height: u32
) -> Result<Vec<[u8; 32]>, PaymentSendFailure> where ES::Target: EntropySource {
let mut onion_session_privs = Vec::with_capacity(route.paths.len());
for _ in 0..route.paths.len() {
Expand All @@ -711,6 +738,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(),
});
Expand Down Expand Up @@ -1153,6 +1181,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),
Expand Down Expand Up @@ -1247,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 }, 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()
Expand Down
17 changes: 17 additions & 0 deletions lightning/src/ln/payment_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1586,6 +1586,7 @@ fn do_test_intercepted_payment(test: InterceptTest) {
#[derive(PartialEq)]
enum AutoRetry {
Success,
Spontaneous,
FailAttempts,
FailTimeout,
FailOnRestart,
Expand All @@ -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);
Expand Down Expand Up @@ -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();
Expand Down