Always forward gossip for all our channels to all our peers
What changed, and why it matters
This commit fixes a Lightning network gossip propagation bug. Some peers ask nodes not to send them gossip updates. Previously, LDK honored that request too strictly: if a node had only one connection to the rest of the network and that connection asked for no gossip, information about the node's own public channels would not spread to other peers. The change ensures that gossip about channels the node itself participates in is always forwarded to all peers, even those that normally filter out gossip. This improves network visibility of the node's own channels but does not create a direct way to steal funds or crash the node.
No immediate security response needed; this is a beneficial correctness fix. Operators should upgrade to ensure their public channel gossip reaches all peers, especially in LSP/public-node topologies. Reviewers may want to confirm that honoring peer filters for non-own gossip remains intact and that the new our_node_id field cannot be confused across multiple node_signer identities.
Security signals we found
Information-disclosure / routing-visibility issue: own public channel gossip could be suppressed by a single peer's filter, isolating channel announcements from the wider network
Protocol-layer fix in BOLT 7 gossip forwarding logic
New test covers the bypass behavior for own-channel gossip
Evidence from the diff
The patch modifies PeerManager in lightning/src/ln/peer_handler.rs to bypass peer gossip filters for messages involving the local node. It stores our_node_id in PeerManager and uses it to compute our_channel / our_announcement flags for ChannelAnnouncement, ChannelUpdate, and NodeAnnouncement broadcasts. When the local node is a counterparty (or the announced node), the message is sent regardless of peer.should_forward_channel_announcement() or should_forward_node_announcement(). A unit test verifies that a peer with a dropped GossipTimestampFilter still receives channel updates for the sender’s own channel but not unrelated gossip.
Changed components
lightning/src/ln/peer_handler.rsPeerManager gossip broadcast paths (ChannelAnnouncement, ChannelUpdate, NodeAnnouncement)Inspect captured patch +90 / −10
diff --git a/lightning/src/ln/peer_handler.rs b/lightning/src/ln/peer_handler.rs
index d0a3a74..4c379c2 100644
--- a/lightning/src/ln/peer_handler.rs
+++ b/lightning/src/ln/peer_handler.rs
@@ -1114,6 +1114,7 @@ pub struct PeerManager<
gossip_processing_backlog_lifted: AtomicBool,
node_signer: NS,
+ our_node_id: NodeId,
logger: L,
secp_ctx: Secp256k1<secp256k1::SignOnly>,
@@ -1328,6 +1329,9 @@ where
let ephemeral_hash = Sha256::from_engine(ephemeral_key_midstate.clone()).to_byte_array();
secp_ctx.seeded_randomize(&ephemeral_hash);
+ let our_node_pubkey =
+ node_signer.get_node_id(Recipient::Node).expect("node_id must be available");
+
PeerManager {
message_handler,
peers: FairRwLock::new(new_hash_map()),
@@ -1339,6 +1343,7 @@ where
gossip_processing_backlog_lifted: AtomicBool::new(false),
last_node_announcement_serial: AtomicU32::new(current_time),
logger,
+ our_node_id: NodeId::from_pubkey(&our_node_pubkey),
node_signer,
secp_ctx,
}
@@ -2667,12 +2672,16 @@ where
BroadcastGossipMessage::ChannelAnnouncement(ref msg) => {
log_gossip!(self.logger, "Sending message to all peers except {:?} or the announced channel's counterparties: {:?}", except_node, msg);
let encoded_msg = encode_msg!(msg);
+ let our_channel = self.our_node_id == msg.contents.node_id_1
+ || self.our_node_id == msg.contents.node_id_2;
for (_, peer_mutex) in peers.iter() {
let mut peer = peer_mutex.lock().unwrap();
- if !peer.handshake_complete()
- || !peer.should_forward_channel_announcement(msg.contents.short_channel_id)
- {
+ if !peer.handshake_complete() {
+ continue;
+ }
+ let scid = msg.contents.short_channel_id;
+ if !our_channel && !peer.should_forward_channel_announcement(scid) {
continue;
}
debug_assert!(peer.their_node_id.is_some());
@@ -2711,12 +2720,15 @@ where
msg
);
let encoded_msg = encode_msg!(msg);
+ let our_announcement = self.our_node_id == msg.contents.node_id;
for (_, peer_mutex) in peers.iter() {
let mut peer = peer_mutex.lock().unwrap();
- if !peer.handshake_complete()
- || !peer.should_forward_node_announcement(msg.contents.node_id)
- {
+ if !peer.handshake_complete() {
+ continue;
+ }
+ let node_id = msg.contents.node_id;
+ if !our_announcement && !peer.should_forward_node_announcement(node_id) {
continue;
}
debug_assert!(peer.their_node_id.is_some());
@@ -2745,7 +2757,7 @@ where
peer.gossip_broadcast_buffer.push_back(encoded_message);
}
},
- BroadcastGossipMessage::ChannelUpdate { msg, node_id_1: _, node_id_2: _ } => {
+ BroadcastGossipMessage::ChannelUpdate { msg, node_id_1, node_id_2 } => {
log_gossip!(
self.logger,
"Sending message to all peers except {:?}: {:?}",
@@ -2753,12 +2765,15 @@ where
msg
);
let encoded_msg = encode_msg!(msg);
+ let our_channel = self.our_node_id == *node_id_1 || self.our_node_id == *node_id_2;
for (_, peer_mutex) in peers.iter() {
let mut peer = peer_mutex.lock().unwrap();
- if !peer.handshake_complete()
- || !peer.should_forward_channel_announcement(msg.contents.short_channel_id)
- {
+ if !peer.handshake_complete() {
+ continue;
+ }
+ let scid = msg.contents.short_channel_id;
+ if !our_channel && !peer.should_forward_channel_announcement(scid) {
continue;
}
debug_assert!(peer.their_node_id.is_some());
@@ -4890,6 +4905,71 @@ mod tests {
assert_eq!(filter_addresses(None), None);
}
+ #[test]
+ fn test_forward_gossip_for_our_channels_ignores_peer_filter() {
+ // Tests that gossip for channels where we are one of the endpoints is forwarded to all
+ // peers, regardless of any gossip filters they may have set. This ensures that updates
+ // for our own channels always propagate to all connected peers.
+
+ let cfgs = create_peermgr_cfgs(2);
+ let peers = create_network(2, &cfgs);
+
+ let id_0 = peers[0].node_signer.get_node_id(Recipient::Node).unwrap();
+
+ // Connect the peers and exchange the initial connection handshake (but not the final Init
+ // message).
+ let (mut fd_0_1, mut fd_1_0) = establish_connection(&peers[0], &peers[1]);
+
+ // Once peer 1 receives the Init message in the last read_event, it'll generate a
+ // `GossipTimestampFilter` which will request gossip. Instead we drop it here.
+ cfgs[1]
+ .routing_handler
+ .pending_events
+ .lock()
+ .unwrap()
+ .retain(|ev| !matches!(ev, MessageSendEvent::SendGossipTimestampFilter { .. }));
+
+ peers[1].process_events();
+ let data_1_0 = fd_1_0.outbound_data.lock().unwrap().split_off(0);
+ peers[0].read_event(&mut fd_0_1, &data_1_0).unwrap(); // Init message
+
+ peers[0].process_events();
+ assert!(fd_0_1.outbound_data.lock().unwrap().is_empty());
+ assert!(fd_1_0.outbound_data.lock().unwrap().is_empty());
+
+ let mut check_message_received = |expected_received: bool| {
+ let initial_count = cfgs[1].routing_handler.chan_upds_recvd.load(Ordering::Acquire);
+
+ peers[0].process_events();
+ let data_0_1 = fd_0_1.outbound_data.lock().unwrap().split_off(0);
+ assert_eq!(data_0_1.is_empty(), !expected_received);
+ peers[1].read_event(&mut fd_1_0, &data_0_1).unwrap();
+
+ let final_count = cfgs[1].routing_handler.chan_upds_recvd.load(Ordering::Acquire);
+ assert_eq!(final_count > initial_count, expected_received);
+ };
+
+ // Broadcast a gossip message that is unrelated to us and check that it doesn't get relayed
+ let unrelated_msg_ev = MessageSendEvent::BroadcastChannelUpdate {
+ msg: test_utils::get_dummy_channel_update(43),
+ node_id_1: NodeId::from_slice(&[2; 33]).unwrap(),
+ node_id_2: NodeId::from_slice(&[3; 33]).unwrap(),
+ };
+ cfgs[0].routing_handler.pending_events.lock().unwrap().push(unrelated_msg_ev);
+
+ check_message_received(false);
+
+ // Broadcast a gossip message that we're a party to and check that its relayed
+ let our_channel_msg_ev = MessageSendEvent::BroadcastChannelUpdate {
+ msg: test_utils::get_dummy_channel_update(43),
+ node_id_1: NodeId::from_pubkey(&id_0),
+ node_id_2: NodeId::from_slice(&[3; 33]).unwrap(),
+ };
+ cfgs[0].routing_handler.pending_events.lock().unwrap().push(our_channel_msg_ev);
+
+ check_message_received(true);
+ }
+
#[test]
#[cfg(feature = "std")]
fn test_process_events_multithreaded() {
Why this scored 36/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.