12 KiB
Reticulum Integration
This document explains how Qortal Hub integrates the Reticulum Network Stack (RNS) for decentralised, end-to-end encrypted peer-to-peer communication.
What Reticulum Provides
Reticulum is a cryptography-based networking stack designed for reliable communication over any medium. In Qortal Hub it serves as the transport layer for:
- Presence — signed, encrypted envelopes broadcast to overlay peers so users see each other as online.
- Direct voice calls — peer-to-peer audio with room IDs of the form
dmv:<sha256_prefix>. - Group voice calls — multi-party audio with topology-aware forwarding (see
docs/group-audio-calls.md). - Q-Chat file transfers — encrypted file offers and deliveries with a 2-hour TTL.
Architecture Overview
Renderer (React)
│ window.electronAPI.reticulum*()
│ window.groupCall.*()
▼
Electron main process
│ reticulum-daemon.ts — spawns and manages rnsd
│ reticulum-bridge.ts — commands the Python bridge
│ reticulum-mesh.ts — hub-to-hub mesh coordinator
│ group-call.ts — group call manager
▼
Python bridge (presence_bridge.py) ← fd3/fd4 binary IPC
▼
rnsd (Reticulum Network Stack daemon)
▼
Network (LAN AutoInterface / hub TCPClient+BackboneInterface)
Components
Daemon — reticulum-daemon.ts
Manages the rnsd process (module RNS.Utilities.rnsd).
Python resolution order:
- PyInstaller one-file binary under
resources/reticulum/(packaged builds). - venv under
resources/reticulum-runtime/venv/(dev/optional). - System Python with
rnsinstalled (pip install rns) — no env var needed. - System Python when
QORTAL_RETICULUM_SYSTEM=1is set.
Set QORTAL_RETICULUM_NO_SYSTEM=1 to opt out of (3) and (4) in dev.
Key paths and ports:
| Item | Value |
|---|---|
| Config directory | appData/qortal-hub/reticulum — shared by all local app instances |
| Daemon base port | 37428 + N where N is the instance index |
| Control base port | 37429 + N |
| Instance name | qortal-hub-shared |
| Discovery announce interval | 5 minutes |
| Daemon stop timeout | 10 seconds |
Multi-instance behaviour: The daemon is shared across all open Hub windows. A JSON instance registry tracks active app PIDs. On quit, the daemon is only stopped when no other instance is still active (ref-counted).
Process priority: On startup, the daemon PID is re-niced to -7 by default. Override with QORTAL_RETICULUM_PRIORITY_NICE=<value> or disable with QORTAL_RETICULUM_PRIORITY_NICE=off.
Reachability states reported by the daemon:
| State | Meaning |
|---|---|
unknown |
Status not yet determined |
lan-only |
Reachable only on the local network |
hub-connected |
Connected to at least one bootstrap hub |
disconnected |
No RNS connectivity |
Bridge — reticulum-bridge.ts
A Python subprocess (presence_bridge.py) launched after the daemon is ready.
- Communicates with the Electron main process via extra stdio file descriptors:
- fd 3 — Electron → Python (commands + outbound audio batches)
- fd 4 — Python → Electron (events + inbound audio batches)
- Commands use a JSON request/response model with frame IDs for correlation.
- Audio frames use a separate binary format (see Audio IPC below).
Bridge states: stopped → starting → ready / degraded
Key bridge actions:
| Action | Purpose |
|---|---|
publish_presence / forward_presence |
Broadcast signed presence envelopes |
send_group_call / fanout_group_call |
Group call signaling |
open_group_audio_link |
Establish a Reticulum audio link to a peer |
send_group_audio_link_heartbeat |
Keep audio links alive |
accept_qchat_file_resource |
Accept an incoming file offer |
send_qchat_file_resource |
Send a file offer to a peer |
authorize_qchat_file_resource |
Authorise file delivery |
Mesh Coordinator — reticulum-mesh.ts
Handles hub-to-hub mesh networking, separate from the TLS P2P layer (p2p-network.ts).
Interface types:
| Platform | Listen interface | Outbound hub interface |
|---|---|---|
| Linux | BackboneInterface (port 4243) |
BackboneInterface |
| Windows / macOS | TCPServerInterface (port 4243) |
TCPClientInterface |
Community mesh discovery uses RNS AutoInterface with autoconnect_discovered_interfaces capped at 8 peers.
Default bootstrap hubs:
| Name | Host | Port |
|---|---|---|
| Backbone Client Qortal Hub | phantom.mobilefabrik.com |
4400 |
| Crowetic Reticulum Hub | reticulum.qortal.link |
4444 |
| Crowetic Reticulum Hub 2 | reticulum2.qortal.link |
4444 |
UPnP: The coordinator attempts to map the mesh listen port via UPnP and records the discovered WAN IP in reachable_on so remote peers can reach this node. A manually configured meshReachableOnHost always takes precedence over the UPnP-discovered value.
Persistent state is stored in appData/qortal-hub/reticulum-mesh-state.json.
Audio IPC — reticulum-audio-ipc.ts
Binary message format used on fd 3/fd 4 for audio frames:
Header:
magic: "QAUD" (4 bytes, ASCII)
version: 1 (1 byte)
bodyLen: uint32 (4 bytes)
Body (one or more frames):
frame_count
linkIdLen | linkId
roomIdLen | roomId
peerPresenceHashLen | peerPresenceHash
peerCallHashLen | peerCallHash
payloadLen | payload
- The bridge batches multiple frames per message to reduce syscall overhead.
- Fairness and pressure control prevent any single peer leg from starving others.
- Buffer objects carry timing metadata via
Symbol.for('qortal.*')symbols.
For the full audio path (capture → Opus → encrypt → send → receive → decrypt → playout) see docs/group-audio-calls.md.
Identities
Qortal Hub uses two separate Reticulum identities with different lifetimes and scopes.
Local (Per-Installation) Identity
Source: presence_bridge.py → ensure_identity()
Stored at: userData/reticulum/presence-bridge.identity
On first startup the Python bridge checks whether the file exists:
- File found → loaded with
RNS.Identity.from_file()— the same identity is reused every time. - File missing → a new
RNS.Identity()is generated and immediately written to disk.
The file path is passed from the Electron main process via the QORTAL_RETICULUM_IDENTITY_PATH environment variable (set in reticulum-bridge.ts → spawnAndHandshake()). The resulting public key is what reticulumGetLocalIdentityPublicKeyBase64() returns and is embedded as the rk field in GC_JOIN messages for voice call authentication.
Conclusion: unique per installation, generated once, persisted across restarts.
Mesh Network Identity
Source: reticulum-mesh-store.ts → getBundledMeshNetworkIdentityPath()
Stored at: userData/reticulum/mesh-network.identity (copied from app bundle)
This file is shipped inside every Qortal Hub app bundle (resources/reticulum/mesh-network.identity) and is identical for all installations. On first use ensureMeshNetworkIdentityIfNeeded() copies it into userData. It is referenced as network_identity in the managed Reticulum config so that all Hub instances join the same authenticated mesh segment and can discover each other's private gateways.
Conclusion: shared by all Hub users, not generated — bundled with the app.
Summary
| Identity | Scope | Generated | Stored |
|---|---|---|---|
| Local bridge identity | Per installation | Once, on first startup | userData/reticulum/presence-bridge.identity |
| Mesh network identity | All Hub installations | Never (bundled) | userData/reticulum/mesh-network.identity (copied from bundle) |
Startup and Shutdown
Startup sequence (index.ts)
recoverReticulumStateForAppLaunch()— cleans up any orphaned daemon from a previous crash.startReticulumForAppLaunch()— spawns the daemon and waits up to 10 seconds for it to be ready.ensureReticulumManagersStarted()— starts the presence manager, call manager, and group call manager.
Shutdown sequence
planReticulumAppQuit()— checks the instance registry;shouldStopSharedDaemonis true only when this is the last active instance.stopReticulumBridge()— terminates the Python bridge process.stopReticulumMeshCoordinator()— tears down mesh UPnP mappings and stops mesh state.- Daemon process is stopped only if
shouldStopSharedDaemonis true.
Renderer API
Exposed via window.electronAPI (context bridge in electron/src/preload.ts):
| Method | Description |
|---|---|
reticulumGetStatus() |
One-shot snapshot: daemon PID, mode, bridge state, reachability, hub interface counts |
onReticulumStatus(cb) |
Subscribe to live status updates; returns an unsubscribe function |
reticulumGetOverlayPeers() |
List of active Reticulum links with presence hashes and connection timestamps |
reticulumGetMeshStatus() |
Mesh enable flag, listen port, UPnP state, reachability hosts |
reticulumEnsureMeshNetworkIdentity() |
Create the mesh identity file if it does not exist yet |
reticulumGetLocalDestinationHash() |
This instance's 32-char hex RNS destination hash |
reticulumGetLocalIdentityPublicKeyBase64() |
RNS.Identity public key (used in GC_JOIN messages) |
Presence Flow
- The renderer calls
window.groupCall.gcallProxySignPresenceMessage()to sign a presence envelope with the wallet key. - The Electron main process forwards the signed envelope to the Python bridge via
publish_presence. - The bridge broadcasts it over Reticulum links to connected peers.
- Incoming presence envelopes are forwarded back to the renderer via the
reticulum:presenceUpdateIPC event. - Subscriptions are batched on a 16 ms interval in the preload layer to avoid flooding the renderer.
Q-Chat File Transfer
Files are offered and delivered over Reticulum links with the following constraints:
| Constraint | Value |
|---|---|
| Offer TTL | 2 hours |
| Completed-send cache grace | 7 days |
| Signature max age | 24 hours |
| Signature max future skew | 2 minutes |
| Bridge attach retry interval | 3 seconds |
Environment Variables
| Variable | Effect |
|---|---|
QORTAL_RETICULUM_SYSTEM=1 |
Force use of system Python even in packaged builds |
QORTAL_RETICULUM_NO_SYSTEM=1 |
Disable system Python fallback in dev |
QORTAL_RETICULUM_PRIORITY_NICE=<n> |
Override rnsd process nice value (default -7) |
QORTAL_RETICULUM_PRIORITY_NICE=off |
Disable nice adjustment entirely |
Relevant Files
| File | Purpose |
|---|---|
electron/src/reticulum-daemon.ts |
rnsd process lifecycle, config generation, instance registry |
electron/src/reticulum-bridge.ts |
Python bridge process, command/event protocol, audio IPC |
electron/src/reticulum-launch.ts |
App-launch readiness wait |
electron/src/reticulum-mesh.ts |
Hub-to-hub mesh coordinator, UPnP |
electron/src/reticulum-mesh-store.ts |
Persistent mesh state (listen port, UPnP, reachable host) |
electron/src/reticulum-mesh-constants.ts |
Default listen port (4243), max outbound peers (8) |
electron/src/reticulum-audio-ipc.ts |
Binary audio frame encoding/decoding (QAUD format) |
electron/src/reticulum-audio-link-fallback-policy.ts |
Link quality decisions and fallback policy |
electron/src/reticulum-bridge-rebind.ts |
Bridge consumer rebinding for multi-instance scenarios |
electron/src/group-call-wire-reticulum.ts |
Group call wire protocol over Reticulum |
electron/src/preload.ts |
Context bridge — window.electronAPI.reticulum* methods |
electron/resources/presence_bridge.py |
Python bridge process (presence, calls, file transfers) |