Skip to content

Commit 3e1a18c

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 dcf0c20 commit 3e1a18c

File tree

4 files changed

+449
-9
lines changed

4 files changed

+449
-9
lines changed

lightning/src/chain/chainmonitor.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1279,15 +1279,17 @@ where
12791279
/// [`ChannelManager`]: crate::ln::channelmanager::ChannelManager
12801280
pub fn flush(&self, count: usize, logger: &L) {
12811281
let _guard = self.flush_lock.lock().unwrap();
1282-
if count > 0 {
1283-
log_info!(logger, "Flushing up to {} monitor operations", count);
1282+
if count == 0 {
1283+
return;
12841284
}
1285+
log_info!(logger, "Flushing up to {} monitor operations", count);
12851286
for _ in 0..count {
12861287
let mut queue = self.pending_ops.lock().unwrap();
12871288
let op = match queue.pop_front() {
12881289
Some(op) => op,
12891290
None => {
12901291
debug_assert!(false, "flush count exceeded queue length");
1292+
log_error!(logger, "flush count exceeded queue length");
12911293
return;
12921294
},
12931295
};
@@ -1341,6 +1343,10 @@ where
13411343
},
13421344
}
13431345
}
1346+
1347+
// A flushed monitor update may have generated new events, so assume we have
1348+
// some and wake the event processor.
1349+
self.event_notifier.notify();
13441350
}
13451351
}
13461352

lightning/src/chain/channelmonitor.rs

Lines changed: 182 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1378,8 +1378,8 @@ pub(crate) struct ChannelMonitorImpl<Signer: EcdsaChannelSigner> {
13781378
/// In-memory only HTLC ids used to track upstream HTLCs that have been failed backwards due to
13791379
/// a downstream channel force-close remaining unconfirmed by the time the upstream timeout
13801380
/// expires. This is used to tell us we already generated an event to fail this HTLC back
1381-
/// during a previous block scan.
1382-
failed_back_htlc_ids: HashSet<SentHTLCId>,
1381+
/// during a previous block scan. Not serialized.
1382+
pub(crate) failed_back_htlc_ids: HashSet<SentHTLCId>,
13831383

13841384
// The auxiliary HTLC data associated with a holder commitment transaction. This includes
13851385
// non-dust HTLC sources, along with dust HTLCs and their sources. Note that this assumes any
@@ -4299,6 +4299,55 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
42994299

43004300
self.latest_update_id = updates.update_id;
43014301

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

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

68367000
use crate::chain::chaininterface::LowerBoundedFeeEstimator;
6837-
use crate::events::ClosureReason;
7001+
use crate::events::{ClosureReason, Event};
68387002

68397003
use super::ChannelMonitorUpdateStep;
68407004
use crate::chain::channelmonitor::{ChannelMonitor, WithChannelMonitor};
@@ -6957,8 +7121,21 @@ mod tests {
69577121
check_spends!(htlc_txn[1], broadcast_tx);
69587122

69597123
check_closed_broadcast(&nodes[1], 1, true);
6960-
check_closed_event(&nodes[1], 1, ClosureReason::CommitmentTxConfirmed, &[nodes[0].node.get_our_node_id()], 100000);
6961-
check_added_monitors(&nodes[1], 1);
7124+
if !use_local_txn {
7125+
// When the counterparty commitment confirms, FundingSpendConfirmation matures
7126+
// immediately (no CSV delay), so funding_spend_confirmed is set. The new payment's
7127+
// commitment update then triggers immediate HTLC failure, generating payment events
7128+
// alongside the channel close event.
7129+
let events = nodes[1].node.get_and_clear_pending_events();
7130+
assert_eq!(events.len(), 3);
7131+
assert!(events.iter().any(|e| matches!(e, Event::PaymentPathFailed { .. })));
7132+
assert!(events.iter().any(|e| matches!(e, Event::PaymentFailed { .. })));
7133+
assert!(events.iter().any(|e| matches!(e, Event::ChannelClosed { .. })));
7134+
check_added_monitors(&nodes[1], 2);
7135+
} else {
7136+
check_closed_event(&nodes[1], 1, ClosureReason::CommitmentTxConfirmed, &[nodes[0].node.get_our_node_id()], 100000);
7137+
check_added_monitors(&nodes[1], 1);
7138+
}
69627139
}
69637140

69647141
#[test]

0 commit comments

Comments
 (0)