diff options
| author | bitflash-maintainer <noreply@bitflash.invalid> | 2026-09-05 22:46:52 +0000 |
|---|---|---|
| committer | bitflash-maintainer <noreply@bitflash.invalid> | 2026-09-05 22:46:52 +0000 |
| commit | ceae3d076b5ad11ee84729ae43f14e9312896646 (patch) | |
| tree | 7e9132aa871fdc01cb95268d2f5ea3bb11853ddf /docs/rendezvous.md | |
| parent | be63e83d79e7e00e6b8ca889a70e6a19886aa1d8 (diff) | |
| parent | 66e0b9158d050c46b1ff2509faeee3f4573cac1c (diff) | |
Bring main onto the line this project actually ships
main had drifted to a history with no ancestor in common with the tree we
build and deploy. It sat at be63e83 (2026-08-09) with 28 commits; the shipped
line, deploy/soak-2026-08-21, had 383. The two shared nothing -- git
merge-base returns no result at all -- so anyone cloning this repository
landed on a default branch that was neither current nor related to the
running node.
The 28 commits on main were audited before this merge: every one is already
present in the tree by content, including the two that are hardest to see
because they were rewritten afterwards -- the payout reorg-safety work
(PayoutBlockStillOnChain, since evolved to re-check on every retry rather
than once at maturity) and the nostr relay certificate verification
(SSL_set1_host, now with a Windows root-store path). Nothing on main is lost
by this merge.
Recorded as a merge rather than a force-push: main only ever moves forward,
its old history stays reachable as the first parent, and this is revertible.
The tree is taken wholesale from the merged branch, so main's content is now
exactly what was built and tested.
What this brings onto main:
- compactblock: BIP152-style compact block relay ported from upstream 1.2.23,
primitives and unit test only, with the RAND_bytes failure path closed and
the reconstruction allocation bounded.
- upstream/main 17741d9: isolated testnet parameters and genesis, optional
2 MB large pages for RandomX. Mainnet genesis verified byte-identical
through upstream's BuildGenesisBlock() refactor.
- deploy: every image built on bookworm so binaries run on Debian 12, and the
container build now honours the RandomX and libsecp256k1 pins it had been
silently ignoring since before 1.2.15.
Gates at this tree: 15/15 selftests, compactblock's 17 checks, consensus
constants unchanged against e4f898c apart from the pre-existing
MAX_BLOCK_SIZE_GEN, mainnet genesis intact, money-log check ok.
Diffstat (limited to 'docs/rendezvous.md')
| -rw-r--r-- | docs/rendezvous.md | 336 |
1 files changed, 336 insertions, 0 deletions
diff --git a/docs/rendezvous.md b/docs/rendezvous.md new file mode 100644 index 0000000..68b5df3 --- /dev/null +++ b/docs/rendezvous.md | |||
| @@ -0,0 +1,336 @@ | |||
| 1 | # Bitflash `.btf` Rendezvous Protocol | ||
| 2 | |||
| 3 | This is the specification of how Bitflash nodes reach each other, and how miners | ||
| 4 | reach pools, without revealing an IP address and without any central directory. | ||
| 5 | It documents the protocol as implemented in `src/btfaddr.cpp`, `src/btfchan.cpp`, | ||
| 6 | `src/btfrv.cpp`, `src/btftunnel.cpp` and `src/nostr.cpp`. | ||
| 7 | |||
| 8 | The design is ported from the Itzal hidden service design. A `.btf` address names | ||
| 9 | a public key, a node proves ownership of that key by signing a service | ||
| 10 | descriptor, other nodes find the descriptor over Nostr, and the two sides then | ||
| 11 | meet through an untrusted relay over an end to end encrypted tunnel. The relay | ||
| 12 | carries ciphertext only and never learns the plaintext. | ||
| 13 | |||
| 14 | ## 1. Overview | ||
| 15 | |||
| 16 | A connection to a `.btf` service has three layers: | ||
| 17 | |||
| 18 | 1. **Discovery (Nostr).** The service publishes a signed descriptor as a Nostr | ||
| 19 | event. A caller fetches it by the service public key and verifies the | ||
| 20 | signature. The descriptor says which rendezvous relay the service is waiting | ||
| 21 | at, which encryption key to use, and optionally which Tor hidden service can | ||
| 22 | be dialed directly. | ||
| 23 | 2. **Rendezvous (TCP).** Caller and service both connect out to the same relay | ||
| 24 | and are paired by public key. Neither needs an inbound port or a public IP. | ||
| 25 | After pairing the relay is a blind byte pipe. | ||
| 26 | 3. **Direct onion fallback bypass.** If the descriptor carries a valid signed | ||
| 27 | `.onion:port` endpoint and the caller is running with Tor mode, the caller | ||
| 28 | tries that endpoint before using rendezvous. This removes the rendezvous | ||
| 29 | relay from the data path for peers that choose to run hidden services. | ||
| 30 | 4. **Encrypted channel (X25519 + XChaCha20-Poly1305).** Over the paired pipe the | ||
| 31 | caller and service run an ephemeral to static handshake and then exchange | ||
| 32 | authenticated frames. The relay sees only ciphertext. | ||
| 33 | |||
| 34 | The caller ends up holding an ordinary local socket that behaves like a direct | ||
| 35 | TCP connection to the peer. The node treats it exactly like any other peer | ||
| 36 | socket. | ||
| 37 | |||
| 38 | ## 2. Identities and keys | ||
| 39 | |||
| 40 | A node holds two independent keypairs. | ||
| 41 | |||
| 42 | - **Signing / identity key**: a secp256k1 x-only key (BIP340 Schnorr). This is | ||
| 43 | the `.btf` identity and also the node's Nostr identity. Persisted in | ||
| 44 | `nostr.key`. | ||
| 45 | - **Encryption key**: an X25519 (Curve25519) keypair. Persisted in | ||
| 46 | `btf_enc.key`. Its public half is published in the descriptor as `enc` and is | ||
| 47 | used for the tunnel key agreement. | ||
| 48 | |||
| 49 | The identity key names the service and authenticates the descriptor. The | ||
| 50 | encryption key secures the tunnel. They are separate so the long term identity | ||
| 51 | never performs Diffie-Hellman and the encryption key can be rotated in a new | ||
| 52 | signed descriptor. | ||
| 53 | |||
| 54 | ## 3. The `.btf` address | ||
| 55 | |||
| 56 | An address is the identity public key plus a short checksum, Base32 encoded, with | ||
| 57 | the suffix `.btf`. | ||
| 58 | |||
| 59 | ``` | ||
| 60 | label = Base32( pubkey[32] || checksum[2] ) | ||
| 61 | address = label + ".btf" | ||
| 62 | ``` | ||
| 63 | |||
| 64 | - `pubkey` is the 32 byte secp256k1 x-only public key. | ||
| 65 | - `checksum` is the first two bytes of `SHA256(".btf-checksum-v1" || pubkey)`. | ||
| 66 | - Base32 uses the lowercase alphabet `abcdefghijklmnopqrstuvwxyz234567`, no | ||
| 67 | padding. The 34 byte payload encodes to 55 characters. | ||
| 68 | - Parsing is case insensitive, trims surrounding whitespace, and strips a | ||
| 69 | trailing dot. A wrong checksum is rejected. | ||
| 70 | |||
| 71 | The address is self certifying: it *is* the public key, so no registry has to be | ||
| 72 | trusted to map a name to a key. | ||
| 73 | |||
| 74 | ## 4. Discovery over Nostr | ||
| 75 | |||
| 76 | Nodes publish and read three kinds of parameterized replaceable Nostr events. | ||
| 77 | Parameterized replaceable means the newest event for a given author and `d` tag | ||
| 78 | supersedes the older one, so a descriptor is always the latest state. | ||
| 79 | |||
| 80 | | Purpose | kind | `d` tag | | ||
| 81 | |---|---|---| | ||
| 82 | | Service descriptor | 38501 | `btf-descriptor-2` | | ||
| 83 | | Rendezvous relay announcement | 38502 | `btf-relay-2` | | ||
| 84 | | Pool announcement | 38503 | `btf-pool-2` | | ||
| 85 | |||
| 86 | The Nostr event is signed with the node's identity key, so the event author | ||
| 87 | equals the `.btf` public key. That binds the descriptor to the address at the | ||
| 88 | Nostr layer, and the descriptor is *also* signed inside its own content (see | ||
| 89 | below), so a relay operator cannot forge or swap a descriptor for an address it | ||
| 90 | does not control. | ||
| 91 | |||
| 92 | **Resolving an address.** To reach `X.btf`, a caller parses the address to the 32 | ||
| 93 | byte key and sends a Nostr `REQ` filtered by `authors = [X]`, `kinds = [38501]`, | ||
| 94 | `#d = ["btf-descriptor-2"]`, `limit = 1`. It verifies the returned descriptor and | ||
| 95 | uses its `meeting_node` and `enc`. | ||
| 96 | |||
| 97 | **Relay auto discovery.** A relay operator runs a rendezvous relay and publishes a | ||
| 98 | kind 38502 event carrying `host:port`. Every node that reads it adds the relay to | ||
| 99 | the set it can register at and dial through. No seed list edit or maintainer | ||
| 100 | approval is needed. A small curated seed list is still shipped for bootstrap: | ||
| 101 | |||
| 102 | ``` | ||
| 103 | 92.246.128.180:8434 | ||
| 104 | 31.44.4.249:8434 | ||
| 105 | 90.156.222.107:8434 | ||
| 106 | ``` | ||
| 107 | |||
| 108 | The default rendezvous port is `8434`. | ||
| 109 | |||
| 110 | ## 5. The service descriptor | ||
| 111 | |||
| 112 | The descriptor is a JSON object, signed with the identity key over a domain | ||
| 113 | separated digest of its own fields. | ||
| 114 | |||
| 115 | Legacy descriptor: | ||
| 116 | |||
| 117 | ```json | ||
| 118 | { | ||
| 119 | "v": 2, | ||
| 120 | "pubkey": "<hex 32-byte x-only identity key>", | ||
| 121 | "enc": "<hex 32-byte X25519 public key>", | ||
| 122 | "meeting_node": "host:port", | ||
| 123 | "created": <unix seconds>, | ||
| 124 | "sig": "<hex 64-byte BIP340 Schnorr signature>" | ||
| 125 | } | ||
| 126 | ``` | ||
| 127 | |||
| 128 | Onion-capable descriptor: | ||
| 129 | |||
| 130 | ```json | ||
| 131 | { | ||
| 132 | "v": 3, | ||
| 133 | "pubkey": "<hex 32-byte x-only identity key>", | ||
| 134 | "enc": "<hex 32-byte X25519 public key>", | ||
| 135 | "meeting_node": "host:port", | ||
| 136 | "onion": "<56-char-v3-name>.onion:8433", | ||
| 137 | "created": <unix seconds>, | ||
| 138 | "sig": "<legacy v2 signature>", | ||
| 139 | "sig3": "<signature over the onion-capable digest>" | ||
| 140 | } | ||
| 141 | ``` | ||
| 142 | |||
| 143 | The legacy signed digest is: | ||
| 144 | |||
| 145 | ``` | ||
| 146 | digest = SHA256( ".btf-descriptor-v2" | ||
| 147 | || pubkey[32] | ||
| 148 | || enc (raw bytes of the string) | ||
| 149 | || meeting_node (raw bytes of the string) | ||
| 150 | || created (8 bytes, big endian) ) | ||
| 151 | ``` | ||
| 152 | |||
| 153 | `sig` is kept on v3 descriptors so old nodes can still resolve and dial through | ||
| 154 | the rendezvous relay. The onion-capable digest is: | ||
| 155 | |||
| 156 | ``` | ||
| 157 | digest3 = SHA256( ".btf-descriptor-v3-onion" | ||
| 158 | || pubkey[32] | ||
| 159 | || enc (raw bytes of the string) | ||
| 160 | || meeting_node (raw bytes of the string) | ||
| 161 | || onion (raw bytes of the normalized string) | ||
| 162 | || created (8 bytes, big endian) ) | ||
| 163 | ``` | ||
| 164 | |||
| 165 | `sig` and `sig3` are deterministic BIP340 Schnorr signatures (no auxiliary | ||
| 166 | randomness) over their respective digests by the identity key. | ||
| 167 | |||
| 168 | **Verification** (untrusted input, must never throw): parse the JSON, require all | ||
| 169 | fields with the right types, confirm `pubkey` equals the address being resolved, | ||
| 170 | recompute the digest, and verify the Schnorr signature. Any failure rejects the | ||
| 171 | descriptor. | ||
| 172 | |||
| 173 | If `onion` is present, readers accept it only when it is a normalized v3 onion | ||
| 174 | endpoint and `sig3` verifies. A bad or tampered onion field is ignored; the v2 | ||
| 175 | descriptor can still be used for rendezvous fallback if `sig` verifies. | ||
| 176 | |||
| 177 | `created` lets a reader prefer the freshest descriptor when more than one is | ||
| 178 | seen. A node without a direct onion endpoint only publishes a descriptor once it | ||
| 179 | has actually registered at a `meeting_node`, so it never advertises a relay it | ||
| 180 | cannot be reached at. A node with a direct onion endpoint may publish before | ||
| 181 | rendezvous registration using `meeting_node = "rendezvous-pending"`; Tor peers | ||
| 182 | can still dial the signed onion endpoint, and rendezvous becomes fallback once | ||
| 183 | registration succeeds. | ||
| 184 | |||
| 185 | ## 6. Rendezvous transport | ||
| 186 | |||
| 187 | The relay is a small TCP server. Every connection opens with a fixed 33 byte | ||
| 188 | header: | ||
| 189 | |||
| 190 | ``` | ||
| 191 | byte 0 role: 'S' (service) or 'C' (client) | ||
| 192 | bytes 1..32 target public key (the .btf identity, 32 bytes) | ||
| 193 | ``` | ||
| 194 | |||
| 195 | **Service registration (`S`).** The service connects out to the relay and sends | ||
| 196 | `'S' || own_pubkey`. The relay stores the connection in a table keyed by the | ||
| 197 | public key and leaves it open, waiting. There is one slot per key: a new | ||
| 198 | registration for the same key replaces and closes the old one. The service does | ||
| 199 | not block waiting to be paired at registration time; it is considered registered | ||
| 200 | the moment the relay reads the header, which is what lets it publish a descriptor | ||
| 201 | pointing at this relay. | ||
| 202 | |||
| 203 | **Client dial (`C`).** The caller connects out to the same relay and sends | ||
| 204 | `'C' || target_pubkey`. The relay looks up the waiting service: | ||
| 205 | |||
| 206 | - If present, it removes it from the table, writes a single `0x01` byte to both | ||
| 207 | sockets, and then forwards raw bytes in both directions until either side | ||
| 208 | closes. From this point the relay is a blind pipe. | ||
| 209 | - If absent, it writes `0x00` to the caller and closes. | ||
| 210 | |||
| 211 | Both sides connect *outbound* to the relay, so neither needs a public IP or an | ||
| 212 | open inbound port. The relay never sees the identity of the caller (the caller | ||
| 213 | sends no key of its own, only the target it wants). | ||
| 214 | |||
| 215 | **Liveness.** A registration socket is idle by design and can outlive the network | ||
| 216 | path that created it (a NAT drops the idle mapping, the relay restarts, a route | ||
| 217 | changes) with no FIN and nothing readable. TCP keepalive was measured and does | ||
| 218 | not reliably detect this. The service therefore imposes its own timeout on the | ||
| 219 | wait for the pairing byte and re-registers on expiry. Expiring is not a failure; | ||
| 220 | it costs one reconnect per interval and returns the loop to a known state. | ||
| 221 | |||
| 222 | ## 7. The encrypted channel | ||
| 223 | |||
| 224 | Once paired through the relay, the caller and service run an end to end handshake | ||
| 225 | and then exchange authenticated frames. The primitive is libsodium | ||
| 226 | `crypto_box_curve25519xchacha20poly1305` (X25519 key agreement, XChaCha20-Poly1305 | ||
| 227 | AEAD), the same construction as Rust's `crypto_box` ChaChaBox. | ||
| 228 | |||
| 229 | **Handshake (ephemeral to static).** | ||
| 230 | |||
| 231 | 1. The caller generates a fresh ephemeral X25519 keypair, computes | ||
| 232 | `shared = X25519(eph_sk, service_enc_pub)` reduced to a box key | ||
| 233 | (`crypto_box_beforenm`), and sends the 32 byte ephemeral public key over the | ||
| 234 | paired pipe. | ||
| 235 | 2. The service reads the ephemeral public key and computes the same | ||
| 236 | `shared = X25519(enc_sk, eph_pub)`. | ||
| 237 | |||
| 238 | Because the caller uses a new ephemeral key per connection, each session has an | ||
| 239 | independent key and forward secrecy against later compromise of the service | ||
| 240 | encryption key. | ||
| 241 | |||
| 242 | **Framing.** Every message is length prefixed: | ||
| 243 | |||
| 244 | ``` | ||
| 245 | frame = len[4] (big endian, length of the body that follows) | ||
| 246 | body = nonce[24] || ciphertext || mac[16] | ||
| 247 | ``` | ||
| 248 | |||
| 249 | The nonce is 24 random bytes per frame (XChaCha20), the MAC is 16 bytes | ||
| 250 | (Poly1305). A body shorter than `nonce + mac` or larger than 1 MiB is rejected. | ||
| 251 | Decryption that fails authentication drops the frame and tears the tunnel down. | ||
| 252 | |||
| 253 | The tunnel is transparent: the application is handed a local socket pair end and | ||
| 254 | reads and writes plaintext. Two pump threads move data between that socket and | ||
| 255 | the relay socket, encrypting outbound and decrypting inbound. | ||
| 256 | |||
| 257 | ## 8. End to end connection flow | ||
| 258 | |||
| 259 | To dial `X.btf`: | ||
| 260 | |||
| 261 | 1. Parse `X.btf` to the 32 byte identity key. | ||
| 262 | 2. Fetch and verify the descriptor over Nostr (kind 38501). Obtain | ||
| 263 | `meeting_node` and `enc`. | ||
| 264 | 3. Connect to `meeting_node`, send `'C' || X`, and wait for `0x01`. | ||
| 265 | 4. Generate an ephemeral X25519 key, derive the shared key against `enc`, send | ||
| 266 | the ephemeral public key. | ||
| 267 | 5. Exchange XChaCha20-Poly1305 frames. Hand the application a local socket that | ||
| 268 | speaks the ordinary peer protocol. | ||
| 269 | |||
| 270 | A service, symmetrically: registers at a relay (`'S' || own_pubkey`), publishes a | ||
| 271 | descriptor pointing at that relay, waits for `0x01`, reads the caller ephemeral | ||
| 272 | public key, derives the shared key with its encryption secret, and serves the | ||
| 273 | tunnel. | ||
| 274 | |||
| 275 | ## 9. Security properties | ||
| 276 | |||
| 277 | - **Address unforgeability.** The address is the identity key and the descriptor | ||
| 278 | is Schnorr signed by it, and the Nostr event is signed by it. A relay or Nostr | ||
| 279 | operator cannot publish a working descriptor for an address it does not own. | ||
| 280 | - **Relay confidentiality.** The relay forwards ciphertext and never holds the | ||
| 281 | channel key. It cannot read or modify traffic undetected; a tampered frame | ||
| 282 | fails the Poly1305 tag and the tunnel closes. | ||
| 283 | - **Caller anonymity to the service and relay.** The caller presents no identity | ||
| 284 | key, only the target it wants to reach. It connects outbound, so its address is | ||
| 285 | not published anywhere. | ||
| 286 | - **Forward secrecy.** The caller's per connection ephemeral key means a later | ||
| 287 | compromise of the service encryption key does not decrypt past sessions. | ||
| 288 | - **IP privacy.** Neither side needs a public IP or an inbound port for | ||
| 289 | rendezvous. With a signed `.onion` endpoint, the reachable side exposes only a | ||
| 290 | Tor hidden service and the relay is not in the peer connection path. | ||
| 291 | |||
| 292 | ## 10. Threat model and limitations | ||
| 293 | |||
| 294 | - **The relay is trusted for availability, not for confidentiality.** It can | ||
| 295 | refuse to pair, drop a connection, or go offline. This is why relays are | ||
| 296 | plural, auto discovered, and re-registration is periodic. It cannot read | ||
| 297 | traffic. | ||
| 298 | - **Traffic analysis is not addressed.** A network observer, or the relay, can | ||
| 299 | see connection timing and byte volumes. There is no padding or mixing. | ||
| 300 | - **Single slot registration causes churn.** One waiting slot per key per relay | ||
| 301 | means a race or a stale registration can briefly make a service unreachable at | ||
| 302 | that relay. Multiple relays and the wait timeout bound the damage, but this is | ||
| 303 | a known source of connection churn. | ||
| 304 | - **Descriptor freshness is best effort.** `created` is a self reported | ||
| 305 | timestamp; readers prefer the newest descriptor they have seen but there is no | ||
| 306 | global clock. | ||
| 307 | - **Discovery depends on public Nostr relays.** If the relays a node uses are all | ||
| 308 | unreachable, discovery stalls until one is reachable again. Relay TLS is | ||
| 309 | verified, which closes a man in the middle path on discovery, but availability | ||
| 310 | still rests on the relay set. | ||
| 311 | - **Direct onion needs Tor.** With `-managedtor`, Bitflash starts a Tor process, | ||
| 312 | writes a local hidden-service config, and advertises the generated onion | ||
| 313 | endpoint. With `-onionservice`, an operator can still run Tor manually and | ||
| 314 | provide the endpoint. In both cases Tor owns the hidden-service private key and | ||
| 315 | uptime. | ||
| 316 | |||
| 317 | ## 11. Wire reference | ||
| 318 | |||
| 319 | | Item | Value | | ||
| 320 | |---|---| | ||
| 321 | | `.btf` Base32 alphabet | `abcdefghijklmnopqrstuvwxyz234567`, no padding | | ||
| 322 | | Address payload | `pubkey[32] || checksum[2]`, 55 Base32 chars | | ||
| 323 | | Address checksum | `SHA256(".btf-checksum-v1" || pubkey)[0..2]` | | ||
| 324 | | Descriptor domain sep | `.btf-descriptor-v2`; optional onion sep `.btf-descriptor-v3-onion` | | ||
| 325 | | Descriptor version | `2`; `3` when a signed onion endpoint is present | | ||
| 326 | | Descriptor signature | BIP340 Schnorr, deterministic (no aux rand), v3 keeps `sig` plus `sig3` | | ||
| 327 | | Nostr descriptor event | kind `38501`, `d = btf-descriptor-2` | | ||
| 328 | | Nostr relay event | kind `38502`, `d = btf-relay-2` | | ||
| 329 | | Nostr pool event | kind `38503`, `d = btf-pool-2` | | ||
| 330 | | Rendezvous port (default) | `8434` | | ||
| 331 | | Rendezvous header | `role[1] ('S'/'C') || pubkey[32]`, 33 bytes | | ||
| 332 | | Rendezvous pairing byte | `0x01` paired, `0x00` no service | | ||
| 333 | | Channel primitive | `crypto_box_curve25519xchacha20poly1305` | | ||
| 334 | | Nonce / MAC / key sizes | 24 / 16 / 32 bytes | | ||
| 335 | | Frame | `len[4 big endian] || nonce[24] || ct || mac[16]` | | ||
| 336 | | Max frame body | 1 MiB | | ||