Files
dchain/deploy/single/README.md
vsecoder 7e7393e4f8 chore: initial commit for v0.0.1
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
2026-04-17 14:16:44 +03:00

388 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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=...` вручную |