Skip to content

Commit 25c9ca3

Browse files
committed
Fail HTLCs from late counterparty commitment updates after funding spend
When a ChannelMonitorUpdate containing a new counterparty commitment is dispatched (e.g. via deferred writes) before a channel force-closes but only applied to the in-memory monitor after the commitment transaction has already confirmed on-chain, the outbound HTLCs in that update must be failed back. Add fail_htlcs_from_update_after_funding_spend to ChannelMonitorImpl which detects this race condition during update_monitor. When a LatestCounterpartyCommitmentTXInfo or LatestCounterpartyCommitment update is applied and the funding output has already been spent, the function iterates all outbound HTLCs from the update and creates OnchainEvent::HTLCUpdate entries for those that need to be failed back. These entries mature after ANTI_REORG_DELAY blocks, giving time for the peer to potentially broadcast the newer commitment. HTLCs that appear as non-dust outputs in the confirmed commitment (whether counterparty or holder) are skipped, as they will be resolved on-chain via the normal HTLC timeout/success path. HTLCs already fulfilled by the counterparty (tracked in counterparty_fulfilled_htlcs) are also skipped. Duplicate failures from previously-known counterparty commitments are handled gracefully by the ChannelManager. AI tools were used in preparing this commit.
1 parent d25dd01 commit 25c9ca3

File tree

3 files changed

+429
-5
lines changed

3 files changed

+429
-5
lines changed

lightning/src/chain/channelmonitor.rs

Lines changed: 176 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4290,6 +4290,55 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
42904290

42914291
self.latest_update_id = updates.update_id;
42924292

4293+
// If a counterparty commitment update was applied while the funding output has already
4294+
// been spent on-chain, fail back the outbound HTLCs from the update. This handles the
4295+
// race where a monitor update is dispatched before the channel force-closes but only
4296+
// applied after the commitment transaction confirms.
4297+
for update in updates.updates.iter() {
4298+
match update {
4299+
ChannelMonitorUpdateStep::LatestCounterpartyCommitmentTXInfo {
4300+
htlc_outputs, ..
4301+
} => {
4302+
// Only outbound HTLCs have a source; inbound ones are `None`
4303+
// and skipped by the `filter_map`.
4304+
self.fail_htlcs_from_update_after_funding_spend(
4305+
htlc_outputs.iter().filter_map(|(htlc, source)| {
4306+
source.as_ref().map(|s| (&**s, htlc.payment_hash, htlc.amount_msat))
4307+
}),
4308+
logger,
4309+
);
4310+
},
4311+
ChannelMonitorUpdateStep::LatestCounterpartyCommitment {
4312+
commitment_txs, htlc_data,
4313+
} => {
4314+
// On a counterparty commitment, `offered=false` means offered by
4315+
// us (outbound). `nondust_htlc_sources` contains sources only for
4316+
// these outbound nondust HTLCs, matching the filter order.
4317+
debug_assert_eq!(
4318+
commitment_txs[0].nondust_htlcs().iter()
4319+
.filter(|htlc| !htlc.offered).count(),
4320+
htlc_data.nondust_htlc_sources.len(),
4321+
);
4322+
let nondust = commitment_txs[0]
4323+
.nondust_htlcs()
4324+
.iter()
4325+
.filter(|htlc| !htlc.offered)
4326+
.zip(htlc_data.nondust_htlc_sources.iter())
4327+
.map(|(htlc, source)| (source, htlc.payment_hash, htlc.amount_msat));
4328+
// Only outbound dust HTLCs have a source; inbound ones are `None`
4329+
// and skipped by the `filter_map`.
4330+
let dust = htlc_data.dust_htlcs.iter().filter_map(|(htlc, source)| {
4331+
source.as_ref().map(|s| (s, htlc.payment_hash, htlc.amount_msat))
4332+
});
4333+
self.fail_htlcs_from_update_after_funding_spend(
4334+
nondust.chain(dust),
4335+
logger,
4336+
);
4337+
},
4338+
_ => {},
4339+
}
4340+
}
4341+
42934342
// Refuse updates after we've detected a spend onchain (or if the channel was otherwise
42944343
// closed), but only if the update isn't the kind of update we expect to see after channel
42954344
// closure.
@@ -4336,6 +4385,117 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
43364385
self.funding_spend_seen || self.lockdown_from_offchain || self.holder_tx_signed
43374386
}
43384387

