Discovery
The Discovery subsystem handles gossip-based peer discovery and catalog synchronization. It maintains a network-wide view of all peers, their capabilities, and their available streams and groups.
Overview
Discovery uses three complementary mechanisms:
| Mechanism | Transport | Purpose |
|---|---|---|
| DHT Bootstrap | Mainline DHT (pkarr) | Automatic peer discovery via shared NetworkId |
| Announce | /mosaik/announce | Real-time gossip broadcasts via iroh-gossip |
| Catalog Sync | /mosaik/catalog-sync | Full bidirectional catalog exchange for catch-up |
Nodes sharing the same NetworkId automatically discover each other through the DHT — no hardcoded bootstrap peers are required. Once an initial connection is established, the Announce and Catalog Sync protocols take over for real-time updates.
See the DHT Bootstrap sub-chapter for details on the automatic discovery mechanism.
Configuration
Configure discovery through the Network builder:
use mosaik::{Network, discovery};
use std::time::Duration;
let network = Network::builder(network_id)
.with_discovery(
discovery::Config::builder()
.with_bootstrap(bootstrap_addr)
.with_tags("matcher")
.with_tags(["validator", "signer"]) // additive
.with_purge_after(Duration::from_secs(600))
.with_announce_interval(Duration::from_secs(10))
.with_announce_jitter(0.3)
.with_max_time_drift(Duration::from_secs(5))
.with_events_backlog(200)
)
.build()
.await?;
Configuration Options
| Field | Default | Description |
|---|---|---|
bootstrap_peers | [] | Initial peers to connect to on startup |
tags | [] | Tags to advertise about this node |
announce_interval | 15s | How often to re-announce via gossip |
announce_jitter | 0.5 | Max jitter factor (0.0–1.0) for announce timing |
purge_after | 300s | Duration after which stale entries are purged |
max_time_drift | 10s | Maximum acceptable clock drift between peers |
events_backlog | 100 | Past events retained in event broadcast channel |
dht_publish_interval | 300s | How often to publish to the DHT (None to disable) |
dht_poll_interval | 60s | How often to poll the DHT for peers (None to disable) |
Both with_bootstrap() and with_tags() are additive — calling them multiple times adds to the list.
Accessing Discovery
let discovery = network.discovery();
The Discovery handle is cheap to clone.
Core API
Catalog Access
// Get current snapshot of all known peers
let catalog = discovery.catalog();
for (peer_id, entry) in catalog.iter() {
println!("{}: tags={:?}", peer_id, entry.tags);
}
// Watch for catalog changes
let mut watch = discovery.catalog_watch();
loop {
watch.changed().await?;
let catalog = watch.borrow();
println!("Catalog updated: {} peers", catalog.len());
}
Dialing Peers
// Connect to bootstrap peers manually
discovery.dial(bootstrap_addr);
// Dial multiple peers
discovery.dial([addr1, addr2, addr3]);
Manual Sync
// Trigger a full catalog sync with a specific peer
discovery.sync_with(peer_addr).await?;
Managing Tags
// Add tags at runtime
discovery.add_tags("new-role");
discovery.add_tags(["role-a", "role-b"]);
// Remove tags
discovery.remove_tags("old-role");
Changing tags triggers an immediate re-announcement to the network.
Local Entry
// Get this node's signed entry (as others see it)
let my_entry = discovery.me();
println!("I am: {:?}", my_entry);
Unsigned Entries
For testing or manual feeds, you can insert entries that aren’t cryptographically signed:
use mosaik::discovery::PeerEntry;
// Insert an unsigned entry (local-only, not gossiped)
discovery.insert(PeerEntry { /* ... */ });
// Remove a specific peer
discovery.remove(peer_id);
// Clear all unsigned entries
discovery.clear_unsigned();
Injecting Signed Entries
// Feed a signed entry from an external source
let success = discovery.feed(signed_peer_entry);
Events
Subscribe to discovery lifecycle events:
let mut events = discovery.events();
while let Ok(event) = events.recv().await {
match event {
Event::PeerDiscovered(entry) => {
println!("New peer: {}", entry.peer_id());
}
Event::PeerUpdated(entry) => {
println!("Peer updated: {}", entry.peer_id());
}
Event::PeerDeparted(peer_id) => {
println!("Peer left: {}", peer_id);
}
}
}
See the Events sub-chapter for details.
How It Works
Announce Protocol
Every node periodically broadcasts its SignedPeerEntry via iroh-gossip. The announce includes:
- Node identity (PeerId)
- Network ID
- Tags
- Available streams and groups
- Version (start + update timestamps)
The jitter on the announce interval prevents all nodes from announcing simultaneously. When a node changes its metadata (adds a tag, creates a stream, joins a group), it re-announces immediately.
Catalog Sync Protocol
When a new node connects to a peer, they exchange their full catalogs bidirectionally. This ensures a new node quickly learns about all existing peers without waiting for gossip cycles.
Signed vs Unsigned Entries
| Property | Signed | Unsigned |
|---|---|---|
| Source | Created by the peer itself | Injected locally |
| Verification | Cryptographic signature | None |
| Gossip | Yes — propagated network-wide | No — local only |
| Use case | Normal operation | Testing, manual feeds |
Staleness Detection
Each entry has a two-part version: (start_timestamp, update_timestamp). If update_timestamp falls behind the current time by more than purge_after, the entry is hidden from the public catalog API and eventually removed.
See the Catalog sub-chapter for the full catalog API.