Skip to content

Commit a1fc862

Browse files
jkczyzclaude
andcommitted
Exit quiescence when tx_init_rbf is rejected with Abort
When tx_init_rbf is rejected with ChannelError::Abort (e.g., insufficient RBF feerate, negotiation in progress, feerate too high), the error is converted to a tx_abort message but quiescence is never exited and holding cells are never freed. This leaves the channel stuck in a quiescent state. Fix this by intercepting ChannelError::Abort before try_channel_entry! in internal_tx_init_rbf, calling exit_quiescence on the channel, and returning the error with exited_quiescence set so that handle_error frees holding cells. Also make exit_quiescence available in non-test builds by removing its cfg gate. Update tests to use the proper RBF initiation flow (with tampered feerates) so that handle_tx_abort correctly echoes the abort and exits quiescence, rather than manually crafting tx_init_rbf messages that leave node 0 without proper negotiation state. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 1098cc3 commit a1fc862

3 files changed

Lines changed: 63 additions & 20 deletions

File tree

lightning/src/ln/channel.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13851,8 +13851,6 @@ where
1385113851
Some(msgs::Stfu { channel_id: self.context.channel_id, initiator })
1385213852
}
1385313853

13854-
#[cfg(any(test, fuzzing, feature = "_test_utils"))]
13855-
#[rustfmt::skip]
1385613854
pub fn exit_quiescence(&mut self) -> bool {
1385713855
// Make sure we either finished the quiescence handshake and are quiescent, or we never
1385813856
// attempted to initiate quiescence at all.

lightning/src/ln/channelmanager.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13118,6 +13118,15 @@ This indicates a bug inside LDK. Please report this error at https://github.com/
1311813118
&self.fee_estimator,
1311913119
&self.logger,
1312013120
);
13121+
if let Err(ChannelError::Abort(_)) = &init_res {
13122+
funded_channel.exit_quiescence();
13123+
let chan_id = funded_channel.context.channel_id();
13124+
let res = MsgHandleErrInternal::from_chan_no_close(
13125+
init_res.unwrap_err(),
13126+
chan_id,
13127+
);
13128+
return Err(res.with_exited_quiescence(true));
13129+
}
1312113130
let tx_ack_rbf_msg = try_channel_entry!(self, peer_state, init_res, chan_entry);
1312213131
peer_state.pending_msg_events.push(MessageSendEvent::SendTxAckRbf {
1312313132
node_id: *counterparty_node_id,

lightning/src/ln/splicing_tests.rs

Lines changed: 54 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4404,33 +4404,54 @@ fn test_splice_rbf_insufficient_feerate() {
44044404
.is_ok());
44054405

44064406
// Acceptor-side: tx_init_rbf with an insufficient feerate is also rejected.
4407-
reenter_quiescence(&nodes[0], &nodes[1], &channel_id);
4407+
// Node 0 initiates a proper RBF but we tamper the feerate to be insufficient.
4408+
provide_utxo_reserves(&nodes, 2, added_value * 2);
4409+
let _funding_contribution =
4410+
do_initiate_rbf_splice_in(&nodes[0], &nodes[1], channel_id, added_value, min_rbf_feerate);
44084411

4409-
let tx_init_rbf = msgs::TxInitRbf {
4410-
channel_id,
4411-
locktime: 0,
4412-
feerate_sat_per_1000_weight: FEERATE_FLOOR_SATS_PER_KW,
4413-
funding_output_contribution: Some(added_value.to_sat() as i64),
4414-
};
4412+
let stfu_0 = get_event_msg!(nodes[0], MessageSendEvent::SendStfu, node_id_1);
4413+
nodes[1].node.handle_stfu(node_id_0, &stfu_0);
4414+
let stfu_1 = get_event_msg!(nodes[1], MessageSendEvent::SendStfu, node_id_0);
4415+
nodes[0].node.handle_stfu(node_id_1, &stfu_1);
44154416

4417+
let mut tx_init_rbf = get_event_msg!(nodes[0], MessageSendEvent::SendTxInitRbf, node_id_1);
4418+
tx_init_rbf.feerate_sat_per_1000_weight = FEERATE_FLOOR_SATS_PER_KW;
44164419
nodes[1].node.handle_tx_init_rbf(node_id_0, &tx_init_rbf);
44174420

44184421
let tx_abort = get_event_msg!(nodes[1], MessageSendEvent::SendTxAbort, node_id_0);
44194422
assert_eq!(tx_abort.channel_id, channel_id);
44204423

4424+
// Node 0 echoes tx_abort and exits quiescence.
4425+
nodes[0].node.handle_tx_abort(node_id_1, &tx_abort);
4426+
let tx_abort_echo = get_event_msg!(nodes[0], MessageSendEvent::SendTxAbort, node_id_1);
4427+
4428+
let events = nodes[0].node.get_and_clear_pending_events();
4429+
assert_eq!(events.len(), 2);
4430+
assert!(
4431+
matches!(&events[0], Event::SpliceFailed { channel_id: cid, .. } if *cid == channel_id)
4432+
);
4433+
assert!(
4434+
matches!(&events[1], Event::DiscardFunding { channel_id: cid, .. } if *cid == channel_id)
4435+
);
4436+
4437+
// Node 1 handles the echo (no-op since it already aborted).
4438+
nodes[1].node.handle_tx_abort(node_id_0, &tx_abort_echo);
4439+
44214440
// Acceptor-side: a counterparty feerate that satisfies the spec's 25/24 rule (264) is
44224441
// accepted, even though our own RBF floor (+25 sat/kwu = 278) is higher.
4423-
// After tx_abort the channel remains quiescent, so no need to re-enter quiescence.
4424-
nodes[0].node.handle_tx_abort(node_id_1, &tx_abort);
4442+
// Node 0 initiates another proper RBF but we tamper the feerate to the 25/24 value.
4443+
provide_utxo_reserves(&nodes, 2, added_value * 2);
4444+
let _funding_contribution =
4445+
do_initiate_rbf_splice_in(&nodes[0], &nodes[1], channel_id, added_value, min_rbf_feerate);
44254446

4426-
let rbf_feerate_25_24 = ((FEERATE_FLOOR_SATS_PER_KW as u64) * 25).div_ceil(24) as u32;
4427-
let tx_init_rbf = msgs::TxInitRbf {
4428-
channel_id,
4429-
locktime: 0,
4430-
feerate_sat_per_1000_weight: rbf_feerate_25_24,
4431-
funding_output_contribution: Some(added_value.to_sat() as i64),
4432-
};
4447+
let stfu_0 = get_event_msg!(nodes[0], MessageSendEvent::SendStfu, node_id_1);
4448+
nodes[1].node.handle_stfu(node_id_0, &stfu_0);
4449+
let stfu_1 = get_event_msg!(nodes[1], MessageSendEvent::SendStfu, node_id_0);
4450+
nodes[0].node.handle_stfu(node_id_1, &stfu_1);
44334451

4452+
let mut tx_init_rbf = get_event_msg!(nodes[0], MessageSendEvent::SendTxInitRbf, node_id_1);
4453+
let rbf_feerate_25_24 = ((FEERATE_FLOOR_SATS_PER_KW as u64) * 25).div_ceil(24) as u32;
4454+
tx_init_rbf.feerate_sat_per_1000_weight = rbf_feerate_25_24;
44344455
nodes[1].node.handle_tx_init_rbf(node_id_0, &tx_init_rbf);
44354456
let _tx_ack_rbf = get_event_msg!(nodes[1], MessageSendEvent::SendTxAckRbf, node_id_0);
44364457
}
@@ -5118,10 +5139,25 @@ fn test_splice_rbf_tiebreak_feerate_too_high_rejected() {
51185139
assert_eq!(tx_init_rbf.feerate_sat_per_1000_weight, high_feerate.to_sat_per_kwu() as u32);
51195140

51205141
// Node 1 handles tx_init_rbf — TooHigh: target (100k) >> max (3k) and fair fee > budget.
5142+
// Node 1 exits quiescence upon rejecting with tx_abort, and since it has a pending
5143+
// QuiescentAction (from its own splice RBF attempt), it immediately re-proposes quiescence.
51215144
nodes[1].node.handle_tx_init_rbf(node_id_0, &tx_init_rbf);
51225145

5123-
let tx_abort = get_event_msg!(nodes[1], MessageSendEvent::SendTxAbort, node_id_0);
5124-
assert_eq!(tx_abort.channel_id, channel_id);
5146+
let msg_events = nodes[1].node.get_and_clear_pending_msg_events();
5147+
assert_eq!(msg_events.len(), 2);
5148+
match &msg_events[0] {
5149+
MessageSendEvent::SendTxAbort { node_id, msg } => {
5150+
assert_eq!(*node_id, node_id_0);
5151+
assert_eq!(msg.channel_id, channel_id);
5152+
},
5153+
_ => panic!("Expected SendTxAbort, got {:?}", msg_events[0]),
5154+
};
5155+
match &msg_events[1] {
5156+
MessageSendEvent::SendStfu { node_id, .. } => {
5157+
assert_eq!(*node_id, node_id_0);
5158+
},
5159+
_ => panic!("Expected SendStfu, got {:?}", msg_events[1]),
5160+
};
51255161
}
51265162

51275163
#[test]

0 commit comments

Comments
 (0)