Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Fix to_remote SpendableOutputs generation in rare reorg cases #1022

Merged

Conversation

TheBlueMatt
Copy link
Collaborator

If we first see a local commitment transaction, and then a reorg
causes the confirmed channel close transaction to instead be a
remote commitment transaction, we would fail a spurious if else
check, resulting in us not generating the correct SpendableOutput
event for the to_remote output now confirmed on chain.

This resolves the incorrect logic and adds a regression test.

@TheBlueMatt TheBlueMatt added this to the 0.0.100 milestone Jul 29, 2021
@TheBlueMatt
Copy link
Collaborator Author

Obviously a pretty important bugfix, so tagging 0.0.100.

@TheBlueMatt TheBlueMatt force-pushed the 2021-07-to-remote-reorg branch from 7282abd to 8a9b7e5 Compare July 29, 2021 20:47
Copy link
Contributor

@valentinewallace valentinewallace left a comment

Choose a reason for hiding this comment

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

LGTM

Comment on lines +2455 to 2457
if let Some(ref broadcasted_holder_revokable_script) = self.broadcasted_holder_revokable_script {
if broadcasted_holder_revokable_script.0 == outp.script_pubkey {
spendable_output = Some(SpendableOutputDescriptor::DelayedPaymentOutput(DelayedPaymentOutputDescriptor {
Copy link
Contributor

Choose a reason for hiding this comment

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

Added some print statements and it seems like the issue was: it'd enter this else if statement, but wouldn't pass the check on the second highlighted line, and then naturally fail to even consider the next else if check

Copy link
Contributor

Choose a reason for hiding this comment

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

Given the second if clause overwrites spendable_output, it seems like essentially a reordering of the clauses?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

That might have worked too, but it's really the if else blocks miss that one in the middle isn't fully captured by the check. Because the check is split across the if let and the inner if, we sometimes incorrectly short-circuit the if else tree.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, I was wondering if they could have possibly been merge-able into one condition.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

No, rather obnoxiously Rust doesn't let you merge if let into any additional constraints.

Copy link

@ariard ariard Aug 3, 2021

Choose a reason for hiding this comment

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

Here my understanding of the fixed issue.

If a holder commitment is broadcast, we set ChannelMonitor::broadcasted_holder_revokable_script to Some in check_spend_holder_transaction, from then always marking the spendable output as a DelayedPaymentOutput in is_paying_spendable_output.

After this PR, the spendable output type detection is moved from a "else if" control flow to a "if" one, thus preventing the accidental jump over StaticPaymentOutput detection.

assert_eq!(outputs.len(), 1);
let spend_tx = nodes[0].keys_manager.backing.spend_spendable_outputs(&[&outputs[0]], Vec::new(),
Builder::new().push_opcode(opcodes::all::OP_RETURN).into_script(), 253, &Secp256k1::new()).unwrap();
check_spends!(spend_tx, remote_txn_b[0]);
Copy link
Contributor

Choose a reason for hiding this comment

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

admittedly not really the place for this remark, but check_spends sounds awfully vague. Especially with the macro not being defined in this file, a more descriptive name might be helpful for readers.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Can't say I disagree. There's a lot of open PRs right now, however, so if we want to change it maybe we should wait until things are a bit calmer in a month or two.

// nodes[1] is waiting for the to_self_delay to expire, which is many more than
// ANTI_REORG_DELAY. Instead, walk it back and confirm the original remote_txn_a commitment
// again and check that nodes[1] generates a similar spendable output.
// Technically a reorg of ANTI_REORG_DELAY violates our assumptions, so this is undefined by
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we have a list for which events we wouldn't know how to handle correctly based on max possible reorg size assumptions? Might be useful to link from this comment.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I think probably roughly all of them. In general we don't bother to correctly handle anything that includes a reorg of ANTI_REORG_DELAY. Many things are likely to work just fine, probably, but it's outside our security model entirely. This could maybe be better documented.

Comment on lines +2455 to 2457
if let Some(ref broadcasted_holder_revokable_script) = self.broadcasted_holder_revokable_script {
if broadcasted_holder_revokable_script.0 == outp.script_pubkey {
spendable_output = Some(SpendableOutputDescriptor::DelayedPaymentOutput(DelayedPaymentOutputDescriptor {
Copy link
Contributor

Choose a reason for hiding this comment

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

Given the second if clause overwrites spendable_output, it seems like essentially a reordering of the clauses?

@ariard
Copy link

ariard commented Aug 3, 2021

Nit, can you take this : ariard@6747827 ?

Copy link

@ariard ariard left a comment

Choose a reason for hiding this comment

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

Otherwise ACK 8a9b7e5.

I did verify the presence of the current issue and effectiveness of the fix. Mea culpa for this, iirc i introduced is_paying_spendable_output

@@ -2451,7 +2451,8 @@ impl<Signer: Sign> ChannelMonitorImpl<Signer> {
output: outp.clone(),
});
break;
} else if let Some(ref broadcasted_holder_revokable_script) = self.broadcasted_holder_revokable_script {
}
if let Some(ref broadcasted_holder_revokable_script) = self.broadcasted_holder_revokable_script {
Copy link

Choose a reason for hiding this comment

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

One step further to minimize unseen interactions like this could be to unset self.broadcasted_holder_revokeable_script in block_disconnected/transaction_unconfirmed's onchain_events_awaiting_threshold_conf. If the removed event is of type MaturingOutput and SpendableOutputDescriptor == DelayedPaymentOutput, self.broadcasted_holder_revokeable_script==None I think ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I mean with this no longer being an if else branch list it should "always work", and unsettnig scripts to match seems risky.

@TheBlueMatt TheBlueMatt force-pushed the 2021-07-to-remote-reorg branch from 8a9b7e5 to 0545842 Compare August 4, 2021 02:33
If we first see a local commitment transaction, and then a reorg
causes the confirmed channel close transaction to instead be a
remote commitment transaction, we would fail a spurious `if else`
check, resulting in us not generating the correct `SpendableOutput`
event for the to_remote output now confirmed on chain.

This resolves the incorrect logic and adds a regression test.
@TheBlueMatt TheBlueMatt force-pushed the 2021-07-to-remote-reorg branch from 0545842 to ad44590 Compare August 4, 2021 02:35
@TheBlueMatt
Copy link
Collaborator Author

Nit, can you take this : ariard/rust-lightning@6747827 ?

I have several more PRs coming in this code area, I'll throw it in the next one.

Otherwise, added a mut which is needed for older rustc support and condensed a comment as @ariard suggested. Diff from previous ACKs is trivial so will take after CI:

$ git diff-tree -U1 8a9b7e5a ad445908
diff --git a/lightning/src/ln/reorg_tests.rs b/lightning/src/ln/reorg_tests.rs
index d4dce6d5..7845c089 100644
--- a/lightning/src/ln/reorg_tests.rs
+++ b/lightning/src/ln/reorg_tests.rs
@@ -435,6 +435,6 @@ fn test_set_outpoints_partial_claiming() {
 fn do_test_to_remote_after_local_detection(style: ConnectStyle) {
-	// In previous code, detection of commitment transaction to_remote outputs in a remote
-	// commitment transaction was dependent on whether a local commitment transaction had been seen
-	// on-chain previously. This resulted in some edge cases around not being able to generate a
-	// SpendableOutput event after a reorg.
+	// In previous code, detection of to_remote outputs in a counterparty commitment transaction
+	// was dependent on whether a local commitment transaction had been seen on-chain previously.
+	// This resulted in some edge cases around not being able to generate a SpendableOutput event
+	// after a reorg.
 	//
@@ -446,3 +446,3 @@ fn do_test_to_remote_after_local_detection(style: ConnectStyle) {
 	let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]);
-	let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
+	let mut nodes = create_network(2, &node_cfgs, &node_chanmgrs);
 
$

@codecov
Copy link

codecov bot commented Aug 4, 2021

Codecov Report

Merging #1022 (ad44590) into main (e26c9b0) will increase coverage by 0.67%.
The diff coverage is 100.00%.

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #1022      +/-   ##
==========================================
+ Coverage   90.82%   91.49%   +0.67%     
==========================================
  Files          60       61       +1     
  Lines       31455    39398    +7943     
==========================================
+ Hits        28568    36046    +7478     
- Misses       2887     3352     +465     
Impacted Files Coverage Δ
lightning/src/chain/channelmonitor.rs 90.80% <100.00%> (+<0.01%) ⬆️
lightning/src/ln/reorg_tests.rs 99.70% <100.00%> (+0.07%) ⬆️
lightning/src/util/events.rs 12.50% <0.00%> (-2.64%) ⬇️
lightning/src/ln/functional_tests.rs 96.78% <0.00%> (-0.60%) ⬇️
lightning/src/ln/chanmon_update_fail_tests.rs 97.25% <0.00%> (-0.49%) ⬇️
lightning/src/lib.rs 100.00% <0.00%> (ø)
lightning/src/ln/features.rs 99.56% <0.00%> (+0.12%) ⬆️
lightning/src/ln/onion_utils.rs 95.21% <0.00%> (+0.29%) ⬆️
lightning-block-sync/src/http.rs 94.04% <0.00%> (+0.33%) ⬆️
lightning/src/chain/onchaintx.rs 94.66% <0.00%> (+0.47%) ⬆️
... and 15 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 e26c9b0...ad44590. Read the comment docs.

@TheBlueMatt TheBlueMatt merged commit 09e1670 into lightningdevkit:main Aug 4, 2021
Sign up for free to 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.

4 participants