Skip to content

JoinMarket Directory Server

Relay server for peer discovery and message routing in the JoinMarket network.

Features

  • Peer Discovery: Register and discover active peers
  • Message Routing: Forward public broadcasts and private messages
  • Connection Management: Handle peer connections and disconnections
  • Handshake Protocol: Verify peer compatibility and network
  • High Performance: Async I/O with optimized message handling
  • Observability: Structured logging with loguru
  • Tor Hidden Service: Run behind Tor for privacy (via separate container)

Running

Use the directory_server/docker-compose.yml deployment for production-style operation behind a Tor hidden service. The compose stack isolates the directory server on an internal network with no direct internet access; only the Tor container bridges both internal and external networks.

  1. Go to the directory_server directory.
  2. Start Tor first with docker compose up -d tor.
  3. Retrieve your .onion address:
cat tor/data/hostname
  1. Create or update .env in the current directory_server/ directory with the complete endpoint. This setting is required for a public directory node:
DIRECTORY_SERVER__NICK_AUTH_DIRECTORY_ID=your56characterhostname.onion:5222
  1. Start the directory with docker compose up -d directory_server.

The Compose deployment refuses to start the directory server if this variable is missing. Do not use docker compose up for the whole stack until it is set.

DIRECTORY_SERVER__NICK_AUTH_DIRECTORY_ID binds each authentication challenge to this specific directory endpoint. Without it, the server cannot advertise nick authentication, clients do not prove ownership of their nicks, and another connection can claim an unverified nick. Treating the setting as optional disables this protection.

Docker Notes

Network Architecture
Internet <--> [tor container] <-- internal network --> [directory_server container]
  • directory_server lives on joinmarket_directory_internal (bridge, internal) -- no direct internet access, no information leakage.
  • tor bridges joinmarket_directory_internal and joinmarket_directory_external, exposing the directory server only as a Tor hidden service.
Debug Image

A debug variant with pdbpp and memray pre-installed is available:

docker pull ghcr.io/joinmarket-ng/joinmarket-ng/directory-server:main-debug

Profile memory with memray:

docker run -it --rm \
  -v $(pwd)/memray-output:/app/memray-output \
  ghcr.io/joinmarket-ng/joinmarket-ng/directory-server:main-debug \
  memray run -o /app/memray-output/profile.bin -m directory_server.main

To attach memray to a running container, add cap_add: [SYS_PTRACE] to the service in docker-compose.yml, then:

docker exec -it joinmarket_directory_server \
  python -m memray attach 1 --verbose

Installation without Docker

See Installation for general setup. For local development:

cd joinmarket-ng
source jmvenv/bin/activate  # If you used install.sh
# OR create venv: python3 -m venv jmvenv && source jmvenv/bin/activate

# Install jmcore first
cd jmcore
pip install -e .

# Install directory server
cd ../directory_server
pip install -e .

# Development
pip install -e ".[dev]"

Configuration

Create a .env file or set environment variables. A public directory node must set DIRECTORY_SERVER__NICK_AUTH_DIRECTORY_ID to the exact onion endpoint through which clients reach it, including the port:

# Network
NETWORK_CONFIG__NETWORK=mainnet  # mainnet, testnet, signet, regtest

# Server
DIRECTORY_SERVER__HOST=127.0.0.1
DIRECTORY_SERVER__PORT=5222
DIRECTORY_SERVER__NICK_AUTH_DIRECTORY_ID=your56characterhostname.onion:5222
DIRECTORY_SERVER__MAX_PEERS=10000
DIRECTORY_SERVER__MESSAGE_RATE_LIMIT=100
LOGGING__LEVEL=INFO

Heartbeat liveness settings (section [directory_server] in config.toml):

  • heartbeat_sweep_interval (default 60.0)
  • heartbeat_idle_threshold (default 600.0)
  • heartbeat_hard_evict (default 1500.0)
  • heartbeat_pong_wait (default 30.0)

Behavior summary:

  • Idle peers are probed with PING/PONG when they advertise "ping": true in handshake features
  • Legacy/non-ping makers receive !orderbook as a compatibility liveness probe
  • Peers idle beyond hard eviction threshold are disconnected

Nick ownership authentication settings (section [directory_server]):

  • nick_auth_directory_id is required for public directory nodes. It binds signed nick proofs to the directory selected by the client and prevents proofs from being replayed to another directory. Without it, nick ownership authentication is not advertised or performed.
  • Set nick_auth_directory_id to the canonical lowercase Tor v3 endpoint, including the port, for example your56characterhostname.onion:5222. Local tests may use an explicit test: identity, but public deployments must not.
  • nick_auth_mode defaults to prefer_verified, which authenticates upgraded clients while retaining legacy client compatibility. require_verified rejects legacy clients, and disabled intentionally turns off this security feature.
  • nick_auth_timeout controls the proof deadline and cannot exceed 30 seconds.

