Files
2026-05-24 15:12:46 +02:00

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:

  1. PyInstaller one-file binary under resources/reticulum/ (packaged builds).
  2. venv under resources/reticulum-runtime/venv/ (dev/optional).
  3. System Python with rns installed (pip install rns) — no env var needed.
  4. System Python when QORTAL_RETICULUM_SYSTEM=1 is 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: stoppedstartingready / 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.pyensure_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.tsspawnAndHandshake()). 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.tsgetBundledMeshNetworkIdentityPath()
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)

  1. recoverReticulumStateForAppLaunch() — cleans up any orphaned daemon from a previous crash.
  2. startReticulumForAppLaunch() — spawns the daemon and waits up to 10 seconds for it to be ready.
  3. ensureReticulumManagersStarted() — starts the presence manager, call manager, and group call manager.

Shutdown sequence

  1. planReticulumAppQuit() — checks the instance registry; shouldStopSharedDaemon is true only when this is the last active instance.
  2. stopReticulumBridge() — terminates the Python bridge process.
  3. stopReticulumMeshCoordinator() — tears down mesh UPnP mappings and stops mesh state.
  4. Daemon process is stopped only if shouldStopSharedDaemon is 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

  1. The renderer calls window.groupCall.gcallProxySignPresenceMessage() to sign a presence envelope with the wallet key.
  2. The Electron main process forwards the signed envelope to the Python bridge via publish_presence.
  3. The bridge broadcasts it over Reticulum links to connected peers.
  4. Incoming presence envelopes are forwarded back to the renderer via the reticulum:presenceUpdate IPC event.
  5. 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)