fix(bootstrap): explicit bootstrap peers must override the static discovery list - #63
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
collectBootstrapSources()unions every configured source, so--bootstrap.addris appended tobootstrap.staticrather than replacing it. Becausebootstrap.staticdefaults 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:
ocf-crdtpubsub topic (crdt.go), so records from both deployments merge into one CRDT DAG.Observed with a node started as
--bootstrap.addr /ip4/<private-host>/tcp/43905/p2p/<id>: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 abootstrapAddrs != nilbranch that uses only the addresses handed to it and ignores the static list — exactly the wanted semantics. But all three production callers passnil(crdt.go:67,crdt.go:221,host.go:426), so that branch is unreachable outsidebootstrap_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.addrwin, andbootstrap.staticis 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.addroverrides the static list; an explicitbootstrap.sourcedoes the same; and the static list is still used when nothing explicit is configured, so the fix cannot silently disable discovery.They reuse
testPeerIDwith differing IPs, which is safe becauseDeduplicateStringsandparseBootstrapMultiaddrsboth dedupe on the full address string — a union would surface aslen(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