Skip to content

Commit 337bef1

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 337bef1

4 files changed

Lines changed: 449 additions & 9 deletions

File tree

lightning/src/chain/chainmonitor.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1274,15 +1274,17 @@ where
12741274
///
12751275
/// [`ChannelManager`]: crate::ln::channelmanager::ChannelManager
12761276
pub fn flush(&self, count: usize, logger: &L) {
1277-
if count > 0 {
1278-
log_info!(logger, "Flushing up to {} monitor operations", count);
1277+
if count == 0 {
1278+
return;
12791279
}
1280+
log_info!(logger, "Flushing up to {} monitor operations", count);
12801281
for _ in 0..count {
12811282
let mut queue = self.pending_ops.lock().unwrap();
12821283
let op = match queue.pop_front() {
12831284
Some(op) => op,
12841285
None => {
12851286
debug_assert!(false, "flush count exceeded queue length");
1287+
log_error!(logger, "flush count exceeded queue length");
12861288
return;
12871289
},
12881290
};
@@ -1334,6 +1336,10 @@ where
13341336
},
13351337
}
13361338
}
1339+
1340+
// A flushed monitor update may have generated new events, so assume we have
1341+
// some and wake the event processor.
1342+
self.event_notifier.notify();
13371343
}
13381344
}
13391345

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
@@ -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,121 @@ 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+
// Defensively mark the HTLC as failed back so the expiry-based failure
4455+
// path in `block_connected` doesn't generate a duplicate `HTLCUpdate`
4456+
// event for the same source.
4457+
self.failed_back_htlc_ids.insert(SentHTLCId::from_source(source));
4458+
if let Some(confirmed_txid) = self.funding_spend_confirmed {
4459+
// Funding spend already confirmed past ANTI_REORG_DELAY: resolve immediately.
4460+
log_trace!(
4461+
logger,
4462+
"Failing HTLC from late counterparty commitment update immediately \
4463+
(funding spend already confirmed)"
4464+
);
4465+
self.pending_monitor_events.push(MonitorEvent::HTLCEvent(HTLCUpdate {
4466+
payment_hash,
4467+
payment_preimage: None,
4468+
source: source.clone(),
4469+
htlc_value_satoshis,
4470+
}));
4471+
self.htlcs_resolved_on_chain.push(IrrevocablyResolvedHTLC {
4472+
commitment_tx_output_idx: None,
4473+
resolving_txid: Some(confirmed_txid),
4474+
resolving_tx: None,
4475+
payment_preimage: None,
4476+
});
4477+
} else {
4478+
// Funding spend still awaiting ANTI_REORG_DELAY: queue the failure.
4479+
let (txid, transaction, height, block_hash) = pending_spend_entry.clone().unwrap();
4480+
let entry = OnchainEventEntry {
4481+
txid,
4482+
transaction,
4483+
height,
4484+
block_hash,
4485+
event: OnchainEvent::HTLCUpdate {
4486+
source: source.clone(),
4487+
payment_hash,
4488+
htlc_value_satoshis,
4489+
commitment_tx_output_idx: None,
4490+
},
4491+
};
4492+
log_trace!(
4493+
logger,
4494+
"Failing HTLC from late counterparty commitment update, \
4495+
waiting for confirmation (at height {})",
4496+
entry.confirmation_threshold()
4497+
);
4498+
self.onchain_events_awaiting_threshold_conf.push(entry);
4499+
}
4500+
}
4501+
}
4502+
43394503
fn get_latest_update_id(&self) -> u64 {
43404504
self.latest_update_id
43414505
}
@@ -6760,7 +6924,7 @@ mod tests {
67606924
use bitcoin::{Sequence, Witness};
67616925

67626926
use crate::chain::chaininterface::LowerBoundedFeeEstimator;
6763-
use crate::events::ClosureReason;
6927+
use crate::events::{ClosureReason, Event};
67646928

67656929
use super::ChannelMonitorUpdateStep;
67666930
use crate::chain::channelmonitor::{ChannelMonitor, WithChannelMonitor};
@@ -6883,8 +7047,21 @@ mod tests {
68837047
check_spends!(htlc_txn[1], broadcast_tx);
68847048

68857049
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);
7050+
if !use_local_txn {
7051+
// When the counterparty commitment confirms, FundingSpendConfirmation matures
7052+
// immediately (no CSV delay), so funding_spend_confirmed is set. The new payment's
7053+
// commitment update then triggers immediate HTLC failure, generating payment events
7054+
// alongside the channel close event.
7055+
let events = nodes[1].node.get_and_clear_pending_events();
7056+
assert_eq!(events.len(), 3);
7057+
assert!(events.iter().any(|e| matches!(e, Event::PaymentPathFailed { .. })));
7058+
assert!(events.iter().any(|e| matches!(e, Event::PaymentFailed { .. })));
7059+
assert!(events.iter().any(|e| matches!(e, Event::ChannelClosed { .. })));
7060+
check_added_monitors(&nodes[1], 2);
7061+
} else {
7062+
check_closed_event(&nodes[1], 1, ClosureReason::CommitmentTxConfirmed, &[nodes[0].node.get_our_node_id()], 100000);
7063+
check_added_monitors(&nodes[1], 1);
7064+
}
68887065
}
68897066

68907067
#[test]

0 commit comments

Comments
 (0)