Set dont_forward on private channel updates and add tests
What changed, and why it matters
This commit fixes a protocol-compliance issue in LDK's Lightning channel update messages. Private (unannounced) channels now correctly set the 'dont_forward' flag on their channel_update messages, telling other nodes not to relay them across the network. Public channels continue not to set this flag. The change also adds a warning log when a node receives a channel_update for an unknown channel that has dont_forward set, which can help diagnose configuration mistakes. This is a correctness and privacy improvement rather than a critical vulnerability.
Review and merge. The change is low-risk and improves protocol correctness. Operators running nodes with private channels should ensure they are on a version containing this commit to avoid unintended gossip propagation of private channel parameters.
Security signals we found
Protocol compliance: BOLT 7 message_flags bit 1 (dont_forward) now honored for private channels
Privacy improvement: private channel updates are explicitly marked non-relayable
Diagnostic logging added for unknown-channel updates with dont_forward set
No memory safety, cryptographic, or authorization changes observed
Evidence from the diff
The patch modifies ChannelManager::get_channel_update to set bit 1 (dont_forward) of message_flags when the channel is private (chan.context.should_announce() is false). Previously message_flags was hard-coded to 1 (only the required bit). The patch also adds a warning log in handle_channel_update when an unknown short_channel_id is received with dont_forward set, suggesting the operator check the tx_index passed to chain::Confirm::transactions_confirmed. Two unit tests verify the flag behavior and the warning log.
Changed components
lightning/src/ln/channelmanager.rslightning/src/ln/priv_short_conf_tests.rsInspect captured patch +113 / −1
diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs
index dafeffe..56e9732 100644
--- a/lightning/src/ln/channelmanager.rs
+++ b/lightning/src/ln/channelmanager.rs
@@ -5132,7 +5132,7 @@ where
chain_hash: self.chain_hash,
short_channel_id,
timestamp: chan.context.get_update_time_counter(),
- message_flags: 1, // Only must_be_one
+ message_flags: 1 | if !chan.context.should_announce() { 1 << 1 } else { 0 }, // must_be_one + dont_forward
channel_flags: (!were_node_one) as u8 | ((!enabled as u8) << 1),
cltv_expiry_delta: chan.context.get_cltv_expiry_delta(),
htlc_minimum_msat: chan.context.get_counterparty_htlc_minimum_msat(),
@@ -12290,6 +12290,9 @@ This indicates a bug inside LDK. Please report this error at https://github.com/
Some((cp_id, chan_id)) => (cp_id.clone(), chan_id.clone()),
None => {
// It's not a local channel
+ if msg.contents.message_flags & (1 << 1) != 0 {
+ log_warn!(self.logger, "Received channel_update for unknown channel {} with dont_forward set.\n\tYou may wish to check if an incorrect tx_index was passed to chain::Confirm::transactions_confirmed.", msg.contents.short_channel_id);
+ }
return Ok(NotifyOption::SkipPersistNoEvents)
}
};
diff --git a/lightning/src/ln/priv_short_conf_tests.rs b/lightning/src/ln/priv_short_conf_tests.rs
index 83aaca2..e26776e 100644
--- a/lightning/src/ln/priv_short_conf_tests.rs
+++ b/lightning/src/ln/priv_short_conf_tests.rs
@@ -1520,3 +1520,112 @@ fn test_0conf_ann_sigs_racing_conf() {
let as_announcement = nodes[0].node.get_and_clear_pending_msg_events();
assert_eq!(as_announcement.len(), 1);
}
+
+#[test]
+fn test_channel_update_dont_forward_flag() {
+ // Test that the `dont_forward` bit (bit 1 of message_flags) is set correctly:
+ // - For private channels: message_flags should have bit 1 set (value 3 = must_be_one + dont_forward)
+ // - For public channels: message_flags should NOT have bit 1 set (value 1 = must_be_one only)
+ let chanmon_cfgs = create_chanmon_cfgs(3);
+ let node_cfgs = create_node_cfgs(3, &chanmon_cfgs);
+ let node_chanmgrs = create_node_chanmgrs(3, &node_cfgs, &[None, None, None]);
+ let nodes = create_network(3, &node_cfgs, &node_chanmgrs);
+ let node_b_id = nodes[1].node.get_our_node_id();
+ let node_c_id = nodes[2].node.get_our_node_id();
+
+ // Create a public (announced) channel between nodes[0] and nodes[1]
+ create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 500_000_000);
+
+ // Create a private (unannounced) channel between nodes[1] and nodes[2]
+ create_unannounced_chan_between_nodes_with_value(&nodes, 1, 2, 1_000_000, 500_000_000);
+
+ // Get the channel details for both channels
+ let public_channel = nodes[0].node.list_channels().into_iter()
+ .find(|c| c.counterparty.node_id == node_b_id).unwrap();
+ let private_channel = nodes[1].node.list_channels().into_iter()
+ .find(|c| c.counterparty.node_id == node_c_id).unwrap();
+
+ // Verify is_announced correctly reflects the channel type
+ assert!(public_channel.is_announced, "Public channel should have is_announced = true");
+ assert!(!private_channel.is_announced, "Private channel should have is_announced = false");
+
+ // Trigger channel_update by changing config on the public channel
+ let mut new_config = public_channel.config.unwrap();
+ new_config.forwarding_fee_base_msat += 10;
+ nodes[0].node.update_channel_config(&node_b_id, &[public_channel.channel_id], &new_config).unwrap();
+
+ // Get the channel_update for the public channel and verify dont_forward is NOT set
+ let events = nodes[0].node.get_and_clear_pending_msg_events();
+ let public_channel_update = events.iter().find_map(|e| {
+ if let MessageSendEvent::BroadcastChannelUpdate { ref msg, .. } = e {
+ Some(msg.clone())
+ } else {
+ None
+ }
+ }).expect("Expected BroadcastChannelUpdate for public channel");
+ // message_flags should be 1 (only must_be_one bit set, dont_forward NOT set)
+ assert_eq!(
+ public_channel_update.contents.message_flags & (1 << 1), 0,
+ "Public channel update should NOT have dont_forward bit set"
+ );
+ assert_eq!(
+ public_channel_update.contents.message_flags & 1, 1,
+ "Public channel update should have must_be_one bit set"
+ );
+
+ // Trigger channel_update by changing config on the private channel
+ let mut new_config = private_channel.config.unwrap();
+ new_config.forwarding_fee_base_msat += 10;
+ nodes[1].node.update_channel_config(&node_c_id, &[private_channel.channel_id], &new_config).unwrap();
+
+ // Get the channel_update for the private channel and verify dont_forward IS set
+ let private_channel_update =
+ get_event_msg!(nodes[1], MessageSendEvent::SendChannelUpdate, node_c_id);
+ // message_flags should have dont_forward bit set
+ assert_ne!(
+ private_channel_update.contents.message_flags & (1 << 1), 0,
+ "Private channel update should have dont_forward bit set"
+ );
+ assert_eq!(
+ private_channel_update.contents.message_flags & 1, 1,
+ "Private channel update should have must_be_one bit set"
+ );
+}
+
+#[test]
+fn test_unknown_channel_update_with_dont_forward_logs_warning() {
+ use bitcoin::constants::ChainHash;
+ use bitcoin::secp256k1::ecdsa::Signature;
+ use bitcoin::secp256k1::ffi::Signature as FFISignature;
+ use bitcoin::Network;
+
+ 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 unknown_scid = 42;
+ let msg = msgs::ChannelUpdate {
+ signature: Signature::from(unsafe { FFISignature::new() }),
+ contents: msgs::UnsignedChannelUpdate {
+ chain_hash: ChainHash::using_genesis_block(Network::Testnet),
+ short_channel_id: unknown_scid,
+ timestamp: 0,
+ message_flags: 1 | (1 << 1), // must_be_one + dont_forward
+ channel_flags: 0,
+ cltv_expiry_delta: 0,
+ htlc_minimum_msat: 0,
+ htlc_maximum_msat: msgs::MAX_VALUE_MSAT,
+ fee_base_msat: 0,
+ fee_proportional_millionths: 0,
+ excess_data: Vec::new(),
+ },
+ };
+
+ nodes[0].node.handle_channel_update(nodes[1].node.get_our_node_id(), &msg);
+ nodes[0].logger.assert_log_contains(
+ "lightning::ln::channelmanager",
+ "Received channel_update for unknown channel",
+ 1,
+ );
+}
Why this scored 48/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.