Skip to content

Reload pending outbound payments from ChannelMonitors on startup - #1104

Merged
TheBlueMatt merged 11 commits into
lightningdevkit:mainfrom
TheBlueMatt:2021-10-payment-id-in-monitors
Oct 22, 2021
Merged

Reload pending outbound payments from ChannelMonitors on startup#1104
TheBlueMatt merged 11 commits into
lightningdevkit:mainfrom
TheBlueMatt:2021-10-payment-id-in-monitors

Conversation

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

When we add payment retries, we need some kind of on-disk consistency story. I think a compelling one is described, roughly at [1] - make the retryer persist first, before calling into the ChannelManager, then when reloading we either have a pending payment there or its pending nowhere. Once the send_payment method returns, we'll either have a pending payment persisted in both the retryer and a ChannelMonitor, or in neither. Note that we may not have anything in a ChannelManager until much later, preventing us from retrying the payment if we crash.

In such a case, we'll force-close the relevant channel (its ChannelMonitor has run ahead of the manager), but the actual HTLC will still be pending, just not present in ChannelManager at all. Ideally, we'd then be able to retry it over a whole new path.

This PR addresses this situation by having ChannelManager examine the available ChannelMonitors at startup, rebuilding pending payment entries on the basis of what's in its ChannelMonitors.

Note that we aren't particularly concerned with the payment resolution end of things - if ChannelManager thinks a payment is still pending when it has been fully resolved, we'll eventually consider it sent and generate a fresh PaymentSent when the ChannelMonitor resolves the current state.

This should fix#1102, but generally needs lots more tests (its not tested at all, aside from a hacky thing at the end to compare the loaded-from-disk and the rebuilt set of payments, which passes most tests).

[1] #1059 (comment)

@TheBlueMattTheBlueMatt added this to the 0.0.102 milestone Oct 4, 2021
@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch from e97930f to a0fe042CompareOctober 5, 2021 05:59
@codecov

codecovBot commented Oct 5, 2021

Copy link
Copy Markdown

Codecov Report

Merging #1104 (7af5d12) into main (107c6c7) will increase coverage by 0.06%.
The diff coverage is 96.03%.

Impacted file tree graph

@@ Coverage Diff @@## main #1104 +/- ##
==========================================
+ Coverage 90.40% 90.47% +0.06% 
==========================================
Files 68 68 Lines 34796 35176 +380 ==========================================
+ Hits 31458 31826 +368 - Misses 3338 3350 +12 
Impacted FilesCoverage Δ
lightning/src/util/ser.rs89.02% <80.00%> (-0.12%)⬇️
lightning/src/ln/channelmanager.rs83.86% <90.06%> (+0.34%)⬆️
lightning/src/ln/channel.rs88.37% <96.22%> (+0.05%)⬆️
lightning/src/chain/channelmonitor.rs91.11% <97.56%> (+0.17%)⬆️
lightning/src/ln/payment_tests.rs99.09% <99.26%> (+0.25%)⬆️
lightning/src/ln/functional_test_utils.rs95.09% <100.00%> (+<0.01%)⬆️
lightning/src/ln/functional_tests.rs97.37% <100.00%> (-0.04%)⬇️
lightning/src/ln/onion_route_tests.rs96.60% <100.00%> (ø)
lightning/src/ln/features.rs99.43% <0.00%> (+<0.01%)⬆️
... and 2 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 107c6c7...7af5d12. Read the comment docs.

@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

This is now based on #1108, which should go first.

@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

Depends on #1109 as well now.

@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch from a0fe042 to f821c93CompareOctober 5, 2021 23:26
@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

Now includes at least a naive test of payment data reloading, which works 🎉

@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch 4 times, most recently from 6e60c4f to a7cea3dCompareOctober 11, 2021 00:53
@TheBlueMattTheBlueMatt modified the milestones: 0.0.102, 0.0.103Oct 15, 2021
@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch from a7cea3d to 7d23578CompareOctober 20, 2021 01:25
@TheBlueMatt
TheBlueMatt marked this pull request as ready for review October 20, 2021 01:25
@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

Rebased on upstream!

@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch from 7d23578 to df8b291CompareOctober 20, 2021 02:54
@jkczyz
jkczyz self-requested a review October 20, 2021 17:56

@valentinewallacevalentinewallace left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Mostly wondering how we handle failures that were previously fulfilled

Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/chain/channelmonitor.rs
Comment threadlightning/src/chain/channelmonitor.rs Outdated
@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch from df8b291 to 42a854cCompareOctober 20, 2021 21:13

@jkczyzjkczyz left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Sorry about any confusion around terminology. I don't have as a firm of grasp as I'd like to around some of this.

Comment on lines +344 to +363
pub(super) struct RAAUpdates {
pub commitment_update: Option<msgs::CommitmentUpdate>,
pub to_forward_htlcs: Vec<(PendingHTLCInfo, u64)>,
pub failed_htlcs: Vec<(HTLCSource, PaymentHash, HTLCFailReason)>,
pub finalized_claim_htlcs: Vec<HTLCSource>,
pub monitor_update: ChannelMonitorUpdate,
pub holding_cell_failed_htlcs: Vec<(HTLCSource, PaymentHash)>,
}

/// The return value of `monitor_updating_resotred`
pub(super) struct MonitorRestoreUpdates {
pub raa: Option<msgs::RevokeAndACK>,
pub commitment_update: Option<msgs::CommitmentUpdate>,
pub order: RAACommitmentOrder,
pub forwards: Vec<(PendingHTLCInfo, u64)>,
pub failures: Vec<(HTLCSource, PaymentHash, HTLCFailReason)>,
pub finalized_claims: Vec<HTLCSource>,
pub funding_broadcastable: Option<Transaction>,
pub funding_locked: Option<msgs::FundingLocked>,
}

@jkczyzjkczyzOct 20, 2021

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Where it makes sense, could we use consistent field names between these two structs?

Comment threadlightning/src/ln/channel.rs Outdated
}
self.monitor_pending_forwards.append(&mut to_forward_infos);
self.monitor_pending_failures.append(&mut revoked_htlcs);
self.monitor_pending_finalized_fulfills.append(&mut finalized_claim_htlcs);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

In terms of terminology, is there a difference between a "fulfill" and a "claim"? Does it depend on the context?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

It mostly just depends on when the code was written :p

commitment_update: Some(commitment_update),
finalized_claim_htlcs,
to_forward_htlcs: to_forward_infos,
failed_htlcs: revoked_htlcs,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Similar question wrt "failed" and "revoked". Just want to make sure I understand if it is context dependent.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

Same answer, there really isn't.

Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment on lines +2018 to +2023
if let hash_map::Entry::Occupied(payment) = &payment_entry {
if !payment.get().is_retryable() {
return Err(APIError::RouteError {
err: "Payment already completed"
});
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It's unclear to me why this shouldn't be if payment.get().is_complete() { as indicated by the error message. Do we want to hit this for legacy payments, too?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

Hmm, yes, we probably should hit this for legacy payments. I mean its unreachable for legacy payments, I believe (retry_payment refuses to call send_payment_internal at all), but nothing wrong with an extra check here.

Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment on lines +419 to +444
/// When a pending payment completes, we continue tracking it until all pendings HTLCs have
/// been resolved. This ensures we don't look up pending payments in ChannelMonitors on restart
/// and add a pending payment that was already completed.
Fulfilled {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Terminology question related to my earlier comment: is there a difference between "completes" and "fulfilled" as it pertains to the first sentence in the docs and the variant name?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

No, I just missed the docs in the rename - once we get one HTLC fulfillment we consider the payment fulfilled.

@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

Pushed a fix for the lockorder comment, leaving other stuff until we settle on naming.

@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch from 2aef42c to ca6ae4dCompareOctober 21, 2021 00:00
Comment threadlightning/src/chain/channelmonitor.rs Outdated
Comment threadlightning/src/chain/channelmonitor.rs Outdated
Comment threadlightning/src/chain/channelmonitor.rs
Comment threadlightning/src/chain/channelmonitor.rs Outdated
Comment threadlightning/src/chain/channelmonitor.rs Outdated
Comment threadlightning/src/chain/channelmonitor.rs Outdated
@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch from ca6ae4d to a050236CompareOctober 21, 2021 19:15
@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

Ok, I believe I've addressed all feedback and renamed things to be more clear.

Comment threadlightning/src/chain/channelmonitor.rs
Comment threadlightning/src/chain/channelmonitor.rs Outdated
Comment threadlightning/src/chain/channelmonitor.rs Outdated
Comment threadlightning/src/ln/channel.rs Outdated
Comment threadlightning/src/ln/channel.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch from a050236 to f47acdbCompareOctober 21, 2021 20:41
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/chain/channelmonitor.rs Outdated
// We only rebuild the pending payments map if we were most recently serialized by
// 0.0.102+
for (_, monitor) in args.channel_monitors {
if by_id.get(&monitor.get_funding_txo().0.to_channel_id()).is_none() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hmm, I don't see why we don't load pending outbound payments for open channels as well. Couldn't we still have a situation where the ChannelManager doesn't get persisted after initiating an outbound payment, but the ChannelMonitor still knows about it?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

The channel monitor knowing about a payment that the ChannelManager does not implies by definition that the ChannelMonitor has some newer state than the ChannelManager. It's possible I missed some edge case but it feels pretty straightforward?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Ah, right 🤦‍♀️

Fwiw, when I add this diff, it gets hit a fair amount of times and all tests pass:

diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs
index af8a65b9..5d7ee3a5 100644
--- a/lightning/src/ln/channelmanager.rs
+++ b/lightning/src/ln/channelmanager.rs
@@ -5939,7 +5939,23 @@ impl<'a, Signer: Sign, M: Deref, T: Deref, K: Deref, F: Deref, L: Deref>
}
}
}
- }
+ } else {
+ for (htlc_source, htlc) in monitor.get_pending_outbound_htlcs() {
+ if let HTLCSource::OutboundRoute { payment_id, session_priv, path, payment_secret, .. } = htlc_source {
+ let path_amt = path.last().unwrap().fee_msat;
+ let mut session_priv_bytes = [0; 32];
+ session_priv_bytes[..].copy_from_slice(&session_priv[..]);
+ match pending_outbound_payments.as_mut().unwrap().entry(payment_id) {
+ hash_map::Entry::Occupied(mut entry) => {
+ assert!(!entry.get_mut().insert(session_priv_bytes, path_amt));
+ },
+ hash_map::Entry::Vacant(entry) => {
+ panic!("no entry for this payment");
+ }
+ }
+ }
+ }
+}
}
}

which is reassuring as well

@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch from f47acdb to 4687624CompareOctober 21, 2021 23:14

@valentinewallacevalentinewallace left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Think I'm ACK mod outstanding feedback. Might give it one more glance in the morning. Thanks for the additional test coverage!

Comment threadlightning/src/ln/channelmanager.rs Outdated
// We only rebuild the pending payments map if we were most recently serialized by
// 0.0.102+
for (_, monitor) in args.channel_monitors {
if by_id.get(&monitor.get_funding_txo().0.to_channel_id()).is_none() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Ah, right 🤦‍♀️

Fwiw, when I add this diff, it gets hit a fair amount of times and all tests pass:

diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs
index af8a65b9..5d7ee3a5 100644
--- a/lightning/src/ln/channelmanager.rs
+++ b/lightning/src/ln/channelmanager.rs
@@ -5939,7 +5939,23 @@ impl<'a, Signer: Sign, M: Deref, T: Deref, K: Deref, F: Deref, L: Deref>
}
}
}
- }
+ } else {
+ for (htlc_source, htlc) in monitor.get_pending_outbound_htlcs() {
+ if let HTLCSource::OutboundRoute { payment_id, session_priv, path, payment_secret, .. } = htlc_source {
+ let path_amt = path.last().unwrap().fee_msat;
+ let mut session_priv_bytes = [0; 32];
+ session_priv_bytes[..].copy_from_slice(&session_priv[..]);
+ match pending_outbound_payments.as_mut().unwrap().entry(payment_id) {
+ hash_map::Entry::Occupied(mut entry) => {
+ assert!(!entry.get_mut().insert(session_priv_bytes, path_amt));
+ },
+ hash_map::Entry::Vacant(entry) => {
+ panic!("no entry for this payment");
+ }
+ }
+ }
+ }
+}
}
}

which is reassuring as well

Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/payment_tests.rs Outdated
}

#[test]
fn retry_with_no_persist() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hmm, the new version of this test passes with the code that had the find_map bug

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

Ah! Yep, added even more coverage :)

@jkczyzjkczyz left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Also pretty close on an ACK after remaining feedback is addressed.

@valentinewallace Nice work on finding the test gaps. I was staring at retry_with_no_persist trying to figure out if the bug was being exercised. Mutation testing FTW lol.

I wonder if there's any improvements we can make to our testing strategy to make reviewing a bit easier. Some of the tests are pretty lengthy/verbose but I'm not sure if they can be broken up into smaller tests and still exercise the behavior.

