Skip to content

Commit d82abc2

Browse files
committed
Emit SpliceFailed event during channel shutdown
When a channel has a pending splice and is shutdown, generate a SpliceFailed event when necessary. This allows users to reclaim any contributed UTXOs.
1 parent 8852e8e commit d82abc2

File tree

7 files changed

+187
-2
lines changed

7 files changed

+187
-2
lines changed

lightning/src/ln/channel.rs

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1191,6 +1191,9 @@ pub(crate) struct ShutdownResult {
11911191
pub(crate) unbroadcasted_funding_tx: Option<Transaction>,
11921192
pub(crate) channel_funding_txo: Option<OutPoint>,
11931193
pub(crate) last_local_balance_msat: u64,
1194+
/// If a splice was in progress when the channel was shut down, this contains
1195+
/// the splice funding information for emitting a SpliceFailed event.
1196+
pub(crate) splice_funding_failed: Option<SpliceFundingFailed>,
11941197
}
11951198

11961199
/// Tracks the transaction number, along with current and next commitment points.
@@ -2686,6 +2689,15 @@ pub(crate) struct SpliceInstructions {
26862689
locktime: u32,
26872690
}
26882691

2692+
impl SpliceInstructions {
2693+
fn into_contributed_inputs_and_outputs(self) -> (Vec<bitcoin::OutPoint>, Vec<TxOut>) {
2694+
(
2695+
self.our_funding_inputs.into_iter().map(|input| input.utxo.outpoint).collect(),
2696+
self.our_funding_outputs,
2697+
)
2698+
}
2699+
}
2700+
26892701
impl_writeable_tlv_based!(SpliceInstructions, {
26902702
(1, adjusted_funding_contribution, required),
26912703
(3, our_funding_inputs, required_vec),
@@ -6040,6 +6052,7 @@ where
60406052
is_manual_broadcast: self.is_manual_broadcast,
60416053
channel_funding_txo: funding.get_funding_txo(),
60426054
last_local_balance_msat: funding.value_to_self_msat,
6055+
splice_funding_failed: None,
60436056
}
60446057
}
60456058

@@ -6824,7 +6837,38 @@ where
68246837
}
68256838

68266839
pub fn force_shutdown(&mut self, closure_reason: ClosureReason) -> ShutdownResult {
6827-
self.context.force_shutdown(&self.funding, closure_reason)
6840+
let splice_funding_failed =
6841+
if matches!(self.context.channel_state, ChannelState::ChannelReady(_)) {
6842+
if self.should_reset_pending_splice_state() {
6843+
self.reset_pending_splice_state()
6844+
} else {
6845+
match self.quiescent_action.take() {
6846+
Some(QuiescentAction::Splice(instructions)) => {
6847+
self.context.channel_state.clear_awaiting_quiescence();
6848+
let (inputs, outputs) =
6849+
instructions.into_contributed_inputs_and_outputs();
6850+
Some(SpliceFundingFailed {
6851+
funding_txo: None,
6852+
channel_type: None,
6853+
contributed_inputs: inputs,
6854+
contributed_outputs: outputs,
6855+
})
6856+
},
6857+
#[cfg(any(test, fuzzing))]
6858+
Some(quiescent_action) => {
6859+
self.quiescent_action = Some(quiescent_action);
6860+
None
6861+
},
6862+
None => None,
6863+
}
6864+
}
6865+
} else {
6866+
None
6867+
};
6868+
6869+
let mut shutdown_result = self.context.force_shutdown(&self.funding, closure_reason);
6870+
shutdown_result.splice_funding_failed = splice_funding_failed;
6871+
shutdown_result
68286872
}
68296873

68306874
fn interactive_tx_constructor_mut(&mut self) -> Option<&mut InteractiveTxConstructor> {
@@ -10372,6 +10416,7 @@ where
1037210416
is_manual_broadcast: self.context.is_manual_broadcast,
1037310417
channel_funding_txo: self.funding.get_funding_txo(),
1037410418
last_local_balance_msat: self.funding.value_to_self_msat,
10419+
splice_funding_failed: None,
1037510420
}
1037610421
}
1037710422

lightning/src/ln/channelmanager.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4536,6 +4536,18 @@ where
45364536
last_local_balance_msat: Some(shutdown_res.last_local_balance_msat),
45374537
}, None));
45384538

