Skip to content

Commit efd46d1

Browse files
committed
Do not FC on HTLC expiry for outgoing payments.
Previously `should_broadcast_holder_commitment_txn` would FC a channel if an outbound HTLC that hasn't been resolved was `LATENCY_GRACE_PERIOD_BLOCKS` past expiry. For outgoing payments, we can avoid FC the channel since we are not in a race to claim an inbound HTLC. For cases in which a node has been offline for a while, this could help to fail the HTLC on reconnection instead of causing a FC.
1 parent 81495c6 commit efd46d1

File tree

8 files changed

+331
-91
lines changed

8 files changed

+331
-91
lines changed

lightning/src/chain/channelmonitor.rs

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5899,26 +5899,38 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
58995899
// chain when our counterparty is waiting for expiration to off-chain fail an HTLC
59005900
// we give ourselves a few blocks of headroom after expiration before going
59015901
// on-chain for an expired HTLC.
5902-
let htlc_outbound = $holder_tx == htlc.offered;
5903-
if ( htlc_outbound && htlc.cltv_expiry + LATENCY_GRACE_PERIOD_BLOCKS <= height) ||
5904-
(!htlc_outbound && htlc.cltv_expiry <= height + CLTV_CLAIM_BUFFER && self.payment_preimages.contains_key(&htlc.payment_hash)) {
5905-
log_info!(logger, "Force-closing channel due to {} HTLC timeout - HTLC with payment hash {} expires at {}", if htlc_outbound { "outbound" } else { "inbound"}, htlc.payment_hash, htlc.cltv_expiry);
5906-
return Some(htlc.payment_hash);
5902+
let htlc_outbound = $holder_tx == htlc.0.offered;
5903+
let has_incoming = if htlc_outbound {
5904+
if let Some(source) = htlc.1.as_deref() {
5905+
match *source {
5906+
HTLCSource::OutboundRoute { .. } => false,
5907+
HTLCSource::PreviousHopData(_) => true,
5908+
}
5909+
} else {
5910+
panic!("Every offered non-dust HTLC should have a corresponding source");
5911+
}
5912+
} else {
5913+
true
5914+
};
5915+
if (htlc_outbound && has_incoming && htlc.0.cltv_expiry + LATENCY_GRACE_PERIOD_BLOCKS <= height) ||
5916+
(!htlc_outbound && htlc.0.cltv_expiry <= height + CLTV_CLAIM_BUFFER && self.payment_preimages.contains_key(&htlc.0.payment_hash)) {
5917+
log_info!(logger, "Force-closing channel due to {} HTLC timeout - HTLC with payment hash {} expires at {}", if htlc_outbound { "outbound" } else { "inbound"}, htlc.0.payment_hash, htlc.0.cltv_expiry);
5918+
return Some(htlc.0.payment_hash);
59075919
}
59085920
}
59095921
}
59105922
}
59115923

5912-
scan_commitment!(holder_commitment_htlcs!(self, CURRENT), true);
5924+
scan_commitment!(holder_commitment_htlcs!(self, CURRENT_WITH_SOURCES), true);
59135925

59145926
if let Some(ref txid) = self.funding.current_counterparty_commitment_txid {
59155927
if let Some(ref htlc_outputs) = self.funding.counterparty_claimable_outpoints.get(txid) {
5916-
scan_commitment!(htlc_outputs.iter().map(|&(ref a, _)| a), false);
5928+
scan_commitment!(htlc_outputs.iter(), false);
59175929
}
59185930
}
59195931
if let Some(ref txid) = self.funding.prev_counterparty_commitment_txid {
59205932
if let Some(ref htlc_outputs) = self.funding.counterparty_claimable_outpoints.get(txid) {
5921-
scan_commitment!(htlc_outputs.iter().map(|&(ref a, _)| a), false);
5933+
scan_commitment!(htlc_outputs.iter(), false);
59225934
}
59235935
}
59245936

lightning/src/ln/async_payments_tests.rs

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3031,6 +3031,92 @@ fn held_htlc_timeout() {
30313031
);
30323032
}
30333033

