Locktimed packages fixes - #3923

Merged
TheBlueMatt merged 5 commits into
lightningdevkit:mainfrom
TheBlueMatt:2025-07-3860-plus-plus
Jul 15, 2025
Merged

Locktimed packages fixes#3923
TheBlueMatt merged 5 commits into
lightningdevkit:mainfrom
TheBlueMatt:2025-07-3860-plus-plus

Conversation

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

See individual commits for more. This is #3860 with an additional issue I found while writing a test for it fixed.

TheBlueMattand others added 2 commits July 10, 2025 17:56
In a number of tests we require available UTXOs to do HTLC anchor
claims by bringing our own fees. We previously wrote that out in
each test, which is somewhat verbose, so here we simply add a test
utility that gives each node a full BTC in a single UTXO.
We have to prune locktimed packages when their inputs are spent,
otherwise the notification of the watched outputs might be missed. This
can lead to locktimed packages with spent inputs being added back to
the pending claim requests in the future, and they are never cleaned
up until node restart.
Resolves: lightningdevkit#3859
@TheBlueMattTheBlueMatt added this to the 0.2 milestone Jul 10, 2025
@ldk-reviews-bot

ldk-reviews-bot commented Jul 10, 2025

Copy link
Copy Markdown

👋 Thanks for assigning @tankyleo as a reviewer!
I'll wait for their review and will help manage the review process.
Once they submit their review, I'll check if a second reviewer would be helpful.