/// monitor update failure must *not* have been sent to the remote end, and must instead
/// have been dropped. They will be regenerated when monitor_updating_restored is called.
pub fn monitor_update_failed(&mut self, resend_raa: bool, resend_commitment: bool, mut pending_forwards: Vec<(PendingHTLCInfo, u64)>, mut pending_fails: Vec<(HTLCSource, PaymentHash, HTLCFailReason)>) {
pub fn monitor_update_failed(&mut self, resend_raa: bool, resend_commitment: bool,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: move these parameters down a line, aligned with the other parameters

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

For a small function that doesn't seem to look any better to my eye? Feels very "rustfmt wants your code to use as many lines as possible" to me :).

Comment threadlightning/src/ln/functional_tests.rs Outdated
Comment on lines +642 to +686
// Now reload nodes[1]...
persister = test_utils::TestPersister::new();
let keys_manager = &chanmon_cfgs[1].keys_manager;
new_chain_monitor = test_utils::TestChainMonitor::new(Some(nodes[1].chain_source), nodes[1].tx_broadcaster.clone(), nodes[1].logger, node_cfgs[1].fee_estimator, &persister, keys_manager);
nodes[1].chain_monitor = &new_chain_monitor;
let mut chan_0_monitor_read = &chan_0_monitor_serialized.0[..];
let (_, mut chan_0_monitor) = <(BlockHash, ChannelMonitor<EnforcingSigner>)>::read(
&mut chan_0_monitor_read, keys_manager).unwrap();
assert!(chan_0_monitor_read.is_empty());

let (_, nodes_1_deserialized_tmp) = {
let mut channel_monitors = HashMap::new();
channel_monitors.insert(chan_0_monitor.get_funding_txo().0, &mut chan_0_monitor);
<(BlockHash, ChannelManager<EnforcingSigner, &test_utils::TestChainMonitor, &test_utils::TestBroadcaster, &test_utils::TestKeysInterface, &test_utils::TestFeeEstimator, &test_utils::TestLogger>)>
::read(&mut io::Cursor::new(&chan_manager_serialized.0[..]), ChannelManagerReadArgs {
default_config: Default::default(),
keys_manager,
fee_estimator: node_cfgs[1].fee_estimator,
chain_monitor: nodes[1].chain_monitor,
tx_broadcaster: nodes[1].tx_broadcaster.clone(),
logger: nodes[1].logger,
channel_monitors,
}).unwrap()
};
nodes_1_deserialized = nodes_1_deserialized_tmp;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is pretty verbose and looks repeated from retry_with_no_persist. Wondering if we could make a utility function for reloading so the tests are easier to read.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

Yea, its repeated in quite a few tests. I believe we could may be macro it, but it requires a few variables declared at top of function scope. I'll look at it as a followup, its not a new problem.

Comment threadlightning/src/ln/channelmanager.rs Outdated
@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch from 4687624 to 4348833CompareOctober 22, 2021 05:24

@valentinewallacevalentinewallace left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM 🚀

Comment threadlightning/src/ln/payment_tests.rs
@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch 2 times, most recently from b67f15c to d9a85b3CompareOctober 22, 2021 18:06
@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

Pushed yet more test coverage, I believe all outstanding comments have been addressed.

@valentinewallacevalentinewallace left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ACK after squash

@jkczyzjkczyz left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This substantially improves readability at the callsite and in the
function.
This improves readability at the callsite and in the function.
This allows us to read a `HashMap` that has values which may be
skipped if they are some backwards-compatibility type.
We also take this opportunity to fail deserialization if keys are
duplicated.
When an HTLC has been failed, we track it up until the point there
exists no broadcastable commitment transaction which has the HTLC
present, at which point Channel returns the HTLCSource back to the
ChannelManager, which fails the HTLC backwards appropriately.
When an HTLC is fulfilled, however, we fulfill on the backwards path
immediately. This is great for claiming upstream HTLCs, but when we
want to track pending payments, we need to ensure we can check with
ChannelMonitor data to rebuild pending payments. In order to do so,
we need an event similar to the HTLC failure event, but for
fulfills instead.
Specifically, if we force-close a channel, we remove its off-chain
`Channel` object entirely, at which point, on reload, we may notice
HTLC(s) which are not present in our pending payments map (as they
may have received a payment preimage, but not fully committed to
it). Thus, we'd conclude we still have a retryable payment, which
is untrue.
This commit does so, informing the ChannelManager via a new return
element where appropriate of the HTLCSource corresponding to the
failed HTLC.
In the next commit, we will reload lost pending payments from
ChannelMonitors during restart. However, in order to avoid
re-adding pending payments which have already been fulfilled, we
must ensure that we do not fully remove pending payments until all
HTLCs for the payment have been fully removed from their
ChannelMonitors.
We do so here, introducing a new PendingOutboundPayment variant
called `Completed` which only tracks the set of pending HTLCs.
If we go to send a payment, add the HTLC(s) to the channel(s),
commit the ChannelMonitor updates to disk, and then crash, we'll
come back up with no pending payments but HTLC(s) ready to be
claim/failed.
This makes it rather impractical to write a payment sender/retryer,
as you cannot guarantee atomicity - you cannot guarantee you'll
have retry data persisted even if the HTLC(s) are actually pending.
Because ChannelMonitors are *the* atomically-persisted data in LDK,
we lean on their current HTLC data to figure out what HTLC(s) are a
part of an outbound payment, rebuilding the pending payments list
on reload.
test_dup_htlc_onchain_fails_on_reload is now more of a
payment_test than a functional_test, testing for handling of
pending payments.
Peers probably shouldn't do this, but if they want to give us free
money, we should take it and not generate any spurious events.
@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch from d9a85b3 to 7af5d12CompareOctober 22, 2021 18:41
@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

Squashed without diff, will land after CI 🎉

$ git diff-tree -U1 d9a85b35b 7af5d127a
$

@TheBlueMatt
TheBlueMatt merged commit 0a31c12 into lightningdevkit:mainOct 22, 2021
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Generate PaymentPathFailed for MonitorUpdate-sourced HTLC failures when we don't have a corresponding pending payment entry

3 participants

@TheBlueMatt@jkczyz@valentinewallace
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Reload pending outbound payments from ChannelMonitors on startup by TheBlueMatt · Pull Request #1104 · lightningdevkit/rust-lightning · GitHub
Skip to content

Reload pending outbound payments from ChannelMonitors on startup - #1104

Merged
TheBlueMatt merged 11 commits into
lightningdevkit:mainfrom
TheBlueMatt:2021-10-payment-id-in-monitors
Oct 22, 2021
Merged

Reload pending outbound payments from ChannelMonitors on startup#1104
TheBlueMatt merged 11 commits into
lightningdevkit:mainfrom
TheBlueMatt:2021-10-payment-id-in-monitors

Conversation

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

When we add payment retries, we need some kind of on-disk consistency story. I think a compelling one is described, roughly at [1] - make the retryer persist first, before calling into the ChannelManager, then when reloading we either have a pending payment there or its pending nowhere. Once the send_payment method returns, we'll either have a pending payment persisted in both the retryer and a ChannelMonitor, or in neither. Note that we may not have anything in a ChannelManager until much later, preventing us from retrying the payment if we crash.

In such a case, we'll force-close the relevant channel (its ChannelMonitor has run ahead of the manager), but the actual HTLC will still be pending, just not present in ChannelManager at all. Ideally, we'd then be able to retry it over a whole new path.

This PR addresses this situation by having ChannelManager examine the available ChannelMonitors at startup, rebuilding pending payment entries on the basis of what's in its ChannelMonitors.

Note that we aren't particularly concerned with the payment resolution end of things - if ChannelManager thinks a payment is still pending when it has been fully resolved, we'll eventually consider it sent and generate a fresh PaymentSent when the ChannelMonitor resolves the current state.

This should fix#1102, but generally needs lots more tests (its not tested at all, aside from a hacky thing at the end to compare the loaded-from-disk and the rebuilt set of payments, which passes most tests).

[1] #1059 (comment)

@TheBlueMattTheBlueMatt added this to the 0.0.102 milestone Oct 4, 2021
@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch from e97930f to a0fe042CompareOctober 5, 2021 05:59
@codecov

codecovBot commented Oct 5, 2021

Copy link
Copy Markdown

Codecov Report

Merging #1104 (7af5d12) into main (107c6c7) will increase coverage by 0.06%.
The diff coverage is 96.03%.

Impacted file tree graph

@@ Coverage Diff @@## main #1104 +/- ##
==========================================
+ Coverage 90.40% 90.47% +0.06% 
==========================================
Files 68 68 Lines 34796 35176 +380 ==========================================
+ Hits 31458 31826 +368 - Misses 3338 3350 +12 
Impacted FilesCoverage Δ
lightning/src/util/ser.rs89.02% <80.00%> (-0.12%)⬇️
lightning/src/ln/channelmanager.rs83.86% <90.06%> (+0.34%)⬆️
lightning/src/ln/channel.rs88.37% <96.22%> (+0.05%)⬆️
lightning/src/chain/channelmonitor.rs91.11% <97.56%> (+0.17%)⬆️
lightning/src/ln/payment_tests.rs99.09% <99.26%> (+0.25%)⬆️
lightning/src/ln/functional_test_utils.rs95.09% <100.00%> (+<0.01%)⬆️
lightning/src/ln/functional_tests.rs97.37% <100.00%> (-0.04%)⬇️
lightning/src/ln/onion_route_tests.rs96.60% <100.00%> (ø)
lightning/src/ln/features.rs99.43% <0.00%> (+<0.01%)⬆️
... and 2 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 107c6c7...7af5d12. Read the comment docs.

@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

This is now based on #1108, which should go first.

@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

Depends on #1109 as well now.

@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch from a0fe042 to f821c93CompareOctober 5, 2021 23:26
@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

Now includes at least a naive test of payment data reloading, which works 🎉

@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch 4 times, most recently from 6e60c4f to a7cea3dCompareOctober 11, 2021 00:53
@TheBlueMattTheBlueMatt modified the milestones: 0.0.102, 0.0.103Oct 15, 2021
@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch from a7cea3d to 7d23578CompareOctober 20, 2021 01:25
@TheBlueMatt
TheBlueMatt marked this pull request as ready for review October 20, 2021 01:25
@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

Rebased on upstream!

@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch from 7d23578 to df8b291CompareOctober 20, 2021 02:54
@jkczyz
jkczyz self-requested a review October 20, 2021 17:56

@valentinewallacevalentinewallace left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Mostly wondering how we handle failures that were previously fulfilled

Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/chain/channelmonitor.rs
Comment threadlightning/src/chain/channelmonitor.rs Outdated
@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch from df8b291 to 42a854cCompareOctober 20, 2021 21:13

@jkczyzjkczyz left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Sorry about any confusion around terminology. I don't have as a firm of grasp as I'd like to around some of this.

Comment on lines +344 to +363
pub(super) struct RAAUpdates {
pub commitment_update: Option<msgs::CommitmentUpdate>,
pub to_forward_htlcs: Vec<(PendingHTLCInfo, u64)>,
pub failed_htlcs: Vec<(HTLCSource, PaymentHash, HTLCFailReason)>,
pub finalized_claim_htlcs: Vec<HTLCSource>,
pub monitor_update: ChannelMonitorUpdate,
pub holding_cell_failed_htlcs: Vec<(HTLCSource, PaymentHash)>,
}

/// The return value of `monitor_updating_resotred`
pub(super) struct MonitorRestoreUpdates {
pub raa: Option<msgs::RevokeAndACK>,
pub commitment_update: Option<msgs::CommitmentUpdate>,
pub order: RAACommitmentOrder,
pub forwards: Vec<(PendingHTLCInfo, u64)>,
pub failures: Vec<(HTLCSource, PaymentHash, HTLCFailReason)>,
pub finalized_claims: Vec<HTLCSource>,
pub funding_broadcastable: Option<Transaction>,
pub funding_locked: Option<msgs::FundingLocked>,
}

@jkczyzjkczyzOct 20, 2021

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Where it makes sense, could we use consistent field names between these two structs?

Comment threadlightning/src/ln/channel.rs Outdated
}
self.monitor_pending_forwards.append(&mut to_forward_infos);
self.monitor_pending_failures.append(&mut revoked_htlcs);
self.monitor_pending_finalized_fulfills.append(&mut finalized_claim_htlcs);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

In terms of terminology, is there a difference between a "fulfill" and a "claim"? Does it depend on the context?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

It mostly just depends on when the code was written :p

commitment_update: Some(commitment_update),
finalized_claim_htlcs,
to_forward_htlcs: to_forward_infos,
failed_htlcs: revoked_htlcs,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Similar question wrt "failed" and "revoked". Just want to make sure I understand if it is context dependent.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

Same answer, there really isn't.

Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment on lines +2018 to +2023
if let hash_map::Entry::Occupied(payment) = &payment_entry {
if !payment.get().is_retryable() {
return Err(APIError::RouteError {
err: "Payment already completed"
});
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It's unclear to me why this shouldn't be if payment.get().is_complete() { as indicated by the error message. Do we want to hit this for legacy payments, too?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

Hmm, yes, we probably should hit this for legacy payments. I mean its unreachable for legacy payments, I believe (retry_payment refuses to call send_payment_internal at all), but nothing wrong with an extra check here.

Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment on lines +419 to +444
/// When a pending payment completes, we continue tracking it until all pendings HTLCs have
/// been resolved. This ensures we don't look up pending payments in ChannelMonitors on restart
/// and add a pending payment that was already completed.
Fulfilled {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Terminology question related to my earlier comment: is there a difference between "completes" and "fulfilled" as it pertains to the first sentence in the docs and the variant name?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

No, I just missed the docs in the rename - once we get one HTLC fulfillment we consider the payment fulfilled.

@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

Pushed a fix for the lockorder comment, leaving other stuff until we settle on naming.

@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch from 2aef42c to ca6ae4dCompareOctober 21, 2021 00:00
Comment threadlightning/src/chain/channelmonitor.rs Outdated
Comment threadlightning/src/chain/channelmonitor.rs Outdated
Comment threadlightning/src/chain/channelmonitor.rs
Comment threadlightning/src/chain/channelmonitor.rs Outdated
Comment threadlightning/src/chain/channelmonitor.rs Outdated
Comment threadlightning/src/chain/channelmonitor.rs Outdated
@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch from ca6ae4d to a050236CompareOctober 21, 2021 19:15
@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

Ok, I believe I've addressed all feedback and renamed things to be more clear.

Comment threadlightning/src/chain/channelmonitor.rs
Comment threadlightning/src/chain/channelmonitor.rs Outdated
Comment threadlightning/src/chain/channelmonitor.rs Outdated
Comment threadlightning/src/ln/channel.rs Outdated
Comment threadlightning/src/ln/channel.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch from a050236 to f47acdbCompareOctober 21, 2021 20:41
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/chain/channelmonitor.rs Outdated
// We only rebuild the pending payments map if we were most recently serialized by
// 0.0.102+
for (_, monitor) in args.channel_monitors {
if by_id.get(&monitor.get_funding_txo().0.to_channel_id()).is_none() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hmm, I don't see why we don't load pending outbound payments for open channels as well. Couldn't we still have a situation where the ChannelManager doesn't get persisted after initiating an outbound payment, but the ChannelMonitor still knows about it?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

The channel monitor knowing about a payment that the ChannelManager does not implies by definition that the ChannelMonitor has some newer state than the ChannelManager. It's possible I missed some edge case but it feels pretty straightforward?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Ah, right 🤦‍♀️

Fwiw, when I add this diff, it gets hit a fair amount of times and all tests pass:

diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs
index af8a65b9..5d7ee3a5 100644
--- a/lightning/src/ln/channelmanager.rs
+++ b/lightning/src/ln/channelmanager.rs
@@ -5939,7 +5939,23 @@ impl<'a, Signer: Sign, M: Deref, T: Deref, K: Deref, F: Deref, L: Deref>
}
}
}
- }
+ } else {
+ for (htlc_source, htlc) in monitor.get_pending_outbound_htlcs() {
+ if let HTLCSource::OutboundRoute { payment_id, session_priv, path, payment_secret, .. } = htlc_source {
+ let path_amt = path.last().unwrap().fee_msat;
+ let mut session_priv_bytes = [0; 32];
+ session_priv_bytes[..].copy_from_slice(&session_priv[..]);
+ match pending_outbound_payments.as_mut().unwrap().entry(payment_id) {
+ hash_map::Entry::Occupied(mut entry) => {
+ assert!(!entry.get_mut().insert(session_priv_bytes, path_amt));
+ },
+ hash_map::Entry::Vacant(entry) => {
+ panic!("no entry for this payment");
+ }
+ }
+ }
+ }
+}
}
}

which is reassuring as well

@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch from f47acdb to 4687624CompareOctober 21, 2021 23:14

@valentinewallacevalentinewallace left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Think I'm ACK mod outstanding feedback. Might give it one more glance in the morning. Thanks for the additional test coverage!

Comment threadlightning/src/ln/channelmanager.rs Outdated
// We only rebuild the pending payments map if we were most recently serialized by
// 0.0.102+
for (_, monitor) in args.channel_monitors {
if by_id.get(&monitor.get_funding_txo().0.to_channel_id()).is_none() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Ah, right 🤦‍♀️

Fwiw, when I add this diff, it gets hit a fair amount of times and all tests pass:

diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs
index af8a65b9..5d7ee3a5 100644
--- a/lightning/src/ln/channelmanager.rs
+++ b/lightning/src/ln/channelmanager.rs
@@ -5939,7 +5939,23 @@ impl<'a, Signer: Sign, M: Deref, T: Deref, K: Deref, F: Deref, L: Deref>
}
}
}
- }
+ } else {
+ for (htlc_source, htlc) in monitor.get_pending_outbound_htlcs() {
+ if let HTLCSource::OutboundRoute { payment_id, session_priv, path, payment_secret, .. } = htlc_source {
+ let path_amt = path.last().unwrap().fee_msat;
+ let mut session_priv_bytes = [0; 32];
+ session_priv_bytes[..].copy_from_slice(&session_priv[..]);
+ match pending_outbound_payments.as_mut().unwrap().entry(payment_id) {
+ hash_map::Entry::Occupied(mut entry) => {
+ assert!(!entry.get_mut().insert(session_priv_bytes, path_amt));
+ },
+ hash_map::Entry::Vacant(entry) => {
+ panic!("no entry for this payment");
+ }
+ }
+ }
+ }
+}
}
}

which is reassuring as well

Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/payment_tests.rs Outdated
}

#[test]
fn retry_with_no_persist() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hmm, the new version of this test passes with the code that had the find_map bug

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

Ah! Yep, added even more coverage :)

@jkczyzjkczyz left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Also pretty close on an ACK after remaining feedback is addressed.

@valentinewallace Nice work on finding the test gaps. I was staring at retry_with_no_persist trying to figure out if the bug was being exercised. Mutation testing FTW lol.

I wonder if there's any improvements we can make to our testing strategy to make reviewing a bit easier. Some of the tests are pretty lengthy/verbose but I'm not sure if they can be broken up into smaller tests and still exercise the behavior.