3034+
#[test]
3035+
fn fail_held_htlc_on_reconnect() {
3036+
// Test that if a held HTLC by the sender LSP fails but the async sender is offline, the HTLC
3037+
// is failed on reconnect instead of FC the channel.
3038+
let chanmon_cfgs = create_chanmon_cfgs(4);
3039+
let node_cfgs = create_node_cfgs(4, &chanmon_cfgs);
3040+
3041+
let (sender_cfg, recipient_cfg) = (often_offline_node_cfg(), often_offline_node_cfg());
3042+
let mut sender_lsp_cfg = test_default_channel_config();
3043+
sender_lsp_cfg.enable_htlc_hold = true;
3044+
let mut invoice_server_cfg = test_default_channel_config();
3045+
invoice_server_cfg.accept_forwards_to_priv_channels = true;
3046+
3047+
let node_chanmgrs = create_node_chanmgrs(
3048+
4,
3049+
&node_cfgs,
3050+
&[Some(sender_cfg), Some(sender_lsp_cfg), Some(invoice_server_cfg), Some(recipient_cfg)],
3051+
);
3052+
let nodes = create_network(4, &node_cfgs, &node_chanmgrs);
3053+
let chan = create_unannounced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0);
3054+
create_announced_chan_between_nodes_with_value(&nodes, 1, 2, 1_000_000, 0);
3055+
create_unannounced_chan_between_nodes_with_value(&nodes, 2, 3, 1_000_000, 0);
3056+
unify_blockheight_across_nodes(&nodes);
3057+
let sender = &nodes[0];
3058+
let sender_lsp = &nodes[1];
3059+
let invoice_server = &nodes[2];
3060+
let recipient = &nodes[3];
3061+
3062+
let amt_msat = 5000;
3063+
let (_, peer_node_id, static_invoice_om) = build_async_offer_and_init_payment(amt_msat, &nodes);
3064+
let payment_hash =
3065+
lock_in_htlc_for_static_invoice(&static_invoice_om, peer_node_id, sender, sender_lsp);
3066+
3067+
sender_lsp.node.process_pending_htlc_forwards();
3068+
let (peer_id, held_htlc_om) =
3069+
extract_held_htlc_available_oms(sender, &[sender_lsp, invoice_server, recipient])
3070+
.pop()
3071+
.unwrap();
3072+
recipient.onion_messenger.handle_onion_message(peer_id, &held_htlc_om);
3073+
3074+
let _ = extract_release_htlc_oms(recipient, &[sender, sender_lsp, invoice_server]);
3075+
3076+
// Disconnect async sender <-> sender LSP
3077+
sender.node.peer_disconnected(sender_lsp.node.get_our_node_id());
3078+
sender_lsp.node.peer_disconnected(sender.node.get_our_node_id());
3079+
3080+
// Connect blocks such that they cause the HTLC to timeout
3081+
let chan_id = chan.0.channel_id;
3082+
let channel =
3083+
sender.node.list_channels().iter().find(|c| c.channel_id == chan_id).unwrap().clone();
3084+
let htlc_expiry = channel
3085+
.pending_outbound_htlcs
3086+
.iter()
3087+
.find(|htlc| htlc.payment_hash == payment_hash)
3088+
.unwrap()
3089+
.cltv_expiry;
3090+
let blocks_to_connect = htlc_expiry - sender.best_block_info().1 + 100;
3091+
connect_blocks(sender, blocks_to_connect);
3092+
connect_blocks(sender_lsp, blocks_to_connect);
3093+
3094+
sender_lsp.node.process_pending_htlc_forwards();
3095+
let mut evs = sender_lsp.node.get_and_clear_pending_events();
3096+
assert_eq!(evs.len(), 1);
3097+
match evs.pop().unwrap() {
3098+
Event::HTLCHandlingFailed { failure_type, failure_reason, .. } => {
3099+
assert!(matches!(failure_type, HTLCHandlingFailureType::InvalidForward { .. }));
3100+
assert!(matches!(
3101+
failure_reason,
3102+
Some(HTLCHandlingFailureReason::Local {
3103+
reason: LocalHTLCFailureReason::ForwardExpiryBuffer
3104+
})
3105+
));
3106+
},
3107+
_ => panic!(),
3108+
}
3109+
3110+
// After reconnecting, check that HTLC was failed and channel is open.
3111+
let mut reconnect_args = ReconnectArgs::new(&sender, &sender_lsp);
3112+
reconnect_args.pending_cell_htlc_fails.0 = 1;
3113+
reconnect_nodes(reconnect_args);
3114+
3115+
expect_payment_failed!(sender, payment_hash, false);
3116+
assert_eq!(sender.node.list_channels().len(), 1);
3117+
assert_eq!(sender_lsp.node.list_channels().len(), 2);
3118+
}
3119+
30343120
#[test]
30353121
fn intercepted_hold_htlc() {
30363122
// Test a payment `sender --> LSP --> recipient` such that the HTLC is both a hold htlc and an

lightning/src/ln/async_signer_tests.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1014,6 +1014,12 @@ fn do_test_async_holder_signatures(keyed_anchors: bool, p2a_anchor: bool, remote
10141014
// We'll connect blocks until the sender has to go onchain to time out the HTLC.
10151015
connect_blocks(&nodes[0], TEST_FINAL_CLTV + LATENCY_GRACE_PERIOD_BLOCKS + 1);
10161016

1017+
let closure_reason = ClosureReason::HTLCsTimedOut { payment_hash: Some(payment_hash) };
1018+
nodes[0]
1019+
.node
1020+
.force_close_broadcasting_latest_txn(&chan_id, &node_b_id, closure_reason.to_string())
1021+
.unwrap();
1022+
10171023
// No transaction should be broadcast since the signer is not available yet.
10181024
assert!(nodes[0].tx_broadcaster.txn_broadcast().is_empty());
10191025
assert!(nodes[0].chain_monitor.chain_monitor.get_and_clear_pending_events().is_empty());
@@ -1076,7 +1082,11 @@ fn do_test_async_holder_signatures(keyed_anchors: bool, p2a_anchor: bool, remote
10761082
let closure_reason = if remote_commitment {
10771083
ClosureReason::CommitmentTxConfirmed
10781084
} else {
1079-
ClosureReason::HTLCsTimedOut { payment_hash: Some(payment_hash) }
1085+
let closure_reason = ClosureReason::HTLCsTimedOut { payment_hash: Some(payment_hash) };
1086+
ClosureReason::HolderForceClosed {
1087+
broadcasted_latest_txn: Some(true),
1088+
message: closure_reason.to_string(),
1089+
}
10801090
};
10811091
check_closed_event(&nodes[0], 1, closure_reason, false, &[node_b_id], 100_000);
10821092

lightning/src/ln/chanmon_update_fail_tests.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,11 @@ fn test_monitor_and_persister_update_fail() {
118118
chain_mon
119119
.chain_monitor
120120
.block_connected(&create_dummy_block(BlockHash::all_zeros(), 42, Vec::new()), 200);
121+
chain_mon.chain_monitor.get_monitor(chan.2).unwrap().broadcast_latest_holder_commitment_txn(
122+
&&tx_broadcaster,
123+
&nodes[0].fee_estimator,
124+
&nodes[0].logger,
125+
);
121126

122127
// Try to update ChannelMonitor
123128
nodes[1].node.claim_funds(preimage);

0 commit comments

Comments
 (0)