Skip to content

Commit 93388e4

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 that won't have an inbound HTLC, block time to allow before we FC could be higher since we are not in a race to claim an inbound HTLC. Here we use 2016 blocks which is roughly 2 weeks. 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 93388e4

File tree

2 files changed

+85
-8
lines changed

2 files changed

+85
-8
lines changed

lightning/src/chain/channelmonitor.rs

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5899,26 +5899,39 @@ 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 && !has_incoming && htlc.0.cltv_expiry + 2016 <= height) ||
5917+
(!htlc_outbound && htlc.0.cltv_expiry <= height + CLTV_CLAIM_BUFFER && self.payment_preimages.contains_key(&htlc.0.payment_hash)) {
5918+
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);
5919+
return Some(htlc.0.payment_hash);
59075920
}
59085921
}
59095922
}
59105923
}
59115924

5912-
scan_commitment!(holder_commitment_htlcs!(self, CURRENT), true);
5925+
scan_commitment!(holder_commitment_htlcs!(self, CURRENT_WITH_SOURCES), true);
59135926

59145927
if let Some(ref txid) = self.funding.current_counterparty_commitment_txid {
59155928
if let Some(ref htlc_outputs) = self.funding.counterparty_claimable_outpoints.get(txid) {
5916-
scan_commitment!(htlc_outputs.iter().map(|&(ref a, _)| a), false);
5929+
scan_commitment!(htlc_outputs.iter(), false);
59175930
}
59185931
}
59195932
if let Some(ref txid) = self.funding.prev_counterparty_commitment_txid {
59205933
if let Some(ref htlc_outputs) = self.funding.counterparty_claimable_outpoints.get(txid) {
5921-
scan_commitment!(htlc_outputs.iter().map(|&(ref a, _)| a), false);
5934+
scan_commitment!(htlc_outputs.iter(), false);
59225935
}
59235936
}
59245937

lightning/src/ln/reload_tests.rs

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -919,6 +919,70 @@ fn test_partial_claim_before_restart() {
919919
do_test_partial_claim_before_restart(true, true);
920920
}
921921

922+
#[test]
923+
fn test_no_fc_outgoing_payment() {
924+
let chanmon_cfgs = create_chanmon_cfgs(3);
925+
let node_cfgs = create_node_cfgs(3, &chanmon_cfgs);
926+
let persister;
927+
let new_chain_monitor;
928+
929+
let mut intercept_forwards_config = test_default_channel_config();
930+
intercept_forwards_config.accept_intercept_htlcs = true;
931+
let node_chanmgrs = create_node_chanmgrs(3, &node_cfgs, &[None, Some(intercept_forwards_config), None]);
932+
let nodes_0_deserialized;
933+
934+
let mut nodes = create_network(3, &node_cfgs, &node_chanmgrs);
935+
936+
let chan_id_1 = create_announced_chan_between_nodes(&nodes, 0, 1).2;
937+
let _chan_id_2 = create_announced_chan_between_nodes(&nodes, 1, 2).2;
938+
939+
// Send payment from 0 -> 1 -> 2 that will be failed back
940+
let (_, payment_hash, ..) = route_payment(&nodes[0], &[&nodes[1], &nodes[2]], 10_000);
941+
942+
nodes[2].node.fail_htlc_backwards(&payment_hash);
943+
expect_and_process_pending_htlcs_and_htlc_handling_failed(
944+
&nodes[2],
945+
&[HTLCHandlingFailureType::Receive { payment_hash }]
946+
);
947+
check_added_monitors!(nodes[2], 1);
948+
949+
// After committing the HTLCs, fail it back from 2 -> 1
950+
let updates = get_htlc_update_msgs!(nodes[2], nodes[1].node.get_our_node_id());
951+
assert_eq!(updates.update_fail_htlcs.len(), 1);
952+
953+
nodes[1].node.handle_update_fail_htlc(nodes[2].node.get_our_node_id(), &updates.update_fail_htlcs[0]);
954+
commitment_signed_dance!(nodes[1], nodes[2], &updates.commitment_signed, true);
955+
956+
let _updates = get_htlc_update_msgs!(nodes[1], nodes[0].node.get_our_node_id());
957+
958+
// Disconnect the peers before failing from 1 -> 0
959+
nodes[0].node.peer_disconnected(nodes[1].node.get_our_node_id());
960+
nodes[1].node.peer_disconnected(nodes[0].node.get_our_node_id());
961+
962+
let chan_0_monitor_serialized = get_monitor!(nodes[0], chan_id_1).encode();
963+
reload_node!(nodes[0], nodes[0].node.encode(), &[&chan_0_monitor_serialized], persister, new_chain_monitor, nodes_0_deserialized);
964+
965+
// While disconnected, connect blocks such that it goes past HTLC expiry. When nodes reconnect instead of FC the
966+
// channel, check that the HTLC was failed and channel is still open.
967+
let channel = nodes[0].node.list_channels().iter().find(|c| c.channel_id == chan_id_1).unwrap().clone();
968+
let htlc_expiry = channel.pending_outbound_htlcs.iter().find(|outbound_htlc| outbound_htlc.payment_hash == payment_hash).unwrap().cltv_expiry;
969+
let blocks_to_connect = 200;
970+
connect_blocks(&nodes[0], htlc_expiry - nodes[0].best_block_info().1 + blocks_to_connect);
971+
connect_blocks(&nodes[1], htlc_expiry - nodes[1].best_block_info().1 + blocks_to_connect);
972+
973+
let mut reconnect_args = ReconnectArgs::new(&nodes[0], &nodes[1]);
974+
reconnect_args.pending_htlc_fails.0 = 1;
975+
reconnect_nodes(reconnect_args);
976+
977+
expect_payment_failed!(nodes[0], payment_hash, true);
978+
979+
assert_eq!(nodes[0].node.list_channels().len(), 1);
980+
assert_eq!(nodes[1].node.list_channels().len(), 2);
981+
982+
assert_eq!(nodes[0].node.list_channels().iter().find(|chan| chan.channel_id == chan_id_1).unwrap().pending_outbound_htlcs.len(), 0);
983+
assert_eq!(nodes[1].node.list_channels().iter().find(|chan| chan.channel_id == chan_id_1).unwrap().pending_inbound_htlcs.len(), 0);
984+
}
985+
922986
fn do_forwarded_payment_no_manager_persistence(use_cs_commitment: bool, claim_htlc: bool, use_intercept: bool) {
923987
if !use_cs_commitment { assert!(!claim_htlc); }
924988
// If we go to forward a payment, and the ChannelMonitor persistence completes, but the

0 commit comments

Comments
 (0)