/// monitor update failure must *not* have been sent to the remote end, and must instead
/// have been dropped. They will be regenerated when monitor_updating_restored is called.
pub fn monitor_update_failed(&mut self, resend_raa: bool, resend_commitment: bool, mut pending_forwards: Vec<(PendingHTLCInfo, u64)>, mut pending_fails: Vec<(HTLCSource, PaymentHash, HTLCFailReason)>) {
pub fn monitor_update_failed(&mut self, resend_raa: bool, resend_commitment: bool,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: move these parameters down a line, aligned with the other parameters

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

For a small function that doesn't seem to look any better to my eye? Feels very "rustfmt wants your code to use as many lines as possible" to me :).

Comment threadlightning/src/ln/functional_tests.rs Outdated
Comment on lines +642 to +686
// Now reload nodes[1]...
persister = test_utils::TestPersister::new();
let keys_manager = &chanmon_cfgs[1].keys_manager;
new_chain_monitor = test_utils::TestChainMonitor::new(Some(nodes[1].chain_source), nodes[1].tx_broadcaster.clone(), nodes[1].logger, node_cfgs[1].fee_estimator, &persister, keys_manager);
nodes[1].chain_monitor = &new_chain_monitor;
let mut chan_0_monitor_read = &chan_0_monitor_serialized.0[..];
let (_, mut chan_0_monitor) = <(BlockHash, ChannelMonitor<EnforcingSigner>)>::read(
&mut chan_0_monitor_read, keys_manager).unwrap();
assert!(chan_0_monitor_read.is_empty());

let (_, nodes_1_deserialized_tmp) = {
let mut channel_monitors = HashMap::new();
channel_monitors.insert(chan_0_monitor.get_funding_txo().0, &mut chan_0_monitor);
<(BlockHash, ChannelManager<EnforcingSigner, &test_utils::TestChainMonitor, &test_utils::TestBroadcaster, &test_utils::TestKeysInterface, &test_utils::TestFeeEstimator, &test_utils::TestLogger>)>
::read(&mut io::Cursor::new(&chan_manager_serialized.0[..]), ChannelManagerReadArgs {
default_config: Default::default(),
keys_manager,
fee_estimator: node_cfgs[1].fee_estimator,
chain_monitor: nodes[1].chain_monitor,
tx_broadcaster: nodes[1].tx_broadcaster.clone(),
logger: nodes[1].logger,
channel_monitors,
}).unwrap()
};
nodes_1_deserialized = nodes_1_deserialized_tmp;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is pretty verbose and looks repeated from retry_with_no_persist. Wondering if we could make a utility function for reloading so the tests are easier to read.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

Yea, its repeated in quite a few tests. I believe we could may be macro it, but it requires a few variables declared at top of function scope. I'll look at it as a followup, its not a new problem.

Comment threadlightning/src/ln/channelmanager.rs Outdated
@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch from 4687624 to 4348833CompareOctober 22, 2021 05:24

@valentinewallacevalentinewallace left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM 🚀

Comment threadlightning/src/ln/payment_tests.rs
@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch 2 times, most recently from b67f15c to d9a85b3CompareOctober 22, 2021 18:06
@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

Pushed yet more test coverage, I believe all outstanding comments have been addressed.

@valentinewallacevalentinewallace left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ACK after squash

@jkczyzjkczyz left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This substantially improves readability at the callsite and in the
function.
This improves readability at the callsite and in the function.
This allows us to read a `HashMap` that has values which may be
skipped if they are some backwards-compatibility type.
We also take this opportunity to fail deserialization if keys are
duplicated.
When an HTLC has been failed, we track it up until the point there
exists no broadcastable commitment transaction which has the HTLC
present, at which point Channel returns the HTLCSource back to the
ChannelManager, which fails the HTLC backwards appropriately.
When an HTLC is fulfilled, however, we fulfill on the backwards path
immediately. This is great for claiming upstream HTLCs, but when we
want to track pending payments, we need to ensure we can check with
ChannelMonitor data to rebuild pending payments. In order to do so,
we need an event similar to the HTLC failure event, but for
fulfills instead.
Specifically, if we force-close a channel, we remove its off-chain
`Channel` object entirely, at which point, on reload, we may notice
HTLC(s) which are not present in our pending payments map (as they
may have received a payment preimage, but not fully committed to
it). Thus, we'd conclude we still have a retryable payment, which
is untrue.
This commit does so, informing the ChannelManager via a new return
element where appropriate of the HTLCSource corresponding to the
failed HTLC.
In the next commit, we will reload lost pending payments from
ChannelMonitors during restart. However, in order to avoid
re-adding pending payments which have already been fulfilled, we
must ensure that we do not fully remove pending payments until all
HTLCs for the payment have been fully removed from their
ChannelMonitors.
We do so here, introducing a new PendingOutboundPayment variant
called `Completed` which only tracks the set of pending HTLCs.
If we go to send a payment, add the HTLC(s) to the channel(s),
commit the ChannelMonitor updates to disk, and then crash, we'll
come back up with no pending payments but HTLC(s) ready to be
claim/failed.
This makes it rather impractical to write a payment sender/retryer,
as you cannot guarantee atomicity - you cannot guarantee you'll
have retry data persisted even if the HTLC(s) are actually pending.
Because ChannelMonitors are *the* atomically-persisted data in LDK,
we lean on their current HTLC data to figure out what HTLC(s) are a
part of an outbound payment, rebuilding the pending payments list
on reload.
test_dup_htlc_onchain_fails_on_reload is now more of a
payment_test than a functional_test, testing for handling of
pending payments.
Peers probably shouldn't do this, but if they want to give us free
money, we should take it and not generate any spurious events.
@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch from d9a85b3 to 7af5d12CompareOctober 22, 2021 18:41
@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

Squashed without diff, will land after CI 🎉

$ git diff-tree -U1 d9a85b35b 7af5d127a
$

@TheBlueMatt
TheBlueMatt merged commit 0a31c12 into lightningdevkit:mainOct 22, 2021
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Generate PaymentPathFailed for MonitorUpdate-sourced HTLC failures when we don't have a corresponding pending payment entry

3 participants

@TheBlueMatt@jkczyz@valentinewallace
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Reload pending outbound payments from ChannelMonitors on startup by TheBlueMatt · Pull Request #1104 · lightningdevkit/rust-lightning · GitHub
Skip to content

Reload pending outbound payments from ChannelMonitors on startup - #1104

Merged
TheBlueMatt merged 11 commits into
lightningdevkit:mainfrom
TheBlueMatt:2021-10-payment-id-in-monitors
Oct 22, 2021
Merged

Reload pending outbound payments from ChannelMonitors on startup#1104
TheBlueMatt merged 11 commits into
lightningdevkit:mainfrom
TheBlueMatt:2021-10-payment-id-in-monitors

Conversation

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

When we add payment retries, we need some kind of on-disk consistency story. I think a compelling one is described, roughly at [1] - make the retryer persist first, before calling into the ChannelManager, then when reloading we either have a pending payment there or its pending nowhere. Once the send_payment method returns, we'll either have a pending payment persisted in both the retryer and a ChannelMonitor, or in neither. Note that we may not have anything in a ChannelManager until much later, preventing us from retrying the payment if we crash.

In such a case, we'll force-close the relevant channel (its ChannelMonitor has run ahead of the manager), but the actual HTLC will still be pending, just not present in ChannelManager at all. Ideally, we'd then be able to retry it over a whole new path.

This PR addresses this situation by having ChannelManager examine the available ChannelMonitors at startup, rebuilding pending payment entries on the basis of what's in its ChannelMonitors.

Note that we aren't particularly concerned with the payment resolution end of things - if ChannelManager thinks a payment is still pending when it has been fully resolved, we'll eventually consider it sent and generate a fresh PaymentSent when the ChannelMonitor resolves the current state.

This should fix#1102, but generally needs lots more tests (its not tested at all, aside from a hacky thing at the end to compare the loaded-from-disk and the rebuilt set of payments, which passes most tests).

[1] #1059 (comment)

@TheBlueMattTheBlueMatt added this to the 0.0.102 milestone Oct 4, 2021
@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch from e97930f to a0fe042CompareOctober 5, 2021 05:59
@codecov

codecovBot commented Oct 5, 2021

Copy link
Copy Markdown

Codecov Report

Merging #1104 (7af5d12) into main (107c6c7) will increase coverage by 0.06%.
The diff coverage is 96.03%.

Impacted file tree graph

@@ Coverage Diff @@## main #1104 +/- ##
==========================================
+ Coverage 90.40% 90.47% +0.06% 
==========================================
Files 68 68 Lines 34796 35176 +380 ==========================================
+ Hits 31458 31826 +368 - Misses 3338 3350 +12 
Impacted FilesCoverage Δ
lightning/src/util/ser.rs89.02% <80.00%> (-0.12%)⬇️
lightning/src/ln/channelmanager.rs83.86% <90.06%> (+0.34%)⬆️
lightning/src/ln/channel.rs88.37% <96.22%> (+0.05%)⬆️
lightning/src/chain/channelmonitor.rs91.11% <97.56%> (+0.17%)⬆️
lightning/src/ln/payment_tests.rs99.09% <99.26%> (+0.25%)⬆️
lightning/src/ln/functional_test_utils.rs95.09% <100.00%> (+<0.01%)⬆️
lightning/src/ln/functional_tests.rs97.37% <100.00%> (-0.04%)⬇️
lightning/src/ln/onion_route_tests.rs96.60% <100.00%> (ø)
lightning/src/ln/features.rs99.43% <0.00%> (+<0.01%)⬆️
... and 2 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 107c6c7...7af5d12. Read the comment docs.

@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

This is now based on #1108, which should go first.

@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

Depends on #1109 as well now.

@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch from a0fe042 to f821c93CompareOctober 5, 2021 23:26
@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

Now includes at least a naive test of payment data reloading, which works 🎉

@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch 4 times, most recently from 6e60c4f to a7cea3dCompareOctober 11, 2021 00:53
@TheBlueMattTheBlueMatt modified the milestones: 0.0.102, 0.0.103Oct 15, 2021
@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch from a7cea3d to 7d23578CompareOctober 20, 2021 01:25
@TheBlueMatt
TheBlueMatt marked this pull request as ready for review October 20, 2021 01:25
@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

Rebased on upstream!

@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch from 7d23578 to df8b291CompareOctober 20, 2021 02:54
@jkczyz
jkczyz self-requested a review October 20, 2021 17:56

@valentinewallacevalentinewallace left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Mostly wondering how we handle failures that were previously fulfilled

Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/chain/channelmonitor.rs
Comment threadlightning/src/chain/channelmonitor.rs Outdated
@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch from df8b291 to 42a854cCompareOctober 20, 2021 21:13

@jkczyzjkczyz left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Sorry about any confusion around terminology. I don't have as a firm of grasp as I'd like to around some of this.

Comment on lines +344 to +363
pub(super) struct RAAUpdates {
pub commitment_update: Option<msgs::CommitmentUpdate>,
pub to_forward_htlcs: Vec<(PendingHTLCInfo, u64)>,
pub failed_htlcs: Vec<(HTLCSource, PaymentHash, HTLCFailReason)>,
pub finalized_claim_htlcs: Vec<HTLCSource>,
pub monitor_update: ChannelMonitorUpdate,
pub holding_cell_failed_htlcs: Vec<(HTLCSource, PaymentHash)>,
}

/// The return value of `monitor_updating_resotred`
pub(super) struct MonitorRestoreUpdates {
pub raa: Option<msgs::RevokeAndACK>,
pub commitment_update: Option<msgs::CommitmentUpdate>,
pub order: RAACommitmentOrder,
pub forwards: Vec<(PendingHTLCInfo, u64)>,
pub failures: Vec<(HTLCSource, PaymentHash, HTLCFailReason)>,
pub finalized_claims: Vec<HTLCSource>,
pub funding_broadcastable: Option<Transaction>,
pub funding_locked: Option<msgs::FundingLocked>,
}

@jkczyzjkczyzOct 20, 2021

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Where it makes sense, could we use consistent field names between these two structs?

Comment threadlightning/src/ln/channel.rs Outdated
}
self.monitor_pending_forwards.append(&mut to_forward_infos);
self.monitor_pending_failures.append(&mut revoked_htlcs);
self.monitor_pending_finalized_fulfills.append(&mut finalized_claim_htlcs);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

In terms of terminology, is there a difference between a "fulfill" and a "claim"? Does it depend on the context?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

It mostly just depends on when the code was written :p

commitment_update: Some(commitment_update),
finalized_claim_htlcs,
to_forward_htlcs: to_forward_infos,
failed_htlcs: revoked_htlcs,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Similar question wrt "failed" and "revoked". Just want to make sure I understand if it is context dependent.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

Same answer, there really isn't.

Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment on lines +2018 to +2023
if let hash_map::Entry::Occupied(payment) = &payment_entry {
if !payment.get().is_retryable() {
return Err(APIError::RouteError {
err: "Payment already completed"
});
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It's unclear to me why this shouldn't be if payment.get().is_complete() { as indicated by the error message. Do we want to hit this for legacy payments, too?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

Hmm, yes, we probably should hit this for legacy payments. I mean its unreachable for legacy payments, I believe (retry_payment refuses to call send_payment_internal at all), but nothing wrong with an extra check here.

Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment on lines +419 to +444
/// When a pending payment completes, we continue tracking it until all pendings HTLCs have
/// been resolved. This ensures we don't look up pending payments in ChannelMonitors on restart
/// and add a pending payment that was already completed.
Fulfilled {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Terminology question related to my earlier comment: is there a difference between "completes" and "fulfilled" as it pertains to the first sentence in the docs and the variant name?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

No, I just missed the docs in the rename - once we get one HTLC fulfillment we consider the payment fulfilled.

@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

Pushed a fix for the lockorder comment, leaving other stuff until we settle on naming.

@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch from 2aef42c to ca6ae4dCompareOctober 21, 2021 00:00
Comment threadlightning/src/chain/channelmonitor.rs Outdated
Comment threadlightning/src/chain/channelmonitor.rs Outdated
Comment threadlightning/src/chain/channelmonitor.rs
Comment threadlightning/src/chain/channelmonitor.rs Outdated
Comment threadlightning/src/chain/channelmonitor.rs Outdated
Comment threadlightning/src/chain/channelmonitor.rs Outdated
@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch from ca6ae4d to a050236CompareOctober 21, 2021 19:15
@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

Ok, I believe I've addressed all feedback and renamed things to be more clear.

Comment threadlightning/src/chain/channelmonitor.rs
Comment threadlightning/src/chain/channelmonitor.rs Outdated
Comment threadlightning/src/chain/channelmonitor.rs Outdated
Comment threadlightning/src/ln/channel.rs Outdated
Comment threadlightning/src/ln/channel.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch from a050236 to f47acdbCompareOctober 21, 2021 20:41
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/chain/channelmonitor.rs Outdated
// We only rebuild the pending payments map if we were most recently serialized by
// 0.0.102+
for (_, monitor) in args.channel_monitors {
if by_id.get(&monitor.get_funding_txo().0.to_channel_id()).is_none() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hmm, I don't see why we don't load pending outbound payments for open channels as well. Couldn't we still have a situation where the ChannelManager doesn't get persisted after initiating an outbound payment, but the ChannelMonitor still knows about it?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

The channel monitor knowing about a payment that the ChannelManager does not implies by definition that the ChannelMonitor has some newer state than the ChannelManager. It's possible I missed some edge case but it feels pretty straightforward?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Ah, right 🤦‍♀️

Fwiw, when I add this diff, it gets hit a fair amount of times and all tests pass:

diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs
index af8a65b9..5d7ee3a5 100644
--- a/lightning/src/ln/channelmanager.rs
+++ b/lightning/src/ln/channelmanager.rs
@@ -5939,7 +5939,23 @@ impl<'a, Signer: Sign, M: Deref, T: Deref, K: Deref, F: Deref, L: Deref>
}
}
}
- }
+ } else {
+ for (htlc_source, htlc) in monitor.get_pending_outbound_htlcs() {
+ if let HTLCSource::OutboundRoute { payment_id, session_priv, path, payment_secret, .. } = htlc_source {
+ let path_amt = path.last().unwrap().fee_msat;
+ let mut session_priv_bytes = [0; 32];
+ session_priv_bytes[..].copy_from_slice(&session_priv[..]);
+ match pending_outbound_payments.as_mut().unwrap().entry(payment_id) {
+ hash_map::Entry::Occupied(mut entry) => {
+ assert!(!entry.get_mut().insert(session_priv_bytes, path_amt));
+ },
+ hash_map::Entry::Vacant(entry) => {
+ panic!("no entry for this payment");
+ }
+ }
+ }
+ }
+}
}
}

which is reassuring as well

@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch from f47acdb to 4687624CompareOctober 21, 2021 23:14

@valentinewallacevalentinewallace left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Think I'm ACK mod outstanding feedback. Might give it one more glance in the morning. Thanks for the additional test coverage!

Comment threadlightning/src/ln/channelmanager.rs Outdated
// We only rebuild the pending payments map if we were most recently serialized by
// 0.0.102+
for (_, monitor) in args.channel_monitors {
if by_id.get(&monitor.get_funding_txo().0.to_channel_id()).is_none() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Ah, right 🤦‍♀️

Fwiw, when I add this diff, it gets hit a fair amount of times and all tests pass:

diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs
index af8a65b9..5d7ee3a5 100644
--- a/lightning/src/ln/channelmanager.rs
+++ b/lightning/src/ln/channelmanager.rs
@@ -5939,7 +5939,23 @@ impl<'a, Signer: Sign, M: Deref, T: Deref, K: Deref, F: Deref, L: Deref>
}
}
}
- }
+ } else {
+ for (htlc_source, htlc) in monitor.get_pending_outbound_htlcs() {
+ if let HTLCSource::OutboundRoute { payment_id, session_priv, path, payment_secret, .. } = htlc_source {
+ let path_amt = path.last().unwrap().fee_msat;
+ let mut session_priv_bytes = [0; 32];
+ session_priv_bytes[..].copy_from_slice(&session_priv[..]);
+ match pending_outbound_payments.as_mut().unwrap().entry(payment_id) {
+ hash_map::Entry::Occupied(mut entry) => {
+ assert!(!entry.get_mut().insert(session_priv_bytes, path_amt));
+ },
+ hash_map::Entry::Vacant(entry) => {
+ panic!("no entry for this payment");
+ }
+ }
+ }
+ }
+}
}
}

which is reassuring as well

Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/payment_tests.rs Outdated
}

#[test]
fn retry_with_no_persist() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hmm, the new version of this test passes with the code that had the find_map bug

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

Ah! Yep, added even more coverage :)

@jkczyzjkczyz left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Also pretty close on an ACK after remaining feedback is addressed.

@valentinewallace Nice work on finding the test gaps. I was staring at retry_with_no_persist trying to figure out if the bug was being exercised. Mutation testing FTW lol.

I wonder if there's any improvements we can make to our testing strategy to make reviewing a bit easier. Some of the tests are pretty lengthy/verbose but I'm not sure if they can be broken up into smaller tests and still exercise the behavior.

