Add outpoint index in watch_outputs to fix tracking - #653

Merged
TheBlueMatt merged 3 commits into
lightningdevkit:masterfrom
ariard:2020-06-fix-outputs-tracking
Oct 15, 2020
Merged

Add outpoint index in watch_outputs to fix tracking#653
TheBlueMatt merged 3 commits into
lightningdevkit:masterfrom
ariard:2020-06-fix-outputs-tracking

Conversation

@ariard

Copy link
Copy Markdown

Previously, outputs were monitored based on txid and an index yelled
from an enumeration over the returned selected outputs by monitoring
code. This is broken we don't have a guarantee that HTLC outputs are
ranking first after introduction of anchor outputs.

I think alternatively we can fix sorting in build_commitment_transaction to always order HTLCs first but sounds less robust to me.

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

Can you elaborate on "don't have a guarantee that HTLC outputs are ranking first after introduction of anchor outputs."? Specifically, we should always know exactly what the list of outputs in a commitment transaction is, why can we not use that?

@ariard

Copy link
Copy Markdown
Author

On "don't have a guarantee that HTLC outputs are ranking first after introduction of anchor outputs" it needs an amendment, I think we don't have previously guarantee that HTLC outputs were ranking first before to_local/to_remote as comparators are in order : value, script_pubkey, (timelocks), (hash). So this issue sounds to have been silently avoided by our test framework.

We always know the list but not their order and that matters to match by outpoint ?

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

Right, but I don't see where the current code is making any assumptions about HTLC output ordering - watch_outputs seems to always be called with something like watch_outputs.append(&mut tx.output.clone()); which means enumerate() does the correct thing.

@jkczyz

Copy link
Copy Markdown
Contributor

I ran across an issue today that looks to be resolved by this PR. Here we are pushing outputs to watch and later assume they are indexed by how they appear in the transaction.

@TheBlueMatt concurred that this fix is appropriate.

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

Right, I think I realized this was actually right (and we get it wrong in a few places), but forgot to comment here. In any case, this really needs a robust test to ensure we never hit such an error in the future - our test chain monitoring code should refuse to match things that don't have the correct output index.

@TheBlueMattTheBlueMatt added this to the 0.0.12 milestone Sep 27, 2020
@TheBlueMatt

Copy link
Copy Markdown
Collaborator

Is this fixed in #649 or do we need to rebase this on top of it/ whats the status here?

@ariard

Copy link
Copy Markdown
Author

@TheBlueMatt@jkczyz I'll rebase this on top of #649. Without I've test breakage on my anchor branch, but surely needs it own test coverage.

@ariard

Copy link
Copy Markdown
Author

@TheBlueMatt@jkczyz Thanks for review finally updated at 80c0e8c, see commit messages for explaining the bug. Or IRC conv of 10/06/2020.

@codecov

codecovBot commented Oct 7, 2020

Copy link
Copy Markdown

Codecov Report

Merging #653 into master will decrease coverage by 0.04%.
The diff coverage is 95.52%.

Impacted file tree graph

@@ Coverage Diff @@## master #653 +/- ##
==========================================
- Coverage 91.39% 91.35% -0.05% 
==========================================
Files 37 37 Lines 21964 21974 +10 ==========================================
Hits 20074 20074 - Misses 1890 1900 +10 
Impacted FilesCoverage Δ
lightning/src/chain/channelmonitor.rs95.52% <91.42%> (-0.20%)⬇️
lightning/src/chain/chainmonitor.rs97.10% <100.00%> (ø)
lightning/src/ln/functional_tests.rs96.98% <100.00%> (-0.13%)⬇️

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 df778b6...27ee115. Read the comment docs.

@TheBlueMattTheBlueMatt left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I'm a little confused why something like this doesn't catch the bug, even on your new test:

@@ -1811,10 +1811,32 @@ impl<ChanSigner: ChannelKeys> ChannelMonitor<ChanSigner> {
/// Checks if a given transaction spends any watched outputs.
fn spends_watched_output(&self, tx: &Transaction) -> bool {
+ #[cfg(test)]
+ {
+ // If we see a transaction which we registered previously, make sure the registration
+ // matches the actual transaction.
+ if let Some(outputs) = self.get_outputs_to_watch().get(&tx.txid()) {
+ for (idx, script_pubkey) in outputs.iter().enumerate() {
+ assert!(idx < tx.output.len());
+ assert_eq!(tx.output[idx].script_pubkey, *script_pubkey);
+ }
+ }
+ }
for input in tx.input.iter() {
if let Some(outputs) = self.get_outputs_to_watch().get(&input.previous_output.txid) {
for (idx, _script_pubkey) in outputs.iter().enumerate() {
if idx == input.previous_output.vout as usize {
+ #[cfg(test)]
+ {
+ // If the expected script is a known type, check that the witness
+ // appears to be spending the correct type (ie that the match would
+ // actually succeed in BIP 158/159-style filters).
+ if _script_pubkey.is_v0_p2wsh() {
+ assert_eq!(&bitcoin::Address::p2wsh(&Script::from(input.witness.last().unwrap().clone()), bitcoin::Network::Bitcoin).script_pubkey(), _script_pubkey);
+ } else if _script_pubkey.is_v0_p2wpkh() {
+ assert_eq!(&bitcoin::Address::p2wpkh(&bitcoin::PublicKey::from_slice(&input.witness.last().unwrap()).unwrap(), bitcoin::Network::Bitcoin).unwrap().script_pubkey(), _script_pubkey);
+ }
+ }
return true;
}
}

Comment threadlightning/src/ln/functional_tests.rs Outdated
Comment threadlightning/src/ln/functional_tests.rs Outdated
@ariard

Copy link
Copy Markdown
Author

I'm a little confused why something like this doesn't catch the bug, even on your new test:

What did you observe ? I tested your diff on master with new test and effectively it's failing as the index as yelled by the iterator enumeration isn't the real index at which the output should be watched and filtered.

You just have watched_outputs.len() < commitment_tx.output.len()

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

What did you observe ?

It looked to me like your new test was failing at the assertion at the end both with and without the above diff, not ever hitting the new assertions, did I do something wrong?

@TheBlueMattTheBlueMatt left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I'd still like to keep the second part of the new assertions. While it doesn't hit here because we're spending something which didn't get registered, I could see us screwing up and registering a script wrong in the future without having the transaction in the current matched set.

 if let Some(outputs) = self.get_outputs_to_watch().get(&input.previous_output.txid) {
for (idx, _script_pubkey) in outputs.iter().enumerate() {
if idx == input.previous_output.vout as usize {
+ #[cfg(test)]
+ {
+ // If the expected script is a known type, check that the witness
+ // appears to be spending the correct type (ie that the match would
+ // actually succeed in BIP 158/159-style filters).
+ if _script_pubkey.is_v0_p2wsh() {
+ assert_eq!(&bitcoin::Address::p2wsh(&Script::from(input.witness.last().unwrap().clone()), bitcoin::Network::Bitcoin).script_pubkey(), _script_pubkey);
+ } else if _script_pubkey.is_v0_p2wpkh() {
+ assert_eq!(&bitcoin::Address::p2wpkh(&bitcoin::PublicKey::from_slice(&input.witness.last().unwrap()).unwrap(), bitcoin::Network::Bitcoin).unwrap().script_pubkey(), _script_pubkey);
+ } else { panic!(); }
+ }
return true;
}
}

Comment threadlightning/src/chain/channelmonitor.rs Outdated
Comment threadlightning/src/chain/channelmonitor.rs Outdated
Antoine Riard added 2 commits October 10, 2020 18:51
Previously, outputs were monitored based on txid and an index yelled
from an enumeration over the returned selected outputs by monitoring
code. This is always have been broken but was only discovered while
introducing anchor outputs as those ones rank always first per BIP69.
We didn't have test cases where a HTLC was bigger than a party balance
on a holder commitment and thus not ranking first.
Next commit introduce test coverage.
This test is a mutation to underscore the detetection logic bug
we had before lightningdevkit#653. HTLC value routed is above the remaining
balance, thus inverting HTLC and `to_remote` output. HTLC
will come second and it wouldn't be seen by pre-lightningdevkit#653 detection
as we were eneumerate()'ing on a watched outputs vector (Vec<TxOut>)
thus implictly relying on outputs order detection for correct
spending children filtering.
@ariard

Copy link
Copy Markdown
Author

Updated at 324edf1

See modification of your supplementary diff and caveat comment to keep passing test_no_failure_dust_htlc_local_commitment, which is intentionally throwing junk in monitoring code to test robustness.

if *idx == input.previous_output.vout {
#[cfg(test)]
{
// If the witness is empty this transaction is a dummy one expressely

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Can we just...drop that test and panic instead? What was the rationale behind connecting garbage that should only ever be an indication the user is being duped by a bogus chain source (which is explicitly not in our threat model, at least not yet).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Tested was added in #333, but can't find the rational. If I remember loosely, at some point we had bug in our dust HTLC canceling back logic at commitment transaction confirmation. Mutating with the following doesn't break the test so I presume it was an oversight as it doesn't actually cover anything. Removed.

Diff:

diff --git a/lightning/src/chain/channelmonitor.rs b/lightning/src/chain/channelmonitor.rs
index 3af98121..927d9d70 100644
--- a/lightning/src/chain/channelmonitor.rs
+++ b/lightning/src/chain/channelmonitor.rs
@@ -1418,12 +1418,12 @@ impl<ChanSigner: ChannelKeys> ChannelMonitor<ChanSigner> {
}
}
}
- if let Some(ref txid) = self.current_counterparty_commitment_txid {
- check_htlc_fails!(txid, "current", 'current_loop);
- }
- if let Some(ref txid) = self.prev_counterparty_commitment_txid {
- check_htlc_fails!(txid, "previous", 'prev_loop);
- }
+ //if let Some(ref txid) = self.current_counterparty_commitment_txid {
+ // check_htlc_fails!(txid, "current", 'current_loop);
+ //}
+ //if let Some(ref txid) = self.prev_counterparty_commitment_txid {
+ // check_htlc_fails!(txid, "previous", 'prev_loop);
+ //}
if let Some(revocation_points) = self.their_cur_revocation_points {
let revocation_point_option =

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I think this test was added in case of future changes of the monitoring code (check_spend_counterparty) which may have broken the no-dust-HTLC-canceling-back.

We remove test_no_failure_dust_htlc_local_commitment from our test
framework as this test deliberately throwing junk transaction in
our monitoring parsing code is hitting new assertions.
This test was added in lightningdevkit#333, but it sounds as an oversight as the
correctness intention of this test (i.e verifying lack of dust
HTLCs canceling back in case of junk commitment transaction) doesn't
currently break.
let output_scripts = txouts.iter().map(|o| o.script_pubkey.clone()).collect();
self.outputs_to_watch.insert(txid.clone(), output_scripts).is_none()
let idx_and_scripts = txouts.iter().map(|o| (o.0, o.1.script_pubkey.clone())).collect();
self.outputs_to_watch.insert(txid.clone(), idx_and_scripts).is_none()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Can we iterate the new watch txn to assert they're known types so that the panic!() two hunks down is definitely correct?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Actually, its all test-only, it doesnt matter.

@TheBlueMattTheBlueMatt left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

One comment, otherwise ACK.

@TheBlueMatt
TheBlueMatt merged commit 8a79877 into lightningdevkit:masterOct 15, 2020
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.

3 participants

@ariard@TheBlueMatt@jkczyz
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n 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;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Add outpoint index in watch_outputs to fix tracking - #653

Merged
TheBlueMatt merged 3 commits into
lightningdevkit:masterfrom
ariard:2020-06-fix-outputs-tracking
Oct 15, 2020
Merged

Add outpoint index in watch_outputs to fix tracking#653
TheBlueMatt merged 3 commits into
lightningdevkit:masterfrom
ariard:2020-06-fix-outputs-tracking

Conversation

@ariard

Copy link
Copy Markdown

Previously, outputs were monitored based on txid and an index yelled
from an enumeration over the returned selected outputs by monitoring
code. This is broken we don't have a guarantee that HTLC outputs are
ranking first after introduction of anchor outputs.

I think alternatively we can fix sorting in build_commitment_transaction to always order HTLCs first but sounds less robust to me.

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

Can you elaborate on "don't have a guarantee that HTLC outputs are ranking first after introduction of anchor outputs."? Specifically, we should always know exactly what the list of outputs in a commitment transaction is, why can we not use that?

@ariard

Copy link
Copy Markdown
Author

On "don't have a guarantee that HTLC outputs are ranking first after introduction of anchor outputs" it needs an amendment, I think we don't have previously guarantee that HTLC outputs were ranking first before to_local/to_remote as comparators are in order : value, script_pubkey, (timelocks), (hash). So this issue sounds to have been silently avoided by our test framework.

We always know the list but not their order and that matters to match by outpoint ?

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

Right, but I don't see where the current code is making any assumptions about HTLC output ordering - watch_outputs seems to always be called with something like watch_outputs.append(&mut tx.output.clone()); which means enumerate() does the correct thing.

@jkczyz

Copy link
Copy Markdown
Contributor

I ran across an issue today that looks to be resolved by this PR. Here we are pushing outputs to watch and later assume they are indexed by how they appear in the transaction.

@TheBlueMatt concurred that this fix is appropriate.

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

Right, I think I realized this was actually right (and we get it wrong in a few places), but forgot to comment here. In any case, this really needs a robust test to ensure we never hit such an error in the future - our test chain monitoring code should refuse to match things that don't have the correct output index.

@TheBlueMattTheBlueMatt added this to the 0.0.12 milestone Sep 27, 2020
@TheBlueMatt

Copy link
Copy Markdown
Collaborator

Is this fixed in #649 or do we need to rebase this on top of it/ whats the status here?

@ariard

Copy link
Copy Markdown
Author

@TheBlueMatt@jkczyz I'll rebase this on top of #649. Without I've test breakage on my anchor branch, but surely needs it own test coverage.

@ariard

Copy link
Copy Markdown
Author

@TheBlueMatt@jkczyz Thanks for review finally updated at 80c0e8c, see commit messages for explaining the bug. Or IRC conv of 10/06/2020.

@codecov

codecovBot commented Oct 7, 2020

Copy link
Copy Markdown

Codecov Report

Merging #653 into master will decrease coverage by 0.04%.
The diff coverage is 95.52%.

Impacted file tree graph

@@ Coverage Diff @@## master #653 +/- ##
==========================================
- Coverage 91.39% 91.35% -0.05% 
==========================================
Files 37 37 Lines 21964 21974 +10 ==========================================
Hits 20074 20074 - Misses 1890 1900 +10 
Impacted FilesCoverage Δ
lightning/src/chain/channelmonitor.rs95.52% <91.42%> (-0.20%)⬇️
lightning/src/chain/chainmonitor.rs97.10% <100.00%> (ø)
lightning/src/ln/functional_tests.rs96.98% <100.00%> (-0.13%)⬇️

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 df778b6...27ee115. Read the comment docs.

@TheBlueMattTheBlueMatt left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I'm a little confused why something like this doesn't catch the bug, even on your new test:

@@ -1811,10 +1811,32 @@ impl<ChanSigner: ChannelKeys> ChannelMonitor<ChanSigner> {
/// Checks if a given transaction spends any watched outputs.
fn spends_watched_output(&self, tx: &Transaction) -> bool {
+ #[cfg(test)]
+ {
+ // If we see a transaction which we registered previously, make sure the registration
+ // matches the actual transaction.
+ if let Some(outputs) = self.get_outputs_to_watch().get(&tx.txid()) {
+ for (idx, script_pubkey) in outputs.iter().enumerate() {
+ assert!(idx < tx.output.len());
+ assert_eq!(tx.output[idx].script_pubkey, *script_pubkey);
+ }
+ }
+ }
for input in tx.input.iter() {
if let Some(outputs) = self.get_outputs_to_watch().get(&input.previous_output.txid) {
for (idx, _script_pubkey) in outputs.iter().enumerate() {
if idx == input.previous_output.vout as usize {
+ #[cfg(test)]
+ {
+ // If the expected script is a known type, check that the witness
+ // appears to be spending the correct type (ie that the match would
+ // actually succeed in BIP 158/159-style filters).
+ if _script_pubkey.is_v0_p2wsh() {
+ assert_eq!(&bitcoin::Address::p2wsh(&Script::from(input.witness.last().unwrap().clone()), bitcoin::Network::Bitcoin).script_pubkey(), _script_pubkey);
+ } else if _script_pubkey.is_v0_p2wpkh() {
+ assert_eq!(&bitcoin::Address::p2wpkh(&bitcoin::PublicKey::from_slice(&input.witness.last().unwrap()).unwrap(), bitcoin::Network::Bitcoin).unwrap().script_pubkey(), _script_pubkey);
+ }
+ }
return true;
}
}

Comment threadlightning/src/ln/functional_tests.rs Outdated
Comment threadlightning/src/ln/functional_tests.rs Outdated
@ariard

Copy link
Copy Markdown
Author

I'm a little confused why something like this doesn't catch the bug, even on your new test:

What did you observe ? I tested your diff on master with new test and effectively it's failing as the index as yelled by the iterator enumeration isn't the real index at which the output should be watched and filtered.

You just have watched_outputs.len() < commitment_tx.output.len()

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

What did you observe ?

It looked to me like your new test was failing at the assertion at the end both with and without the above diff, not ever hitting the new assertions, did I do something wrong?

@TheBlueMattTheBlueMatt left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I'd still like to keep the second part of the new assertions. While it doesn't hit here because we're spending something which didn't get registered, I could see us screwing up and registering a script wrong in the future without having the transaction in the current matched set.

 if let Some(outputs) = self.get_outputs_to_watch().get(&input.previous_output.txid) {
for (idx, _script_pubkey) in outputs.iter().enumerate() {
if idx == input.previous_output.vout as usize {
+ #[cfg(test)]
+ {
+ // If the expected script is a known type, check that the witness
+ // appears to be spending the correct type (ie that the match would
+ // actually succeed in BIP 158/159-style filters).
+ if _script_pubkey.is_v0_p2wsh() {
+ assert_eq!(&bitcoin::Address::p2wsh(&Script::from(input.witness.last().unwrap().clone()), bitcoin::Network::Bitcoin).script_pubkey(), _script_pubkey);
+ } else if _script_pubkey.is_v0_p2wpkh() {
+ assert_eq!(&bitcoin::Address::p2wpkh(&bitcoin::PublicKey::from_slice(&input.witness.last().unwrap()).unwrap(), bitcoin::Network::Bitcoin).unwrap().script_pubkey(), _script_pubkey);
+ } else { panic!(); }
+ }
return true;
}
}

Comment threadlightning/src/chain/channelmonitor.rs Outdated
Comment threadlightning/src/chain/channelmonitor.rs Outdated
Antoine Riard added 2 commits October 10, 2020 18:51
Previously, outputs were monitored based on txid and an index yelled
from an enumeration over the returned selected outputs by monitoring
code. This is always have been broken but was only discovered while
introducing anchor outputs as those ones rank always first per BIP69.
We didn't have test cases where a HTLC was bigger than a party balance
on a holder commitment and thus not ranking first.
Next commit introduce test coverage.
This test is a mutation to underscore the detetection logic bug
we had before lightningdevkit#653. HTLC value routed is above the remaining
balance, thus inverting HTLC and `to_remote` output. HTLC
will come second and it wouldn't be seen by pre-lightningdevkit#653 detection
as we were eneumerate()'ing on a watched outputs vector (Vec<TxOut>)
thus implictly relying on outputs order detection for correct
spending children filtering.
@ariard

Copy link
Copy Markdown
Author

Updated at 324edf1

See modification of your supplementary diff and caveat comment to keep passing test_no_failure_dust_htlc_local_commitment, which is intentionally throwing junk in monitoring code to test robustness.

if *idx == input.previous_output.vout {
#[cfg(test)]
{
// If the witness is empty this transaction is a dummy one expressely

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Can we just...drop that test and panic instead? What was the rationale behind connecting garbage that should only ever be an indication the user is being duped by a bogus chain source (which is explicitly not in our threat model, at least not yet).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Tested was added in #333, but can't find the rational. If I remember loosely, at some point we had bug in our dust HTLC canceling back logic at commitment transaction confirmation. Mutating with the following doesn't break the test so I presume it was an oversight as it doesn't actually cover anything. Removed.

Diff:

diff --git a/lightning/src/chain/channelmonitor.rs b/lightning/src/chain/channelmonitor.rs
index 3af98121..927d9d70 100644
--- a/lightning/src/chain/channelmonitor.rs
+++ b/lightning/src/chain/channelmonitor.rs
@@ -1418,12 +1418,12 @@ impl<ChanSigner: ChannelKeys> ChannelMonitor<ChanSigner> {
}
}
}
- if let Some(ref txid) = self.current_counterparty_commitment_txid {
- check_htlc_fails!(txid, "current", 'current_loop);
- }
- if let Some(ref txid) = self.prev_counterparty_commitment_txid {
- check_htlc_fails!(txid, "previous", 'prev_loop);
- }
+ //if let Some(ref txid) = self.current_counterparty_commitment_txid {
+ // check_htlc_fails!(txid, "current", 'current_loop);
+ //}
+ //if let Some(ref txid) = self.prev_counterparty_commitment_txid {
+ // check_htlc_fails!(txid, "previous", 'prev_loop);
+ //}
if let Some(revocation_points) = self.their_cur_revocation_points {
let revocation_point_option =

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I think this test was added in case of future changes of the monitoring code (check_spend_counterparty) which may have broken the no-dust-HTLC-canceling-back.

We remove test_no_failure_dust_htlc_local_commitment from our test
framework as this test deliberately throwing junk transaction in
our monitoring parsing code is hitting new assertions.
This test was added in lightningdevkit#333, but it sounds as an oversight as the
correctness intention of this test (i.e verifying lack of dust
HTLCs canceling back in case of junk commitment transaction) doesn't
currently break.
let output_scripts = txouts.iter().map(|o| o.script_pubkey.clone()).collect();
self.outputs_to_watch.insert(txid.clone(), output_scripts).is_none()
let idx_and_scripts = txouts.iter().map(|o| (o.0, o.1.script_pubkey.clone())).collect();
self.outputs_to_watch.insert(txid.clone(), idx_and_scripts).is_none()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Can we iterate the new watch txn to assert they're known types so that the panic!() two hunks down is definitely correct?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Actually, its all test-only, it doesnt matter.

@TheBlueMattTheBlueMatt left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

One comment, otherwise ACK.

@TheBlueMatt
TheBlueMatt merged commit 8a79877 into lightningdevkit:masterOct 15, 2020
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.

3 participants

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

Add outpoint index in watch_outputs to fix tracking - #653

Merged
TheBlueMatt merged 3 commits into
lightningdevkit:masterfrom
ariard:2020-06-fix-outputs-tracking
Oct 15, 2020
Merged

Add outpoint index in watch_outputs to fix tracking#653
TheBlueMatt merged 3 commits into
lightningdevkit:masterfrom
ariard:2020-06-fix-outputs-tracking

Conversation

@ariard

Copy link
Copy Markdown

Previously, outputs were monitored based on txid and an index yelled
from an enumeration over the returned selected outputs by monitoring
code. This is broken we don't have a guarantee that HTLC outputs are
ranking first after introduction of anchor outputs.

I think alternatively we can fix sorting in build_commitment_transaction to always order HTLCs first but sounds less robust to me.

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

Can you elaborate on "don't have a guarantee that HTLC outputs are ranking first after introduction of anchor outputs."? Specifically, we should always know exactly what the list of outputs in a commitment transaction is, why can we not use that?

@ariard

Copy link
Copy Markdown
Author

On "don't have a guarantee that HTLC outputs are ranking first after introduction of anchor outputs" it needs an amendment, I think we don't have previously guarantee that HTLC outputs were ranking first before to_local/to_remote as comparators are in order : value, script_pubkey, (timelocks), (hash). So this issue sounds to have been silently avoided by our test framework.

We always know the list but not their order and that matters to match by outpoint ?

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

Right, but I don't see where the current code is making any assumptions about HTLC output ordering - watch_outputs seems to always be called with something like watch_outputs.append(&mut tx.output.clone()); which means enumerate() does the correct thing.

@jkczyz

Copy link
Copy Markdown
Contributor

I ran across an issue today that looks to be resolved by this PR. Here we are pushing outputs to watch and later assume they are indexed by how they appear in the transaction.

@TheBlueMatt concurred that this fix is appropriate.

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

Right, I think I realized this was actually right (and we get it wrong in a few places), but forgot to comment here. In any case, this really needs a robust test to ensure we never hit such an error in the future - our test chain monitoring code should refuse to match things that don't have the correct output index.

@TheBlueMattTheBlueMatt added this to the 0.0.12 milestone Sep 27, 2020
@TheBlueMatt

Copy link
Copy Markdown
Collaborator

Is this fixed in #649 or do we need to rebase this on top of it/ whats the status here?

@ariard

Copy link
Copy Markdown
Author

@TheBlueMatt@jkczyz I'll rebase this on top of #649. Without I've test breakage on my anchor branch, but surely needs it own test coverage.

@ariard

Copy link
Copy Markdown
Author

@TheBlueMatt@jkczyz Thanks for review finally updated at 80c0e8c, see commit messages for explaining the bug. Or IRC conv of 10/06/2020.

@codecov

codecovBot commented Oct 7, 2020

Copy link
Copy Markdown

Codecov Report

Merging #653 into master will decrease coverage by 0.04%.
The diff coverage is 95.52%.

Impacted file tree graph

@@ Coverage Diff @@## master #653 +/- ##
==========================================
- Coverage 91.39% 91.35% -0.05% 
==========================================
Files 37 37 Lines 21964 21974 +10 ==========================================
Hits 20074 20074 - Misses 1890 1900 +10 
Impacted FilesCoverage Δ
lightning/src/chain/channelmonitor.rs95.52% <91.42%> (-0.20%)⬇️
lightning/src/chain/chainmonitor.rs97.10% <100.00%> (ø)
lightning/src/ln/functional_tests.rs96.98% <100.00%> (-0.13%)⬇️

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 df778b6...27ee115. Read the comment docs.

@TheBlueMattTheBlueMatt left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I'm a little confused why something like this doesn't catch the bug, even on your new test:

@@ -1811,10 +1811,32 @@ impl<ChanSigner: ChannelKeys> ChannelMonitor<ChanSigner> {
/// Checks if a given transaction spends any watched outputs.
fn spends_watched_output(&self, tx: &Transaction) -> bool {
+ #[cfg(test)]
+ {
+ // If we see a transaction which we registered previously, make sure the registration
+ // matches the actual transaction.
+ if let Some(outputs) = self.get_outputs_to_watch().get(&tx.txid()) {
+ for (idx, script_pubkey) in outputs.iter().enumerate() {
+ assert!(idx < tx.output.len());
+ assert_eq!(tx.output[idx].script_pubkey, *script_pubkey);
+ }
+ }
+ }
for input in tx.input.iter() {
if let Some(outputs) = self.get_outputs_to_watch().get(&input.previous_output.txid) {
for (idx, _script_pubkey) in outputs.iter().enumerate() {
if idx == input.previous_output.vout as usize {
+ #[cfg(test)]
+ {
+ // If the expected script is a known type, check that the witness
+ // appears to be spending the correct type (ie that the match would
+ // actually succeed in BIP 158/159-style filters).
+ if _script_pubkey.is_v0_p2wsh() {
+ assert_eq!(&bitcoin::Address::p2wsh(&Script::from(input.witness.last().unwrap().clone()), bitcoin::Network::Bitcoin).script_pubkey(), _script_pubkey);
+ } else if _script_pubkey.is_v0_p2wpkh() {
+ assert_eq!(&bitcoin::Address::p2wpkh(&bitcoin::PublicKey::from_slice(&input.witness.last().unwrap()).unwrap(), bitcoin::Network::Bitcoin).unwrap().script_pubkey(), _script_pubkey);
+ }
+ }
return true;
}
}

Comment threadlightning/src/ln/functional_tests.rs Outdated
Comment threadlightning/src/ln/functional_tests.rs Outdated
@ariard

Copy link
Copy Markdown
Author

I'm a little confused why something like this doesn't catch the bug, even on your new test:

What did you observe ? I tested your diff on master with new test and effectively it's failing as the index as yelled by the iterator enumeration isn't the real index at which the output should be watched and filtered.

You just have watched_outputs.len() < commitment_tx.output.len()

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

What did you observe ?

It looked to me like your new test was failing at the assertion at the end both with and without the above diff, not ever hitting the new assertions, did I do something wrong?

@TheBlueMattTheBlueMatt left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I'd still like to keep the second part of the new assertions. While it doesn't hit here because we're spending something which didn't get registered, I could see us screwing up and registering a script wrong in the future without having the transaction in the current matched set.

 if let Some(outputs) = self.get_outputs_to_watch().get(&input.previous_output.txid) {
for (idx, _script_pubkey) in outputs.iter().enumerate() {
if idx == input.previous_output.vout as usize {
+ #[cfg(test)]
+ {
+ // If the expected script is a known type, check that the witness
+ // appears to be spending the correct type (ie that the match would
+ // actually succeed in BIP 158/159-style filters).
+ if _script_pubkey.is_v0_p2wsh() {
+ assert_eq!(&bitcoin::Address::p2wsh(&Script::from(input.witness.last().unwrap().clone()), bitcoin::Network::Bitcoin).script_pubkey(), _script_pubkey);
+ } else if _script_pubkey.is_v0_p2wpkh() {
+ assert_eq!(&bitcoin::Address::p2wpkh(&bitcoin::PublicKey::from_slice(&input.witness.last().unwrap()).unwrap(), bitcoin::Network::Bitcoin).unwrap().script_pubkey(), _script_pubkey);
+ } else { panic!(); }
+ }
return true;
}
}

Comment threadlightning/src/chain/channelmonitor.rs Outdated
Comment threadlightning/src/chain/channelmonitor.rs Outdated
Antoine Riard added 2 commits October 10, 2020 18:51
Previously, outputs were monitored based on txid and an index yelled
from an enumeration over the returned selected outputs by monitoring
code. This is always have been broken but was only discovered while
introducing anchor outputs as those ones rank always first per BIP69.
We didn't have test cases where a HTLC was bigger than a party balance
on a holder commitment and thus not ranking first.
Next commit introduce test coverage.
This test is a mutation to underscore the detetection logic bug
we had before lightningdevkit#653. HTLC value routed is above the remaining
balance, thus inverting HTLC and `to_remote` output. HTLC
will come second and it wouldn't be seen by pre-lightningdevkit#653 detection
as we were eneumerate()'ing on a watched outputs vector (Vec<TxOut>)
thus implictly relying on outputs order detection for correct
spending children filtering.
@ariard

Copy link
Copy Markdown
Author

Updated at 324edf1

See modification of your supplementary diff and caveat comment to keep passing test_no_failure_dust_htlc_local_commitment, which is intentionally throwing junk in monitoring code to test robustness.

if *idx == input.previous_output.vout {
#[cfg(test)]
{
// If the witness is empty this transaction is a dummy one expressely

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Can we just...drop that test and panic instead? What was the rationale behind connecting garbage that should only ever be an indication the user is being duped by a bogus chain source (which is explicitly not in our threat model, at least not yet).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Tested was added in #333, but can't find the rational. If I remember loosely, at some point we had bug in our dust HTLC canceling back logic at commitment transaction confirmation. Mutating with the following doesn't break the test so I presume it was an oversight as it doesn't actually cover anything. Removed.

Diff:

diff --git a/lightning/src/chain/channelmonitor.rs b/lightning/src/chain/channelmonitor.rs
index 3af98121..927d9d70 100644
--- a/lightning/src/chain/channelmonitor.rs
+++ b/lightning/src/chain/channelmonitor.rs
@@ -1418,12 +1418,12 @@ impl<ChanSigner: ChannelKeys> ChannelMonitor<ChanSigner> {
}
}
}
- if let Some(ref txid) = self.current_counterparty_commitment_txid {
- check_htlc_fails!(txid, "current", 'current_loop);
- }
- if let Some(ref txid) = self.prev_counterparty_commitment_txid {
- check_htlc_fails!(txid, "previous", 'prev_loop);
- }
+ //if let Some(ref txid) = self.current_counterparty_commitment_txid {
+ // check_htlc_fails!(txid, "current", 'current_loop);
+ //}
+ //if let Some(ref txid) = self.prev_counterparty_commitment_txid {
+ // check_htlc_fails!(txid, "previous", 'prev_loop);
+ //}
if let Some(revocation_points) = self.their_cur_revocation_points {
let revocation_point_option =

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I think this test was added in case of future changes of the monitoring code (check_spend_counterparty) which may have broken the no-dust-HTLC-canceling-back.

We remove test_no_failure_dust_htlc_local_commitment from our test
framework as this test deliberately throwing junk transaction in
our monitoring parsing code is hitting new assertions.
This test was added in lightningdevkit#333, but it sounds as an oversight as the
correctness intention of this test (i.e verifying lack of dust
HTLCs canceling back in case of junk commitment transaction) doesn't
currently break.
let output_scripts = txouts.iter().map(|o| o.script_pubkey.clone()).collect();
self.outputs_to_watch.insert(txid.clone(), output_scripts).is_none()
let idx_and_scripts = txouts.iter().map(|o| (o.0, o.1.script_pubkey.clone())).collect();
self.outputs_to_watch.insert(txid.clone(), idx_and_scripts).is_none()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Can we iterate the new watch txn to assert they're known types so that the panic!() two hunks down is definitely correct?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Actually, its all test-only, it doesnt matter.

@TheBlueMattTheBlueMatt left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

One comment, otherwise ACK.

@TheBlueMatt
TheBlueMatt merged commit 8a79877 into lightningdevkit:masterOct 15, 2020
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.

3 participants

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

Add outpoint index in watch_outputs to fix tracking - #653

Merged
TheBlueMatt merged 3 commits into
lightningdevkit:masterfrom
ariard:2020-06-fix-outputs-tracking
Oct 15, 2020
Merged

Add outpoint index in watch_outputs to fix tracking#653
TheBlueMatt merged 3 commits into
lightningdevkit:masterfrom
ariard:2020-06-fix-outputs-tracking

Conversation

@ariard

Copy link
Copy Markdown

Previously, outputs were monitored based on txid and an index yelled
from an enumeration over the returned selected outputs by monitoring
code. This is broken we don't have a guarantee that HTLC outputs are
ranking first after introduction of anchor outputs.

I think alternatively we can fix sorting in build_commitment_transaction to always order HTLCs first but sounds less robust to me.

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

Can you elaborate on "don't have a guarantee that HTLC outputs are ranking first after introduction of anchor outputs."? Specifically, we should always know exactly what the list of outputs in a commitment transaction is, why can we not use that?

@ariard

Copy link
Copy Markdown
Author

On "don't have a guarantee that HTLC outputs are ranking first after introduction of anchor outputs" it needs an amendment, I think we don't have previously guarantee that HTLC outputs were ranking first before to_local/to_remote as comparators are in order : value, script_pubkey, (timelocks), (hash). So this issue sounds to have been silently avoided by our test framework.

We always know the list but not their order and that matters to match by outpoint ?

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

Right, but I don't see where the current code is making any assumptions about HTLC output ordering - watch_outputs seems to always be called with something like watch_outputs.append(&mut tx.output.clone()); which means enumerate() does the correct thing.

@jkczyz

Copy link
Copy Markdown
Contributor

I ran across an issue today that looks to be resolved by this PR. Here we are pushing outputs to watch and later assume they are indexed by how they appear in the transaction.

@TheBlueMatt concurred that this fix is appropriate.

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

Right, I think I realized this was actually right (and we get it wrong in a few places), but forgot to comment here. In any case, this really needs a robust test to ensure we never hit such an error in the future - our test chain monitoring code should refuse to match things that don't have the correct output index.

@TheBlueMattTheBlueMatt added this to the 0.0.12 milestone Sep 27, 2020
@TheBlueMatt

Copy link
Copy Markdown
Collaborator

Is this fixed in #649 or do we need to rebase this on top of it/ whats the status here?

@ariard

Copy link
Copy Markdown
Author

@TheBlueMatt@jkczyz I'll rebase this on top of #649. Without I've test breakage on my anchor branch, but surely needs it own test coverage.

@ariard

Copy link
Copy Markdown
Author

@TheBlueMatt@jkczyz Thanks for review finally updated at 80c0e8c, see commit messages for explaining the bug. Or IRC conv of 10/06/2020.

@codecov

codecovBot commented Oct 7, 2020

Copy link
Copy Markdown

Codecov Report

Merging #653 into master will decrease coverage by 0.04%.
The diff coverage is 95.52%.

Impacted file tree graph

@@ Coverage Diff @@## master #653 +/- ##
==========================================
- Coverage 91.39% 91.35% -0.05% 
==========================================
Files 37 37 Lines 21964 21974 +10 ==========================================
Hits 20074 20074 - Misses 1890 1900 +10 
Impacted FilesCoverage Δ
lightning/src/chain/channelmonitor.rs95.52% <91.42%> (-0.20%)⬇️
lightning/src/chain/chainmonitor.rs97.10% <100.00%> (ø)
lightning/src/ln/functional_tests.rs96.98% <100.00%> (-0.13%)⬇️

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 df778b6...27ee115. Read the comment docs.

@TheBlueMattTheBlueMatt left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I'm a little confused why something like this doesn't catch the bug, even on your new test:

@@ -1811,10 +1811,32 @@ impl<ChanSigner: ChannelKeys> ChannelMonitor<ChanSigner> {
/// Checks if a given transaction spends any watched outputs.
fn spends_watched_output(&self, tx: &Transaction) -> bool {
+ #[cfg(test)]
+ {
+ // If we see a transaction which we registered previously, make sure the registration
+ // matches the actual transaction.
+ if let Some(outputs) = self.get_outputs_to_watch().get(&tx.txid()) {
+ for (idx, script_pubkey) in outputs.iter().enumerate() {
+ assert!(idx < tx.output.len());
+ assert_eq!(tx.output[idx].script_pubkey, *script_pubkey);
+ }
+ }
+ }
for input in tx.input.iter() {
if let Some(outputs) = self.get_outputs_to_watch().get(&input.previous_output.txid) {
for (idx, _script_pubkey) in outputs.iter().enumerate() {
if idx == input.previous_output.vout as usize {
+ #[cfg(test)]
+ {
+ // If the expected script is a known type, check that the witness
+ // appears to be spending the correct type (ie that the match would
+ // actually succeed in BIP 158/159-style filters).
+ if _script_pubkey.is_v0_p2wsh() {
+ assert_eq!(&bitcoin::Address::p2wsh(&Script::from(input.witness.last().unwrap().clone()), bitcoin::Network::Bitcoin).script_pubkey(), _script_pubkey);
+ } else if _script_pubkey.is_v0_p2wpkh() {
+ assert_eq!(&bitcoin::Address::p2wpkh(&bitcoin::PublicKey::from_slice(&input.witness.last().unwrap()).unwrap(), bitcoin::Network::Bitcoin).unwrap().script_pubkey(), _script_pubkey);
+ }
+ }
return true;
}
}

Comment threadlightning/src/ln/functional_tests.rs Outdated
Comment threadlightning/src/ln/functional_tests.rs Outdated
@ariard

Copy link
Copy Markdown
Author

I'm a little confused why something like this doesn't catch the bug, even on your new test:

What did you observe ? I tested your diff on master with new test and effectively it's failing as the index as yelled by the iterator enumeration isn't the real index at which the output should be watched and filtered.

You just have watched_outputs.len() < commitment_tx.output.len()

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

What did you observe ?

It looked to me like your new test was failing at the assertion at the end both with and without the above diff, not ever hitting the new assertions, did I do something wrong?

@TheBlueMattTheBlueMatt left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I'd still like to keep the second part of the new assertions. While it doesn't hit here because we're spending something which didn't get registered, I could see us screwing up and registering a script wrong in the future without having the transaction in the current matched set.

 if let Some(outputs) = self.get_outputs_to_watch().get(&input.previous_output.txid) {
for (idx, _script_pubkey) in outputs.iter().enumerate() {
if idx == input.previous_output.vout as usize {
+ #[cfg(test)]
+ {
+ // If the expected script is a known type, check that the witness
+ // appears to be spending the correct type (ie that the match would
+ // actually succeed in BIP 158/159-style filters).
+ if _script_pubkey.is_v0_p2wsh() {
+ assert_eq!(&bitcoin::Address::p2wsh(&Script::from(input.witness.last().unwrap().clone()), bitcoin::Network::Bitcoin).script_pubkey(), _script_pubkey);
+ } else if _script_pubkey.is_v0_p2wpkh() {
+ assert_eq!(&bitcoin::Address::p2wpkh(&bitcoin::PublicKey::from_slice(&input.witness.last().unwrap()).unwrap(), bitcoin::Network::Bitcoin).unwrap().script_pubkey(), _script_pubkey);
+ } else { panic!(); }
+ }
return true;
}
}

Comment threadlightning/src/chain/channelmonitor.rs Outdated
Comment threadlightning/src/chain/channelmonitor.rs Outdated
Antoine Riard added 2 commits October 10, 2020 18:51
Previously, outputs were monitored based on txid and an index yelled
from an enumeration over the returned selected outputs by monitoring
code. This is always have been broken but was only discovered while
introducing anchor outputs as those ones rank always first per BIP69.
We didn't have test cases where a HTLC was bigger than a party balance
on a holder commitment and thus not ranking first.
Next commit introduce test coverage.
This test is a mutation to underscore the detetection logic bug
we had before lightningdevkit#653. HTLC value routed is above the remaining
balance, thus inverting HTLC and `to_remote` output. HTLC
will come second and it wouldn't be seen by pre-lightningdevkit#653 detection
as we were eneumerate()'ing on a watched outputs vector (Vec<TxOut>)
thus implictly relying on outputs order detection for correct
spending children filtering.
@ariard

Copy link
Copy Markdown
Author

Updated at 324edf1

See modification of your supplementary diff and caveat comment to keep passing test_no_failure_dust_htlc_local_commitment, which is intentionally throwing junk in monitoring code to test robustness.

if *idx == input.previous_output.vout {
#[cfg(test)]
{
// If the witness is empty this transaction is a dummy one expressely

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Can we just...drop that test and panic instead? What was the rationale behind connecting garbage that should only ever be an indication the user is being duped by a bogus chain source (which is explicitly not in our threat model, at least not yet).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Tested was added in #333, but can't find the rational. If I remember loosely, at some point we had bug in our dust HTLC canceling back logic at commitment transaction confirmation. Mutating with the following doesn't break the test so I presume it was an oversight as it doesn't actually cover anything. Removed.

Diff:

diff --git a/lightning/src/chain/channelmonitor.rs b/lightning/src/chain/channelmonitor.rs
index 3af98121..927d9d70 100644
--- a/lightning/src/chain/channelmonitor.rs
+++ b/lightning/src/chain/channelmonitor.rs
@@ -1418,12 +1418,12 @@ impl<ChanSigner: ChannelKeys> ChannelMonitor<ChanSigner> {
}
}
}
- if let Some(ref txid) = self.current_counterparty_commitment_txid {
- check_htlc_fails!(txid, "current", 'current_loop);
- }
- if let Some(ref txid) = self.prev_counterparty_commitment_txid {
- check_htlc_fails!(txid, "previous", 'prev_loop);
- }
+ //if let Some(ref txid) = self.current_counterparty_commitment_txid {
+ // check_htlc_fails!(txid, "current", 'current_loop);
+ //}
+ //if let Some(ref txid) = self.prev_counterparty_commitment_txid {
+ // check_htlc_fails!(txid, "previous", 'prev_loop);
+ //}
if let Some(revocation_points) = self.their_cur_revocation_points {
let revocation_point_option =

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I think this test was added in case of future changes of the monitoring code (check_spend_counterparty) which may have broken the no-dust-HTLC-canceling-back.

We remove test_no_failure_dust_htlc_local_commitment from our test
framework as this test deliberately throwing junk transaction in
our monitoring parsing code is hitting new assertions.
This test was added in lightningdevkit#333, but it sounds as an oversight as the
correctness intention of this test (i.e verifying lack of dust
HTLCs canceling back in case of junk commitment transaction) doesn't
currently break.
let output_scripts = txouts.iter().map(|o| o.script_pubkey.clone()).collect();
self.outputs_to_watch.insert(txid.clone(), output_scripts).is_none()
let idx_and_scripts = txouts.iter().map(|o| (o.0, o.1.script_pubkey.clone())).collect();
self.outputs_to_watch.insert(txid.clone(), idx_and_scripts).is_none()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Can we iterate the new watch txn to assert they're known types so that the panic!() two hunks down is definitely correct?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Actually, its all test-only, it doesnt matter.

@TheBlueMattTheBlueMatt left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

One comment, otherwise ACK.

@TheBlueMatt
TheBlueMatt merged commit 8a79877 into lightningdevkit:masterOct 15, 2020
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.

3 participants

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

Add outpoint index in watch_outputs to fix tracking - #653

Merged
TheBlueMatt merged 3 commits into
lightningdevkit:masterfrom
ariard:2020-06-fix-outputs-tracking
Oct 15, 2020
Merged

Add outpoint index in watch_outputs to fix tracking#653
TheBlueMatt merged 3 commits into
lightningdevkit:masterfrom
ariard:2020-06-fix-outputs-tracking

Conversation

@ariard

Copy link
Copy Markdown

Previously, outputs were monitored based on txid and an index yelled
from an enumeration over the returned selected outputs by monitoring
code. This is broken we don't have a guarantee that HTLC outputs are
ranking first after introduction of anchor outputs.

I think alternatively we can fix sorting in build_commitment_transaction to always order HTLCs first but sounds less robust to me.

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

Can you elaborate on "don't have a guarantee that HTLC outputs are ranking first after introduction of anchor outputs."? Specifically, we should always know exactly what the list of outputs in a commitment transaction is, why can we not use that?

@ariard

Copy link
Copy Markdown
Author

On "don't have a guarantee that HTLC outputs are ranking first after introduction of anchor outputs" it needs an amendment, I think we don't have previously guarantee that HTLC outputs were ranking first before to_local/to_remote as comparators are in order : value, script_pubkey, (timelocks), (hash). So this issue sounds to have been silently avoided by our test framework.

We always know the list but not their order and that matters to match by outpoint ?

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

Right, but I don't see where the current code is making any assumptions about HTLC output ordering - watch_outputs seems to always be called with something like watch_outputs.append(&mut tx.output.clone()); which means enumerate() does the correct thing.

@jkczyz

Copy link
Copy Markdown
Contributor

I ran across an issue today that looks to be resolved by this PR. Here we are pushing outputs to watch and later assume they are indexed by how they appear in the transaction.

@TheBlueMatt concurred that this fix is appropriate.

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

Right, I think I realized this was actually right (and we get it wrong in a few places), but forgot to comment here. In any case, this really needs a robust test to ensure we never hit such an error in the future - our test chain monitoring code should refuse to match things that don't have the correct output index.

@TheBlueMattTheBlueMatt added this to the 0.0.12 milestone Sep 27, 2020
@TheBlueMatt

Copy link
Copy Markdown
Collaborator

Is this fixed in #649 or do we need to rebase this on top of it/ whats the status here?

@ariard

Copy link
Copy Markdown
Author

@TheBlueMatt@jkczyz I'll rebase this on top of #649. Without I've test breakage on my anchor branch, but surely needs it own test coverage.

@ariard

Copy link
Copy Markdown
Author

@TheBlueMatt@jkczyz Thanks for review finally updated at 80c0e8c, see commit messages for explaining the bug. Or IRC conv of 10/06/2020.

@codecov

codecovBot commented Oct 7, 2020

Copy link
Copy Markdown

Codecov Report

Merging #653 into master will decrease coverage by 0.04%.
The diff coverage is 95.52%.

Impacted file tree graph

@@ Coverage Diff @@## master #653 +/- ##
==========================================
- Coverage 91.39% 91.35% -0.05% 
==========================================
Files 37 37 Lines 21964 21974 +10 ==========================================
Hits 20074 20074 - Misses 1890 1900 +10 
Impacted FilesCoverage Δ
lightning/src/chain/channelmonitor.rs95.52% <91.42%> (-0.20%)⬇️
lightning/src/chain/chainmonitor.rs97.10% <100.00%> (ø)
lightning/src/ln/functional_tests.rs96.98% <100.00%> (-0.13%)⬇️

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 df778b6...27ee115. Read the comment docs.

@TheBlueMattTheBlueMatt left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I'm a little confused why something like this doesn't catch the bug, even on your new test:

@@ -1811,10 +1811,32 @@ impl<ChanSigner: ChannelKeys> ChannelMonitor<ChanSigner> {
/// Checks if a given transaction spends any watched outputs.
fn spends_watched_output(&self, tx: &Transaction) -> bool {
+ #[cfg(test)]
+ {
+ // If we see a transaction which we registered previously, make sure the registration
+ // matches the actual transaction.
+ if let Some(outputs) = self.get_outputs_to_watch().get(&tx.txid()) {
+ for (idx, script_pubkey) in outputs.iter().enumerate() {
+ assert!(idx < tx.output.len());
+ assert_eq!(tx.output[idx].script_pubkey, *script_pubkey);
+ }
+ }
+ }
for input in tx.input.iter() {
if let Some(outputs) = self.get_outputs_to_watch().get(&input.previous_output.txid) {
for (idx, _script_pubkey) in outputs.iter().enumerate() {
if idx == input.previous_output.vout as usize {
+ #[cfg(test)]
+ {
+ // If the expected script is a known type, check that the witness
+ // appears to be spending the correct type (ie that the match would
+ // actually succeed in BIP 158/159-style filters).
+ if _script_pubkey.is_v0_p2wsh() {
+ assert_eq!(&bitcoin::Address::p2wsh(&Script::from(input.witness.last().unwrap().clone()), bitcoin::Network::Bitcoin).script_pubkey(), _script_pubkey);
+ } else if _script_pubkey.is_v0_p2wpkh() {
+ assert_eq!(&bitcoin::Address::p2wpkh(&bitcoin::PublicKey::from_slice(&input.witness.last().unwrap()).unwrap(), bitcoin::Network::Bitcoin).unwrap().script_pubkey(), _script_pubkey);
+ }
+ }
return true;
}
}

Comment threadlightning/src/ln/functional_tests.rs Outdated
Comment threadlightning/src/ln/functional_tests.rs Outdated
@ariard

Copy link
Copy Markdown
Author

I'm a little confused why something like this doesn't catch the bug, even on your new test:

What did you observe ? I tested your diff on master with new test and effectively it's failing as the index as yelled by the iterator enumeration isn't the real index at which the output should be watched and filtered.

You just have watched_outputs.len() < commitment_tx.output.len()

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

What did you observe ?

It looked to me like your new test was failing at the assertion at the end both with and without the above diff, not ever hitting the new assertions, did I do something wrong?

@TheBlueMattTheBlueMatt left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I'd still like to keep the second part of the new assertions. While it doesn't hit here because we're spending something which didn't get registered, I could see us screwing up and registering a script wrong in the future without having the transaction in the current matched set.

 if let Some(outputs) = self.get_outputs_to_watch().get(&input.previous_output.txid) {
for (idx, _script_pubkey) in outputs.iter().enumerate() {
if idx == input.previous_output.vout as usize {
+ #[cfg(test)]
+ {
+ // If the expected script is a known type, check that the witness
+ // appears to be spending the correct type (ie that the match would
+ // actually succeed in BIP 158/159-style filters).
+ if _script_pubkey.is_v0_p2wsh() {
+ assert_eq!(&bitcoin::Address::p2wsh(&Script::from(input.witness.last().unwrap().clone()), bitcoin::Network::Bitcoin).script_pubkey(), _script_pubkey);
+ } else if _script_pubkey.is_v0_p2wpkh() {
+ assert_eq!(&bitcoin::Address::p2wpkh(&bitcoin::PublicKey::from_slice(&input.witness.last().unwrap()).unwrap(), bitcoin::Network::Bitcoin).unwrap().script_pubkey(), _script_pubkey);
+ } else { panic!(); }
+ }
return true;
}
}

Comment threadlightning/src/chain/channelmonitor.rs Outdated
Comment threadlightning/src/chain/channelmonitor.rs Outdated
Antoine Riard added 2 commits October 10, 2020 18:51
Previously, outputs were monitored based on txid and an index yelled
from an enumeration over the returned selected outputs by monitoring
code. This is always have been broken but was only discovered while
introducing anchor outputs as those ones rank always first per BIP69.
We didn't have test cases where a HTLC was bigger than a party balance
on a holder commitment and thus not ranking first.
Next commit introduce test coverage.
This test is a mutation to underscore the detetection logic bug
we had before lightningdevkit#653. HTLC value routed is above the remaining
balance, thus inverting HTLC and `to_remote` output. HTLC
will come second and it wouldn't be seen by pre-lightningdevkit#653 detection
as we were eneumerate()'ing on a watched outputs vector (Vec<TxOut>)
thus implictly relying on outputs order detection for correct
spending children filtering.
@ariard

Copy link
Copy Markdown
Author

Updated at 324edf1

See modification of your supplementary diff and caveat comment to keep passing test_no_failure_dust_htlc_local_commitment, which is intentionally throwing junk in monitoring code to test robustness.

if *idx == input.previous_output.vout {
#[cfg(test)]
{
// If the witness is empty this transaction is a dummy one expressely

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Can we just...drop that test and panic instead? What was the rationale behind connecting garbage that should only ever be an indication the user is being duped by a bogus chain source (which is explicitly not in our threat model, at least not yet).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Tested was added in #333, but can't find the rational. If I remember loosely, at some point we had bug in our dust HTLC canceling back logic at commitment transaction confirmation. Mutating with the following doesn't break the test so I presume it was an oversight as it doesn't actually cover anything. Removed.

Diff:

diff --git a/lightning/src/chain/channelmonitor.rs b/lightning/src/chain/channelmonitor.rs
index 3af98121..927d9d70 100644
--- a/lightning/src/chain/channelmonitor.rs
+++ b/lightning/src/chain/channelmonitor.rs
@@ -1418,12 +1418,12 @@ impl<ChanSigner: ChannelKeys> ChannelMonitor<ChanSigner> {
}
}
}
- if let Some(ref txid) = self.current_counterparty_commitment_txid {
- check_htlc_fails!(txid, "current", 'current_loop);
- }
- if let Some(ref txid) = self.prev_counterparty_commitment_txid {
- check_htlc_fails!(txid, "previous", 'prev_loop);
- }
+ //if let Some(ref txid) = self.current_counterparty_commitment_txid {
+ // check_htlc_fails!(txid, "current", 'current_loop);
+ //}
+ //if let Some(ref txid) = self.prev_counterparty_commitment_txid {
+ // check_htlc_fails!(txid, "previous", 'prev_loop);
+ //}
if let Some(revocation_points) = self.their_cur_revocation_points {
let revocation_point_option =

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I think this test was added in case of future changes of the monitoring code (check_spend_counterparty) which may have broken the no-dust-HTLC-canceling-back.

We remove test_no_failure_dust_htlc_local_commitment from our test
framework as this test deliberately throwing junk transaction in
our monitoring parsing code is hitting new assertions.
This test was added in lightningdevkit#333, but it sounds as an oversight as the
correctness intention of this test (i.e verifying lack of dust
HTLCs canceling back in case of junk commitment transaction) doesn't
currently break.
let output_scripts = txouts.iter().map(|o| o.script_pubkey.clone()).collect();
self.outputs_to_watch.insert(txid.clone(), output_scripts).is_none()
let idx_and_scripts = txouts.iter().map(|o| (o.0, o.1.script_pubkey.clone())).collect();
self.outputs_to_watch.insert(txid.clone(), idx_and_scripts).is_none()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Can we iterate the new watch txn to assert they're known types so that the panic!() two hunks down is definitely correct?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Actually, its all test-only, it doesnt matter.

@TheBlueMattTheBlueMatt left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

One comment, otherwise ACK.

@TheBlueMatt
TheBlueMatt merged commit 8a79877 into lightningdevkit:masterOct 15, 2020
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.

3 participants

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

Add outpoint index in watch_outputs to fix tracking - #653

Merged
TheBlueMatt merged 3 commits into
lightningdevkit:masterfrom
ariard:2020-06-fix-outputs-tracking
Oct 15, 2020
Merged

Add outpoint index in watch_outputs to fix tracking#653
TheBlueMatt merged 3 commits into
lightningdevkit:masterfrom
ariard:2020-06-fix-outputs-tracking

Conversation

@ariard

Copy link
Copy Markdown

Previously, outputs were monitored based on txid and an index yelled
from an enumeration over the returned selected outputs by monitoring
code. This is broken we don't have a guarantee that HTLC outputs are
ranking first after introduction of anchor outputs.

I think alternatively we can fix sorting in build_commitment_transaction to always order HTLCs first but sounds less robust to me.

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

Can you elaborate on "don't have a guarantee that HTLC outputs are ranking first after introduction of anchor outputs."? Specifically, we should always know exactly what the list of outputs in a commitment transaction is, why can we not use that?

@ariard

Copy link
Copy Markdown
Author

On "don't have a guarantee that HTLC outputs are ranking first after introduction of anchor outputs" it needs an amendment, I think we don't have previously guarantee that HTLC outputs were ranking first before to_local/to_remote as comparators are in order : value, script_pubkey, (timelocks), (hash). So this issue sounds to have been silently avoided by our test framework.

We always know the list but not their order and that matters to match by outpoint ?

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

Right, but I don't see where the current code is making any assumptions about HTLC output ordering - watch_outputs seems to always be called with something like watch_outputs.append(&mut tx.output.clone()); which means enumerate() does the correct thing.

@jkczyz

Copy link
Copy Markdown
Contributor

I ran across an issue today that looks to be resolved by this PR. Here we are pushing outputs to watch and later assume they are indexed by how they appear in the transaction.

@TheBlueMatt concurred that this fix is appropriate.

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

Right, I think I realized this was actually right (and we get it wrong in a few places), but forgot to comment here. In any case, this really needs a robust test to ensure we never hit such an error in the future - our test chain monitoring code should refuse to match things that don't have the correct output index.

@TheBlueMattTheBlueMatt added this to the 0.0.12 milestone Sep 27, 2020
@TheBlueMatt

Copy link
Copy Markdown
Collaborator

Is this fixed in #649 or do we need to rebase this on top of it/ whats the status here?

@ariard

Copy link
Copy Markdown
Author

@TheBlueMatt@jkczyz I'll rebase this on top of #649. Without I've test breakage on my anchor branch, but surely needs it own test coverage.

@ariard

Copy link
Copy Markdown
Author

@TheBlueMatt@jkczyz Thanks for review finally updated at 80c0e8c, see commit messages for explaining the bug. Or IRC conv of 10/06/2020.

@codecov

codecovBot commented Oct 7, 2020

Copy link
Copy Markdown

Codecov Report

Merging #653 into master will decrease coverage by 0.04%.
The diff coverage is 95.52%.

Impacted file tree graph

@@ Coverage Diff @@## master #653 +/- ##
==========================================
- Coverage 91.39% 91.35% -0.05% 
==========================================
Files 37 37 Lines 21964 21974 +10 ==========================================
Hits 20074 20074 - Misses 1890 1900 +10 
Impacted FilesCoverage Δ
lightning/src/chain/channelmonitor.rs95.52% <91.42%> (-0.20%)⬇️
lightning/src/chain/chainmonitor.rs97.10% <100.00%> (ø)
lightning/src/ln/functional_tests.rs96.98% <100.00%> (-0.13%)⬇️

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 df778b6...27ee115. Read the comment docs.

@TheBlueMattTheBlueMatt left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I'm a little confused why something like this doesn't catch the bug, even on your new test:

@@ -1811,10 +1811,32 @@ impl<ChanSigner: ChannelKeys> ChannelMonitor<ChanSigner> {
/// Checks if a given transaction spends any watched outputs.
fn spends_watched_output(&self, tx: &Transaction) -> bool {
+ #[cfg(test)]
+ {
+ // If we see a transaction which we registered previously, make sure the registration
+ // matches the actual transaction.
+ if let Some(outputs) = self.get_outputs_to_watch().get(&tx.txid()) {
+ for (idx, script_pubkey) in outputs.iter().enumerate() {
+ assert!(idx < tx.output.len());
+ assert_eq!(tx.output[idx].script_pubkey, *script_pubkey);
+ }
+ }
+ }
for input in tx.input.iter() {
if let Some(outputs) = self.get_outputs_to_watch().get(&input.previous_output.txid) {
for (idx, _script_pubkey) in outputs.iter().enumerate() {
if idx == input.previous_output.vout as usize {
+ #[cfg(test)]
+ {
+ // If the expected script is a known type, check that the witness
+ // appears to be spending the correct type (ie that the match would
+ // actually succeed in BIP 158/159-style filters).
+ if _script_pubkey.is_v0_p2wsh() {
+ assert_eq!(&bitcoin::Address::p2wsh(&Script::from(input.witness.last().unwrap().clone()), bitcoin::Network::Bitcoin).script_pubkey(), _script_pubkey);
+ } else if _script_pubkey.is_v0_p2wpkh() {
+ assert_eq!(&bitcoin::Address::p2wpkh(&bitcoin::PublicKey::from_slice(&input.witness.last().unwrap()).unwrap(), bitcoin::Network::Bitcoin).unwrap().script_pubkey(), _script_pubkey);
+ }
+ }
return true;
}
}

Comment threadlightning/src/ln/functional_tests.rs Outdated
Comment threadlightning/src/ln/functional_tests.rs Outdated
@ariard

Copy link
Copy Markdown
Author

I'm a little confused why something like this doesn't catch the bug, even on your new test:

What did you observe ? I tested your diff on master with new test and effectively it's failing as the index as yelled by the iterator enumeration isn't the real index at which the output should be watched and filtered.

You just have watched_outputs.len() < commitment_tx.output.len()

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

What did you observe ?

It looked to me like your new test was failing at the assertion at the end both with and without the above diff, not ever hitting the new assertions, did I do something wrong?

@TheBlueMattTheBlueMatt left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I'd still like to keep the second part of the new assertions. While it doesn't hit here because we're spending something which didn't get registered, I could see us screwing up and registering a script wrong in the future without having the transaction in the current matched set.

 if let Some(outputs) = self.get_outputs_to_watch().get(&input.previous_output.txid) {
for (idx, _script_pubkey) in outputs.iter().enumerate() {
if idx == input.previous_output.vout as usize {
+ #[cfg(test)]
+ {
+ // If the expected script is a known type, check that the witness
+ // appears to be spending the correct type (ie that the match would
+ // actually succeed in BIP 158/159-style filters).
+ if _script_pubkey.is_v0_p2wsh() {
+ assert_eq!(&bitcoin::Address::p2wsh(&Script::from(input.witness.last().unwrap().clone()), bitcoin::Network::Bitcoin).script_pubkey(), _script_pubkey);
+ } else if _script_pubkey.is_v0_p2wpkh() {
+ assert_eq!(&bitcoin::Address::p2wpkh(&bitcoin::PublicKey::from_slice(&input.witness.last().unwrap()).unwrap(), bitcoin::Network::Bitcoin).unwrap().script_pubkey(), _script_pubkey);
+ } else { panic!(); }
+ }
return true;
}
}

Comment threadlightning/src/chain/channelmonitor.rs Outdated
Comment threadlightning/src/chain/channelmonitor.rs Outdated
Antoine Riard added 2 commits October 10, 2020 18:51
Previously, outputs were monitored based on txid and an index yelled
from an enumeration over the returned selected outputs by monitoring
code. This is always have been broken but was only discovered while
introducing anchor outputs as those ones rank always first per BIP69.
We didn't have test cases where a HTLC was bigger than a party balance
on a holder commitment and thus not ranking first.
Next commit introduce test coverage.
This test is a mutation to underscore the detetection logic bug
we had before lightningdevkit#653. HTLC value routed is above the remaining
balance, thus inverting HTLC and `to_remote` output. HTLC
will come second and it wouldn't be seen by pre-lightningdevkit#653 detection
as we were eneumerate()'ing on a watched outputs vector (Vec<TxOut>)
thus implictly relying on outputs order detection for correct
spending children filtering.
@ariard

Copy link
Copy Markdown
Author

Updated at 324edf1

See modification of your supplementary diff and caveat comment to keep passing test_no_failure_dust_htlc_local_commitment, which is intentionally throwing junk in monitoring code to test robustness.

if *idx == input.previous_output.vout {
#[cfg(test)]
{
// If the witness is empty this transaction is a dummy one expressely

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Can we just...drop that test and panic instead? What was the rationale behind connecting garbage that should only ever be an indication the user is being duped by a bogus chain source (which is explicitly not in our threat model, at least not yet).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Tested was added in #333, but can't find the rational. If I remember loosely, at some point we had bug in our dust HTLC canceling back logic at commitment transaction confirmation. Mutating with the following doesn't break the test so I presume it was an oversight as it doesn't actually cover anything. Removed.

Diff:

diff --git a/lightning/src/chain/channelmonitor.rs b/lightning/src/chain/channelmonitor.rs
index 3af98121..927d9d70 100644
--- a/lightning/src/chain/channelmonitor.rs
+++ b/lightning/src/chain/channelmonitor.rs
@@ -1418,12 +1418,12 @@ impl<ChanSigner: ChannelKeys> ChannelMonitor<ChanSigner> {
}
}
}
- if let Some(ref txid) = self.current_counterparty_commitment_txid {
- check_htlc_fails!(txid, "current", 'current_loop);
- }
- if let Some(ref txid) = self.prev_counterparty_commitment_txid {
- check_htlc_fails!(txid, "previous", 'prev_loop);
- }
+ //if let Some(ref txid) = self.current_counterparty_commitment_txid {
+ // check_htlc_fails!(txid, "current", 'current_loop);
+ //}
+ //if let Some(ref txid) = self.prev_counterparty_commitment_txid {
+ // check_htlc_fails!(txid, "previous", 'prev_loop);
+ //}
if let Some(revocation_points) = self.their_cur_revocation_points {
let revocation_point_option =

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I think this test was added in case of future changes of the monitoring code (check_spend_counterparty) which may have broken the no-dust-HTLC-canceling-back.

We remove test_no_failure_dust_htlc_local_commitment from our test
framework as this test deliberately throwing junk transaction in
our monitoring parsing code is hitting new assertions.
This test was added in lightningdevkit#333, but it sounds as an oversight as the
correctness intention of this test (i.e verifying lack of dust
HTLCs canceling back in case of junk commitment transaction) doesn't
currently break.
let output_scripts = txouts.iter().map(|o| o.script_pubkey.clone()).collect();
self.outputs_to_watch.insert(txid.clone(), output_scripts).is_none()
let idx_and_scripts = txouts.iter().map(|o| (o.0, o.1.script_pubkey.clone())).collect();
self.outputs_to_watch.insert(txid.clone(), idx_and_scripts).is_none()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Can we iterate the new watch txn to assert they're known types so that the panic!() two hunks down is definitely correct?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Actually, its all test-only, it doesnt matter.

@TheBlueMattTheBlueMatt left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

One comment, otherwise ACK.

@TheBlueMatt
TheBlueMatt merged commit 8a79877 into lightningdevkit:masterOct 15, 2020
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.

3 participants

@ariard@TheBlueMatt@jkczyz
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add outpoint index in watch_outputs to fix tracking - #653

Merged
TheBlueMatt merged 3 commits into
lightningdevkit:masterfrom
ariard:2020-06-fix-outputs-tracking
Oct 15, 2020
Merged

Add outpoint index in watch_outputs to fix tracking#653
TheBlueMatt merged 3 commits into
lightningdevkit:masterfrom
ariard:2020-06-fix-outputs-tracking

Conversation

@ariard

Copy link
Copy Markdown

Previously, outputs were monitored based on txid and an index yelled
from an enumeration over the returned selected outputs by monitoring
code. This is broken we don't have a guarantee that HTLC outputs are
ranking first after introduction of anchor outputs.

I think alternatively we can fix sorting in build_commitment_transaction to always order HTLCs first but sounds less robust to me.

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

Can you elaborate on "don't have a guarantee that HTLC outputs are ranking first after introduction of anchor outputs."? Specifically, we should always know exactly what the list of outputs in a commitment transaction is, why can we not use that?

@ariard

Copy link
Copy Markdown
Author

On "don't have a guarantee that HTLC outputs are ranking first after introduction of anchor outputs" it needs an amendment, I think we don't have previously guarantee that HTLC outputs were ranking first before to_local/to_remote as comparators are in order : value, script_pubkey, (timelocks), (hash). So this issue sounds to have been silently avoided by our test framework.

We always know the list but not their order and that matters to match by outpoint ?

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

Right, but I don't see where the current code is making any assumptions about HTLC output ordering - watch_outputs seems to always be called with something like watch_outputs.append(&mut tx.output.clone()); which means enumerate() does the correct thing.

@jkczyz

Copy link
Copy Markdown
Contributor

I ran across an issue today that looks to be resolved by this PR. Here we are pushing outputs to watch and later assume they are indexed by how they appear in the transaction.

@TheBlueMatt concurred that this fix is appropriate.

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

Right, I think I realized this was actually right (and we get it wrong in a few places), but forgot to comment here. In any case, this really needs a robust test to ensure we never hit such an error in the future - our test chain monitoring code should refuse to match things that don't have the correct output index.

@TheBlueMattTheBlueMatt added this to the 0.0.12 milestone Sep 27, 2020
@TheBlueMatt

Copy link
Copy Markdown
Collaborator

Is this fixed in #649 or do we need to rebase this on top of it/ whats the status here?

@ariard

Copy link
Copy Markdown
Author

@TheBlueMatt@jkczyz I'll rebase this on top of #649. Without I've test breakage on my anchor branch, but surely needs it own test coverage.

@ariard

Copy link
Copy Markdown
Author

@TheBlueMatt@jkczyz Thanks for review finally updated at 80c0e8c, see commit messages for explaining the bug. Or IRC conv of 10/06/2020.

@codecov

codecovBot commented Oct 7, 2020

Copy link
Copy Markdown

Codecov Report

Merging #653 into master will decrease coverage by 0.04%.
The diff coverage is 95.52%.

Impacted file tree graph

@@ Coverage Diff @@## master #653 +/- ##
==========================================
- Coverage 91.39% 91.35% -0.05% 
==========================================
Files 37 37 Lines 21964 21974 +10 ==========================================
Hits 20074 20074 - Misses 1890 1900 +10 
Impacted FilesCoverage Δ
lightning/src/chain/channelmonitor.rs95.52% <91.42%> (-0.20%)⬇️
lightning/src/chain/chainmonitor.rs97.10% <100.00%> (ø)
lightning/src/ln/functional_tests.rs96.98% <100.00%> (-0.13%)⬇️

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 df778b6...27ee115. Read the comment docs.

@TheBlueMattTheBlueMatt left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I'm a little confused why something like this doesn't catch the bug, even on your new test:

@@ -1811,10 +1811,32 @@ impl<ChanSigner: ChannelKeys> ChannelMonitor<ChanSigner> {
/// Checks if a given transaction spends any watched outputs.
fn spends_watched_output(&self, tx: &Transaction) -> bool {
+ #[cfg(test)]
+ {
+ // If we see a transaction which we registered previously, make sure the registration
+ // matches the actual transaction.
+ if let Some(outputs) = self.get_outputs_to_watch().get(&tx.txid()) {
+ for (idx, script_pubkey) in outputs.iter().enumerate() {
+ assert!(idx < tx.output.len());
+ assert_eq!(tx.output[idx].script_pubkey, *script_pubkey);
+ }
+ }
+ }
for input in tx.input.iter() {
if let Some(outputs) = self.get_outputs_to_watch().get(&input.previous_output.txid) {
for (idx, _script_pubkey) in outputs.iter().enumerate() {
if idx == input.previous_output.vout as usize {
+ #[cfg(test)]
+ {
+ // If the expected script is a known type, check that the witness
+ // appears to be spending the correct type (ie that the match would
+ // actually succeed in BIP 158/159-style filters).
+ if _script_pubkey.is_v0_p2wsh() {
+ assert_eq!(&bitcoin::Address::p2wsh(&Script::from(input.witness.last().unwrap().clone()), bitcoin::Network::Bitcoin).script_pubkey(), _script_pubkey);
+ } else if _script_pubkey.is_v0_p2wpkh() {
+ assert_eq!(&bitcoin::Address::p2wpkh(&bitcoin::PublicKey::from_slice(&input.witness.last().unwrap()).unwrap(), bitcoin::Network::Bitcoin).unwrap().script_pubkey(), _script_pubkey);
+ }
+ }
return true;
}
}

Comment threadlightning/src/ln/functional_tests.rs Outdated
Comment threadlightning/src/ln/functional_tests.rs Outdated
@ariard

Copy link
Copy Markdown
Author

I'm a little confused why something like this doesn't catch the bug, even on your new test:

What did you observe ? I tested your diff on master with new test and effectively it's failing as the index as yelled by the iterator enumeration isn't the real index at which the output should be watched and filtered.

You just have watched_outputs.len() < commitment_tx.output.len()

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

What did you observe ?

It looked to me like your new test was failing at the assertion at the end both with and without the above diff, not ever hitting the new assertions, did I do something wrong?

@TheBlueMattTheBlueMatt left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I'd still like to keep the second part of the new assertions. While it doesn't hit here because we're spending something which didn't get registered, I could see us screwing up and registering a script wrong in the future without having the transaction in the current matched set.

 if let Some(outputs) = self.get_outputs_to_watch().get(&input.previous_output.txid) {
for (idx, _script_pubkey) in outputs.iter().enumerate() {
if idx == input.previous_output.vout as usize {
+ #[cfg(test)]
+ {
+ // If the expected script is a known type, check that the witness
+ // appears to be spending the correct type (ie that the match would
+ // actually succeed in BIP 158/159-style filters).
+ if _script_pubkey.is_v0_p2wsh() {
+ assert_eq!(&bitcoin::Address::p2wsh(&Script::from(input.witness.last().unwrap().clone()), bitcoin::Network::Bitcoin).script_pubkey(), _script_pubkey);
+ } else if _script_pubkey.is_v0_p2wpkh() {
+ assert_eq!(&bitcoin::Address::p2wpkh(&bitcoin::PublicKey::from_slice(&input.witness.last().unwrap()).unwrap(), bitcoin::Network::Bitcoin).unwrap().script_pubkey(), _script_pubkey);
+ } else { panic!(); }
+ }
return true;
}
}

Comment threadlightning/src/chain/channelmonitor.rs Outdated
Comment threadlightning/src/chain/channelmonitor.rs Outdated
Antoine Riard added 2 commits October 10, 2020 18:51
Previously, outputs were monitored based on txid and an index yelled
from an enumeration over the returned selected outputs by monitoring
code. This is always have been broken but was only discovered while
introducing anchor outputs as those ones rank always first per BIP69.
We didn't have test cases where a HTLC was bigger than a party balance
on a holder commitment and thus not ranking first.
Next commit introduce test coverage.
This test is a mutation to underscore the detetection logic bug
we had before lightningdevkit#653. HTLC value routed is above the remaining
balance, thus inverting HTLC and `to_remote` output. HTLC
will come second and it wouldn't be seen by pre-lightningdevkit#653 detection
as we were eneumerate()'ing on a watched outputs vector (Vec<TxOut>)
thus implictly relying on outputs order detection for correct
spending children filtering.
@ariard

Copy link
Copy Markdown
Author

Updated at 324edf1

See modification of your supplementary diff and caveat comment to keep passing test_no_failure_dust_htlc_local_commitment, which is intentionally throwing junk in monitoring code to test robustness.

if *idx == input.previous_output.vout {
#[cfg(test)]
{
// If the witness is empty this transaction is a dummy one expressely

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Can we just...drop that test and panic instead? What was the rationale behind connecting garbage that should only ever be an indication the user is being duped by a bogus chain source (which is explicitly not in our threat model, at least not yet).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Tested was added in #333, but can't find the rational. If I remember loosely, at some point we had bug in our dust HTLC canceling back logic at commitment transaction confirmation. Mutating with the following doesn't break the test so I presume it was an oversight as it doesn't actually cover anything. Removed.

Diff:

diff --git a/lightning/src/chain/channelmonitor.rs b/lightning/src/chain/channelmonitor.rs
index 3af98121..927d9d70 100644
--- a/lightning/src/chain/channelmonitor.rs
+++ b/lightning/src/chain/channelmonitor.rs
@@ -1418,12 +1418,12 @@ impl<ChanSigner: ChannelKeys> ChannelMonitor<ChanSigner> {
}
}
}
- if let Some(ref txid) = self.current_counterparty_commitment_txid {
- check_htlc_fails!(txid, "current", 'current_loop);
- }
- if let Some(ref txid) = self.prev_counterparty_commitment_txid {
- check_htlc_fails!(txid, "previous", 'prev_loop);
- }
+ //if let Some(ref txid) = self.current_counterparty_commitment_txid {
+ // check_htlc_fails!(txid, "current", 'current_loop);
+ //}
+ //if let Some(ref txid) = self.prev_counterparty_commitment_txid {
+ // check_htlc_fails!(txid, "previous", 'prev_loop);
+ //}
if let Some(revocation_points) = self.their_cur_revocation_points {
let revocation_point_option =

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I think this test was added in case of future changes of the monitoring code (check_spend_counterparty) which may have broken the no-dust-HTLC-canceling-back.

We remove test_no_failure_dust_htlc_local_commitment from our test
framework as this test deliberately throwing junk transaction in
our monitoring parsing code is hitting new assertions.
This test was added in lightningdevkit#333, but it sounds as an oversight as the
correctness intention of this test (i.e verifying lack of dust
HTLCs canceling back in case of junk commitment transaction) doesn't
currently break.
let output_scripts = txouts.iter().map(|o| o.script_pubkey.clone()).collect();
self.outputs_to_watch.insert(txid.clone(), output_scripts).is_none()
let idx_and_scripts = txouts.iter().map(|o| (o.0, o.1.script_pubkey.clone())).collect();
self.outputs_to_watch.insert(txid.clone(), idx_and_scripts).is_none()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Can we iterate the new watch txn to assert they're known types so that the panic!() two hunks down is definitely correct?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Actually, its all test-only, it doesnt matter.

@TheBlueMattTheBlueMatt left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

One comment, otherwise ACK.

@TheBlueMatt
TheBlueMatt merged commit 8a79877 into lightningdevkit:masterOct 15, 2020
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.

3 participants

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

Add outpoint index in watch_outputs to fix tracking - #653

Merged
TheBlueMatt merged 3 commits into
lightningdevkit:masterfrom
ariard:2020-06-fix-outputs-tracking
Oct 15, 2020
Merged

Add outpoint index in watch_outputs to fix tracking#653
TheBlueMatt merged 3 commits into
lightningdevkit:masterfrom
ariard:2020-06-fix-outputs-tracking

Conversation

@ariard

Copy link
Copy Markdown

Previously, outputs were monitored based on txid and an index yelled
from an enumeration over the returned selected outputs by monitoring
code. This is broken we don't have a guarantee that HTLC outputs are
ranking first after introduction of anchor outputs.

I think alternatively we can fix sorting in build_commitment_transaction to always order HTLCs first but sounds less robust to me.

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

Can you elaborate on "don't have a guarantee that HTLC outputs are ranking first after introduction of anchor outputs."? Specifically, we should always know exactly what the list of outputs in a commitment transaction is, why can we not use that?

@ariard

Copy link
Copy Markdown
Author

On "don't have a guarantee that HTLC outputs are ranking first after introduction of anchor outputs" it needs an amendment, I think we don't have previously guarantee that HTLC outputs were ranking first before to_local/to_remote as comparators are in order : value, script_pubkey, (timelocks), (hash). So this issue sounds to have been silently avoided by our test framework.

We always know the list but not their order and that matters to match by outpoint ?

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

Right, but I don't see where the current code is making any assumptions about HTLC output ordering - watch_outputs seems to always be called with something like watch_outputs.append(&mut tx.output.clone()); which means enumerate() does the correct thing.

@jkczyz

Copy link
Copy Markdown
Contributor

I ran across an issue today that looks to be resolved by this PR. Here we are pushing outputs to watch and later assume they are indexed by how they appear in the transaction.

@TheBlueMatt concurred that this fix is appropriate.

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

Right, I think I realized this was actually right (and we get it wrong in a few places), but forgot to comment here. In any case, this really needs a robust test to ensure we never hit such an error in the future - our test chain monitoring code should refuse to match things that don't have the correct output index.

@TheBlueMattTheBlueMatt added this to the 0.0.12 milestone Sep 27, 2020
@TheBlueMatt

Copy link
Copy Markdown
Collaborator

Is this fixed in #649 or do we need to rebase this on top of it/ whats the status here?

@ariard

Copy link
Copy Markdown
Author

@TheBlueMatt@jkczyz I'll rebase this on top of #649. Without I've test breakage on my anchor branch, but surely needs it own test coverage.

@ariard

Copy link
Copy Markdown
Author

@TheBlueMatt@jkczyz Thanks for review finally updated at 80c0e8c, see commit messages for explaining the bug. Or IRC conv of 10/06/2020.

@codecov

codecovBot commented Oct 7, 2020

Copy link
Copy Markdown

Codecov Report

Merging #653 into master will decrease coverage by 0.04%.
The diff coverage is 95.52%.

Impacted file tree graph

@@ Coverage Diff @@## master #653 +/- ##
==========================================
- Coverage 91.39% 91.35% -0.05% 
==========================================
Files 37 37 Lines 21964 21974 +10 ==========================================
Hits 20074 20074 - Misses 1890 1900 +10 
Impacted FilesCoverage Δ
lightning/src/chain/channelmonitor.rs95.52% <91.42%> (-0.20%)⬇️
lightning/src/chain/chainmonitor.rs97.10% <100.00%> (ø)
lightning/src/ln/functional_tests.rs96.98% <100.00%> (-0.13%)⬇️

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 df778b6...27ee115. Read the comment docs.

@TheBlueMattTheBlueMatt left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I'm a little confused why something like this doesn't catch the bug, even on your new test:

@@ -1811,10 +1811,32 @@ impl<ChanSigner: ChannelKeys> ChannelMonitor<ChanSigner> {
/// Checks if a given transaction spends any watched outputs.
fn spends_watched_output(&self, tx: &Transaction) -> bool {
+ #[cfg(test)]
+ {
+ // If we see a transaction which we registered previously, make sure the registration
+ // matches the actual transaction.
+ if let Some(outputs) = self.get_outputs_to_watch().get(&tx.txid()) {
+ for (idx, script_pubkey) in outputs.iter().enumerate() {
+ assert!(idx < tx.output.len());
+ assert_eq!(tx.output[idx].script_pubkey, *script_pubkey);
+ }
+ }
+ }
for input in tx.input.iter() {
if let Some(outputs) = self.get_outputs_to_watch().get(&input.previous_output.txid) {
for (idx, _script_pubkey) in outputs.iter().enumerate() {
if idx == input.previous_output.vout as usize {
+ #[cfg(test)]
+ {
+ // If the expected script is a known type, check that the witness
+ // appears to be spending the correct type (ie that the match would
+ // actually succeed in BIP 158/159-style filters).
+ if _script_pubkey.is_v0_p2wsh() {
+ assert_eq!(&bitcoin::Address::p2wsh(&Script::from(input.witness.last().unwrap().clone()), bitcoin::Network::Bitcoin).script_pubkey(), _script_pubkey);
+ } else if _script_pubkey.is_v0_p2wpkh() {
+ assert_eq!(&bitcoin::Address::p2wpkh(&bitcoin::PublicKey::from_slice(&input.witness.last().unwrap()).unwrap(), bitcoin::Network::Bitcoin).unwrap().script_pubkey(), _script_pubkey);
+ }
+ }
return true;
}
}

Comment threadlightning/src/ln/functional_tests.rs Outdated
Comment threadlightning/src/ln/functional_tests.rs Outdated
@ariard

Copy link
Copy Markdown
Author

I'm a little confused why something like this doesn't catch the bug, even on your new test:

What did you observe ? I tested your diff on master with new test and effectively it's failing as the index as yelled by the iterator enumeration isn't the real index at which the output should be watched and filtered.

You just have watched_outputs.len() < commitment_tx.output.len()

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

What did you observe ?

It looked to me like your new test was failing at the assertion at the end both with and without the above diff, not ever hitting the new assertions, did I do something wrong?

@TheBlueMattTheBlueMatt left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I'd still like to keep the second part of the new assertions. While it doesn't hit here because we're spending something which didn't get registered, I could see us screwing up and registering a script wrong in the future without having the transaction in the current matched set.

 if let Some(outputs) = self.get_outputs_to_watch().get(&input.previous_output.txid) {
for (idx, _script_pubkey) in outputs.iter().enumerate() {
if idx == input.previous_output.vout as usize {
+ #[cfg(test)]
+ {
+ // If the expected script is a known type, check that the witness
+ // appears to be spending the correct type (ie that the match would
+ // actually succeed in BIP 158/159-style filters).
+ if _script_pubkey.is_v0_p2wsh() {
+ assert_eq!(&bitcoin::Address::p2wsh(&Script::from(input.witness.last().unwrap().clone()), bitcoin::Network::Bitcoin).script_pubkey(), _script_pubkey);
+ } else if _script_pubkey.is_v0_p2wpkh() {
+ assert_eq!(&bitcoin::Address::p2wpkh(&bitcoin::PublicKey::from_slice(&input.witness.last().unwrap()).unwrap(), bitcoin::Network::Bitcoin).unwrap().script_pubkey(), _script_pubkey);
+ } else { panic!(); }
+ }
return true;
}
}

Comment threadlightning/src/chain/channelmonitor.rs Outdated
Comment threadlightning/src/chain/channelmonitor.rs Outdated
Antoine Riard added 2 commits October 10, 2020 18:51
Previously, outputs were monitored based on txid and an index yelled
from an enumeration over the returned selected outputs by monitoring
code. This is always have been broken but was only discovered while
introducing anchor outputs as those ones rank always first per BIP69.
We didn't have test cases where a HTLC was bigger than a party balance
on a holder commitment and thus not ranking first.
Next commit introduce test coverage.
This test is a mutation to underscore the detetection logic bug
we had before lightningdevkit#653. HTLC value routed is above the remaining
balance, thus inverting HTLC and `to_remote` output. HTLC
will come second and it wouldn't be seen by pre-lightningdevkit#653 detection
as we were eneumerate()'ing on a watched outputs vector (Vec<TxOut>)
thus implictly relying on outputs order detection for correct
spending children filtering.
@ariard

Copy link
Copy Markdown
Author

Updated at 324edf1

See modification of your supplementary diff and caveat comment to keep passing test_no_failure_dust_htlc_local_commitment, which is intentionally throwing junk in monitoring code to test robustness.

if *idx == input.previous_output.vout {
#[cfg(test)]
{
// If the witness is empty this transaction is a dummy one expressely

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Can we just...drop that test and panic instead? What was the rationale behind connecting garbage that should only ever be an indication the user is being duped by a bogus chain source (which is explicitly not in our threat model, at least not yet).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Tested was added in #333, but can't find the rational. If I remember loosely, at some point we had bug in our dust HTLC canceling back logic at commitment transaction confirmation. Mutating with the following doesn't break the test so I presume it was an oversight as it doesn't actually cover anything. Removed.

Diff:

diff --git a/lightning/src/chain/channelmonitor.rs b/lightning/src/chain/channelmonitor.rs
index 3af98121..927d9d70 100644
--- a/lightning/src/chain/channelmonitor.rs
+++ b/lightning/src/chain/channelmonitor.rs
@@ -1418,12 +1418,12 @@ impl<ChanSigner: ChannelKeys> ChannelMonitor<ChanSigner> {
}
}
}
- if let Some(ref txid) = self.current_counterparty_commitment_txid {
- check_htlc_fails!(txid, "current", 'current_loop);
- }
- if let Some(ref txid) = self.prev_counterparty_commitment_txid {
- check_htlc_fails!(txid, "previous", 'prev_loop);
- }
+ //if let Some(ref txid) = self.current_counterparty_commitment_txid {
+ // check_htlc_fails!(txid, "current", 'current_loop);
+ //}
+ //if let Some(ref txid) = self.prev_counterparty_commitment_txid {
+ // check_htlc_fails!(txid, "previous", 'prev_loop);
+ //}
if let Some(revocation_points) = self.their_cur_revocation_points {
let revocation_point_option =

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I think this test was added in case of future changes of the monitoring code (check_spend_counterparty) which may have broken the no-dust-HTLC-canceling-back.

We remove test_no_failure_dust_htlc_local_commitment from our test
framework as this test deliberately throwing junk transaction in
our monitoring parsing code is hitting new assertions.
This test was added in lightningdevkit#333, but it sounds as an oversight as the
correctness intention of this test (i.e verifying lack of dust
HTLCs canceling back in case of junk commitment transaction) doesn't
currently break.
let output_scripts = txouts.iter().map(|o| o.script_pubkey.clone()).collect();
self.outputs_to_watch.insert(txid.clone(), output_scripts).is_none()
let idx_and_scripts = txouts.iter().map(|o| (o.0, o.1.script_pubkey.clone())).collect();
self.outputs_to_watch.insert(txid.clone(), idx_and_scripts).is_none()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Can we iterate the new watch txn to assert they're known types so that the panic!() two hunks down is definitely correct?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Actually, its all test-only, it doesnt matter.

@TheBlueMattTheBlueMatt left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

One comment, otherwise ACK.

@TheBlueMatt
TheBlueMatt merged commit 8a79877 into lightningdevkit:masterOct 15, 2020
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.

3 participants

@ariard@TheBlueMatt@jkczyz