From: Matt Corallo Date: Wed, 1 May 2024 14:32:47 +0000 (+0000) Subject: Don't attempt to resume channels if we already exchanged funding X-Git-Url: http://git.bitcoin.ninja/index.cgi?a=commitdiff_plain;h=53cd31a2ece2e3abb04d649de8d63716de2dae65;hp=1d421d3362a88105b982d197312c0c64ba8e661e;p=rust-lightning Don't attempt to resume channels if we already exchanged funding ddf75afd16 introduced the ability to re-exchange our `ChannelOpen` after a peer disconnects if we didn't complete funding on our end. It did not implement nor consider what would happen if we re-connected after we created our own funding transactions, and currently it panics (and even if it did not it would replay the `FundingTransactionGenerated` event to users). While we'd very much like to replay the `open_channel` flow even if we have already received an `accept_channel` and funded the channel, we cannot as the peer will likely provide different key material in their `accept_channel`, causing us to need a different funding transaction. Thus, here, we simply close channels which have been funded but not yet signed when our peer disconnects. --- diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 5fbdb595..793d9af8 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -7520,6 +7520,12 @@ impl OutboundV1Channel where SP::Target: SignerProvider { Ok(self.get_open_channel(chain_hash)) } + /// Returns true if we can resume the channel by sending the [`msgs::OpenChannel`] again. + pub fn is_resumable(&self) -> bool { + !self.context.have_received_message() && + self.context.cur_holder_commitment_transaction_number == INITIAL_COMMITMENT_NUMBER + } + pub fn get_open_channel(&self, chain_hash: ChainHash) -> msgs::OpenChannel { if !self.context.is_outbound() { panic!("Tried to open a channel for an inbound channel?"); diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index f2b3bf15..f3e0ae5e 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -9954,11 +9954,14 @@ where } &mut chan.context }, - // We retain UnfundedOutboundV1 channel for some time in case - // peer unexpectedly disconnects, and intends to reconnect again. - ChannelPhase::UnfundedOutboundV1(_) => { - return true; - }, + // If we get disconnected and haven't yet committed to a funding + // transaction, we can replay the `open_channel` on reconnection, so don't + // bother dropping the channel here. However, if we already committed to + // the funding transaction we don't yet support replaying the funding + // handshake (and bailing if the peer rejects it), so we force-close in + // that case. + ChannelPhase::UnfundedOutboundV1(chan) if chan.is_resumable() => return true, + ChannelPhase::UnfundedOutboundV1(chan) => &mut chan.context, // Unfunded inbound channels will always be removed. ChannelPhase::UnfundedInboundV1(chan) => { &mut chan.context diff --git a/lightning/src/ln/functional_tests.rs b/lightning/src/ln/functional_tests.rs index 5b1420e3..3f9e263f 100644 --- a/lightning/src/ln/functional_tests.rs +++ b/lightning/src/ln/functional_tests.rs @@ -61,6 +61,38 @@ use crate::ln::chan_utils::CommitmentTransaction; use super::channel::UNFUNDED_CHANNEL_AGE_LIMIT_TICKS; +#[test] +fn test_channel_resumption_fail_post_funding() { + // If we fail to exchange funding with a peer prior to it disconnecting we'll resume the + // channel open on reconnect, however if we do exchange funding we do not currently support + // replaying it and here test that the channel closes. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + nodes[0].node.create_channel(nodes[1].node.get_our_node_id(), 1_000_000, 0, 42, None, None).unwrap(); + let open_chan = get_event_msg!(nodes[0], MessageSendEvent::SendOpenChannel, nodes[1].node.get_our_node_id()); + nodes[1].node.handle_open_channel(&nodes[0].node.get_our_node_id(), &open_chan); + let accept_chan = get_event_msg!(nodes[1], MessageSendEvent::SendAcceptChannel, nodes[0].node.get_our_node_id()); + nodes[0].node.handle_accept_channel(&nodes[1].node.get_our_node_id(), &accept_chan); + + let (temp_chan_id, tx, funding_output) = + create_funding_transaction(&nodes[0], &nodes[1].node.get_our_node_id(), 1_000_000, 42); + let new_chan_id = ChannelId::v1_from_funding_outpoint(funding_output); + nodes[0].node.funding_transaction_generated(&temp_chan_id, &nodes[1].node.get_our_node_id(), tx).unwrap(); + + nodes[0].node.peer_disconnected(&nodes[1].node.get_our_node_id()); + check_closed_events(&nodes[0], &[ExpectedCloseEvent::from_id_reason(new_chan_id, true, ClosureReason::DisconnectedPeer)]); + + // After ddf75afd16 we'd panic on reconnection if we exchanged funding info, so test that + // explicitly here. + nodes[0].node.peer_connected(&nodes[1].node.get_our_node_id(), &msgs::Init { + features: nodes[1].node.init_features(), networks: None, remote_network_address: None + }, true).unwrap(); + assert_eq!(nodes[0].node.get_and_clear_pending_msg_events(), Vec::new()); +} + #[test] fn test_insane_channel_opens() { // Stand up a network of 2 nodes @@ -3738,10 +3770,10 @@ fn test_peer_disconnected_before_funding_broadcasted() { nodes[0].node.timer_tick_occurred(); } - // Ensure that the channel is closed with `ClosureReason::HolderForceClosed` - // when the peers are disconnected and do not reconnect before the funding - // transaction is broadcasted. - check_closed_event!(&nodes[0], 2, ClosureReason::HolderForceClosed, true + // Ensure that the channel is closed with `ClosureReason::DisconnectedPeer` and a + // `DiscardFunding` event when the peers are disconnected and do not reconnect before the + // funding transaction is broadcasted. + check_closed_event!(&nodes[0], 2, ClosureReason::DisconnectedPeer, true , [nodes[1].node.get_our_node_id()], 1000000); check_closed_event!(&nodes[1], 1, ClosureReason::DisconnectedPeer, false , [nodes[0].node.get_our_node_id()], 1000000);