/// monitor update failure must *not* have been sent to the remote end, and must instead
/// have been dropped. They will be regenerated when monitor_updating_restored is called.
pub fn monitor_update_failed(&mut self, resend_raa: bool, resend_commitment: bool, mut pending_forwards: Vec<(PendingHTLCInfo, u64)>, mut pending_fails: Vec<(HTLCSource, PaymentHash, HTLCFailReason)>) {
pub fn monitor_update_failed(&mut self, resend_raa: bool, resend_commitment: bool,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: move these parameters down a line, aligned with the other parameters

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

For a small function that doesn't seem to look any better to my eye? Feels very "rustfmt wants your code to use as many lines as possible" to me :).

Comment threadlightning/src/ln/functional_tests.rs Outdated
Comment on lines +642 to +686
// Now reload nodes[1]...
persister = test_utils::TestPersister::new();
let keys_manager = &chanmon_cfgs[1].keys_manager;
new_chain_monitor = test_utils::TestChainMonitor::new(Some(nodes[1].chain_source), nodes[1].tx_broadcaster.clone(), nodes[1].logger, node_cfgs[1].fee_estimator, &persister, keys_manager);
nodes[1].chain_monitor = &new_chain_monitor;
let mut chan_0_monitor_read = &chan_0_monitor_serialized.0[..];
let (_, mut chan_0_monitor) = <(BlockHash, ChannelMonitor<EnforcingSigner>)>::read(
&mut chan_0_monitor_read, keys_manager).unwrap();
assert!(chan_0_monitor_read.is_empty());

let (_, nodes_1_deserialized_tmp) = {
let mut channel_monitors = HashMap::new();
channel_monitors.insert(chan_0_monitor.get_funding_txo().0, &mut chan_0_monitor);
<(BlockHash, ChannelManager<EnforcingSigner, &test_utils::TestChainMonitor, &test_utils::TestBroadcaster, &test_utils::TestKeysInterface, &test_utils::TestFeeEstimator, &test_utils::TestLogger>)>
::read(&mut io::Cursor::new(&chan_manager_serialized.0[..]), ChannelManagerReadArgs {
default_config: Default::default(),
keys_manager,
fee_estimator: node_cfgs[1].fee_estimator,
chain_monitor: nodes[1].chain_monitor,
tx_broadcaster: nodes[1].tx_broadcaster.clone(),
logger: nodes[1].logger,
channel_monitors,
}).unwrap()
};
nodes_1_deserialized = nodes_1_deserialized_tmp;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is pretty verbose and looks repeated from retry_with_no_persist. Wondering if we could make a utility function for reloading so the tests are easier to read.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

Yea, its repeated in quite a few tests. I believe we could may be macro it, but it requires a few variables declared at top of function scope. I'll look at it as a followup, its not a new problem.

Comment threadlightning/src/ln/channelmanager.rs Outdated
@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch from 4687624 to 4348833CompareOctober 22, 2021 05:24

@valentinewallacevalentinewallace left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM 🚀

Comment threadlightning/src/ln/payment_tests.rs
@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch 2 times, most recently from b67f15c to d9a85b3CompareOctober 22, 2021 18:06
@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

Pushed yet more test coverage, I believe all outstanding comments have been addressed.

@valentinewallacevalentinewallace left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ACK after squash

@jkczyzjkczyz left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This substantially improves readability at the callsite and in the
function.
This improves readability at the callsite and in the function.
This allows us to read a `HashMap` that has values which may be
skipped if they are some backwards-compatibility type.
We also take this opportunity to fail deserialization if keys are
duplicated.
When an HTLC has been failed, we track it up until the point there
exists no broadcastable commitment transaction which has the HTLC
present, at which point Channel returns the HTLCSource back to the
ChannelManager, which fails the HTLC backwards appropriately.
When an HTLC is fulfilled, however, we fulfill on the backwards path
immediately. This is great for claiming upstream HTLCs, but when we
want to track pending payments, we need to ensure we can check with
ChannelMonitor data to rebuild pending payments. In order to do so,
we need an event similar to the HTLC failure event, but for
fulfills instead.
Specifically, if we force-close a channel, we remove its off-chain
`Channel` object entirely, at which point, on reload, we may notice
HTLC(s) which are not present in our pending payments map (as they
may have received a payment preimage, but not fully committed to
it). Thus, we'd conclude we still have a retryable payment, which
is untrue.
This commit does so, informing the ChannelManager via a new return
element where appropriate of the HTLCSource corresponding to the
failed HTLC.
In the next commit, we will reload lost pending payments from
ChannelMonitors during restart. However, in order to avoid
re-adding pending payments which have already been fulfilled, we
must ensure that we do not fully remove pending payments until all
HTLCs for the payment have been fully removed from their
ChannelMonitors.
We do so here, introducing a new PendingOutboundPayment variant
called `Completed` which only tracks the set of pending HTLCs.
If we go to send a payment, add the HTLC(s) to the channel(s),
commit the ChannelMonitor updates to disk, and then crash, we'll
come back up with no pending payments but HTLC(s) ready to be
claim/failed.
This makes it rather impractical to write a payment sender/retryer,
as you cannot guarantee atomicity - you cannot guarantee you'll
have retry data persisted even if the HTLC(s) are actually pending.
Because ChannelMonitors are *the* atomically-persisted data in LDK,
we lean on their current HTLC data to figure out what HTLC(s) are a
part of an outbound payment, rebuilding the pending payments list
on reload.
test_dup_htlc_onchain_fails_on_reload is now more of a
payment_test than a functional_test, testing for handling of
pending payments.
Peers probably shouldn't do this, but if they want to give us free
money, we should take it and not generate any spurious events.
@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch from d9a85b3 to 7af5d12CompareOctober 22, 2021 18:41
@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

Squashed without diff, will land after CI 🎉

$ git diff-tree -U1 d9a85b35b 7af5d127a
$

@TheBlueMatt
TheBlueMatt merged commit 0a31c12 into lightningdevkit:mainOct 22, 2021
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Generate PaymentPathFailed for MonitorUpdate-sourced HTLC failures when we don't have a corresponding pending payment entry

3 participants

@TheBlueMatt@jkczyz@valentinewallace
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Reload pending outbound payments from ChannelMonitors on startup by TheBlueMatt · Pull Request #1104 · lightningdevkit/rust-lightning · GitHub
Skip to content

Reload pending outbound payments from ChannelMonitors on startup - #1104

Merged
TheBlueMatt merged 11 commits into
lightningdevkit:mainfrom
TheBlueMatt:2021-10-payment-id-in-monitors
Oct 22, 2021
Merged

Reload pending outbound payments from ChannelMonitors on startup#1104
TheBlueMatt merged 11 commits into
lightningdevkit:mainfrom
TheBlueMatt:2021-10-payment-id-in-monitors

Conversation

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

When we add payment retries, we need some kind of on-disk consistency story. I think a compelling one is described, roughly at [1] - make the retryer persist first, before calling into the ChannelManager, then when reloading we either have a pending payment there or its pending nowhere. Once the send_payment method returns, we'll either have a pending payment persisted in both the retryer and a ChannelMonitor, or in neither. Note that we may not have anything in a ChannelManager until much later, preventing us from retrying the payment if we crash.

In such a case, we'll force-close the relevant channel (its ChannelMonitor has run ahead of the manager), but the actual HTLC will still be pending, just not present in ChannelManager at all. Ideally, we'd then be able to retry it over a whole new path.

This PR addresses this situation by having ChannelManager examine the available ChannelMonitors at startup, rebuilding pending payment entries on the basis of what's in its ChannelMonitors.

Note that we aren't particularly concerned with the payment resolution end of things - if ChannelManager thinks a payment is still pending when it has been fully resolved, we'll eventually consider it sent and generate a fresh PaymentSent when the ChannelMonitor resolves the current state.

This should fix#1102, but generally needs lots more tests (its not tested at all, aside from a hacky thing at the end to compare the loaded-from-disk and the rebuilt set of payments, which passes most tests).

[1] #1059 (comment)

@TheBlueMattTheBlueMatt added this to the 0.0.102 milestone Oct 4, 2021
@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch from e97930f to a0fe042CompareOctober 5, 2021 05:59
@codecov

codecovBot commented Oct 5, 2021

Copy link
Copy Markdown

Codecov Report

Merging #1104 (7af5d12) into main (107c6c7) will increase coverage by 0.06%.
The diff coverage is 96.03%.

Impacted file tree graph

@@ Coverage Diff @@## main #1104 +/- ##
==========================================
+ Coverage 90.40% 90.47% +0.06% 
==========================================
Files 68 68 Lines 34796 35176 +380 ==========================================
+ Hits 31458 31826 +368 - Misses 3338 3350 +12 
Impacted FilesCoverage Δ
lightning/src/util/ser.rs89.02% <80.00%> (-0.12%)⬇️
lightning/src/ln/channelmanager.rs83.86% <90.06%> (+0.34%)⬆️
lightning/src/ln/channel.rs88.37% <96.22%> (+0.05%)⬆️
lightning/src/chain/channelmonitor.rs91.11% <97.56%> (+0.17%)⬆️
lightning/src/ln/payment_tests.rs99.09% <99.26%> (+0.25%)⬆️
lightning/src/ln/functional_test_utils.rs95.09% <100.00%> (+<0.01%)⬆️
lightning/src/ln/functional_tests.rs97.37% <100.00%> (-0.04%)⬇️
lightning/src/ln/onion_route_tests.rs96.60% <100.00%> (ø)
lightning/src/ln/features.rs99.43% <0.00%> (+<0.01%)⬆️
... and 2 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 107c6c7...7af5d12. Read the comment docs.

@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

This is now based on #1108, which should go first.

@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

Depends on #1109 as well now.

@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch from a0fe042 to f821c93CompareOctober 5, 2021 23:26
@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

Now includes at least a naive test of payment data reloading, which works 🎉

@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch 4 times, most recently from 6e60c4f to a7cea3dCompareOctober 11, 2021 00:53
@TheBlueMattTheBlueMatt modified the milestones: 0.0.102, 0.0.103Oct 15, 2021
@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch from a7cea3d to 7d23578CompareOctober 20, 2021 01:25
@TheBlueMatt
TheBlueMatt marked this pull request as ready for review October 20, 2021 01:25
@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

Rebased on upstream!

@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch from 7d23578 to df8b291CompareOctober 20, 2021 02:54
@jkczyz
jkczyz self-requested a review October 20, 2021 17:56

@valentinewallacevalentinewallace left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Mostly wondering how we handle failures that were previously fulfilled

Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/chain/channelmonitor.rs
Comment threadlightning/src/chain/channelmonitor.rs Outdated
@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch from df8b291 to 42a854cCompareOctober 20, 2021 21:13

@jkczyzjkczyz left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Sorry about any confusion around terminology. I don't have as a firm of grasp as I'd like to around some of this.

Comment on lines +344 to +363
pub(super) struct RAAUpdates {
pub commitment_update: Option<msgs::CommitmentUpdate>,
pub to_forward_htlcs: Vec<(PendingHTLCInfo, u64)>,
pub failed_htlcs: Vec<(HTLCSource, PaymentHash, HTLCFailReason)>,
pub finalized_claim_htlcs: Vec<HTLCSource>,
pub monitor_update: ChannelMonitorUpdate,
pub holding_cell_failed_htlcs: Vec<(HTLCSource, PaymentHash)>,
}

/// The return value of `monitor_updating_resotred`
pub(super) struct MonitorRestoreUpdates {
pub raa: Option<msgs::RevokeAndACK>,
pub commitment_update: Option<msgs::CommitmentUpdate>,
pub order: RAACommitmentOrder,
pub forwards: Vec<(PendingHTLCInfo, u64)>,
pub failures: Vec<(HTLCSource, PaymentHash, HTLCFailReason)>,
pub finalized_claims: Vec<HTLCSource>,
pub funding_broadcastable: Option<Transaction>,
pub funding_locked: Option<msgs::FundingLocked>,
}

@jkczyzjkczyzOct 20, 2021

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Where it makes sense, could we use consistent field names between these two structs?

Comment threadlightning/src/ln/channel.rs Outdated
}
self.monitor_pending_forwards.append(&mut to_forward_infos);
self.monitor_pending_failures.append(&mut revoked_htlcs);
self.monitor_pending_finalized_fulfills.append(&mut finalized_claim_htlcs);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

In terms of terminology, is there a difference between a "fulfill" and a "claim"? Does it depend on the context?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

It mostly just depends on when the code was written :p

commitment_update: Some(commitment_update),
finalized_claim_htlcs,
to_forward_htlcs: to_forward_infos,
failed_htlcs: revoked_htlcs,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Similar question wrt "failed" and "revoked". Just want to make sure I understand if it is context dependent.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

Same answer, there really isn't.

Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment on lines +2018 to +2023
if let hash_map::Entry::Occupied(payment) = &payment_entry {
if !payment.get().is_retryable() {
return Err(APIError::RouteError {
err: "Payment already completed"
});
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It's unclear to me why this shouldn't be if payment.get().is_complete() { as indicated by the error message. Do we want to hit this for legacy payments, too?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

Hmm, yes, we probably should hit this for legacy payments. I mean its unreachable for legacy payments, I believe (retry_payment refuses to call send_payment_internal at all), but nothing wrong with an extra check here.

Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment on lines +419 to +444
/// When a pending payment completes, we continue tracking it until all pendings HTLCs have
/// been resolved. This ensures we don't look up pending payments in ChannelMonitors on restart
/// and add a pending payment that was already completed.
Fulfilled {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Terminology question related to my earlier comment: is there a difference between "completes" and "fulfilled" as it pertains to the first sentence in the docs and the variant name?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

No, I just missed the docs in the rename - once we get one HTLC fulfillment we consider the payment fulfilled.

@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

Pushed a fix for the lockorder comment, leaving other stuff until we settle on naming.

@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch from 2aef42c to ca6ae4dCompareOctober 21, 2021 00:00
Comment threadlightning/src/chain/channelmonitor.rs Outdated
Comment threadlightning/src/chain/channelmonitor.rs Outdated
Comment threadlightning/src/chain/channelmonitor.rs
Comment threadlightning/src/chain/channelmonitor.rs Outdated
Comment threadlightning/src/chain/channelmonitor.rs Outdated
Comment threadlightning/src/chain/channelmonitor.rs Outdated
@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch from ca6ae4d to a050236CompareOctober 21, 2021 19:15
@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

Ok, I believe I've addressed all feedback and renamed things to be more clear.

Comment threadlightning/src/chain/channelmonitor.rs
Comment threadlightning/src/chain/channelmonitor.rs Outdated
Comment threadlightning/src/chain/channelmonitor.rs Outdated
Comment threadlightning/src/ln/channel.rs Outdated
Comment threadlightning/src/ln/channel.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch from a050236 to f47acdbCompareOctober 21, 2021 20:41
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/chain/channelmonitor.rs Outdated
// We only rebuild the pending payments map if we were most recently serialized by
// 0.0.102+
for (_, monitor) in args.channel_monitors {
if by_id.get(&monitor.get_funding_txo().0.to_channel_id()).is_none() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hmm, I don't see why we don't load pending outbound payments for open channels as well. Couldn't we still have a situation where the ChannelManager doesn't get persisted after initiating an outbound payment, but the ChannelMonitor still knows about it?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

The channel monitor knowing about a payment that the ChannelManager does not implies by definition that the ChannelMonitor has some newer state than the ChannelManager. It's possible I missed some edge case but it feels pretty straightforward?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Ah, right 🤦‍♀️

Fwiw, when I add this diff, it gets hit a fair amount of times and all tests pass:

diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs
index af8a65b9..5d7ee3a5 100644
--- a/lightning/src/ln/channelmanager.rs
+++ b/lightning/src/ln/channelmanager.rs
@@ -5939,7 +5939,23 @@ impl<'a, Signer: Sign, M: Deref, T: Deref, K: Deref, F: Deref, L: Deref>
}
}
}
- }
+ } else {
+ for (htlc_source, htlc) in monitor.get_pending_outbound_htlcs() {
+ if let HTLCSource::OutboundRoute { payment_id, session_priv, path, payment_secret, .. } = htlc_source {
+ let path_amt = path.last().unwrap().fee_msat;
+ let mut session_priv_bytes = [0; 32];
+ session_priv_bytes[..].copy_from_slice(&session_priv[..]);
+ match pending_outbound_payments.as_mut().unwrap().entry(payment_id) {
+ hash_map::Entry::Occupied(mut entry) => {
+ assert!(!entry.get_mut().insert(session_priv_bytes, path_amt));
+ },
+ hash_map::Entry::Vacant(entry) => {
+ panic!("no entry for this payment");
+ }
+ }
+ }
+ }
+}
}
}

which is reassuring as well

@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch from f47acdb to 4687624CompareOctober 21, 2021 23:14

@valentinewallacevalentinewallace left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Think I'm ACK mod outstanding feedback. Might give it one more glance in the morning. Thanks for the additional test coverage!

Comment threadlightning/src/ln/channelmanager.rs Outdated
// We only rebuild the pending payments map if we were most recently serialized by
// 0.0.102+
for (_, monitor) in args.channel_monitors {
if by_id.get(&monitor.get_funding_txo().0.to_channel_id()).is_none() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Ah, right 🤦‍♀️

Fwiw, when I add this diff, it gets hit a fair amount of times and all tests pass:

diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs
index af8a65b9..5d7ee3a5 100644
--- a/lightning/src/ln/channelmanager.rs
+++ b/lightning/src/ln/channelmanager.rs
@@ -5939,7 +5939,23 @@ impl<'a, Signer: Sign, M: Deref, T: Deref, K: Deref, F: Deref, L: Deref>
}
}
}
- }
+ } else {
+ for (htlc_source, htlc) in monitor.get_pending_outbound_htlcs() {
+ if let HTLCSource::OutboundRoute { payment_id, session_priv, path, payment_secret, .. } = htlc_source {
+ let path_amt = path.last().unwrap().fee_msat;
+ let mut session_priv_bytes = [0; 32];
+ session_priv_bytes[..].copy_from_slice(&session_priv[..]);
+ match pending_outbound_payments.as_mut().unwrap().entry(payment_id) {
+ hash_map::Entry::Occupied(mut entry) => {
+ assert!(!entry.get_mut().insert(session_priv_bytes, path_amt));
+ },
+ hash_map::Entry::Vacant(entry) => {
+ panic!("no entry for this payment");
+ }
+ }
+ }
+ }
+}
}
}

which is reassuring as well

Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/payment_tests.rs Outdated
}

#[test]
fn retry_with_no_persist() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hmm, the new version of this test passes with the code that had the find_map bug

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

Ah! Yep, added even more coverage :)

@jkczyzjkczyz left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Also pretty close on an ACK after remaining feedback is addressed.

@valentinewallace Nice work on finding the test gaps. I was staring at retry_with_no_persist trying to figure out if the bug was being exercised. Mutation testing FTW lol.

I wonder if there's any improvements we can make to our testing strategy to make reviewing a bit easier. Some of the tests are pretty lengthy/verbose but I'm not sure if they can be broken up into smaller tests and still exercise the behavior.

