Skip to content

Commit 37d3702

Browse files
committed
lightning: skip resolved HTLC claim replays
Filter regenerated HTLC claim requests once ChannelMonitor has persisted anti-reorg finality for the commitment HTLC output spend. This keeps replayed preimage updates from recreating claims after OnchainTxHandler has cleaned up its active retry state, relying on the monitor's persisted HTLC resolution state.
1 parent 914026a commit 37d3702

2 files changed

Lines changed: 106 additions & 3 deletions

File tree

lightning/src/chain/channelmonitor.rs

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4938,7 +4938,10 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
49384938
.iter()
49394939
.filter_map(|(htlc, _)| {
49404940
if let Some(transaction_output_index) = htlc.transaction_output_index {
4941-
if htlc.offered && htlc.payment_hash == matching_payment_hash {
4941+
if htlc.offered
4942+
&& htlc.payment_hash == matching_payment_hash
4943+
&& !self.is_htlc_output_spent_on_chain(htlc)
4944+
{
49424945
let htlc_data = PackageSolvingData::CounterpartyOfferedHTLCOutput(
49434946
CounterpartyOfferedHTLCOutput::build(
49444947
per_commitment_point,
@@ -4964,6 +4967,20 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
49644967
.collect()
49654968
}
49664969

4970+
fn is_htlc_output_spent_on_chain(&self, htlc: &HTLCOutputInCommitment) -> bool {
4971+
if let Some(transaction_output_index) = htlc.transaction_output_index {
4972+
// Only suppress claims once the commitment HTLC output spend has
4973+
// reached anti-reorg finality. Any output created by that spend may
4974+
// still be CSV-delayed, but the original HTLC outpoint should not be
4975+
// re-claimed.
4976+
self.htlcs_resolved_on_chain.iter().any(|resolved_htlc| {
4977+
resolved_htlc.commitment_tx_output_idx == Some(transaction_output_index)
4978+
})
4979+
} else {
4980+
false
4981+
}
4982+
}
4983+
49674984
/// Returns the HTLC claim requests and the counterparty output info.
49684985
fn get_counterparty_output_claim_info(
49694986
&self, funding_spent: &FundingScope, commitment_number: u64, commitment_txid: Txid,
@@ -5011,6 +5028,9 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
50115028
// per_commitment_data is corrupt or our commitment signing key leaked!
50125029
return (claimable_outpoints, to_counterparty_output_info);
50135030
}
5031+
if self.is_htlc_output_spent_on_chain(htlc) {
5032+
continue;
5033+
}
50145034
let preimage = if htlc.offered {
50155035
if let Some((p, _)) = self.payment_preimages.get(&htlc.payment_hash) {
50165036
Some(*p)
@@ -5112,6 +5132,9 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
51125132
let mut htlcs = Vec::with_capacity(holder_tx.nondust_htlcs().len());
51135133
debug_assert_eq!(holder_tx.nondust_htlcs().len(), holder_tx.counterparty_htlc_sigs.len());
51145134
for (htlc, counterparty_sig) in holder_tx.nondust_htlcs().iter().zip(holder_tx.counterparty_htlc_sigs.iter()) {
5135+
if self.is_htlc_output_spent_on_chain(htlc) {
5136+
continue;
5137+
}
51155138
assert!(htlc.transaction_output_index.is_some(), "Expected transaction output index for non-dust HTLC");
51165139

51175140
let preimage = if htlc.offered {

lightning/src/ln/monitor_tests.rs

Lines changed: 82 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2458,12 +2458,15 @@ fn test_restored_packages_retry() {
24582458
fn do_test_duplicate_delayed_holder_htlc_claims_after_claim_funds_replay(p2a_anchor: bool) {
24592459
let chanmon_cfgs = create_chanmon_cfgs(2);
24602460
let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
2461+
let persister;
2462+
let new_chain_monitor;
2463+
let node_deserialized;
24612464
let mut anchors_config = test_default_channel_config();
24622465
anchors_config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = true;
24632466
anchors_config.channel_handshake_config.negotiate_anchor_zero_fee_commitments = p2a_anchor;
24642467
let node_chanmgrs =
24652468
create_node_chanmgrs(2, &node_cfgs, &[Some(anchors_config.clone()), Some(anchors_config)]);
2466-
let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
2469+
let mut nodes = create_network(2, &node_cfgs, &node_chanmgrs);
24672470

24682471
let coinbase_tx = provide_anchor_reserves(&nodes);
24692472
let (_, _, chan_id, funding_tx) =
@@ -2542,11 +2545,14 @@ fn do_test_duplicate_delayed_holder_htlc_claims_after_claim_funds_replay(p2a_anc
25422545
// the delayed package's outpoints.
25432546
connect_blocks(&nodes[0], TEST_FINAL_CLTV + 1);
25442547

2545-
let mut htlc_event_sizes = nodes[0]
2548+
let events = nodes[0]
25462549
.chain_monitor
25472550
.chain_monitor
25482551
.get_and_clear_pending_events()
25492552
.into_iter()
2553+
.collect::<Vec<_>>();
2554+
let mut htlc_event_sizes = events
2555+
.iter()
25502556
.filter_map(|event| {
25512557
if let Event::BumpTransaction(BumpTransactionEvent::HTLCResolution {
25522558
htlc_descriptors, ..
@@ -2560,6 +2566,80 @@ fn do_test_duplicate_delayed_holder_htlc_claims_after_claim_funds_replay(p2a_anc
25602566
.collect::<Vec<_>>();
25612567
htlc_event_sizes.sort_unstable();
25622568
assert_eq!(htlc_event_sizes, vec![1, 2]);
2569+
2570+
// Drive only the replayed single-HTLC event on-chain so we can replay the
2571+
// preimage once the spend is anti-reorg final, then again after reload.
2572+
for event in events {
2573+
if let Event::BumpTransaction(event) = event {
2574+
let is_single_htlc = if let BumpTransactionEvent::HTLCResolution {
2575+
ref htlc_descriptors,
2576+
..
2577+
} = event
2578+
{
2579+
htlc_descriptors.len() == 1
2580+
} else {
2581+
false
2582+
};
2583+
if is_single_htlc {
2584+
nodes[0].bump_tx_handler.handle_event(&event);
2585+
break;
2586+
}
2587+
}
2588+
}
2589+
let mut htlc_txn = nodes[0].tx_broadcaster.unique_txn_broadcast();
2590+
assert_eq!(htlc_txn.len(), 1);
2591+
let htlc_tx = htlc_txn.pop().unwrap();
2592+
mine_transaction(&nodes[0], &htlc_tx);
2593+
connect_blocks(&nodes[0], ANTI_REORG_DELAY - 1);
2594+
assert!(nodes[0].chain_monitor.chain_monitor.get_and_clear_pending_events().is_empty());
2595+
2596+
// The spend has passed anti-reorg finality, but its CSV-delayed output is
2597+
// not yet spendable. Replaying the preimage in this window must not create
2598+
// a new conflicting claim for the already-spent commitment HTLC output.
2599+
get_monitor!(nodes[0], chan_id).provide_payment_preimage_unsafe_legacy(
2600+
&claim_hash,
2601+
&claim_preimage,
2602+
&node_cfgs[0].tx_broadcaster,
2603+
&LowerBoundedFeeEstimator::new(node_cfgs[0].fee_estimator),
2604+
&nodes[0].logger,
2605+
);
2606+
assert!(nodes[0].chain_monitor.chain_monitor.get_and_clear_pending_events().is_empty());
2607+
let balances = nodes[0]
2608+
.chain_monitor
2609+
.chain_monitor
2610+
.get_monitor(chan_id)
2611+
.unwrap()
2612+
.get_claimable_balances();
2613+
assert!(balances.iter().any(|balance| matches!(
2614+
balance,
2615+
Balance::ClaimableAwaitingConfirmations {
2616+
amount_satoshis: 12_000,
2617+
source: BalanceSource::Htlc,
2618+
..
2619+
}
2620+
)));
2621+
2622+
connect_blocks(&nodes[0], BREAKDOWN_TIMEOUT as u32 - ANTI_REORG_DELAY);
2623+
let _ = nodes[0].chain_monitor.chain_monitor.get_and_clear_pending_events();
2624+
2625+
// Reload before replaying the preimage so the regression covers persisted
2626+
// resolution state, not only in-memory filtering.
2627+
let serialized_channel_manager = nodes[0].node.encode();
2628+
let serialized_monitor = get_monitor!(nodes[0], chan_id).encode();
2629+
reload_node!(
2630+
nodes[0], &serialized_channel_manager, &[&serialized_monitor], persister,
2631+
new_chain_monitor, node_deserialized
2632+
);
2633+
2634+
// Replaying the preimage update must not regenerate a claim for the HTLC
2635+
// whose commitment output has anti-reorg persisted resolution state.
2636+
get_monitor!(nodes[0], chan_id).provide_payment_preimage_unsafe_legacy(
2637+
&claim_hash, &claim_preimage, &node_cfgs[0].tx_broadcaster,
2638+
&LowerBoundedFeeEstimator::new(node_cfgs[0].fee_estimator), &nodes[0].logger,
2639+
);
2640+
assert!(nodes[0].chain_monitor.chain_monitor.get_and_clear_pending_events().is_empty());
2641+
expect_payment_claimed!(nodes[0], claim_hash, 12_000_000);
2642+
check_added_monitors(&nodes[0], 1);
25632643
}
25642644

25652645
#[test]

0 commit comments

Comments
 (0)