Skip to content

fix(relay): require all three NIP-29 discovery kinds before treating a channel as reconciled (#3460) - #4039

Open
iroiro147 wants to merge 1 commit into
block:mainfrom
iroiro147:claude/issue3460-20260731
Open

fix(relay): require all three NIP-29 discovery kinds before treating a channel as reconciled (#3460)#4039
iroiro147 wants to merge 1 commit into
block:mainfrom
iroiro147:claude/issue3460-20260731

Conversation

@iroiro147

Copy link
Copy Markdown
Contributor

Summary

Tightens the boot-time channel-discovery reconcile so a channel is only considered "reconciled" when all three NIP-29 discovery events (metadata 39000, admins 39001, members 39002) are present — not just 39000. Fixes the #3460 symptom where a channel a user had joined disappeared from their Desktop sidebar after a relaunch.

Root cause

Emission of the relay-signed discovery events is out-of-band and best-effort. Every site that changes membership or metadata tries to emit, but tolerates failure — e.g. handle_join_request warn!-logs and proceeds when emit_group_discovery_events errors (side_effects.rs). That means a channel can exist whose metadata event (kind:39000) was published at creation (when the member list was empty) while the members event (kind:39002) was rolled back or never written.

The reconcile pass at boot previously probed only for kind:39000 to decide whether a channel needed (re-)emission. Once 39000 existed, every subsequent startup classified the channel as reconciled and skipped re-emission — so a channel whose 39002 never landed stayed broken forever. The relay's published member list then omitted pubkeys the channel_members table held, the client's get_channels({kinds:[39002],"#p":[my_key]}) query returned empty on cold boot, isMember derived false, and the sidebar dropped the channel behind a "Join" prompt. This matches the reporter's own follow-up note that the relay's 39002 member-list event never included their pubkey even though the DB row was correct.

Fix

reconcile_channel_events now probes one addressable event per required kind (REQUIRED_DISCOVERY_KINDS = [39000, 39001, 39002]) for each channel, and re-emits the complete emit_group_discovery_events snapshot whenever any of the three is missing. Re-emission is idempotent (it replaces the current snapshot from the DB), so catching the "39002 was never written" shape also repairs the "39001 (admins) got lost" shape for free.

Fail-closed on a probe query error: the channel is skipped rather than re-emitting into a shaky DB (matches the existing error-tolerance of this boot path). Probe cost is one query per kind per channel, once per boot.

Test

  • reconcile_probes_metadata_admins_and_members_kinds pins REQUIRED_DISCOVERY_KINDS == [39000, 39001, 39002] (order + set + distinctness) so the revert can't regress silently.
  • Full relay lib suite: 794 pass / 9 pre-existing env-gated failures (api::media/api::admin, unchanged baseline; 0 media/admin files touched in this diff).

Environment note

End-to-end validation (a scratch Postgres with a channel whose 39002 is absent) requires the live relay Postgres fixture this worktree lacks. The change is behaviorally additive: channels that were already fully reconciled are untouched; only previously-stranded 39001/39002-missing channels get an initial emission at boot.

…a channel as reconciled (block#3460)

When a channel was created via direct SQL (e.g. test seed), its kind:39000
metadata event was emitted at creation with the then-empty member list. If a
later kind:39002 (members) emission failed after a join — and the join's
emit_group_discovery_events tolerates failure — every subsequent reconcile
pass only probed for 39000, classified the channel as reconciled, and left
39002 missing forever. The relay-side NIP-29 discovery stream then omitted
pubkeys the channel_members table held, and the client's get_channels('#p':
my key] query returned empty on cold boot, hiding the channel from the
sidebar.

Probe one addressable event per required kind (39000 metadata, 39001 admins,
39002 members) during reconcile_channel_events and re-emit all of them
whenever any is missing. Fail closed on a query error (skip the channel) —
never re-emit into a degrade this boot. reconcile_channel_events now
requires all three discovery kinds before calling a channel reconciled.

Unit test: pins [39000, 39001, 39002] as REQUIRED_DISCOVERY_KINDS and
documents the probe order; buzz-relay lib suite 794 pass, 9 pre-existing
api::media/admin environment failures (unchanged baseline).

Signed-off-by: iroiro147 <sarthak.singh@juspay.in>
@iroiro147
iroiro147 requested a review from a team as a code owner August 1, 2026 01:00
@AIAlhello

Copy link
Copy Markdown

Independent reproduction confirming this root cause, and confirming the bug is still live on current main — this PR isn't merged yet.

Setup: self-hosted relay (Docker Compose, ghcr.io/block/buzz:main), relay owner testing membership in a private channel they administer.

Relay-level membership is correct (rules out an identity/auth issue):

$ buzz-admin list-members
pubkey role created_at
aa45ab99... owner 2026-07-26T17:10:30Z
a3540917... member 2026-07-26T18:15:17Z

Join event is accepted by the relay:

$ buzz channels join --channel
{"accepted":true,"event_id":"d8d4047571bc8b07c1ab3a86f67e6db417fefcb9b7c54eca4e7a5e922d381eaf","message":""}

Immediate re-query of the same channel omits the joining pubkey:

$ buzz channels members --channel
[{"pubkey":"","role":"owner"},{"pubkey":"","role":"bot"}]

Tested on two builds several weeks apart — self-built from main @ d8281b9, and the current desktop-bundled CLI, v0.5.14 — same result on both, so this is not fixed by the unrelated membership work in #5493.

This lines up exactly with the root cause described here: the join's kind:39002 emission apparently didn't land, and since boot-time reconcile only ever checks for kind:39000, the channel gets permanently marked "reconciled" and the missing member-list event is never retried. An accepted event_id with a DB write that never surfaces is a clean match for that failure mode.

Happy to test this fix against my own setup once merged, or provide relay logs / DB channel_members state if useful before then.

@iroiro147

Copy link
Copy Markdown
Contributor Author

@AIAlhello thank you — this is a particularly valuable repro because it rules out the two alternative explanations at the relay level: buzz-admin list-members showing correct membership eliminates identity/auth, and the accepted join event (accepted: true with an event id) eliminates write-path rejection. What remains is exactly what this PR targets: the discovery read path treating a channel as complete before all three NIP-29 kinds have arrived, so the immediate re-query serves a stale membership view.

Your confirmation that it's still live on current main with the stock ghcr.io/block/buzz:main image is exactly the kind of evidence that helps maintainers prioritise. The branch here is current and green; nothing needed from you — but if you get the chance to test this PR's build against your setup, the expected result is that the re-query right after join includes the joining pubkey.

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