Comment threadlightning/src/ln/reorg_tests.rs Outdated
When we have an outpoint to claim which is lock-timed and the
locktime is reached, we add it to
`OnchainTxHandler::claimable_outpoints` to indicate the outpoint is
now being claimed. However, `claimable_outpoints` is supposed to
track when the outpoint first appeared on chain so that we can
remove the claim if the outpoint is reorged out.
Sadly, in the handling for lock-timed packages, we incorrectly
stored the current height in `claimable_outpoints`, causing such
claims to be removed in case of a reorg right after they were
generated, even if the output we intend to claim isn't removed at
all.
Here we start tracking when the outpoint we're spending was created
in `PackageSolvingData`'s constituent types. While we could have
tracked this information in `PackageTemplate`, it would preclude
later merging packages that are spending outpoints included in
different blocks, which we don't necessarily want to do.
When we have an outpoint to claim which is lock-timed and the
locktime is reached, we add it to
`OnchainTxHandler::claimable_outpoints` to indicate the outpoint is
now being claimed. However, `claimable_outpoints` is supposed to
track when the outpoint first appeared on chain so that we can
remove the claim if the outpoint is reorged out.
Sadly, in the handling for lock-timed packages, we incorrectly
stored the current height in `claimable_outpoints`, causing such
claims to be removed in case of a reorg right after they were
generated, even if the output we intend to claim isn't removed at
all.
Here we use the creation-height tracking added in the previous
commit to actually address the issue, using the tracked height when
adding a claim to `OnchainTxHandler::claimable_outpoints`.
In cases where we have no information, we continue to use the
current height, retaining the issue for locktimed packages on
upgrades, but this simplifies cases where we actually don't have
the information available anyway.
@TheBlueMatt
TheBlueMattforce-pushed the 2025-07-3860-plus-plus branch from ff95a56 to 21be9c5CompareJuly 10, 2025 22:19
Comment threadlightning/src/chain/onchaintx.rs
for (k, outpoint_confirmation_height) in req.outpoints_and_creation_heights() {
let creation_height = outpoint_confirmation_height.unwrap_or(conf_height);
log_info!(logger, "Registering claiming request for {}:{}, which exists as of height {creation_height}", k.txid, k.vout);
self.claimable_outpoints.insert(k.clone(), (claim_id, creation_height));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hm, shouldn't we prefer signed_locktime over creation_height for outpoints that have one though? While the outpoint hasn't been reorged out, claiming it is no longer possible once the block at signed_locktime is disconnected.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

In practice I think this would just result in us broadcasting things that cannot enter the mempool until we get back to the expected height. If we have any other claims that were merged into the same package for whatever reason, and they are still valid at the disconnected block height, then this would be a greater issue.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

The problem is that when things time out via claimable_outpoints, we don't stop claiming them, we remove them. We don't get them back after a reorg at that point. We could move to pushing things into the locked-packages vec after a block-disconnect, but that seems like a bigger change?

Comment threadlightning/src/ln/reorg_tests.rs Outdated
Comment threadlightning/src/ln/reorg_tests.rs Outdated
@ldk-reviews-bot

Copy link
Copy Markdown

👋 The first review has been submitted!

Do you think this PR is ready for a second reviewer? If so, click here to assign a second reviewer.

@tankyleo
tankyleo self-requested a review July 11, 2025 16:55
@ldk-reviews-bot

Copy link
Copy Markdown

🔔 1st Reminder

Hey @tankyleo! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

This adds a single test which exercises both the ability to prune
locktimed packages when inputs are spent as well as the
creation-height tracking for locktimed packages.
@TheBlueMatt
TheBlueMattforce-pushed the 2025-07-3860-plus-plus branch from 21be9c5 to d7726efCompareJuly 14, 2025 12:53
@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

Cleaned up and improved the test slightly:

$ git diff-tree -U2 21be9c5ed d7726eff0
diff --git a/lightning/src/ln/reorg_tests.rs b/lightning/src/ln/reorg_tests.rs
index 6faabac4c0..c5f512b319 100644
--- a/lightning/src/ln/reorg_tests.rs+++ b/lightning/src/ln/reorg_tests.rs@@ -13,5 +13,5 @@
use crate::chain::chaininterface::LowerBoundedFeeEstimator;
-use crate::chain::channelmonitor::{ANTI_REORG_DELAY, LATENCY_GRACE_PERIOD_BLOCKS};+use crate::chain::channelmonitor::{ANTI_REORG_DELAY, Balance, LATENCY_GRACE_PERIOD_BLOCKS};
use crate::chain::transaction::OutPoint;
use crate::chain::Confirm;
@@ -1058,12 +1058,19 @@ fn do_test_split_htlc_expiry_tracking(use_third_htlc: bool, reorg_out: bool) {
if reorg_out {
- // Reorg out bs_htlc_spend_tx, letting node A the claim all the HTLCs instead.+ // Reorg out bs_htlc_spend_tx, letting node A claim all the HTLCs instead.
disconnect_blocks(&nodes[0], ANTI_REORG_DELAY - 2);
assert_eq!(nodes[0].tx_broadcaster.txn_broadcast().len(), 0);
- // As soon as bs_htlc_spend_tx is disconnected+ // As soon as bs_htlc_spend_tx is disconnected, node A should consider all HTLCs+ // claimable-on-timeout.
disconnect_blocks(&nodes[0], 1);
let balances = nodes[0].chain_monitor.chain_monitor.get_claimable_balances(&[]);
assert_eq!(balances.len(), if use_third_htlc { 3 } else { 2 });
+ for balance in balances {+ if let Balance::MaybeTimeoutClaimableHTLC { .. } = balance {+ } else {+ panic!("Unexpected balance {balance:?}");+ }+ }
connect_blocks(&nodes[0], 100);

@TheBlueMatt
TheBlueMatt merged commit 7e4598d into lightningdevkit:mainJul 15, 2025
@TheBlueMattTheBlueMatt mentioned this pull request Jul 15, 2025
@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

Backported in #3932

@TheBlueMattTheBlueMatt self-assigned this Jul 17, 2025
TheBlueMatt added a commit to TheBlueMatt/rust-lightning that referenced this pull request Jul 24, 2025
v0.1.5 - Jul 16, 2025 - "Async Path Reduction"
Performance Improvements
========================
* `NetworkGraph`'s expensive internal consistency checks have now been
disabled in debug builds in addition to release builds (lightningdevkit#3687).
Bug Fixes
=========
* Pathfinding which results in a multi-path payment is now substantially
smarter, using fewer paths and better optimizing fees and successes (lightningdevkit#3890).
* A counterparty delaying claiming multiple HTLCs with different expiries can
no longer cause our `ChannelMonitor` to continuously rebroadcast invalid
transactions or RBF bump attempts (lightningdevkit#3923).
* Reorgs can no longer cause us to fail to claim HTLCs after a counterparty
delayed claiming multiple HTLCs with different expiries (lightningdevkit#3923).
* Force-closing a channel while it is blocked on another channel's async
`ChannelMonitorUpdate` can no longer lead to a panic (lightningdevkit#3858).
* `ChannelMonitorUpdate`s can no longer be released to storage too early when
doing async updates or on restart. This only impacts async
`ChannelMonitorUpdate` persistence and can lead to loss of funds only in rare
cases with `ChannelMonitorUpdate` persistence order inversions (lightningdevkit#3907).
Security
========
0.1.5 fixes a vulnerability which could allow a peer to overdraw their reserve
value, potentially cutting into commitment transaction fees on channels with a
low reserve.
* Due to a bug in checking whether an HTLC is dust during acceptance, near-dust
HTLCs were not counted towards the commitment transaction fee, but did
eventually contribute to it when we built a commitment transaction. This can
be used by a counterparty to overdraw their reserve value, or, for channels
with a low reserve value, cut into the commitment transaction fee (lightningdevkit#3933).
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

5 participants

@TheBlueMatt@ldk-reviews-bot@wpaulino@tankyleo@whfuyn
, '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

Locktimed packages fixes - #3923

Merged
TheBlueMatt merged 5 commits into
lightningdevkit:mainfrom
TheBlueMatt:2025-07-3860-plus-plus
Jul 15, 2025
Merged

Locktimed packages fixes#3923
TheBlueMatt merged 5 commits into
lightningdevkit:mainfrom
TheBlueMatt:2025-07-3860-plus-plus

Conversation

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

See individual commits for more. This is #3860 with an additional issue I found while writing a test for it fixed.

TheBlueMattand others added 2 commits July 10, 2025 17:56
In a number of tests we require available UTXOs to do HTLC anchor
claims by bringing our own fees. We previously wrote that out in
each test, which is somewhat verbose, so here we simply add a test
utility that gives each node a full BTC in a single UTXO.
We have to prune locktimed packages when their inputs are spent,
otherwise the notification of the watched outputs might be missed. This
can lead to locktimed packages with spent inputs being added back to
the pending claim requests in the future, and they are never cleaned
up until node restart.
Resolves: lightningdevkit#3859
@TheBlueMattTheBlueMatt added this to the 0.2 milestone Jul 10, 2025
@ldk-reviews-bot

ldk-reviews-bot commented Jul 10, 2025

Copy link
Copy Markdown

👋 Thanks for assigning @tankyleo as a reviewer!
I'll wait for their review and will help manage the review process.
Once they submit their review, I'll check if a second reviewer would be helpful.

Comment threadlightning/src/ln/reorg_tests.rs Outdated
When we have an outpoint to claim which is lock-timed and the
locktime is reached, we add it to
`OnchainTxHandler::claimable_outpoints` to indicate the outpoint is
now being claimed. However, `claimable_outpoints` is supposed to
track when the outpoint first appeared on chain so that we can
remove the claim if the outpoint is reorged out.
Sadly, in the handling for lock-timed packages, we incorrectly
stored the current height in `claimable_outpoints`, causing such
claims to be removed in case of a reorg right after they were
generated, even if the output we intend to claim isn't removed at
all.
Here we start tracking when the outpoint we're spending was created
in `PackageSolvingData`'s constituent types. While we could have
tracked this information in `PackageTemplate`, it would preclude
later merging packages that are spending outpoints included in
different blocks, which we don't necessarily want to do.
When we have an outpoint to claim which is lock-timed and the
locktime is reached, we add it to
`OnchainTxHandler::claimable_outpoints` to indicate the outpoint is
now being claimed. However, `claimable_outpoints` is supposed to
track when the outpoint first appeared on chain so that we can
remove the claim if the outpoint is reorged out.
Sadly, in the handling for lock-timed packages, we incorrectly
stored the current height in `claimable_outpoints`, causing such
claims to be removed in case of a reorg right after they were
generated, even if the output we intend to claim isn't removed at
all.
Here we use the creation-height tracking added in the previous
commit to actually address the issue, using the tracked height when
adding a claim to `OnchainTxHandler::claimable_outpoints`.
In cases where we have no information, we continue to use the
current height, retaining the issue for locktimed packages on
upgrades, but this simplifies cases where we actually don't have
the information available anyway.
@TheBlueMatt
TheBlueMattforce-pushed the 2025-07-3860-plus-plus branch from ff95a56 to 21be9c5CompareJuly 10, 2025 22:19
Comment threadlightning/src/chain/onchaintx.rs
for (k, outpoint_confirmation_height) in req.outpoints_and_creation_heights() {
let creation_height = outpoint_confirmation_height.unwrap_or(conf_height);
log_info!(logger, "Registering claiming request for {}:{}, which exists as of height {creation_height}", k.txid, k.vout);
self.claimable_outpoints.insert(k.clone(), (claim_id, creation_height));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hm, shouldn't we prefer signed_locktime over creation_height for outpoints that have one though? While the outpoint hasn't been reorged out, claiming it is no longer possible once the block at signed_locktime is disconnected.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

In practice I think this would just result in us broadcasting things that cannot enter the mempool until we get back to the expected height. If we have any other claims that were merged into the same package for whatever reason, and they are still valid at the disconnected block height, then this would be a greater issue.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

The problem is that when things time out via claimable_outpoints, we don't stop claiming them, we remove them. We don't get them back after a reorg at that point. We could move to pushing things into the locked-packages vec after a block-disconnect, but that seems like a bigger change?

Comment threadlightning/src/ln/reorg_tests.rs Outdated
Comment threadlightning/src/ln/reorg_tests.rs Outdated
@ldk-reviews-bot

Copy link
Copy Markdown

👋 The first review has been submitted!

Do you think this PR is ready for a second reviewer? If so, click here to assign a second reviewer.

@tankyleo
tankyleo self-requested a review July 11, 2025 16:55
@ldk-reviews-bot

Copy link
Copy Markdown

🔔 1st Reminder

Hey @tankyleo! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

This adds a single test which exercises both the ability to prune
locktimed packages when inputs are spent as well as the
creation-height tracking for locktimed packages.
@TheBlueMatt
TheBlueMattforce-pushed the 2025-07-3860-plus-plus branch from 21be9c5 to d7726efCompareJuly 14, 2025 12:53
@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

Cleaned up and improved the test slightly:

$ git diff-tree -U2 21be9c5ed d7726eff0
diff --git a/lightning/src/ln/reorg_tests.rs b/lightning/src/ln/reorg_tests.rs
index 6faabac4c0..c5f512b319 100644
--- a/lightning/src/ln/reorg_tests.rs+++ b/lightning/src/ln/reorg_tests.rs@@ -13,5 +13,5 @@
use crate::chain::chaininterface::LowerBoundedFeeEstimator;
-use crate::chain::channelmonitor::{ANTI_REORG_DELAY, LATENCY_GRACE_PERIOD_BLOCKS};+use crate::chain::channelmonitor::{ANTI_REORG_DELAY, Balance, LATENCY_GRACE_PERIOD_BLOCKS};
use crate::chain::transaction::OutPoint;
use crate::chain::Confirm;
@@ -1058,12 +1058,19 @@ fn do_test_split_htlc_expiry_tracking(use_third_htlc: bool, reorg_out: bool) {
if reorg_out {
- // Reorg out bs_htlc_spend_tx, letting node A the claim all the HTLCs instead.+ // Reorg out bs_htlc_spend_tx, letting node A claim all the HTLCs instead.
disconnect_blocks(&nodes[0], ANTI_REORG_DELAY - 2);
assert_eq!(nodes[0].tx_broadcaster.txn_broadcast().len(), 0);
- // As soon as bs_htlc_spend_tx is disconnected+ // As soon as bs_htlc_spend_tx is disconnected, node A should consider all HTLCs+ // claimable-on-timeout.
disconnect_blocks(&nodes[0], 1);
let balances = nodes[0].chain_monitor.chain_monitor.get_claimable_balances(&[]);
assert_eq!(balances.len(), if use_third_htlc { 3 } else { 2 });
+ for balance in balances {+ if let Balance::MaybeTimeoutClaimableHTLC { .. } = balance {+ } else {+ panic!("Unexpected balance {balance:?}");+ }+ }
connect_blocks(&nodes[0], 100);

@TheBlueMatt
TheBlueMatt merged commit 7e4598d into lightningdevkit:mainJul 15, 2025
@TheBlueMattTheBlueMatt mentioned this pull request Jul 15, 2025
@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

Backported in #3932

@TheBlueMattTheBlueMatt self-assigned this Jul 17, 2025
TheBlueMatt added a commit to TheBlueMatt/rust-lightning that referenced this pull request Jul 24, 2025
v0.1.5 - Jul 16, 2025 - "Async Path Reduction"
Performance Improvements
========================
* `NetworkGraph`'s expensive internal consistency checks have now been
disabled in debug builds in addition to release builds (lightningdevkit#3687).
Bug Fixes
=========
* Pathfinding which results in a multi-path payment is now substantially
smarter, using fewer paths and better optimizing fees and successes (lightningdevkit#3890).
* A counterparty delaying claiming multiple HTLCs with different expiries can
no longer cause our `ChannelMonitor` to continuously rebroadcast invalid
transactions or RBF bump attempts (lightningdevkit#3923).
* Reorgs can no longer cause us to fail to claim HTLCs after a counterparty
delayed claiming multiple HTLCs with different expiries (lightningdevkit#3923).
* Force-closing a channel while it is blocked on another channel's async
`ChannelMonitorUpdate` can no longer lead to a panic (lightningdevkit#3858).
* `ChannelMonitorUpdate`s can no longer be released to storage too early when
doing async updates or on restart. This only impacts async
`ChannelMonitorUpdate` persistence and can lead to loss of funds only in rare
cases with `ChannelMonitorUpdate` persistence order inversions (lightningdevkit#3907).
Security
========
0.1.5 fixes a vulnerability which could allow a peer to overdraw their reserve
value, potentially cutting into commitment transaction fees on channels with a
low reserve.
* Due to a bug in checking whether an HTLC is dust during acceptance, near-dust
HTLCs were not counted towards the commitment transaction fee, but did
eventually contribute to it when we built a commitment transaction. This can
be used by a counterparty to overdraw their reserve value, or, for channels
with a low reserve value, cut into the commitment transaction fee (lightningdevkit#3933).
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

5 participants

@TheBlueMatt@ldk-reviews-bot@wpaulino@tankyleo@whfuyn
, '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

Locktimed packages fixes - #3923

Merged
TheBlueMatt merged 5 commits into
lightningdevkit:mainfrom
TheBlueMatt:2025-07-3860-plus-plus
Jul 15, 2025
Merged

Locktimed packages fixes#3923
TheBlueMatt merged 5 commits into
lightningdevkit:mainfrom
TheBlueMatt:2025-07-3860-plus-plus

Conversation

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

See individual commits for more. This is #3860 with an additional issue I found while writing a test for it fixed.

TheBlueMattand others added 2 commits July 10, 2025 17:56
In a number of tests we require available UTXOs to do HTLC anchor
claims by bringing our own fees. We previously wrote that out in
each test, which is somewhat verbose, so here we simply add a test
utility that gives each node a full BTC in a single UTXO.
We have to prune locktimed packages when their inputs are spent,
otherwise the notification of the watched outputs might be missed. This
can lead to locktimed packages with spent inputs being added back to
the pending claim requests in the future, and they are never cleaned
up until node restart.
Resolves: lightningdevkit#3859
@TheBlueMattTheBlueMatt added this to the 0.2 milestone Jul 10, 2025
@ldk-reviews-bot

ldk-reviews-bot commented Jul 10, 2025

Copy link
Copy Markdown

👋 Thanks for assigning @tankyleo as a reviewer!
I'll wait for their review and will help manage the review process.
Once they submit their review, I'll check if a second reviewer would be helpful.

Comment threadlightning/src/ln/reorg_tests.rs Outdated
When we have an outpoint to claim which is lock-timed and the
locktime is reached, we add it to
`OnchainTxHandler::claimable_outpoints` to indicate the outpoint is
now being claimed. However, `claimable_outpoints` is supposed to
track when the outpoint first appeared on chain so that we can
remove the claim if the outpoint is reorged out.
Sadly, in the handling for lock-timed packages, we incorrectly
stored the current height in `claimable_outpoints`, causing such
claims to be removed in case of a reorg right after they were
generated, even if the output we intend to claim isn't removed at
all.
Here we start tracking when the outpoint we're spending was created
in `PackageSolvingData`'s constituent types. While we could have
tracked this information in `PackageTemplate`, it would preclude
later merging packages that are spending outpoints included in
different blocks, which we don't necessarily want to do.
When we have an outpoint to claim which is lock-timed and the
locktime is reached, we add it to
`OnchainTxHandler::claimable_outpoints` to indicate the outpoint is
now being claimed. However, `claimable_outpoints` is supposed to
track when the outpoint first appeared on chain so that we can
remove the claim if the outpoint is reorged out.
Sadly, in the handling for lock-timed packages, we incorrectly
stored the current height in `claimable_outpoints`, causing such
claims to be removed in case of a reorg right after they were
generated, even if the output we intend to claim isn't removed at
all.
Here we use the creation-height tracking added in the previous
commit to actually address the issue, using the tracked height when
adding a claim to `OnchainTxHandler::claimable_outpoints`.
In cases where we have no information, we continue to use the
current height, retaining the issue for locktimed packages on
upgrades, but this simplifies cases where we actually don't have
the information available anyway.
@TheBlueMatt
TheBlueMattforce-pushed the 2025-07-3860-plus-plus branch from ff95a56 to 21be9c5CompareJuly 10, 2025 22:19
Comment threadlightning/src/chain/onchaintx.rs
for (k, outpoint_confirmation_height) in req.outpoints_and_creation_heights() {
let creation_height = outpoint_confirmation_height.unwrap_or(conf_height);
log_info!(logger, "Registering claiming request for {}:{}, which exists as of height {creation_height}", k.txid, k.vout);
self.claimable_outpoints.insert(k.clone(), (claim_id, creation_height));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hm, shouldn't we prefer signed_locktime over creation_height for outpoints that have one though? While the outpoint hasn't been reorged out, claiming it is no longer possible once the block at signed_locktime is disconnected.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

In practice I think this would just result in us broadcasting things that cannot enter the mempool until we get back to the expected height. If we have any other claims that were merged into the same package for whatever reason, and they are still valid at the disconnected block height, then this would be a greater issue.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

The problem is that when things time out via claimable_outpoints, we don't stop claiming them, we remove them. We don't get them back after a reorg at that point. We could move to pushing things into the locked-packages vec after a block-disconnect, but that seems like a bigger change?

Comment threadlightning/src/ln/reorg_tests.rs Outdated
Comment threadlightning/src/ln/reorg_tests.rs Outdated
@ldk-reviews-bot

Copy link
Copy Markdown

👋 The first review has been submitted!

Do you think this PR is ready for a second reviewer? If so, click here to assign a second reviewer.

@tankyleo
tankyleo self-requested a review July 11, 2025 16:55
@ldk-reviews-bot

Copy link
Copy Markdown

🔔 1st Reminder

Hey @tankyleo! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

This adds a single test which exercises both the ability to prune
locktimed packages when inputs are spent as well as the
creation-height tracking for locktimed packages.
@TheBlueMatt
TheBlueMattforce-pushed the 2025-07-3860-plus-plus branch from 21be9c5 to d7726efCompareJuly 14, 2025 12:53
@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

Cleaned up and improved the test slightly:

$ git diff-tree -U2 21be9c5ed d7726eff0
diff --git a/lightning/src/ln/reorg_tests.rs b/lightning/src/ln/reorg_tests.rs
index 6faabac4c0..c5f512b319 100644
--- a/lightning/src/ln/reorg_tests.rs+++ b/lightning/src/ln/reorg_tests.rs@@ -13,5 +13,5 @@
use crate::chain::chaininterface::LowerBoundedFeeEstimator;
-use crate::chain::channelmonitor::{ANTI_REORG_DELAY, LATENCY_GRACE_PERIOD_BLOCKS};+use crate::chain::channelmonitor::{ANTI_REORG_DELAY, Balance, LATENCY_GRACE_PERIOD_BLOCKS};
use crate::chain::transaction::OutPoint;
use crate::chain::Confirm;
@@ -1058,12 +1058,19 @@ fn do_test_split_htlc_expiry_tracking(use_third_htlc: bool, reorg_out: bool) {
if reorg_out {
- // Reorg out bs_htlc_spend_tx, letting node A the claim all the HTLCs instead.+ // Reorg out bs_htlc_spend_tx, letting node A claim all the HTLCs instead.
disconnect_blocks(&nodes[0], ANTI_REORG_DELAY - 2);
assert_eq!(nodes[0].tx_broadcaster.txn_broadcast().len(), 0);
- // As soon as bs_htlc_spend_tx is disconnected+ // As soon as bs_htlc_spend_tx is disconnected, node A should consider all HTLCs+ // claimable-on-timeout.
disconnect_blocks(&nodes[0], 1);
let balances = nodes[0].chain_monitor.chain_monitor.get_claimable_balances(&[]);
assert_eq!(balances.len(), if use_third_htlc { 3 } else { 2 });
+ for balance in balances {+ if let Balance::MaybeTimeoutClaimableHTLC { .. } = balance {+ } else {+ panic!("Unexpected balance {balance:?}");+ }+ }
connect_blocks(&nodes[0], 100);

@TheBlueMatt
TheBlueMatt merged commit 7e4598d into lightningdevkit:mainJul 15, 2025
@TheBlueMattTheBlueMatt mentioned this pull request Jul 15, 2025
@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

Backported in #3932

@TheBlueMattTheBlueMatt self-assigned this Jul 17, 2025
TheBlueMatt added a commit to TheBlueMatt/rust-lightning that referenced this pull request Jul 24, 2025
v0.1.5 - Jul 16, 2025 - "Async Path Reduction"
Performance Improvements
========================
* `NetworkGraph`'s expensive internal consistency checks have now been
disabled in debug builds in addition to release builds (lightningdevkit#3687).
Bug Fixes
=========
* Pathfinding which results in a multi-path payment is now substantially
smarter, using fewer paths and better optimizing fees and successes (lightningdevkit#3890).
* A counterparty delaying claiming multiple HTLCs with different expiries can
no longer cause our `ChannelMonitor` to continuously rebroadcast invalid
transactions or RBF bump attempts (lightningdevkit#3923).
* Reorgs can no longer cause us to fail to claim HTLCs after a counterparty
delayed claiming multiple HTLCs with different expiries (lightningdevkit#3923).
* Force-closing a channel while it is blocked on another channel's async
`ChannelMonitorUpdate` can no longer lead to a panic (lightningdevkit#3858).
* `ChannelMonitorUpdate`s can no longer be released to storage too early when
doing async updates or on restart. This only impacts async
`ChannelMonitorUpdate` persistence and can lead to loss of funds only in rare
cases with `ChannelMonitorUpdate` persistence order inversions (lightningdevkit#3907).
Security
========
0.1.5 fixes a vulnerability which could allow a peer to overdraw their reserve
value, potentially cutting into commitment transaction fees on channels with a
low reserve.
* Due to a bug in checking whether an HTLC is dust during acceptance, near-dust
HTLCs were not counted towards the commitment transaction fee, but did
eventually contribute to it when we built a commitment transaction. This can
be used by a counterparty to overdraw their reserve value, or, for channels
with a low reserve value, cut into the commitment transaction fee (lightningdevkit#3933).
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

5 participants

@TheBlueMatt@ldk-reviews-bot@wpaulino@tankyleo@whfuyn
, '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

Locktimed packages fixes - #3923

Merged
TheBlueMatt merged 5 commits into
lightningdevkit:mainfrom
TheBlueMatt:2025-07-3860-plus-plus
Jul 15, 2025
Merged

Locktimed packages fixes#3923
TheBlueMatt merged 5 commits into
lightningdevkit:mainfrom
TheBlueMatt:2025-07-3860-plus-plus

Conversation

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

See individual commits for more. This is #3860 with an additional issue I found while writing a test for it fixed.

TheBlueMattand others added 2 commits July 10, 2025 17:56
In a number of tests we require available UTXOs to do HTLC anchor
claims by bringing our own fees. We previously wrote that out in
each test, which is somewhat verbose, so here we simply add a test
utility that gives each node a full BTC in a single UTXO.
We have to prune locktimed packages when their inputs are spent,
otherwise the notification of the watched outputs might be missed. This
can lead to locktimed packages with spent inputs being added back to
the pending claim requests in the future, and they are never cleaned
up until node restart.
Resolves: lightningdevkit#3859
@TheBlueMattTheBlueMatt added this to the 0.2 milestone Jul 10, 2025
@ldk-reviews-bot

ldk-reviews-bot commented Jul 10, 2025

Copy link
Copy Markdown

👋 Thanks for assigning @tankyleo as a reviewer!
I'll wait for their review and will help manage the review process.
Once they submit their review, I'll check if a second reviewer would be helpful.

Comment threadlightning/src/ln/reorg_tests.rs Outdated
When we have an outpoint to claim which is lock-timed and the
locktime is reached, we add it to
`OnchainTxHandler::claimable_outpoints` to indicate the outpoint is
now being claimed. However, `claimable_outpoints` is supposed to
track when the outpoint first appeared on chain so that we can
remove the claim if the outpoint is reorged out.
Sadly, in the handling for lock-timed packages, we incorrectly
stored the current height in `claimable_outpoints`, causing such
claims to be removed in case of a reorg right after they were
generated, even if the output we intend to claim isn't removed at
all.
Here we start tracking when the outpoint we're spending was created
in `PackageSolvingData`'s constituent types. While we could have
tracked this information in `PackageTemplate`, it would preclude
later merging packages that are spending outpoints included in
different blocks, which we don't necessarily want to do.
When we have an outpoint to claim which is lock-timed and the
locktime is reached, we add it to
`OnchainTxHandler::claimable_outpoints` to indicate the outpoint is
now being claimed. However, `claimable_outpoints` is supposed to
track when the outpoint first appeared on chain so that we can
remove the claim if the outpoint is reorged out.
Sadly, in the handling for lock-timed packages, we incorrectly
stored the current height in `claimable_outpoints`, causing such
claims to be removed in case of a reorg right after they were
generated, even if the output we intend to claim isn't removed at
all.
Here we use the creation-height tracking added in the previous
commit to actually address the issue, using the tracked height when
adding a claim to `OnchainTxHandler::claimable_outpoints`.
In cases where we have no information, we continue to use the
current height, retaining the issue for locktimed packages on
upgrades, but this simplifies cases where we actually don't have
the information available anyway.
@TheBlueMatt
TheBlueMattforce-pushed the 2025-07-3860-plus-plus branch from ff95a56 to 21be9c5CompareJuly 10, 2025 22:19
Comment threadlightning/src/chain/onchaintx.rs
for (k, outpoint_confirmation_height) in req.outpoints_and_creation_heights() {
let creation_height = outpoint_confirmation_height.unwrap_or(conf_height);
log_info!(logger, "Registering claiming request for {}:{}, which exists as of height {creation_height}", k.txid, k.vout);
self.claimable_outpoints.insert(k.clone(), (claim_id, creation_height));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hm, shouldn't we prefer signed_locktime over creation_height for outpoints that have one though? While the outpoint hasn't been reorged out, claiming it is no longer possible once the block at signed_locktime is disconnected.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

In practice I think this would just result in us broadcasting things that cannot enter the mempool until we get back to the expected height. If we have any other claims that were merged into the same package for whatever reason, and they are still valid at the disconnected block height, then this would be a greater issue.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

The problem is that when things time out via claimable_outpoints, we don't stop claiming them, we remove them. We don't get them back after a reorg at that point. We could move to pushing things into the locked-packages vec after a block-disconnect, but that seems like a bigger change?

Comment threadlightning/src/ln/reorg_tests.rs Outdated
Comment threadlightning/src/ln/reorg_tests.rs Outdated
@ldk-reviews-bot

Copy link
Copy Markdown

👋 The first review has been submitted!

Do you think this PR is ready for a second reviewer? If so, click here to assign a second reviewer.

@tankyleo
tankyleo self-requested a review July 11, 2025 16:55
@ldk-reviews-bot

Copy link
Copy Markdown

🔔 1st Reminder

Hey @tankyleo! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

This adds a single test which exercises both the ability to prune
locktimed packages when inputs are spent as well as the
creation-height tracking for locktimed packages.
@TheBlueMatt
TheBlueMattforce-pushed the 2025-07-3860-plus-plus branch from 21be9c5 to d7726efCompareJuly 14, 2025 12:53
@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

Cleaned up and improved the test slightly:

$ git diff-tree -U2 21be9c5ed d7726eff0
diff --git a/lightning/src/ln/reorg_tests.rs b/lightning/src/ln/reorg_tests.rs
index 6faabac4c0..c5f512b319 100644
--- a/lightning/src/ln/reorg_tests.rs+++ b/lightning/src/ln/reorg_tests.rs@@ -13,5 +13,5 @@
use crate::chain::chaininterface::LowerBoundedFeeEstimator;
-use crate::chain::channelmonitor::{ANTI_REORG_DELAY, LATENCY_GRACE_PERIOD_BLOCKS};+use crate::chain::channelmonitor::{ANTI_REORG_DELAY, Balance, LATENCY_GRACE_PERIOD_BLOCKS};
use crate::chain::transaction::OutPoint;
use crate::chain::Confirm;
@@ -1058,12 +1058,19 @@ fn do_test_split_htlc_expiry_tracking(use_third_htlc: bool, reorg_out: bool) {
if reorg_out {
- // Reorg out bs_htlc_spend_tx, letting node A the claim all the HTLCs instead.+ // Reorg out bs_htlc_spend_tx, letting node A claim all the HTLCs instead.
disconnect_blocks(&nodes[0], ANTI_REORG_DELAY - 2);
assert_eq!(nodes[0].tx_broadcaster.txn_broadcast().len(), 0);
- // As soon as bs_htlc_spend_tx is disconnected+ // As soon as bs_htlc_spend_tx is disconnected, node A should consider all HTLCs+ // claimable-on-timeout.
disconnect_blocks(&nodes[0], 1);
let balances = nodes[0].chain_monitor.chain_monitor.get_claimable_balances(&[]);
assert_eq!(balances.len(), if use_third_htlc { 3 } else { 2 });
+ for balance in balances {+ if let Balance::MaybeTimeoutClaimableHTLC { .. } = balance {+ } else {+ panic!("Unexpected balance {balance:?}");+ }+ }
connect_blocks(&nodes[0], 100);

@TheBlueMatt
TheBlueMatt merged commit 7e4598d into lightningdevkit:mainJul 15, 2025
@TheBlueMattTheBlueMatt mentioned this pull request Jul 15, 2025
@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

Backported in #3932

@TheBlueMattTheBlueMatt self-assigned this Jul 17, 2025
TheBlueMatt added a commit to TheBlueMatt/rust-lightning that referenced this pull request Jul 24, 2025
v0.1.5 - Jul 16, 2025 - "Async Path Reduction"
Performance Improvements
========================
* `NetworkGraph`'s expensive internal consistency checks have now been
disabled in debug builds in addition to release builds (lightningdevkit#3687).
Bug Fixes
=========
* Pathfinding which results in a multi-path payment is now substantially
smarter, using fewer paths and better optimizing fees and successes (lightningdevkit#3890).
* A counterparty delaying claiming multiple HTLCs with different expiries can
no longer cause our `ChannelMonitor` to continuously rebroadcast invalid
transactions or RBF bump attempts (lightningdevkit#3923).
* Reorgs can no longer cause us to fail to claim HTLCs after a counterparty
delayed claiming multiple HTLCs with different expiries (lightningdevkit#3923).
* Force-closing a channel while it is blocked on another channel's async
`ChannelMonitorUpdate` can no longer lead to a panic (lightningdevkit#3858).
* `ChannelMonitorUpdate`s can no longer be released to storage too early when
doing async updates or on restart. This only impacts async
`ChannelMonitorUpdate` persistence and can lead to loss of funds only in rare
cases with `ChannelMonitorUpdate` persistence order inversions (lightningdevkit#3907).
Security
========
0.1.5 fixes a vulnerability which could allow a peer to overdraw their reserve
value, potentially cutting into commitment transaction fees on channels with a
low reserve.
* Due to a bug in checking whether an HTLC is dust during acceptance, near-dust
HTLCs were not counted towards the commitment transaction fee, but did
eventually contribute to it when we built a commitment transaction. This can
be used by a counterparty to overdraw their reserve value, or, for channels
with a low reserve value, cut into the commitment transaction fee (lightningdevkit#3933).
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

5 participants

@TheBlueMatt@ldk-reviews-bot@wpaulino@tankyleo@whfuyn
, '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

Locktimed packages fixes - #3923

Merged
TheBlueMatt merged 5 commits into
lightningdevkit:mainfrom
TheBlueMatt:2025-07-3860-plus-plus
Jul 15, 2025
Merged

Locktimed packages fixes#3923
TheBlueMatt merged 5 commits into
lightningdevkit:mainfrom
TheBlueMatt:2025-07-3860-plus-plus

Conversation

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

See individual commits for more. This is #3860 with an additional issue I found while writing a test for it fixed.

TheBlueMattand others added 2 commits July 10, 2025 17:56
In a number of tests we require available UTXOs to do HTLC anchor
claims by bringing our own fees. We previously wrote that out in
each test, which is somewhat verbose, so here we simply add a test
utility that gives each node a full BTC in a single UTXO.
We have to prune locktimed packages when their inputs are spent,
otherwise the notification of the watched outputs might be missed. This
can lead to locktimed packages with spent inputs being added back to
the pending claim requests in the future, and they are never cleaned
up until node restart.
Resolves: lightningdevkit#3859
@TheBlueMattTheBlueMatt added this to the 0.2 milestone Jul 10, 2025
@ldk-reviews-bot

ldk-reviews-bot commented Jul 10, 2025

Copy link
Copy Markdown

👋 Thanks for assigning @tankyleo as a reviewer!
I'll wait for their review and will help manage the review process.
Once they submit their review, I'll check if a second reviewer would be helpful.

Comment threadlightning/src/ln/reorg_tests.rs Outdated
When we have an outpoint to claim which is lock-timed and the
locktime is reached, we add it to
`OnchainTxHandler::claimable_outpoints` to indicate the outpoint is
now being claimed. However, `claimable_outpoints` is supposed to
track when the outpoint first appeared on chain so that we can
remove the claim if the outpoint is reorged out.
Sadly, in the handling for lock-timed packages, we incorrectly
stored the current height in `claimable_outpoints`, causing such
claims to be removed in case of a reorg right after they were
generated, even if the output we intend to claim isn't removed at
all.
Here we start tracking when the outpoint we're spending was created
in `PackageSolvingData`'s constituent types. While we could have
tracked this information in `PackageTemplate`, it would preclude
later merging packages that are spending outpoints included in
different blocks, which we don't necessarily want to do.
When we have an outpoint to claim which is lock-timed and the
locktime is reached, we add it to
`OnchainTxHandler::claimable_outpoints` to indicate the outpoint is
now being claimed. However, `claimable_outpoints` is supposed to
track when the outpoint first appeared on chain so that we can
remove the claim if the outpoint is reorged out.
Sadly, in the handling for lock-timed packages, we incorrectly
stored the current height in `claimable_outpoints`, causing such
claims to be removed in case of a reorg right after they were
generated, even if the output we intend to claim isn't removed at
all.
Here we use the creation-height tracking added in the previous
commit to actually address the issue, using the tracked height when
adding a claim to `OnchainTxHandler::claimable_outpoints`.
In cases where we have no information, we continue to use the
current height, retaining the issue for locktimed packages on
upgrades, but this simplifies cases where we actually don't have
the information available anyway.
@TheBlueMatt
TheBlueMattforce-pushed the 2025-07-3860-plus-plus branch from ff95a56 to 21be9c5CompareJuly 10, 2025 22:19
Comment threadlightning/src/chain/onchaintx.rs
for (k, outpoint_confirmation_height) in req.outpoints_and_creation_heights() {
let creation_height = outpoint_confirmation_height.unwrap_or(conf_height);
log_info!(logger, "Registering claiming request for {}:{}, which exists as of height {creation_height}", k.txid, k.vout);
self.claimable_outpoints.insert(k.clone(), (claim_id, creation_height));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hm, shouldn't we prefer signed_locktime over creation_height for outpoints that have one though? While the outpoint hasn't been reorged out, claiming it is no longer possible once the block at signed_locktime is disconnected.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

In practice I think this would just result in us broadcasting things that cannot enter the mempool until we get back to the expected height. If we have any other claims that were merged into the same package for whatever reason, and they are still valid at the disconnected block height, then this would be a greater issue.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

The problem is that when things time out via claimable_outpoints, we don't stop claiming them, we remove them. We don't get them back after a reorg at that point. We could move to pushing things into the locked-packages vec after a block-disconnect, but that seems like a bigger change?

Comment threadlightning/src/ln/reorg_tests.rs Outdated
Comment threadlightning/src/ln/reorg_tests.rs Outdated
@ldk-reviews-bot

Copy link
Copy Markdown

👋 The first review has been submitted!

Do you think this PR is ready for a second reviewer? If so, click here to assign a second reviewer.

@tankyleo
tankyleo self-requested a review July 11, 2025 16:55
@ldk-reviews-bot

Copy link
Copy Markdown

🔔 1st Reminder

Hey @tankyleo! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

This adds a single test which exercises both the ability to prune
locktimed packages when inputs are spent as well as the
creation-height tracking for locktimed packages.
@TheBlueMatt
TheBlueMattforce-pushed the 2025-07-3860-plus-plus branch from 21be9c5 to d7726efCompareJuly 14, 2025 12:53
@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

Cleaned up and improved the test slightly:

$ git diff-tree -U2 21be9c5ed d7726eff0
diff --git a/lightning/src/ln/reorg_tests.rs b/lightning/src/ln/reorg_tests.rs
index 6faabac4c0..c5f512b319 100644
--- a/lightning/src/ln/reorg_tests.rs+++ b/lightning/src/ln/reorg_tests.rs@@ -13,5 +13,5 @@
use crate::chain::chaininterface::LowerBoundedFeeEstimator;
-use crate::chain::channelmonitor::{ANTI_REORG_DELAY, LATENCY_GRACE_PERIOD_BLOCKS};+use crate::chain::channelmonitor::{ANTI_REORG_DELAY, Balance, LATENCY_GRACE_PERIOD_BLOCKS};
use crate::chain::transaction::OutPoint;
use crate::chain::Confirm;
@@ -1058,12 +1058,19 @@ fn do_test_split_htlc_expiry_tracking(use_third_htlc: bool, reorg_out: bool) {
if reorg_out {
- // Reorg out bs_htlc_spend_tx, letting node A the claim all the HTLCs instead.+ // Reorg out bs_htlc_spend_tx, letting node A claim all the HTLCs instead.
disconnect_blocks(&nodes[0], ANTI_REORG_DELAY - 2);
assert_eq!(nodes[0].tx_broadcaster.txn_broadcast().len(), 0);
- // As soon as bs_htlc_spend_tx is disconnected+ // As soon as bs_htlc_spend_tx is disconnected, node A should consider all HTLCs+ // claimable-on-timeout.
disconnect_blocks(&nodes[0], 1);
let balances = nodes[0].chain_monitor.chain_monitor.get_claimable_balances(&[]);
assert_eq!(balances.len(), if use_third_htlc { 3 } else { 2 });
+ for balance in balances {+ if let Balance::MaybeTimeoutClaimableHTLC { .. } = balance {+ } else {+ panic!("Unexpected balance {balance:?}");+ }+ }
connect_blocks(&nodes[0], 100);

@TheBlueMatt
TheBlueMatt merged commit 7e4598d into lightningdevkit:mainJul 15, 2025
@TheBlueMattTheBlueMatt mentioned this pull request Jul 15, 2025
@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

Backported in #3932

@TheBlueMattTheBlueMatt self-assigned this Jul 17, 2025
TheBlueMatt added a commit to TheBlueMatt/rust-lightning that referenced this pull request Jul 24, 2025
v0.1.5 - Jul 16, 2025 - "Async Path Reduction"
Performance Improvements
========================
* `NetworkGraph`'s expensive internal consistency checks have now been
disabled in debug builds in addition to release builds (lightningdevkit#3687).
Bug Fixes
=========
* Pathfinding which results in a multi-path payment is now substantially
smarter, using fewer paths and better optimizing fees and successes (lightningdevkit#3890).
* A counterparty delaying claiming multiple HTLCs with different expiries can
no longer cause our `ChannelMonitor` to continuously rebroadcast invalid
transactions or RBF bump attempts (lightningdevkit#3923).
* Reorgs can no longer cause us to fail to claim HTLCs after a counterparty
delayed claiming multiple HTLCs with different expiries (lightningdevkit#3923).
* Force-closing a channel while it is blocked on another channel's async
`ChannelMonitorUpdate` can no longer lead to a panic (lightningdevkit#3858).
* `ChannelMonitorUpdate`s can no longer be released to storage too early when
doing async updates or on restart. This only impacts async
`ChannelMonitorUpdate` persistence and can lead to loss of funds only in rare
cases with `ChannelMonitorUpdate` persistence order inversions (lightningdevkit#3907).
Security
========
0.1.5 fixes a vulnerability which could allow a peer to overdraw their reserve
value, potentially cutting into commitment transaction fees on channels with a
low reserve.
* Due to a bug in checking whether an HTLC is dust during acceptance, near-dust
HTLCs were not counted towards the commitment transaction fee, but did
eventually contribute to it when we built a commitment transaction. This can
be used by a counterparty to overdraw their reserve value, or, for channels
with a low reserve value, cut into the commitment transaction fee (lightningdevkit#3933).
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

5 participants

@TheBlueMatt@ldk-reviews-bot@wpaulino@tankyleo@whfuyn
, '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

Locktimed packages fixes - #3923

Merged
TheBlueMatt merged 5 commits into
lightningdevkit:mainfrom
TheBlueMatt:2025-07-3860-plus-plus
Jul 15, 2025
Merged

Locktimed packages fixes#3923
TheBlueMatt merged 5 commits into
lightningdevkit:mainfrom
TheBlueMatt:2025-07-3860-plus-plus

Conversation

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

See individual commits for more. This is #3860 with an additional issue I found while writing a test for it fixed.

TheBlueMattand others added 2 commits July 10, 2025 17:56
In a number of tests we require available UTXOs to do HTLC anchor
claims by bringing our own fees. We previously wrote that out in
each test, which is somewhat verbose, so here we simply add a test
utility that gives each node a full BTC in a single UTXO.
We have to prune locktimed packages when their inputs are spent,
otherwise the notification of the watched outputs might be missed. This
can lead to locktimed packages with spent inputs being added back to
the pending claim requests in the future, and they are never cleaned
up until node restart.
Resolves: lightningdevkit#3859
@TheBlueMattTheBlueMatt added this to the 0.2 milestone Jul 10, 2025
@ldk-reviews-bot

ldk-reviews-bot commented Jul 10, 2025

Copy link
Copy Markdown

👋 Thanks for assigning @tankyleo as a reviewer!
I'll wait for their review and will help manage the review process.
Once they submit their review, I'll check if a second reviewer would be helpful.

Comment threadlightning/src/ln/reorg_tests.rs Outdated
When we have an outpoint to claim which is lock-timed and the
locktime is reached, we add it to
`OnchainTxHandler::claimable_outpoints` to indicate the outpoint is
now being claimed. However, `claimable_outpoints` is supposed to
track when the outpoint first appeared on chain so that we can
remove the claim if the outpoint is reorged out.
Sadly, in the handling for lock-timed packages, we incorrectly
stored the current height in `claimable_outpoints`, causing such
claims to be removed in case of a reorg right after they were
generated, even if the output we intend to claim isn't removed at
all.
Here we start tracking when the outpoint we're spending was created
in `PackageSolvingData`'s constituent types. While we could have
tracked this information in `PackageTemplate`, it would preclude
later merging packages that are spending outpoints included in
different blocks, which we don't necessarily want to do.
When we have an outpoint to claim which is lock-timed and the
locktime is reached, we add it to
`OnchainTxHandler::claimable_outpoints` to indicate the outpoint is
now being claimed. However, `claimable_outpoints` is supposed to
track when the outpoint first appeared on chain so that we can
remove the claim if the outpoint is reorged out.
Sadly, in the handling for lock-timed packages, we incorrectly
stored the current height in `claimable_outpoints`, causing such
claims to be removed in case of a reorg right after they were
generated, even if the output we intend to claim isn't removed at
all.
Here we use the creation-height tracking added in the previous
commit to actually address the issue, using the tracked height when
adding a claim to `OnchainTxHandler::claimable_outpoints`.
In cases where we have no information, we continue to use the
current height, retaining the issue for locktimed packages on
upgrades, but this simplifies cases where we actually don't have
the information available anyway.
@TheBlueMatt
TheBlueMattforce-pushed the 2025-07-3860-plus-plus branch from ff95a56 to 21be9c5CompareJuly 10, 2025 22:19
Comment threadlightning/src/chain/onchaintx.rs
for (k, outpoint_confirmation_height) in req.outpoints_and_creation_heights() {
let creation_height = outpoint_confirmation_height.unwrap_or(conf_height);
log_info!(logger, "Registering claiming request for {}:{}, which exists as of height {creation_height}", k.txid, k.vout);
self.claimable_outpoints.insert(k.clone(), (claim_id, creation_height));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hm, shouldn't we prefer signed_locktime over creation_height for outpoints that have one though? While the outpoint hasn't been reorged out, claiming it is no longer possible once the block at signed_locktime is disconnected.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

In practice I think this would just result in us broadcasting things that cannot enter the mempool until we get back to the expected height. If we have any other claims that were merged into the same package for whatever reason, and they are still valid at the disconnected block height, then this would be a greater issue.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

The problem is that when things time out via claimable_outpoints, we don't stop claiming them, we remove them. We don't get them back after a reorg at that point. We could move to pushing things into the locked-packages vec after a block-disconnect, but that seems like a bigger change?

Comment threadlightning/src/ln/reorg_tests.rs Outdated
Comment threadlightning/src/ln/reorg_tests.rs Outdated
@ldk-reviews-bot

Copy link
Copy Markdown

👋 The first review has been submitted!

Do you think this PR is ready for a second reviewer? If so, click here to assign a second reviewer.

@tankyleo
tankyleo self-requested a review July 11, 2025 16:55
@ldk-reviews-bot

Copy link
Copy Markdown

🔔 1st Reminder

Hey @tankyleo! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

This adds a single test which exercises both the ability to prune
locktimed packages when inputs are spent as well as the
creation-height tracking for locktimed packages.
@TheBlueMatt
TheBlueMattforce-pushed the 2025-07-3860-plus-plus branch from 21be9c5 to d7726efCompareJuly 14, 2025 12:53
@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

Cleaned up and improved the test slightly:

$ git diff-tree -U2 21be9c5ed d7726eff0
diff --git a/lightning/src/ln/reorg_tests.rs b/lightning/src/ln/reorg_tests.rs
index 6faabac4c0..c5f512b319 100644
--- a/lightning/src/ln/reorg_tests.rs+++ b/lightning/src/ln/reorg_tests.rs@@ -13,5 +13,5 @@
use crate::chain::chaininterface::LowerBoundedFeeEstimator;
-use crate::chain::channelmonitor::{ANTI_REORG_DELAY, LATENCY_GRACE_PERIOD_BLOCKS};+use crate::chain::channelmonitor::{ANTI_REORG_DELAY, Balance, LATENCY_GRACE_PERIOD_BLOCKS};
use crate::chain::transaction::OutPoint;
use crate::chain::Confirm;
@@ -1058,12 +1058,19 @@ fn do_test_split_htlc_expiry_tracking(use_third_htlc: bool, reorg_out: bool) {
if reorg_out {
- // Reorg out bs_htlc_spend_tx, letting node A the claim all the HTLCs instead.+ // Reorg out bs_htlc_spend_tx, letting node A claim all the HTLCs instead.
disconnect_blocks(&nodes[0], ANTI_REORG_DELAY - 2);
assert_eq!(nodes[0].tx_broadcaster.txn_broadcast().len(), 0);
- // As soon as bs_htlc_spend_tx is disconnected+ // As soon as bs_htlc_spend_tx is disconnected, node A should consider all HTLCs+ // claimable-on-timeout.
disconnect_blocks(&nodes[0], 1);
let balances = nodes[0].chain_monitor.chain_monitor.get_claimable_balances(&[]);
assert_eq!(balances.len(), if use_third_htlc { 3 } else { 2 });
+ for balance in balances {+ if let Balance::MaybeTimeoutClaimableHTLC { .. } = balance {+ } else {+ panic!("Unexpected balance {balance:?}");+ }+ }
connect_blocks(&nodes[0], 100);

@TheBlueMatt
TheBlueMatt merged commit 7e4598d into lightningdevkit:mainJul 15, 2025
@TheBlueMattTheBlueMatt mentioned this pull request Jul 15, 2025
@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

Backported in #3932

@TheBlueMattTheBlueMatt self-assigned this Jul 17, 2025
TheBlueMatt added a commit to TheBlueMatt/rust-lightning that referenced this pull request Jul 24, 2025
v0.1.5 - Jul 16, 2025 - "Async Path Reduction"
Performance Improvements
========================
* `NetworkGraph`'s expensive internal consistency checks have now been
disabled in debug builds in addition to release builds (lightningdevkit#3687).
Bug Fixes
=========
* Pathfinding which results in a multi-path payment is now substantially
smarter, using fewer paths and better optimizing fees and successes (lightningdevkit#3890).
* A counterparty delaying claiming multiple HTLCs with different expiries can
no longer cause our `ChannelMonitor` to continuously rebroadcast invalid
transactions or RBF bump attempts (lightningdevkit#3923).
* Reorgs can no longer cause us to fail to claim HTLCs after a counterparty
delayed claiming multiple HTLCs with different expiries (lightningdevkit#3923).
* Force-closing a channel while it is blocked on another channel's async
`ChannelMonitorUpdate` can no longer lead to a panic (lightningdevkit#3858).
* `ChannelMonitorUpdate`s can no longer be released to storage too early when
doing async updates or on restart. This only impacts async
`ChannelMonitorUpdate` persistence and can lead to loss of funds only in rare
cases with `ChannelMonitorUpdate` persistence order inversions (lightningdevkit#3907).
Security
========
0.1.5 fixes a vulnerability which could allow a peer to overdraw their reserve
value, potentially cutting into commitment transaction fees on channels with a
low reserve.
* Due to a bug in checking whether an HTLC is dust during acceptance, near-dust
HTLCs were not counted towards the commitment transaction fee, but did
eventually contribute to it when we built a commitment transaction. This can
be used by a counterparty to overdraw their reserve value, or, for channels
with a low reserve value, cut into the commitment transaction fee (lightningdevkit#3933).
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

5 participants

@TheBlueMatt@ldk-reviews-bot@wpaulino@tankyleo@whfuyn
, '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

Locktimed packages fixes - #3923

Merged
TheBlueMatt merged 5 commits into
lightningdevkit:mainfrom
TheBlueMatt:2025-07-3860-plus-plus
Jul 15, 2025
Merged

Locktimed packages fixes#3923
TheBlueMatt merged 5 commits into
lightningdevkit:mainfrom
TheBlueMatt:2025-07-3860-plus-plus

Conversation

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

See individual commits for more. This is #3860 with an additional issue I found while writing a test for it fixed.

TheBlueMattand others added 2 commits July 10, 2025 17:56
In a number of tests we require available UTXOs to do HTLC anchor
claims by bringing our own fees. We previously wrote that out in
each test, which is somewhat verbose, so here we simply add a test
utility that gives each node a full BTC in a single UTXO.
We have to prune locktimed packages when their inputs are spent,
otherwise the notification of the watched outputs might be missed. This
can lead to locktimed packages with spent inputs being added back to
the pending claim requests in the future, and they are never cleaned
up until node restart.
Resolves: lightningdevkit#3859
@TheBlueMattTheBlueMatt added this to the 0.2 milestone Jul 10, 2025
@ldk-reviews-bot

ldk-reviews-bot commented Jul 10, 2025

Copy link
Copy Markdown

👋 Thanks for assigning @tankyleo as a reviewer!
I'll wait for their review and will help manage the review process.
Once they submit their review, I'll check if a second reviewer would be helpful.

Comment threadlightning/src/ln/reorg_tests.rs Outdated
When we have an outpoint to claim which is lock-timed and the
locktime is reached, we add it to
`OnchainTxHandler::claimable_outpoints` to indicate the outpoint is
now being claimed. However, `claimable_outpoints` is supposed to
track when the outpoint first appeared on chain so that we can
remove the claim if the outpoint is reorged out.
Sadly, in the handling for lock-timed packages, we incorrectly
stored the current height in `claimable_outpoints`, causing such
claims to be removed in case of a reorg right after they were
generated, even if the output we intend to claim isn't removed at
all.
Here we start tracking when the outpoint we're spending was created
in `PackageSolvingData`'s constituent types. While we could have
tracked this information in `PackageTemplate`, it would preclude
later merging packages that are spending outpoints included in
different blocks, which we don't necessarily want to do.
When we have an outpoint to claim which is lock-timed and the
locktime is reached, we add it to
`OnchainTxHandler::claimable_outpoints` to indicate the outpoint is
now being claimed. However, `claimable_outpoints` is supposed to
track when the outpoint first appeared on chain so that we can
remove the claim if the outpoint is reorged out.
Sadly, in the handling for lock-timed packages, we incorrectly
stored the current height in `claimable_outpoints`, causing such
claims to be removed in case of a reorg right after they were
generated, even if the output we intend to claim isn't removed at
all.
Here we use the creation-height tracking added in the previous
commit to actually address the issue, using the tracked height when
adding a claim to `OnchainTxHandler::claimable_outpoints`.
In cases where we have no information, we continue to use the
current height, retaining the issue for locktimed packages on
upgrades, but this simplifies cases where we actually don't have
the information available anyway.
@TheBlueMatt
TheBlueMattforce-pushed the 2025-07-3860-plus-plus branch from ff95a56 to 21be9c5CompareJuly 10, 2025 22:19
Comment threadlightning/src/chain/onchaintx.rs
for (k, outpoint_confirmation_height) in req.outpoints_and_creation_heights() {
let creation_height = outpoint_confirmation_height.unwrap_or(conf_height);
log_info!(logger, "Registering claiming request for {}:{}, which exists as of height {creation_height}", k.txid, k.vout);
self.claimable_outpoints.insert(k.clone(), (claim_id, creation_height));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hm, shouldn't we prefer signed_locktime over creation_height for outpoints that have one though? While the outpoint hasn't been reorged out, claiming it is no longer possible once the block at signed_locktime is disconnected.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

In practice I think this would just result in us broadcasting things that cannot enter the mempool until we get back to the expected height. If we have any other claims that were merged into the same package for whatever reason, and they are still valid at the disconnected block height, then this would be a greater issue.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

The problem is that when things time out via claimable_outpoints, we don't stop claiming them, we remove them. We don't get them back after a reorg at that point. We could move to pushing things into the locked-packages vec after a block-disconnect, but that seems like a bigger change?

Comment threadlightning/src/ln/reorg_tests.rs Outdated
Comment threadlightning/src/ln/reorg_tests.rs Outdated
@ldk-reviews-bot

Copy link
Copy Markdown

👋 The first review has been submitted!

Do you think this PR is ready for a second reviewer? If so, click here to assign a second reviewer.

@tankyleo
tankyleo self-requested a review July 11, 2025 16:55
@ldk-reviews-bot

Copy link
Copy Markdown

🔔 1st Reminder

Hey @tankyleo! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

This adds a single test which exercises both the ability to prune
locktimed packages when inputs are spent as well as the
creation-height tracking for locktimed packages.
@TheBlueMatt
TheBlueMattforce-pushed the 2025-07-3860-plus-plus branch from 21be9c5 to d7726efCompareJuly 14, 2025 12:53
@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

Cleaned up and improved the test slightly:

$ git diff-tree -U2 21be9c5ed d7726eff0
diff --git a/lightning/src/ln/reorg_tests.rs b/lightning/src/ln/reorg_tests.rs
index 6faabac4c0..c5f512b319 100644
--- a/lightning/src/ln/reorg_tests.rs+++ b/lightning/src/ln/reorg_tests.rs@@ -13,5 +13,5 @@
use crate::chain::chaininterface::LowerBoundedFeeEstimator;
-use crate::chain::channelmonitor::{ANTI_REORG_DELAY, LATENCY_GRACE_PERIOD_BLOCKS};+use crate::chain::channelmonitor::{ANTI_REORG_DELAY, Balance, LATENCY_GRACE_PERIOD_BLOCKS};
use crate::chain::transaction::OutPoint;
use crate::chain::Confirm;
@@ -1058,12 +1058,19 @@ fn do_test_split_htlc_expiry_tracking(use_third_htlc: bool, reorg_out: bool) {
if reorg_out {
- // Reorg out bs_htlc_spend_tx, letting node A the claim all the HTLCs instead.+ // Reorg out bs_htlc_spend_tx, letting node A claim all the HTLCs instead.
disconnect_blocks(&nodes[0], ANTI_REORG_DELAY - 2);
assert_eq!(nodes[0].tx_broadcaster.txn_broadcast().len(), 0);
- // As soon as bs_htlc_spend_tx is disconnected+ // As soon as bs_htlc_spend_tx is disconnected, node A should consider all HTLCs+ // claimable-on-timeout.
disconnect_blocks(&nodes[0], 1);
let balances = nodes[0].chain_monitor.chain_monitor.get_claimable_balances(&[]);
assert_eq!(balances.len(), if use_third_htlc { 3 } else { 2 });
+ for balance in balances {+ if let Balance::MaybeTimeoutClaimableHTLC { .. } = balance {+ } else {+ panic!("Unexpected balance {balance:?}");+ }+ }
connect_blocks(&nodes[0], 100);

@TheBlueMatt
TheBlueMatt merged commit 7e4598d into lightningdevkit:mainJul 15, 2025
@TheBlueMattTheBlueMatt mentioned this pull request Jul 15, 2025
@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

Backported in #3932

@TheBlueMattTheBlueMatt self-assigned this Jul 17, 2025
TheBlueMatt added a commit to TheBlueMatt/rust-lightning that referenced this pull request Jul 24, 2025
v0.1.5 - Jul 16, 2025 - "Async Path Reduction"
Performance Improvements
========================
* `NetworkGraph`'s expensive internal consistency checks have now been
disabled in debug builds in addition to release builds (lightningdevkit#3687).
Bug Fixes
=========
* Pathfinding which results in a multi-path payment is now substantially
smarter, using fewer paths and better optimizing fees and successes (lightningdevkit#3890).
* A counterparty delaying claiming multiple HTLCs with different expiries can
no longer cause our `ChannelMonitor` to continuously rebroadcast invalid
transactions or RBF bump attempts (lightningdevkit#3923).
* Reorgs can no longer cause us to fail to claim HTLCs after a counterparty
delayed claiming multiple HTLCs with different expiries (lightningdevkit#3923).
* Force-closing a channel while it is blocked on another channel's async
`ChannelMonitorUpdate` can no longer lead to a panic (lightningdevkit#3858).
* `ChannelMonitorUpdate`s can no longer be released to storage too early when
doing async updates or on restart. This only impacts async
`ChannelMonitorUpdate` persistence and can lead to loss of funds only in rare
cases with `ChannelMonitorUpdate` persistence order inversions (lightningdevkit#3907).
Security
========
0.1.5 fixes a vulnerability which could allow a peer to overdraw their reserve
value, potentially cutting into commitment transaction fees on channels with a
low reserve.
* Due to a bug in checking whether an HTLC is dust during acceptance, near-dust
HTLCs were not counted towards the commitment transaction fee, but did
eventually contribute to it when we built a commitment transaction. This can
be used by a counterparty to overdraw their reserve value, or, for channels
with a low reserve value, cut into the commitment transaction fee (lightningdevkit#3933).
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

5 participants

@TheBlueMatt@ldk-reviews-bot@wpaulino@tankyleo@whfuyn
, '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

Locktimed packages fixes - #3923

Merged
TheBlueMatt merged 5 commits into
lightningdevkit:mainfrom
TheBlueMatt:2025-07-3860-plus-plus
Jul 15, 2025
Merged

Locktimed packages fixes#3923
TheBlueMatt merged 5 commits into
lightningdevkit:mainfrom
TheBlueMatt:2025-07-3860-plus-plus

Conversation

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

See individual commits for more. This is #3860 with an additional issue I found while writing a test for it fixed.

TheBlueMattand others added 2 commits July 10, 2025 17:56
In a number of tests we require available UTXOs to do HTLC anchor
claims by bringing our own fees. We previously wrote that out in
each test, which is somewhat verbose, so here we simply add a test
utility that gives each node a full BTC in a single UTXO.
We have to prune locktimed packages when their inputs are spent,
otherwise the notification of the watched outputs might be missed. This
can lead to locktimed packages with spent inputs being added back to
the pending claim requests in the future, and they are never cleaned
up until node restart.
Resolves: lightningdevkit#3859
@TheBlueMattTheBlueMatt added this to the 0.2 milestone Jul 10, 2025
@ldk-reviews-bot

ldk-reviews-bot commented Jul 10, 2025

Copy link
Copy Markdown

👋 Thanks for assigning @tankyleo as a reviewer!
I'll wait for their review and will help manage the review process.
Once they submit their review, I'll check if a second reviewer would be helpful.

Comment threadlightning/src/ln/reorg_tests.rs Outdated
When we have an outpoint to claim which is lock-timed and the
locktime is reached, we add it to
`OnchainTxHandler::claimable_outpoints` to indicate the outpoint is
now being claimed. However, `claimable_outpoints` is supposed to
track when the outpoint first appeared on chain so that we can
remove the claim if the outpoint is reorged out.
Sadly, in the handling for lock-timed packages, we incorrectly
stored the current height in `claimable_outpoints`, causing such
claims to be removed in case of a reorg right after they were
generated, even if the output we intend to claim isn't removed at
all.
Here we start tracking when the outpoint we're spending was created
in `PackageSolvingData`'s constituent types. While we could have
tracked this information in `PackageTemplate`, it would preclude
later merging packages that are spending outpoints included in
different blocks, which we don't necessarily want to do.
When we have an outpoint to claim which is lock-timed and the
locktime is reached, we add it to
`OnchainTxHandler::claimable_outpoints` to indicate the outpoint is
now being claimed. However, `claimable_outpoints` is supposed to
track when the outpoint first appeared on chain so that we can
remove the claim if the outpoint is reorged out.
Sadly, in the handling for lock-timed packages, we incorrectly
stored the current height in `claimable_outpoints`, causing such
claims to be removed in case of a reorg right after they were
generated, even if the output we intend to claim isn't removed at
all.
Here we use the creation-height tracking added in the previous
commit to actually address the issue, using the tracked height when
adding a claim to `OnchainTxHandler::claimable_outpoints`.
In cases where we have no information, we continue to use the
current height, retaining the issue for locktimed packages on
upgrades, but this simplifies cases where we actually don't have
the information available anyway.
@TheBlueMatt
TheBlueMattforce-pushed the 2025-07-3860-plus-plus branch from ff95a56 to 21be9c5CompareJuly 10, 2025 22:19
Comment threadlightning/src/chain/onchaintx.rs
for (k, outpoint_confirmation_height) in req.outpoints_and_creation_heights() {
let creation_height = outpoint_confirmation_height.unwrap_or(conf_height);
log_info!(logger, "Registering claiming request for {}:{}, which exists as of height {creation_height}", k.txid, k.vout);
self.claimable_outpoints.insert(k.clone(), (claim_id, creation_height));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hm, shouldn't we prefer signed_locktime over creation_height for outpoints that have one though? While the outpoint hasn't been reorged out, claiming it is no longer possible once the block at signed_locktime is disconnected.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

In practice I think this would just result in us broadcasting things that cannot enter the mempool until we get back to the expected height. If we have any other claims that were merged into the same package for whatever reason, and they are still valid at the disconnected block height, then this would be a greater issue.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

The problem is that when things time out via claimable_outpoints, we don't stop claiming them, we remove them. We don't get them back after a reorg at that point. We could move to pushing things into the locked-packages vec after a block-disconnect, but that seems like a bigger change?

Comment threadlightning/src/ln/reorg_tests.rs Outdated
Comment threadlightning/src/ln/reorg_tests.rs Outdated
@ldk-reviews-bot

Copy link
Copy Markdown

👋 The first review has been submitted!

Do you think this PR is ready for a second reviewer? If so, click here to assign a second reviewer.

@tankyleo
tankyleo self-requested a review July 11, 2025 16:55
@ldk-reviews-bot

Copy link
Copy Markdown

🔔 1st Reminder

Hey @tankyleo! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

This adds a single test which exercises both the ability to prune
locktimed packages when inputs are spent as well as the
creation-height tracking for locktimed packages.
@TheBlueMatt
TheBlueMattforce-pushed the 2025-07-3860-plus-plus branch from 21be9c5 to d7726efCompareJuly 14, 2025 12:53
@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

Cleaned up and improved the test slightly:

$ git diff-tree -U2 21be9c5ed d7726eff0
diff --git a/lightning/src/ln/reorg_tests.rs b/lightning/src/ln/reorg_tests.rs
index 6faabac4c0..c5f512b319 100644
--- a/lightning/src/ln/reorg_tests.rs+++ b/lightning/src/ln/reorg_tests.rs@@ -13,5 +13,5 @@
use crate::chain::chaininterface::LowerBoundedFeeEstimator;
-use crate::chain::channelmonitor::{ANTI_REORG_DELAY, LATENCY_GRACE_PERIOD_BLOCKS};+use crate::chain::channelmonitor::{ANTI_REORG_DELAY, Balance, LATENCY_GRACE_PERIOD_BLOCKS};
use crate::chain::transaction::OutPoint;
use crate::chain::Confirm;
@@ -1058,12 +1058,19 @@ fn do_test_split_htlc_expiry_tracking(use_third_htlc: bool, reorg_out: bool) {
if reorg_out {
- // Reorg out bs_htlc_spend_tx, letting node A the claim all the HTLCs instead.+ // Reorg out bs_htlc_spend_tx, letting node A claim all the HTLCs instead.
disconnect_blocks(&nodes[0], ANTI_REORG_DELAY - 2);
assert_eq!(nodes[0].tx_broadcaster.txn_broadcast().len(), 0);
- // As soon as bs_htlc_spend_tx is disconnected+ // As soon as bs_htlc_spend_tx is disconnected, node A should consider all HTLCs+ // claimable-on-timeout.
disconnect_blocks(&nodes[0], 1);
let balances = nodes[0].chain_monitor.chain_monitor.get_claimable_balances(&[]);
assert_eq!(balances.len(), if use_third_htlc { 3 } else { 2 });
+ for balance in balances {+ if let Balance::MaybeTimeoutClaimableHTLC { .. } = balance {+ } else {+ panic!("Unexpected balance {balance:?}");+ }+ }
connect_blocks(&nodes[0], 100);

@TheBlueMatt
TheBlueMatt merged commit 7e4598d into lightningdevkit:mainJul 15, 2025
@TheBlueMattTheBlueMatt mentioned this pull request Jul 15, 2025
@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

Backported in #3932

@TheBlueMattTheBlueMatt self-assigned this Jul 17, 2025
TheBlueMatt added a commit to TheBlueMatt/rust-lightning that referenced this pull request Jul 24, 2025
v0.1.5 - Jul 16, 2025 - "Async Path Reduction"
Performance Improvements
========================
* `NetworkGraph`'s expensive internal consistency checks have now been
disabled in debug builds in addition to release builds (lightningdevkit#3687).
Bug Fixes
=========
* Pathfinding which results in a multi-path payment is now substantially
smarter, using fewer paths and better optimizing fees and successes (lightningdevkit#3890).
* A counterparty delaying claiming multiple HTLCs with different expiries can
no longer cause our `ChannelMonitor` to continuously rebroadcast invalid
transactions or RBF bump attempts (lightningdevkit#3923).
* Reorgs can no longer cause us to fail to claim HTLCs after a counterparty
delayed claiming multiple HTLCs with different expiries (lightningdevkit#3923).
* Force-closing a channel while it is blocked on another channel's async
`ChannelMonitorUpdate` can no longer lead to a panic (lightningdevkit#3858).
* `ChannelMonitorUpdate`s can no longer be released to storage too early when
doing async updates or on restart. This only impacts async
`ChannelMonitorUpdate` persistence and can lead to loss of funds only in rare
cases with `ChannelMonitorUpdate` persistence order inversions (lightningdevkit#3907).
Security
========
0.1.5 fixes a vulnerability which could allow a peer to overdraw their reserve
value, potentially cutting into commitment transaction fees on channels with a
low reserve.
* Due to a bug in checking whether an HTLC is dust during acceptance, near-dust
HTLCs were not counted towards the commitment transaction fee, but did
eventually contribute to it when we built a commitment transaction. This can
be used by a counterparty to overdraw their reserve value, or, for channels
with a low reserve value, cut into the commitment transaction fee (lightningdevkit#3933).
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

5 participants

@TheBlueMatt@ldk-reviews-bot@wpaulino@tankyleo@whfuyn