Skip to content

rendezvous: register and wait for a dial as separate steps - #30

Merged
Bitflash-sh merged 1 commit into
mainfrom
fix/rendezvous-register-order
Jul 29, 2026
Merged

rendezvous: register and wait for a dial as separate steps#30
Bitflash-sh merged 1 commit into
mainfrom
fix/rendezvous-register-order

Conversation

@Bitflash-sh

Copy link
Copy Markdown
Owner

This fixes a live deadlock on main. A node built from main today cannot
publish a descriptor and cannot be reached. Released binaries are unaffected —
v1.2.2 was built from 3caee27, before #26.

The bug

RvServiceRegister connects, sends its 'S'+pubkey header, and then blocks:

if (!WriteN(s, hdr, 33)) { CLOSESOCK(s); return RV_INVALID; }
unsigned char paired = 0;
if (!ReadN(s, &paired, 1) || paired != 0x01) { CLOSESOCK(s); return RV_INVALID; }

The relay writes that 0x01 only when a client pairs with the service
(relay/btfrv.cpp). So the call does not return when the node is registered
it returns when somebody has already dialled it. ThreadBtfAccept called
BtfSetActiveRelay after it.

That is a closed loop:

  • to be dialled, a node must publish a descriptor naming its meeting node
  • to publish, BtfActiveRelay() must be set
  • it is only set once somebody has dialled

Why it did not show up before

#26 exposed it. Previously BtfActiveRelay() fell back to
vBtfMeetingRelays[0], so a node always advertised something — and
ThreadBtfAccept picks its relay at random, so the advertisement was correct
roughly one time in the number of relays, and wrong the rest.

That is the connectivity flakiness this project has been chasing. Not dead
relays and not stale descriptors: nodes advertising a meeting node they were not
registered at, most of the time, by design of the fallback.

#26 removed the fallback. On its own terms that was right — advertising a
rendezvous you are not on is exactly what wastes everyone's dials — but it
converted an unreliable heuristic into a reliable deadlock. This is the other
half of that change.

The fix

RvServiceRegister returns as soon as the relay has the service listed.
RvServiceWaitPaired does the blocking read. ThreadBtfAccept registers,
advertises, then waits. RvClientConnect is untouched — a dialer blocking
until it is paired is correct behaviour.

Registration failure also had no log line, which is why this stayed invisible: a
node that never registered was indistinguishable from one waiting quietly for a
peer. There are now lines for registering, failing over, and being dropped.

Verified

A fresh node:

[net]   rendezvous: registered at 90.156.222.107:8434, waiting for a dial
[nostr] Nostr: .btf self-resolve OK via wss://nos.lol (meeting_node=90.156.222.107:8434, ...)

The published meeting node is the relay it is actually on. Two nodes on one
machine then reach each other and exchange data.

Build green on MSYS2 UCRT64.

A node could not become reachable. RvServiceRegister connected, sent its
'S'+pubkey header, and then blocked reading the relay's 0x01 byte -- which the
relay only writes when a client pairs with the service. So the call did not
return when the node was registered; it returned when somebody had already
dialled it.

ThreadBtfAccept called BtfSetActiveRelay after that. The result is a deadlock:

  - to be dialled, a node must publish a descriptor naming its meeting node
  - to publish, BtfActiveRelay() must be set
  - it is only set once somebody has dialled

Before #26 this was masked. BtfActiveRelay() fell back to
vBtfMeetingRelays[0], so a node always published something, and ThreadBtfAccept
picks its relay at random -- so it worked whenever the two happened to match,
roughly one time in the number of relays. That is the flakiness this project has
been chasing: not a bad relay, an advertisement that usually named the wrong one.

#26 removed the fallback, which was right on its own terms and turned an
unreliable heuristic into a reliable deadlock. This is the other half of that
change and they belong together.

RvServiceRegister now returns as soon as the relay has the service listed.
RvServiceWaitPaired does the blocking read. ThreadBtfAccept registers, advertises,
then waits. RvClientConnect is untouched -- a dialer blocking until it is paired
is correct.

Registration failure also had no log line at all, which is why this stayed
invisible: a node that never registered looked identical to one waiting quietly
for a peer. There are now lines for registering, failing over, and being dropped.

