Building a Bootstrap Node
This tutorial walks through the bootstrap example — a ready-to-use bootstrap node for any mosaik network.
What Is a Bootstrap Node?
A bootstrap node is the first peer that other nodes connect to when joining a network. It serves as the initial discovery point — once a new node connects to a bootstrap peer, the gossip protocol takes over and the joining node learns about all other peers.
Bootstrap nodes are typically:
- Long-lived — they run continuously
- Stable identity — they use a fixed secret key so their address doesn’t change across restarts
- Well-known — their address is configured as a bootstrap peer by other nodes
A bootstrap node doesn’t need any special code — it’s just a regular mosaik node that stays online. The bootstrap example can be used in production as a universal bootstrap node for any mosaik network. The example adds CLI configuration with clap.
Project Setup
The bootstrap example is a single file at examples/bootstrap.rs. It uses these dependencies:
[dependencies]
mosaik = "0.2"
tokio = { version = "1", features = ["full"] }
clap = { version = "4", features = ["derive"] }
tracing = "0.1"
tracing-subscriber = "0.3"
anyhow = "1"
The CLI Interface
The example uses clap derive to define command-line options:
use clap::{ArgAction, Parser};
use mosaik::*;
#[derive(Debug, Parser)]
struct Opts {
/// The secret key for stable identity across restarts
#[clap(short, long, env = "MOSAIK_BOOTSTRAP_SECRET")]
secret: Option<SecretKey>,
/// The network ID (hex string or seed that gets hashed)
#[clap(short, long, env = "MOSAIK_BOOTSTRAP_NETWORK_ID")]
network_id: Option<NetworkId>,
/// Other bootstrap nodes to connect to on startup
#[clap(short, long, env = "MOSAIK_BOOTSTRAP_PEERS")]
peers: Vec<PeerId>,
/// Tags to advertise (default: "bootstrap")
#[clap(short, long, default_value = "bootstrap",
env = "MOSAIK_BOOTSTRAP_TAGS")]
tags: Vec<Tag>,
/// Disable relay servers (node must be directly reachable)
#[clap(long, default_value_t = false)]
no_relay: bool,
/// Verbose output (-v debug, -vv trace)
#[clap(short, action = ArgAction::Count)]
verbose: u8,
/// Suppress all output
#[clap(short, long)]
quiet: bool,
}
Every option also supports environment variables (MOSAIK_BOOTSTRAP_*), making it easy to configure in containers or systemd services.
Secret Key Handling
The secret key determines the node’s PeerId. For a bootstrap node, a stable identity is essential so that other nodes can reliably find it:
fn parse_secret_key(s: &str) -> Result<SecretKey, Infallible> {
let bytes = Digest::from(s);
Ok(SecretKey::from_bytes(bytes.as_bytes()))
}
This parser accepts two formats:
- 64-character hex string — used directly as the secret key bytes
- Any other string — treated as a seed, hashed with blake3 into a deterministic key
So --secret=my-bootstrap-1 always produces the same key, making deployment reproducible.
Building the Network
The main function assembles the Network using the builder:
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let opts = Opts::parse();
let secret = opts.secret.unwrap_or_else(|| {
tracing::warn!("No secret key provided, generating random key");
SecretKey::generate(&mut rand::rng())
});
let network_id = opts.network_id.unwrap_or_else(|| {
tracing::warn!("No network id provided, generating random network id");
NetworkId::random()
});
let mut builder = Network::builder(network_id)
.with_secret_key(secret)
.with_discovery(
discovery::Config::builder()
.with_tags(opts.tags.clone())
.with_bootstrap(opts.peers.clone()),
);
if opts.no_relay {
builder = builder.with_relay_mode(iroh::RelayMode::Disabled);
}
let network = builder.build().await?;
tracing::info!("Bootstrap node started");
tracing::info!("Public Id: {}", network.local().id());
tracing::info!("Network Id: {:?}", network.network_id());
// Stay alive forever
core::future::pending::<()>().await;
Ok(())
}
Key points:
with_secret_key()— sets the stable identitywith_discovery()— configures tags and initial peers to dialwith_relay_mode(Disabled)— optional, for nodes with direct connectivitycore::future::pending()— keeps the process alive (the node runs in background tasks)
Running It
# Basic usage with a seed-based secret
cargo run --example bootstrap -- \
--network-id=my-network \
--secret=my-bootstrap-secret
# With environment variables
MOSAIK_BOOTSTRAP_SECRET=my-secret \
MOSAIK_BOOTSTRAP_NETWORK_ID=my-network \
cargo run --example bootstrap
# Multiple bootstrap nodes that know each other
cargo run --example bootstrap -- \
--network-id=my-network \
--secret=node-1 \
--peers=<peer-id-of-node-2>
Using the Bootstrap Node
Other nodes reference the bootstrap node’s address when joining the network:
let network = Network::builder(network_id)
.with_discovery(
discovery::Config::builder()
.with_bootstrap(bootstrap_addr)
)
.build()
.await?;
The joining node connects to the bootstrap peer, performs a full catalog sync, and then discovers all other nodes through gossip. From that point on, the joining node is a full participant — it doesn’t need the bootstrap node for ongoing operation.
Key Takeaways
- Bootstrap nodes are just regular nodes — no special server code needed
- Stable identity via secret key — ensures the address doesn’t change across restarts
- Tags for discoverability — the
"bootstrap"tag lets other nodes identify bootstrap peers - Minimal configuration — a secret key and network ID are all that’s required