/// monitor update failure must *not* have been sent to the remote end, and must instead
/// have been dropped. They will be regenerated when monitor_updating_restored is called.
pub fn monitor_update_failed(&mut self, resend_raa: bool, resend_commitment: bool, mut pending_forwards: Vec<(PendingHTLCInfo, u64)>, mut pending_fails: Vec<(HTLCSource, PaymentHash, HTLCFailReason)>) {
pub fn monitor_update_failed(&mut self, resend_raa: bool, resend_commitment: bool,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: move these parameters down a line, aligned with the other parameters

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

For a small function that doesn't seem to look any better to my eye? Feels very "rustfmt wants your code to use as many lines as possible" to me :).

Comment threadlightning/src/ln/functional_tests.rs Outdated
Comment on lines +642 to +686
// Now reload nodes[1]...
persister = test_utils::TestPersister::new();
let keys_manager = &chanmon_cfgs[1].keys_manager;
new_chain_monitor = test_utils::TestChainMonitor::new(Some(nodes[1].chain_source), nodes[1].tx_broadcaster.clone(), nodes[1].logger, node_cfgs[1].fee_estimator, &persister, keys_manager);
nodes[1].chain_monitor = &new_chain_monitor;
let mut chan_0_monitor_read = &chan_0_monitor_serialized.0[..];
let (_, mut chan_0_monitor) = <(BlockHash, ChannelMonitor<EnforcingSigner>)>::read(
&mut chan_0_monitor_read, keys_manager).unwrap();
assert!(chan_0_monitor_read.is_empty());

let (_, nodes_1_deserialized_tmp) = {
let mut channel_monitors = HashMap::new();
channel_monitors.insert(chan_0_monitor.get_funding_txo().0, &mut chan_0_monitor);
<(BlockHash, ChannelManager<EnforcingSigner, &test_utils::TestChainMonitor, &test_utils::TestBroadcaster, &test_utils::TestKeysInterface, &test_utils::TestFeeEstimator, &test_utils::TestLogger>)>
::read(&mut io::Cursor::new(&chan_manager_serialized.0[..]), ChannelManagerReadArgs {
default_config: Default::default(),
keys_manager,
fee_estimator: node_cfgs[1].fee_estimator,
chain_monitor: nodes[1].chain_monitor,
tx_broadcaster: nodes[1].tx_broadcaster.clone(),
logger: nodes[1].logger,
channel_monitors,
}).unwrap()
};
nodes_1_deserialized = nodes_1_deserialized_tmp;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is pretty verbose and looks repeated from retry_with_no_persist. Wondering if we could make a utility function for reloading so the tests are easier to read.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

Yea, its repeated in quite a few tests. I believe we could may be macro it, but it requires a few variables declared at top of function scope. I'll look at it as a followup, its not a new problem.

Comment threadlightning/src/ln/channelmanager.rs Outdated
@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch from 4687624 to 4348833CompareOctober 22, 2021 05:24

@valentinewallacevalentinewallace left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM 🚀

Comment threadlightning/src/ln/payment_tests.rs
@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch 2 times, most recently from b67f15c to d9a85b3CompareOctober 22, 2021 18:06
@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

Pushed yet more test coverage, I believe all outstanding comments have been addressed.

@valentinewallacevalentinewallace left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ACK after squash

@jkczyzjkczyz left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This substantially improves readability at the callsite and in the
function.
This improves readability at the callsite and in the function.
This allows us to read a `HashMap` that has values which may be
skipped if they are some backwards-compatibility type.
We also take this opportunity to fail deserialization if keys are
duplicated.
When an HTLC has been failed, we track it up until the point there
exists no broadcastable commitment transaction which has the HTLC
present, at which point Channel returns the HTLCSource back to the
ChannelManager, which fails the HTLC backwards appropriately.
When an HTLC is fulfilled, however, we fulfill on the backwards path
immediately. This is great for claiming upstream HTLCs, but when we
want to track pending payments, we need to ensure we can check with
ChannelMonitor data to rebuild pending payments. In order to do so,
we need an event similar to the HTLC failure event, but for
fulfills instead.
Specifically, if we force-close a channel, we remove its off-chain
`Channel` object entirely, at which point, on reload, we may notice
HTLC(s) which are not present in our pending payments map (as they
may have received a payment preimage, but not fully committed to
it). Thus, we'd conclude we still have a retryable payment, which
is untrue.
This commit does so, informing the ChannelManager via a new return
element where appropriate of the HTLCSource corresponding to the
failed HTLC.
In the next commit, we will reload lost pending payments from
ChannelMonitors during restart. However, in order to avoid
re-adding pending payments which have already been fulfilled, we
must ensure that we do not fully remove pending payments until all
HTLCs for the payment have been fully removed from their
ChannelMonitors.
We do so here, introducing a new PendingOutboundPayment variant
called `Completed` which only tracks the set of pending HTLCs.
If we go to send a payment, add the HTLC(s) to the channel(s),
commit the ChannelMonitor updates to disk, and then crash, we'll
come back up with no pending payments but HTLC(s) ready to be
claim/failed.
This makes it rather impractical to write a payment sender/retryer,
as you cannot guarantee atomicity - you cannot guarantee you'll
have retry data persisted even if the HTLC(s) are actually pending.
Because ChannelMonitors are *the* atomically-persisted data in LDK,
we lean on their current HTLC data to figure out what HTLC(s) are a
part of an outbound payment, rebuilding the pending payments list
on reload.
test_dup_htlc_onchain_fails_on_reload is now more of a
payment_test than a functional_test, testing for handling of
pending payments.
Peers probably shouldn't do this, but if they want to give us free
money, we should take it and not generate any spurious events.
@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch from d9a85b3 to 7af5d12CompareOctober 22, 2021 18:41
@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

Squashed without diff, will land after CI 🎉

$ git diff-tree -U1 d9a85b35b 7af5d127a
$

@TheBlueMatt
TheBlueMatt merged commit 0a31c12 into lightningdevkit:mainOct 22, 2021
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Generate PaymentPathFailed for MonitorUpdate-sourced HTLC failures when we don't have a corresponding pending payment entry

3 participants

@TheBlueMatt@jkczyz@valentinewallace
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' Reload pending outbound payments from ChannelMonitors on startup by TheBlueMatt · Pull Request #1104 · lightningdevkit/rust-lightning · GitHub
Skip to content

Reload pending outbound payments from ChannelMonitors on startup - #1104

Merged
TheBlueMatt merged 11 commits into
lightningdevkit:mainfrom
TheBlueMatt:2021-10-payment-id-in-monitors
Oct 22, 2021
Merged

Reload pending outbound payments from ChannelMonitors on startup#1104
TheBlueMatt merged 11 commits into
lightningdevkit:mainfrom
TheBlueMatt:2021-10-payment-id-in-monitors

Conversation

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

When we add payment retries, we need some kind of on-disk consistency story. I think a compelling one is described, roughly at [1] - make the retryer persist first, before calling into the ChannelManager, then when reloading we either have a pending payment there or its pending nowhere. Once the send_payment method returns, we'll either have a pending payment persisted in both the retryer and a ChannelMonitor, or in neither. Note that we may not have anything in a ChannelManager until much later, preventing us from retrying the payment if we crash.

In such a case, we'll force-close the relevant channel (its ChannelMonitor has run ahead of the manager), but the actual HTLC will still be pending, just not present in ChannelManager at all. Ideally, we'd then be able to retry it over a whole new path.

This PR addresses this situation by having ChannelManager examine the available ChannelMonitors at startup, rebuilding pending payment entries on the basis of what's in its ChannelMonitors.

Note that we aren't particularly concerned with the payment resolution end of things - if ChannelManager thinks a payment is still pending when it has been fully resolved, we'll eventually consider it sent and generate a fresh PaymentSent when the ChannelMonitor resolves the current state.

This should fix#1102, but generally needs lots more tests (its not tested at all, aside from a hacky thing at the end to compare the loaded-from-disk and the rebuilt set of payments, which passes most tests).

[1] #1059 (comment)

@TheBlueMattTheBlueMatt added this to the 0.0.102 milestone Oct 4, 2021
@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch from e97930f to a0fe042CompareOctober 5, 2021 05:59
@codecov

codecovBot commented Oct 5, 2021

Copy link
Copy Markdown

Codecov Report

Merging #1104 (7af5d12) into main (107c6c7) will increase coverage by 0.06%.
The diff coverage is 96.03%.

Impacted file tree graph

@@ Coverage Diff @@## main #1104 +/- ##
==========================================
+ Coverage 90.40% 90.47% +0.06% 
==========================================
Files 68 68 Lines 34796 35176 +380 ==========================================
+ Hits 31458 31826 +368 - Misses 3338 3350 +12 
Impacted FilesCoverage Δ
lightning/src/util/ser.rs89.02% <80.00%> (-0.12%)⬇️
lightning/src/ln/channelmanager.rs83.86% <90.06%> (+0.34%)⬆️
lightning/src/ln/channel.rs88.37% <96.22%> (+0.05%)⬆️
lightning/src/chain/channelmonitor.rs91.11% <97.56%> (+0.17%)⬆️
lightning/src/ln/payment_tests.rs99.09% <99.26%> (+0.25%)⬆️
lightning/src/ln/functional_test_utils.rs95.09% <100.00%> (+<0.01%)⬆️
lightning/src/ln/functional_tests.rs97.37% <100.00%> (-0.04%)⬇️
lightning/src/ln/onion_route_tests.rs96.60% <100.00%> (ø)
lightning/src/ln/features.rs99.43% <0.00%> (+<0.01%)⬆️
... and 2 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 107c6c7...7af5d12. Read the comment docs.

@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

This is now based on #1108, which should go first.

@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

Depends on #1109 as well now.

@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch from a0fe042 to f821c93CompareOctober 5, 2021 23:26
@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

Now includes at least a naive test of payment data reloading, which works 🎉

@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch 4 times, most recently from 6e60c4f to a7cea3dCompareOctober 11, 2021 00:53
@TheBlueMattTheBlueMatt modified the milestones: 0.0.102, 0.0.103Oct 15, 2021
@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch from a7cea3d to 7d23578CompareOctober 20, 2021 01:25
@TheBlueMatt
TheBlueMatt marked this pull request as ready for review October 20, 2021 01:25
@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

Rebased on upstream!

@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch from 7d23578 to df8b291CompareOctober 20, 2021 02:54
@jkczyz
jkczyz self-requested a review October 20, 2021 17:56

@valentinewallacevalentinewallace left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Mostly wondering how we handle failures that were previously fulfilled

Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/chain/channelmonitor.rs
Comment threadlightning/src/chain/channelmonitor.rs Outdated
@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch from df8b291 to 42a854cCompareOctober 20, 2021 21:13

@jkczyzjkczyz left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Sorry about any confusion around terminology. I don't have as a firm of grasp as I'd like to around some of this.

Comment on lines +344 to +363
pub(super) struct RAAUpdates {
pub commitment_update: Option<msgs::CommitmentUpdate>,
pub to_forward_htlcs: Vec<(PendingHTLCInfo, u64)>,
pub failed_htlcs: Vec<(HTLCSource, PaymentHash, HTLCFailReason)>,
pub finalized_claim_htlcs: Vec<HTLCSource>,
pub monitor_update: ChannelMonitorUpdate,
pub holding_cell_failed_htlcs: Vec<(HTLCSource, PaymentHash)>,
}

/// The return value of `monitor_updating_resotred`
pub(super) struct MonitorRestoreUpdates {
pub raa: Option<msgs::RevokeAndACK>,
pub commitment_update: Option<msgs::CommitmentUpdate>,
pub order: RAACommitmentOrder,
pub forwards: Vec<(PendingHTLCInfo, u64)>,
pub failures: Vec<(HTLCSource, PaymentHash, HTLCFailReason)>,
pub finalized_claims: Vec<HTLCSource>,
pub funding_broadcastable: Option<Transaction>,
pub funding_locked: Option<msgs::FundingLocked>,
}

@jkczyzjkczyzOct 20, 2021

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Where it makes sense, could we use consistent field names between these two structs?

Comment threadlightning/src/ln/channel.rs Outdated
}
self.monitor_pending_forwards.append(&mut to_forward_infos);
self.monitor_pending_failures.append(&mut revoked_htlcs);
self.monitor_pending_finalized_fulfills.append(&mut finalized_claim_htlcs);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

In terms of terminology, is there a difference between a "fulfill" and a "claim"? Does it depend on the context?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

It mostly just depends on when the code was written :p

commitment_update: Some(commitment_update),
finalized_claim_htlcs,
to_forward_htlcs: to_forward_infos,
failed_htlcs: revoked_htlcs,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Similar question wrt "failed" and "revoked". Just want to make sure I understand if it is context dependent.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

Same answer, there really isn't.

Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment on lines +2018 to +2023
if let hash_map::Entry::Occupied(payment) = &payment_entry {
if !payment.get().is_retryable() {
return Err(APIError::RouteError {
err: "Payment already completed"
});
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It's unclear to me why this shouldn't be if payment.get().is_complete() { as indicated by the error message. Do we want to hit this for legacy payments, too?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

Hmm, yes, we probably should hit this for legacy payments. I mean its unreachable for legacy payments, I believe (retry_payment refuses to call send_payment_internal at all), but nothing wrong with an extra check here.

Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment on lines +419 to +444
/// When a pending payment completes, we continue tracking it until all pendings HTLCs have
/// been resolved. This ensures we don't look up pending payments in ChannelMonitors on restart
/// and add a pending payment that was already completed.
Fulfilled {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Terminology question related to my earlier comment: is there a difference between "completes" and "fulfilled" as it pertains to the first sentence in the docs and the variant name?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

No, I just missed the docs in the rename - once we get one HTLC fulfillment we consider the payment fulfilled.

@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

Pushed a fix for the lockorder comment, leaving other stuff until we settle on naming.

@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch from 2aef42c to ca6ae4dCompareOctober 21, 2021 00:00
Comment threadlightning/src/chain/channelmonitor.rs Outdated
Comment threadlightning/src/chain/channelmonitor.rs Outdated
Comment threadlightning/src/chain/channelmonitor.rs
Comment threadlightning/src/chain/channelmonitor.rs Outdated
Comment threadlightning/src/chain/channelmonitor.rs Outdated
Comment threadlightning/src/chain/channelmonitor.rs Outdated
@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch from ca6ae4d to a050236CompareOctober 21, 2021 19:15
@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

Ok, I believe I've addressed all feedback and renamed things to be more clear.

Comment threadlightning/src/chain/channelmonitor.rs
Comment threadlightning/src/chain/channelmonitor.rs Outdated
Comment threadlightning/src/chain/channelmonitor.rs Outdated
Comment threadlightning/src/ln/channel.rs Outdated
Comment threadlightning/src/ln/channel.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch from a050236 to f47acdbCompareOctober 21, 2021 20:41
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/chain/channelmonitor.rs Outdated
// We only rebuild the pending payments map if we were most recently serialized by
// 0.0.102+
for (_, monitor) in args.channel_monitors {
if by_id.get(&monitor.get_funding_txo().0.to_channel_id()).is_none() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hmm, I don't see why we don't load pending outbound payments for open channels as well. Couldn't we still have a situation where the ChannelManager doesn't get persisted after initiating an outbound payment, but the ChannelMonitor still knows about it?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

The channel monitor knowing about a payment that the ChannelManager does not implies by definition that the ChannelMonitor has some newer state than the ChannelManager. It's possible I missed some edge case but it feels pretty straightforward?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Ah, right 🤦‍♀️

Fwiw, when I add this diff, it gets hit a fair amount of times and all tests pass:

diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs
index af8a65b9..5d7ee3a5 100644
--- a/lightning/src/ln/channelmanager.rs
+++ b/lightning/src/ln/channelmanager.rs
@@ -5939,7 +5939,23 @@ impl<'a, Signer: Sign, M: Deref, T: Deref, K: Deref, F: Deref, L: Deref>
}
}
}
- }
+ } else {
+ for (htlc_source, htlc) in monitor.get_pending_outbound_htlcs() {
+ if let HTLCSource::OutboundRoute { payment_id, session_priv, path, payment_secret, .. } = htlc_source {
+ let path_amt = path.last().unwrap().fee_msat;
+ let mut session_priv_bytes = [0; 32];
+ session_priv_bytes[..].copy_from_slice(&session_priv[..]);
+ match pending_outbound_payments.as_mut().unwrap().entry(payment_id) {
+ hash_map::Entry::Occupied(mut entry) => {
+ assert!(!entry.get_mut().insert(session_priv_bytes, path_amt));
+ },
+ hash_map::Entry::Vacant(entry) => {
+ panic!("no entry for this payment");
+ }
+ }
+ }
+ }
+}
}
}

which is reassuring as well

@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch from f47acdb to 4687624CompareOctober 21, 2021 23:14

@valentinewallacevalentinewallace left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Think I'm ACK mod outstanding feedback. Might give it one more glance in the morning. Thanks for the additional test coverage!

Comment threadlightning/src/ln/channelmanager.rs Outdated
// We only rebuild the pending payments map if we were most recently serialized by
// 0.0.102+
for (_, monitor) in args.channel_monitors {
if by_id.get(&monitor.get_funding_txo().0.to_channel_id()).is_none() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Ah, right 🤦‍♀️

Fwiw, when I add this diff, it gets hit a fair amount of times and all tests pass:

diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs
index af8a65b9..5d7ee3a5 100644
--- a/lightning/src/ln/channelmanager.rs
+++ b/lightning/src/ln/channelmanager.rs
@@ -5939,7 +5939,23 @@ impl<'a, Signer: Sign, M: Deref, T: Deref, K: Deref, F: Deref, L: Deref>
}
}
}
- }
+ } else {
+ for (htlc_source, htlc) in monitor.get_pending_outbound_htlcs() {
+ if let HTLCSource::OutboundRoute { payment_id, session_priv, path, payment_secret, .. } = htlc_source {
+ let path_amt = path.last().unwrap().fee_msat;
+ let mut session_priv_bytes = [0; 32];
+ session_priv_bytes[..].copy_from_slice(&session_priv[..]);
+ match pending_outbound_payments.as_mut().unwrap().entry(payment_id) {
+ hash_map::Entry::Occupied(mut entry) => {
+ assert!(!entry.get_mut().insert(session_priv_bytes, path_amt));
+ },
+ hash_map::Entry::Vacant(entry) => {
+ panic!("no entry for this payment");
+ }
+ }
+ }
+ }
+}
}
}

which is reassuring as well

Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/payment_tests.rs Outdated
}

#[test]
fn retry_with_no_persist() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hmm, the new version of this test passes with the code that had the find_map bug

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

Ah! Yep, added even more coverage :)

@jkczyzjkczyz left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Also pretty close on an ACK after remaining feedback is addressed.

@valentinewallace Nice work on finding the test gaps. I was staring at retry_with_no_persist trying to figure out if the bug was being exercised. Mutation testing FTW lol.

I wonder if there's any improvements we can make to our testing strategy to make reviewing a bit easier. Some of the tests are pretty lengthy/verbose but I'm not sure if they can be broken up into smaller tests and still exercise the behavior.

