Skip to content

fix(bootstrap): explicit bootstrap peers must override the static discovery list - #63

Merged
xzyaoi merged 1 commit into
eth-easl:mainfrom
swiss-ai:fix/bootstrap-explicit-precedence
Sep 6, 2026
Merged

fix(bootstrap): explicit bootstrap peers must override the static discovery list#63
xzyaoi merged 1 commit into
eth-easl:mainfrom
swiss-ai:fix/bootstrap-explicit-precedence

Conversation

@AryanAhadinia

Copy link
Copy Markdown
Contributor

Problem

collectBootstrapSources() unions every configured source, so --bootstrap.addr is appended to bootstrap.static rather than replacing it. Because bootstrap.static defaults to the public discovery endpoints (root.go), a node told to bootstrap from one specific peer joins the public network as well as the one it was pointed at.

This is not cosmetic:

  • All nodes share the single ocf-crdt pubsub topic (crdt.go), so records from both deployments merge into one CRDT DAG.
  • libp2p keys on peer ID, not address. A private bootstrap node whose peer ID is also advertised publicly becomes indistinguishable from the public host — the node "reaches" its configured bootstrap peer ID without ever connecting to the host the operator named, and its records never converge with it.

Observed with a node started as --bootstrap.addr /ip4/<private-host>/tcp/43905/p2p/<id>:

protocol/bootstrap.go:45  Bootstrap peers: [
  /ip4/<other-host>/tcp/43905/p2p/<id>
  /dns4/p2p.opentela.ai/tcp/443/wss/p2p/<id>
  /ip4/140.238.223.116/tcp/43905/p2p/<other-id>
  /ip4/<private-host>/tcp/43905/p2p/<id>          ← the only one asked for
]

It registered into the public DAG and published its labels there, while the bootstrap host it was given never saw it.

The intent already exists in the code

getDefaultBootstrapPeers() has a bootstrapAddrs != nil branch that uses only the addresses handed to it and ignores the static list — exactly the wanted semantics. But all three production callers pass nil (crdt.go:67, crdt.go:221, host.go:426), so that branch is unreachable outside bootstrap_test.go. It is covered by a passing test while being dead in the shipped binary, which is why the behaviour reads as correct on review.

Change

Rather than thread addresses through those three call sites and duplicate the collection logic, make precedence explicit where sources are gathered: bootstrap.sources / bootstrap.source / bootstrap.addrs / bootstrap.addr win, and bootstrap.static is consulted only when none is set.

A default node with no explicit configuration is unaffected and still uses the static discovery list.

Tests

Three added: an explicit bootstrap.addr overrides the static list; an explicit bootstrap.source does the same; and the static list is still used when nothing explicit is configured, so the fix cannot silently disable discovery.

They reuse testPeerID with differing IPs, which is safe because DeduplicateStrings and parseBootstrapMultiaddrs both dedupe on the full address string — a union would surface as len(res) == 2.

Happy to switch to the call-site approach instead if you'd prefer the dead branch become the live one.

🤖 Generated with Claude Code

collectBootstrapSources() unions every source, so --bootstrap.addr is
appended to bootstrap.static rather than replacing it. Since
bootstrap.static defaults to the public discovery endpoints
(bootstraps.opentela.ai and the two IP endpoints in root.go), a node
told to bootstrap from one specific peer joins the public network *as
well as* the one it was pointed at.

That is not cosmetic. All nodes share the single "ocf-crdt" pubsub topic
(crdt.go), so records from both deployments merge into one CRDT DAG. And
because libp2p keys on peer ID rather than address, a private bootstrap
node whose peer ID is also advertised publicly becomes indistinguishable
from the public host: the node "reaches" its configured bootstrap peer ID
without ever connecting to the host the operator named, and its records
never converge with it.

Observed in practice: a node started with
--bootstrap.addr /ip4/<private-host>/tcp/43905/p2p/<id> logged

    Bootstrap peers: [/ip4/<other-host>/tcp/43905/p2p/<id>
                      /dns4/p2p.opentela.ai/tcp/443/wss/p2p/<id> ...
                      /ip4/<private-host>/tcp/43905/p2p/<id>]

registered into the public DAG, and published its labels there, while
the bootstrap host it was given never saw it.

There is already a code path with the intended semantics —
getDefaultBootstrapPeers()'s `bootstrapAddrs != nil` branch uses only the
addresses it is handed and ignores the static list — but all three
production callers (crdt.go:67, crdt.go:221, host.go:426) pass nil, so it
is unreachable outside bootstrap_test.go.

Rather than thread the addresses through those call sites and duplicate
the collection logic, make precedence explicit where sources are
gathered: bootstrap.sources / bootstrap.source / bootstrap.addrs /
bootstrap.addr win, and bootstrap.static is consulted only when none of
them is set. A default node with no explicit configuration is unaffected
and still uses the static discovery list.

Adds three tests: an explicit bootstrap.addr overrides the static list, an
explicit bootstrap.source does the same, and the static list is still used
when nothing explicit is configured (so the fix cannot silently disable
discovery).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@xzyaoi
xzyaoi self-requested a review September 6, 2026 11:20
@xzyaoi
xzyaoi merged commit 1111d5d into eth-easl:main Sep 6, 2026
2 checks passed
@xzyaoi
xzyaoi deleted the fix/bootstrap-explicit-precedence branch September 6, 2026 11:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants