Skip to content

Commit e48dc0e

Browse files
authored
Merge pull request #4155 from TheBlueMatt/2025-09-0.1.6
Release 0.1.6
2 parents 1e50365 + c5d06a6 commit e48dc0e

File tree

6 files changed

+257
-60
lines changed

6 files changed

+257
-60
lines changed

CHANGELOG.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,41 @@
1+
# 0.1.6 - Oct 10, 2025 - "Async Preimage Claims"
2+
3+
## Performance Improvements
4+
* `NetworkGraph::remove_stale_channels_and_tracking` has been sped up by more
5+
than 20x in cases where many entries need to be removed (such as after
6+
initial gossip sync, #4080).
7+
8+
## Bug Fixes
9+
* Delivery of on-chain resolutions of HTLCs to `ChannelManager` has been made
10+
more robust to prevent loss in some exceedingly rare crash cases. This may
11+
marginally increase payment resolution event replays on startup (#3984).
12+
* Corrected forwarding of new gossip to peers which we are sending an initial
13+
gossip sync to (#4107).
14+
* A rare race condition may have resulted in outbound BOLT12 payments
15+
spuriously failing while processing the `Bolt12Invoice` message (#4078).
16+
* If a channel is updated multiple times after a payment is claimed while using
17+
async persistence of the `ChannelMonitorUpdate`s, and the node then restarts
18+
with a stale copy of its `ChannelManager`, the `PaymentClaimed` may have been
19+
lost (#3988).
20+
* If an async-persisted `ChannelMonitorUpdate` for one part of an MPP claim
21+
does not complete before multiple `ChannelMonitorUpdate`s for another channel
22+
in the same MPP claim complete, and the node restarts twice, the preimage may
23+
be lost and the MPP payment part may not be claimed (#3928).
24+
25+
## Security
26+
0.1.6 fixes a denial of service vulnerability and a funds-theft vulnerability.
27+
* When a channel has been force-closed, we have already claimed some of its
28+
HTLCs on-chain, and we later learn a new preimage allowing us to claim
29+
further HTLCs on-chain, we could in some cases generate invalid claim
30+
transactions leading to loss of funds (#4154).
31+
* When a `ChannelMonitor` is created for a channel which is never funded with
32+
a real transaction, `ChannelMonitor::get_claimable_balances` would never be
33+
empty. As a result, `ChannelMonitor::check_and_update_full_resolution_status`
34+
would never indicate the monitor is prunable, and thus
35+
`ChainMonitor::archive_fully_resolved_channel_monitors` would never remove
36+
it. This allows a peer which opens channels without funding them to bloat our
37+
memory and disk space, eventually leading to denial-of-service (#4081).
38+
139
# 0.1.5 - Jul 16, 2025 - "Async Path Reduction"
240

341
## Performance Improvements

lightning/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "lightning"
3-
version = "0.1.5"
3+
version = "0.1.6"
44
authors = ["Matt Corallo"]
55
license = "MIT OR Apache-2.0"
66
repository = "https://github.com/lightningdevkit/rust-lightning/"

lightning/src/chain/channelmonitor.rs

Lines changed: 136 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -3118,7 +3118,7 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
31183118
// First check if a counterparty commitment transaction has been broadcasted:
31193119
macro_rules! claim_htlcs {
31203120
($commitment_number: expr, $txid: expr, $htlcs: expr) => {
3121-
let (htlc_claim_reqs, _) = self.get_counterparty_output_claim_info($commitment_number, $txid, None, $htlcs, confirmed_spend_height);
3121+
let htlc_claim_reqs = self.get_counterparty_output_claims_for_preimage(*payment_preimage, $commitment_number, $txid, $htlcs, confirmed_spend_height);
31223122
let conf_target = self.closure_conf_target();
31233123
self.onchain_tx_handler.update_claims_view_from_requests(htlc_claim_reqs, self.best_block.height, self.best_block.height, broadcaster, conf_target, fee_estimator, logger);
31243124
}
@@ -3728,7 +3728,7 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
37283728
(htlc, htlc_source.as_ref().map(|htlc_source| htlc_source.as_ref()))
37293729
), logger);
37303730
let (htlc_claim_reqs, counterparty_output_info) =
3731-
self.get_counterparty_output_claim_info(commitment_number, commitment_txid, Some(tx), per_commitment_option, Some(height));
3731+
self.get_counterparty_output_claim_info(commitment_number, commitment_txid, tx, per_commitment_claimable_data, Some(height));
37323732
to_counterparty_output_info = counterparty_output_info;
37333733
for req in htlc_claim_reqs {
37343734
claimable_outpoints.push(req);
@@ -3738,78 +3738,162 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
37383738
(claimable_outpoints, to_counterparty_output_info)
37393739
}
37403740

3741-
/// Returns the HTLC claim package templates and the counterparty output info
3742-
fn get_counterparty_output_claim_info(&self, commitment_number: u64, commitment_txid: Txid, tx: Option<&Transaction>, per_commitment_option: Option<&Vec<(HTLCOutputInCommitment, Option<Box<HTLCSource>>)>>, confirmation_height: Option<u32>)
3743-
-> (Vec<PackageTemplate>, CommitmentTxCounterpartyOutputInfo) {
3744-
let mut claimable_outpoints = Vec::new();
3745-
let mut to_counterparty_output_info: CommitmentTxCounterpartyOutputInfo = None;
3741+
fn get_point_for_commitment_number(&self, commitment_number: u64) -> Option<PublicKey> {
3742+
let per_commitment_points = &self.their_cur_per_commitment_points?;
3743+
3744+
// If the counterparty commitment tx is the latest valid state, use their latest
3745+
// per-commitment point
3746+
if per_commitment_points.0 == commitment_number {
3747+
Some(per_commitment_points.1)
3748+
} else if let Some(point) = per_commitment_points.2.as_ref() {
3749+
// If counterparty commitment tx is the state previous to the latest valid state, use
3750+
// their previous per-commitment point (non-atomicity of revocation means it's valid for
3751+
// them to temporarily have two valid commitment txns from our viewpoint)
3752+
if per_commitment_points.0 == commitment_number + 1 {
3753+
Some(*point)
3754+
} else {
3755+
None
3756+
}
3757+
} else {
3758+
None
3759+
}
3760+
}
37463761

3762+
fn get_counterparty_output_claims_for_preimage(
3763+
&self, preimage: PaymentPreimage, commitment_number: u64,
3764+
commitment_txid: Txid,
3765+
per_commitment_option: Option<&Vec<(HTLCOutputInCommitment, Option<Box<HTLCSource>>)>>,
3766+
confirmation_height: Option<u32>,
3767+
) -> Vec<PackageTemplate> {
37473768
let per_commitment_claimable_data = match per_commitment_option {
37483769
Some(outputs) => outputs,
3749-
None => return (claimable_outpoints, to_counterparty_output_info),
3770+
None => return Vec::new(),
37503771
};
3751-
let per_commitment_points = match self.their_cur_per_commitment_points {
3752-
Some(points) => points,
3753-
None => return (claimable_outpoints, to_counterparty_output_info),
3772+
let per_commitment_point = match self.get_point_for_commitment_number(commitment_number) {
3773+
Some(point) => point,
3774+
None => return Vec::new(),
37543775
};
37553776

3756-
let per_commitment_point =
3757-
// If the counterparty commitment tx is the latest valid state, use their latest
3758-
// per-commitment point
3759-
if per_commitment_points.0 == commitment_number { &per_commitment_points.1 }
3760-
else if let Some(point) = per_commitment_points.2.as_ref() {
3761-
// If counterparty commitment tx is the state previous to the latest valid state, use
3762-
// their previous per-commitment point (non-atomicity of revocation means it's valid for
3763-
// them to temporarily have two valid commitment txns from our viewpoint)
3764-
if per_commitment_points.0 == commitment_number + 1 {
3765-
point
3766-
} else { return (claimable_outpoints, to_counterparty_output_info); }
3767-
} else { return (claimable_outpoints, to_counterparty_output_info); };
3768-
3769-
if let Some(transaction) = tx {
3770-
let revocation_pubkey = RevocationKey::from_basepoint(
3771-
&self.onchain_tx_handler.secp_ctx, &self.holder_revocation_basepoint, &per_commitment_point);
3772-
3773-
let delayed_key = DelayedPaymentKey::from_basepoint(&self.onchain_tx_handler.secp_ctx, &self.counterparty_commitment_params.counterparty_delayed_payment_base_key, &per_commitment_point);
3774-
3775-
let revokeable_p2wsh = chan_utils::get_revokeable_redeemscript(&revocation_pubkey,
3776-
self.counterparty_commitment_params.on_counterparty_tx_csv,
3777-
&delayed_key).to_p2wsh();
3778-
for (idx, outp) in transaction.output.iter().enumerate() {
3779-
if outp.script_pubkey == revokeable_p2wsh {
3780-
to_counterparty_output_info =
3781-
Some((idx.try_into().expect("Can't have > 2^32 outputs"), outp.value));
3777+
let matching_payment_hash = PaymentHash::from(preimage);
3778+
per_commitment_claimable_data
3779+
.iter()
3780+
.filter_map(|(htlc, _)| {
3781+
if let Some(transaction_output_index) = htlc.transaction_output_index {
3782+
if htlc.offered && htlc.payment_hash == matching_payment_hash {
3783+
let htlc_data = PackageSolvingData::CounterpartyOfferedHTLCOutput(
3784+
CounterpartyOfferedHTLCOutput::build(
3785+
per_commitment_point,
3786+
self.counterparty_commitment_params.counterparty_delayed_payment_base_key,
3787+
self.counterparty_commitment_params.counterparty_htlc_base_key,
3788+
preimage,
3789+
htlc.clone(),
3790+
self.onchain_tx_handler.channel_type_features().clone(),
3791+
confirmation_height,
3792+
),
3793+
);
3794+
Some(PackageTemplate::build_package(
3795+
commitment_txid,
3796+
transaction_output_index,
3797+
htlc_data,
3798+
htlc.cltv_expiry,
3799+
))
3800+
} else {
3801+
None
3802+
}
3803+
} else {
3804+
None
37823805
}
3806+
})
3807+
.collect()
3808+
}
3809+
3810+
/// Returns the HTLC claim package templates and the counterparty output info
3811+
fn get_counterparty_output_claim_info(
3812+
&self, commitment_number: u64, commitment_txid: Txid,
3813+
tx: &Transaction,
3814+
per_commitment_claimable_data: &[(HTLCOutputInCommitment, Option<Box<HTLCSource>>)],
3815+
confirmation_height: Option<u32>,
3816+
) -> (Vec<PackageTemplate>, CommitmentTxCounterpartyOutputInfo) {
3817+
let mut claimable_outpoints = Vec::new();
3818+
let mut to_counterparty_output_info: CommitmentTxCounterpartyOutputInfo = None;
3819+
3820+
let per_commitment_point = match self.get_point_for_commitment_number(commitment_number) {
3821+
Some(point) => point,
3822+
None => return (claimable_outpoints, to_counterparty_output_info),
3823+
};
3824+
3825+
let revocation_pubkey = RevocationKey::from_basepoint(
3826+
&self.onchain_tx_handler.secp_ctx,
3827+
&self.holder_revocation_basepoint,
3828+
&per_commitment_point,
3829+
);
3830+
let delayed_key = DelayedPaymentKey::from_basepoint(
3831+
&self.onchain_tx_handler.secp_ctx,
3832+
&self.counterparty_commitment_params.counterparty_delayed_payment_base_key,
3833+
&per_commitment_point,
3834+
);
3835+
let revokeable_p2wsh = chan_utils::get_revokeable_redeemscript(
3836+
&revocation_pubkey,
3837+
self.counterparty_commitment_params.on_counterparty_tx_csv,
3838+
&delayed_key,
3839+
)
3840+
.to_p2wsh();
3841+
for (idx, outp) in tx.output.iter().enumerate() {
3842+
if outp.script_pubkey == revokeable_p2wsh {
3843+
to_counterparty_output_info =
3844+
Some((idx.try_into().expect("Can't have > 2^32 outputs"), outp.value));
37833845
}
37843846
}
37853847

3786-
for &(ref htlc, _) in per_commitment_claimable_data.iter() {
3848+
for &(ref htlc, _) in per_commitment_claimable_data.iter() {
37873849
if let Some(transaction_output_index) = htlc.transaction_output_index {
3788-
if let Some(transaction) = tx {
3789-
if transaction_output_index as usize >= transaction.output.len() ||
3790-
transaction.output[transaction_output_index as usize].value != htlc.to_bitcoin_amount() {
3791-
// per_commitment_data is corrupt or our commitment signing key leaked!
3792-
return (claimable_outpoints, to_counterparty_output_info);
3793-
}
3850+
if transaction_output_index as usize >= tx.output.len()
3851+
|| tx.output[transaction_output_index as usize].value
3852+
!= htlc.to_bitcoin_amount()
3853+
{
3854+
// per_commitment_data is corrupt or our commitment signing key leaked!
3855+
return (claimable_outpoints, to_counterparty_output_info);
37943856
}
3795-
let preimage = if htlc.offered { if let Some((p, _)) = self.payment_preimages.get(&htlc.payment_hash) { Some(*p) } else { None } } else { None };
3857+
let preimage = if htlc.offered {
3858+
if let Some((p, _)) = self.payment_preimages.get(&htlc.payment_hash) {
3859+
Some(*p)
3860+
} else {
3861+
None
3862+
}
3863+
} else {
3864+
None
3865+
};
37963866
if preimage.is_some() || !htlc.offered {
37973867
let counterparty_htlc_outp = if htlc.offered {
37983868
PackageSolvingData::CounterpartyOfferedHTLCOutput(
3799-
CounterpartyOfferedHTLCOutput::build(*per_commitment_point,
3869+
CounterpartyOfferedHTLCOutput::build(
3870+
per_commitment_point,
38003871
self.counterparty_commitment_params.counterparty_delayed_payment_base_key,
38013872
self.counterparty_commitment_params.counterparty_htlc_base_key,
3802-
preimage.unwrap(), htlc.clone(), self.onchain_tx_handler.channel_type_features().clone(),
3803-
confirmation_height))
3873+
preimage.unwrap(),
3874+
htlc.clone(),
3875+
self.onchain_tx_handler.channel_type_features().clone(),
3876+
confirmation_height,
3877+
),
3878+
)
38043879
} else {
38053880
PackageSolvingData::CounterpartyReceivedHTLCOutput(
3806-
CounterpartyReceivedHTLCOutput::build(*per_commitment_point,
3881+
CounterpartyReceivedHTLCOutput::build(
3882+
per_commitment_point,
38073883
self.counterparty_commitment_params.counterparty_delayed_payment_base_key,
38083884
self.counterparty_commitment_params.counterparty_htlc_base_key,
3809-
htlc.clone(), self.onchain_tx_handler.channel_type_features().clone(),
3810-
confirmation_height))
3885+
htlc.clone(),
3886+
self.onchain_tx_handler.channel_type_features().clone(),
3887+
confirmation_height,
3888+
),
3889+
)
38113890
};
3812-
let counterparty_package = PackageTemplate::build_package(commitment_txid, transaction_output_index, counterparty_htlc_outp, htlc.cltv_expiry);
3891+
let counterparty_package = PackageTemplate::build_package(
3892+
commitment_txid,
3893+
transaction_output_index,
3894+
counterparty_htlc_outp,
3895+
htlc.cltv_expiry,
3896+
);
38133897
claimable_outpoints.push(counterparty_package);
38143898
}
38153899
}

lightning/src/events/mod.rs

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -229,11 +229,6 @@ impl_writeable_tlv_based_enum_legacy!(PaymentPurpose,
229229
/// Information about an HTLC that is part of a payment that can be claimed.
230230
#[derive(Clone, Debug, PartialEq, Eq)]
231231
pub struct ClaimedHTLC {
232-
/// The counterparty of the channel.
233-
///
234-
/// This value will always be `None` for objects serialized with LDK versions prior to 0.2 and
235-
/// `Some` otherwise.
236-
pub counterparty_node_id: Option<PublicKey>,
237232
/// The `channel_id` of the channel over which the HTLC was received.
238233
pub channel_id: ChannelId,
239234
/// The `user_channel_id` of the channel over which the HTLC was received. This is the value
@@ -264,7 +259,6 @@ impl_writeable_tlv_based!(ClaimedHTLC, {
264259
(0, channel_id, required),
265260
(1, counterparty_skimmed_fee_msat, (default_value, 0u64)),
266261
(2, user_channel_id, required),
267-
(3, counterparty_node_id, option),
268262
(4, cltv_expiry, required),
269263
(6, value_msat, required),
270264
});

lightning/src/ln/channelmanager.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -435,7 +435,6 @@ struct ClaimableHTLC {
435435
impl From<&ClaimableHTLC> for events::ClaimedHTLC {
436436
fn from(val: &ClaimableHTLC) -> Self {
437437
events::ClaimedHTLC {
438-
counterparty_node_id: val.prev_hop.counterparty_node_id,
439438
channel_id: val.prev_hop.channel_id,
440439
user_channel_id: val.prev_hop.user_channel_id.unwrap_or(0),
441440
cltv_expiry: val.cltv_expiry,

lightning/src/ln/monitor_tests.rs

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3693,3 +3693,85 @@ fn test_lost_timeout_monitor_events() {
36933693
do_test_lost_timeout_monitor_events(CommitmentType::LocalWithLastHTLC, false);
36943694
do_test_lost_timeout_monitor_events(CommitmentType::LocalWithLastHTLC, true);
36953695
}
3696+
3697+
#[test]
3698+
fn test_ladder_preimage_htlc_claims() {
3699+
// Tests that when we learn of a preimage via a monitor update we only claim HTLCs with the
3700+
// corresponding payment hash. This test is a reproduction of a scenario that happened in
3701+
// production where the second HTLC claim also included the first HTLC (even though it was
3702+
// already claimed) resulting in an invalid claim transaction.
3703+
let chanmon_cfgs = create_chanmon_cfgs(2);
3704+
let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
3705+
let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]);
3706+
let mut nodes = create_network(2, &node_cfgs, &node_chanmgrs);
3707+
3708+
let node_id_0 = nodes[0].node.get_our_node_id();
3709+
let node_id_1 = nodes[1].node.get_our_node_id();
3710+
3711+
let (_, _, channel_id, _) = create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0);
3712+
3713+
let (payment_preimage1, payment_hash1, _, _) = route_payment(&nodes[0], &[&nodes[1]], 1_000_000);
3714+
let (payment_preimage2, payment_hash2, _, _) = route_payment(&nodes[0], &[&nodes[1]], 1_000_000);
3715+
3716+
nodes[0].node.force_close_broadcasting_latest_txn(&channel_id, &node_id_1, "test".to_string()).unwrap();
3717+
check_added_monitors(&nodes[0], 1);
3718+
check_closed_broadcast(&nodes[0], 1, true);
3719+
let reason = ClosureReason::HolderForceClosed { broadcasted_latest_txn: Some(true) };
3720+
check_closed_event(&nodes[0], 1, reason, false, &[node_id_1], 1_000_000);
3721+
3722+
let commitment_tx = {
3723+
let mut txn = nodes[0].tx_broadcaster.txn_broadcast();
3724+
assert_eq!(txn.len(), 1);
3725+
txn.remove(0)
3726+
};
3727+
mine_transaction(&nodes[0], &commitment_tx);
3728+
mine_transaction(&nodes[1], &commitment_tx);
3729+
3730+
check_closed_broadcast(&nodes[1], 1, true);
3731+
check_added_monitors(&nodes[1], 1);
3732+
check_closed_event(&nodes[1], 1, ClosureReason::CommitmentTxConfirmed, false, &[node_id_0], 1_000_000);
3733+
3734+
nodes[1].node.claim_funds(payment_preimage1);
3735+
expect_payment_claimed!(&nodes[1], payment_hash1, 1_000_000);
3736+
check_added_monitors(&nodes[1], 1);
3737+
3738+
let (htlc1, htlc_claim_tx1) = {
3739+
let mut txn = nodes[1].tx_broadcaster.txn_broadcast();
3740+
assert_eq!(txn.len(), 1);
3741+
let htlc_claim_tx = txn.remove(0);
3742+
assert_eq!(htlc_claim_tx.input.len(), 1);
3743+
check_spends!(htlc_claim_tx, commitment_tx);
3744+
(htlc_claim_tx.input[0].previous_output, htlc_claim_tx)
3745+
};
3746+
mine_transaction(&nodes[0], &htlc_claim_tx1);
3747+
mine_transaction(&nodes[1], &htlc_claim_tx1);
3748+
3749+
connect_blocks(&nodes[0], ANTI_REORG_DELAY - 1);
3750+
connect_blocks(&nodes[1], ANTI_REORG_DELAY - 1);
3751+
3752+
expect_payment_sent(&nodes[0], payment_preimage1, None, true, false);
3753+
check_added_monitors(&nodes[0], 1);
3754+
3755+
nodes[1].node.claim_funds(payment_preimage2);
3756+
expect_payment_claimed!(&nodes[1], payment_hash2, 1_000_000);
3757+
check_added_monitors(&nodes[1], 1);
3758+
3759+
let (htlc2, htlc_claim_tx2) = {
3760+
let mut txn = nodes[1].tx_broadcaster.txn_broadcast();
3761+
assert_eq!(txn.len(), 1, "{:?}", txn.iter().map(|tx| tx.compute_txid()).collect::<Vec<_>>());
3762+
let htlc_claim_tx = txn.remove(0);
3763+
assert_eq!(htlc_claim_tx.input.len(), 1);
3764+
check_spends!(htlc_claim_tx, commitment_tx);
3765+
(htlc_claim_tx.input[0].previous_output, htlc_claim_tx)
3766+
};
3767+
assert_ne!(htlc1, htlc2);
3768+
3769+
mine_transaction(&nodes[0], &htlc_claim_tx2);
3770+
mine_transaction(&nodes[1], &htlc_claim_tx2);
3771+
3772+
connect_blocks(&nodes[0], ANTI_REORG_DELAY - 1);
3773+
connect_blocks(&nodes[1], ANTI_REORG_DELAY - 1);
3774+
3775+
expect_payment_sent(&nodes[0], payment_preimage2, None, true, false);
3776+
check_added_monitors(&nodes[0], 1);
3777+
}

0 commit comments

Comments
 (0)