Return InteractiveTxMsgError from splice_init and tx_init_rbf
What changed, and why it matters
This commit is a code-quality and correctness refactor for the Lightning Dev Kit's handling of splicing and RBF (fee-bump) negotiations. It makes error handling more consistent so that when a channel negotiation fails, the channel properly exits 'quiescence' (a paused state used during splicing) instead of getting stuck. The commit also adds tests confirming that misbehaving peers who send splice or RBF messages before quiescence are established receive a warning and are disconnected. There is no direct evidence this fixes an active exploit, but it removes a class of state-consistency bugs that could lead to stuck channels or incorrect behavior.
Review and merge if part of a normal release cycle. Monitor for related follow-up commits that address any remaining edge cases in quiescence state management during splicing/RBF.
Security signals we found
Refactors error handling to ensure quiescence is exited consistently on abort failures
Removes manual interception of ChannelError::Abort in channelmanager, reducing risk of inconsistent state
Adds regression tests for pre-quiescence splice_init and tx_init_rbf handling
No explicit security advisory, CVE, or vendor security disclosure present in commit or references
Evidence from the diff
The commit changes splice_init and tx_init_rbf in channel.rs to return InteractiveTxMsgError instead of ChannelError. A new helper, quiescent_negotiation_err, automatically exits quiescence when the error is ChannelError::Abort and passes other variants through unchanged. In channelmanager.rs, a new handle_interactive_tx_msg_err helper deduplicates error handling across internal_tx_msg, internal_splice_init, internal_tx_init_rbf, and internal_tx_complete. Previously, the channelmanager manually intercepted ChannelError::Abort for splice_init and tx_init_rbf to exit quiescence; now the channel methods signal this themselves. Two new tests verify that receiving splice_init or tx_init_rbf before quiescence is complete results in a warning and disconnect.
Changed components
lightning/src/ln/channel.rslightning/src/ln/channelmanager.rslightning/src/ln/splicing_tests.rsInspect captured patch +255 / −164
diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs
index e4f2466..93ef2b8 100644
--- a/lightning/src/ln/channel.rs
+++ b/lightning/src/ln/channel.rs
@@ -12723,9 +12723,7 @@ where
}
/// Checks during handling splice_init
- pub fn validate_splice_init(
- &self, msg: &msgs::SpliceInit, our_funding_contribution: SignedAmount,
- ) -> Result<FundingScope, ChannelError> {
+ pub fn validate_splice_init(&self, msg: &msgs::SpliceInit) -> Result<(), ChannelError> {
if self.holder_commitment_point.current_point().is_none() {
return Err(ChannelError::WarnAndDisconnect(format!(
"Channel {} commitment point needs to be advanced once before spliced",
@@ -12762,32 +12760,7 @@ where
)));
}
- self.validate_splice_contributions(our_funding_contribution, their_funding_contribution)
- .map_err(|e| ChannelError::WarnAndDisconnect(e))?;
-
- // Rotate the pubkeys using the prev_funding_txid as a tweak
- let prev_funding_txid = self.funding.get_funding_txid();
- let funding_pubkey = match prev_funding_txid {
- None => {
- debug_assert!(false);
- self.funding.get_holder_pubkeys().funding_pubkey
- },
- Some(prev_funding_txid) => self
- .context
- .holder_signer
- .new_funding_pubkey(prev_funding_txid, &self.context.secp_ctx),
- };
- let mut new_keys = self.funding.get_holder_pubkeys().clone();
- new_keys.funding_pubkey = funding_pubkey;
-
- Ok(FundingScope::for_splice(
- &self.funding,
- &self.context,
- our_funding_contribution,
- their_funding_contribution,
- msg.funding_pubkey,
- new_keys,
- ))
+ Ok(())
}
fn validate_splice_contributions(
@@ -12927,17 +12900,46 @@ where
pub(crate) fn splice_init<ES: EntropySource, L: Logger>(
&mut self, msg: &msgs::SpliceInit, entropy_source: &ES, holder_node_id: &PublicKey,
logger: &L,
- ) -> Result<msgs::SpliceAck, ChannelError> {
+ ) -> Result<msgs::SpliceAck, InteractiveTxMsgError> {
+ self.validate_splice_init(msg).map_err(|e| self.quiescent_negotiation_err(e))?;
+
let feerate = FeeRate::from_sat_per_kwu(msg.funding_feerate_per_kw as u64);
- let (our_funding_contribution, holder_balance) =
- self.resolve_queued_contribution(feerate, logger)?;
+ let (queued_net_value, holder_balance) = self
+ .resolve_queued_contribution(feerate, logger)
+ .map_err(|e| self.quiescent_negotiation_err(e))?;
+
+ let our_funding_contribution = queued_net_value.unwrap_or(SignedAmount::ZERO);
+ let their_funding_contribution = SignedAmount::from_sat(msg.funding_contribution_satoshis);
+ self.validate_splice_contributions(our_funding_contribution, their_funding_contribution)
+ .map_err(|e| self.quiescent_negotiation_err(ChannelError::WarnAndDisconnect(e)))?;
- let splice_funding =
- self.validate_splice_init(msg, our_funding_contribution.unwrap_or(SignedAmount::ZERO))?;
+ // Rotate the pubkeys using the prev_funding_txid as a tweak
+ let prev_funding_txid = self.funding.get_funding_txid();
+ let funding_pubkey = match prev_funding_txid {
+ None => {
+ debug_assert!(false);
+ self.funding.get_holder_pubkeys().funding_pubkey
+ },
+ Some(prev_funding_txid) => self
+ .context
+ .holder_signer
+ .new_funding_pubkey(prev_funding_txid, &self.context.secp_ctx),
+ };
+ let mut holder_pubkeys = self.funding.get_holder_pubkeys().clone();
+ holder_pubkeys.funding_pubkey = funding_pubkey;
+
+ let splice_funding = FundingScope::for_splice(
+ &self.funding,
+ &self.context,
+ our_funding_contribution,
+ their_funding_contribution,
+ msg.funding_pubkey,
+ holder_pubkeys,
+ );
// Adjust for the feerate and clone so we can store it for future RBF re-use.
let (adjusted_contribution, our_funding_inputs, our_funding_outputs) =
- if our_funding_contribution.is_some() {
+ if queued_net_value.is_some() {
let adjusted_contribution = self
.take_queued_funding_contribution()
.expect("queued_funding_contribution was Some")
@@ -12948,7 +12950,6 @@ where
} else {
(None, Default::default(), Default::default())
};
- let our_funding_contribution = our_funding_contribution.unwrap_or(SignedAmount::ZERO);
log_info!(
logger,
@@ -12991,9 +12992,8 @@ where
/// Checks during handling tx_init_rbf for an existing splice
fn validate_tx_init_rbf<F: FeeEstimator>(
- &self, msg: &msgs::TxInitRbf, our_funding_contribution: SignedAmount,
- fee_estimator: &LowerBoundedFeeEstimator<F>,
- ) -> Result<FundingScope, ChannelError> {
+ &self, msg: &msgs::TxInitRbf, fee_estimator: &LowerBoundedFeeEstimator<F>,
+ ) -> Result<(ChannelPublicKeys, PublicKey), ChannelError> {
if self.holder_commitment_point.current_point().is_none() {
return Err(ChannelError::WarnAndDisconnect(format!(
"Channel {} commitment point needs to be advanced once before RBF",
@@ -13059,36 +13059,26 @@ where
return Err(ChannelError::Abort(AbortReason::InsufficientRbfFeerate));
}
- let their_funding_contribution = match msg.funding_output_contribution {
- Some(value) => SignedAmount::from_sat(value),
- None => SignedAmount::ZERO,
- };
-
- self.validate_splice_contributions(our_funding_contribution, their_funding_contribution)
- .map_err(|e| ChannelError::WarnAndDisconnect(e))?;
-
// Reuse funding pubkeys from the last negotiated candidate since all RBF candidates
// for the same splice share the same funding output script.
- let holder_pubkeys = last_candidate.get_holder_pubkeys().clone();
- let counterparty_funding_pubkey = *last_candidate.counterparty_funding_pubkey();
-
- Ok(FundingScope::for_splice(
- &self.funding,
- &self.context,
- our_funding_contribution,
- their_funding_contribution,
- counterparty_funding_pubkey,
- holder_pubkeys,
+ Ok((
+ last_candidate.get_holder_pubkeys().clone(),
+ *last_candidate.counterparty_funding_pubkey(),
))
}
pub(crate) fn tx_init_rbf<ES: EntropySource, F: FeeEstimator, L: Logger>(
&mut self, msg: &msgs::TxInitRbf, entropy_source: &ES, holder_node_id: &PublicKey,
fee_estimator: &LowerBoundedFeeEstimator<F>, logger: &L,
- ) -> Result<msgs::TxAckRbf, ChannelError> {
+ ) -> Result<msgs::TxAckRbf, InteractiveTxMsgError> {
+ let (holder_pubkeys, counterparty_funding_pubkey) = self
+ .validate_tx_init_rbf(msg, fee_estimator)
+ .map_err(|e| self.quiescent_negotiation_err(e))?;
+
let feerate = FeeRate::from_sat_per_kwu(msg.feerate_sat_per_1000_weight as u64);
- let (queued_net_value, holder_balance) =
- self.resolve_queued_contribution(feerate, logger)?;
+ let (queued_net_value, holder_balance) = self
+ .resolve_queued_contribution(feerate, logger)
+ .map_err(|e| self.quiescent_negotiation_err(e))?;
// If no queued contribution, try prior contribution from previous negotiation.
// Failing here means the RBF would erase our splice — reject it.
@@ -13105,19 +13095,31 @@ where
prior
.net_value_for_acceptor_at_feerate(feerate, holder_balance)
.map_err(|_| ChannelError::Abort(AbortReason::InsufficientRbfFeerate))
- })?;
+ })
+ .map_err(|e| self.quiescent_negotiation_err(e))?;
Some(net_value)
} else {
None
};
let our_funding_contribution = queued_net_value.or(prior_net_value);
+ let our_funding_contribution = our_funding_contribution.unwrap_or(SignedAmount::ZERO);
- let rbf_funding = self.validate_tx_init_rbf(
- msg,
- our_funding_contribution.unwrap_or(SignedAmount::ZERO),
- fee_estimator,
- )?;
+ let their_funding_contribution = match msg.funding_output_contribution {
+ Some(value) => SignedAmount::from_sat(value),
+ None => SignedAmount::ZERO,
+ };
+ self.validate_splice_contributions(our_funding_contribution, their_funding_contribution)
+ .map_err(|e| self.quiescent_negotiation_err(ChannelError::WarnAndDisconnect(e)))?;
+
+ let rbf_funding = FundingScope::for_splice(
+ &self.funding,
+ &self.context,
+ our_funding_contribution,
+ their_funding_contribution,
+ counterparty_funding_pubkey,
+ holder_pubkeys,
+ );
// Consume the appropriate contribution source.
let (our_funding_inputs, our_funding_outputs) = if queued_net_value.is_some() {
@@ -13154,8 +13156,6 @@ where
Default::default()
};
- let our_funding_contribution = our_funding_contribution.unwrap_or(SignedAmount::ZERO);
-
log_info!(
logger,
"Starting RBF funding negotiation for channel {} after receiving tx_init_rbf; channel value: {} sats",
@@ -14285,6 +14285,16 @@ where
was_quiescent
}
+ fn quiescent_negotiation_err(&mut self, err: ChannelError) -> InteractiveTxMsgError {
+ let exited_quiescence = if matches!(err, ChannelError::Abort(_)) {
+ debug_assert!(self.context.channel_state.is_quiescent());
+ self.exit_quiescence()
+ } else {
+ false
+ };
+ InteractiveTxMsgError { err, splice_funding_failed: None, exited_quiescence }
+ }
+
pub fn remove_legacy_scids_before_block(&mut self, height: u32) -> alloc::vec::Drain<'_, u64> {
let end = self
.funding
diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs
index ae027da..7ea1497 100644
--- a/lightning/src/ln/channelmanager.rs
+++ b/lightning/src/ln/channelmanager.rs
@@ -11980,6 +11980,39 @@ This indicates a bug inside LDK. Please report this error at https://github.com/
}
}
+ fn handle_interactive_tx_msg_err(
+ &self, err: InteractiveTxMsgError, channel_id: ChannelId, counterparty_node_id: &PublicKey,
+ user_channel_id: u128,
+ ) -> MsgHandleErrInternal {
+ if let Some(splice_funding_failed) = err.splice_funding_failed {
+ let pending_events = &mut self.pending_events.lock().unwrap();
+ pending_events.push_back((
+ events::Event::SpliceFailed {
+ channel_id,
+ counterparty_node_id: *counterparty_node_id,
+ user_channel_id,
+ abandoned_funding_txo: splice_funding_failed.funding_txo,
+ channel_type: splice_funding_failed.channel_type.clone(),
+ },
+ None,
+ ));
+ pending_events.push_back((
+ events::Event::DiscardFunding {
+ channel_id,
+ funding_info: FundingInfo::Contribution {
+ inputs: splice_funding_failed.contributed_inputs,
+ outputs: splice_funding_failed.contributed_outputs,
+ },
+ },
+ None,
+ ));
+ }
+ debug_assert!(!err.exited_quiescence || matches!(err.err, ChannelError::Abort(_)));
+
+ MsgHandleErrInternal::from_chan_no_close(err.err, channel_id)
+ .with_exited_quiescence(err.exited_quiescence)
+ }
+
fn internal_tx_msg<
HandleTxMsgFn: Fn(&mut Channel<SP>) -> Result<InteractiveTxMessageSend, InteractiveTxMsgError>,
>(
@@ -12001,38 +12034,14 @@ This indicates a bug inside LDK. Please report this error at https://github.com/
peer_state.pending_msg_events.push(msg_send_event);
Ok(())
},
- Err(InteractiveTxMsgError {
- err,
- splice_funding_failed,
- exited_quiescence,
- }) => {
- if let Some(splice_funding_failed) = splice_funding_failed {
- let pending_events = &mut self.pending_events.lock().unwrap();
- pending_events.push_back((
- events::Event::SpliceFailed {
- channel_id,
- counterparty_node_id: *counterparty_node_id,
- user_channel_id: channel.context().get_user_id(),
- abandoned_funding_txo: splice_funding_failed.funding_txo,
- channel_type: splice_funding_failed.channel_type.clone(),
- },
- None,
- ));
- pending_events.push_back((
- events::Event::DiscardFunding {
- channel_id,
- funding_info: FundingInfo::Contribution {
- inputs: splice_funding_failed.contributed_inputs,
- outputs: splice_funding_failed.contributed_outputs,
- },
- },
- None,
- ));
- }
- debug_assert!(!exited_quiescence || matches!(err, ChannelError::Abort(_)));
-
- Err(MsgHandleErrInternal::from_chan_no_close(err, channel_id)
- .with_exited_quiescence(exited_quiescence))
+ Err(err) => {
+ let user_channel_id = channel.context().get_user_id();
+ Err(self.handle_interactive_tx_msg_err(
+ err,
+ channel_id,
+ counterparty_node_id,
+ user_channel_id,
+ ))
},
}
},
@@ -12160,38 +12169,14 @@ This indicates a bug inside LDK. Please report this error at https://github.com/
Ok(())
},
- Err(InteractiveTxMsgError {
- err,
- splice_funding_failed,
- exited_quiescence,
- }) => {
- if let Some(splice_funding_failed) = splice_funding_failed {
- let pending_events = &mut self.pending_events.lock().unwrap();
- pending_events.push_back((
- events::Event::SpliceFailed {
- channel_id: msg.channel_id,
- counterparty_node_id,
- user_channel_id: chan.context().get_user_id(),
- abandoned_funding_txo: splice_funding_failed.funding_txo,
- channel_type: splice_funding_failed.channel_type.clone(),
- },
- None,
- ));
- pending_events.push_back((
- events::Event::DiscardFunding {
- channel_id: msg.channel_id,
- funding_info: FundingInfo::Contribution {
- inputs: splice_funding_failed.contributed_inputs,
- outputs: splice_funding_failed.contributed_outputs,
- },
- },
- None,
- ));
- }
- debug_assert!(!exited_quiescence || matches!(err, ChannelError::Abort(_)));
-
- Err(MsgHandleErrInternal::from_chan_no_close(err, msg.channel_id)
- .with_exited_quiescence(exited_quiescence))
+ Err(err) => {
+ let user_channel_id = chan.context().get_user_id();
+ Err(self.handle_interactive_tx_msg_err(
+ err,
+ msg.channel_id,
+ &counterparty_node_id,
+ user_channel_id,
+ ))
},
}
},
@@ -13408,27 +13393,30 @@ This indicates a bug inside LDK. Please report this error at https://github.com/
}
if let Some(ref mut funded_channel) = chan_entry.get_mut().as_funded_mut() {
- let init_res = funded_channel.splice_init(
+ let user_channel_id = funded_channel.context.get_user_id();
+ match funded_channel.splice_init(
msg,
&self.entropy_source,
&self.get_our_node_id(),
&self.logger,
- );
- if let Err(ChannelError::Abort(_)) = &init_res {
- funded_channel.exit_quiescence();
- let chan_id = funded_channel.context.channel_id();
- let res = MsgHandleErrInternal::from_chan_no_close(
- init_res.unwrap_err(),
- chan_id,
- );
- return Err(res.with_exited_quiescence(true));
+ ) {
+ Ok(splice_ack_msg) => {
+ peer_state.pending_msg_events.push(MessageSendEvent::SendSpliceAck {
+ node_id: *counterparty_node_id,
+ msg: splice_ack_msg,
+ });
+ Ok(())
+ },
+ Err(err) => {
+ debug_assert!(err.splice_funding_failed.is_none());
+ Err(self.handle_interactive_tx_msg_err(
+ err,
+ msg.channel_id,
+ counterparty_node_id,
+ user_channel_id,
+ ))
+ },
}
- let splice_ack_msg = try_channel_entry!(self, peer_state, init_res, chan_entry);
- peer_state.pending_msg_events.push(MessageSendEvent::SendSpliceAck {
- node_id: *counterparty_node_id,
- msg: splice_ack_msg,
- });
- Ok(())
} else {
try_channel_entry!(
self,
@@ -13461,28 +13449,31 @@ This indicates a bug inside LDK. Please report this error at https://github.com/
},
hash_map::Entry::Occupied(mut chan_entry) => {
if let Some(ref mut funded_channel) = chan_entry.get_mut().as_funded_mut() {
- let init_res = funded_channel.tx_init_rbf(
+ let user_channel_id = funded_channel.context.get_user_id();
+ match funded_channel.tx_init_rbf(
msg,
&self.entropy_source,
&self.get_our_node_id(),
&self.fee_estimator,
&self.logger,
- );
- if let Err(ChannelError::Abort(_)) = &init_res {
- funded_channel.exit_quiescence();
- let chan_id = funded_channel.context.channel_id();
- let res = MsgHandleErrInternal::from_chan_no_close(
- init_res.unwrap_err(),
- chan_id,
- );
- return Err(res.with_exited_quiescence(true));
+ ) {
+ Ok(tx_ack_rbf_msg) => {
+ peer_state.pending_msg_events.push(MessageSendEvent::SendTxAckRbf {
+ node_id: *counterparty_node_id,
+ msg: tx_ack_rbf_msg,
+ });
+ Ok(())
+ },
+ Err(err) => {
+ debug_assert!(err.splice_funding_failed.is_none());
+ Err(self.handle_interactive_tx_msg_err(
+ err,
+ msg.channel_id,
+ counterparty_node_id,
+ user_channel_id,
+ ))
+ },
}
- let tx_ack_rbf_msg = try_channel_entry!(self, peer_state, init_res, chan_entry);
- peer_state.pending_msg_events.push(MessageSendEvent::SendTxAckRbf {
- node_id: *counterparty_node_id,
- msg: tx_ack_rbf_msg,
- });
- Ok(())
} else {
try_channel_entry!(
self,
diff --git a/lightning/src/ln/splicing_tests.rs b/lightning/src/ln/splicing_tests.rs
index 1902df5..98be113 100644
--- a/lightning/src/ln/splicing_tests.rs
+++ b/lightning/src/ln/splicing_tests.rs
@@ -6843,6 +6843,96 @@ fn test_splice_revalidation_at_quiescence() {
expect_splice_failed_events(&nodes[0], &channel_id, contribution);
}
+#[test]
+fn test_splice_init_before_quiescence_sends_warning() {
+ // A misbehaving peer sends splice_init before quiescence is established. The receiver
+ // should send a warning and disconnect.
+ 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);
+
+ let node_id_1 = nodes[1].node.get_our_node_id();
+
+ let initial_channel_value_sat = 100_000;
+ let (_, _, channel_id, _) =
+ create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_value_sat, 0);
+
+ // Node 0 initiates quiescence.
+ nodes[0].node.maybe_propose_quiescence(&node_id_1, &channel_id).unwrap();
+ let _stfu = get_event_msg!(nodes[0], MessageSendEvent::SendStfu, node_id_1);
+
+ // Misbehaving node 1 sends splice_init before completing the STFU handshake.
+ let funding_pubkey =
+ PublicKey::from_secret_key(&Secp256k1::new(), &SecretKey::from_slice(&[42; 32]).unwrap());
+ let splice_init = msgs::SpliceInit {
+ channel_id,
+ funding_contribution_satoshis: 50_000,
+ funding_feerate_per_kw: FEERATE_FLOOR_SATS_PER_KW,
+ locktime: 0,
+ funding_pubkey,
+ require_confirmed_inputs: None,
+ };
+ nodes[0].node.handle_splice_init(node_id_1, &splice_init);
+
+ // Node 0 should send a warning and disconnect.
+ let msg_events = nodes[0].node.get_and_clear_pending_msg_events();
+ assert_eq!(msg_events.len(), 1);
+ match &msg_events[0] {
+ MessageSendEvent::HandleError { node_id, .. } => assert_eq!(*node_id, node_id_1),
+ other => panic!("Expected HandleError, got {:?}", other),
+ }
+}
+
+#[test]
+fn test_tx_init_rbf_before_quiescence_sends_warning() {
+ // A misbehaving peer sends tx_init_rbf before quiescence is established. The receiver
+ // should send a warning and disconnect.
+ 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);
+
+ let node_id_1 = nodes[1].node.get_our_node_id();
+
+ let initial_channel_value_sat = 100_000;
+ let (_, _, channel_id, _) =
+ create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_value_sat, 0);
+
+ let added_value = Amount::from_sat(50_000);
+ provide_utxo_reserves(&nodes, 2, added_value * 2);
+
+ // Complete a splice-in so there's a pending splice to RBF.
+ let funding_contribution = do_initiate_splice_in(&nodes[0], &nodes[1], channel_id, added_value);
+ let (_splice_tx, _new_funding_script) =
+ splice_channel(&nodes[0], &nodes[1], channel_id, funding_contribution);
+
+ // Node 0 initiates quiescence.
+ nodes[0].node.maybe_propose_quiescence(&node_id_1, &channel_id).unwrap();
+ let _stfu = get_event_msg!(nodes[0], MessageSendEvent::SendStfu, node_id_1);
+
+ // Misbehaving node 1 sends tx_init_rbf before completing the STFU handshake.
+ let tx_init_rbf = msgs::TxInitRbf {
+ channel_id,
+ locktime: 0,
+ feerate_sat_per_1000_weight: FEERATE_FLOOR_SATS_PER_KW + 25,
+ funding_output_contribution: Some(added_value.to_sat() as i64),
+ };
+ nodes[0].node.handle_tx_init_rbf(node_id_1, &tx_init_rbf);
+
+ // Node 0 should send a warning and disconnect.
+ let msg_events = nodes[0].node.get_and_clear_pending_msg_events();
+ assert_eq!(msg_events.len(), 1);
+ match &msg_events[0] {
+ MessageSendEvent::HandleError { node_id, .. } => assert_eq!(*node_id, node_id_1),
+ other => panic!("Expected HandleError, got {:?}", other),
+ }
+
+ // Clean up events from the splice setup.
+ nodes[0].node.get_and_clear_pending_events();
+ nodes[1].node.get_and_clear_pending_events();
+}
+
#[test]
fn test_splice_rbf_rejects_low_feerate_after_several_attempts() {
// After several RBF attempts, the counterparty's RBF feerate must be high enough to
Why this scored 33/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.