Skip to content

Commit a5dcdc1

Browse files
committed
Wipe splice state upon failed interactive funding construction
An interactive funding construction can be considered failed upon a disconnect or a `tx_abort` message. So far, we've consumed the `InteractiveTxConstructor` in the latter case, but not the former. Additionally, we may have splice-specific state that needs to be consumed as well to allow us to negotiate another splice later on. This commit ensures that we properly consume all splice and interactive funding state whenever possible upon a disconnect or `tx_abort`. The interactive funding state is safe to consume as long as we have either yet to reach `AwaitingSignatures`, or we have but `tx_signatures` has not been sent/received. In all of these cases, we also make sure to clear the quiescent state flag such that we're able to resume processing updates on the channel. The splice state is safe to consume as long as we don't have a pending `FundingNegotiation::AwaitingSignatures` with a `tx_signatures` sent/received and we don't have any negotiated candidates. Note that until splice RBF is supported, it is not currently possible to have any negotiated candidates with a pending interactive funding transaction.
1 parent 3e21ba3 commit a5dcdc1

File tree

3 files changed

+299
-36
lines changed

3 files changed

+299
-36
lines changed

lightning/src/ln/channel.rs

Lines changed: 126 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1686,27 +1686,27 @@ where
16861686
let logger = WithChannelContext::from(logger, &self.context(), None);
16871687
log_info!(logger, "Failed interactive transaction negotiation: {reason}");
16881688

