Multi-channel #h filters silently receive zero channel-scoped events
- Dominant language
- Rust
- Stars
- 32.7k
- Forks
- 4.3k
- Avg merge
- 1d 13h
- Merged PRs (30d)
- 253
Description
Subscribing with more than one channel UUID in a single `#h` filter succeeds at the protocol level (the relay sends EOSE, the WS stays open) but never delivers any EVENT frames. Events published to the listed channels are confirmed ingested by the relay and delivered to single-channel subscribers on those channels. They just never reach the multi-channel subscriber.
### Repro
Open an authenticated WS connection (NIP-42) and send:
```
["REQ", "sub1", {"kinds":[9], "#h":["",""], "limit":0}]
```
You get EOSE immediately. Publish a kind:9 event to either channel. The publisher returns success. Single-channel subscribers on each channel receive it. `sub1` receives nothing.
Now send two REQs instead:
```
["REQ", "subA", {"kinds":[9], "#h":[""], "limit":0}]
["REQ", "subB", {"kinds":[9], "#h":[""], "limit":0}]
```
Both receive events as expected.
### Why
`extract_channel_id_from_filters` in `crates/buzz-relay/src/handlers/req.rs:1009` returns `None` when more than one distinct channel UUID appears across the filters. The docstring at lines 999-1008 calls this "slow-path fan-out." But `fan_out_scoped` in `crates/buzz-relay/src/subscription.rs:265` only looks up subscriptions via `channel_kind_index`, and the scoping invariant at `subscription.rs:321-324` says channel-scoped events are not delivered to global subs. A `None` return registers the sub globally, so channel-scoped events never match it.
The end user sees a subscription that the relay accepted and EOSE'd on time, but that silently drops every event matching the published filter contract.
### Filter contract
NIP-01 allows arrays of values for any tagged filter. A client building a subscription that wants events from two channels should be able to send one REQ with two `#h` values. The current behavior treats this as a global subscription, which is not what the filter expresses.
### Proposed fix
When `extract_channel_id_from_filters` sees N > 1 distinct channels, register the subscription under each of those channels' `channel_kind_index` entries instead of returning `None`. Per-channel dispatch stays O(1). The scoping invariant is preserved because the sub is still tied to specific channels, just N of them instead of one. The "no channel tag" case (true global) keeps the existing None path.
### Workaround
Send one REQ per channel, each with a single `#h` value. The relay indexes each REQ separately and delivery works.
### Environment
Observed while building a multi-channel bot against `buzz.viztekpro.com`, relay version matching `main` as of 2026-07-27. Took me about 30 minutes to find because the relay logs show the event was ingested but no fanout line, and the subscriber just sits silent with no error.
Contributor guide
Assessment
This issue has not been assessed yet.