rendezvous: register and wait for a dial as separate steps - #30
Merged
Conversation
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.
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.
This was referenced Jul 29, 2026
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).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This fixes a live deadlock on main. A node built from
maintoday cannotpublish a descriptor and cannot be reached. Released binaries are unaffected —
v1.2.2 was built from
3caee27, before #26.The bug
RvServiceRegisterconnects, sends its'S'+pubkey header, and then blocks:The relay writes that
0x01only 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.
ThreadBtfAcceptcalledBtfSetActiveRelayafter it.That is a closed loop:
BtfActiveRelay()must be setWhy it did not show up before
#26 exposed it. Previously
BtfActiveRelay()fell back tovBtfMeetingRelays[0], so a node always advertised something — andThreadBtfAcceptpicks its relay at random, so the advertisement was correctroughly 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
RvServiceRegisterreturns as soon as the relay has the service listed.RvServiceWaitPaireddoes the blocking read.ThreadBtfAcceptregisters,advertises, then waits.
RvClientConnectis untouched — a dialer blockinguntil 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:
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.