Skip to content

fix(protocol): ask the relays for a stranger's key packages when a session is wanted - #638

Merged
bahdotsh merged 2 commits into
mainfrom
fix/nostr-cold-contact-engine-request
Oct 25, 2026
Merged

bahdotsh merged 2 commits into
mainfrom
fix/nostr-cold-contact-engine-request

Conversation

@bahdotsh

Copy link
Copy Markdown
Member

Closes #633.

What was wrong

A device that knew another only by its address, and shared only a Nostr relay with it, never formed a session on any native binding. The peer's key packages stood on the relay and nothing fetched them:

  • A send with no session only queues, so no frame is built and the transport's lookup in build_event never runs.
  • Discovery's first key-package push never names a peer that has sent nothing.
  • The tick's mesh push returns early because can_reach_recipient is true whenever Nostr is available.

The browser worked only because its node asked the relays itself.

What changes

  • The engine asks. request_nostr_key_package_for in nostr_publication.rs is called where the engine finds no key package for a peer it must reach: ensure_session_establishment (text, strict and media sends), establish_secure_session, and the reconciliation tick while a message waits. The tick is what repeats a lookup no relay could answer.
  • Gates, cheapest first: MLS initialised and encryption on, not ourselves, not blocked, cold contact on, no live mesh link, the transport's per-peer interval lapsed, no session. The interval is read through a new read-only NostrTransport::resolution_due, before the storage read, so a waiting peer costs one read per 5 minutes rather than one per tick.
  • The browser's own request is removed (second commit). a_linked_peer_is_not_looked_up_on_the_relays now pins the engine's link gate.
  • Docs: the Nostr guide's resolving rule, W12 in the web bridge, a new rule in the session-lifecycle state machine, the cold-contact config doc, and a changelog entry.

No UDL, wire, config or event change, so no binding regeneration.

What it costs

Each lookup tells the relays it reaches that this device wants the queried routing tag. That was already true for the browser. It now holds on every binding, at most once per peer per 5 minutes, and only for peers the application asked to reach. Not gated on auto_key_exchange, since fetching is not pushing. Not skipped when an internet relay is up, since a stranger produces no presence on it.

Tests

  • Engine: six unit tests (a send asks, an establish asks, one lookup per interval, none for a blocked peer or with cold contact off, none for a linked peer, the tick asks again) and tests/nostr_cold_contact.rs, an end-to-end run where two engines share only an in-memory relay. The host half there only moves events, as a binding driver does.
  • Transport: resolution_due agrees with the request and changes nothing.
  • Negative control: with the request disabled, five of the new engine tests fail, the end-to-end one included.
  • Local: cargo fmt --check, cargo clippy --workspace -D warnings and cargo doc with -D warnings are clean. cargo test --workspace --lib passes, apart from no_shipped_file_names_a_relay, which fails locally only because it scans a stale, git-ignored agent worktree under .claude/worktrees. The browser pages were not run, because the JavaScript changes are comments only.

Owed after merge

Device runs on iOS and Android, both directions. Two phones with Bluetooth and Wi-Fi off, one holding the other's address from a QR code, sharing only a relay. Neither bridge has exercised this path before.

…ssion is wanted

A device that knew another only by its address, and shared only a Nostr
relay with it, never formed a session on any native binding. The peer's key
packages stood on the relay and nothing fetched them: a send with no
session only queued, so no frame was built for the transport to attach a
lookup to; discovery's first push never names a peer that has sent
nothing; and the tick's mesh push stands down whenever Nostr claims to
reach the peer.

The engine now asks the relays where it finds no key package for a peer
it must reach: a send, an explicit establish_secure_session, and the
reconciliation tick while a message waits, which repeats a lookup no
relay could answer. A blocked peer, a peer on a live mesh link, a peer
with a session, and every peer with cold contact off are never looked up.
The transport's per-peer interval is read before the session check, so a
waiting peer costs one storage read per interval.

Closes #633
The engine asks the relays for a session-less peer's published key
packages itself now, with the same live-link gate the browser applied, so
the browser node's reach_over_nostr and its helpers go.
a_linked_peer_is_not_looked_up_on_the_relays now pins the engine's gate.
@bahdotsh
bahdotsh merged commit e07b88b into main Oct 25, 2026
42 checks passed
@github-actions github-actions Bot locked and limited conversation to collaborators Oct 25, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Nostr cold contact never starts: no production caller requests a stranger's key packages

1 participant