1689-
let _interactive_tx_constructor = match &mut self.phase {
1689+
match &mut self.phase {
16901690
ChannelPhase::Undefined => unreachable!(),
1691-
ChannelPhase::UnfundedOutboundV1(_) | ChannelPhase::UnfundedInboundV1(_) => None,
1691+
ChannelPhase::UnfundedOutboundV1(_) | ChannelPhase::UnfundedInboundV1(_) => {},
16921692
ChannelPhase::UnfundedV2(pending_v2_channel) => {
1693-
pending_v2_channel.interactive_tx_constructor.take()
1693+
pending_v2_channel.interactive_tx_constructor.take();
1694+
},
1695+
ChannelPhase::Funded(funded_channel) => {
1696+
if funded_channel.should_reset_pending_splice_funding_negotiation().unwrap_or(true)
1697+
{
1698+
funded_channel.context.channel_state.clear_quiescent();
1699+
funded_channel
1700+
.pending_splice
1701+
.as_mut()
1702+
.and_then(|pending_splice| pending_splice.funding_negotiation.take());
1703+
} else {
1704+
debug_assert!(false, "We should never fail an interactive funding negotiation once we're exchanging tx_signatures");
1705+
}
1706+
if funded_channel.should_reset_pending_splice_state() {
1707+
funded_channel.pending_splice.take();
1708+
}
16941709
},
1695-
ChannelPhase::Funded(funded_channel) => funded_channel
1696-
.pending_splice
1697-
.as_mut()
1698-
.and_then(|pending_splice| pending_splice.funding_negotiation.take())
1699-
.and_then(|funding_negotiation| {
1700-
if let FundingNegotiation::ConstructingTransaction {
1701-
interactive_tx_constructor,
1702-
..
1703-
} = funding_negotiation
1704-
{
1705-
Some(interactive_tx_constructor)
1706-
} else {
1707-
None
1708-
}
1709-
}),
17101710
};
17111711

17121712
reason.into_tx_abort_msg(self.context().channel_id)
@@ -1818,11 +1818,11 @@ where
18181818
where
18191819
L::Target: Logger,
18201820
{
1821-
// This checks for and resets the interactive negotiation state by `take()`ing it from the channel.
1822-
// The existence of the `tx_constructor` indicates that we have not moved into the signing
1823-
// phase for this interactively constructed transaction and hence we have not exchanged
1824-
// `tx_signatures`. Either way, we never close the channel upon receiving a `tx_abort`:
1825-
// https://github.com/lightning/bolts/blob/247e83d/02-peer-protocol.md?plain=1#L574-L576
1821+
// If we have not sent a `tx_abort` message for this negotiation previously, we need to echo
1822+
// back a tx_abort message according to the spec:
1823+
// https://github.com/lightning/bolts/blob/247e83d/02-peer-protocol.md?plain=1#L560-L561
1824+
// For rationale why we echo back `tx_abort`:
1825+
// https://github.com/lightning/bolts/blob/247e83d/02-peer-protocol.md?plain=1#L578-L580
18261826
let should_ack = match &mut self.phase {
18271827
ChannelPhase::Undefined => unreachable!(),
18281828
ChannelPhase::UnfundedOutboundV1(_) | ChannelPhase::UnfundedInboundV1(_) => {
@@ -1832,19 +1832,37 @@ where
18321832
ChannelPhase::UnfundedV2(pending_v2_channel) => {
18331833
pending_v2_channel.interactive_tx_constructor.take().is_some()
18341834
},
1835-
ChannelPhase::Funded(funded_channel) => funded_channel
1836-
.pending_splice
1837-
.as_mut()
1838-
.and_then(|pending_splice| pending_splice.funding_negotiation.take())
1839-
.is_some(),
1835+
ChannelPhase::Funded(funded_channel) => {
1836+
if let Some(should_reset) =
1837+
funded_channel.should_reset_pending_splice_funding_negotiation()
1838+
{
1839+
if should_reset {
1840+
funded_channel.context.channel_state.clear_quiescent();
1841+
let has_funding_negotiation = funded_channel
1842+
.pending_splice
1843+
.as_mut()
1844+
.and_then(|pending_splice| pending_splice.funding_negotiation.take())
1845+
.is_some();
1846+
if funded_channel.should_reset_pending_splice_state() {
1847+
funded_channel.pending_splice.take();
1848+
}
1849+
1850+
// We may have still tracked the pending funding negotiation state, so we
1851+
// should ack with our own `tx_abort`.
1852+
has_funding_negotiation
1853+
} else {
1854+
return Err(ChannelError::close(
1855+
"Received tx_abort while awaiting tx_signatures exchange".to_owned(),
1856+
));
1857+
}
1858+
} else {
1859+
// We were not tracking the pending funding negotiation state anymore, likely
1860+
// due to a disconnection or already having sent our own `tx_abort`.
1861+
false
1862+
}
1863+
},
18401864
};
18411865

1842-
// NOTE: Since at this point we have not sent a `tx_abort` message for this negotiation
1843-
// previously (tx_constructor was `Some`), we need to echo back a tx_abort message according
1844-
// to the spec:
1845-
// https://github.com/lightning/bolts/blob/247e83d/02-peer-protocol.md?plain=1#L560-L561
1846-
// For rationale why we echo back `tx_abort`:
1847-
// https://github.com/lightning/bolts/blob/247e83d/02-peer-protocol.md?plain=1#L578-L580
18481866
Ok(should_ack.then(|| {
18491867
let logger = WithChannelContext::from(logger, &self.context(), None);
18501868
let reason =
@@ -2554,6 +2572,15 @@ impl FundingNegotiation {
25542572
}
25552573

25562574
impl PendingFunding {
2575+
fn can_abandon_funding_negotiation(&self) -> bool {
2576+
self.funding_negotiation
2577+
.as_ref()
2578+
.map(|funding_negotiation| {
2579+
!matches!(funding_negotiation, FundingNegotiation::AwaitingSignatures { .. })
2580+
})
2581+
.unwrap_or(true)
2582+
}
2583+
25572584
fn check_get_splice_locked<SP: Deref>(
25582585
&mut self, context: &ChannelContext<SP>, confirmed_funding_index: usize, height: u32,
25592586
) -> Option<msgs::SpliceLocked>
@@ -6739,6 +6766,33 @@ where
67396766
)
67406767
}
67416768

6769+
/// Returns `None` if there is no [`FundedChannel::pending_splice`], otherwise a boolean
6770+
/// indicating whether we should reset the splice's [`PendingFunding::funding_negotiation`].
6771+
fn should_reset_pending_splice_funding_negotiation(&self) -> Option<bool> {
6772+
self.pending_splice.as_ref().map(|pending_splice| {
6773+
if pending_splice.can_abandon_funding_negotiation() {
6774+
true
6775+
} else {
6776+
self.context
6777+
.interactive_tx_signing_session
6778+
.as_ref()
6779+
.map(|signing_session| {
6780+
signing_session.holder_tx_signatures().is_some()
6781+
|| signing_session.has_received_tx_signatures()
6782+
})
6783+
.unwrap_or_else(|| {
6784+
debug_assert!(false);
6785+
false
6786+
})
6787+
}
6788+
})
6789+
}
6790+
6791+
fn should_reset_pending_splice_state(&self) -> bool {
6792+
self.should_reset_pending_splice_funding_negotiation().unwrap_or(true)
6793+
&& self.pending_funding().is_empty()
6794+
}
6795+
67426796
#[rustfmt::skip]
67436797
fn check_remote_fee<F: Deref, L: Deref>(
67446798
channel_type: &ChannelTypeFeatures, fee_estimator: &LowerBoundedFeeEstimator<F>,
@@ -8864,7 +8918,21 @@ where
88648918
}
88658919
self.context.channel_state.clear_local_stfu_sent();
88668920
self.context.channel_state.clear_remote_stfu_sent();
8921+
if self.pending_splice.is_none() {
8922+
// If we were in quiescence but a splice was never negotiated, we shouldn't be
8923+
// quiescent anymore upon reconnecting.
8924+
self.context.channel_state.clear_quiescent();
8925+
}
8926+
}
8927+
8928+
if self.should_reset_pending_splice_funding_negotiation().unwrap_or(false) {
88678929
self.context.channel_state.clear_quiescent();
8930+
self.pending_splice
8931+
.as_mut()
8932+
.and_then(|pending_splice| pending_splice.funding_negotiation.take());
8933+
}
8934+
if self.should_reset_pending_splice_state() {
8935+
self.pending_splice.take();
88688936
}
88698937

88708938
self.context.channel_state.set_peer_disconnected();
@@ -13875,7 +13943,12 @@ where
1387513943
}
1387613944
channel_state.clear_local_stfu_sent();
1387713945
channel_state.clear_remote_stfu_sent();
13878-
channel_state.clear_quiescent();
13946+
if self.should_reset_pending_splice_funding_negotiation().unwrap_or(true) {
13947+
// If we were in quiescence but a splice was never negotiated, or the
13948+
// negotiation failed due to disconnecting, we shouldn't be quiescent
13949+
// anymore upon reconnecting.
13950+
channel_state.clear_quiescent();
13951+
}
1387913952
},
1388013953
ChannelState::FundingNegotiated(_)
1388113954
if self.context.interactive_tx_signing_session.is_some() => {},
@@ -14238,6 +14311,14 @@ where
1423814311
let holder_commitment_point_next = self.holder_commitment_point.next_point();
1423914312
let holder_commitment_point_pending_next = self.holder_commitment_point.pending_next_point;
1424014313

14314+
let pending_splice = if self.should_reset_pending_splice_state() {
14315+
None
14316+
} else {
14317+
// We don't have to worry about resetting the pending `FundingNegotiation` because we
14318+
// can only read `FundingNegotiation::AwaitingSignatures` variants anyway.
14319+
self.pending_splice.as_ref()
14320+
};
14321+
1424114322
write_tlv_fields!(writer, {
1424214323
(0, self.context.announcement_sigs, option),
1424314324
// minimum_depth and counterparty_selected_channel_reserve_satoshis used to have a
@@ -14286,7 +14367,7 @@ where
1428614367
(60, self.context.historical_scids, optional_vec), // Added in 0.2
1428714368
(61, fulfill_attribution_data, optional_vec), // Added in 0.2
1428814369
(63, holder_commitment_point_current, option), // Added in 0.2
14289-
(64, self.pending_splice, option), // Added in 0.2
14370+
(64, pending_splice, option), // Added in 0.2
1429014371
(65, self.quiescent_action, option), // Added in 0.2
1429114372
(67, pending_outbound_held_htlc_flags, optional_vec), // Added in 0.2
1429214373
(69, holding_cell_held_htlc_flags, optional_vec), // Added in 0.2
@@ -14950,6 +15031,15 @@ where
1495015031
}
1495115032
};
1495215033

15034+
if let Some(funding_negotiation) = pending_splice
15035+
.as_ref()
15036+
.and_then(|pending_splice| pending_splice.funding_negotiation.as_ref())
15037+
{
15038+
if !matches!(funding_negotiation, FundingNegotiation::AwaitingSignatures { .. }) {
15039+
return Err(DecodeError::InvalidValue);
15040+
}
15041+
}
15042+
1495315043
Ok(FundedChannel {
1495415044
funding: FundingScope {
1495515045
value_to_self_msat,

lightning/src/ln/quiescence_tests.rs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use crate::ln::functional_test_utils::*;
77
use crate::ln::msgs;
88
use crate::ln::msgs::{BaseMessageHandler, ChannelMessageHandler, ErrorAction, MessageSendEvent};
99
use crate::util::errors::APIError;
10+
use crate::util::ser::Writeable;
1011
use crate::util::test_channel_signer::SignerOp;
1112

1213
#[test]
@@ -699,3 +700,62 @@ fn test_quiescence_during_disconnection() {
699700
do_test_quiescence_during_disconnection(false, true);
700701
do_test_quiescence_during_disconnection(true, true);
701702
}
703+
704+
#[test]
705+
fn test_quiescence_termination_on_disconnect() {
706+
do_test_quiescence_termination_on_disconnect(false);
707+
do_test_quiescence_termination_on_disconnect(true);
708+
}
709+
710+
fn do_test_quiescence_termination_on_disconnect(reload: bool) {
711+
// Tests that we terminate quiescence on disconnect if there's no quiescence protocol taking place.
712+
let chanmon_cfgs = create_chanmon_cfgs(2);
713+
let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
714+
let (persister_0a, persister_1a);
715+
let (chain_monitor_0a, chain_monitor_1a);
716+
let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]);
717+
let (node_0a, node_1a);
718+
let mut nodes = create_network(2, &node_cfgs, &node_chanmgrs);
719+
720+
let node_id_0 = nodes[0].node.get_our_node_id();
721+
let node_id_1 = nodes[1].node.get_our_node_id();
722+
723+
let (_, _, channel_id, _) = create_announced_chan_between_nodes(&nodes, 0, 1);
724+
725+
nodes[0].node.maybe_propose_quiescence(&node_id_1, &channel_id).unwrap();
726+
727+
let stfu = get_event_msg!(nodes[0], MessageSendEvent::SendStfu, node_id_1);
728+
nodes[1].node.handle_stfu(node_id_0, &stfu);
729+
let stfu = get_event_msg!(nodes[1], MessageSendEvent::SendStfu, node_id_0);
730+
nodes[0].node.handle_stfu(node_id_1, &stfu);
731+
732+
if reload {
733+
let encoded_monitor_0 = get_monitor!(nodes[0], channel_id).encode();
734+
reload_node!(
735+
nodes[0],
736+
&nodes[0].node.encode(),
737+
&[&encoded_monitor_0],
738+
persister_0a,
739+
chain_monitor_0a,
740+
node_0a
741+
);
742+
let encoded_monitor_1 = get_monitor!(nodes[1], channel_id).encode();
743+
reload_node!(
744+
nodes[1],
745+
&nodes[1].node.encode(),
746+
&[&encoded_monitor_1],
747+
persister_1a,
748+
chain_monitor_1a,
749+
node_1a
750+
);
751+
} else {
752+
nodes[0].node.peer_disconnected(node_id_1);
753+
nodes[1].node.peer_disconnected(node_id_0);
754+
}
755+
756+
let mut reconnect_args = ReconnectArgs::new(&nodes[0], &nodes[1]);
757+
reconnect_args.send_channel_ready = (true, true);
758+
reconnect_nodes(reconnect_args);
759+
760+
send_payment(&nodes[0], &[&nodes[1]], 1_000_000);
761+
}

0 commit comments

Comments
 (0)