diff --git a/.dockerignore b/.dockerignore index 5e4973b3a..9ce0c1f77 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,22 +1,105 @@ -# Exclude git directory +# Version control .git +.gitignore +.gitattributes +.github/ -# Exclude node_modules (installed fresh during build) +# AI tooling state — local-only, often contains tokens/transcripts +.claude/ +.beads/ +.beads_bak/ +.taskmaster/ +.serena/ +.serena-backup/ +.mcp_data/ +.gitbook-cache.json +.trunk/ + +# Linter / formatter configs not needed at runtime +.eslintrc.cjs +.eslintignore +.prettierrc +.prettierrc.json +.requirements + +# Secrets that must never enter the image +.tlsnotary-key +*.tlsnotary-key + +# Dependencies (rebuilt inside the image) node_modules -# Exclude devnet runtime files -testing/devnet/identities -testing/devnet/.env -testing/devnet/demos_peerlist.json -testing/devnet/postgres-data +# Runtime state — must NOT enter the image +.demos_identity +demos_peerlist.json +demos_peerlist.json.example +# data/ contains bundled bootstrap (genesis.json, evmChains, l2ps) that the +# node needs at first boot. Keep it, but exclude any runtime-only artifacts +# that may be written into it. +data/chain.db +data/chain.db-* +data/twitter/ +data/operations.json +logs/ +logs_* +output/ +postgres_* ipfs ipfs_53550 +neo4j_data docker_data -postgres_* -logs -logs_* +last_crash_memory_usage.txt +.RUN blocked_ips.json +publickey_* -# Exclude unnecessary files -*.log +# Build artifacts (regenerated) +dist/ +*.tsbuildinfo + +# Test/dev-only directories (not needed at runtime) +local_tests/ +tests/ +testing/ +omniprotocol_fixtures_scripts/ +specs/ +fixtures/ +res/ +# NOTE: sdk/ is intentionally NOT excluded — src/ imports from `sdk/localsdk/...` +# (resolved via tsconfig baseUrl). Excluding it breaks the runtime build. + +# Documentation (lives in repo, not in container) +ClaudeDocs/ +GUIDELINES/ +documentation/ +history/ +docs/ +*.md +LICENSE.md + +# Project-management metadata +.planning/ +.serena/ +.mycelium/ + +# IDE/OS .DS_Store +.idea/ +.vscode/ +*.log +*.swp +*.swo + +# Secrets / dotfiles (defensive) +.env +.env.* +*.pem +*.key + +# Container scaffolding (the image doesn't need these) +docker-compose.yml +docker-compose.*.yml +Dockerfile.* +postgres/ +tlsnotary/ +monitoring/ diff --git a/.env.example b/.env.example index 3e8d640b0..354e59b47 100644 --- a/.env.example +++ b/.env.example @@ -1,129 +1,182 @@ -# =========================================== -# Core Node Configuration -# =========================================== -CONSENSUS_TIME=10 -RPC_FEE=5 -# Internal server port (used for signaling/coordination, not the main RPC listener) -SERVER_PORT=53550 -# HTTP/RPC listen port — the node binds to this for RPC requests (defaults to 0 if unset, which fails) +# ============================================================================= +# Demos Network Node — Unified Configuration Template +# ============================================================================= +# Usage: +# 1. cp .env.example .env +# 2. Edit .env to fill in any values you need (see "MUST SET" notes below). +# 3. docker compose up (defaults below are tuned for this flow) +# +# Defaults assume the unified docker-compose deployment (postgres + tlsnotary +# + node + monitoring stack). For the legacy `./run` script on bare metal, +# change PG_HOST=localhost and TLSNOTARY_HOST=localhost (notes inline). +# +# YOU SHOULD REVIEW (defaults work for local dev — change for real deployment): +# - EXPOSED_URL (loopback default cannot be reached by other peers) +# - GRAFANA_ADMIN_PASSWORD (default 'demos' is fine for solo, not for shared) +# - Any external API keys you actually want to use (all optional, all bottom) +# ============================================================================= + + +# === NETWORK & PORTS ======================================================== + +# Main HTTP RPC listen port — the node binds here for RPC requests RPC_PORT=53550 -EXPOSED_URL=http://127.0.0.1:53550 -SIGNALING_SERVER_PORT=3005 -# Set this to true in production -PROD=true +# Public URL advertised to peers and SDK clients. +# !!! For real network participation, set this to your reachable address: +# !!! EXPOSED_URL=http://YOUR_PUBLIC_IP:53550 (or DNS name) +# !!! The default 'localhost' is fine for solo dev but useless for any peer. +EXPOSED_URL=http://localhost:53550 -# MCP Server Configuration -MCP_ENABLED=true -MCP_SERVER_PORT=3001 -# Alternative override +# OmniProtocol TCP server (binary RPC). Auto-derived as RPC_PORT+1 when unset. +OMNI_ENABLED=true +OMNI_PORT=53551 +# Modes: HTTP_ONLY | OMNI_PREFERRED | OMNI_ONLY +OMNI_MODE=OMNI_PREFERRED + +# WebRTC signaling server port (peer discovery / handshake coordination) +RPC_SIGNALING_PORT=3005 + +# MCP (Model Context Protocol) server port for AI agent integrations RPC_MCP_PORT=3001 -# =========================================== -# API Keys -# =========================================== -GITHUB_TOKEN= -ETHERSCAN_API_KEY= -HELIUS_API_KEY= -RAPID_API_KEY= -RAPID_API_HOST= -RUBIC_API_REFERRER_ADDRESS= -RUBIC_API_INTEGRATOR_ADDRESS= - -# =========================================== -# Social Integrations -# =========================================== - -# Twitter -TWITTER_USERNAME= -TWITTER_PASSWORD= -TWITTER_EMAIL= - -# Discord -DISCORD_API_URL= -DISCORD_BOT_TOKEN= - -# GitHub OAuth -GITHUB_CLIENT_ID= -GITHUB_CLIENT_SECRET= - -# Human Passport (Gitcoin Passport) -HUMAN_PASSPORT_API_URL=https://api.passport.xyz -HUMAN_PASSPORT_API_KEY= -HUMAN_PASSPORT_SCORER_ID= - -# =========================================== -# L2PS (Layer 2 Private System) Configuration -# =========================================== - -# Batch Aggregator Settings -L2PS_AGGREGATION_INTERVAL_MS=10000 -L2PS_MIN_BATCH_SIZE=1 -L2PS_MAX_BATCH_SIZE=10 -L2PS_CLEANUP_AGE_MS=300000 - -# ZK Proof Settings -L2PS_ZK_ENABLED=true - -# Hash Service Settings -L2PS_HASH_INTERVAL_MS=5000 - -# =========================================== -# OmniProtocol TCP Server Configuration -# =========================================== -OMNI_ENABLED=false -OMNI_PORT=3001 -OMNI_MODE=OMNI_ONLY -OMNI_FATAL=false - -# OmniProtocol TLS Encryption -OMNI_TLS_ENABLED=false -OMNI_TLS_MODE=self-signed -OMNI_CERT_PATH=./certs/node-cert.pem -OMNI_KEY_PATH=./certs/node-key.pem -OMNI_CA_PATH= -OMNI_TLS_MIN_VERSION=TLSv1.3 - -# OmniProtocol Rate Limiting -OMNI_RATE_LIMIT_ENABLED=true -OMNI_MAX_CONNECTIONS_PER_IP=10 -OMNI_MAX_REQUESTS_PER_SECOND_PER_IP=100 -OMNI_MAX_REQUESTS_PER_SECOND_PER_IDENTITY=200 - -# =========================================== -# TLSNotary HTTPS Attestation Configuration -# =========================================== -TLSNOTARY_ENABLED=false -TLSNOTARY_PORT=7047 -TLSNOTARY_SIGNING_KEY= -TLSNOTARY_PROXY_PORT=55688 -TLSNOTARY_MAX_SENT_DATA=16384 -TLSNOTARY_MAX_RECV_DATA=65536 - -# ZK Identity System Configuration -# Points awarded for each successful ZK attestation (default: 10) -ZK_ATTESTATION_POINTS=10 - -# =========================================== -# Prometheus Metrics -# =========================================== -# Exposes metrics at http://localhost:/metrics for Prometheus scraping -# Note: This is the NODE's metrics endpoint, not the Prometheus server port. -# The monitoring stack's Prometheus server runs on port 9091 (see monitoring/docker-compose.yml) + +# === DATABASE (PostgreSQL) ================================================== + +# In docker compose: 'postgres' (the service hostname). +# On bare metal / legacy ./run script: 'localhost'. +PG_HOST=postgres + +# In docker compose: 5432 (container-internal port). +# On bare metal / legacy ./run script: 5332 (host-mapped port). +PG_PORT=5432 + +PG_USER=demosuser +PG_PASSWORD=demospassword +PG_DATABASE=demos + + +# === IDENTITY & PEERS ======================================================= + +# Path to the node's ed25519 identity keypair. Auto-generated on first boot. +# Under docker compose, the entrypoint symlinks .demos_identity into the +# node_state volume so it persists across container recreation. +IDENTITY_FILE=.demos_identity + +# Path to the cached peer list (JSON). Same persistence treatment as identity. +PEER_LIST_FILE=demos_peerlist.json + + +# === MONITORING ============================================================= + +# Exposes Prometheus metrics at http://:/metrics METRICS_ENABLED=true +# Container-internal port. Pinned to 9090 in compose to match the static +# scrape target in monitoring/prometheus/prometheus.yml. Override only +# when running on bare metal (./run path). METRICS_PORT=9090 METRICS_HOST=0.0.0.0 +# Host-mapped port for the metrics endpoint (compose only). Set this if +# 9090 is already in use on the host — Prometheus inside the compose +# network is unaffected. +METRICS_HOST_PORT=9090 + + +# === TLSNOTARY ============================================================== + +# TLSNotary provides cryptographic attestation of HTTPS responses. +# Under docker compose this MUST track the 'tlsnotary' profile in +# COMPOSE_PROFILES below — set both to 'true'/include, or both to +# 'false'/exclude. Mixing them lets the node try to call a sidecar +# that's not running (it warns and continues, but it's noisy). +TLSNOTARY_ENABLED=true + +# In docker compose: 'tlsnotary' (the service hostname). +# On bare metal / legacy ./run script: 'localhost'. +TLSNOTARY_HOST=tlsnotary +TLSNOTARY_PORT=7047 + +# Mode: 'docker' (delegates to the tlsnotary sidecar) | 'ffi' (in-process FFI binding) +TLSNOTARY_MODE=docker + +# If true, TLSNotary failures crash the node. Keep false for graceful degradation. +TLSNOTARY_FATAL=false + +# Signing key — ONLY needed when TLSNOTARY_MODE=ffi (in-process Rust binding). +# In docker mode (the default), the tlsnotary sidecar container manages its +# own key and exposes the public key via GET /info — leave this empty. +TLSNOTARY_SIGNING_KEY= + + +# === LOGGING & MISC ========================================================= + +# Levels: debug | info | warning | error | critical +# 'info' is recommended. 'debug' produces a wall of per-block consensus JSON +# that drowns useful messages — only enable when actually debugging. +LOG_LEVEL=info + +# Set true in production (enables DTR production relay and prod-only safety checks) +PROD=false + +# Advanced: enable ZK proofs in the L2PS layer (off by default; experimental) +L2PS_ZK_ENABLED=false + + +# === DOCKER COMPOSE ONLY ==================================================== +# These are consumed by docker-compose.yml, NOT the node binary itself. + +# Which compose profiles to bring up. Comma-separated. +# +# Available profiles: +# tlsnotary — TLSNotary sidecar (HTTPS attestation) +# monitoring — Prometheus + Grafana +# full — Node Exporter (host CPU/RAM/disk metrics, pair with monitoring) +# neo4j — Neo4j (only for CGC/KYC features) +# +# Empty = the bare minimum (postgres + node). Common pairings: +# monitoring,tlsnotary (full stack — recommended default) +# monitoring (skip TLSNotary; ALSO set TLSNOTARY_ENABLED=false) +# monitoring,full (add host metrics) +COMPOSE_PROFILES=monitoring,tlsnotary + +# Image coordinates for the node. Defaults to a local build. +# Override to pull a pre-built image: e.g. ghcr.io/kynesyslabs/node and v0.9.8 +IMAGE_NAME=demos-node +IMAGE_TAG=local + +# Grafana dashboard (monitoring profile) +GRAFANA_PORT=3000 +GRAFANA_ADMIN_USER=admin +GRAFANA_ADMIN_PASSWORD=demos + +# Prometheus server external port (host-mapped). Internal container port stays 9090. +PROMETHEUS_PORT=9091 + + +# === OPTIONAL EXTERNAL API KEYS ============================================= +# Uncomment and fill in only the ones you need. Each unlocks a specific feature. + +# Solana RPC endpoint override (default: public mainnet RPC) +#SOLANA_RPC= + +# Etherscan — for EVM chain block explorer queries +#ETHERSCAN_API_KEY= + +# Helius — Solana enhanced RPC (NFTs, DAS API, etc.) +#HELIUS_API_KEY= + +# Nomis — on-chain reputation scoring +#NOMIS_API_KEY= +#NOMIS_CLIENT_ID= + +# RapidAPI — used by various enrichment routes +#RAPID_API_KEY= -# =========================================== -# Access Control -# =========================================== +# GitHub — required for GitHub-based identity attestations +#GITHUB_TOKEN= -# HTTP Server Rate Limiting Whitelist -# Comma-separated list of IPs to bypass rate limiting (also used for other node IP addresses) -WHITELISTED_IPS= -# Comma-separated list of hex public keys to bypass rate limiting -# These keys must provide valid signature in request headers -WHITELISTED_KEYS= +# Discord — required for Discord-based identity attestations +#DISCORD_BOT_TOKEN= -# ed25519 pubkey allowed to access protected endpoints -SUDO_PUBKEY= +# Human Passport (Gitcoin) — sybil resistance scoring +#HUMAN_PASSPORT_API_KEY= diff --git a/.gitignore b/.gitignore index 7f28a0403..da83bacac 100644 --- a/.gitignore +++ b/.gitignore @@ -63,6 +63,7 @@ history/tmp/ # ---- AI Tooling (local-only) ---- CLAUDE.md AGENTS.md +TEAM.md mcp.json .serena/ .serena-backup/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..9c0b76764 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,206 @@ +# syntax=docker/dockerfile:1.7 +# +# Demos Network Node - Production Image +# +# Multistage build: +# 1. builder - installs deps, native modules, applies falcon-sign patch +# 2. runtime - minimal image, non-root user, healthcheck, exec CMD +# +# Run with --no-tui (passed in CMD) since TUI requires a TTY. +# All persistent state lives under /app and is intended to be bind/volume mounted: +# - /app/.demos_identity (file, bind-mount) +# - /app/demos_peerlist.json (file, bind-mount) +# - /app/data (dir, volume) +# - /app/logs (dir, volume) + +# ============================================================================= +# Stage 1: builder +# Installs build toolchain, resolves dependencies, compiles native modules, +# and patches falcon-sign to log uncaught exceptions before rethrowing. +# ============================================================================= +FROM oven/bun:1.3-debian AS builder + +# Build-time deps for native modules (bufferutil, utf-8-validate, etc.). +# python3-setuptools is required for node-gyp on newer Pythons. +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential \ + python3 \ + python3-setuptools \ + ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +# Copy manifests first to maximise layer cache reuse on dep installs. +COPY package.json bun.lock ./ + +# Resolve all dependencies. Split into two RUN steps so `|| true` only +# tolerates `bun pm trust` failures (some packages have no postinstall +# script to trust), not `bun install` failures — a failed install must +# fail the build, otherwise we'd build a silently broken image. +RUN bun install --frozen-lockfile +RUN bun pm trust --all || true + +# Native websocket accelerators - explicit install so they are not skipped +# when the lockfile considers them optional peers. +RUN bun add bufferutil utf-8-validate + +# Bring in the runtime payload. Explicit allow-list (rather than `COPY . .`) +# so host-only files (run wrapper, captraf.sh, jest config, etc.) and any +# new top-level files added later don't silently land in the image. +COPY src/ ./src/ +COPY scripts/ ./scripts/ +COPY data/ ./data/ +COPY sdk/ ./sdk/ +COPY libs/ ./libs/ +COPY tsconfig.json bunfig.toml ormconfig.json ./ + +# Patch falcon-sign so uncaught WASM exceptions are logged before rethrow. +# Mirrors scripts/run::patch_falcon_sign. Idempotent and tolerant of a +# missing target file (some dependency configurations omit it). +RUN set -eu; \ + FALCON_FILE="/app/node_modules/falcon-sign/kernel/n3_v1/wasmFile/falcon512.js"; \ + if [ -f "$FALCON_FILE" ]; then \ + if grep -q "console.error(ex)" "$FALCON_FILE"; then \ + echo "falcon-sign already patched"; \ + else \ + sed -i '/throw ex;/i\ console.error(ex);' "$FALCON_FILE"; \ + echo "falcon-sign patched"; \ + fi; \ + else \ + echo "falcon-sign target missing, skipping patch"; \ + fi + +# Prune node_modules: remove READMEs, docs, tests, and source maps from +# transitive deps. Saves ~200-400 MB on a typical install. ARG-gated so a +# future build can opt out with --build-arg PRUNE_MODULES=false if needed. +# +# Implemented as a portable shell pass instead of pulling in node-prune so +# the build stays hermetic (no external binary download at build time). +ARG PRUNE_MODULES=true +RUN if [ "$PRUNE_MODULES" = "true" ]; then \ + echo "node_modules size before prune: $(du -sh node_modules | cut -f1)"; \ + # Documentation and license files (typical: 60-100 MB) + find node_modules -type f \( \ + -iname 'README*' -o \ + -iname 'CHANGELOG*' -o \ + -iname 'CHANGES*' -o \ + -iname 'HISTORY*' -o \ + -iname 'CONTRIBUTING*' -o \ + -iname 'AUTHORS*' -o \ + -iname 'CONTRIBUTORS*' -o \ + -iname 'LICEN[CS]E*' -o \ + -iname 'NOTICE*' -o \ + -iname 'NOTICES*' -o \ + -iname 'PATENTS*' -o \ + -iname 'governance.md' -o \ + -iname 'security.md' -o \ + -iname 'code_of_conduct*' \ + \) -delete 2>/dev/null || true; \ + # Source maps (typical: 80-150 MB) + find node_modules -type f \( -name '*.map' -o -name '*.ts.map' \) -delete 2>/dev/null || true; \ + # Test/example directories that ship inside packages. + # NB: bare 'test/' and 'tests/' are NOT safe to prune — some packages + # (notably viem via @pancakeswap) use them as production module + # namespaces (e.g. viem/actions/test/dropTransaction.js). Only prune + # patterns that are unambiguously non-runtime. + find node_modules -type d \( \ + -name '__tests__' -o \ + -name '__mocks__' -o \ + -name '.test' -o \ + -name 'example' -o \ + -name 'examples' -o \ + -name 'docs' -o \ + -name 'doc' \ + \) -prune -exec rm -rf {} + 2>/dev/null || true; \ + # Misc dotfiles and CI configs + find node_modules -maxdepth 4 -type f \( \ + -name '.editorconfig' -o \ + -name '.eslintrc*' -o \ + -name '.prettierrc*' -o \ + -name '.travis.yml' -o \ + -name '.appveyor.yml' -o \ + -name '.gitattributes' -o \ + -name '.npmignore' -o \ + -name '.nvmrc' -o \ + -name '.yarnrc' -o \ + -name 'jest.config.*' -o \ + -name 'tsconfig.test.json' -o \ + -name '*.tsbuildinfo' \ + \) -delete 2>/dev/null || true; \ + echo "node_modules size after prune: $(du -sh node_modules | cut -f1)"; \ + fi + +# ============================================================================= +# Stage 2: runtime +# Minimal image with only what the node needs at run time. Runs as non-root. +# ============================================================================= +FROM oven/bun:1.3-debian AS runtime + +# OCI image metadata. +LABEL org.opencontainers.image.source="https://github.com/kynesyslabs/node" \ + org.opencontainers.image.description="Demos Network node (RPC + consensus runtime)" \ + org.opencontainers.image.licenses="CC-BY-NC-SA-4.0" \ + org.opencontainers.image.vendor="Kynesys Labs" + +# Runtime essentials only: curl for HEALTHCHECK, ca-certificates for TLS. +RUN apt-get update && apt-get install -y --no-install-recommends \ + curl \ + ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +# Non-root user. The base image already ships a `bun` user/group at uid/gid +# 1000; rename them to `demos` in-place so the uid stays predictable for +# host bind-mount permissions and /etc/passwd reads meaningfully. +RUN groupmod --new-name demos bun \ + && usermod --login demos --home /app bun + +WORKDIR /app + +# Copy the built tree from the builder stage. Defense-in-depth: the +# application's source and node_modules are owned by root with no +# write permission for the demos runtime user, so a code-execution +# exploit at runtime cannot rewrite the app itself. The demos user +# only gets write access on: +# - /app (entrypoint creates symlinks here) +# - /app/data, logs, state (volume mount points) +# +# COPY --chown bakes ownership into the same layer as the copy itself, +# avoiding a 2 GB chown layer that would otherwise duplicate the tree. +COPY --from=builder --chown=root:demos /app /app +RUN chmod 0755 /app/scripts/docker-entrypoint.sh \ + && mkdir -p /app/data /app/logs /app/state \ + && chown demos:demos /app /app/data /app/logs /app/state \ + && chmod 0755 /app /app/data /app/logs /app/state + +# Sensible image-level defaults. Anything else (DATABASE_URL, EXPOSED_URL, +# IDENTITY_FILE, PEER_LIST_FILE, etc.) must be supplied at runtime. +ENV NODE_ENV=production \ + RPC_PORT=53550 \ + METRICS_HOST=0.0.0.0 + +# Exposed services: +# 53550 - RPC (HTTP/JSON-RPC) +# 53551 - Omni / cross-chain bridge endpoint +# 3005 - WebRTC signaling +# 3001 - MCP server +# 9090 - Prometheus metrics +# Note: 7047 (TLSNotary) is intentionally NOT exposed - it runs in its own container. +EXPOSE 53550 53551 3005 3001 9090 + +# Ephemeral runtime state mount points. The entrypoint symlinks the node's +# legacy repo-root files (.demos_identity, demos_peerlist.json, .tlsnotary-key, +# output/) into /app/state so a single named volume covers all of them. +VOLUME ["/app/data", "/app/logs", "/app/state"] + +USER demos + +# Liveness check against the RPC HTTP root. start-period covers DB connect, +# peer discovery, and chain bootstrap which can take ~30-60s on cold start. +HEALTHCHECK --interval=30s --timeout=5s --start-period=60s --retries=3 \ + CMD curl -fsS "http://localhost:${RPC_PORT:-53550}/" >/dev/null || exit 1 + +# The entrypoint bridges runtime files into /app/state for persistence, +# then execs the node. --no-tui is a CMD argument (no TTY in containers). +ENTRYPOINT ["/app/scripts/docker-entrypoint.sh"] +CMD ["bun", "-r", "tsconfig-paths/register", "src/index.ts", "--", "--no-tui"] diff --git a/INSTALL.md b/INSTALL.md index 9b6e7924a..b16569a10 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -1,124 +1,301 @@ -# 🚀 Demos Node Installation Guide +# Installing the Demos Node -**Welcome to the Demos Network!** This guide will help you set up a Demos node on Linux Debian (Ubuntu)-based systems. It walks through the installation steps—including the installation of dependencies—in more detail and is intended to help less experienced users get through the process. +This guide covers two ways to run a Demos node: -After installing the required Linux operating system on a bare metal or computer running the required hardware specs, you can simply copy and paste each line into your Linux system’s terminal. Note that lines with hashtags are comments; they do not need to be pasted into the terminal. +- **Track 1: Docker Compose (recommended)** — single-command bring-up of the full stack (node + Postgres + TLSNotary + monitoring). This is the path most operators should take. +- **Track 2: Bare metal with `./run`** — host-native install using Bun and a sidecar Postgres container. Useful for development, debugging, and TUI-based operation. -## 📋 System Requirements +Pick one. Both tracks produce a working node; they differ only in where the binary and its dependencies live. -### Operating System - -Install a Debian-based Linux distro like Ubuntu: [https://ubuntu.com/download/desktop](https://ubuntu.com/download/desktop) - -Each computer’s hardware can behave differently during Ubuntu installation. This may require a bit of troubleshooting on your end, but is not covered by this guide, as the main focus of this guide is specific to Demos node installation. +## System Requirements -### Hardware Minimum Requirements +### Hardware Minimum - 4GB RAM - 4 CPU cores (2GHz+) - Modern SSD - 200 Mbps internet connection -- Ubuntu 22.04 LTS or newer (or compatible Linux distribution) -### Hardware Recommended Specs +### Hardware Recommended - 8GB RAM - 6 CPU cores (2GHz+) - Modern SSD - 1 Gbps internet connection -## ⚡ Installation Steps - Short Version +### Operating System -This is the abridged installation guide. If this works for your system and allows the node to operate, then you are done here. If this results in errors, proceed to the full installation guide, which walks you through additional steps. All steps will require the use of the terminal. +Linux is the supported and tested target. Ubuntu 22.04 LTS or newer is recommended. Any modern distro that runs Docker works for Track 1. -### 1. Install Prerequisites +--- + +## Track 1: Docker Compose (recommended) + +### Prerequisites + +That's the whole list: -Open a terminal and enter: +- **Docker** 20.10 or newer +- **Docker Compose** v2 (the `docker compose` plugin, not the legacy `docker-compose` Python script) + +No Bun, no Postgres, no Rust toolchain on the host. Everything else runs inside containers. + +Verify: ```bash -sudo apt update && sudo apt upgrade -y -sudo apt install -y curl git wget build-essential ca-certificates gnupg lsb-release +docker --version +docker compose version ``` -### 2. Install the Following Packages +If you don't have Docker installed, follow the official guide: [https://docs.docker.com/engine/install/](https://docs.docker.com/engine/install/) and add yourself to the `docker` group so you don't need `sudo`: -- **Install Docker and docker-compose for non-root users** - - Docker: [https://docs.docker.com/get-started/get-docker/](https://docs.docker.com/get-started/get-docker/) - - Docker Compose: [https://docs.docker.com/compose/install/](https://docs.docker.com/compose/install/) -- **Install Bun** - - We suggest managing bun with mise ([https://mise.jdx.dev/getting-started.html](https://mise.jdx.dev/getting-started.html) and `mise use -g bun@latest`) for convenience +```bash +sudo usermod -aG docker $USER +newgrp docker +``` -### 3. Clone the Repository +### Quickstart (3 steps) ```bash -cd ~ -git clone https://github.com/kynesyslabs/node.git -# Double check that you are on testnet branch -git branch -# switch to your node directory -cd node +git clone https://github.com/kynesyslabs/node.git && cd node +cp .env.example .env # defaults are fine; edit only if you want to override +docker compose up ``` -### 4. Install Dependencies +The first run pulls images and builds the node container, so expect a few minutes. Subsequent starts are near-instant. + +When the stack is healthy: + +- Node RPC: http://localhost:53550 (try `curl http://localhost:53550/info`) +- Grafana: http://localhost:3000 (default `admin` / `demos`) +- Prometheus: http://localhost:9091 + +### Environment file + +`.env.example` is the canonical template. Copy it to `.env` and edit. The variables you actually care about: + +| Variable | Default | Notes | +|----------|---------|-------| +| `TLSNOTARY_ENABLED` | `true` | Set to `false` to skip TLSNotary entirely. | +| `TLSNOTARY_SIGNING_KEY` | _(empty)_ | **Leave empty in docker mode (the default).** The TLSNotary sidecar manages its own key. Only set this if you've switched `TLSNOTARY_MODE` to `ffi` (in-process Rust binding). | +| `RPC_PORT` | `53550` | Host-mapped port for the node RPC. Change if 53550 is taken. | +| `EXPOSED_URL` | `http://localhost:53550` | Public URL advertised to peers. Set to your public IP/domain when running on a VPS. | +| `PG_USER` / `PG_PASSWORD` / `PG_DATABASE` | `demosuser` / `demospassword` / `demos` | Postgres credentials. Change `PG_PASSWORD` if you're paranoid; it never leaves the compose network. | +| `METRICS_ENABLED` / `METRICS_PORT` | `true` / `9090` | Prometheus scrape endpoint on the node. | +| `GRAFANA_ADMIN_PASSWORD` | `demos` | Grafana admin password. Change this. | +| `COMPOSE_PROFILES` | `monitoring,tlsnotary` | Which compose profiles are active. See below. | + +Leave `PG_HOST=postgres` and `TLSNOTARY_HOST=tlsnotary` exactly as shipped — those are the in-network service names. They are only `localhost` for the bare-metal path (Track 2). + +### Joining the network — required steps before real-world use + +A fresh node will run as an isolated single-validator chain by default. To participate in the actual Demos testnet/mainnet, you must do **two things**: + +**1. Set `EXPOSED_URL` to your reachable address.** + +The default `http://localhost:53550` is fine for local dev but useless for any peer trying to connect to you. Before going live: + +```env +# In .env +EXPOSED_URL=http://YOUR_PUBLIC_IP:53550 +# or for a DNS name: +EXPOSED_URL=http://node.example.com:53550 +``` + +If running behind NAT, port-forward `53550` (RPC) and `53551` (OmniProtocol TCP) to this host. The firewall section below has the exact rules. + +**2. Provide a bootstrap peerlist.** + +The node persists its peerlist in the `demos_node_state` volume at `demos_peerlist.json`. On a brand-new node this file is empty (only the node itself), so it cannot discover other peers. To bootstrap, write one or more known peer URLs into that volume before first start: ```bash -# Install all dependencies (requires Rust/Cargo for wstcp) -./scripts/install-deps.sh +# Create a bootstrap peerlist on your host +cat > /tmp/demos_peerlist.json < **Note:** The install script requires [Rust](https://rustup.rs/) to be installed. It will install the `wstcp` tool needed for TLSNotary WebSocket proxying. +Replace `bootstrap1.demos.network` etc. with current bootstrap nodes from the team. The node will merge any peers it learns from these into the file as it runs. + +### Network Exposure + +When running on a VPS or any machine you want peers to connect to, you must allow inbound connections on these ports: + +| Port | Protocol | Direction | Purpose | +|------|----------|-----------|---------| +| 53550 | TCP | inbound | Node RPC (HTTP) | +| 53551 | TCP | inbound | OmniProtocol (peer-to-peer binary RPC) | +| 7047 | TCP | inbound | TLSNotary attestation server (only if you want others to attest against your notary) | -### 5. Run Node and Generate Keys +These ports should NOT be exposed publicly: + +| Port | Why | +|------|-----| +| 5432 | Postgres — internal compose network only | +| 9090 | Node metrics — keep behind your firewall or VPN | +| 9091 | Prometheus — keep behind your firewall or VPN | +| 3000 | Grafana — only public if you want a public dashboard | + +`ufw` example for an Ubuntu VPS: ```bash -# Start both database and node for the first time, if successful, you will see your node’s private -# and public keys along with the database loaded and the node will join the consensus process -./run -# Stop the node for now so that you can edit the configuration files -Ctrl+C +sudo ufw allow 53550/tcp comment "Demos RPC" +sudo ufw allow 53551/tcp comment "Demos OmniProtocol" +sudo ufw allow 7047/tcp comment "Demos TLSNotary" +sudo ufw enable ``` -### 6. Configure the Node +If you're behind NAT (home server, dev box, etc.), forward 53550 and 53551 from your router to this host. TLSNotary forwarding is optional — only needed if you want peers to use *your* notary container. + +Don't forget to set `EXPOSED_URL` in `.env` to the address peers will reach you at (public IP, DNS name, or NAT-mapped hostname). + +### Profiles + +The compose file splits optional services into profiles so you only run what you need. + +| Profile | What it adds | +|---------|--------------| +| _(none)_ | postgres + node — the bare minimum stack | +| `tlsnotary` | TLSNotary sidecar (HTTPS attestation). Pair with `TLSNOTARY_ENABLED=true` | +| `monitoring` | prometheus + grafana | +| `full` | node-exporter (host CPU/RAM/disk metrics) — pair with `monitoring` | +| `neo4j` | neo4j (only needed for CGC/KYC features) | + +`.env.example` ships with `COMPOSE_PROFILES=monitoring,tlsnotary` so a plain `docker compose up` brings up the recommended full stack. -Copy the example configuration files into working copies: +Concrete commands: ```bash -cp env.example .env -cp demos_peerlist.json.example demos_peerlist.json +# Default — postgres + node + tlsnotary + monitoring +# (uses COMPOSE_PROFILES from .env.example) +docker compose up + +# Minimal — only node + postgres, no sidecars +# Also set TLSNOTARY_ENABLED=false in .env to silence the node-side warnings +COMPOSE_PROFILES= docker compose up + +# Add host-level metrics (node-exporter) +COMPOSE_PROFILES=monitoring,tlsnotary,full docker compose up + +# Add Neo4j (only if you actually need CGC/KYC) +COMPOSE_PROFILES=monitoring,tlsnotary,neo4j docker compose up ``` -**Edit .env file:** The most important setting is `EXPOSED_URL`. Set it based on your setup: +### Where data lives -- Local testing: `http://localhost:53550` -- Remote machine: `http://YOUR_PUBLIC_IP:53550` -- Behind proxy: `https://demos.example.com` +State is stored in named Docker volumes, not bind mounts. They survive `docker compose down` but **not** `docker compose down -v`. -**Edit demos_peerlist.json:** Add known peers in the format: +| Volume | Holds | +|--------|-------| +| `demos_pgdata` | PostgreSQL data directory (chain state, indexes) | +| `demos_node_state` | Your `.demos_identity` (private key), `demos_peerlist.json`, `.tlsnotary-key`, `output/` | +| `demos_node_data` | Bundled bootstrap data (genesis.json, evmChains, l2ps) plus chain runtime artifacts | +| `demos_node_logs` | Node logs | +| `demos_prometheus_data` | Prometheus TSDB | +| `demos_grafana_data` | Grafana dashboards, users, settings | +| `demos_neo4j_data` / `demos_neo4j_logs` | Neo4j (only if `neo4j` profile is on) | -```json -{ - "publickey": "connectionstring" # Example: “publickey”:”http://localhost:53550” -} +The `demos_` prefix is set explicitly in `docker-compose.yml` so the names are stable regardless of the project directory name. Inspect a volume: + +```bash +docker volume inspect demos_node_state ``` -For local testing, you can use your own public key (found in the “publickey” file in your node directory after the first run). +### Backup the node identity -### 7. Run Node +Your `.demos_identity` is the private key — losing it means losing your node's network identity. Back up the entire `demos_node_state` volume: ```bash -# Start the node again if you would like to keep it running -./run +docker run --rm -v demos_node_state:/src -v "$PWD":/dst alpine \ + tar czf /dst/node-state-backup.tar.gz -C /src . ``` +That writes `node-state-backup.tar.gz` into your current directory. To restore, do the reverse: stop the stack, recreate an empty `demos_node_state` volume, and `tar xzf` the backup back into it. + +### View logs + +```bash +docker compose logs -f node # follow node logs +docker compose logs -f postgres # follow postgres logs +docker compose logs -f tlsnotary # follow tlsnotary logs +docker compose logs -f # follow everything +``` + +### Connect to the RPC + +```bash +curl http://localhost:53550/ # liveness check (root) +curl http://localhost:53550/info # node info (version, identity, etc.) +``` + +### Update the node + +```bash +git pull +docker compose up -d --build +``` + +The `--build` rebuilds the node image against the new source. Volumes (and therefore your identity, chain data, peerlist cache) are preserved. + +### Stop the node + +```bash +docker compose down # stop containers, KEEP volumes +docker compose down -v # stop AND DELETE all volumes (nuclear) +``` + +### Wipe everything + +```bash +docker compose down -v +``` + +This deletes your identity, chain data, Prometheus history, Grafana dashboards — everything. Only do this if you actually want to start from scratch. + +### Troubleshooting + +**TLSNotary feature not initializing** +> In docker mode (the default) no signing key is required — the sidecar manages its own. If you see initialization warnings, check that the `tlsnotary` container is reachable: `curl http://localhost:7047/info` should return JSON. If you switched to `TLSNOTARY_MODE=ffi`, you need a 64-char hex `TLSNOTARY_SIGNING_KEY` in `.env` (or leave it empty and the node auto-generates one in `.tlsnotary-key`). + +**EXPOSED_URL warning at startup** +> `⚠️ EXPOSED_URL is set to a loopback/unroutable address` means peers can't reach you. Fine for local dev. For real deployment set `EXPOSED_URL=http://YOUR_PUBLIC_IP:53550` in `.env`. + +**Port 53550 already in use** +> Set `RPC_PORT` (and optionally `EXPOSED_URL`) in `.env` to a free port, then `docker compose up -d` again. + +**Postgres connection refused / node fails to connect to DB** +> Postgres takes a few seconds to become healthy on first boot. The node has a `depends_on: condition: service_healthy` so this should be automatic — but if it persists, check `docker compose logs postgres` for crash loops or auth failures. + +**Out of disk** +> Volumes grow over time. Inspect with `docker system df -v`. To reclaim space without losing identity, only prune images: `docker image prune -af`. To reclaim everything: `docker compose down -v` (destructive). + +**`docker compose: command not found`** +> You have the legacy Python `docker-compose` instead of the v2 plugin. Install the plugin per [https://docs.docker.com/compose/install/](https://docs.docker.com/compose/install/). + +**Grafana password change doesn't take effect** +> Grafana persists the password in `demos_grafana_data` after first boot. To change it, either edit it in the Grafana UI, or `docker compose down && docker volume rm demos_grafana_data && docker compose up -d` (resets dashboards too). + --- -## 🔧 Full Installation Guide +## Track 2: Bare metal with `./run` -Start by installing a Debian Linux distro like Ubuntu: [https://ubuntu.com/download/desktop](https://ubuntu.com/download/desktop) +This is the original install path. The node binary runs natively on the host via Bun; only Postgres runs in a sidecar container managed by the `./run` script. Pick this track if you want host-native execution, are doing kernel-level debugging, or just prefer it. -### 1. Install Prerequisites +> Note: `./run` at the repo root is a thin wrapper that forwards to `scripts/run` (the actual implementation). Always invoke `./run` from the repo root — never call `scripts/run` directly. The wrapper exists so the legacy command path keeps working. -Open a terminal and enter: +### 1. Install Prerequisites ```bash sudo apt update && sudo apt upgrade -y @@ -127,6 +304,8 @@ sudo apt install -y curl git wget build-essential ca-certificates gnupg lsb-rele ### 2. Install Docker +The `./run` script still uses Docker for the Postgres sidecar. + ```bash # Remove old Docker versions sudo apt remove docker docker-engine docker.io containerd runc @@ -184,9 +363,7 @@ curl -fsSL https://bun.sh/install | bash bun -v ``` -### Demos Node Installation - -#### 1. Clone Repository +### 4. Clone Repository ```bash cd ~ @@ -197,7 +374,7 @@ cd node git branch ``` -#### 2. Install Dependencies +### 5. Install Dependencies ```bash # Install all dependencies (requires Rust/Cargo for wstcp) @@ -211,28 +388,24 @@ git branch > source ~/.cargo/env > ``` -## 🎯 Starting and Configuring the Node - -### 1. Start the Node - -You can start the node using the `run` script shown below. +### 6. First Run — Generate Keys ```bash ./run ``` -Running the node the first time will generate a private key for your node and store it in the `.demos_identity` file by default. The public key for your node is printed on the terminal when the node runs and is also saved in a `publickey_*` file in the same directory. +Running the node the first time will generate a private key for your node and store it in the `.demos_identity` file by default. The public key is printed on the terminal and saved in a `publickey_*` file in the same directory. + +Press `Ctrl+C` (or `Q` in the TUI) to stop the node so you can edit the configuration files. ### Run script usage -``` -🚀 Demos Network Node Runner +```text +Demos Network Node Runner USAGE: ./run [OPTIONS] -Welcome to Demos Network! This script helps you run your blockchain node easily. - OPTIONS: -p Node port (default: 53550) -d PostgreSQL port (default: 5332) @@ -263,168 +436,129 @@ EXAMPLES: # -d: database port (default 5332) ``` -### 2. Check Node Status +### 7. Configure the Node -In a new terminal window: +Copy the templates and edit them: ```bash -# Check if node is running -curl http://localhost:53550 - -# Check ports -sudo lsof -i :53550 # Node port -sudo lsof -i :5332 # Database port - -# Check Docker containers -docker ps - -# Stop the node for now so that you can edit the configuration files -Ctrl+C -``` - -### 3. Further configuration - -The `.env` and `demos_peerlist.json` files are used to configure the demos node. Copy the templates using the command below: - -```bash -cp env.example .env +cp .env.example .env cp demos_peerlist.json.example demos_peerlist.json ``` -### 4. Edit Configuration Files - -```bash -nano .env -``` - -Set the following variable: +For the bare-metal path, override these in `.env`: -- **EXPOSED_URL:** Your node's public URL - - Local testing: `http://localhost:53550` - - Remote server: `http://YOUR_PUBLIC_IP:53550` - - Behind proxy: `https://your-domain.com` +- `PG_HOST=localhost` +- `PG_PORT=5332` (the host-mapped port the `./run` Postgres sidecar uses) +- `TLSNOTARY_HOST=localhost` -### 5. Node Identity - Public and Private Keys +Set `EXPOSED_URL` based on your setup: -After running the node for the first time your keypair will be generated. You should find the `publickey_*` and `.demos_identity` files inside the node directory. +- Local testing: `http://localhost:53550` +- Remote machine: `http://YOUR_PUBLIC_IP:53550` +- Behind proxy: `https://demos.example.com` -The public key file contains your public key, this can be shared and utilized in your node. The `.demos_identity` is your private key, **KEEP THIS PRIVATE**. Back up both public and private keys. +If you're using FFI mode (`TLSNOTARY_MODE=ffi`), set `TLSNOTARY_SIGNING_KEY` in `.env` to a 64-char hex secp256k1 key — or leave it empty and the node auto-generates one into `.tlsnotary-key` on first boot. To skip TLSNotary entirely, set `TLSNOTARY_ENABLED=false`. -### 6. Joining a network +### 8. Joining a Network -To join a network, you can edit the `demos_peerlist.json` file to add known peers in the format: +Edit `demos_peerlist.json` to add known peers: ```jsonc { - "publickey": "connectionstring", // Example: "0xd0b2be2cb6d...": "http://otherpeer.localhost" + "publickey": "connectionstring" // Example: "0xd0b2be2cb6d...": "http://otherpeer.localhost" } ``` > [!IMPORTANT] > When joining a network, please make sure your node's exposed URL is accessible by the other nodes. If they can't access it, your node won't be able to participate in the consensus. -### 7. Start the Node Again +### 9. Start the Node ```bash -# Restart the node again with your new settings if you’d like to keep it running ./run ``` -## ✅ Verification - -### Check Node Status +### Verify In a new terminal: ```bash -# Check if node is running +# Liveness curl http://localhost:53550 +curl http://localhost:53550/info -# Check ports -sudo lsof -i :53550 # Node port -sudo lsof -i :5332 # Database port - -# Use different ports if defaults are busy -./run -p 53551 -d 5333 -# -p: node port (default 53550) -# -d: database port (default 5332) +# Ports +sudo lsof -i :53550 # Node port +sudo lsof -i :5332 # Database port -# Check Docker containers +# Containers (Postgres sidecar) docker ps ``` -### View Logs - -The node will output logs showing: - -- Database connection status -- RPC server initialization -- Your node's public key -- Peer connection attempts - ### Stopping the Node ```bash -# Press Ctrl+C in the terminal running the node +# Press Ctrl+C in the terminal running the node (or `Q` in the TUI) -# Stop database +# Stop the Postgres sidecar cd postgres_5332 ./stop.sh ``` -## 🛠️ Troubleshooting +### Bare-metal Troubleshooting -### Port Already in Use +**Port already in use** ```bash -# Check what's using the port sudo lsof -i :5332 - -# Use different port ./run -d 5333 ``` -### Docker Permission Issues +**Docker permission issues** ```bash sudo usermod -aG docker $USER newgrp docker ``` -### Database Connection Timeout +**Database connection timeout** ```bash # Restart Docker sudo systemctl restart docker -# Clean and restart +# Clean and restart the postgres sidecar cd postgres_5332 ./clean.sh ./start.sh ``` -### Missing Dependencies +**Missing dependencies** ```bash rm -rf node_modules bun.lockb bun install ``` -## 🔒 Security Notes +--- + +## Security Notes 1. **Backup your identity files:** - - `.demos_identity` (private key - KEEP SECRET) - - `public.key` (public identifier) + - `.demos_identity` (private key — KEEP SECRET) + - `publickey_*` (public identifier) + + Track 1 users: see the volume backup snippet above. Track 2 users: copy the files out of the node directory. -2. **Set proper permissions:** +2. **Set proper permissions on bare metal:** ```bash chmod 600 .demos_identity ``` -3. **Never share your private key** +3. **Never share your private key.** -## 🌐 Network Information +## Network Information > **Note:** These are the default ports. If you have modified any port settings in your `.env` file or run script flags, make sure to open those custom ports instead. @@ -446,14 +580,13 @@ bun install | 3000 | Grafana | Dashboard UI (monitoring stack) | | 5332 | PostgreSQL | Database (local only, do not expose) | -- Logs directory: `logs_53550_demos_identity/` - Configuration: `.env` and `demos_peerlist.json` -## ➡️ Next Steps +## Next Steps Once your node is running: -1. Note your public key from the console output -2. Share your connection string with other node operators to form a network -3. Monitor the logs for successful peer connections -4. Check the Demos Network documentation for updates +1. Note your public key from the console output (or from the `publickey_*` file). +2. Share your connection string with other node operators to form a network. +3. Monitor the logs (`docker compose logs -f node` for Track 1, terminal output or `logs/` for Track 2) for successful peer connections. +4. Check the Demos Network documentation for updates. diff --git a/README.md b/README.md index 8fa25daba..29383dc31 100644 --- a/README.md +++ b/README.md @@ -31,23 +31,64 @@ Demos is defined by the Yellowpaper publicly available in [its own repository](h ## Installation -For detailed installation instructions, please refer to [INSTALL.md](INSTALL.md). The installation guide covers: +Docker Compose is now the recommended way to run a Demos node. For full installation instructions — both the Docker path and the bare-metal `./run` path — see [INSTALL.md](INSTALL.md). The guide covers: -- System prerequisites and dependencies -- Docker and container setup +- Docker Compose quickstart (recommended) +- Bare-metal install with Bun + Postgres (alternative) - Node configuration and key generation - Network peer configuration - Troubleshooting common issues -## Quick Start +## Quick Start (Docker) -1. Install prerequisites (Docker, Bun runtime) -2. Clone this repository -3. Install dependencies with `bun install` -4. Configure your node settings -5. Run `./run` to start the node +```bash +git clone https://github.com/kynesyslabs/node.git && cd node +cp .env.example .env # defaults are fine; edit only if you want to override +docker compose up +``` + +Once the stack is healthy: RPC at http://localhost:53550 (try `curl http://localhost:53550/info`) and Grafana at http://localhost:3000 (default `admin` / `demos`). + +See [INSTALL.md](INSTALL.md) for profiles, env vars, volumes, upgrades, and troubleshooting. + +## Publishing the Image + +The compose file uses `${IMAGE_NAME}:${IMAGE_TAG}` (default `demos-node:local`) for the node service, so the same compose can either build locally or pull from a registry by switching `.env`. + +**Build and tag for a registry:** + +```bash +# Pick your registry coordinates +export IMAGE_NAME=ghcr.io/kynesyslabs/node # or docker.io//demos-node, etc. +export IMAGE_TAG=v0.9.8 # or git sha, or 'latest' + +docker build -t "$IMAGE_NAME:$IMAGE_TAG" . +docker push "$IMAGE_NAME:$IMAGE_TAG" +``` + +**Multi-arch (recommended for public registries):** + +```bash +docker buildx build \ + --platform linux/amd64,linux/arm64 \ + -t "$IMAGE_NAME:$IMAGE_TAG" \ + --push . +``` + +**Pull on a target host** — write `IMAGE_NAME` and `IMAGE_TAG` into that host's `.env`, then: + +```bash +docker compose pull node +docker compose up -d +``` + +For private registries, run `docker login ` first. + +## Advanced / Bare-Metal Run -For complete step-by-step instructions, see [INSTALL.md](INSTALL.md). +If you'd rather run the node directly on the host (no Docker for the node itself), the legacy `./run` shell script is still supported. It installs Bun, runs Postgres in a sidecar container, and starts the node natively — useful for development, debugging, and TUI-based operation. + +See [INSTALL.md → Track 2: Bare metal with `./run`](INSTALL.md) for the full walkthrough. ## Terminal User Interface (TUI) @@ -81,32 +122,11 @@ For debugging and development, you can disable the TUI and use traditional scrol This provides linear console output that can be easily piped, searched with grep, or redirected to files. -## Monitoring with Prometheus & Grafana - -The node includes a full monitoring stack with Prometheus metrics and pre-built Grafana dashboards. +## Monitoring -### Enabling Metrics +Prometheus + Grafana are part of the unified compose (default profile). Once `docker compose up` is healthy, Grafana is at http://localhost:3000 (`admin` / `demos`) with pre-provisioned dashboards. Configuration knobs (`GRAFANA_ADMIN_PASSWORD`, `PROMETHEUS_RETENTION`, etc.) live in `.env` — see [INSTALL.md → Track 1](INSTALL.md) for the full list. -Metrics are enabled by default. To configure, add to your `.env` file: - -```env -METRICS_ENABLED=true -METRICS_PORT=9090 -``` - -The node will expose metrics at `http://localhost:9090/metrics`. - -### Starting the Monitoring Stack - -```bash -cd monitoring -docker compose up -d -``` - -**Access Grafana**: http://localhost:3000 -**Default credentials**: admin / demos - -### Available Metrics +Available node-side metrics (scraped at `node:9090/metrics`): | Metric | Description | | ----------------------------------- | ----------------------- | @@ -117,25 +137,7 @@ docker compose up -d | `demos_system_memory_usage_percent` | Memory utilization | | `demos_service_docker_container_up` | Container health status | -### Configuration - -The node and monitoring stack are configurable via environment variables: - -**Node metrics (in `.env`):** -| Variable | Default | Description | -|----------|---------|-------------| -| `METRICS_ENABLED` | `true` | Enable/disable metrics endpoint | -| `METRICS_PORT` | `9090` | Node metrics endpoint port | - -**Monitoring stack (in `monitoring/.env`):** -| Variable | Default | Description | -|----------|---------|-------------| -| `PROMETHEUS_PORT` | `9091` | Prometheus server port | -| `GRAFANA_PORT` | `3000` | Grafana dashboard port | -| `GRAFANA_ADMIN_PASSWORD` | `demos` | Grafana admin password | -| `PROMETHEUS_RETENTION` | `15d` | Data retention period | - -For detailed monitoring documentation, see [monitoring/README.md](monitoring/README.md). +For dashboard internals and customization, see [monitoring/README.md](monitoring/README.md). ## Technology Stack @@ -155,20 +157,20 @@ After installation, configure your node by editing: ## Network Ports -The following ports must be open for the node to function properly. +For the docker-compose path see [INSTALL.md → Network Exposure](INSTALL.md#network-exposure) — that section has the canonical inbound/outbound rules and a `ufw` example for VPS deployments. -> **Note:** These are the default ports. If you have modified any port settings in your `.env` file or run script flags, make sure to open those custom ports instead. +For the bare-metal `./run` path: -### Required Ports +### Required Ports (bare metal) | Port | Protocol | Description | | ----------- | -------- | ------------------------------ | | 53550 | TCP | Node RPC API | -| 53551 | TCP/UDP | OmniProtocol P2P communication | -| 7047 | TCP | TLSNotary server | -| 55000-60000 | TCP/UDP | WebSocket proxy for TLSNotary | +| 53551 | TCP | OmniProtocol P2P communication | +| 7047 | TCP | TLSNotary server (FFI/docker) | +| 55000-60000 | TCP | WebSocket proxy for TLSNotary FFI mode | -### Optional Ports +### Optional Ports (bare metal) | Port | Protocol | Description | | ---- | -------- | ------------------------------------------------- | @@ -182,9 +184,9 @@ The following ports must be open for the node to function properly. ```bash # Required sudo ufw allow 53550/tcp # Node RPC -sudo ufw allow 53551 # OmniProtocol (TCP+UDP) +sudo ufw allow 53551/tcp # OmniProtocol sudo ufw allow 7047/tcp # TLSNotary -sudo ufw allow 55000:60000 # TLSNotary WS proxy (TCP+UDP) +sudo ufw allow 55000:60000/tcp # TLSNotary WS proxy (FFI mode) ``` ## Security diff --git a/docker-compose.yml b/docker-compose.yml index b7a3b5295..b35565906 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,19 +1,309 @@ +# ============================================================================= +# Demos Network Node — Unified Docker Compose +# ============================================================================= +# One-command bring-up: +# cp .env.example .env +# docker compose up +# +# Profiles (every optional service is gated; .env.example sets COMPOSE_PROFILES +# = monitoring,tlsnotary by default so a plain `docker compose up` brings up +# the recommended full stack): +# +# No profile postgres + node -> minimum stack +# tlsnotary + tlsnotary sidecar -> notarization +# (pair with TLSNOTARY_ENABLED in .env) +# monitoring + prometheus + grafana -> dashboards +# full + node-exporter -> host metrics +# neo4j + neo4j -> CGC/KYC only +# +# Persistent state lives in named volumes (prefixed `demos_`): +# demos_pgdata, demos_node_data, demos_node_logs, demos_node_state, +# demos_prometheus_data, demos_grafana_data, demos_neo4j_data, +# demos_neo4j_logs +# +# Wipe everything (DANGER): docker compose down -v +# ============================================================================= services: + # --------------------------------------------------------------------------- + # PostgreSQL — primary datastore for the node (TypeORM with synchronize=true) + # Internal-only by default. Uncomment the ports stanza below to expose to host. + # --------------------------------------------------------------------------- + postgres: + image: postgres:16-alpine + container_name: demos-postgres + restart: unless-stopped + environment: + POSTGRES_USER: ${PG_USER:-demosuser} + POSTGRES_PASSWORD: ${PG_PASSWORD:-demospassword} + POSTGRES_DB: ${PG_DATABASE:-demos} + volumes: + - pgdata:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${PG_USER:-demosuser} -d ${PG_DATABASE:-demos}"] + interval: 5s + timeout: 5s + retries: 10 + networks: + - demos-network + # ports: + # - "${PG_HOST_PORT:-5332}:5432" # uncomment to access DB from host + + # --------------------------------------------------------------------------- + # TLSNotary — provides cryptographic attestation of HTTPS responses. + # Healthcheck uses localhost INSIDE the container (correct — it's its own loopback). + # --------------------------------------------------------------------------- + tlsnotary: + image: ghcr.io/tlsnotary/tlsn/notary-server:v0.1.0-alpha.12 + container_name: demos-tlsnotary + restart: unless-stopped + platform: linux/amd64 + # Profile-gated so COMPOSE_PROFILES=...,tlsnotary actually controls the + # sidecar. Drop 'tlsnotary' from COMPOSE_PROFILES to skip it; pair that + # with TLSNOTARY_ENABLED=false in .env so the node doesn't try to call it. + profiles: [tlsnotary] + environment: + NS_NOTARIZATION__MAX_SENT_DATA: ${TLSNOTARY_MAX_SENT_DATA:-32768} + ports: + - "${TLSNOTARY_PORT:-7047}:7047" + # The upstream image has no curl/wget so its packaged HTTP healthcheck + # always fails. Use bash's /dev/tcp pseudo-device to verify the notary + # is listening — bash IS in the image, the default sh (dash) is not enough. + healthcheck: + test: ["CMD", "bash", "-c", "exec 3<>/dev/tcp/127.0.0.1/7047"] + interval: 10s + timeout: 5s + retries: 3 + start_period: 10s + networks: + - demos-network + + # --------------------------------------------------------------------------- + # Demos node itself. + # Reads env from .env. PG_HOST=postgres and TLSNOTARY_HOST=tlsnotary are the + # in-network service names — DO NOT override to localhost when using compose. + # --------------------------------------------------------------------------- + node: + # IMAGE_NAME / IMAGE_TAG let you pull a pre-built image from a registry + # instead of building locally. See README → "Publishing the image" for + # the push/pull workflow. Defaults to a local build tagged demos-node:local. + image: ${IMAGE_NAME:-demos-node}:${IMAGE_TAG:-local} + build: + context: . + dockerfile: Dockerfile + container_name: demos-node + restart: unless-stopped + depends_on: + postgres: + condition: service_healthy + # tlsnotary is profile-gated; if its profile is off, Compose drops + # this dependency silently and the node starts without it. Make sure + # to also set TLSNOTARY_ENABLED=false in .env in that case. + tlsnotary: + condition: service_started + required: false + environment: + # Network & ports + RPC_PORT: ${RPC_PORT:-53550} + EXPOSED_URL: ${EXPOSED_URL:-http://localhost:53550} + OMNI_ENABLED: ${OMNI_ENABLED:-true} + OMNI_PORT: ${OMNI_PORT:-53551} + OMNI_MODE: ${OMNI_MODE:-OMNI_PREFERRED} + RPC_SIGNALING_PORT: ${RPC_SIGNALING_PORT:-3005} + RPC_MCP_PORT: ${RPC_MCP_PORT:-3001} + # Database — service name from this compose + PG_HOST: postgres + PG_PORT: 5432 + PG_USER: ${PG_USER:-demosuser} + PG_PASSWORD: ${PG_PASSWORD:-demospassword} + PG_DATABASE: ${PG_DATABASE:-demos} + # Identity & peers — entrypoint symlinks these into /app/state on boot + IDENTITY_FILE: ${IDENTITY_FILE:-.demos_identity} + PEER_LIST_FILE: ${PEER_LIST_FILE:-demos_peerlist.json} + # Monitoring + METRICS_ENABLED: ${METRICS_ENABLED:-true} + # METRICS_PORT is pinned to 9090 inside the container so the + # prometheus.yml scrape target (node:9090) stays valid. Use + # METRICS_HOST_PORT in the ports stanza to remap on the host. + METRICS_PORT: 9090 + METRICS_HOST: ${METRICS_HOST:-0.0.0.0} + # TLSNotary — service name from this compose + TLSNOTARY_ENABLED: ${TLSNOTARY_ENABLED:-true} + TLSNOTARY_HOST: tlsnotary + TLSNOTARY_PORT: 7047 + TLSNOTARY_MODE: ${TLSNOTARY_MODE:-docker} + TLSNOTARY_FATAL: ${TLSNOTARY_FATAL:-false} + TLSNOTARY_SIGNING_KEY: ${TLSNOTARY_SIGNING_KEY:-} + # Logging & misc + LOG_LEVEL: ${LOG_LEVEL:-info} + PROD: ${PROD:-false} + L2PS_ZK_ENABLED: ${L2PS_ZK_ENABLED:-false} + # Optional external API keys (passed through if set in .env) + SOLANA_RPC: ${SOLANA_RPC:-} + ETHERSCAN_API_KEY: ${ETHERSCAN_API_KEY:-} + HELIUS_API_KEY: ${HELIUS_API_KEY:-} + NOMIS_API_KEY: ${NOMIS_API_KEY:-} + NOMIS_CLIENT_ID: ${NOMIS_CLIENT_ID:-} + RAPID_API_KEY: ${RAPID_API_KEY:-} + GITHUB_TOKEN: ${GITHUB_TOKEN:-} + DISCORD_BOT_TOKEN: ${DISCORD_BOT_TOKEN:-} + HUMAN_PASSPORT_API_KEY: ${HUMAN_PASSPORT_API_KEY:-} + volumes: + - node_data:/app/data # bundled bootstrap (genesis.json, evmChains, l2ps) + chain.db + - node_logs:/app/logs # all node logs + - node_state:/app/state # identity, peerlist, tlsnotary key, output/ + ports: + # Both sides of each mapping must use the same variable, otherwise + # overriding RPC_PORT (etc.) routes the host port to a container + # port where nothing is listening. + - "${RPC_PORT:-53550}:${RPC_PORT:-53550}" + - "${OMNI_PORT:-53551}:${OMNI_PORT:-53551}" + - "${RPC_SIGNALING_PORT:-3005}:${RPC_SIGNALING_PORT:-3005}" + - "${RPC_MCP_PORT:-3001}:${RPC_MCP_PORT:-3001}" + # METRICS_PORT — keep container port pinned to 9090 so the + # prometheus.yml scrape target (node:9090) stays valid. The + # host-side port is independently configurable via METRICS_HOST_PORT. + - "${METRICS_HOST_PORT:-9090}:9090" + networks: + - demos-network + + # --------------------------------------------------------------------------- + # Prometheus — metrics scraping. Reads ./monitoring/prometheus/prometheus.yml. + # --------------------------------------------------------------------------- + prometheus: + image: prom/prometheus:v2.48.0 + container_name: demos-prometheus + restart: unless-stopped + profiles: [monitoring] + command: + - --config.file=/etc/prometheus/prometheus.yml + - --storage.tsdb.path=/prometheus + - --storage.tsdb.retention.time=${PROMETHEUS_RETENTION:-15d} + - --web.console.libraries=/etc/prometheus/console_libraries + - --web.console.templates=/etc/prometheus/consoles + - --web.enable-lifecycle + volumes: + - ./monitoring/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro + - prometheus_data:/prometheus + ports: + - "${PROMETHEUS_PORT:-9091}:9090" + networks: + - demos-network + + # --------------------------------------------------------------------------- + # Grafana — dashboards. Provisioned datasource points at prometheus:9090. + # --------------------------------------------------------------------------- + grafana: + image: grafana/grafana:10.2.2 + container_name: demos-grafana + restart: unless-stopped + profiles: [monitoring] + depends_on: + - prometheus + environment: + GF_SECURITY_ADMIN_USER: ${GRAFANA_ADMIN_USER:-admin} + GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_ADMIN_PASSWORD:-demos} + GF_USERS_ALLOW_SIGN_UP: "false" + GF_SERVER_ROOT_URL: ${GRAFANA_ROOT_URL:-http://localhost:3000} + GF_INSTALL_PLUGINS: grafana-clock-panel + GF_ANALYTICS_REPORTING_ENABLED: "false" + GF_ANALYTICS_CHECK_FOR_UPDATES: "false" + GF_ANALYTICS_CHECK_FOR_PLUGIN_UPDATES: "false" + GF_USERS_DEFAULT_THEME: dark + GF_AUTH_ANONYMOUS_ENABLED: "false" + GF_BRANDING_APP_TITLE: Demos Network + GF_BRANDING_LOGIN_TITLE: Demos Network + GF_BRANDING_LOGIN_SUBTITLE: Node Monitoring + GF_BRANDING_LOGIN_LOGO: /public/img/demos-logo.svg + GF_BRANDING_MENU_LOGO: /public/img/demos-logo.svg + GF_BRANDING_FAV_ICON: /public/img/favicon.png + GF_BRANDING_HIDE_VERSION: "true" + GF_DASHBOARDS_DEFAULT_HOME_DASHBOARD_PATH: /etc/grafana/provisioning/dashboards/json/demos-overview.json + GF_FEATURE_TOGGLES_ENABLE: publicDashboards,topnav,newPanelChromeUI + GF_NEWS_NEWS_FEED_ENABLED: "false" + GF_SECURITY_DISABLE_GRAVATAR: "true" + GF_DATE_FORMATS_USE_BROWSER_LOCALE: "true" + GF_DATE_FORMATS_DEFAULT_TIMEZONE: browser + volumes: + - grafana_data:/var/lib/grafana + - ./monitoring/grafana/provisioning/datasources:/etc/grafana/provisioning/datasources:ro + - ./monitoring/grafana/provisioning/dashboards:/etc/grafana/provisioning/dashboards:ro + - ./monitoring/grafana/grafana.ini:/etc/grafana/grafana.ini:ro + - ./monitoring/grafana/branding/demos-logo-white.svg:/usr/share/grafana/public/img/demos-logo.svg:ro + - ./monitoring/grafana/branding/demos-icon.svg:/usr/share/grafana/public/img/demos-icon.svg:ro + - ./monitoring/grafana/branding/favicon.png:/usr/share/grafana/public/img/favicon.png:ro + ports: + - "${GRAFANA_PORT:-3000}:3000" + networks: + - demos-network + + # --------------------------------------------------------------------------- + # node-exporter — host-level metrics (CPU/RAM/disk/network). Optional. + # Enable with: docker compose --profile monitoring --profile full up + # --------------------------------------------------------------------------- + node-exporter: + image: prom/node-exporter:v1.7.0 + container_name: demos-node-exporter + restart: unless-stopped + profiles: [full] + command: + - --path.procfs=/host/proc + - --path.sysfs=/host/sys + - --path.rootfs=/rootfs + - --collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/) + volumes: + - /proc:/host/proc:ro + - /sys:/host/sys:ro + - /:/rootfs:ro + ports: + - "${NODE_EXPORTER_PORT:-9100}:9100" + networks: + - demos-network + + # --------------------------------------------------------------------------- + # Neo4j — only needed if CGC/KYC features are in use. Opt-in. + # Enable with: docker compose --profile neo4j up + # --------------------------------------------------------------------------- neo4j: image: neo4j:5.21 - container_name: neo4j-cgc + container_name: demos-neo4j restart: unless-stopped - ports: - - "7474:7474" - - "7687:7687" + profiles: [neo4j] environment: - - NEO4J_AUTH=neo4j/MaE,MEL23lon! - - NEO4J_ACCEPT_LICENSE_AGREEMENT=yes + NEO4J_AUTH: ${NEO4J_AUTH:-neo4j/changeme-please} + NEO4J_ACCEPT_LICENSE_AGREEMENT: "yes" volumes: - neo4j_data:/data - neo4j_logs:/logs + ports: + - "${NEO4J_HTTP_PORT:-7474}:7474" + - "${NEO4J_BOLT_PORT:-7687}:7687" + networks: + - demos-network + +networks: + demos-network: + name: demos-network + driver: bridge volumes: + # Explicit `name:` overrides Compose's project-name prefix so the volumes + # have predictable identifiers that match what the docs reference. + # Without this, `node_state` would actually be `_node_state`, + # silently breaking seeding commands like `docker run -v node_state:/state`. + pgdata: + name: demos_pgdata + node_data: + name: demos_node_data + node_logs: + name: demos_node_logs + node_state: + name: demos_node_state + prometheus_data: + name: demos_prometheus_data + grafana_data: + name: demos_grafana_data neo4j_data: + name: demos_neo4j_data neo4j_logs: + name: demos_neo4j_logs diff --git a/monitoring/prometheus/prometheus.yml b/monitoring/prometheus/prometheus.yml index 4d9a51988..ac37d4cb6 100644 --- a/monitoring/prometheus/prometheus.yml +++ b/monitoring/prometheus/prometheus.yml @@ -30,16 +30,16 @@ scrape_configs: # Prometheus self-monitoring - job_name: prometheus static_configs: - - targets: [localhost:9090] + - targets: [prometheus:9090] metrics_path: /metrics # Demos Network Node metrics # The node exposes metrics at /metrics endpoint on METRICS_PORT (default 9090) # NOTE: If you changed METRICS_PORT in your main .env file, update the target below to match - # For example, if METRICS_PORT=3333, change the target to 'host.docker.internal:3333' + # For example, if METRICS_PORT=3333, change the target to 'node:3333' - job_name: demos-node static_configs: - - targets: [host.docker.internal:9090] # Must match METRICS_PORT from main .env + - targets: [node:9090] # service name 'node' from unified docker-compose.yml; matches METRICS_PORT from .env labels: instance: local-node environment: development diff --git a/package.json b/package.json index d52848fc3..27bf99cdf 100644 --- a/package.json +++ b/package.json @@ -78,9 +78,12 @@ "eslint": "^8.57.1", "jest": "^29.7.0", "knip": "^5.74.0", + "npm-check-updates": "^16.14.18", + "prettier": "3.8.0", "ts-jest": "^29.3.2", "ts-node": "^10.9.1", "ts-node-dev": "^2.0.0", + "tsx": "^3.12.8", "typescript": "^5.9.3" }, "dependencies": { @@ -128,12 +131,10 @@ "node-fetch": "2", "node-forge": "^1.3.3", "node-seal": "^5.1.3", - "npm-check-updates": "^16.14.18", "ntp-client": "^0.5.3", "object-sizeof": "^2.6.3", "pg": "^8.12.0", "poseidon-lite": "^0.3.0", - "prettier": "3.8.0", "prom-client": "^15.1.3", "reflect-metadata": "^0.1.13", "rijndael-js": "^2.0.0", @@ -147,7 +148,6 @@ "superdilithium": "^2.0.6", "terminal-kit": "^3.1.1", "tsconfig-paths": "^4.2.0", - "tsx": "^3.12.8", "tweetnacl": "^1.0.3", "typeorm": "^0.3.17", "web3": "^4.16.0", diff --git a/scripts/docker-entrypoint.sh b/scripts/docker-entrypoint.sh new file mode 100755 index 000000000..20d500c9b --- /dev/null +++ b/scripts/docker-entrypoint.sh @@ -0,0 +1,60 @@ +#!/bin/sh +# Demos Network — container entrypoint. +# +# Bridges the writable runtime files the node creates at /app/ to the +# persistent volume at /app/state, so they survive `docker compose down`. +# Also routes the timestamped logs__/ directory into /app/logs. +# +# Files bridged: +# /app/.demos_identity <- /app/state/.demos_identity +# /app/demos_peerlist.json <- /app/state/demos_peerlist.json +# /app/.tlsnotary-key <- /app/state/.tlsnotary-key +# /app/output <- /app/state/output +# /app/publickey_* <- /app/state/publickey_* (after creation) +# +# Logs: +# /app/logs_* -> /app/logs/ (the legacy logger writes one dir per port+id) +set -eu + +STATE_DIR="${STATE_DIR:-/app/state}" +LOGS_DIR="${LOGS_DIR:-/app/logs}" + +mkdir -p "$STATE_DIR" "$LOGS_DIR" + +# Files: create on volume if absent, then symlink from /app. +# The node generates these on demand on first boot — we don't pre-create. +for f in .demos_identity demos_peerlist.json .tlsnotary-key; do + target="$STATE_DIR/$f" + link="/app/$f" + # If /app already has a real file (e.g. baked from image), move it once + if [ -e "$link" ] && [ ! -L "$link" ]; then + if [ ! -e "$target" ]; then + mv "$link" "$target" + else + rm -f "$link" + fi + fi + # Create symlink (idempotent — pointing into the volume) + [ -L "$link" ] || ln -s "$target" "$link" +done + +# Directory: output/ +# rmdir would only work on an empty dir, and 'ln -sfn TARGET /app/output' +# does NOT replace a real directory — it creates output/output inside it. +# So if /app/output exists as a non-symlink directory, blow it away first. +if [ ! -L /app/output ]; then + mkdir -p "$STATE_DIR/output" + if [ -d /app/output ]; then + rm -rf /app/output + fi + ln -sfn "$STATE_DIR/output" /app/output +fi + +# publickey_* files — created at runtime by the node. Watch via post-start hook +# is overkill; instead, we pre-create the symlink target dir and rely on the +# node writing into /app/. After first boot, the file lives at /app/publickey_* +# (in container ephemeral layer). To persist, we'd need either the node code +# to write into state/, or a periodic sync. For now, accept that publickey_* +# is regenerable from the identity on every boot (it is — see src/index.ts). + +exec "$@" diff --git a/src/features/tlsnotary/TLSNotaryService.ts b/src/features/tlsnotary/TLSNotaryService.ts index 27e533b8f..3326e1d7f 100644 --- a/src/features/tlsnotary/TLSNotaryService.ts +++ b/src/features/tlsnotary/TLSNotaryService.ts @@ -36,6 +36,8 @@ export type TLSNotaryMode = "ffi" | "docker" * Service configuration options */ export interface TLSNotaryServiceConfig { + /** Hostname of the notary in docker mode. Defaults to 'localhost'. */ + host?: string /** Port to run the notary WebSocket server on */ port: number /** 32-byte secp256k1 private key (hex string or Uint8Array) - only used in FFI mode */ @@ -206,6 +208,7 @@ export function getConfigFromEnv(): TLSNotaryServiceConfig | null { } return { + host: process.env.TLSNOTARY_HOST ?? "localhost", port: parseInt(process.env.TLSNOTARY_PORT ?? "7047", 10), signingKey, maxSentData: parseInt( @@ -439,14 +442,15 @@ export class TLSNotaryService { private async startDockerMode(): Promise { const debug = isTLSNotaryDebug() const fatal = isTLSNotaryFatal() + const host = this.config.host ?? "localhost" log.info( - `[TLSNotary] Docker mode: checking container on port ${this.config.port}...`, + `[TLSNotary] Docker mode: checking container at ${host}:${this.config.port}...`, ) try { // Try to fetch /info endpoint to verify container is running - const infoUrl = `http://localhost:${this.config.port}/info` + const infoUrl = `http://${host}:${this.config.port}/info` const response = await fetch(infoUrl, { signal: AbortSignal.timeout(5000), }) @@ -477,11 +481,14 @@ export class TLSNotaryService { const message = error instanceof Error ? error.message : String(error) log.error( - `[TLSNotary] Failed to connect to Docker notary on port ${this.config.port}: ${message}`, + `[TLSNotary] Failed to connect to Docker notary at ${host}:${this.config.port}: ${message}`, ) log.error("[TLSNotary] Make sure the Docker container is running:") log.error( - "[TLSNotary] cd tlsnotary && TLSNOTARY_PORT=${TLSNOTARY_PORT} docker compose up -d", + "[TLSNotary] docker compose up -d tlsnotary (root compose)", + ) + log.error( + "[TLSNotary] cd tlsnotary && docker compose up -d (legacy standalone)", ) if (fatal) { diff --git a/src/index.ts b/src/index.ts index 64f0aeaa9..88acb7036 100644 --- a/src/index.ts +++ b/src/index.ts @@ -329,6 +329,39 @@ async function preMainLoop() { const ourselves = "http://127.0.0.1:" + indexState.SERVER_PORT getSharedState.connectionString = ourselves log.info("Our connection string is: " + ourselves) + // REVIEW: Warn operators when EXPOSED_URL points at a loopback/unroutable + // host so they don't silently run an unreachable node on the public network. + try { + const exposedUrl = Config.getInstance().core.exposedUrl + const loopbackHosts = new Set([ + "localhost", + "127.0.0.1", + "0.0.0.0", + "::1", + "host.docker.internal", + ]) + const exposedHost = new URL(exposedUrl).hostname + if (loopbackHosts.has(exposedHost)) { + log.warning( + "\n============================================================\n" + + "⚠️ EXPOSED_URL is set to a loopback/unroutable address:\n" + + ` ${exposedUrl}\n` + + " Other peers cannot reach this node at this address.\n" + + " For real network participation, set EXPOSED_URL in .env\n" + + " to your public IP or DNS name (e.g. http://YOUR_IP:53550).\n" + + " See INSTALL.md → \"Joining the network\".\n" + + "============================================================", + ) + } + } catch (err) { + // Malformed EXPOSED_URL — surface it so the operator can fix .env, + // but don't crash the node over a config quirk. + const message = err instanceof Error ? err.message : String(err) + log.warning( + `[CONFIG] EXPOSED_URL is not a valid URL — loopback check skipped. ` + + `Value: "${Config.getInstance().core.exposedUrl}". Error: ${message}`, + ) + } // And saves the public key file await fs.promises.writeFile( "publickey_" + getSharedState.signingAlgorithm + "_" + publicKeyHex,