/// monitor update failure must *not* have been sent to the remote end, and must instead
/// have been dropped. They will be regenerated when monitor_updating_restored is called.
pub fn monitor_update_failed(&mut self, resend_raa: bool, resend_commitment: bool, mut pending_forwards: Vec<(PendingHTLCInfo, u64)>, mut pending_fails: Vec<(HTLCSource, PaymentHash, HTLCFailReason)>) {
pub fn monitor_update_failed(&mut self, resend_raa: bool, resend_commitment: bool,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: move these parameters down a line, aligned with the other parameters

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

For a small function that doesn't seem to look any better to my eye? Feels very "rustfmt wants your code to use as many lines as possible" to me :).

Comment threadlightning/src/ln/functional_tests.rs Outdated
Comment on lines +642 to +686
// Now reload nodes[1]...
persister = test_utils::TestPersister::new();
let keys_manager = &chanmon_cfgs[1].keys_manager;
new_chain_monitor = test_utils::TestChainMonitor::new(Some(nodes[1].chain_source), nodes[1].tx_broadcaster.clone(), nodes[1].logger, node_cfgs[1].fee_estimator, &persister, keys_manager);
nodes[1].chain_monitor = &new_chain_monitor;
let mut chan_0_monitor_read = &chan_0_monitor_serialized.0[..];
let (_, mut chan_0_monitor) = <(BlockHash, ChannelMonitor<EnforcingSigner>)>::read(
&mut chan_0_monitor_read, keys_manager).unwrap();
assert!(chan_0_monitor_read.is_empty());

let (_, nodes_1_deserialized_tmp) = {
let mut channel_monitors = HashMap::new();
channel_monitors.insert(chan_0_monitor.get_funding_txo().0, &mut chan_0_monitor);
<(BlockHash, ChannelManager<EnforcingSigner, &test_utils::TestChainMonitor, &test_utils::TestBroadcaster, &test_utils::TestKeysInterface, &test_utils::TestFeeEstimator, &test_utils::TestLogger>)>
::read(&mut io::Cursor::new(&chan_manager_serialized.0[..]), ChannelManagerReadArgs {
default_config: Default::default(),
keys_manager,
fee_estimator: node_cfgs[1].fee_estimator,
chain_monitor: nodes[1].chain_monitor,
tx_broadcaster: nodes[1].tx_broadcaster.clone(),
logger: nodes[1].logger,
channel_monitors,
}).unwrap()
};
nodes_1_deserialized = nodes_1_deserialized_tmp;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is pretty verbose and looks repeated from retry_with_no_persist. Wondering if we could make a utility function for reloading so the tests are easier to read.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

Yea, its repeated in quite a few tests. I believe we could may be macro it, but it requires a few variables declared at top of function scope. I'll look at it as a followup, its not a new problem.

Comment threadlightning/src/ln/channelmanager.rs Outdated
@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch from 4687624 to 4348833CompareOctober 22, 2021 05:24

@valentinewallacevalentinewallace left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM 🚀

Comment threadlightning/src/ln/payment_tests.rs
@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch 2 times, most recently from b67f15c to d9a85b3CompareOctober 22, 2021 18:06
@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

Pushed yet more test coverage, I believe all outstanding comments have been addressed.

@valentinewallacevalentinewallace left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ACK after squash

@jkczyzjkczyz left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This substantially improves readability at the callsite and in the
function.
This improves readability at the callsite and in the function.
This allows us to read a `HashMap` that has values which may be
skipped if they are some backwards-compatibility type.
We also take this opportunity to fail deserialization if keys are
duplicated.
When an HTLC has been failed, we track it up until the point there
exists no broadcastable commitment transaction which has the HTLC
present, at which point Channel returns the HTLCSource back to the
ChannelManager, which fails the HTLC backwards appropriately.
When an HTLC is fulfilled, however, we fulfill on the backwards path
immediately. This is great for claiming upstream HTLCs, but when we
want to track pending payments, we need to ensure we can check with
ChannelMonitor data to rebuild pending payments. In order to do so,
we need an event similar to the HTLC failure event, but for
fulfills instead.
Specifically, if we force-close a channel, we remove its off-chain
`Channel` object entirely, at which point, on reload, we may notice
HTLC(s) which are not present in our pending payments map (as they
may have received a payment preimage, but not fully committed to
it). Thus, we'd conclude we still have a retryable payment, which
is untrue.
This commit does so, informing the ChannelManager via a new return
element where appropriate of the HTLCSource corresponding to the
failed HTLC.
In the next commit, we will reload lost pending payments from
ChannelMonitors during restart. However, in order to avoid
re-adding pending payments which have already been fulfilled, we
must ensure that we do not fully remove pending payments until all
HTLCs for the payment have been fully removed from their
ChannelMonitors.
We do so here, introducing a new PendingOutboundPayment variant
called `Completed` which only tracks the set of pending HTLCs.
If we go to send a payment, add the HTLC(s) to the channel(s),
commit the ChannelMonitor updates to disk, and then crash, we'll
come back up with no pending payments but HTLC(s) ready to be
claim/failed.
This makes it rather impractical to write a payment sender/retryer,
as you cannot guarantee atomicity - you cannot guarantee you'll
have retry data persisted even if the HTLC(s) are actually pending.
Because ChannelMonitors are *the* atomically-persisted data in LDK,
we lean on their current HTLC data to figure out what HTLC(s) are a
part of an outbound payment, rebuilding the pending payments list
on reload.
test_dup_htlc_onchain_fails_on_reload is now more of a
payment_test than a functional_test, testing for handling of
pending payments.
Peers probably shouldn't do this, but if they want to give us free
money, we should take it and not generate any spurious events.
@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch from d9a85b3 to 7af5d12CompareOctober 22, 2021 18:41
@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

Squashed without diff, will land after CI 🎉

$ git diff-tree -U1 d9a85b35b 7af5d127a
$

@TheBlueMatt
TheBlueMatt merged commit 0a31c12 into lightningdevkit:mainOct 22, 2021
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Generate PaymentPathFailed for MonitorUpdate-sourced HTLC failures when we don't have a corresponding pending payment entry

3 participants

@TheBlueMatt@jkczyz@valentinewallace
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Reload pending outbound payments from ChannelMonitors on startup by TheBlueMatt · Pull Request #1104 · lightningdevkit/rust-lightning · GitHub
Skip to content

Reload pending outbound payments from ChannelMonitors on startup - #1104

Merged
TheBlueMatt merged 11 commits into
lightningdevkit:mainfrom
TheBlueMatt:2021-10-payment-id-in-monitors
Oct 22, 2021
Merged

Reload pending outbound payments from ChannelMonitors on startup#1104
TheBlueMatt merged 11 commits into
lightningdevkit:mainfrom
TheBlueMatt:2021-10-payment-id-in-monitors

Conversation

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

When we add payment retries, we need some kind of on-disk consistency story. I think a compelling one is described, roughly at [1] - make the retryer persist first, before calling into the ChannelManager, then when reloading we either have a pending payment there or its pending nowhere. Once the send_payment method returns, we'll either have a pending payment persisted in both the retryer and a ChannelMonitor, or in neither. Note that we may not have anything in a ChannelManager until much later, preventing us from retrying the payment if we crash.

In such a case, we'll force-close the relevant channel (its ChannelMonitor has run ahead of the manager), but the actual HTLC will still be pending, just not present in ChannelManager at all. Ideally, we'd then be able to retry it over a whole new path.

This PR addresses this situation by having ChannelManager examine the available ChannelMonitors at startup, rebuilding pending payment entries on the basis of what's in its ChannelMonitors.

Note that we aren't particularly concerned with the payment resolution end of things - if ChannelManager thinks a payment is still pending when it has been fully resolved, we'll eventually consider it sent and generate a fresh PaymentSent when the ChannelMonitor resolves the current state.

This should fix#1102, but generally needs lots more tests (its not tested at all, aside from a hacky thing at the end to compare the loaded-from-disk and the rebuilt set of payments, which passes most tests).

[1] #1059 (comment)

@TheBlueMattTheBlueMatt added this to the 0.0.102 milestone Oct 4, 2021
@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch from e97930f to a0fe042CompareOctober 5, 2021 05:59
@codecov

codecovBot commented Oct 5, 2021

Copy link
Copy Markdown

Codecov Report

Merging #1104 (7af5d12) into main (107c6c7) will increase coverage by 0.06%.
The diff coverage is 96.03%.

Impacted file tree graph

@@ Coverage Diff @@## main #1104 +/- ##
==========================================
+ Coverage 90.40% 90.47% +0.06% 
==========================================
Files 68 68 Lines 34796 35176 +380 ==========================================
+ Hits 31458 31826 +368 - Misses 3338 3350 +12 
Impacted FilesCoverage Δ
lightning/src/util/ser.rs89.02% <80.00%> (-0.12%)⬇️
lightning/src/ln/channelmanager.rs83.86% <90.06%> (+0.34%)⬆️
lightning/src/ln/channel.rs88.37% <96.22%> (+0.05%)⬆️
lightning/src/chain/channelmonitor.rs91.11% <97.56%> (+0.17%)⬆️
lightning/src/ln/payment_tests.rs99.09% <99.26%> (+0.25%)⬆️
lightning/src/ln/functional_test_utils.rs95.09% <100.00%> (+<0.01%)⬆️
lightning/src/ln/functional_tests.rs97.37% <100.00%> (-0.04%)⬇️
lightning/src/ln/onion_route_tests.rs96.60% <100.00%> (ø)
lightning/src/ln/features.rs99.43% <0.00%> (+<0.01%)⬆️
... and 2 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 107c6c7...7af5d12. Read the comment docs.

@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

This is now based on #1108, which should go first.

@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

Depends on #1109 as well now.

@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch from a0fe042 to f821c93CompareOctober 5, 2021 23:26
@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

Now includes at least a naive test of payment data reloading, which works 🎉

@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch 4 times, most recently from 6e60c4f to a7cea3dCompareOctober 11, 2021 00:53
@TheBlueMattTheBlueMatt modified the milestones: 0.0.102, 0.0.103Oct 15, 2021
@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch from a7cea3d to 7d23578CompareOctober 20, 2021 01:25
@TheBlueMatt
TheBlueMatt marked this pull request as ready for review October 20, 2021 01:25
@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

Rebased on upstream!

@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch from 7d23578 to df8b291CompareOctober 20, 2021 02:54
@jkczyz
jkczyz self-requested a review October 20, 2021 17:56

@valentinewallacevalentinewallace left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Mostly wondering how we handle failures that were previously fulfilled

Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/chain/channelmonitor.rs
Comment threadlightning/src/chain/channelmonitor.rs Outdated
@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch from df8b291 to 42a854cCompareOctober 20, 2021 21:13

@jkczyzjkczyz left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Sorry about any confusion around terminology. I don't have as a firm of grasp as I'd like to around some of this.

Comment on lines +344 to +363
pub(super) struct RAAUpdates {
pub commitment_update: Option<msgs::CommitmentUpdate>,
pub to_forward_htlcs: Vec<(PendingHTLCInfo, u64)>,
pub failed_htlcs: Vec<(HTLCSource, PaymentHash, HTLCFailReason)>,
pub finalized_claim_htlcs: Vec<HTLCSource>,
pub monitor_update: ChannelMonitorUpdate,
pub holding_cell_failed_htlcs: Vec<(HTLCSource, PaymentHash)>,
}

/// The return value of `monitor_updating_resotred`
pub(super) struct MonitorRestoreUpdates {
pub raa: Option<msgs::RevokeAndACK>,
pub commitment_update: Option<msgs::CommitmentUpdate>,
pub order: RAACommitmentOrder,
pub forwards: Vec<(PendingHTLCInfo, u64)>,
pub failures: Vec<(HTLCSource, PaymentHash, HTLCFailReason)>,
pub finalized_claims: Vec<HTLCSource>,
pub funding_broadcastable: Option<Transaction>,
pub funding_locked: Option<msgs::FundingLocked>,
}

@jkczyzjkczyzOct 20, 2021

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Where it makes sense, could we use consistent field names between these two structs?

Comment threadlightning/src/ln/channel.rs Outdated
}
self.monitor_pending_forwards.append(&mut to_forward_infos);
self.monitor_pending_failures.append(&mut revoked_htlcs);
self.monitor_pending_finalized_fulfills.append(&mut finalized_claim_htlcs);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

In terms of terminology, is there a difference between a "fulfill" and a "claim"? Does it depend on the context?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

It mostly just depends on when the code was written :p

commitment_update: Some(commitment_update),
finalized_claim_htlcs,
to_forward_htlcs: to_forward_infos,
failed_htlcs: revoked_htlcs,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Similar question wrt "failed" and "revoked". Just want to make sure I understand if it is context dependent.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

Same answer, there really isn't.

Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment on lines +2018 to +2023
if let hash_map::Entry::Occupied(payment) = &payment_entry {
if !payment.get().is_retryable() {
return Err(APIError::RouteError {
err: "Payment already completed"
});
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It's unclear to me why this shouldn't be if payment.get().is_complete() { as indicated by the error message. Do we want to hit this for legacy payments, too?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

Hmm, yes, we probably should hit this for legacy payments. I mean its unreachable for legacy payments, I believe (retry_payment refuses to call send_payment_internal at all), but nothing wrong with an extra check here.

Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment on lines +419 to +444
/// When a pending payment completes, we continue tracking it until all pendings HTLCs have
/// been resolved. This ensures we don't look up pending payments in ChannelMonitors on restart
/// and add a pending payment that was already completed.
Fulfilled {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Terminology question related to my earlier comment: is there a difference between "completes" and "fulfilled" as it pertains to the first sentence in the docs and the variant name?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

No, I just missed the docs in the rename - once we get one HTLC fulfillment we consider the payment fulfilled.

@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

Pushed a fix for the lockorder comment, leaving other stuff until we settle on naming.

@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch from 2aef42c to ca6ae4dCompareOctober 21, 2021 00:00
Comment threadlightning/src/chain/channelmonitor.rs Outdated
Comment threadlightning/src/chain/channelmonitor.rs Outdated
Comment threadlightning/src/chain/channelmonitor.rs
Comment threadlightning/src/chain/channelmonitor.rs Outdated
Comment threadlightning/src/chain/channelmonitor.rs Outdated
Comment threadlightning/src/chain/channelmonitor.rs Outdated
@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch from ca6ae4d to a050236CompareOctober 21, 2021 19:15
@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

Ok, I believe I've addressed all feedback and renamed things to be more clear.

Comment threadlightning/src/chain/channelmonitor.rs
Comment threadlightning/src/chain/channelmonitor.rs Outdated
Comment threadlightning/src/chain/channelmonitor.rs Outdated
Comment threadlightning/src/ln/channel.rs Outdated
Comment threadlightning/src/ln/channel.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch from a050236 to f47acdbCompareOctober 21, 2021 20:41
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/chain/channelmonitor.rs Outdated
// We only rebuild the pending payments map if we were most recently serialized by
// 0.0.102+
for (_, monitor) in args.channel_monitors {
if by_id.get(&monitor.get_funding_txo().0.to_channel_id()).is_none() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hmm, I don't see why we don't load pending outbound payments for open channels as well. Couldn't we still have a situation where the ChannelManager doesn't get persisted after initiating an outbound payment, but the ChannelMonitor still knows about it?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

The channel monitor knowing about a payment that the ChannelManager does not implies by definition that the ChannelMonitor has some newer state than the ChannelManager. It's possible I missed some edge case but it feels pretty straightforward?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Ah, right 🤦‍♀️

Fwiw, when I add this diff, it gets hit a fair amount of times and all tests pass:

diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs
index af8a65b9..5d7ee3a5 100644
--- a/lightning/src/ln/channelmanager.rs
+++ b/lightning/src/ln/channelmanager.rs
@@ -5939,7 +5939,23 @@ impl<'a, Signer: Sign, M: Deref, T: Deref, K: Deref, F: Deref, L: Deref>
}
}
}
- }
+ } else {
+ for (htlc_source, htlc) in monitor.get_pending_outbound_htlcs() {
+ if let HTLCSource::OutboundRoute { payment_id, session_priv, path, payment_secret, .. } = htlc_source {
+ let path_amt = path.last().unwrap().fee_msat;
+ let mut session_priv_bytes = [0; 32];
+ session_priv_bytes[..].copy_from_slice(&session_priv[..]);
+ match pending_outbound_payments.as_mut().unwrap().entry(payment_id) {
+ hash_map::Entry::Occupied(mut entry) => {
+ assert!(!entry.get_mut().insert(session_priv_bytes, path_amt));
+ },
+ hash_map::Entry::Vacant(entry) => {
+ panic!("no entry for this payment");
+ }
+ }
+ }
+ }
+}
}
}

which is reassuring as well

@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch from f47acdb to 4687624CompareOctober 21, 2021 23:14

@valentinewallacevalentinewallace left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Think I'm ACK mod outstanding feedback. Might give it one more glance in the morning. Thanks for the additional test coverage!

Comment threadlightning/src/ln/channelmanager.rs Outdated
// We only rebuild the pending payments map if we were most recently serialized by
// 0.0.102+
for (_, monitor) in args.channel_monitors {
if by_id.get(&monitor.get_funding_txo().0.to_channel_id()).is_none() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Ah, right 🤦‍♀️

Fwiw, when I add this diff, it gets hit a fair amount of times and all tests pass:

diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs
index af8a65b9..5d7ee3a5 100644
--- a/lightning/src/ln/channelmanager.rs
+++ b/lightning/src/ln/channelmanager.rs
@@ -5939,7 +5939,23 @@ impl<'a, Signer: Sign, M: Deref, T: Deref, K: Deref, F: Deref, L: Deref>
}
}
}
- }
+ } else {
+ for (htlc_source, htlc) in monitor.get_pending_outbound_htlcs() {
+ if let HTLCSource::OutboundRoute { payment_id, session_priv, path, payment_secret, .. } = htlc_source {
+ let path_amt = path.last().unwrap().fee_msat;
+ let mut session_priv_bytes = [0; 32];
+ session_priv_bytes[..].copy_from_slice(&session_priv[..]);
+ match pending_outbound_payments.as_mut().unwrap().entry(payment_id) {
+ hash_map::Entry::Occupied(mut entry) => {
+ assert!(!entry.get_mut().insert(session_priv_bytes, path_amt));
+ },
+ hash_map::Entry::Vacant(entry) => {
+ panic!("no entry for this payment");
+ }
+ }
+ }
+ }
+}
}
}

which is reassuring as well

Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/payment_tests.rs Outdated
}

#[test]
fn retry_with_no_persist() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hmm, the new version of this test passes with the code that had the find_map bug

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

Ah! Yep, added even more coverage :)

@jkczyzjkczyz left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Also pretty close on an ACK after remaining feedback is addressed.

@valentinewallace Nice work on finding the test gaps. I was staring at retry_with_no_persist trying to figure out if the bug was being exercised. Mutation testing FTW lol.

I wonder if there's any improvements we can make to our testing strategy to make reviewing a bit easier. Some of the tests are pretty lengthy/verbose but I'm not sure if they can be broken up into smaller tests and still exercise the behavior.