4539+
if let Some(splice_funding_failed) = shutdown_res.splice_funding_failed.take() {
4540+
pending_events.push_back((events::Event::SpliceFailed {
4541+
channel_id: shutdown_res.channel_id,
4542+
counterparty_node_id: shutdown_res.counterparty_node_id,
4543+
user_channel_id: shutdown_res.user_channel_id,
4544+
abandoned_funding_txo: splice_funding_failed.funding_txo,
4545+
channel_type: splice_funding_failed.channel_type,
4546+
contributed_inputs: splice_funding_failed.contributed_inputs,
4547+
contributed_outputs: splice_funding_failed.contributed_outputs,
4548+
}, None));
4549+
}
4550+
45394551
if let Some(transaction) = shutdown_res.unbroadcasted_funding_tx {
45404552
let funding_info = if shutdown_res.is_manual_broadcast {
45414553
FundingInfo::OutPoint {

lightning/src/ln/functional_test_utils.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2151,6 +2151,7 @@ pub struct ExpectedCloseEvent {
21512151
pub channel_id: Option<ChannelId>,
21522152
pub counterparty_node_id: Option<PublicKey>,
21532153
pub discard_funding: bool,
2154+
pub splice_failed: bool,
21542155
pub reason: Option<ClosureReason>,
21552156
pub channel_funding_txo: Option<OutPoint>,
21562157
pub user_channel_id: Option<u128>,
@@ -2165,6 +2166,7 @@ impl ExpectedCloseEvent {
21652166
channel_id: Some(channel_id),
21662167
counterparty_node_id: None,
21672168
discard_funding,
2169+
splice_failed: false,
21682170
reason: Some(reason),
21692171
channel_funding_txo: None,
21702172
user_channel_id: None,
@@ -2176,8 +2178,14 @@ impl ExpectedCloseEvent {
21762178
pub fn check_closed_events(node: &Node, expected_close_events: &[ExpectedCloseEvent]) {
21772179
let closed_events_count = expected_close_events.len();
21782180
let discard_events_count = expected_close_events.iter().filter(|e| e.discard_funding).count();
2181+
let splice_events_count = expected_close_events.iter().filter(|e| e.splice_failed).count();
21792182
let events = node.node.get_and_clear_pending_events();
2180-
assert_eq!(events.len(), closed_events_count + discard_events_count, "{:?}", events);
2183+
assert_eq!(
2184+
events.len(),
2185+
closed_events_count + discard_events_count + splice_events_count,
2186+
"{:?}",
2187+
events
2188+
);
21812189
for expected_event in expected_close_events {
21822190
assert!(events.iter().any(|e| matches!(
21832191
e,
@@ -2207,6 +2215,10 @@ pub fn check_closed_events(node: &Node, expected_close_events: &[ExpectedCloseEv
22072215
events.iter().filter(|e| matches!(e, Event::DiscardFunding { .. },)).count(),
22082216
discard_events_count
22092217
);
2218+
assert_eq!(
2219+
events.iter().filter(|e| matches!(e, Event::SpliceFailed { .. },)).count(),
2220+
splice_events_count
2221+
);
22102222
}
22112223

22122224
/// Check that a channel's closing channel events has been issued
@@ -2228,6 +2240,7 @@ pub fn check_closed_event(
22282240
channel_id: None,
22292241
counterparty_node_id: Some(*node_id),
22302242
discard_funding: is_check_discard_funding,
2243+
splice_failed: false,
22312244
reason: Some(expected_reason.clone()),
22322245
channel_funding_txo: None,
22332246
user_channel_id: None,

lightning/src/ln/monitor_tests.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1467,6 +1467,7 @@ fn do_test_revoked_counterparty_commitment_balances(keyed_anchors: bool, p2a_anc
14671467
channel_id: Some(chan_id),
14681468
counterparty_node_id: Some(nodes[0].node.get_our_node_id()),
14691469
discard_funding: false,
1470+
splice_failed: false,
14701471
reason: None, // Could be due to any HTLC timing out, so don't bother checking
14711472
channel_funding_txo: None,
14721473
user_channel_id: None,

lightning/src/ln/reorg_tests.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -504,6 +504,7 @@ fn test_set_outpoints_partial_claiming() {
504504
channel_id: Some(chan.2),
505505
counterparty_node_id: Some(nodes[0].node.get_our_node_id()),
506506
discard_funding: false,
507+
splice_failed: false,
507508
reason: None, // Could be due to either HTLC timing out, so don't bother checking
508509
channel_funding_txo: None,
509510
user_channel_id: None,

lightning/src/ln/shutdown_tests.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -636,6 +636,7 @@ fn do_htlc_fail_async_shutdown(blinded_recipient: bool) {
636636
channel_id: None,
637637
counterparty_node_id: Some(node_a_id),
638638
discard_funding: false,
639+
splice_failed: false,
639640
reason: Some(ClosureReason::LocallyInitiatedCooperativeClosure),
640641
channel_funding_txo: None,
641642
user_channel_id: None,
@@ -645,6 +646,7 @@ fn do_htlc_fail_async_shutdown(blinded_recipient: bool) {
645646
channel_id: None,
646647
counterparty_node_id: Some(node_c_id),
647648
discard_funding: false,
649+
splice_failed: false,
648650
reason: Some(ClosureReason::CounterpartyInitiatedCooperativeClosure),
649651
channel_funding_txo: None,
650652
user_channel_id: None,

lightning/src/ln/splicing_tests.rs

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1281,3 +1281,114 @@ fn fail_splice_on_tx_abort() {
12811281
let tx_abort = get_event_msg!(initiator, MessageSendEvent::SendTxAbort, node_id_acceptor);
12821282
acceptor.node.handle_tx_abort(node_id_initiator, &tx_abort);
12831283
}
1284+
1285+
#[test]
1286+
fn fail_splice_on_channel_close() {
1287+
let chanmon_cfgs = create_chanmon_cfgs(2);
1288+
let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
1289+
let config = test_default_anchors_channel_config();
1290+
let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]);
1291+
let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
1292+
1293+
let initiator = &nodes[0];
1294+
let acceptor = &nodes[1];
1295+
1296+
let _node_id_initiator = initiator.node.get_our_node_id();
1297+
let node_id_acceptor = acceptor.node.get_our_node_id();
1298+
1299+
let initial_channel_capacity = 100_000;
1300+
let (_, _, channel_id, _) =
1301+
create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_capacity, 0);
1302+
1303+
let coinbase_tx = provide_anchor_reserves(&nodes);
1304+
let splice_in_amount = initial_channel_capacity / 2;
1305+
let contribution = SpliceContribution::SpliceIn {
1306+
value: Amount::from_sat(splice_in_amount),
1307+
inputs: vec![FundingTxInput::new_p2wpkh(coinbase_tx, 0).unwrap()],
1308+
change_script: Some(nodes[0].wallet_source.get_change_script().unwrap()),
1309+
};
1310+
1311+
// Close the channel before completion of interactive-tx construction.
1312+
let _ = complete_splice_handshake(initiator, acceptor, channel_id, contribution.clone());
1313+
let _tx_add_input =
1314+
get_event_msg!(initiator, MessageSendEvent::SendTxAddInput, node_id_acceptor);
1315+
1316+
initiator
1317+
.node
1318+
.force_close_broadcasting_latest_txn(&channel_id, &node_id_acceptor, "test".to_owned())
1319+
.unwrap();
1320+
handle_bump_events(initiator, true, 0);
1321+
check_closed_events(
1322+
&nodes[0],
1323+
&[ExpectedCloseEvent {
1324+
channel_id: Some(channel_id),
1325+
discard_funding: false,
1326+
splice_failed: true,
1327+
channel_funding_txo: None,
1328+
user_channel_id: Some(42),
1329+
..Default::default()
1330+
}],
1331+
);
1332+
check_closed_broadcast(&nodes[0], 1, true);
1333+
check_added_monitors(&nodes[0], 1);
1334+
}
1335+
1336+
#[test]
1337+
fn fail_quiescent_action_on_channel_close() {
1338+
let chanmon_cfgs = create_chanmon_cfgs(2);
1339+
let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
1340+
let config = test_default_anchors_channel_config();
1341+
let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]);
1342+
let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
1343+
1344+
let initiator = &nodes[0];
1345+
let acceptor = &nodes[1];
1346+
1347+
let _node_id_initiator = initiator.node.get_our_node_id();
1348+
let node_id_acceptor = acceptor.node.get_our_node_id();
1349+
1350+
let initial_channel_capacity = 100_000;
1351+
let (_, _, channel_id, _) =
1352+
create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_capacity, 0);
1353+
1354+
let coinbase_tx = provide_anchor_reserves(&nodes);
1355+
let splice_in_amount = initial_channel_capacity / 2;
1356+
let contribution = SpliceContribution::SpliceIn {
1357+
value: Amount::from_sat(splice_in_amount),
1358+
inputs: vec![FundingTxInput::new_p2wpkh(coinbase_tx, 0).unwrap()],
1359+
change_script: Some(nodes[0].wallet_source.get_change_script().unwrap()),
1360+
};
1361+
1362+
// Close the channel before completion of STFU handshake.
1363+
initiator
1364+
.node
1365+
.splice_channel(
1366+
&channel_id,
1367+
&node_id_acceptor,
1368+
contribution,
1369+
FEERATE_FLOOR_SATS_PER_KW,
1370+
None,
1371+
)
1372+
.unwrap();
1373+
1374+
let _stfu_init = get_event_msg!(initiator, MessageSendEvent::SendStfu, node_id_acceptor);
1375+
1376+
initiator
1377+
.node
1378+
.force_close_broadcasting_latest_txn(&channel_id, &node_id_acceptor, "test".to_owned())
1379+
.unwrap();
1380+
handle_bump_events(initiator, true, 0);
1381+
check_closed_events(
1382+
&nodes[0],
1383+
&[ExpectedCloseEvent {
1384+
channel_id: Some(channel_id),
1385+
discard_funding: false,
1386+
splice_failed: true,
1387+
channel_funding_txo: None,
1388+
user_channel_id: Some(42),
1389+
..Default::default()
1390+
}],
1391+
);
1392+
check_closed_broadcast(&nodes[0], 1, true);
1393+
check_added_monitors(&nodes[0], 1);
1394+
}

0 commit comments

Comments
 (0)