Skip to content

Commit b171ff4

Browse files
committed
fix(network): add mesh repair mechanism for gossipsub
- Add mesh_peer_count() and topic_peer_count() to monitor mesh status - Add refresh_subscription() to force SUBSCRIBE exchange with peers - Add repair_mesh_if_needed() that runs every 10s to fix empty mesh - Trigger subscription refresh when new platform validator is identified - Fixes issue where validators joining existing network don't form mesh
1 parent d621eb8 commit b171ff4

2 files changed

Lines changed: 109 additions & 6 deletions

File tree

crates/network/src/behaviour.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,46 @@ impl MiniChainBehaviour {
123123
.map_err(|e| anyhow::anyhow!("Publish error: {:?}", e))?;
124124
Ok(())
125125
}
126+
127+
/// Get the number of peers in the mesh for our topic
128+
pub fn mesh_peer_count(&self) -> usize {
129+
let topic = gossipsub::IdentTopic::new(GOSSIP_TOPIC);
130+
self.gossipsub.mesh_peers(&topic.hash()).count()
131+
}
132+
133+
/// Get all peers subscribed to our topic (including those not in mesh)
134+
pub fn topic_peer_count(&self) -> usize {
135+
let topic_hash = gossipsub::IdentTopic::new(GOSSIP_TOPIC).hash();
136+
self.gossipsub
137+
.all_peers()
138+
.filter(|(_, topics)| topics.iter().any(|t| **t == topic_hash))
139+
.count()
140+
}
141+
142+
/// Force re-subscribe to refresh SUBSCRIBE messages to all peers
143+
/// This is useful when new peers join and don't receive our subscription
144+
pub fn refresh_subscription(&mut self) -> anyhow::Result<()> {
145+
let topic = IdentTopic::new(GOSSIP_TOPIC);
146+
// Unsubscribe and re-subscribe to force sending SUBSCRIBE to all peers
147+
let _ = self.gossipsub.unsubscribe(&topic);
148+
self.gossipsub
149+
.subscribe(&topic)
150+
.map_err(|e| anyhow::anyhow!("Re-subscribe error: {:?}", e))?;
151+
Ok(())
152+
}
153+
154+
/// Add a peer to the mesh by sending GRAFT
155+
/// Note: This only works if the peer is already subscribed to the topic
156+
pub fn add_peer_to_mesh(&mut self, peer_id: &libp2p::PeerId) {
157+
// Add as explicit peer temporarily to force mesh inclusion
158+
// Then remove to allow normal mesh behavior
159+
self.gossipsub.add_explicit_peer(peer_id);
160+
}
161+
162+
/// Remove explicit peer status (allows normal mesh behavior)
163+
pub fn remove_explicit_peer(&mut self, peer_id: &libp2p::PeerId) {
164+
self.gossipsub.remove_explicit_peer(peer_id);
165+
}
126166
}
127167

128168
/// Sync request message

crates/network/src/node.rs

Lines changed: 69 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,48 @@ impl NetworkNode {
275275
}
276276
}
277277

