DChain single-node blockchain + React Native messenger client. Core: - PBFT consensus with multi-sig validator admission + equivocation slashing - BadgerDB + schema migration scaffold (CurrentSchemaVersion=0) - libp2p gossipsub (tx/v1, blocks/v1, relay/v1, version/v1) - Native Go contracts (username_registry) alongside WASM (wazero) - WebSocket gateway with topic-based fanout + Ed25519-nonce auth - Relay mailbox with NaCl envelope encryption (X25519 + Ed25519) - Prometheus /metrics, per-IP rate limit, body-size cap Deployment: - Single-node compose (deploy/single/) with Caddy TLS + optional Prometheus - 3-node dev compose (docker-compose.yml) with mocked internet topology - 3-validator prod compose (deploy/prod/) for federation - Auto-update from Gitea via /api/update-check + systemd timer - Build-time version injection (ldflags → node --version) - UI / Swagger toggle flags (DCHAIN_DISABLE_UI, DCHAIN_DISABLE_SWAGGER) Client (client-app/): - Expo / React Native / NativeWind - E2E NaCl encryption, typing indicator, contact requests - Auto-discovery of canonical contracts, chain_id aware, WS reconnect on node switch Documentation: - README.md, CHANGELOG.md, CONTEXT.md - deploy/single/README.md with 6 operator scenarios - deploy/UPDATE_STRATEGY.md with 4-layer forward-compat design - docs/contracts/*.md per contract
DChain single-node deployment
Один узел + опционально Caddy TLS + опционально Prometheus/Grafana. Подходит под четыре основных сценария:
- личная нода — публичная или приватная с токеном,
- первый узел новой сети (genesis),
- присоединение к существующей сети (relay / observer / validator),
- headless API-нода для мобильных клиентов — без HTML-UI.
Для 3-валидаторного кластера смотри ../prod/.
Навигация
- 0. Что поднимается
- 1. Быстрый старт
- 2. Сценарии конфигурации
- 3. HTTP-поверхность
- 4. Auto-update от Gitea
- 5. Обновление / бэкап / восстановление
- 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. Быстрый старт
# 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://<host>: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.
# 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 хочется оставить как доку для разработчиков.
# 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 foundGET /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 выключен)
Когда подходит: персональная нода под мессенджер, вы — единственный пользователь, никому посторонним не должна быть видна даже статистика.
# 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 <token>→401. /swaggerпо-прежнему отдаётся (он не кастомизируется под токены, а API-вызовы из Swagger UI будут возвращать 401 — это нормально).- P2P порт
4001остаётся открытым — без него нода не синкается с сетью.
Передать токен клиенту:
// 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 поверхности не должно быть на виду.
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, но шумит в логах).
DCHAIN_GENESIS=true
DCHAIN_ANNOUNCE=/ip4/203.0.113.10/tcp/4001
DOMAIN=dchain.example.com
Проверка:
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, но не голосует.
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:
# 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 без явного согласия
Проверка:
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-обновления:
sudo cp systemd/dchain-update.{service,timer} /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now dchain-update.timer
Скрипт update.sh:
- спрашивает
/api/update-check— еслиupdate_available: false, выходит; - делает
git fetch --tags, checkout на новый тег; - semver-guard: блокирует major-скачок (vN.x → vN+1.y) если
UPDATE_ALLOW_MAJOR != true; - ребилдит образ с injected версией (
VERSION_TAG/COMMIT/DATE/DIRTY); - smoke-test
node --version; docker compose up -d --force-recreate node;- polling
/api/netstats— до 60 сек, fail loud если не ожил.
Подробнее — ../UPDATE_STRATEGY.md.
5. Обновление / бэкап / восстановление
# Ручное обновление (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 |
/ возвращает 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=... вручную |