Skip to content

Commit 4e3025e

Browse files
committed
lightning: skip resolved HTLC claim replays
Filter regenerated HTLC claim requests only after ChannelMonitor has persisted final HTLC resolution for the commitment output. This keeps replayed preimage updates from recreating claims once the monitor has durable resolution state, while preserving live conflicting claims before final resolution so they can be retried if a counterparty spend reorgs out.
1 parent 74de5b4 commit 4e3025e

2 files changed

Lines changed: 116 additions & 3 deletions

File tree

lightning/src/chain/channelmonitor.rs

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4936,7 +4936,10 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
49364936
.iter()
49374937
.filter_map(|(htlc, _)| {
49384938
if let Some(transaction_output_index) = htlc.transaction_output_index {
4939-
if htlc.offered && htlc.payment_hash == matching_payment_hash {
4939+
if htlc.offered
4940+
&& htlc.payment_hash == matching_payment_hash
4941+
&& !self.is_htlc_output_spent_on_chain(htlc)
4942+
{
49404943
let htlc_data = PackageSolvingData::CounterpartyOfferedHTLCOutput(
49414944
CounterpartyOfferedHTLCOutput::build(
49424945
per_commitment_point,
@@ -4962,6 +4965,21 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
49624965
.collect()
49634966
}
49644967

4968+
fn is_htlc_output_spent_on_chain(&self, htlc: &HTLCOutputInCommitment) -> bool {
4969+
if let Some(transaction_output_index) = htlc.transaction_output_index {
4970+
// Only suppress claims once the monitor has persisted final HTLC
4971+
// resolution. While a conflicting spend is still awaiting anti-reorg
4972+
// confirmation, a replayed preimage may create a live conflicting
4973+
// claim; keeping that claim in OnchainTxHandler preserves retry state
4974+
// if the spend reorgs out.
4975+
self.htlcs_resolved_on_chain.iter().any(|resolved_htlc| {
4976+
resolved_htlc.commitment_tx_output_idx == Some(transaction_output_index)
4977+
})
4978+
} else {
4979+
false
4980+
}
4981+
}
4982+
49654983
/// Returns the HTLC claim requests and the counterparty output info.
49664984
fn get_counterparty_output_claim_info(
49674985
&self, funding_spent: &FundingScope, commitment_number: u64, commitment_txid: Txid,
@@ -5009,6 +5027,9 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
50095027
// per_commitment_data is corrupt or our commitment signing key leaked!
50105028
return (claimable_outpoints, to_counterparty_output_info);
50115029
}
5030+
if self.is_htlc_output_spent_on_chain(htlc) {
5031+
continue;
5032+
}
50125033
let preimage = if htlc.offered {
50135034
if let Some((p, _)) = self.payment_preimages.get(&htlc.payment_hash) {
50145035
Some(*p)
@@ -5110,6 +5131,9 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
51105131
let mut htlcs = Vec::with_capacity(holder_tx.nondust_htlcs().len());
51115132
debug_assert_eq!(holder_tx.nondust_htlcs().len(), holder_tx.counterparty_htlc_sigs.len());
51125133
for (htlc, counterparty_sig) in holder_tx.nondust_htlcs().iter().zip(holder_tx.counterparty_htlc_sigs.iter()) {
5134+
if self.is_htlc_output_spent_on_chain(htlc) {
5135+
continue;
5136+
}
51135137
assert!(htlc.transaction_output_index.is_some(), "Expected transaction output index for non-dust HTLC");
51145138

51155139
let preimage = if htlc.offered {

lightning/src/ln/monitor_tests.rs

Lines changed: 91 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2391,12 +2391,15 @@ fn test_restored_packages_retry() {
23912391
fn do_test_duplicate_delayed_holder_htlc_claims_after_claim_funds_replay(p2a_anchor: bool) {
23922392
let chanmon_cfgs = create_chanmon_cfgs(2);
23932393
let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
2394+
let persister;
2395+
let new_chain_monitor;
2396+
let node_deserialized;
23942397
let mut anchors_config = test_default_channel_config();
23952398
anchors_config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = true;
23962399
anchors_config.channel_handshake_config.negotiate_anchor_zero_fee_commitments = p2a_anchor;
23972400
let node_chanmgrs =
23982401
create_node_chanmgrs(2, &node_cfgs, &[Some(anchors_config.clone()), Some(anchors_config)]);
2399-
let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
2402+
let mut nodes = create_network(2, &node_cfgs, &node_chanmgrs);
24002403

24012404
let coinbase_tx = provide_anchor_reserves(&nodes);
24022405
let (_, _, chan_id, funding_tx) =
@@ -2475,11 +2478,14 @@ fn do_test_duplicate_delayed_holder_htlc_claims_after_claim_funds_replay(p2a_anc
24752478
// the delayed package's outpoints.
24762479
connect_blocks(&nodes[0], TEST_FINAL_CLTV + 1);
24772480

2478-
let mut htlc_event_sizes = nodes[0]
2481+
let events = nodes[0]
24792482
.chain_monitor
24802483
.chain_monitor
24812484
.get_and_clear_pending_events()
24822485
.into_iter()
2486+
.collect::<Vec<_>>();
2487+
let mut htlc_event_sizes = events
2488+
.iter()
24832489
.filter_map(|event| {
24842490
if let Event::BumpTransaction(BumpTransactionEvent::HTLCResolution {
24852491
htlc_descriptors, ..
@@ -2493,6 +2499,89 @@ fn do_test_duplicate_delayed_holder_htlc_claims_after_claim_funds_replay(p2a_anc
24932499
.collect::<Vec<_>>();
24942500
htlc_event_sizes.sort_unstable();
24952501
assert_eq!(htlc_event_sizes, vec![1, 2]);
2502+
2503+
// Drive only the replayed single-HTLC event on-chain. A preimage replay
2504+
// before its CSV-delayed output is final may create a live conflicting
2505+
// claim, so the final replay assertion below waits for the monitor's
2506+
// persisted resolution state instead.
2507+
for event in events {
2508+
if let Event::BumpTransaction(event) = event {
2509+
let is_single_htlc = if let BumpTransactionEvent::HTLCResolution {
2510+
ref htlc_descriptors,
2511+
..
2512+
} = event
2513+
{
2514+
htlc_descriptors.len() == 1
2515+
} else {
2516+
false
2517+
};
2518+
if is_single_htlc {
2519+
nodes[0].bump_tx_handler.handle_event(&event);
2520+
break;
2521+
}
2522+
}
2523+
}
2524+
let mut htlc_txn = nodes[0].tx_broadcaster.unique_txn_broadcast();
2525+
assert_eq!(htlc_txn.len(), 1);
2526+
let htlc_tx = htlc_txn.pop().unwrap();
2527+
mine_transaction(&nodes[0], &htlc_tx);
2528+
connect_blocks(&nodes[0], ANTI_REORG_DELAY - 1);
2529+
assert!(nodes[0].chain_monitor.chain_monitor.get_and_clear_pending_events().is_empty());
2530+
2531+
// The spend has passed OnchainTxHandler's anti-reorg cleanup, but its
2532+
// CSV-delayed output is not yet final according to the monitor. Replaying
2533+
// the preimage in this window creates a live conflicting claim, which is
2534+
// kept as retry state in case the spend reorgs out.
2535+
get_monitor!(nodes[0], chan_id).provide_payment_preimage_unsafe_legacy(
2536+
&claim_hash,
2537+
&claim_preimage,
2538+
&node_cfgs[0].tx_broadcaster,
2539+
&LowerBoundedFeeEstimator::new(node_cfgs[0].fee_estimator),
2540+
&nodes[0].logger,
2541+
);
2542+
let live_conflict_events = nodes[0]
2543+
.chain_monitor
2544+
.chain_monitor
2545+
.get_and_clear_pending_events()
2546+
.into_iter()
2547+
.collect::<Vec<_>>();
2548+
let mut live_conflict_htlc_event_sizes = live_conflict_events
2549+
.iter()
2550+
.filter_map(|event| {
2551+
if let Event::BumpTransaction(BumpTransactionEvent::HTLCResolution {
2552+
htlc_descriptors, ..
2553+
}) = event
2554+
{
2555+
Some(htlc_descriptors.len())
2556+
} else {
2557+
None
2558+
}
2559+
})
2560+
.collect::<Vec<_>>();
2561+
live_conflict_htlc_event_sizes.sort_unstable();
2562+
assert_eq!(live_conflict_htlc_event_sizes, vec![1]);
2563+
2564+
connect_blocks(&nodes[0], BREAKDOWN_TIMEOUT as u32 - ANTI_REORG_DELAY);
2565+
let _ = nodes[0].chain_monitor.chain_monitor.get_and_clear_pending_events();
2566+
2567+
// Reload before replaying the preimage so the regression covers persisted
2568+
// resolution state, not only in-memory filtering.
2569+
let serialized_channel_manager = nodes[0].node.encode();
2570+
let serialized_monitor = get_monitor!(nodes[0], chan_id).encode();
2571+
reload_node!(
2572+
nodes[0], &serialized_channel_manager, &[&serialized_monitor], persister,
2573+
new_chain_monitor, node_deserialized
2574+
);
2575+
2576+
// Replaying the preimage update must not regenerate a claim for the HTLC
2577+
// whose commitment output has final persisted resolution state.
2578+
get_monitor!(nodes[0], chan_id).provide_payment_preimage_unsafe_legacy(
2579+
&claim_hash, &claim_preimage, &node_cfgs[0].tx_broadcaster,
2580+
&LowerBoundedFeeEstimator::new(node_cfgs[0].fee_estimator), &nodes[0].logger,
2581+
);
2582+
assert!(nodes[0].chain_monitor.chain_monitor.get_and_clear_pending_events().is_empty());
2583+
expect_payment_claimed!(nodes[0], claim_hash, 12_000_000);
2584+
check_added_monitors(&nodes[0], 1);
24962585
}
24972586

24982587
#[test]

0 commit comments

Comments
 (0)