The directory keys relay ownership by nick. A peer's self-declared onion location is advertised connection metadata and is not reserved or treated as proof of endpoint ownership.

For local test deployments reached through an alias or forwarded port, configure network_config.nick_auth_directory_ids on each client. The map key is the exact selected host:port, and the value is the directory's configured test: identity. Production .onion identities are derived from and matched exactly against the selected endpoint.

For the production Compose stack, retrieve the generated hostname first and configure the resulting hostname.onion:5222 value as DIRECTORY_SERVER__NICK_AUTH_DIRECTORY_ID before starting the directory server container. Do not use the container hostname as the identity for a public Tor endpoint.

Optional

Vanity Onion Address

Generate a vanity .onion address using mkp224o before first start. The tool creates one output directory per matching address and runs until killed. Five-character prefixes are fast; six characters can take hours.

docker run --rm -it --network none -v $PWD:/keys \
  ghcr.io/cathugger/mkp224o:master -d /keys desired_prefix

Copy the generated keys into the Tor data directory:

mv desired_prefix*.onion/hs_ed25519_public_key \
   desired_prefix*.onion/hs_ed25519_secret_key \
   desired_prefix*.onion/hostname \
   tor/data/
chown -R 1000:1000 tor/data/

If the stack is already running, restart Tor to pick up the new keys:

docker compose restart tor
cat tor/data/hostname

Health Check & Monitoring

The directory server provides comprehensive health check and monitoring capabilities.

Health Check Endpoint

An HTTP server runs on port 8080 (configurable via HEALTH_CHECK_HOST and HEALTH_CHECK_PORT) providing:

GET /health - Basic health check

curl http://localhost:8080/health
# {"status": "healthy"}

GET /status - Detailed server statistics

curl http://localhost:8080/status
# {
#   "network": "mainnet",
#   "uptime_seconds": 3600,
#   "server_status": "running",
#   "max_peers": 1000,
#   "stats": {
#     "total_peers": 150,
#     "connected_peers": 150,
#     "passive_peers": 45,
#     "active_peers": 105
#   },
#   "connected_peers": {
#     "total": 150,
#     "nicks": ["maker1", "taker1", ...]
#   },
#   "passive_peers": {
#     "total": 45,
#     "nicks": ["taker1", "taker2", ...]
#   },
#   "active_peers": {
#     "total": 105,
#     "nicks": ["maker1", "maker2", ...]
#   },
#   "active_connections": 150
# }

CLI Tool

Use jm-directory-ctl to query server status:

# Check server health
jm-directory-ctl health

# Get detailed status (human-readable)
jm-directory-ctl status

# Get status as JSON
jm-directory-ctl status --json

# Query remote server
jm-directory-ctl status --host 192.168.1.10 --port 8080

Signal-based Status Logging

Send SIGUSR1 signal to trigger detailed status logging to the server logs:

# Docker
docker kill -s SIGUSR1 joinmarket_directory_server

# Local process
kill -USR1 $(pgrep jm-directory-server)

This will log comprehensive status including: - Network type and uptime - Connected peers count and list - Passive peers (orderbook watchers/takers - NOT-SERVING-ONION) - Active peers (makers - serving onion address) - Active connections

Docker Health Check

The Docker image includes automatic health checks using the CLI command:

HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
    CMD ["jm-directory-ctl", "health"]

Check container health status:

docker ps  # Shows (healthy) or (unhealthy)
docker inspect joinmarket_directory_server | grep -A 10 Health

Protocol and Security Context

Command Reference

jm-directory-ctl --help
usage: jm-directory-ctl [-h] [--host HOST] [--log-level LOG_LEVEL]
                        [--port PORT]
                        {health,status} ...

JoinMarket Directory Server CLI

positional arguments:
  {health,status}       Available commands
    health              Check server health
    status              Get server status

options:
  -h, --help            show this help message and exit
  --host HOST           Health check server host (default: 127.0.0.1)
  --log-level, -l LOG_LEVEL
                        Log level (default: INFO)
  --port PORT           Health check server port (default: 8080)
jm-directory-ctl health --help
usage: jm-directory-ctl health [-h] [--json]

options:
  -h, --help  show this help message and exit
  --json      Output as JSON
jm-directory-ctl status --help
usage: jm-directory-ctl status [-h] [--json]

options:
  -h, --help  show this help message and exit
  --json      Output as JSON