4388+
/// Given outbound HTLCs from a counterparty commitment update, checks if the funding output
4389+
/// has been spent on-chain. If so, creates `OnchainEvent::HTLCUpdate` entries to fail back
4390+
/// HTLCs that weren't already known to the monitor.
4391+
///
4392+
/// This handles the race where a `ChannelMonitorUpdate` with a new counterparty commitment
4393+
/// is dispatched (e.g., via deferred writes) before the channel force-closes, but only
4394+
/// applied to the in-memory monitor after the commitment transaction has already confirmed.
4395+
///
4396+
/// Only truly new HTLCs (not present in any previously-known commitment) need to be failed
4397+
/// here. HTLCs that were already tracked by the monitor will be handled by the existing
4398+
/// `fail_unbroadcast_htlcs` logic when the spending transaction confirms.
4399+
fn fail_htlcs_from_update_after_funding_spend<'a, L: Logger>(
4400+
&mut self, htlcs: impl Iterator<Item = (&'a HTLCSource, PaymentHash, u64)>,
4401+
logger: &WithContext<L>,
4402+
) {
4403+
let pending_spend_entry = self
4404+
.onchain_events_awaiting_threshold_conf
4405+
.iter()
4406+
.find(|event| matches!(event.event, OnchainEvent::FundingSpendConfirmation { .. }))
4407+
.map(|entry| (entry.txid, entry.transaction.clone(), entry.height, entry.block_hash));
4408+
if self.funding_spend_confirmed.is_none() && pending_spend_entry.is_none() {
4409+
return;
4410+
}
4411+
4412+
// Check HTLC sources against all previously-known commitments to find truly new
4413+
// ones. After the update has been applied, `prev_counterparty_commitment_txid` holds
4414+
// what was `current` before this update, so it represents the already-known
4415+
// counterparty state. HTLCs already present in any of these will be handled by
4416+
// `fail_unbroadcast_htlcs` when the spending transaction confirms.
4417+
let is_source_known = |source: &HTLCSource| {
4418+
if let Some(ref txid) = self.funding.prev_counterparty_commitment_txid {
4419+
if let Some(htlc_list) = self.funding.counterparty_claimable_outpoints.get(txid) {
4420+
if htlc_list.iter().any(|(_, s)| s.as_ref().map(|s| s.as_ref()) == Some(source))
4421+
{
4422+
return true;
4423+
}
4424+
}
4425+
}
4426+
// Note that we don't care about the case where a counterparty sent us a fresh local commitment transaction
4427+
// post-closure (with the `ChannelManager` still operating the channel). First of all we only care about
4428+
// resolving outbound HTLCs, which fundamentally have to be initiated by us. However we also don't mind
4429+
// looking at the current holder commitment transaction's HTLCs as any fresh outbound HTLCs will have to
4430+
// first come in a locally-initiated update to the counterparty's commitment transaction which we can, by
4431+
// refusing to apply the update, prevent the counterparty from ever seeing (as no messages can be sent until
4432+
// the monitor is updated). Thus, the HTLCs we care about can never appear in the holder commitment
4433+
// transaction.
4434+
if holder_commitment_htlcs!(self, CURRENT_WITH_SOURCES).any(|(_, s)| s == Some(source))
4435+
{
4436+
return true;
4437+
}
4438+
if let Some(mut iter) = holder_commitment_htlcs!(self, PREV_WITH_SOURCES) {
4439+
if iter.any(|(_, s)| s == Some(source)) {
4440+
return true;
4441+
}
4442+
}
4443+
false
4444+
};
4445+
for (source, payment_hash, amount_msat) in htlcs {
4446+
if is_source_known(source) {
4447+
continue;
4448+
}
4449+
if self.counterparty_fulfilled_htlcs.get(&SentHTLCId::from_source(source)).is_some() {
4450+
continue;
4451+
}
4452+
let htlc_value_satoshis = Some(amount_msat / 1000);
4453+
let logger = WithContext::from(logger, None, None, Some(payment_hash));
4454+
if let Some(confirmed_txid) = self.funding_spend_confirmed {
4455+
// Funding spend already confirmed past ANTI_REORG_DELAY: resolve immediately.
4456+
log_trace!(
4457+
logger,
4458+
"Failing HTLC from late counterparty commitment update immediately \
4459+
(funding spend already confirmed)"
4460+
);
4461+
self.pending_monitor_events.push(MonitorEvent::HTLCEvent(HTLCUpdate {
4462+
payment_hash,
4463+
payment_preimage: None,
4464+
source: source.clone(),
4465+
htlc_value_satoshis,
4466+
}));
4467+
self.htlcs_resolved_on_chain.push(IrrevocablyResolvedHTLC {
4468+
commitment_tx_output_idx: None,
4469+
resolving_txid: Some(confirmed_txid),
4470+
resolving_tx: None,
4471+
payment_preimage: None,
4472+
});
4473+
} else {
4474+
// Funding spend still awaiting ANTI_REORG_DELAY: queue the failure.
4475+
let (txid, transaction, height, block_hash) = pending_spend_entry.clone().unwrap();
4476+
let entry = OnchainEventEntry {
4477+
txid,
4478+
transaction,
4479+
height,
4480+
block_hash,
4481+
event: OnchainEvent::HTLCUpdate {
4482+
source: source.clone(),
4483+
payment_hash,
4484+
htlc_value_satoshis,
4485+
commitment_tx_output_idx: None,
4486+
},
4487+
};
4488+
log_trace!(
4489+
logger,
4490+
"Failing HTLC from late counterparty commitment update, \
4491+
waiting for confirmation (at height {})",
4492+
entry.confirmation_threshold()
4493+
);
4494+
self.onchain_events_awaiting_threshold_conf.push(entry);
4495+
}
4496+
}
4497+
}
4498+
43394499
fn get_latest_update_id(&self) -> u64 {
43404500
self.latest_update_id
43414501
}
@@ -6760,7 +6920,7 @@ mod tests {
67606920
use bitcoin::{Sequence, Witness};
67616921

67626922
use crate::chain::chaininterface::LowerBoundedFeeEstimator;
6763-
use crate::events::ClosureReason;
6923+
use crate::events::{ClosureReason, Event};
67646924

67656925
use super::ChannelMonitorUpdateStep;
67666926
use crate::chain::channelmonitor::{ChannelMonitor, WithChannelMonitor};
@@ -6883,8 +7043,21 @@ mod tests {
68837043
check_spends!(htlc_txn[1], broadcast_tx);
68847044

68857045
check_closed_broadcast(&nodes[1], 1, true);
6886-
check_closed_event(&nodes[1], 1, ClosureReason::CommitmentTxConfirmed, &[nodes[0].node.get_our_node_id()], 100000);
6887-
check_added_monitors(&nodes[1], 1);
7046+
if !use_local_txn {
7047+
// When the counterparty commitment confirms, FundingSpendConfirmation matures
7048+
// immediately (no CSV delay), so funding_spend_confirmed is set. The new payment's
7049+
// commitment update then triggers immediate HTLC failure, generating payment events
7050+
// alongside the channel close event.
7051+
let events = nodes[1].node.get_and_clear_pending_events();
7052+
assert_eq!(events.len(), 3);
7053+
assert!(events.iter().any(|e| matches!(e, Event::PaymentPathFailed { .. })));
7054+
assert!(events.iter().any(|e| matches!(e, Event::PaymentFailed { .. })));
7055+
assert!(events.iter().any(|e| matches!(e, Event::ChannelClosed { .. })));
7056+
check_added_monitors(&nodes[1], 2);
7057+
} else {
7058+
check_closed_event(&nodes[1], 1, ClosureReason::CommitmentTxConfirmed, &[nodes[0].node.get_our_node_id()], 100000);
7059+
check_added_monitors(&nodes[1], 1);
7060+
}
68887061
}
68897062

68907063
#[test]

0 commit comments

Comments
 (0)