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
388 lines
17 KiB
Markdown
388 lines
17 KiB
Markdown
# 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://<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.
|
||
|
||
```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 <token>` → `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=...` вручную |
|