# DChain single-node deployment Один узел + опционально Caddy TLS + опционально Prometheus/Grafana. Подходит под четыре основных сценария: 1. **личная нода** — публичная или приватная с токеном, 2. **первый узел новой сети** (genesis), 3. **присоединение к существующей сети** (relay / observer / validator), 4. **headless API-нода** для мобильных клиентов — без HTML-UI. Для 3-валидаторного кластера смотри `../prod/`. --- ## Навигация - [0. Что поднимается](#0-что-поднимается) - [1. Быстрый старт](#1-быстрый-старт) - [2. Сценарии конфигурации](#2-сценарии-конфигурации) - [2.1. Публичная нода с UI и открытым Swagger](#21-публичная-нода-с-ui-и-открытым-swagger) - [2.2. Headless API-нода (без UI, Swagger открыт)](#22-headless-api-нода-без-ui-swagger-открыт) - [2.3. Полностью приватная (токен на всё, UI выключен)](#23-полностью-приватная-токен-на-всё-ui-выключен) - [2.4. Только-API без Swagger](#24-только-api-без-swagger) - [2.5. Первая нода новой сети (--genesis)](#25-первая-нода-новой-сети---genesis) - [2.6. Присоединение к существующей сети (--join)](#26-присоединение-к-существующей-сети---join) - [3. HTTP-поверхность](#3-http-поверхность) - [4. Auto-update от Gitea](#4-auto-update-от-gitea) - [5. Обновление / бэкап / восстановление](#5-обновление--бэкап--восстановление) - [6. Troubleshooting](#6-troubleshooting) --- ## 0. Что поднимается Базовый compose (`docker compose up -d`) поднимает: | Сервис | Что это | Порты | |--------|---------|-------| | `node` | сама нода DChain (`dchain-node-slim` image) | `4001` (libp2p P2P, наружу), `8080` (HTTP/WS — только через Caddy) | | `caddy` | TLS edge с auto-HTTPS (Let's Encrypt) | `80`, `443`, `443/udp` | С `--profile monitor` добавляются: | Сервис | Что это | Порты | |--------|---------|-------| | `prometheus` | метрики + TSDB (30 дней retention) | внутри сети | | `grafana` | дашборды | `3000` | --- ## 1. Быстрый старт ```bash # 1. Сгенерируй ключ ноды (один раз, храни в безопасности). docker build -t dchain-node-slim -f ../prod/Dockerfile.slim ../.. mkdir -p keys docker run --rm --entrypoint /usr/local/bin/client \ -v "$PWD/keys:/out" dchain-node-slim \ keygen --out /out/node.json # 2. Скопируй env и отредактируй. cp node.env.example node.env $EDITOR node.env # минимум: DCHAIN_ANNOUNCE, DOMAIN, DCHAIN_API_TOKEN # 3. Подними. docker compose up -d # 4. (опционально) Мониторинг. GRAFANA_ADMIN_PW=$(openssl rand -hex 16) \ docker compose --profile monitor up -d # → Grafana http://:3000, источник http://prometheus:9090 # 5. Проверь живость. curl -s https://$DOMAIN/api/netstats curl -s https://$DOMAIN/api/well-known-version ``` > **Windows:** если запускаете через Docker Desktop и Git Bash, добавляйте > `MSYS_NO_PATHCONV=1` перед командами с `/out`, `/keys` и подобными Unix-путями > — иначе Git Bash сконвертирует их в Windows-пути. --- ## 2. Сценарии конфигурации Все сценарии отличаются только содержимым `node.env`. Пересоздавать контейнер: `docker compose up -d --force-recreate node`. ### 2.1. Публичная нода с UI и открытым Swagger **Когда подходит:** вы хотите показать Explorer всем (адрес для поиска по pubkey, история блоков, список валидаторов), и оставить Swagger как живую документацию API. ```ini # node.env DCHAIN_ANNOUNCE=/ip4/203.0.113.10/tcp/4001 DOMAIN=dchain.example.com ACME_EMAIL=you@example.com # никакого токена — публичный режим # UI и Swagger зажгутся по умолчанию (флаги ниже не задаём) ``` Результат: | URL | Что там | |-----|---------| | `https://$DOMAIN/` | Блок-эксплорер (главная) | | `https://$DOMAIN/address?pub=…` | Баланс + история по pubkey | | `https://$DOMAIN/tx?id=…` | Детали транзакции | | `https://$DOMAIN/validators` | Список валидаторов | | `https://$DOMAIN/tokens` | Зарегистрированные токены | | `https://$DOMAIN/swagger` | **Swagger UI** — интерактивная OpenAPI спека | | `https://$DOMAIN/swagger/openapi.json` | Сырой OpenAPI JSON — для codegen | | `https://$DOMAIN/api/*` | Вся JSON-API поверхность | | `https://$DOMAIN/metrics` | Prometheus exposition | ### 2.2. Headless API-нода (без UI, Swagger открыт) **Когда подходит:** нода — это бэкенд для мобильного приложения, HTML-эксплорер не нужен, но Swagger хочется оставить как доку для разработчиков. ```ini # node.env DCHAIN_ANNOUNCE=/ip4/203.0.113.20/tcp/4001 DOMAIN=api.dchain.example.com # Отключаем HTML-страницы эксплорера, но НЕ Swagger. DCHAIN_DISABLE_UI=true ``` Эффект: - `GET /` → `404 page not found` - `GET /address`, `/tx`, `/validators`, `/tokens`, `/contract` и все `/assets/explorer/*` → 404. - `GET /swagger` → Swagger UI, работает (без изменений). - `GET /api/*`, `GET /metrics`, `GET /api/ws` → работают. Нода логирует: ``` [NODE] explorer UI: disabled (--disable-ui) [NODE] swagger: http://0.0.0.0:8080/swagger ``` ### 2.3. Полностью приватная (токен на всё, UI выключен) **Когда подходит:** персональная нода под мессенджер, вы — единственный пользователь, никому посторонним не должна быть видна даже статистика. ```ini # node.env DCHAIN_ANNOUNCE=/ip4/203.0.113.30/tcp/4001 DOMAIN=node.personal.example DCHAIN_API_TOKEN=$(openssl rand -hex 32) # скопируйте в клиент DCHAIN_API_PRIVATE=true # закрывает и read-эндпоинты # UI вам не нужен, а кому бы и был — всё равно 401 без токена. DCHAIN_DISABLE_UI=true ``` Эффект: - Любой `/api/*` без `Authorization: Bearer ` → `401`. - `/swagger` по-прежнему отдаётся (он не кастомизируется под токены, а API-вызовы из Swagger UI будут возвращать 401 — это нормально). - P2P порт `4001` остаётся открытым — без него нода не синкается с сетью. Передать токен клиенту: ```ts // client-app/lib/api.ts — в post()/get() добавить: headers: { 'Authorization': 'Bearer ' + YOUR_TOKEN } // для WebSocket — токен как query-параметр: this.url = base.replace(/^http/, 'ws') + '/api/ws?token=' + YOUR_TOKEN; ``` ### 2.4. Только-API без Swagger **Когда подходит:** максимально hardened headless-нода. Даже описание API поверхности не должно быть на виду. ```ini DCHAIN_ANNOUNCE=/ip4/203.0.113.40/tcp/4001 DOMAIN=rpc.dchain.example.com DCHAIN_DISABLE_UI=true DCHAIN_DISABLE_SWAGGER=true ``` Эффект: - `/` → 404, `/swagger` → 404, `/api/*` → работает. - В логах: ``` [NODE] explorer UI: disabled (--disable-ui) [NODE] swagger: disabled (--disable-swagger) ``` - Swagger спеку всё равно можно сгенерить локально: `go run ./cmd/node` в dev-режиме → `http://localhost:8080/swagger/openapi.json` → сохранить. ### 2.5. Первая нода новой сети (`--genesis`) Любой из сценариев выше + установить `DCHAIN_GENESIS=true` при самом первом запуске. Нода создаст блок 0 со своим же pubkey как единственным валидатором. После первого успешного старта удалите эту строку из `node.env` (no-op, но шумит в логах). ```ini DCHAIN_GENESIS=true DCHAIN_ANNOUNCE=/ip4/203.0.113.10/tcp/4001 DOMAIN=dchain.example.com ``` Проверка: ```bash curl -s https://$DOMAIN/api/netstats | jq .validator_count # → 1 curl -s https://$DOMAIN/api/network-info | jq .genesis_hash # сохраните ``` ### 2.6. Присоединение к существующей сети (`--join`) Любой из сценариев + `DCHAIN_JOIN` со списком HTTP URL-ов seed-нод. Нода подтянет `chain_id`, genesis hash, список валидаторов и пиров автоматически через `/api/network-info`. Запускается как **observer** по умолчанию — применяет блоки и принимает tx, но не голосует. ```ini DCHAIN_JOIN=https://seed1.dchain.example.com,https://seed2.dchain.example.com DCHAIN_ANNOUNCE=/ip4/203.0.113.50/tcp/4001 DOMAIN=node2.example.com ``` Чтобы стать валидатором — существующий валидатор должен подать `ADD_VALIDATOR` с мульти-подписями. См. `../prod/README.md` → "Add a 4th validator". --- ## 3. HTTP-поверхность Что отдаёт нода по умолчанию (все `/api/*` всегда включены, даже с `DCHAIN_DISABLE_UI=true`): ### Публичные health / discovery | Endpoint | Назначение | |----------|-----------| | `/api/netstats` | tip height, total tx count, supply, validator count | | `/api/network-info` | one-shot bootstrap payload для нового клиента/ноды | | `/api/well-known-version` | node_version, protocol_version, features[], build{tag, commit, date, dirty} | | `/api/well-known-contracts` | канонические contract_id → name map | | `/api/update-check` | сравнивает свой commit с Gitea release (нужен `DCHAIN_UPDATE_SOURCE_URL`) | | `/api/validators` | активный validator set | | `/api/peers` | живые libp2p пиры + их версия (из gossip-топика `dchain/version/v1`) | ### Chain explorer JSON | Endpoint | Назначение | |----------|-----------| | `/api/blocks?limit=N` | последние N блоков | | `/api/block/{index}` | один блок | | `/api/txs/recent?limit=N` | последние N tx | | `/api/tx/{id}` | одна транзакция | | `/api/address/{pubkey_or_DC-addr}` | баланс + история | | `/api/identity/{pubkey_or_DC-addr}` | ed25519 ↔ x25519 binding | | `/api/relays` | зарегистрированные relay-ноды | | `/api/contracts` / `/api/contracts/{id}` / `/api/contracts/{id}/state/{key}` | контракты | | `/api/tokens` / `/api/tokens/{id}` / `/api/nfts` | токены и NFT | | `/api/channels/{id}` / `/api/channels/{id}/members` | каналы и члены (для fan-out) | ### Submit / Real-time | Endpoint | Назначение | |----------|-----------| | `POST /api/tx` | submit подписанной tx (rate-limit + body-cap; token-gate если задан) | | `GET /api/ws` | WebSocket (auth, topic subscribe, submit_tx, typing) | | `GET /api/events` | SSE (односторонний legacy stream) | ### HTML (выключается `DCHAIN_DISABLE_UI=true`) | Endpoint | Назначение | |----------|-----------| | `/` | главная эксплорера | | `/address`, `/tx`, `/node`, `/relays`, `/validators`, `/contract`, `/tokens`, `/token` | страницы | | `/assets/explorer/*.js|css` | статические ассеты | ### Swagger (выключается `DCHAIN_DISABLE_SWAGGER=true`) | Endpoint | Назначение | |----------|-----------| | `/swagger` | Swagger UI (грузит swagger-ui-dist с unpkg) | | `/swagger/openapi.json` | сырая OpenAPI 3.0 спека | ### Prometheus | Endpoint | Назначение | |----------|-----------| | `/metrics` | exposition, всегда включён | > **Защита `/metrics`:** у эндпоинта нет встроенной авторизации. В > публичной деплое закройте его на уровне Caddy — пример в > `Caddyfile`: рестрикт по IP/токену scrape-сервера. --- ## 4. Auto-update от Gitea После поднятия проекта на Gitea: ```ini # node.env DCHAIN_UPDATE_SOURCE_URL=https://gitea.example.com/api/v1/repos/dchain/dchain/releases/latest DCHAIN_UPDATE_SOURCE_TOKEN= # опционально, для приватных repo UPDATE_ALLOW_MAJOR=false # блокирует v1.x → v2.y без явного согласия ``` Проверка: ```bash curl -s https://$DOMAIN/api/update-check | jq . # { # "current": { "tag": "v0.5.0", "commit": "abc1234", ... }, # "latest": { "tag": "v0.5.1", "url": "https://gitea...", ... }, # "update_available": true, # "checked_at": "2026-04-17T10:41:03Z" # } ``` systemd-таймер для бесшумного hourly-обновления: ```bash sudo cp systemd/dchain-update.{service,timer} /etc/systemd/system/ sudo systemctl daemon-reload sudo systemctl enable --now dchain-update.timer ``` Скрипт `update.sh`: 1. спрашивает `/api/update-check` — если `update_available: false`, выходит; 2. делает `git fetch --tags`, checkout на новый тег; 3. **semver-guard**: блокирует major-скачок (vN.x → vN+1.y) если `UPDATE_ALLOW_MAJOR != true`; 4. ребилдит образ с injected версией (`VERSION_TAG/COMMIT/DATE/DIRTY`); 5. smoke-test `node --version`; 6. `docker compose up -d --force-recreate node`; 7. polling `/api/netstats` — до 60 сек, fail loud если не ожил. Подробнее — `../UPDATE_STRATEGY.md`. --- ## 5. Обновление / бэкап / восстановление ```bash # Ручное обновление (downtime ~5-8 сек): docker compose pull docker compose build docker compose up -d --force-recreate node # Проверить что новая версия поднялась: docker exec dchain_node /usr/local/bin/node --version curl -s https://$DOMAIN/api/well-known-version | jq .build # Backup chain state: docker run --rm -v dchain-single_node_data:/data -v "$PWD":/bak alpine \ tar czf /bak/dchain-$(date +%F).tar.gz -C /data . # Восстановление: docker compose stop node docker run --rm -v dchain-single_node_data:/data -v "$PWD":/bak alpine \ sh -c "rm -rf /data/* && tar xzf /bak/dchain-2026-04-10.tar.gz -C /data" docker compose up -d node ``` --- ## 6. Troubleshooting | Симптом | Проверка | |---------|----------| | `failed to get certificate` в Caddy | DNS A-record на DOMAIN → этот хост? Порт 80 открыт? | | `/api/tx` возвращает 401 | Токен в заголовке совпадает с `DCHAIN_API_TOKEN`? | | Ноды не видят друг друга | Порт 4001 открыт? `DCHAIN_ANNOUNCE` = публичный IP? | | Блоки не растут (validator mode) | `docker compose logs node | grep PBFT` — собирается ли quorum? | | `/` возвращает 404 | `DCHAIN_DISABLE_UI=true` установлен — либо уберите, либо используйте `/api/*` | | `/swagger` возвращает 404 | `DCHAIN_DISABLE_SWAGGER=true` — уберите, либо хостьте `openapi.json` отдельно | | `update-check` возвращает 503 | `DCHAIN_UPDATE_SOURCE_URL` не задан или пустой | | `update-check` возвращает 502 | Gitea недоступна или URL неверный — проверьте `curl $DCHAIN_UPDATE_SOURCE_URL` руками | | `FATAL: genesis hash mismatch` | В volume чейн с другим genesis. `docker volume rm dchain-single_node_data` → `up -d` (потеря локальных данных) | | Диск растёт | BadgerDB GC работает раз в 5 мин; для блокчейна с десятками тысяч блоков обычно < 500 MB | | `--version` выдаёт `dev` | Образ собран без `--build-arg VERSION_*` — ребилдните через `update.sh` или `docker build --build-arg VERSION_TAG=...` вручную |