The network

Discovery & bootstrap.

How a client and a worker find each other without either of them being told an address by hand — and why the node that introduces them is a door, not a gatekeeper.

The shape of it

Introductions there, jobs direct.

A bootstrap node answers "who else is here" and relays for peers stuck behind NAT. Once two peers have found each other they connect directly, and the bootstrap node is not involved in the job at all.

rootmode-bootstrap entry point + relay · no directory, sees no jobs join / publish join / look up rootmode-worker "I do llm, and here are my models" desktop client "who does llm?" direct connection — jobs never touch the bootstrap node

On your own network

Nothing is configured. At all.

Start a worker. Open the client. The worker appears. That's the whole procedure — no addresses, no bootstrap node, nothing pasted.

Both announce themselves over mDNS, the same mechanism that makes printers and Chromecasts show up. The client reacts to the announcement the moment it arrives rather than polling, so a worker started while the app is open appears within a second. For one person and their own machines, this covers the case completely.

Beyond the LAN

To find a peer you need a peer.

Every peer-to-peer network has the same chicken-and-egg problem, and the universal answer is to ship entry points in the binary — IPFS does it, Ethereum does it, BitTorrent does it. rootmode does it in DEFAULT_BOOTSTRAP.

crates/rootmode-p2p/src/lib.rs
pub const DEFAULT_BOOTSTRAP: &[&str] = &[
    "/dns4/bootstrap.rootmode.ai/tcp/4001",
];

With that filled in, docker run … rootmode-worker joins the network and starts receiving jobs, and opening the client lists what people are serving. Neither needs configuring. ROOTMODE_BOOTSTRAP overrides the list at runtime for both binaries — which is how you test a private network without touching the source.

The list is empty until you run bootstrap nodes and add their addresses.

Until then there's no open network to join, only the local-network discovery above. That's an operational step, not a code one: stand up one or more nodes on stable public addresses, then paste what they print into the constant and rebuild.

The /p2p/12D3Koo… suffix is optional. Including it means libp2p refuses to talk to anything else at that address, which is stronger. Leaving it out means the address is a DNS name you can re-key without reissuing builds — at the cost that a hostile node in that position could show you a biased view of who's on the network. It still couldn't read your jobs, forge a worker, or defeat a pinned peer: every peer authenticates with its own key and every result is checked against its hash.

What it is, and isn't

An ordinary peer with a stable address.

You dial it once to join; it tells you about peers near you, and from then on lookups are peer-to-peer. Three things it is not:

Not a registry

Advertisements are stored across the DHT by key, not handed to it for safekeeping. It holds the ones that happen to land near its node id, like any other participant.

Not an authority

It cannot approve, deny, or evict anyone.

Not in the data path

Once two peers have found each other they connect directly, and it takes no part in the job.

It does one extra job: it runs a relay, so a worker that can't accept an inbound connection is still reachable. Even then it only brokers the connection — the two peers attempt a direct upgrade and the traffic moves off the relay. Run more than one; any node can be one. If every bootstrap node vanishes, running peers keep working and only new joins are affected.

Running one

One container, one open port.

bootstrap node
$ docker build -f docker/Dockerfile.bootstrap -t rootmode-bootstrap .

$ docker run -d --name rootmode-bootstrap \
    -p 4001:4001 \
    -v rootmode-bootstrap:/var/lib/rootmode \
    rootmode-bootstrap \
      --listen /ip4/0.0.0.0/tcp/4001 \
      --key /var/lib/rootmode/bootstrap.key \
      --announce /dns4/bootstrap.example.com/tcp/4001

$ docker logs rootmode-bootstrap
peer id   12D3KooWEYBDUKv5KLXV2oL3TgLjU4HypJ9KVv7DPjUo5u5gHCCV
give this to workers and clients:
  /dns4/bootstrap.example.com/tcp/4001/p2p/12D3KooWEYBDUKv5…
Mount the volume.

It holds bootstrap.key, and the peer id is derived from it. Lose the key and the address everyone is configured with stops working.

--announce is what gets printed for others to use. Without it the node prints the addresses it bound to, which inside a container are the container's own and no use to anybody outside. Open the port — TCP, whatever you passed to --listen. docker compose up -d bootstrap does the same thing from the compose file.

Pointing nodes at it

Two sides, four lines of config.

A worker

worker.toml
[p2p]
enabled   = true
bootstrap = ["/dns4/bootstrap.example.com/tcp/4001/p2p/12D3Koo…"]
listen    = ["/ip4/0.0.0.0/tcp/4101"]
relay      = true    # leave on if this box is behind NAT
dht_server = false   # turn on only with a public address

On start it joins, then publishes two kinds of record — rootmode/cap/llm and rootmode/cap/image for what it can do, and rootmode/model/<id> for each model it actually serves. Those come from the backends, so a worker advertises what its vLLM or ComfyUI really has, not what the config wishes for. It republishes periodically, so a node that stays up stays findable.

A client

Settings → discovery → paste the bootstrap address, one per line. The client then asks the network who serves llm and image every 90 seconds, and any peer it finds appears on the peers screen marked discovered, alongside peers added by hand. Discovery hands you a key, never an address — the network resolves the key to a route. That's why discovered endpoints look like p2p://<64 hex characters>.

Identity

One key, two encodings.

A rootmode peer id is a hex ed25519 public key. A libp2p PeerId is a multihash of the same key — and because ed25519 keys are short, libp2p embeds the key rather than hashing it. Same identity, written two ways:

peer_idef9084b8a01c18be4b75fb7e0b72b7b4d57b5be99dddf2c430e4db01a43ea8a7
libp2p12D3KooWRwXRPuPux1G6SZaxcGobMHx4qbcZmXfjPGgcPQUbk8dC

This is why pinning still works over discovery. Paste a peer id into a peer's public key field and the connection is checked twice: the libp2p connection is cryptographically bound to the key, and the announce it sends must match. Either mismatch refuses the connection.

Discovery is not trust.

Anyone can advertise anything. A DHT record is a claim, not a credential — a node can announce llama-3.1-405b and serve nonsense. Discovery gets you candidates, not trust. What helps today: pin the keys of peers you actually trust, and use the worker's allow_peers on the other side. What isn't built yet: signed result receipts, reputation, and any notion of payment. Treat a discovered peer as a stranger, because it is one.

Troubleshooting

When they can't see each other.

Client finds nothing

Check the worker logged announcing [...] to the network, and that both sides point at the same bootstrap address — including the same /p2p/ suffix.

Publishing needs a moment after start: the worker waits for the bootstrap connection before its first announce.

Discovered, but offline

The client found the advertisement but can't reach the node. If the worker is behind NAT, set relay = true and make sure the bootstrap node is reachable from both sides.

In Docker with published ports rather than --network host, the worker advertises its container address. Set ROOTMODE_P2P_EXTERNAL (or [p2p] external) to the address others should actually dial.

Discovered, online, but the wrong peer

That's mismatch: the key you pinned is not the key that answered. Do not send it a job.