@helia/ipns: fetch cold IPNS-over-pubsub records from rendezvous-CID providers instead of waiting for subscription-change?
- Dominant language
- TypeScript
- Stars
- 1.3k
- Forks
- 159
- Avg merge
- 2h 11m
- Merged PRs (30d)
- 16
Description
### Package
`@helia/ipns` (`packages/ipns`), current npm `9.2.1`
### Context
Since #906, `PubSubRouting` can fetch an IPNS record over the `libp2p/fetch` protocol instead of only waiting for a gossipsub push. That's great. But `get()` today only fetches from peers gossipsub *already* reports as subscribers:
```js
for (const peerId of this.libp2p.services.pubsub.getSubscribers(topic)) {
promises.push(this.#fetchFromPeer(topic, routingKey, peerId, ...))
}
if (promises.length > 0) { /* Promise.any */ }
throw new NotFoundError('Pubsub routing does not actively query peers')
```
On a cold resolve, `getSubscribers(topic)` is empty, so `get()` throws immediately. The record only becomes fetchable after the topic's peers re-announce via a `subscription-change` event (the `#fetchFromPeer` triggered from the `subscription-change` listener). In practice that adds a real latency floor before the first fetch can happen, even though the publisher (kubo's go-libp2p-pubsub-router, or another Helia via `registerLookupFunction`) will answer a `libp2p/fetch` request for the record regardless of the subscription handshake.
### Idea
IPNS-over-pubsub publishers announce themselves as **providers of the topic's rendezvous CID** — the standard go-libp2p-pubsub discovery key, `CID(sha256("floodsub:" + topic))`. So on a cold `get()`, in parallel with the existing subscriber-fetch, `PubSubRouting` could:
1. `contentRouting.findProviders(rendezvousCidForTopic(topic))`
2. dial each discovered provider
3. `fetch(peerId, routingKey)` the moment the dial completes
First signature-valid record wins; losers get aborted. This skips the wait for `subscription-change` entirely and reuses the exact `libp2p/fetch` path #906 added — it just sources candidate peers from content routing instead of waiting for gossipsub to confirm them.
We've implemented exactly this downstream (fetch in parallel from both current subscribers *and* freshly-discovered providers of the rendezvous CID, first valid record wins) and it reliably resolves cold IPNS names well under the subscription-wait floor, then falls back to the existing path on a miss.
### Question
Does this belong in `@helia/ipns` `PubSubRouting` itself, or is it better kept as downstream orchestration around the router?
Reasons it might fit upstream:
- The rendezvous CID (`floodsub:` → sha256 → CID) is a standard libp2p-pubsub discovery key, not app-specific.
- It reuses the fetch path from #906 verbatim; only the peer-sourcing changes.
- Every Helia-over-pubsub consumer would get faster cold resolves.
Reasons it might not:
- `get()` currently never actively queries the network ("Pubsub routing does not actively query peers") — provider discovery + dialing is a deliberate behavior/cost change and may warrant an opt-in (e.g. an init flag).
- It introduces a `contentRouting` dependency and dial amplification into a router that today only listens.
If you'd welcome it upstream I'm happy to open a PR (likely gated behind an init option, defaulting off). Wanted to check direction before writing it.
Contributor guide
No contributing guide indexed for this repository
Research direction
Start in packages/ipns at PubSubRouting.get() and the existing #fetchFromPeer path added in #906. Read how current subscribers are obtained and how subscription-change triggers fetching, then inspect the proposed contentRouting.findProviders flow and rendezvous CID handling. Done means the upstream direction and any opt-in behavior are settled before implementation.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- distributed-systems, networking
- Issue type
- Feature
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100