Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 21 additions & 8 deletions lightning/src/chain/channelmonitor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5899,26 +5899,39 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
// chain when our counterparty is waiting for expiration to off-chain fail an HTLC
// we give ourselves a few blocks of headroom after expiration before going
// on-chain for an expired HTLC.
let htlc_outbound = $holder_tx == htlc.offered;
if ( htlc_outbound && htlc.cltv_expiry + LATENCY_GRACE_PERIOD_BLOCKS <= height) ||
(!htlc_outbound && htlc.cltv_expiry <= height + CLTV_CLAIM_BUFFER && self.payment_preimages.contains_key(&htlc.payment_hash)) {
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);
return Some(htlc.payment_hash);
let htlc_outbound = $holder_tx == htlc.0.offered;
let has_incoming = if htlc_outbound {
if let Some(source) = htlc.1.as_deref() {
match *source {
HTLCSource::OutboundRoute { .. } => false,
HTLCSource::PreviousHopData(_) => true,
}
} else {
panic!("Every offered non-dust HTLC should have a corresponding source");
}
} else {
true
};
if (htlc_outbound && has_incoming && htlc.0.cltv_expiry + LATENCY_GRACE_PERIOD_BLOCKS <= height) ||
(htlc_outbound && !has_incoming && htlc.0.cltv_expiry + 2016 <= height) ||
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think per the issue, we shouldn't FC at all for these timed out outbound payments. I can't really see a downside to doing that since the user can FC manually or contact their counterparty out-of-band, though I could be missing something. It should be really rare/weird for the counterparty to not just fail the HTLC back on reconnect anyway.

Asked @wpaulino offline and he seemed to confirm this approach

(!htlc_outbound && htlc.0.cltv_expiry <= height + CLTV_CLAIM_BUFFER && self.payment_preimages.contains_key(&htlc.0.payment_hash)) {
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);
return Some(htlc.0.payment_hash);
}
}
}
}

scan_commitment!(holder_commitment_htlcs!(self, CURRENT), true);
scan_commitment!(holder_commitment_htlcs!(self, CURRENT_WITH_SOURCES), true);

if let Some(ref txid) = self.funding.current_counterparty_commitment_txid {
if let Some(ref htlc_outputs) = self.funding.counterparty_claimable_outpoints.get(txid) {
scan_commitment!(htlc_outputs.iter().map(|&(ref a, _)| a), false);
scan_commitment!(htlc_outputs.iter(), false);
}
}
if let Some(ref txid) = self.funding.prev_counterparty_commitment_txid {
if let Some(ref htlc_outputs) = self.funding.counterparty_claimable_outpoints.get(txid) {
scan_commitment!(htlc_outputs.iter().map(|&(ref a, _)| a), false);
scan_commitment!(htlc_outputs.iter(), false);
}
}

Expand Down
64 changes: 64 additions & 0 deletions lightning/src/ln/reload_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -919,6 +919,70 @@ fn test_partial_claim_before_restart() {
do_test_partial_claim_before_restart(true, true);
}

#[test]
fn test_no_fc_outgoing_payment() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice to test the async payment version. It should be easy to copy the held_htlc_timeout test and add a disconnect + connect extra blocks until the HTLC fully times out, and ensure the HTLC is failed back on disconnect without FC.

let chanmon_cfgs = create_chanmon_cfgs(3);
let node_cfgs = create_node_cfgs(3, &chanmon_cfgs);
let persister;
let new_chain_monitor;

let mut intercept_forwards_config = test_default_channel_config();
intercept_forwards_config.accept_intercept_htlcs = true;
let node_chanmgrs = create_node_chanmgrs(3, &node_cfgs, &[None, Some(intercept_forwards_config), None]);
let nodes_0_deserialized;

let mut nodes = create_network(3, &node_cfgs, &node_chanmgrs);

let chan_id_1 = create_announced_chan_between_nodes(&nodes, 0, 1).2;
let _chan_id_2 = create_announced_chan_between_nodes(&nodes, 1, 2).2;

// Send payment from 0 -> 1 -> 2 that will be failed back
let (_, payment_hash, ..) = route_payment(&nodes[0], &[&nodes[1], &nodes[2]], 10_000);

nodes[2].node.fail_htlc_backwards(&payment_hash);
expect_and_process_pending_htlcs_and_htlc_handling_failed(
&nodes[2],
&[HTLCHandlingFailureType::Receive { payment_hash }]
);
check_added_monitors!(nodes[2], 1);

// After committing the HTLCs, fail it back from 2 -> 1
let updates = get_htlc_update_msgs!(nodes[2], nodes[1].node.get_our_node_id());
assert_eq!(updates.update_fail_htlcs.len(), 1);

nodes[1].node.handle_update_fail_htlc(nodes[2].node.get_our_node_id(), &updates.update_fail_htlcs[0]);
commitment_signed_dance!(nodes[1], nodes[2], &updates.commitment_signed, true);

let _updates = get_htlc_update_msgs!(nodes[1], nodes[0].node.get_our_node_id());

// Disconnect the peers before failing from 1 -> 0
nodes[0].node.peer_disconnected(nodes[1].node.get_our_node_id());
nodes[1].node.peer_disconnected(nodes[0].node.get_our_node_id());

let chan_0_monitor_serialized = get_monitor!(nodes[0], chan_id_1).encode();
reload_node!(nodes[0], nodes[0].node.encode(), &[&chan_0_monitor_serialized], persister, new_chain_monitor, nodes_0_deserialized);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For my understanding, is the reload to make the test more realistic? I would think just disconnecting would be enough


// While disconnected, connect blocks such that it goes past HTLC expiry. When nodes reconnect instead of FC the
// channel, check that the HTLC was failed and channel is still open.
let channel = nodes[0].node.list_channels().iter().find(|c| c.channel_id == chan_id_1).unwrap().clone();
let htlc_expiry = channel.pending_outbound_htlcs.iter().find(|outbound_htlc| outbound_htlc.payment_hash == payment_hash).unwrap().cltv_expiry;
let blocks_to_connect = 200;
connect_blocks(&nodes[0], htlc_expiry - nodes[0].best_block_info().1 + blocks_to_connect);
connect_blocks(&nodes[1], htlc_expiry - nodes[1].best_block_info().1 + blocks_to_connect);

let mut reconnect_args = ReconnectArgs::new(&nodes[0], &nodes[1]);
reconnect_args.pending_htlc_fails.0 = 1;
reconnect_nodes(reconnect_args);

expect_payment_failed!(nodes[0], payment_hash, true);

assert_eq!(nodes[0].node.list_channels().len(), 1);
assert_eq!(nodes[1].node.list_channels().len(), 2);

assert_eq!(nodes[0].node.list_channels().iter().find(|chan| chan.channel_id == chan_id_1).unwrap().pending_outbound_htlcs.len(), 0);
assert_eq!(nodes[1].node.list_channels().iter().find(|chan| chan.channel_id == chan_id_1).unwrap().pending_inbound_htlcs.len(), 0);
}

fn do_forwarded_payment_no_manager_persistence(use_cs_commitment: bool, claim_htlc: bool, use_intercept: bool) {
if !use_cs_commitment { assert!(!claim_htlc); }
// If we go to forward a payment, and the ChannelMonitor persistence completes, but the
Expand Down
Loading