Verified: a fresh node reports `registered at 90.156.222.107:8434, waiting for a
dial` and then self-resolves with `meeting_node=90.156.222.107:8434` -- the relay
it is actually on. Two nodes on one machine reach each other.
@Bitflash-sh
Bitflash-sh merged commit 428a7b5 into main Jul 29, 2026
Vic-Nas added a commit to Vic-Nas/bitflash-mod that referenced this pull request Jul 29, 2026
RvServiceRegister used to block until someone actually paired with us
before returning -- so BtfSetActiveRelay (what we advertise as our
meeting node) only got called *after* we were already reached there.
In the gap between starting registration and that first pairing, we
had nothing confirmed to advertise, so the fallback answered with
whatever relay was cached or first on the list -- right by chance,
wrong the rest of the time. That's the connectivity flakiness this
project has chased for a while: not dead relays, nodes confidently
advertising a meeting node they hadn't actually confirmed yet.

RvServiceRegister now returns as soon as the relay lists us -- it
doesn't wait for a dial. BtfSetActiveRelay runs right after that, so
we only ever advertise a relay we've just confirmed we're on.
RvServiceWaitPaired does the blocking part separately, called after
the advertisement is already live. Relay protocol is unchanged; the
relay already lists a service the instant it reads the header, this
only changes when the client stops blocking on the read.

Same class of bug as Bitflash-sh#30, adapted rather than ported --
this codebase's relay-thread structure (BtfAcceptOneFn, one thread per
concurrently-registered relay) diverged from theirs (single-relay
round robin), so the fix needed the same split applied to a different
shape rather than a cherry-pick.

Also updates test_btfrv.cpp/test_btftunnel.cpp/test_relayproc.cpp,
which called RvServiceRegister directly and relied on it blocking.
Bitflash-sh added a commit that referenced this pull request Jul 29, 2026
Everything since 1.2.2 is about nodes being able to find and reach each other,
which is the problem this project actually had:

  #30  rendezvous registration and waiting split apart. Registering used to
       block until somebody dialled, so a node could not advertise a meeting
       node until it had already been reached at one. This is the root cause
       of the flaky connectivity, not dead relays or stale descriptors.
  #26  a node no longer advertises a rendezvous it is not registered at
  #25  .btf peers that answered are remembered and dialled first (36.3s -> 2.1s)
  #32  peers exchange signed descriptors, so discovery survives a relay outage
  #29  debug.log stopped dropping lines under concurrency
  #28  /port, so two nodes can share a machine
Vic-Nas added a commit to Vic-Nas/bitflash-mod that referenced this pull request Jul 29, 2026
…-sh#32)

Discovery has depended entirely on the Nostr relays. Two connected
nodes never told each other who else existed, so a relay outage left
a running network unable to grow, and a node that found one stale
peer had no way to learn there was anything better.

Peers now trade signed descriptors over a new "btfpeers" message: our
own first, then peers that actually answered us. The receiver checks
each Schnorr signature against the key the .btf address decodes to,
so the sender is trusted for nothing -- forging an entry needs
somebody else's secret key. Per-message caps (MAX_PEX_DESCRIPTORS,
MAX_PEX_DESCRIPTOR_BYTES) and a per-connection rate limit
(PEX_MIN_INTERVAL via CNode::nLastPexRecv) bound flooding with
self-generated keys, which signatures alone don't prevent.

Unknown commands are already ignored, so a node without this just
drops the message -- not a protocol break.

Adapted rather than cherry-picked, same as the other Bitflash-sh#25/Bitflash-sh#28/Bitflash-sh#29/Bitflash-sh#30
ports: our net.cpp diverged (CachedBtfPeer/RememberBtfPeer already
exist here from the peer-cache port, and use our own
BtfPeerCacheHexToBytes/BytesToHex naming rather than generic
HexToBytes/BytesToHex to avoid symbol collisions elsewhere in this
codebase). CachedBtfPeer gains a `desc` field -- the peer's own
descriptor kept verbatim, since that signature is what makes a peer
relayable to someone else. RememberBtfPeer takes it as an optional
arg and never lets a plain dial (empty desc) clear one already on
file, so learning a peer through exchange doesn't get undone the next
time we happen to also reach them directly.

nostr.cpp gains BtfLocalDescriptor() (reads strBtfActiveRelay
directly, not through BtfActiveRelay()'s fallback -- only a
rendezvous we've actually confirmed registration at is worth handing
to a peer) and BtfSecpContext() (so net.cpp can verify without
pulling secp256k1 into net.h).
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