namesys is permanently locked out of an IPNS name if a pubsub RPC subscription joins its record topic first
Checklist
- This is a bug report, not a question.
- I have searched on the issue tracker for my bug.
- I am running the latest kubo version or have an issue updating.
Installation method
third-party binary (the kubo npm package)
Version
Kubo version: 0.43.0-e9914bb47
Repo version: 18
System version: amd64/linux
Golang version: go1.26.5Config
Two default --profile=test repos with Pubsub.Enabled=true and Ipns.UsePubsub=true. On the resolving daemon Routing.Type=none, so IPNS can only resolve over pubsub (with the DHT on, the DHT masks the failure but the namesys subscription is still never created, see below).
Description
If a client opens a pubsub sub on an IPNS record topic (/record/<base64url("/ipns/" + multihash)>) before namesys has resolved that name, namesys can never join the topic on that daemon: name resolve <name> fails with could not resolve name, name pubsub subs never lists the name, and cancelling the RPC subscription does not repair it. Only a daemon restart does. Nothing is logged, even with namesys and pubsub-valuestore at debug.
The topic name is deterministic from the IPNS name, so any application that uses the pubsub RPC to watch for IPNS record arrivals (a kubo-rpc-client user, for instance) can lock its own daemon out of a name with one ordering mistake.
Steps to reproduce
Daemon B is the publisher, daemon A the resolver. Both have pubsub enabled and are connected to each other; A has Routing.Type=none.
# B: two keys, both published
ipfs key gen victim
ipfs key gen control
CID=$(echo hello | ipfs add -q)
ipfs name publish --key=victim --allow-offline --ttl=1m /ipfs/$CID
ipfs name publish --key=control --allow-offline --ttl=1m /ipfs/$CIDThe record topic for a name is /record/ + base64url (no padding) of the bytes "/ipns/" + <peer id multihash bytes>, i.e. what KeyToTopic in go-libp2p-pubsub-router produces for the key /ipns/<mh>.
# A: control name, resolve first (namesys joins the topic)
ipfs name resolve --nocache <control>
# -> /ipfs/Qm... (0.2s)
ipfs name pubsub subs
# -> /ipns/<control>
# A: victim name, RPC-subscribe to its record topic first, then resolve
ipfs pubsub sub /record/<topic of victim> &
ipfs name resolve --nocache <victim>
# -> Error: could not resolve name (0.2s)
ipfs name pubsub subs
# -> only /ipns/<control>; the victim never appears
# A: cancel the RPC subscription, resolve again
kill %1
ipfs pubsub ls
# -> the victim topic is still listed
ipfs name resolve --nocache <victim>
# -> Error: could not resolve name
# A: restart the daemon, resolve first, THEN subscribe over the RPC
ipfs name resolve --nocache <victim>
# -> /ipfs/Qm...
ipfs pubsub sub /record/<topic of victim> &
# B republishes: the RPC stream receives the record and name resolve returns the new valueRoot cause
- The pubsub RPC subscribes with the deprecated
PubSub.Subscribe(topic)(core/coreapi/pubsub.go), which creates theTopichandle viatryJoinand keeps it inmyTopics. Cancelling the subscription only callsSubscription.Cancel(), neverTopic.Close(), so the handle stays for the life of the process. - namesys'
PubsubValueStore.Subscribe(go-libp2p-pubsub-router/pubsub.go,createTopicHandler) callsp.ps.Join(topic), which returnstopic already existswhen the handle is already inmyTopics(go-libp2p-pubsub/pubsub.go,Join). - That error propagates out of
GetValue/SearchValue, so the resolve fails, and it is not logged anywhere.
The reverse order works because Subscribe tolerates an existing topic handle (tryJoin returns the existing one) while Join does not.
Expected behavior
Either order should work: an RPC subscription on a record topic should not prevent namesys from resolving that name. Failing that, the error should at least be logged and surfaced by name resolve instead of the generic could not resolve name, and pubsub sub cancellation should release the topic handle when it was the only subscriber.
Possible fixes
- Make the pubsub RPC and namesys share topic handles (e.g. a per-node topic registry, or have
PubsubValueStore.createTopicHandlerfall back to the existing handle whenJoinreports it already exists). - Have the coreapi
SubscribeuseJoin+Topic.Subscribeand close the topic on cancel when it was the last subscriber. - Log the
Joinerror inPubsubValueStore.Subscribe.
Source: ipfs/kubo