/// monitor update failure must *not* have been sent to the remote end, and must instead
/// have been dropped. They will be regenerated when monitor_updating_restored is called.
pub fn monitor_update_failed(&mut self, resend_raa: bool, resend_commitment: bool, mut pending_forwards: Vec<(PendingHTLCInfo, u64)>, mut pending_fails: Vec<(HTLCSource, PaymentHash, HTLCFailReason)>) {
pub fn monitor_update_failed(&mut self, resend_raa: bool, resend_commitment: bool,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: move these parameters down a line, aligned with the other parameters

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

For a small function that doesn't seem to look any better to my eye? Feels very "rustfmt wants your code to use as many lines as possible" to me :).

Comment threadlightning/src/ln/functional_tests.rs Outdated
Comment on lines +642 to +686
// Now reload nodes[1]...
persister = test_utils::TestPersister::new();
let keys_manager = &chanmon_cfgs[1].keys_manager;
new_chain_monitor = test_utils::TestChainMonitor::new(Some(nodes[1].chain_source), nodes[1].tx_broadcaster.clone(), nodes[1].logger, node_cfgs[1].fee_estimator, &persister, keys_manager);
nodes[1].chain_monitor = &new_chain_monitor;
let mut chan_0_monitor_read = &chan_0_monitor_serialized.0[..];
let (_, mut chan_0_monitor) = <(BlockHash, ChannelMonitor<EnforcingSigner>)>::read(
&mut chan_0_monitor_read, keys_manager).unwrap();
assert!(chan_0_monitor_read.is_empty());

let (_, nodes_1_deserialized_tmp) = {
let mut channel_monitors = HashMap::new();
channel_monitors.insert(chan_0_monitor.get_funding_txo().0, &mut chan_0_monitor);
<(BlockHash, ChannelManager<EnforcingSigner, &test_utils::TestChainMonitor, &test_utils::TestBroadcaster, &test_utils::TestKeysInterface, &test_utils::TestFeeEstimator, &test_utils::TestLogger>)>
::read(&mut io::Cursor::new(&chan_manager_serialized.0[..]), ChannelManagerReadArgs {
default_config: Default::default(),
keys_manager,
fee_estimator: node_cfgs[1].fee_estimator,
chain_monitor: nodes[1].chain_monitor,
tx_broadcaster: nodes[1].tx_broadcaster.clone(),
logger: nodes[1].logger,
channel_monitors,
}).unwrap()
};
nodes_1_deserialized = nodes_1_deserialized_tmp;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is pretty verbose and looks repeated from retry_with_no_persist. Wondering if we could make a utility function for reloading so the tests are easier to read.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

Yea, its repeated in quite a few tests. I believe we could may be macro it, but it requires a few variables declared at top of function scope. I'll look at it as a followup, its not a new problem.

Comment threadlightning/src/ln/channelmanager.rs Outdated
@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch from 4687624 to 4348833CompareOctober 22, 2021 05:24

@valentinewallacevalentinewallace left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM 🚀

Comment threadlightning/src/ln/payment_tests.rs
@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch 2 times, most recently from b67f15c to d9a85b3CompareOctober 22, 2021 18:06
@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

Pushed yet more test coverage, I believe all outstanding comments have been addressed.

@valentinewallacevalentinewallace left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ACK after squash

@jkczyzjkczyz left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This substantially improves readability at the callsite and in the
function.
This improves readability at the callsite and in the function.
This allows us to read a `HashMap` that has values which may be
skipped if they are some backwards-compatibility type.
We also take this opportunity to fail deserialization if keys are
duplicated.
When an HTLC has been failed, we track it up until the point there
exists no broadcastable commitment transaction which has the HTLC
present, at which point Channel returns the HTLCSource back to the
ChannelManager, which fails the HTLC backwards appropriately.
When an HTLC is fulfilled, however, we fulfill on the backwards path
immediately. This is great for claiming upstream HTLCs, but when we
want to track pending payments, we need to ensure we can check with
ChannelMonitor data to rebuild pending payments. In order to do so,
we need an event similar to the HTLC failure event, but for
fulfills instead.
Specifically, if we force-close a channel, we remove its off-chain
`Channel` object entirely, at which point, on reload, we may notice
HTLC(s) which are not present in our pending payments map (as they
may have received a payment preimage, but not fully committed to
it). Thus, we'd conclude we still have a retryable payment, which
is untrue.
This commit does so, informing the ChannelManager via a new return
element where appropriate of the HTLCSource corresponding to the
failed HTLC.
In the next commit, we will reload lost pending payments from
ChannelMonitors during restart. However, in order to avoid
re-adding pending payments which have already been fulfilled, we
must ensure that we do not fully remove pending payments until all
HTLCs for the payment have been fully removed from their
ChannelMonitors.
We do so here, introducing a new PendingOutboundPayment variant
called `Completed` which only tracks the set of pending HTLCs.
If we go to send a payment, add the HTLC(s) to the channel(s),
commit the ChannelMonitor updates to disk, and then crash, we'll
come back up with no pending payments but HTLC(s) ready to be
claim/failed.
This makes it rather impractical to write a payment sender/retryer,
as you cannot guarantee atomicity - you cannot guarantee you'll
have retry data persisted even if the HTLC(s) are actually pending.
Because ChannelMonitors are *the* atomically-persisted data in LDK,
we lean on their current HTLC data to figure out what HTLC(s) are a
part of an outbound payment, rebuilding the pending payments list
on reload.
test_dup_htlc_onchain_fails_on_reload is now more of a
payment_test than a functional_test, testing for handling of
pending payments.
Peers probably shouldn't do this, but if they want to give us free
money, we should take it and not generate any spurious events.
@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch from d9a85b3 to 7af5d12CompareOctober 22, 2021 18:41
@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

Squashed without diff, will land after CI 🎉

$ git diff-tree -U1 d9a85b35b 7af5d127a
$

@TheBlueMatt
TheBlueMatt merged commit 0a31c12 into lightningdevkit:mainOct 22, 2021
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Generate PaymentPathFailed for MonitorUpdate-sourced HTLC failures when we don't have a corresponding pending payment entry

3 participants

@TheBlueMatt@jkczyz@valentinewallace
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Reload pending outbound payments from ChannelMonitors on startup by TheBlueMatt · Pull Request #1104 · lightningdevkit/rust-lightning · GitHub
Skip to content

Reload pending outbound payments from ChannelMonitors on startup - #1104

Merged
TheBlueMatt merged 11 commits into
lightningdevkit:mainfrom
TheBlueMatt:2021-10-payment-id-in-monitors
Oct 22, 2021
Merged

Reload pending outbound payments from ChannelMonitors on startup#1104
TheBlueMatt merged 11 commits into
lightningdevkit:mainfrom
TheBlueMatt:2021-10-payment-id-in-monitors

Conversation

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

When we add payment retries, we need some kind of on-disk consistency story. I think a compelling one is described, roughly at [1] - make the retryer persist first, before calling into the ChannelManager, then when reloading we either have a pending payment there or its pending nowhere. Once the send_payment method returns, we'll either have a pending payment persisted in both the retryer and a ChannelMonitor, or in neither. Note that we may not have anything in a ChannelManager until much later, preventing us from retrying the payment if we crash.

In such a case, we'll force-close the relevant channel (its ChannelMonitor has run ahead of the manager), but the actual HTLC will still be pending, just not present in ChannelManager at all. Ideally, we'd then be able to retry it over a whole new path.

This PR addresses this situation by having ChannelManager examine the available ChannelMonitors at startup, rebuilding pending payment entries on the basis of what's in its ChannelMonitors.

Note that we aren't particularly concerned with the payment resolution end of things - if ChannelManager thinks a payment is still pending when it has been fully resolved, we'll eventually consider it sent and generate a fresh PaymentSent when the ChannelMonitor resolves the current state.

This should fix#1102, but generally needs lots more tests (its not tested at all, aside from a hacky thing at the end to compare the loaded-from-disk and the rebuilt set of payments, which passes most tests).

[1] #1059 (comment)

@TheBlueMattTheBlueMatt added this to the 0.0.102 milestone Oct 4, 2021
@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch from e97930f to a0fe042CompareOctober 5, 2021 05:59
@codecov

codecovBot commented Oct 5, 2021

Copy link
Copy Markdown

Codecov Report

Merging #1104 (7af5d12) into main (107c6c7) will increase coverage by 0.06%.
The diff coverage is 96.03%.

Impacted file tree graph

@@ Coverage Diff @@## main #1104 +/- ##
==========================================
+ Coverage 90.40% 90.47% +0.06% 
==========================================
Files 68 68 Lines 34796 35176 +380 ==========================================
+ Hits 31458 31826 +368 - Misses 3338 3350 +12 
Impacted FilesCoverage Δ
lightning/src/util/ser.rs89.02% <80.00%> (-0.12%)⬇️
lightning/src/ln/channelmanager.rs83.86% <90.06%> (+0.34%)⬆️
lightning/src/ln/channel.rs88.37% <96.22%> (+0.05%)⬆️
lightning/src/chain/channelmonitor.rs91.11% <97.56%> (+0.17%)⬆️
lightning/src/ln/payment_tests.rs99.09% <99.26%> (+0.25%)⬆️
lightning/src/ln/functional_test_utils.rs95.09% <100.00%> (+<0.01%)⬆️
lightning/src/ln/functional_tests.rs97.37% <100.00%> (-0.04%)⬇️
lightning/src/ln/onion_route_tests.rs96.60% <100.00%> (ø)
lightning/src/ln/features.rs99.43% <0.00%> (+<0.01%)⬆️
... and 2 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 107c6c7...7af5d12. Read the comment docs.

@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

This is now based on #1108, which should go first.

@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

Depends on #1109 as well now.

@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch from a0fe042 to f821c93CompareOctober 5, 2021 23:26
@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

Now includes at least a naive test of payment data reloading, which works 🎉

@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch 4 times, most recently from 6e60c4f to a7cea3dCompareOctober 11, 2021 00:53
@TheBlueMattTheBlueMatt modified the milestones: 0.0.102, 0.0.103Oct 15, 2021
@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch from a7cea3d to 7d23578CompareOctober 20, 2021 01:25
@TheBlueMatt
TheBlueMatt marked this pull request as ready for review October 20, 2021 01:25
@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

Rebased on upstream!

@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch from 7d23578 to df8b291CompareOctober 20, 2021 02:54
@jkczyz
jkczyz self-requested a review October 20, 2021 17:56

@valentinewallacevalentinewallace left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Mostly wondering how we handle failures that were previously fulfilled

Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/chain/channelmonitor.rs
Comment threadlightning/src/chain/channelmonitor.rs Outdated
@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch from df8b291 to 42a854cCompareOctober 20, 2021 21:13

@jkczyzjkczyz left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Sorry about any confusion around terminology. I don't have as a firm of grasp as I'd like to around some of this.

Comment on lines +344 to +363
pub(super) struct RAAUpdates {
pub commitment_update: Option<msgs::CommitmentUpdate>,
pub to_forward_htlcs: Vec<(PendingHTLCInfo, u64)>,
pub failed_htlcs: Vec<(HTLCSource, PaymentHash, HTLCFailReason)>,
pub finalized_claim_htlcs: Vec<HTLCSource>,
pub monitor_update: ChannelMonitorUpdate,
pub holding_cell_failed_htlcs: Vec<(HTLCSource, PaymentHash)>,
}

/// The return value of `monitor_updating_resotred`
pub(super) struct MonitorRestoreUpdates {
pub raa: Option<msgs::RevokeAndACK>,
pub commitment_update: Option<msgs::CommitmentUpdate>,
pub order: RAACommitmentOrder,
pub forwards: Vec<(PendingHTLCInfo, u64)>,
pub failures: Vec<(HTLCSource, PaymentHash, HTLCFailReason)>,
pub finalized_claims: Vec<HTLCSource>,
pub funding_broadcastable: Option<Transaction>,
pub funding_locked: Option<msgs::FundingLocked>,
}

@jkczyzjkczyzOct 20, 2021

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Where it makes sense, could we use consistent field names between these two structs?

Comment threadlightning/src/ln/channel.rs Outdated
}
self.monitor_pending_forwards.append(&mut to_forward_infos);
self.monitor_pending_failures.append(&mut revoked_htlcs);
self.monitor_pending_finalized_fulfills.append(&mut finalized_claim_htlcs);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

In terms of terminology, is there a difference between a "fulfill" and a "claim"? Does it depend on the context?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

It mostly just depends on when the code was written :p

commitment_update: Some(commitment_update),
finalized_claim_htlcs,
to_forward_htlcs: to_forward_infos,
failed_htlcs: revoked_htlcs,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Similar question wrt "failed" and "revoked". Just want to make sure I understand if it is context dependent.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

Same answer, there really isn't.

Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment on lines +2018 to +2023
if let hash_map::Entry::Occupied(payment) = &payment_entry {
if !payment.get().is_retryable() {
return Err(APIError::RouteError {
err: "Payment already completed"
});
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It's unclear to me why this shouldn't be if payment.get().is_complete() { as indicated by the error message. Do we want to hit this for legacy payments, too?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

Hmm, yes, we probably should hit this for legacy payments. I mean its unreachable for legacy payments, I believe (retry_payment refuses to call send_payment_internal at all), but nothing wrong with an extra check here.

Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment on lines +419 to +444
/// When a pending payment completes, we continue tracking it until all pendings HTLCs have
/// been resolved. This ensures we don't look up pending payments in ChannelMonitors on restart
/// and add a pending payment that was already completed.
Fulfilled {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Terminology question related to my earlier comment: is there a difference between "completes" and "fulfilled" as it pertains to the first sentence in the docs and the variant name?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

No, I just missed the docs in the rename - once we get one HTLC fulfillment we consider the payment fulfilled.

@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

Pushed a fix for the lockorder comment, leaving other stuff until we settle on naming.

@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch from 2aef42c to ca6ae4dCompareOctober 21, 2021 00:00
Comment threadlightning/src/chain/channelmonitor.rs Outdated
Comment threadlightning/src/chain/channelmonitor.rs Outdated
Comment threadlightning/src/chain/channelmonitor.rs
Comment threadlightning/src/chain/channelmonitor.rs Outdated
Comment threadlightning/src/chain/channelmonitor.rs Outdated
Comment threadlightning/src/chain/channelmonitor.rs Outdated
@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch from ca6ae4d to a050236CompareOctober 21, 2021 19:15
@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

Ok, I believe I've addressed all feedback and renamed things to be more clear.

Comment threadlightning/src/chain/channelmonitor.rs
Comment threadlightning/src/chain/channelmonitor.rs Outdated
Comment threadlightning/src/chain/channelmonitor.rs Outdated
Comment threadlightning/src/ln/channel.rs Outdated
Comment threadlightning/src/ln/channel.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch from a050236 to f47acdbCompareOctober 21, 2021 20:41
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/chain/channelmonitor.rs Outdated
// We only rebuild the pending payments map if we were most recently serialized by
// 0.0.102+
for (_, monitor) in args.channel_monitors {
if by_id.get(&monitor.get_funding_txo().0.to_channel_id()).is_none() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hmm, I don't see why we don't load pending outbound payments for open channels as well. Couldn't we still have a situation where the ChannelManager doesn't get persisted after initiating an outbound payment, but the ChannelMonitor still knows about it?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

The channel monitor knowing about a payment that the ChannelManager does not implies by definition that the ChannelMonitor has some newer state than the ChannelManager. It's possible I missed some edge case but it feels pretty straightforward?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Ah, right 🤦‍♀️

Fwiw, when I add this diff, it gets hit a fair amount of times and all tests pass:

diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs
index af8a65b9..5d7ee3a5 100644
--- a/lightning/src/ln/channelmanager.rs
+++ b/lightning/src/ln/channelmanager.rs
@@ -5939,7 +5939,23 @@ impl<'a, Signer: Sign, M: Deref, T: Deref, K: Deref, F: Deref, L: Deref>
}
}
}
- }
+ } else {
+ for (htlc_source, htlc) in monitor.get_pending_outbound_htlcs() {
+ if let HTLCSource::OutboundRoute { payment_id, session_priv, path, payment_secret, .. } = htlc_source {
+ let path_amt = path.last().unwrap().fee_msat;
+ let mut session_priv_bytes = [0; 32];
+ session_priv_bytes[..].copy_from_slice(&session_priv[..]);
+ match pending_outbound_payments.as_mut().unwrap().entry(payment_id) {
+ hash_map::Entry::Occupied(mut entry) => {
+ assert!(!entry.get_mut().insert(session_priv_bytes, path_amt));
+ },
+ hash_map::Entry::Vacant(entry) => {
+ panic!("no entry for this payment");
+ }
+ }
+ }
+ }
+}
}
}

which is reassuring as well

@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch from f47acdb to 4687624CompareOctober 21, 2021 23:14

@valentinewallacevalentinewallace left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Think I'm ACK mod outstanding feedback. Might give it one more glance in the morning. Thanks for the additional test coverage!

Comment threadlightning/src/ln/channelmanager.rs Outdated
// We only rebuild the pending payments map if we were most recently serialized by
// 0.0.102+
for (_, monitor) in args.channel_monitors {
if by_id.get(&monitor.get_funding_txo().0.to_channel_id()).is_none() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Ah, right 🤦‍♀️

Fwiw, when I add this diff, it gets hit a fair amount of times and all tests pass:

diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs
index af8a65b9..5d7ee3a5 100644
--- a/lightning/src/ln/channelmanager.rs
+++ b/lightning/src/ln/channelmanager.rs
@@ -5939,7 +5939,23 @@ impl<'a, Signer: Sign, M: Deref, T: Deref, K: Deref, F: Deref, L: Deref>
}
}
}
- }
+ } else {
+ for (htlc_source, htlc) in monitor.get_pending_outbound_htlcs() {
+ if let HTLCSource::OutboundRoute { payment_id, session_priv, path, payment_secret, .. } = htlc_source {
+ let path_amt = path.last().unwrap().fee_msat;
+ let mut session_priv_bytes = [0; 32];
+ session_priv_bytes[..].copy_from_slice(&session_priv[..]);
+ match pending_outbound_payments.as_mut().unwrap().entry(payment_id) {
+ hash_map::Entry::Occupied(mut entry) => {
+ assert!(!entry.get_mut().insert(session_priv_bytes, path_amt));
+ },
+ hash_map::Entry::Vacant(entry) => {
+ panic!("no entry for this payment");
+ }
+ }
+ }
+ }
+}
}
}

which is reassuring as well

Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/payment_tests.rs Outdated
}

#[test]
fn retry_with_no_persist() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hmm, the new version of this test passes with the code that had the find_map bug

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

Ah! Yep, added even more coverage :)

@jkczyzjkczyz left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Also pretty close on an ACK after remaining feedback is addressed.

@valentinewallace Nice work on finding the test gaps. I was staring at retry_with_no_persist trying to figure out if the bug was being exercised. Mutation testing FTW lol.

I wonder if there's any improvements we can make to our testing strategy to make reviewing a bit easier. Some of the tests are pretty lengthy/verbose but I'm not sure if they can be broken up into smaller tests and still exercise the behavior.

/// monitor update failure must *not* have been sent to the remote end, and must instead
/// have been dropped. They will be regenerated when monitor_updating_restored is called.
pub fn monitor_update_failed(&mut self, resend_raa: bool, resend_commitment: bool, mut pending_forwards: Vec<(PendingHTLCInfo, u64)>, mut pending_fails: Vec<(HTLCSource, PaymentHash, HTLCFailReason)>) {
pub fn monitor_update_failed(&mut self, resend_raa: bool, resend_commitment: bool,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: move these parameters down a line, aligned with the other parameters

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

For a small function that doesn't seem to look any better to my eye? Feels very "rustfmt wants your code to use as many lines as possible" to me :).

Comment threadlightning/src/ln/functional_tests.rs Outdated
Comment on lines +642 to +686
// Now reload nodes[1]...
persister = test_utils::TestPersister::new();
let keys_manager = &chanmon_cfgs[1].keys_manager;
new_chain_monitor = test_utils::TestChainMonitor::new(Some(nodes[1].chain_source), nodes[1].tx_broadcaster.clone(), nodes[1].logger, node_cfgs[1].fee_estimator, &persister, keys_manager);
nodes[1].chain_monitor = &new_chain_monitor;
let mut chan_0_monitor_read = &chan_0_monitor_serialized.0[..];
let (_, mut chan_0_monitor) = <(BlockHash, ChannelMonitor<EnforcingSigner>)>::read(
&mut chan_0_monitor_read, keys_manager).unwrap();
assert!(chan_0_monitor_read.is_empty());

let (_, nodes_1_deserialized_tmp) = {
let mut channel_monitors = HashMap::new();
channel_monitors.insert(chan_0_monitor.get_funding_txo().0, &mut chan_0_monitor);
<(BlockHash, ChannelManager<EnforcingSigner, &test_utils::TestChainMonitor, &test_utils::TestBroadcaster, &test_utils::TestKeysInterface, &test_utils::TestFeeEstimator, &test_utils::TestLogger>)>
::read(&mut io::Cursor::new(&chan_manager_serialized.0[..]), ChannelManagerReadArgs {
default_config: Default::default(),
keys_manager,
fee_estimator: node_cfgs[1].fee_estimator,
chain_monitor: nodes[1].chain_monitor,
tx_broadcaster: nodes[1].tx_broadcaster.clone(),
logger: nodes[1].logger,
channel_monitors,
}).unwrap()
};
nodes_1_deserialized = nodes_1_deserialized_tmp;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is pretty verbose and looks repeated from retry_with_no_persist. Wondering if we could make a utility function for reloading so the tests are easier to read.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

Yea, its repeated in quite a few tests. I believe we could may be macro it, but it requires a few variables declared at top of function scope. I'll look at it as a followup, its not a new problem.

Comment threadlightning/src/ln/channelmanager.rs Outdated
@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch from 4687624 to 4348833CompareOctober 22, 2021 05:24

@valentinewallacevalentinewallace left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM 🚀

Comment threadlightning/src/ln/payment_tests.rs
@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch 2 times, most recently from b67f15c to d9a85b3CompareOctober 22, 2021 18:06
@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

Pushed yet more test coverage, I believe all outstanding comments have been addressed.

@valentinewallacevalentinewallace left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ACK after squash

@jkczyzjkczyz left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This substantially improves readability at the callsite and in the
function.
This improves readability at the callsite and in the function.
This allows us to read a `HashMap` that has values which may be
skipped if they are some backwards-compatibility type.
We also take this opportunity to fail deserialization if keys are
duplicated.
When an HTLC has been failed, we track it up until the point there
exists no broadcastable commitment transaction which has the HTLC
present, at which point Channel returns the HTLCSource back to the
ChannelManager, which fails the HTLC backwards appropriately.
When an HTLC is fulfilled, however, we fulfill on the backwards path
immediately. This is great for claiming upstream HTLCs, but when we
want to track pending payments, we need to ensure we can check with
ChannelMonitor data to rebuild pending payments. In order to do so,
we need an event similar to the HTLC failure event, but for
fulfills instead.
Specifically, if we force-close a channel, we remove its off-chain
`Channel` object entirely, at which point, on reload, we may notice
HTLC(s) which are not present in our pending payments map (as they
may have received a payment preimage, but not fully committed to
it). Thus, we'd conclude we still have a retryable payment, which
is untrue.
This commit does so, informing the ChannelManager via a new return
element where appropriate of the HTLCSource corresponding to the
failed HTLC.
In the next commit, we will reload lost pending payments from
ChannelMonitors during restart. However, in order to avoid
re-adding pending payments which have already been fulfilled, we
must ensure that we do not fully remove pending payments until all
HTLCs for the payment have been fully removed from their
ChannelMonitors.
We do so here, introducing a new PendingOutboundPayment variant
called `Completed` which only tracks the set of pending HTLCs.
If we go to send a payment, add the HTLC(s) to the channel(s),
commit the ChannelMonitor updates to disk, and then crash, we'll
come back up with no pending payments but HTLC(s) ready to be
claim/failed.
This makes it rather impractical to write a payment sender/retryer,
as you cannot guarantee atomicity - you cannot guarantee you'll
have retry data persisted even if the HTLC(s) are actually pending.
Because ChannelMonitors are *the* atomically-persisted data in LDK,
we lean on their current HTLC data to figure out what HTLC(s) are a
part of an outbound payment, rebuilding the pending payments list
on reload.
test_dup_htlc_onchain_fails_on_reload is now more of a
payment_test than a functional_test, testing for handling of
pending payments.
Peers probably shouldn't do this, but if they want to give us free
money, we should take it and not generate any spurious events.
@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch from d9a85b3 to 7af5d12CompareOctober 22, 2021 18:41
@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

Squashed without diff, will land after CI 🎉

$ git diff-tree -U1 d9a85b35b 7af5d127a
$

@TheBlueMatt
TheBlueMatt merged commit 0a31c12 into lightningdevkit:mainOct 22, 2021
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Generate PaymentPathFailed for MonitorUpdate-sourced HTLC failures when we don't have a corresponding pending payment entry

3 participants

@TheBlueMatt@jkczyz@valentinewallace
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); Reload pending outbound payments from ChannelMonitors on startup by TheBlueMatt · Pull Request #1104 · lightningdevkit/rust-lightning · GitHub
Skip to content

Reload pending outbound payments from ChannelMonitors on startup - #1104

Merged
TheBlueMatt merged 11 commits into
lightningdevkit:mainfrom
TheBlueMatt:2021-10-payment-id-in-monitors
Oct 22, 2021
Merged

Reload pending outbound payments from ChannelMonitors on startup#1104
TheBlueMatt merged 11 commits into
lightningdevkit:mainfrom
TheBlueMatt:2021-10-payment-id-in-monitors

Conversation

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

When we add payment retries, we need some kind of on-disk consistency story. I think a compelling one is described, roughly at [1] - make the retryer persist first, before calling into the ChannelManager, then when reloading we either have a pending payment there or its pending nowhere. Once the send_payment method returns, we'll either have a pending payment persisted in both the retryer and a ChannelMonitor, or in neither. Note that we may not have anything in a ChannelManager until much later, preventing us from retrying the payment if we crash.

In such a case, we'll force-close the relevant channel (its ChannelMonitor has run ahead of the manager), but the actual HTLC will still be pending, just not present in ChannelManager at all. Ideally, we'd then be able to retry it over a whole new path.

This PR addresses this situation by having ChannelManager examine the available ChannelMonitors at startup, rebuilding pending payment entries on the basis of what's in its ChannelMonitors.

Note that we aren't particularly concerned with the payment resolution end of things - if ChannelManager thinks a payment is still pending when it has been fully resolved, we'll eventually consider it sent and generate a fresh PaymentSent when the ChannelMonitor resolves the current state.

This should fix#1102, but generally needs lots more tests (its not tested at all, aside from a hacky thing at the end to compare the loaded-from-disk and the rebuilt set of payments, which passes most tests).

[1] #1059 (comment)

@TheBlueMattTheBlueMatt added this to the 0.0.102 milestone Oct 4, 2021
@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch from e97930f to a0fe042CompareOctober 5, 2021 05:59
@codecov

codecovBot commented Oct 5, 2021

Copy link
Copy Markdown

Codecov Report

Merging #1104 (7af5d12) into main (107c6c7) will increase coverage by 0.06%.
The diff coverage is 96.03%.

Impacted file tree graph

@@ Coverage Diff @@## main #1104 +/- ##
==========================================
+ Coverage 90.40% 90.47% +0.06% 
==========================================
Files 68 68 Lines 34796 35176 +380 ==========================================
+ Hits 31458 31826 +368 - Misses 3338 3350 +12 
Impacted FilesCoverage Δ
lightning/src/util/ser.rs89.02% <80.00%> (-0.12%)⬇️
lightning/src/ln/channelmanager.rs83.86% <90.06%> (+0.34%)⬆️
lightning/src/ln/channel.rs88.37% <96.22%> (+0.05%)⬆️
lightning/src/chain/channelmonitor.rs91.11% <97.56%> (+0.17%)⬆️
lightning/src/ln/payment_tests.rs99.09% <99.26%> (+0.25%)⬆️
lightning/src/ln/functional_test_utils.rs95.09% <100.00%> (+<0.01%)⬆️
lightning/src/ln/functional_tests.rs97.37% <100.00%> (-0.04%)⬇️
lightning/src/ln/onion_route_tests.rs96.60% <100.00%> (ø)
lightning/src/ln/features.rs99.43% <0.00%> (+<0.01%)⬆️
... and 2 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 107c6c7...7af5d12. Read the comment docs.

@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

This is now based on #1108, which should go first.

@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

Depends on #1109 as well now.

@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch from a0fe042 to f821c93CompareOctober 5, 2021 23:26
@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

Now includes at least a naive test of payment data reloading, which works 🎉

@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch 4 times, most recently from 6e60c4f to a7cea3dCompareOctober 11, 2021 00:53
@TheBlueMattTheBlueMatt modified the milestones: 0.0.102, 0.0.103Oct 15, 2021
@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch from a7cea3d to 7d23578CompareOctober 20, 2021 01:25
@TheBlueMatt
TheBlueMatt marked this pull request as ready for review October 20, 2021 01:25
@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

Rebased on upstream!

@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch from 7d23578 to df8b291CompareOctober 20, 2021 02:54
@jkczyz
jkczyz self-requested a review October 20, 2021 17:56

@valentinewallacevalentinewallace left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Mostly wondering how we handle failures that were previously fulfilled

Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/chain/channelmonitor.rs
Comment threadlightning/src/chain/channelmonitor.rs Outdated
@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch from df8b291 to 42a854cCompareOctober 20, 2021 21:13

@jkczyzjkczyz left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Sorry about any confusion around terminology. I don't have as a firm of grasp as I'd like to around some of this.

Comment on lines +344 to +363
pub(super) struct RAAUpdates {
pub commitment_update: Option<msgs::CommitmentUpdate>,
pub to_forward_htlcs: Vec<(PendingHTLCInfo, u64)>,
pub failed_htlcs: Vec<(HTLCSource, PaymentHash, HTLCFailReason)>,
pub finalized_claim_htlcs: Vec<HTLCSource>,
pub monitor_update: ChannelMonitorUpdate,
pub holding_cell_failed_htlcs: Vec<(HTLCSource, PaymentHash)>,
}

/// The return value of `monitor_updating_resotred`
pub(super) struct MonitorRestoreUpdates {
pub raa: Option<msgs::RevokeAndACK>,
pub commitment_update: Option<msgs::CommitmentUpdate>,
pub order: RAACommitmentOrder,
pub forwards: Vec<(PendingHTLCInfo, u64)>,
pub failures: Vec<(HTLCSource, PaymentHash, HTLCFailReason)>,
pub finalized_claims: Vec<HTLCSource>,
pub funding_broadcastable: Option<Transaction>,
pub funding_locked: Option<msgs::FundingLocked>,
}

@jkczyzjkczyzOct 20, 2021

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Where it makes sense, could we use consistent field names between these two structs?

Comment threadlightning/src/ln/channel.rs Outdated
}
self.monitor_pending_forwards.append(&mut to_forward_infos);
self.monitor_pending_failures.append(&mut revoked_htlcs);
self.monitor_pending_finalized_fulfills.append(&mut finalized_claim_htlcs);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

