Skip to content

fix(relay,desktop): only advertise NIP-43 when enforced; probe pairing by supported_nips#601

Merged
tlongwell-block merged 1 commit into
mainfrom
fix/nip43-advertisement-and-pairing-probe
May 15, 2026
Merged

fix(relay,desktop): only advertise NIP-43 when enforced; probe pairing by supported_nips#601
tlongwell-block merged 1 commit into
mainfrom
fix/nip43-advertisement-and-pairing-probe

Conversation

@tlongwell-block

Copy link
Copy Markdown
Collaborator

Summary

Mobile pairing from the desktop GUI was broken on the staging relay (sprout-oss.stage.blox.sqprod.co). The phone got "Could not reach the pairing relay" because the QR's relay URL pointed at wss://…/pair, which 404s — the pair sidecar isn't deployed on this relay.

Two coupled bugs:

  1. Relay advertised NIP-43 in NIP-11's supported_nips unconditionally, regardless of whether SPROUT_REQUIRE_RELAY_MEMBERSHIP was actually on.
  2. Desktop probe_relay_requires_auth keyed off limitation.auth_required — which is also true for plain NIP-42 / NIP-OA relays that don't need a pair sidecar at all.

Together they produced the failure on every open-but-stable-key relay (i.e. all current Sprout deployments).

Fix

crates/sprout-relay/src/nip11.rs / router.rs:

  • Drop 43 from the static SUPPORTED_NIPS constant.
  • RelayInfo::build(relay_self: Option<&str>, advertise_nip43: bool) — two facts, gated independently.
  • self follows stable signing-key (preserves NIP-29 group-metadata verification: per nips/29.md line 129, kinds 39000/39001/39002 are signed by the relay master key and verified against NIP-11 self; Sprout signs those unconditionally).
  • NIP-43 advertised only when stable-key AND require_relay_membership.
  • debug_assert!(!advertise_nip43 || relay_self.is_some()) — NIP-43 events are verified against self; advertising NIP-43 without it would be a programmer bug.
  • Single helper nip11_facts(&state) consumed by both the dedicated /info handler and the content-negotiated root handler so they can't drift.

desktop/src-tauri/src/commands/pairing.rs:

  • Rename probe_relay_requires_authprobe_relay_supports_nip43. Read supported_nips and look for numeric 43.
  • Unreachable / malformed / non-ws(s):// responses fall through to using the main relay (better to fail loudly there than misroute to an undeployed sidecar).

Tests

New in crates/sprout-relay/src/nip11.rs:

Test What it pins
nip43_not_in_static_supported_nips NIP-43 is conditional, not unconditional
build_open_relay_ephemeral_key_omits_self_and_nip43 Neither advertised on dev/CI
build_open_relay_stable_key_advertises_self_but_not_nip43 The staging-default shape — the bug we're fixing
build_membership_relay_advertises_self_and_nip43 Both advertised on real NIP-43 deployments
build_nip43_without_self_panics_in_debug The debug_assert
  • All 152 sprout-relay lib tests pass (+5 net new; 1 existing test got its comment updated, none changed semantically).
  • Existing e2e test_nip11_relay_info unaffected (it asserts field presence and auth_required: true, no specific NIP list).
  • cargo clippy -p sprout-relay --no-deps -- -D warnings clean.
  • Desktop cargo check clean.

Behavior change

Relay shape NIP-11 self NIP-11 supported_nips includes 43 Desktop QR points to
Dev / ephemeral key omitted no main relay
Open / stable key (today's staging) set (was set before too) no (was yes — bug) main relay (was …/pair)
Membership enforced set yes …/pair

Out of scope but related

  1. The sprout-pair-relay sidecar isn't built, deployed, or routed by the staging helm chart. Filing a follow-up for when membership is turned on (three pieces per Mari's audit: Dockerfile build, pod deployment binding 127.0.0.1:5000, Istio VirtualService /pair match).
  2. Cloudflare Access in front of the public hostname blocks phones without WARP regardless. Policy decision.

Review

Pre-reviewed in-channel with Max and Mari:

  • Mari caught a critical regression in v1 where I'd coupled self to NIP-43 — that would have silently broken NIP-29 verification on every open-but-stable-key relay. v2 decouples them.
  • Max caught a doc-comment overclaim ("falls back via the user") which is now corrected.
  • All three converged at 9/10+ on minimalism, elegance, and correctness.

…ported_nips

The desktop pairing flow rewrites the QR's relay URL to wss://…/pair
whenever the target relay's NIP-11 doc reports auth as required. That
heuristic is too broad: NIP-42 and NIP-OA relays also report
auth_required: true, but the /pair sidecar is only needed for
NIP-43 (relay membership), where unpaired peers can't reach the main
relay yet.