278+
/// Repair the gossipsub mesh if it's empty but we have connected peers
279+
/// This handles the case where SUBSCRIBE messages weren't exchanged properly
280+
pub fn repair_mesh_if_needed(&mut self) {
281+
let connected_peers = self.peers.read().len();
282+
let mesh_peers = self.swarm.behaviour().mesh_peer_count();
283+
let topic_peers = self.swarm.behaviour().topic_peer_count();
284+
285+
// Log mesh status for debugging
286+
if connected_peers > 0 {
287+
debug!(
288+
"Mesh status: {} connected, {} in topic, {} in mesh",
289+
connected_peers, topic_peers, mesh_peers
290+
);
291+
}
292+
293+
// If we have connected peers but none in the topic, force re-subscribe
294+
// This sends our SUBSCRIBE to all connected gossipsub peers
295+
if connected_peers > 0 && topic_peers == 0 {
296+
info!(
297+
"Mesh repair: {} peers connected but none subscribed to topic, refreshing subscription",
298+
connected_peers
299+
);
300+
if let Err(e) = self.swarm.behaviour_mut().refresh_subscription() {
301+
warn!("Failed to refresh subscription: {}", e);
302+
}
303+
}
304+
305+
// If we have peers in topic but mesh is still empty, add them explicitly
306+
// This happens when GRAFT messages fail to form the mesh
307+
if topic_peers > 0 && mesh_peers == 0 {
308+
info!(
309+
"Mesh repair: {} peers in topic but mesh empty, adding peers explicitly",
310+
topic_peers
311+
);
312+
// Get list of connected peers and add them
313+
let peers_to_add: Vec<PeerId> = self.peers.read().iter().cloned().collect();
314+
for peer_id in peers_to_add {
315+
self.swarm.behaviour_mut().add_peer_to_mesh(&peer_id);
316+
}
317+
}
318+
}
319+
278320
/// Broadcast a message via gossip
279321
pub fn broadcast(&mut self, message: &SignedNetworkMessage) -> anyhow::Result<()> {
280322
let data = bincode::serialize(message)?;
@@ -314,10 +356,14 @@ impl NetworkNode {
314356

315357
/// Run the event loop (should be spawned as a task)
316358
/// Includes automatic retry of bootstrap peers every 30 seconds if not connected
359+
/// and mesh repair every 10 seconds
317360
pub async fn run(&mut self) {
318361
let mut bootstrap_retry_interval = tokio::time::interval(Duration::from_secs(30));
319362
bootstrap_retry_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
320363

364+
let mut mesh_repair_interval = tokio::time::interval(Duration::from_secs(10));
365+
mesh_repair_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
366+
321367
loop {
322368
tokio::select! {
323369
event = self.swarm.select_next_some() => {
@@ -326,6 +372,9 @@ impl NetworkNode {
326372
_ = bootstrap_retry_interval.tick() => {
327373
self.retry_bootstrap_if_needed();
328374
}
375+
_ = mesh_repair_interval.tick() => {
376+
self.repair_mesh_if_needed();
377+
}
329378
}
330379
}
331380
}
@@ -483,6 +532,7 @@ impl NetworkNode {
483532
// Extract hotkey from agent_version if present
484533
// Format: "platform-validator/1.0.0/HOTKEY_HEX"
485534
let hotkey = info.agent_version.split('/').nth(2).map(String::from);
535+
let is_platform_validator = info.agent_version.starts_with("platform-validator/");
486536

487537
info!(
488538
"Identify received from {}: agent={}, hotkey={:?}",
@@ -501,12 +551,25 @@ impl NetworkNode {
501551
})
502552
.await;
503553

504-
// NOTE: Do NOT call add_explicit_peer here!
505-
// Explicit peers become "direct peers" that bypass the gossipsub mesh.
506-
// The mesh should form automatically via the gossipsub protocol:
507-
// 1. Connection established -> gossipsub protocol negotiated
508-
// 2. SUBSCRIBE messages exchanged -> peers know each other's topics
509-
// 3. GRAFT/PRUNE in heartbeats -> mesh forms naturally
554+
// If this is a platform validator, check mesh status and trigger refresh if needed
555+
// This ensures that new validators joining the network get properly added to the mesh
556+
if is_platform_validator {
557+
let mesh_peers = self.swarm.behaviour().mesh_peer_count();
558+
let topic_peers = self.swarm.behaviour().topic_peer_count();
559+
560+
debug!(
561+
"New platform validator {}: mesh has {} peers, topic has {} peers",
562+
peer_id, mesh_peers, topic_peers
563+
);
564+
565+
// If mesh is empty but we have this peer, refresh to exchange SUBSCRIBEs
566+
if mesh_peers == 0 {
567+
info!("Mesh empty after new validator joined, refreshing subscription");
568+
if let Err(e) = self.swarm.behaviour_mut().refresh_subscription() {
569+
warn!("Failed to refresh subscription: {}", e);
570+
}
571+
}
572+
}
510573

511574
// Also connect to other peers they know about through their observed addr
512575
// This helps with peer discovery in small networks

0 commit comments

Comments
 (0)