In terms of terminology, is there a difference between a "fulfill" and a "claim"? Does it depend on the context?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

It mostly just depends on when the code was written :p

commitment_update: Some(commitment_update),
finalized_claim_htlcs,
to_forward_htlcs: to_forward_infos,
failed_htlcs: revoked_htlcs,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Similar question wrt "failed" and "revoked". Just want to make sure I understand if it is context dependent.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

Same answer, there really isn't.

Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment on lines +2018 to +2023
if let hash_map::Entry::Occupied(payment) = &payment_entry {
if !payment.get().is_retryable() {
return Err(APIError::RouteError {
err: "Payment already completed"
});
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It's unclear to me why this shouldn't be if payment.get().is_complete() { as indicated by the error message. Do we want to hit this for legacy payments, too?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

Hmm, yes, we probably should hit this for legacy payments. I mean its unreachable for legacy payments, I believe (retry_payment refuses to call send_payment_internal at all), but nothing wrong with an extra check here.

Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment on lines +419 to +444
/// When a pending payment completes, we continue tracking it until all pendings HTLCs have
/// been resolved. This ensures we don't look up pending payments in ChannelMonitors on restart
/// and add a pending payment that was already completed.
Fulfilled {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Terminology question related to my earlier comment: is there a difference between "completes" and "fulfilled" as it pertains to the first sentence in the docs and the variant name?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

No, I just missed the docs in the rename - once we get one HTLC fulfillment we consider the payment fulfilled.

@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

Pushed a fix for the lockorder comment, leaving other stuff until we settle on naming.

@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch from 2aef42c to ca6ae4dCompareOctober 21, 2021 00:00
Comment threadlightning/src/chain/channelmonitor.rs Outdated
Comment threadlightning/src/chain/channelmonitor.rs Outdated
Comment threadlightning/src/chain/channelmonitor.rs
Comment threadlightning/src/chain/channelmonitor.rs Outdated
Comment threadlightning/src/chain/channelmonitor.rs Outdated
Comment threadlightning/src/chain/channelmonitor.rs Outdated
@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch from ca6ae4d to a050236CompareOctober 21, 2021 19:15
@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

Ok, I believe I've addressed all feedback and renamed things to be more clear.

Comment threadlightning/src/chain/channelmonitor.rs
Comment threadlightning/src/chain/channelmonitor.rs Outdated
Comment threadlightning/src/chain/channelmonitor.rs Outdated
Comment threadlightning/src/ln/channel.rs Outdated
Comment threadlightning/src/ln/channel.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch from a050236 to f47acdbCompareOctober 21, 2021 20:41
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/chain/channelmonitor.rs Outdated
// We only rebuild the pending payments map if we were most recently serialized by
// 0.0.102+
for (_, monitor) in args.channel_monitors {
if by_id.get(&monitor.get_funding_txo().0.to_channel_id()).is_none() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hmm, I don't see why we don't load pending outbound payments for open channels as well. Couldn't we still have a situation where the ChannelManager doesn't get persisted after initiating an outbound payment, but the ChannelMonitor still knows about it?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

The channel monitor knowing about a payment that the ChannelManager does not implies by definition that the ChannelMonitor has some newer state than the ChannelManager. It's possible I missed some edge case but it feels pretty straightforward?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Ah, right 🤦‍♀️

Fwiw, when I add this diff, it gets hit a fair amount of times and all tests pass:

diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs
index af8a65b9..5d7ee3a5 100644
--- a/lightning/src/ln/channelmanager.rs
+++ b/lightning/src/ln/channelmanager.rs
@@ -5939,7 +5939,23 @@ impl<'a, Signer: Sign, M: Deref, T: Deref, K: Deref, F: Deref, L: Deref>
}
}
}
- }
+ } else {
+ for (htlc_source, htlc) in monitor.get_pending_outbound_htlcs() {
+ if let HTLCSource::OutboundRoute { payment_id, session_priv, path, payment_secret, .. } = htlc_source {
+ let path_amt = path.last().unwrap().fee_msat;
+ let mut session_priv_bytes = [0; 32];
+ session_priv_bytes[..].copy_from_slice(&session_priv[..]);
+ match pending_outbound_payments.as_mut().unwrap().entry(payment_id) {
+ hash_map::Entry::Occupied(mut entry) => {
+ assert!(!entry.get_mut().insert(session_priv_bytes, path_amt));
+ },
+ hash_map::Entry::Vacant(entry) => {
+ panic!("no entry for this payment");
+ }
+ }
+ }
+ }
+}
}
}

which is reassuring as well

@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch from f47acdb to 4687624CompareOctober 21, 2021 23:14

@valentinewallacevalentinewallace left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Think I'm ACK mod outstanding feedback. Might give it one more glance in the morning. Thanks for the additional test coverage!

Comment threadlightning/src/ln/channelmanager.rs Outdated
// We only rebuild the pending payments map if we were most recently serialized by
// 0.0.102+
for (_, monitor) in args.channel_monitors {
if by_id.get(&monitor.get_funding_txo().0.to_channel_id()).is_none() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Ah, right 🤦‍♀️

Fwiw, when I add this diff, it gets hit a fair amount of times and all tests pass:

diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs
index af8a65b9..5d7ee3a5 100644
--- a/lightning/src/ln/channelmanager.rs
+++ b/lightning/src/ln/channelmanager.rs
@@ -5939,7 +5939,23 @@ impl<'a, Signer: Sign, M: Deref, T: Deref, K: Deref, F: Deref, L: Deref>
}
}
}
- }
+ } else {
+ for (htlc_source, htlc) in monitor.get_pending_outbound_htlcs() {
+ if let HTLCSource::OutboundRoute { payment_id, session_priv, path, payment_secret, .. } = htlc_source {
+ let path_amt = path.last().unwrap().fee_msat;
+ let mut session_priv_bytes = [0; 32];
+ session_priv_bytes[..].copy_from_slice(&session_priv[..]);
+ match pending_outbound_payments.as_mut().unwrap().entry(payment_id) {
+ hash_map::Entry::Occupied(mut entry) => {
+ assert!(!entry.get_mut().insert(session_priv_bytes, path_amt));
+ },
+ hash_map::Entry::Vacant(entry) => {
+ panic!("no entry for this payment");
+ }
+ }
+ }
+ }
+}
}
}

which is reassuring as well

Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/payment_tests.rs Outdated
}

#[test]
fn retry_with_no_persist() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hmm, the new version of this test passes with the code that had the find_map bug

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

Ah! Yep, added even more coverage :)

@jkczyzjkczyz left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Also pretty close on an ACK after remaining feedback is addressed.

@valentinewallace Nice work on finding the test gaps. I was staring at retry_with_no_persist trying to figure out if the bug was being exercised. Mutation testing FTW lol.

I wonder if there's any improvements we can make to our testing strategy to make reviewing a bit easier. Some of the tests are pretty lengthy/verbose but I'm not sure if they can be broken up into smaller tests and still exercise the behavior.

/// monitor update failure must *not* have been sent to the remote end, and must instead
/// have been dropped. They will be regenerated when monitor_updating_restored is called.
pub fn monitor_update_failed(&mut self, resend_raa: bool, resend_commitment: bool, mut pending_forwards: Vec<(PendingHTLCInfo, u64)>, mut pending_fails: Vec<(HTLCSource, PaymentHash, HTLCFailReason)>) {
pub fn monitor_update_failed(&mut self, resend_raa: bool, resend_commitment: bool,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: move these parameters down a line, aligned with the other parameters

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

For a small function that doesn't seem to look any better to my eye? Feels very "rustfmt wants your code to use as many lines as possible" to me :).

Comment threadlightning/src/ln/functional_tests.rs Outdated
Comment on lines +642 to +686
// Now reload nodes[1]...
persister = test_utils::TestPersister::new();
let keys_manager = &chanmon_cfgs[1].keys_manager;
new_chain_monitor = test_utils::TestChainMonitor::new(Some(nodes[1].chain_source), nodes[1].tx_broadcaster.clone(), nodes[1].logger, node_cfgs[1].fee_estimator, &persister, keys_manager);
nodes[1].chain_monitor = &new_chain_monitor;
let mut chan_0_monitor_read = &chan_0_monitor_serialized.0[..];
let (_, mut chan_0_monitor) = <(BlockHash, ChannelMonitor<EnforcingSigner>)>::read(
&mut chan_0_monitor_read, keys_manager).unwrap();
assert!(chan_0_monitor_read.is_empty());

let (_, nodes_1_deserialized_tmp) = {
let mut channel_monitors = HashMap::new();
channel_monitors.insert(chan_0_monitor.get_funding_txo().0, &mut chan_0_monitor);
<(BlockHash, ChannelManager<EnforcingSigner, &test_utils::TestChainMonitor, &test_utils::TestBroadcaster, &test_utils::TestKeysInterface, &test_utils::TestFeeEstimator, &test_utils::TestLogger>)>
::read(&mut io::Cursor::new(&chan_manager_serialized.0[..]), ChannelManagerReadArgs {
default_config: Default::default(),
keys_manager,
fee_estimator: node_cfgs[1].fee_estimator,
chain_monitor: nodes[1].chain_monitor,
tx_broadcaster: nodes[1].tx_broadcaster.clone(),
logger: nodes[1].logger,
channel_monitors,
}).unwrap()
};
nodes_1_deserialized = nodes_1_deserialized_tmp;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is pretty verbose and looks repeated from retry_with_no_persist. Wondering if we could make a utility function for reloading so the tests are easier to read.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

Yea, its repeated in quite a few tests. I believe we could may be macro it, but it requires a few variables declared at top of function scope. I'll look at it as a followup, its not a new problem.

Comment threadlightning/src/ln/channelmanager.rs Outdated
@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch from 4687624 to 4348833CompareOctober 22, 2021 05:24

@valentinewallacevalentinewallace left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM 🚀

Comment threadlightning/src/ln/payment_tests.rs
@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch 2 times, most recently from b67f15c to d9a85b3CompareOctober 22, 2021 18:06
@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

Pushed yet more test coverage, I believe all outstanding comments have been addressed.

@valentinewallacevalentinewallace left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ACK after squash

@jkczyzjkczyz left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This substantially improves readability at the callsite and in the
function.
This improves readability at the callsite and in the function.
This allows us to read a `HashMap` that has values which may be
skipped if they are some backwards-compatibility type.
We also take this opportunity to fail deserialization if keys are
duplicated.
When an HTLC has been failed, we track it up until the point there
exists no broadcastable commitment transaction which has the HTLC
present, at which point Channel returns the HTLCSource back to the
ChannelManager, which fails the HTLC backwards appropriately.
When an HTLC is fulfilled, however, we fulfill on the backwards path
immediately. This is great for claiming upstream HTLCs, but when we
want to track pending payments, we need to ensure we can check with
ChannelMonitor data to rebuild pending payments. In order to do so,
we need an event similar to the HTLC failure event, but for
fulfills instead.
Specifically, if we force-close a channel, we remove its off-chain
`Channel` object entirely, at which point, on reload, we may notice
HTLC(s) which are not present in our pending payments map (as they
may have received a payment preimage, but not fully committed to
it). Thus, we'd conclude we still have a retryable payment, which
is untrue.
This commit does so, informing the ChannelManager via a new return
element where appropriate of the HTLCSource corresponding to the
failed HTLC.
In the next commit, we will reload lost pending payments from
ChannelMonitors during restart. However, in order to avoid
re-adding pending payments which have already been fulfilled, we
must ensure that we do not fully remove pending payments until all
HTLCs for the payment have been fully removed from their
ChannelMonitors.
We do so here, introducing a new PendingOutboundPayment variant
called `Completed` which only tracks the set of pending HTLCs.
If we go to send a payment, add the HTLC(s) to the channel(s),
commit the ChannelMonitor updates to disk, and then crash, we'll
come back up with no pending payments but HTLC(s) ready to be
claim/failed.
This makes it rather impractical to write a payment sender/retryer,
as you cannot guarantee atomicity - you cannot guarantee you'll
have retry data persisted even if the HTLC(s) are actually pending.
Because ChannelMonitors are *the* atomically-persisted data in LDK,
we lean on their current HTLC data to figure out what HTLC(s) are a
part of an outbound payment, rebuilding the pending payments list
on reload.
test_dup_htlc_onchain_fails_on_reload is now more of a
payment_test than a functional_test, testing for handling of
pending payments.
Peers probably shouldn't do this, but if they want to give us free
money, we should take it and not generate any spurious events.
@TheBlueMatt
TheBlueMattforce-pushed the 2021-10-payment-id-in-monitors branch from d9a85b3 to 7af5d12CompareOctober 22, 2021 18:41
@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

Squashed without diff, will land after CI 🎉

$ git diff-tree -U1 d9a85b35b 7af5d127a
$

@TheBlueMatt
TheBlueMatt merged commit 0a31c12 into lightningdevkit:mainOct 22, 2021
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Generate PaymentPathFailed for MonitorUpdate-sourced HTLC failures when we don't have a corresponding pending payment entry

3 participants

@TheBlueMatt@jkczyz@valentinewallace