The relay made it worse by unconditionally advertising NIP-43 in
supported_nips, regardless of whether SPROUT_REQUIRE_RELAY_MEMBERSHIP
was on. Result on staging (membership off, no sidecar deployed): the
QR pointed at a 404, and the phone gave up with a connect error.

Relay-side fix
--------------

- Drop NIP-43 from the static SUPPORTED_NIPS.
- RelayInfo::build(relay_self: Option<&str>, advertise_nip43: bool):
  two facts, gated separately. NIP-29 group metadata (kinds
  39000/39001/39002) are signed by the relay master key per nips/29.md
  and verified against NIP-11 self, so self must follow stable-key
  identity, not membership. NIP-43 is gated on stable-key AND
  require_relay_membership.
- debug_assert!(!advertise_nip43 || relay_self.is_some()): NIP-43
  events are verified against self; advertising NIP-43 without self
  would leave clients unable to verify membership events.
- Single helper nip11_facts(&state) consumed by both /info and the
  content-negotiated root handler so they can't drift.

Desktop-side fix
----------------

- Rename probe_relay_requires_auth -> probe_relay_supports_nip43 and
  read supported_nips for 43 instead of limitation.auth_required.
- Unreachable / malformed / non-ws(s) responses now fall through to
  using the main relay rather than misrouting to an undeployed /pair
  sidecar.

Tests
-----

- nip43_not_in_static_supported_nips
- build_open_relay_ephemeral_key_omits_self_and_nip43
- build_open_relay_stable_key_advertises_self_but_not_nip43
  (the staging-default regression we must not reintroduce)
- build_membership_relay_advertises_self_and_nip43
- build_nip43_without_self_panics_in_debug (the debug_assert)
- Existing 150 sprout-relay lib tests still pass; existing e2e
  test_nip11_relay_info unaffected.

Signed-off-by: Tyler Longwell <tlongwell@squareup.com>
@tlongwell-block tlongwell-block merged commit 7171d49 into main May 15, 2026
15 checks passed
@tlongwell-block tlongwell-block deleted the fix/nip43-advertisement-and-pairing-probe branch May 15, 2026 19:11
tlongwell-block added a commit that referenced this pull request May 15, 2026
* origin/main: (33 commits)
  dev-mcp: add view_image tool (#602)
  fix(relay,desktop): only advertise NIP-43 when enforced; probe pairing by supported_nips (#601)
  fix(desktop): derive unread state from NIP-RS + relay catch-up only (#599)
  docs(testing): rewrite TESTING.md for current API and CLI-first workflow (#597)
  fix(agent): fix OpenAI-compat request body serialization and max_tokens (#595)
  feat(desktop): per-persona and per-agent env var overrides (#594)
  fix(desktop): stop pinning agents to deprecated SPROUT_ACP_TURN_TIMEOUT (#592)
  fix(desktop): populate member_count in get_channels so channel browser shows real counts (#548)
  fix(desktop): autofocus message composer on channel/thread open (#572)
  refactor(cli): restructure flat commands into 12 subcommand groups (#585)
  feat(sdk): add builder functions for workflows, DMs, and presence (#589)
  feat(desktop): add message more-actions dropdown menu (#590)
  fix(mobile): preserve channel list across background/resume reconnection (#588)
  Redesign Home as an inbox (#582)
  fix(desktop): drive unread badges from live subscription, not refetched lastMessageAt (#581)
  fix(desktop): refine header scaling and shadow (#573)
  fix(desktop): keep day dividers below header (#574)
  Move agent activity below composer (#579)
  docs(nips): NIP-AE — Agent Engrams (#575)
  refactor: extract shared @mention resolver into sprout-sdk (#580)
  ...

Signed-off-by: Tyler Longwell <tlongwell@squareup.com>
tlongwell-block added a commit that referenced this pull request May 15, 2026
Signed-off-by: Tyler Longwell <tlongwell@squareup.com>

* origin/main:
  dev-mcp: add view_image tool (#602)
  fix(relay,desktop): only advertise NIP-43 when enforced; probe pairing by supported_nips (#601)
  fix(desktop): derive unread state from NIP-RS + relay catch-up only (#599)
  docs(testing): rewrite TESTING.md for current API and CLI-first workflow (#597)
  fix(agent): fix OpenAI-compat request body serialization and max_tokens (#595)
  feat(desktop): per-persona and per-agent env var overrides (#594)
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.

1 participant