upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/docs/rendezvous.md
diff options
context:
space:
mode:
authorBitflash <admin@bitflash.network>2026-08-10 11:21:39 -0300
committerBitflash <admin@bitflash.network>2026-08-10 11:21:39 -0300
commit64317cc5a3416d77c9171351c367ab8c24c4dff1 (patch)
tree1cc7b3cfd9253bcab08a88778df2b8798dc5350d /docs/rendezvous.md
parentdfab2226330ccf5b6529a0e7355e1f574526de94 (diff)
docs: specify the .btf rendezvous protocol
Diffstat (limited to 'docs/rendezvous.md')
-rw-r--r--docs/rendezvous.md288
1 files changed, 288 insertions, 0 deletions
diff --git a/docs/rendezvous.md b/docs/rendezvous.md
new file mode 100644
index 0000000..ac8e598
--- /dev/null
+++ b/docs/rendezvous.md
@@ -0,0 +1,288 @@
1# Bitflash `.btf` Rendezvous Protocol
2
3This is the specification of how Bitflash nodes reach each other, and how miners
4reach pools, without revealing an IP address and without any central directory.
5It documents the protocol as implemented in `src/btfaddr.cpp`, `src/btfchan.cpp`,
6`src/btfrv.cpp`, `src/btftunnel.cpp` and `src/nostr.cpp`.
7
8The design is ported from the Itzal hidden service design. A `.btf` address names
9a public key, a node proves ownership of that key by signing a service
10descriptor, other nodes find the descriptor over Nostr, and the two sides then
11meet through an untrusted relay over an end to end encrypted tunnel. The relay
12carries ciphertext only and never learns the plaintext.
13
14## 1. Overview
15
16A connection to a `.btf` service has three layers:
17
181. **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 and which encryption key to use.
222. **Rendezvous (TCP).** Caller and service both connect out to the same relay
23 and are paired by public key. Neither needs an inbound port or a public IP.
24 After pairing the relay is a blind byte pipe.
253. **Encrypted channel (X25519 + XChaCha20-Poly1305).** Over the paired pipe the
26 caller and service run an ephemeral to static handshake and then exchange
27 authenticated frames. The relay sees only ciphertext.
28
29The caller ends up holding an ordinary local socket that behaves like a direct
30TCP connection to the peer. The node treats it exactly like any other peer
31socket.
32
33## 2. Identities and keys
34
35A node holds two independent keypairs.
36
37- **Signing / identity key**: a secp256k1 x-only key (BIP340 Schnorr). This is
38 the `.btf` identity and also the node's Nostr identity. Persisted in
39 `nostr.key`.
40- **Encryption key**: an X25519 (Curve25519) keypair. Persisted in
41 `btf_enc.key`. Its public half is published in the descriptor as `enc` and is
42 used for the tunnel key agreement.
43
44The identity key names the service and authenticates the descriptor. The
45encryption key secures the tunnel. They are separate so the long term identity
46never performs Diffie-Hellman and the encryption key can be rotated in a new
47signed descriptor.
48
49## 3. The `.btf` address
50
51An address is the identity public key plus a short checksum, Base32 encoded, with
52the suffix `.btf`.
53
54```
55label = Base32( pubkey[32] || checksum[2] )
56address = label + ".btf"
57```
58
59- `pubkey` is the 32 byte secp256k1 x-only public key.
60- `checksum` is the first two bytes of `SHA256(".btf-checksum-v1" || pubkey)`.
61- Base32 uses the lowercase alphabet `abcdefghijklmnopqrstuvwxyz234567`, no
62 padding. The 34 byte payload encodes to 55 characters.
63- Parsing is case insensitive, trims surrounding whitespace, and strips a
64 trailing dot. A wrong checksum is rejected.
65
66The address is self certifying: it *is* the public key, so no registry has to be
67trusted to map a name to a key.
68
69## 4. Discovery over Nostr
70
71Nodes publish and read three kinds of parameterized replaceable Nostr events.
72Parameterized replaceable means the newest event for a given author and `d` tag
73supersedes the older one, so a descriptor is always the latest state.
74
75| Purpose | kind | `d` tag |
76|---|---|---|
77| Service descriptor | 38501 | `btf-descriptor-2` |
78| Rendezvous relay announcement | 38502 | `btf-relay-2` |
79| Pool announcement | 38503 | `btf-pool-2` |
80
81The Nostr event is signed with the node's identity key, so the event author
82equals the `.btf` public key. That binds the descriptor to the address at the
83Nostr layer, and the descriptor is *also* signed inside its own content (see
84below), so a relay operator cannot forge or swap a descriptor for an address it
85does not control.
86
87**Resolving an address.** To reach `X.btf`, a caller parses the address to the 32
88byte key and sends a Nostr `REQ` filtered by `authors = [X]`, `kinds = [38501]`,
89`#d = ["btf-descriptor-2"]`, `limit = 1`. It verifies the returned descriptor and
90uses its `meeting_node` and `enc`.
91
92**Relay auto discovery.** A relay operator runs a rendezvous relay and publishes a
93kind 38502 event carrying `host:port`. Every node that reads it adds the relay to
94the set it can register at and dial through. No seed list edit or maintainer
95approval is needed. A small curated seed list is still shipped for bootstrap:
96
97```
9892.246.128.180:8434
9931.44.4.249:8434
10090.156.222.107:8434
101```
102
103The default rendezvous port is `8434`.
104
105## 5. The service descriptor
106
107The descriptor is a JSON object, signed with the identity key over a domain
108separated digest of its own fields.
109
110```json
111{
112 "v": 2,
113 "pubkey": "<hex 32-byte x-only identity key>",
114 "enc": "<hex 32-byte X25519 public key>",
115 "meeting_node": "host:port",
116 "created": <unix seconds>,
117 "sig": "<hex 64-byte BIP340 Schnorr signature>"
118}
119```
120
121The signed digest is:
122
123```
124digest = SHA256( ".btf-descriptor-v2"
125 || pubkey[32]
126 || enc (raw bytes of the string)
127 || meeting_node (raw bytes of the string)
128 || created (8 bytes, big endian) )
129```
130
131The signature is a deterministic BIP340 Schnorr signature (no auxiliary
132randomness) over `digest` by the identity key.
133
134**Verification** (untrusted input, must never throw): parse the JSON, require all
135fields with the right types, confirm `pubkey` equals the address being resolved,
136recompute the digest, and verify the Schnorr signature. Any failure rejects the
137descriptor.
138
139`created` lets a reader prefer the freshest descriptor when more than one is
140seen. A node only publishes a descriptor once it has actually registered at a
141`meeting_node`, so it never advertises a relay it cannot be reached at.
142
143## 6. Rendezvous transport
144
145The relay is a small TCP server. Every connection opens with a fixed 33 byte
146header:
147
148```
149byte 0 role: 'S' (service) or 'C' (client)
150bytes 1..32 target public key (the .btf identity, 32 bytes)
151```
152
153**Service registration (`S`).** The service connects out to the relay and sends
154`'S' || own_pubkey`. The relay stores the connection in a table keyed by the
155public key and leaves it open, waiting. There is one slot per key: a new
156registration for the same key replaces and closes the old one. The service does
157not block waiting to be paired at registration time; it is considered registered
158the moment the relay reads the header, which is what lets it publish a descriptor
159pointing at this relay.
160
161**Client dial (`C`).** The caller connects out to the same relay and sends
162`'C' || target_pubkey`. The relay looks up the waiting service:
163
164- If present, it removes it from the table, writes a single `0x01` byte to both
165 sockets, and then forwards raw bytes in both directions until either side
166 closes. From this point the relay is a blind pipe.
167- If absent, it writes `0x00` to the caller and closes.
168
169Both sides connect *outbound* to the relay, so neither needs a public IP or an
170open inbound port. The relay never sees the identity of the caller (the caller
171sends no key of its own, only the target it wants).
172
173**Liveness.** A registration socket is idle by design and can outlive the network
174path that created it (a NAT drops the idle mapping, the relay restarts, a route
175changes) with no FIN and nothing readable. TCP keepalive was measured and does
176not reliably detect this. The service therefore imposes its own timeout on the
177wait for the pairing byte and re-registers on expiry. Expiring is not a failure;
178it costs one reconnect per interval and returns the loop to a known state.
179
180## 7. The encrypted channel
181
182Once paired through the relay, the caller and service run an end to end handshake
183and then exchange authenticated frames. The primitive is libsodium
184`crypto_box_curve25519xchacha20poly1305` (X25519 key agreement, XChaCha20-Poly1305
185AEAD), the same construction as Rust's `crypto_box` ChaChaBox.
186
187**Handshake (ephemeral to static).**
188
1891. The caller generates a fresh ephemeral X25519 keypair, computes
190 `shared = X25519(eph_sk, service_enc_pub)` reduced to a box key
191 (`crypto_box_beforenm`), and sends the 32 byte ephemeral public key over the
192 paired pipe.
1932. The service reads the ephemeral public key and computes the same
194 `shared = X25519(enc_sk, eph_pub)`.
195
196Because the caller uses a new ephemeral key per connection, each session has an
197independent key and forward secrecy against later compromise of the service
198encryption key.
199
200**Framing.** Every message is length prefixed:
201
202```
203frame = len[4] (big endian, length of the body that follows)
204 body = nonce[24] || ciphertext || mac[16]
205```
206
207The nonce is 24 random bytes per frame (XChaCha20), the MAC is 16 bytes
208(Poly1305). A body shorter than `nonce + mac` or larger than 1 MiB is rejected.
209Decryption that fails authentication drops the frame and tears the tunnel down.
210
211The tunnel is transparent: the application is handed a local socket pair end and
212reads and writes plaintext. Two pump threads move data between that socket and
213the relay socket, encrypting outbound and decrypting inbound.
214
215## 8. End to end connection flow
216
217To dial `X.btf`:
218
2191. Parse `X.btf` to the 32 byte identity key.
2202. Fetch and verify the descriptor over Nostr (kind 38501). Obtain
221 `meeting_node` and `enc`.
2223. Connect to `meeting_node`, send `'C' || X`, and wait for `0x01`.
2234. Generate an ephemeral X25519 key, derive the shared key against `enc`, send
224 the ephemeral public key.
2255. Exchange XChaCha20-Poly1305 frames. Hand the application a local socket that
226 speaks the ordinary peer protocol.
227
228A service, symmetrically: registers at a relay (`'S' || own_pubkey`), publishes a
229descriptor pointing at that relay, waits for `0x01`, reads the caller ephemeral
230public key, derives the shared key with its encryption secret, and serves the
231tunnel.
232
233## 9. Security properties
234
235- **Address unforgeability.** The address is the identity key and the descriptor
236 is Schnorr signed by it, and the Nostr event is signed by it. A relay or Nostr
237 operator cannot publish a working descriptor for an address it does not own.
238- **Relay confidentiality.** The relay forwards ciphertext and never holds the
239 channel key. It cannot read or modify traffic undetected; a tampered frame
240 fails the Poly1305 tag and the tunnel closes.
241- **Caller anonymity to the service and relay.** The caller presents no identity
242 key, only the target it wants to reach. It connects outbound, so its address is
243 not published anywhere.
244- **Forward secrecy.** The caller's per connection ephemeral key means a later
245 compromise of the service encryption key does not decrypt past sessions.
246- **IP privacy.** Neither side needs a public IP or an inbound port. The observable
247 network fact is that each side has a TCP connection to a relay.
248
249## 10. Threat model and limitations
250
251- **The relay is trusted for availability, not for confidentiality.** It can
252 refuse to pair, drop a connection, or go offline. This is why relays are
253 plural, auto discovered, and re-registration is periodic. It cannot read
254 traffic.
255- **Traffic analysis is not addressed.** A network observer, or the relay, can
256 see connection timing and byte volumes. There is no padding or mixing.
257- **Single slot registration causes churn.** One waiting slot per key per relay
258 means a race or a stale registration can briefly make a service unreachable at
259 that relay. Multiple relays and the wait timeout bound the damage, but this is
260 a known source of connection churn.
261- **Descriptor freshness is best effort.** `created` is a self reported
262 timestamp; readers prefer the newest descriptor they have seen but there is no
263 global clock.
264- **Discovery depends on public Nostr relays.** If the relays a node uses are all
265 unreachable, discovery stalls until one is reachable again. Relay TLS is
266 verified, which closes a man in the middle path on discovery, but availability
267 still rests on the relay set.
268
269## 11. Wire reference
270
271| Item | Value |
272|---|---|
273| `.btf` Base32 alphabet | `abcdefghijklmnopqrstuvwxyz234567`, no padding |
274| Address payload | `pubkey[32] || checksum[2]`, 55 Base32 chars |
275| Address checksum | `SHA256(".btf-checksum-v1" || pubkey)[0..2]` |
276| Descriptor domain sep | `.btf-descriptor-v2` |
277| Descriptor version | `2` |
278| Descriptor signature | BIP340 Schnorr, deterministic (no aux rand) |
279| Nostr descriptor event | kind `38501`, `d = btf-descriptor-2` |
280| Nostr relay event | kind `38502`, `d = btf-relay-2` |
281| Nostr pool event | kind `38503`, `d = btf-pool-2` |
282| Rendezvous port (default) | `8434` |
283| Rendezvous header | `role[1] ('S'/'C') || pubkey[32]`, 33 bytes |
284| Rendezvous pairing byte | `0x01` paired, `0x00` no service |
285| Channel primitive | `crypto_box_curve25519xchacha20poly1305` |
286| Nonce / MAC / key sizes | 24 / 16 / 32 bytes |
287| Frame | `len[4 big endian] || nonce[24] || ct || mac[16]` |
288| Max frame body | 1 MiB |