Update `chanmon_consistency` to include 0FC and 0-reserve channels
What changed, and why it matters
This commit only changes a fuzzing test file. It expands an existing test harness to exercise two new channel types—zero-fee commitment transactions and zero-reserve channels—so the project's automated fuzzer can look for bugs in those features. There is no change to production code, no fix for a known bug, and no security patch.
No security action needed. Treat as a normal test/fuzzing harness enhancement.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The diff modifies fuzz/src/chanmon_consistency.rs. It replaces the previous boolean anchors flag with a ChanType enum (Legacy, KeyedAnchors, ZeroFeeCommitments), consumes extra bits from the fuzz input byte to select the channel type, sets the corresponding handshake config flags, and adds trusted-peer zero-reserve open/accept paths via create_channel_to_trusted_peer_0reserve and accept_inbound_channel_from_trusted_peer. The public test entry points now call do_test once instead of twice with different anchor settings. No library code is altered.
Changed components
fuzz/src/chanmon_consistency.rsInspect captured patch +99 / −36
diff --git a/fuzz/src/chanmon_consistency.rs b/fuzz/src/chanmon_consistency.rs
index e4fd347..9e88083 100644
--- a/fuzz/src/chanmon_consistency.rs
+++ b/fuzz/src/chanmon_consistency.rs
@@ -53,6 +53,7 @@ use lightning::ln::channel::{
use lightning::ln::channel_state::ChannelDetails;
use lightning::ln::channelmanager::{
ChainParameters, ChannelManager, ChannelManagerReadArgs, PaymentId, RecentPaymentDetails,
+ TrustedChannelFeatures,
};
use lightning::ln::functional_test_utils::*;
use lightning::ln::funding::{FundingContribution, FundingTemplate};
@@ -862,30 +863,41 @@ fn assert_action_timeout_awaiting_response(action: &msgs::ErrorAction) {
));
}
+enum ChanType {
+ Legacy,
+ KeyedAnchors,
+ ZeroFeeCommitments,
+}
+
#[inline]
-pub fn do_test<Out: Output + MaybeSend + MaybeSync>(
- data: &[u8], underlying_out: Out, anchors: bool,
-) {
+pub fn do_test<Out: Output + MaybeSend + MaybeSync>(data: &[u8], underlying_out: Out) {
let out = SearchingOutput::new(underlying_out);
let broadcast_a = Arc::new(TestBroadcaster { txn_broadcasted: RefCell::new(Vec::new()) });
let broadcast_b = Arc::new(TestBroadcaster { txn_broadcasted: RefCell::new(Vec::new()) });
let broadcast_c = Arc::new(TestBroadcaster { txn_broadcasted: RefCell::new(Vec::new()) });
let router = FuzzRouter {};
- // Read initial monitor styles from fuzz input (1 byte: 2 bits per node)
- let initial_mon_styles = if !data.is_empty() { data[0] } else { 0 };
+ // Read initial monitor styles and channel type from fuzz input byte 0:
+ // bits 0-2: monitor styles (1 bit per node)
+ // bits 3-4: channel type (0=Legacy, 1=KeyedAnchors, 2=ZeroFeeCommitments)
+ let config_byte = if !data.is_empty() { data[0] } else { 0 };
+ let chan_type = match (config_byte >> 3) & 0b11 {
+ 0 => ChanType::Legacy,
+ 1 => ChanType::KeyedAnchors,
+ _ => ChanType::ZeroFeeCommitments,
+ };
let mon_style = [
- RefCell::new(if initial_mon_styles & 0b01 != 0 {
+ RefCell::new(if config_byte & 0b01 != 0 {
ChannelMonitorUpdateStatus::InProgress
} else {
ChannelMonitorUpdateStatus::Completed
}),
- RefCell::new(if initial_mon_styles & 0b10 != 0 {
+ RefCell::new(if config_byte & 0b10 != 0 {
ChannelMonitorUpdateStatus::InProgress
} else {
ChannelMonitorUpdateStatus::Completed
}),
- RefCell::new(if initial_mon_styles & 0b100 != 0 {
+ RefCell::new(if config_byte & 0b100 != 0 {
ChannelMonitorUpdateStatus::InProgress
} else {
ChannelMonitorUpdateStatus::Completed
@@ -925,8 +937,19 @@ pub fn do_test<Out: Output + MaybeSend + MaybeSync>(
config.channel_config.forwarding_fee_proportional_millionths = 0;
config.channel_handshake_config.announce_for_forwarding = true;
config.reject_inbound_splices = false;
- if !anchors {
- config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = false;
+ match chan_type {
+ ChanType::Legacy => {
+ config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = false;
+ config.channel_handshake_config.negotiate_anchor_zero_fee_commitments = false;
+ },
+ ChanType::KeyedAnchors => {
+ config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = true;
+ config.channel_handshake_config.negotiate_anchor_zero_fee_commitments = false;
+ },
+ ChanType::ZeroFeeCommitments => {
+ config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = false;
+ config.channel_handshake_config.negotiate_anchor_zero_fee_commitments = true;
+ },
}
let network = Network::Bitcoin;
let best_block_timestamp = genesis_block(network).header.time;
@@ -977,8 +1000,19 @@ pub fn do_test<Out: Output + MaybeSend + MaybeSync>(
config.channel_config.forwarding_fee_proportional_millionths = 0;
config.channel_handshake_config.announce_for_forwarding = true;
config.reject_inbound_splices = false;
- if !anchors {
- config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = false;
+ match chan_type {
+ ChanType::Legacy => {
+ config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = false;
+ config.channel_handshake_config.negotiate_anchor_zero_fee_commitments = false;
+ },
+ ChanType::KeyedAnchors => {
+ config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = true;
+ config.channel_handshake_config.negotiate_anchor_zero_fee_commitments = false;
+ },
+ ChanType::ZeroFeeCommitments => {
+ config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = false;
+ config.channel_handshake_config.negotiate_anchor_zero_fee_commitments = true;
+ },
}
let mut monitors = new_hash_map();
@@ -1077,8 +1111,23 @@ pub fn do_test<Out: Output + MaybeSend + MaybeSync>(
}};
}
macro_rules! make_channel {
- ($source: expr, $dest: expr, $source_monitor: expr, $dest_monitor: expr, $dest_keys_manager: expr, $chan_id: expr) => {{
- $source.create_channel($dest.get_our_node_id(), 100_000, 42, 0, None, None).unwrap();
+ ($source: expr, $dest: expr, $source_monitor: expr, $dest_monitor: expr, $dest_keys_manager: expr, $chan_id: expr, $trusted_open: expr, $trusted_accept: expr) => {{
+ if $trusted_open {
+ $source
+ .create_channel_to_trusted_peer_0reserve(
+ $dest.get_our_node_id(),
+ 100_000,
+ 42,
+ 0,
+ None,
+ None,
+ )
+ .unwrap();
+ } else {
+ $source
+ .create_channel($dest.get_our_node_id(), 100_000, 42, 0, None, None)
+ .unwrap();
+ }
let open_channel = {
let events = $source.get_and_clear_pending_msg_events();
assert_eq!(events.len(), 1);
@@ -1103,14 +1152,26 @@ pub fn do_test<Out: Output + MaybeSend + MaybeSync>(
random_bytes
.copy_from_slice(&$dest_keys_manager.get_secure_random_bytes()[..16]);
let user_channel_id = u128::from_be_bytes(random_bytes);
- $dest
- .accept_inbound_channel(
- temporary_channel_id,
- counterparty_node_id,
- user_channel_id,
- None,
- )
- .unwrap();
+ if $trusted_accept {
+ $dest
+ .accept_inbound_channel_from_trusted_peer(
+ temporary_channel_id,
+ counterparty_node_id,
+ user_channel_id,
+ TrustedChannelFeatures::ZeroReserve,
+ None,
+ )
+ .unwrap();
+ } else {
+ $dest
+ .accept_inbound_channel(
+ temporary_channel_id,
+ counterparty_node_id,
+ user_channel_id,
+ None,
+ )
+ .unwrap();
+ }
} else {
panic!("Wrong event type");
}
@@ -1286,12 +1347,16 @@ pub fn do_test<Out: Output + MaybeSend + MaybeSync>(
// Fuzz mode uses XOR-based hashing (all bytes XOR to one byte), and
// versions 0-5 cause collisions between A-B and B-C channel pairs
// (e.g., A-B with Version(1) collides with B-C with Version(3)).
- make_channel!(nodes[0], nodes[1], monitor_a, monitor_b, keys_manager_b, 1);
- make_channel!(nodes[0], nodes[1], monitor_a, monitor_b, keys_manager_b, 2);
- make_channel!(nodes[0], nodes[1], monitor_a, monitor_b, keys_manager_b, 3);
- make_channel!(nodes[1], nodes[2], monitor_b, monitor_c, keys_manager_c, 4);
- make_channel!(nodes[1], nodes[2], monitor_b, monitor_c, keys_manager_c, 5);
- make_channel!(nodes[1], nodes[2], monitor_b, monitor_c, keys_manager_c, 6);
+ // A-B: channel 2 A and B have 0-reserve (trusted open + trusted accept),
+ // channel 3 A has 0-reserve (trusted accept)
+ make_channel!(nodes[0], nodes[1], monitor_a, monitor_b, keys_manager_b, 1, false, false);
+ make_channel!(nodes[0], nodes[1], monitor_a, monitor_b, keys_manager_b, 2, true, true);
+ make_channel!(nodes[0], nodes[1], monitor_a, monitor_b, keys_manager_b, 3, false, true);
+ // B-C: channel 4 B has 0-reserve (via trusted accept),
+ // channel 5 C has 0-reserve (via trusted open)
+ make_channel!(nodes[1], nodes[2], monitor_b, monitor_c, keys_manager_c, 4, false, true);
+ make_channel!(nodes[1], nodes[2], monitor_b, monitor_c, keys_manager_c, 5, true, false);
+ make_channel!(nodes[1], nodes[2], monitor_b, monitor_c, keys_manager_c, 6, false, false);
// Wipe the transactions-broadcasted set to make sure we don't broadcast any transactions
// during normal operation in `test_return`.
@@ -1375,7 +1440,7 @@ pub fn do_test<Out: Output + MaybeSend + MaybeSync>(
}};
}
- let mut read_pos = 1; // First byte was consumed for initial mon_style
+ let mut read_pos = 1; // First byte was consumed for initial config (mon_style + chan_type)
macro_rules! get_slice {
($len: expr) => {{
let slice_len = $len as usize;
@@ -2332,7 +2397,7 @@ pub fn do_test<Out: Output + MaybeSend + MaybeSync>(
0x80 => {
let mut max_feerate = last_htlc_clear_fee_a;
- if !anchors {
+ if matches!(chan_type, ChanType::Legacy) {
max_feerate *= FEE_SPIKE_BUFFER_FEE_INCREASE_MULTIPLE as u32;
}
if fee_est_a.ret_val.fetch_add(250, atomic::Ordering::AcqRel) + 250 > max_feerate {
@@ -2347,7 +2412,7 @@ pub fn do_test<Out: Output + MaybeSend + MaybeSync>(
0x84 => {
let mut max_feerate = last_htlc_clear_fee_b;
- if !anchors {
+ if matches!(chan_type, ChanType::Legacy) {
max_feerate *= FEE_SPIKE_BUFFER_FEE_INCREASE_MULTIPLE as u32;
}
if fee_est_b.ret_val.fetch_add(250, atomic::Ordering::AcqRel) + 250 > max_feerate {
@@ -2362,7 +2427,7 @@ pub fn do_test<Out: Output + MaybeSend + MaybeSync>(
0x88 => {
let mut max_feerate = last_htlc_clear_fee_c;
- if !anchors {
+ if matches!(chan_type, ChanType::Legacy) {
max_feerate *= FEE_SPIKE_BUFFER_FEE_INCREASE_MULTIPLE as u32;
}
if fee_est_c.ret_val.fetch_add(250, atomic::Ordering::AcqRel) + 250 > max_feerate {
@@ -2832,12 +2897,10 @@ impl<O: Output> SearchingOutput<O> {
}
pub fn chanmon_consistency_test<Out: Output + MaybeSend + MaybeSync>(data: &[u8], out: Out) {
- do_test(data, out.clone(), false);
- do_test(data, out, true);
+ do_test(data, out);
}
#[no_mangle]
pub extern "C" fn chanmon_consistency_run(data: *const u8, datalen: usize) {
- do_test(unsafe { std::slice::from_raw_parts(data, datalen) }, test_logger::DevNull {}, false);
- do_test(unsafe { std::slice::from_raw_parts(data, datalen) }, test_logger::DevNull {}, true);
+ do_test(unsafe { std::slice::from_raw_parts(data, datalen) }, test_logger::DevNull {});
}
Why this scored 15/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.