Skip to content

Commit 195cebc

Browse files
committed
Add splice state fuzz checks
Expose a fuzz-only splice state probe so chanmon consistency can check tx_abort cleanup without relying on API error strings. The helper reports queued, active, and pending splice states directly.
1 parent a961243 commit 195cebc

3 files changed

Lines changed: 88 additions & 1 deletion

File tree

fuzz/src/chanmon_consistency.rs

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ use lightning::chain::{
4949
};
5050
use lightning::events;
5151
use lightning::ln::channel::{
52-
FEE_SPIKE_BUFFER_FEE_INCREASE_MULTIPLE, MAX_STD_OUTPUT_DUST_LIMIT_SATOSHIS,
52+
SpliceProbeState, FEE_SPIKE_BUFFER_FEE_INCREASE_MULTIPLE, MAX_STD_OUTPUT_DUST_LIMIT_SATOSHIS,
5353
};
5454
use lightning::ln::channel_state::ChannelDetails;
5555
use lightning::ln::channelmanager::{
@@ -2224,6 +2224,27 @@ fn build_node_config(chan_type: ChanType) -> UserConfig {
22242224
config
22252225
}
22262226

2227+
fn assert_no_stale_splice_negotiation(
2228+
node: &HarnessNode<'_>, channel_id: &ChannelId, counterparty_node_id: &PublicKey, context: &str,
2229+
) {
2230+
let state = match node.node.splice_probe_state(channel_id, counterparty_node_id) {
2231+
Ok(state) => state,
2232+
Err(APIError::ChannelUnavailable { ref err })
2233+
if err.starts_with("No such channel_id") || err.starts_with("No such peer") =>
2234+
{
2235+
// The channel may already be removed when close-related cleanup events are handled.
2236+
return;
2237+
},
2238+
Err(e) => panic!("{:?}", e),
2239+
};
2240+
assert!(
2241+
matches!(state, SpliceProbeState::None | SpliceProbeState::PendingWithoutNegotiation),
2242+
"{} left splice negotiation state behind: {:?}",
2243+
context,
2244+
state
2245+
);
2246+
}
2247+
22272248
fn assert_test_invariants(nodes: &[HarnessNode<'_>; 3]) {
22282249
assert_eq!(nodes[0].list_channels().len(), 3);
22292250
assert_eq!(nodes[1].list_channels().len(), 6);
@@ -2918,6 +2939,12 @@ impl<'a, Out: Output + MaybeSend + MaybeSync> Harness<'a, Out> {
29182939
MessageSendEvent::SendTxAbort { ref node_id, ref msg } => {
29192940
let dest_idx = log_peer_message(node_idx, node_id, nodes, out, "tx_abort");
29202941
nodes[dest_idx].handle_tx_abort(source_node_id, msg);
2942+
assert_no_stale_splice_negotiation(
2943+
&nodes[dest_idx],
2944+
&msg.channel_id,
2945+
&source_node_id,
2946+
"tx_abort",
2947+
);
29212948
None
29222949
},
29232950
MessageSendEvent::SendTxInitRbf { ref node_id, ref msg } => {

lightning/src/ln/channel.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1537,6 +1537,20 @@ enum ChannelPhase<SP: SignerProvider> {
15371537
Funded(FundedChannel<SP>),
15381538
}
15391539

1540+
/// Fuzz-only view of splice state used to validate cleanup paths.
1541+
#[cfg(fuzzing)]
1542+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1543+
pub enum SpliceProbeState {
1544+
/// No pending splice state is present.
1545+
None,
1546+
/// A splice action is queued to start once quiescence completes.
1547+
QueuedAction,
1548+
/// A splice negotiation is active.
1549+
ActiveNegotiation { is_initiator: bool },
1550+
/// Splice state remains, but no negotiation is currently active.
1551+
PendingWithoutNegotiation,
1552+
}
1553+
15401554
impl<SP: SignerProvider> Channel<SP>
15411555
where
15421556
SP::EcdsaSigner: ChannelSigner,
@@ -1617,6 +1631,14 @@ where
16171631
}
16181632
}
16191633

1634+
/// Returns a read-only view of splice state for fuzz invariants.
1635+
#[cfg(fuzzing)]
1636+
pub fn splice_probe_state(&self) -> SpliceProbeState {
1637+
self.as_funded()
1638+
.map(|channel| channel.splice_probe_state())
1639+
.unwrap_or(SpliceProbeState::None)
1640+
}
1641+
16201642
pub fn as_funded_mut(&mut self) -> Option<&mut FundedChannel<SP>> {
16211643
if let ChannelPhase::Funded(channel) = &mut self.phase {
16221644
Some(channel)
@@ -7331,6 +7353,22 @@ where
73317353
}
73327354
}
73337355

7356+
#[cfg(fuzzing)]
7357+
fn splice_probe_state(&self) -> SpliceProbeState {
7358+
if self.quiescent_action.is_some() {
7359+
return SpliceProbeState::QueuedAction;
7360+
}
7361+
if let Some(pending_splice) = &self.pending_splice {
7362+
if let Some(funding_negotiation) = &pending_splice.funding_negotiation {
7363+
return SpliceProbeState::ActiveNegotiation {
7364+
is_initiator: funding_negotiation.is_initiator(),
7365+
};
7366+
}
7367+
return SpliceProbeState::PendingWithoutNegotiation;
7368+
}
7369+
SpliceProbeState::None
7370+
}
7371+
73347372
fn funding_and_pending_funding_iter_mut(&mut self) -> impl Iterator<Item = &mut FundingScope> {
73357373
core::iter::once(&mut self.funding).chain(
73367374
self.pending_splice

lightning/src/ln/channelmanager.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4976,6 +4976,28 @@ impl<
49764976
}
49774977
}
49784978

4979+
/// Returns a read-only view of channel splice state for fuzz invariants.
4980+
#[cfg(fuzzing)]
4981+
pub fn splice_probe_state(
4982+
&self, channel_id: &ChannelId, counterparty_node_id: &PublicKey,
4983+
) -> Result<channel::SpliceProbeState, APIError> {
4984+
let per_peer_state = self.per_peer_state.read().unwrap();
4985+
4986+
let peer_state_mutex = match per_peer_state
4987+
.get(counterparty_node_id)
4988+
.ok_or_else(|| APIError::no_such_peer(counterparty_node_id))
4989+
{
4990+
Ok(p) => p,
4991+
Err(e) => return Err(e),
4992+
};
4993+
4994+
let peer_state = peer_state_mutex.lock().unwrap();
4995+
match peer_state.channel_by_id.get(channel_id) {
4996+
Some(chan_phase) => Ok(chan_phase.splice_probe_state()),
4997+
None => Err(APIError::no_such_channel_for_peer(channel_id, counterparty_node_id)),
4998+
}
4999+
}
5000+
49795001
/// Cancels an in-flight [`FundingContribution`].
49805002
///
49815003
/// This is primarily useful after receiving an [`Event::FundingTransactionReadyForSigning`] for

0 commit comments

Comments
 (0)