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 so the directory can advertise the nick authentication extension and perform nick ownership authentication:
DIRECTORY_SERVER__NICK_AUTH_DIRECTORY_ID=your56characterhostname.onion:5222
  1. Start the directory with docker compose up -d directory_server.

Running the whole stack before this variable is set leaves nick authentication disabled. The directory logs a warning and its handshake does not advertise nick_auth in that state.

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:

# Network
NETWORK=mainnet  # mainnet, testnet, signet, regtest
HOST=127.0.0.1
PORT=5222

# Server
MAX_PEERS=10000
MESSAGE_RATE_LIMIT=100
LOG_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_mode defaults to prefer_verified, which authenticates upgraded clients while retaining legacy client compatibility. require_verified rejects legacy clients, and disabled does not advertise the extension.
  • nick_auth_directory_id must be the canonical lowercase Tor v3 endpoint, including the port, for example your56characterhostname.onion:5222. Authentication is not advertised until this identity is configured. Local tests may use an explicit test: identity.
  • 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, configure the resulting hostname.onion:5222 value as DIRECTORY_SERVER__NICK_AUTH_DIRECTORY_ID, then restart 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