Skip to content

Commit 081de37

Browse files
joostjagerclaude
andcommitted
fuzz: add force-close support to chanmon_consistency
Add two new fuzzer actions (0xc0, 0xc1) to force-close one channel on each peer pair (A-B and B-C). This tests channel monitor consistency when channels are intentionally closed. Changes: - Add fc_ab/fc_bc bools to track force-closed channels - Handle HandleError, BroadcastChannelUpdate, and ChannelClosed events generated by force-close - Update test_return! macro to account for closed channels - Skip force-closed channels in 0xff validation Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent e0ba40e commit 081de37

1 file changed

Lines changed: 61 additions & 9 deletions

File tree

fuzz/src/chanmon_consistency.rs

Lines changed: 61 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1270,16 +1270,29 @@ pub fn do_test<Out: Output>(data: &[u8], underlying_out: Out, anchors: bool) {
12701270

12711271
let pending_payments = RefCell::new([Vec::new(), Vec::new(), Vec::new()]);
12721272
let resolved_payments = RefCell::new([Vec::new(), Vec::new(), Vec::new()]);
1273+
let mut fc_ab = false; // Force-closed one A-B channel
1274+
let mut fc_bc = false; // Force-closed one B-C channel
12731275

12741276
macro_rules! test_return {
12751277
() => {{
1276-
assert_eq!(nodes[0].list_channels().len(), 3);
1277-
assert_eq!(nodes[1].list_channels().len(), 6);
1278-
assert_eq!(nodes[2].list_channels().len(), 3);
1279-
1280-
// At no point should we have broadcasted any transactions after the initial channel
1281-
// opens.
1282-
assert!(broadcast.txn_broadcasted.borrow().is_empty());
1278+
// Node A has 3 A-B channels, minus 1 if we force-closed.
1279+
let expected_a = if fc_ab { 2 } else { 3 };
1280+
assert_eq!(nodes[0].list_channels().len(), expected_a);
1281+
// Node B has 3 A-B + 3 B-C channels. Counterparty may not have processed
1282+
// the force-close yet, so we bound the count.
1283+
let node_b_chans = nodes[1].list_channels().len();
1284+
let min_b = 6 - (if fc_ab { 1 } else { 0 }) - (if fc_bc { 1 } else { 0 });
1285+
let max_b = 6 - (if fc_bc { 1 } else { 0 });
1286+
assert!(node_b_chans >= min_b && node_b_chans <= max_b);
1287+
// Node C has 3 B-C channels. Counterparty may not have processed force-close.
1288+
let node_c_chans = nodes[2].list_channels().len();
1289+
let min_c = if fc_bc { 2 } else { 3 };
1290+
assert!(node_c_chans >= min_c && node_c_chans <= 3);
1291+
1292+
// Only check for no broadcasts if no force-closes happened.
1293+
if !fc_ab && !fc_bc {
1294+
assert!(broadcast.txn_broadcasted.borrow().is_empty());
1295+
}
12831296

12841297
return;
12851298
}};
@@ -1352,6 +1365,8 @@ pub fn do_test<Out: Output>(data: &[u8], underlying_out: Out, anchors: bool) {
13521365
},
13531366
MessageSendEvent::SendChannelReady { .. } => continue,
13541367
MessageSendEvent::SendAnnouncementSignatures { .. } => continue,
1368+
MessageSendEvent::HandleError { .. } => continue,
1369+
MessageSendEvent::BroadcastChannelUpdate { .. } => continue,
13551370
MessageSendEvent::SendChannelUpdate { ref node_id, ref msg } => {
13561371
assert_eq!(msg.contents.channel_flags & 2, 0); // The disable bit must never be set!
13571372
if Some(*node_id) == expect_drop_id { panic!("peer_disconnected should drop msgs bound for the disconnected peer"); }
@@ -1573,6 +1588,12 @@ pub fn do_test<Out: Output>(data: &[u8], underlying_out: Out, anchors: bool) {
15731588
// force-close which we should detect as an error).
15741589
assert_eq!(msg.contents.channel_flags & 2, 0);
15751590
},
1591+
MessageSendEvent::HandleError { .. } => {
1592+
// Can be generated by force-close
1593+
},
1594+
MessageSendEvent::BroadcastChannelUpdate { .. } => {
1595+
// Can be generated by force-close (with disable bit set)
1596+
},
15761597
_ => if out.may_fail.load(atomic::Ordering::Acquire) {
15771598
return;
15781599
} else {
@@ -1616,6 +1637,8 @@ pub fn do_test<Out: Output>(data: &[u8], underlying_out: Out, anchors: bool) {
16161637
MessageSendEvent::SendChannelUpdate { ref msg, .. } => {
16171638
assert_eq!(msg.contents.channel_flags & 2, 0); // The disable bit must never be set!
16181639
},
1640+
MessageSendEvent::HandleError { .. } => {},
1641+
MessageSendEvent::BroadcastChannelUpdate { .. } => {},
16191642
_ => {
16201643
if out.may_fail.load(atomic::Ordering::Acquire) {
16211644
return;
@@ -1643,6 +1666,8 @@ pub fn do_test<Out: Output>(data: &[u8], underlying_out: Out, anchors: bool) {
16431666
MessageSendEvent::SendChannelUpdate { ref msg, .. } => {
16441667
assert_eq!(msg.contents.channel_flags & 2, 0); // The disable bit must never be set!
16451668
},
1669+
MessageSendEvent::HandleError { .. } => {},
1670+
MessageSendEvent::BroadcastChannelUpdate { .. } => {},
16461671
_ => {
16471672
if out.may_fail.load(atomic::Ordering::Acquire) {
16481673
return;
@@ -1737,6 +1762,7 @@ pub fn do_test<Out: Output>(data: &[u8], underlying_out: Out, anchors: bool) {
17371762
},
17381763
events::Event::SplicePending { .. } => {},
17391764
events::Event::SpliceFailed { .. } => {},
1765+
events::Event::ChannelClosed { .. } => {},
17401766

17411767
_ => {
17421768
if out.may_fail.load(atomic::Ordering::Acquire) {
@@ -2326,6 +2352,25 @@ pub fn do_test<Out: Output>(data: &[u8], underlying_out: Out, anchors: bool) {
23262352
}
23272353
},
23282354

2355+
// Force close A-B channel 0
2356+
0xc0 => {
2357+
let _ = nodes[0].force_close_broadcasting_latest_txn(
2358+
&chan_a_id,
2359+
&nodes[1].get_our_node_id(),
2360+
"]]]]".to_string(),
2361+
);
2362+
fc_ab = true;
2363+
},
2364+
// Force close B-C channel 0
2365+
0xc1 => {
2366+
let _ = nodes[1].force_close_broadcasting_latest_txn(
2367+
&chan_b_id,
2368+
&nodes[2].get_our_node_id(),
2369+
"]]]]".to_string(),
2370+
);
2371+
fc_bc = true;
2372+
},
2373+
23292374
0xb0 | 0xb1 | 0xb2 => {
23302375
// Restart node A, picking among the in-flight `ChannelMonitor`s to use based on
23312376
// the value of `v` we're matching.
@@ -2545,13 +2590,20 @@ pub fn do_test<Out: Output>(data: &[u8], underlying_out: Out, anchors: bool) {
25452590
process_all_events!();
25462591

25472592
// Finally, make sure that at least one end of each channel can make a substantial payment
2548-
for &scid in &chan_ab_scids {
2593+
// (skip channel 0 if it was force-closed)
2594+
for (i, &scid) in chan_ab_scids.iter().enumerate() {
2595+
if fc_ab && i == 0 {
2596+
continue;
2597+
}
25492598
assert!(
25502599
send(0, 1, scid, 10_000_000, &mut p_ctr)
25512600
|| send(1, 0, scid, 10_000_000, &mut p_ctr)
25522601
);
25532602
}
2554-
for &scid in &chan_bc_scids {
2603+
for (i, &scid) in chan_bc_scids.iter().enumerate() {
2604+
if fc_bc && i == 0 {
2605+
continue;
2606+
}
25552607
assert!(
25562608
send(1, 2, scid, 10_000_000, &mut p_ctr)
25572609
|| send(2, 1, scid, 10_000_000, &mut p_ctr)

0 commit comments

Comments
 (0)