vsecoder 7e6fe2c2a0 fix(desktop): infinite render loop opening a chat (Maximum update depth)
The Conversation selector `useStore(s => s.messages[address] ?? [])`
allocates a fresh empty array on every call when the chat has no cached
messages. Zustand compares selector results with `===`, so the new []
is different from the previous [], marking the slice as "changed",
which re-renders Conversation, which calls the selector again, which
produces another new []... "Maximum update depth exceeded" inside
seconds.

Fix: module-level `const EMPTY_MESSAGES: Message[] = []` returned as the
fallback. Same object reference every render, zustand's === bails
early, no re-render.

This crash only showed up after opening a chat whose messages hadn't
been cached yet — picking any entry in ChatList that hadn't received
an envelope would hang the renderer. PaneBoundary (added in the prior
commit) now catches it visibly instead of blacking out the whole
window, but we still want the real fix.
2026-04-22 18:58:57 +03:00
2026-04-17 14:16:44 +03:00
2026-04-17 14:16:44 +03:00
2026-04-17 14:16:44 +03:00
2026-04-17 14:16:44 +03:00
2026-04-17 14:16:44 +03:00
2026-04-17 14:16:44 +03:00
2026-04-17 14:16:44 +03:00
2026-04-17 14:16:44 +03:00
2026-04-17 14:16:44 +03:00

DChain

Блокчейн-стек для децентрализованного мессенджера + социальной ленты:

  • PBFT консенсус с multi-sig validator governance и equivocation slashing
  • Native Go контракты рядом с WASM (wazero) — нулевая задержка для системных сервисов типа username_registry
  • WebSocket push API — клиент не опрашивает, все события прилетают на соединение
  • E2E-шифрованный relay mailbox на libp2p gossipsub с TTL live-detection (1:1 чаты через NaCl box; посты в ленте — plaintext-публичные)
  • Социальная лента v2.0.0 (заменила каналы): публичные посты с оплатой за размер (автор платит, хостящая релей-нода получает); on-chain граф подписок + лайки; off-chain просмотры + хэштеги; мандаторный server-side scrubber метаданных (EXIF/GPS-стрип + FFmpeg sidecar для видео); share-to-chat c embedded post-карточкой
  • Система обновлений: build-time версия → /api/well-known-version, peer-version gossip, /api/update-check против Gitea releases, update.sh с semver guard
  • Prometheus /metrics, Caddy auto-HTTPS, observer mode, load-test

Содержание


Быстрый старт

Одна нода в Docker, HTTP API на localhost:8080, Explorer UI и Swagger открыты:

# 1. Собираем prod-образ
docker build -t dchain-node-slim -f deploy/prod/Dockerfile.slim .

# 2. Ключ ноды (один раз)
mkdir -p keys
docker run --rm --entrypoint /usr/local/bin/client \
  -v "$PWD/keys:/out" dchain-node-slim \
  keygen --out /out/node.json

# 3. Запуск (genesis-валидатор)
docker run -d --name dchain --restart unless-stopped \
  -p 4001:4001 -p 8080:8080 \
  -v dchain_data:/data \
  -v "$PWD/keys:/keys:ro" \
  -e DCHAIN_GENESIS=true \
  -e DCHAIN_ANNOUNCE=/ip4/127.0.0.1/tcp/4001 \
  dchain-node-slim \
  --db=/data/chain --mailbox-db=/data/mailbox --key=/keys/node.json \
  --relay-key=/data/relay.json --listen=/ip4/0.0.0.0/tcp/4001 --stats-addr=:8080

# 4. Проверка
open http://localhost:8080/            # Explorer
open http://localhost:8080/swagger     # Swagger UI
curl -s http://localhost:8080/api/well-known-version | jq .

3-node dev-кластер (для тестов PBFT кворума, slashing, federation): docker compose up --build -d — см. docs/quickstart.md.

Поднятие ноды — пошагово

Ниже — полный минимум для двух сценариев, которые покрывают 99% случаев: первая нода сети (genesis) и присоединение к существующей сети. Все флаги читаются также из соответствующего DCHAIN_* env-var (CLI > env > default).

Шаг 1. Ключи

# Ключ identity ноды (Ed25519 — подпись блоков + tx)
./client keygen --out keys/node.json
# relay-ключ (X25519 — E2E-mailbox) создаётся нодой сам при первом старте,
# но можно задать путь заранее через --relay-key.

Шаг 2a. Первая нода (genesis)

Поднимает новую сеть с одним валидатором. --genesis=true только для самой первой ноды и только один раз — если блок 0 уже есть в --db, флаг игнорируется.

./node \
  --genesis=true \
  --key=keys/node.json \
  --db=./chaindata \
  --mailbox-db=./mailboxdata \
  --feed-db=./feeddata \
  --listen=/ip4/0.0.0.0/tcp/4001 \
  --announce=/ip4/<ВАШ-ПУБЛИЧНЫЙ-IP>/tcp/4001 \
  --stats-addr=:8080 \
  --register-relay=true \
  --relay-fee=1000

--announce обязателен для любой ноды смотрящей в интернет (VPS / внешний IP / Docker с проброшенным портом). Без него libp2p пытается UPnP/NAT-PMP и чаще всего промахивается.

Шаг 2b. Вторая и последующие ноды

Нужен один из двух способов узнать первую ноду. Второй удобнее.

Через HTTP URL живой ноды (рекомендуется — нода сама заберёт multiaddr через /api/network-info, проверит genesis_hash и синхронизирует цепь):

./node \
  --join=https://first-node.example.com \
  --key=keys/node.json \
  --db=./chaindata \
  --mailbox-db=./mailboxdata \
  --feed-db=./feeddata \
  --listen=/ip4/0.0.0.0/tcp/4001 \
  --announce=/ip4/<ВАШ-ПУБЛИЧНЫЙ-IP>/tcp/4001 \
  --stats-addr=:8080 \
  --register-relay=true \
  --relay-fee=1000

Через libp2p multiaddr (если есть прямой мульти-адрес):

./node \
  --peers=/ip4/1.2.3.4/tcp/4001/p2p/12D3KooW... \
  # остальные флаги как выше

Автоприсоединение к validator set происходит не само: после того как нода синхронизируется, действующий validator должен вызвать client add-validator --target <your-pub> --cosigs ... (multi-sig admit). До этого новая нода живёт как observer — читает и гоняет tx, но не голосует. Запустить ноду явно как observer (никогда не проситься в validator set): --observer=true.

Все флаги node

CLI / env / default. Группы:

Storage

Флаг Env Default Назначение
--db DCHAIN_DB ./chaindata BadgerDB блокчейна
--mailbox-db DCHAIN_MAILBOX_DB ./mailboxdata E2E-конверты 1:1 чатов
--feed-db DCHAIN_FEED_DB ./feeddata Тела постов ленты (off-chain)
--feed-ttl-days DCHAIN_FEED_TTL_DAYS 30 Через сколько дней тела постов auto-evict'ятся (метаданные on-chain остаются вечно)

Identity

Флаг Env Default Назначение
--key DCHAIN_KEY ./node.json Ed25519 ключ ноды
--relay-key DCHAIN_RELAY_KEY ./relay.json X25519 ключ для relay-mailbox (создастся сам)
--wallet DCHAIN_WALLET Отдельный payout-кошелёк (опционально)
--wallet-pass DCHAIN_WALLET_PASS Парольная фраза для wallet-файла

Network

Флаг Env Default Назначение
--listen DCHAIN_LISTEN /ip4/0.0.0.0/tcp/4001 libp2p listen multiaddr
--announce DCHAIN_ANNOUNCE Multiaddr который нода рассказывает пирам (обязателен на VPS/внешнем IP)
--peers DCHAIN_PEERS Bootstrap multiaddrs, comma-separated
--join DCHAIN_JOIN HTTP URL живой ноды для авто-дискавери — получает peers и genesis_hash
--allow-genesis-mismatch false Отключить защиту, падающую при расхождении локального и seed'ового genesis (только для явной миграции)

Consensus & role

Флаг Env Default Назначение
--genesis DCHAIN_GENESIS false Создать блок 0 (только для самой первой ноды сети)
--validators DCHAIN_VALIDATORS Исходный validator set (CSV pub-keys) — применяется только при genesis
--observer DCHAIN_OBSERVER false Observer-режим: синхронизируется и отдаёт API, но не голосует и не предлагает блоки
--heartbeat DCHAIN_HEARTBEAT true Периодический HEARTBEAT-tx (нужен для liveness-детекции валидаторов)

Relay / mailbox

Флаг Env Default Назначение
--register-relay DCHAIN_REGISTER_RELAY false Отправить REGISTER_RELAY tx на старте (объявить ноду публичным relay'ем)
--relay-fee DCHAIN_RELAY_FEE 1000 Плата за доставку одного сообщения в µT (1000 = 0.001 T). 0 = бесплатный relay

Media scrubber (feed)

Флаг Env Default Назначение
--media-sidecar-url DCHAIN_MEDIA_SIDECAR_URL URL FFmpeg-сайдкара для видео-скраба. Пустой = только картинки
--allow-unscrubbed-video DCHAIN_ALLOW_UNSCRUBBED_VIDEO false Принимать видео без серверного скраба (опасно — EXIF/GPS/автор-теги останутся)

HTTP API

Флаг Env Default Назначение
--stats-addr DCHAIN_STATS_ADDR :8080 Адрес HTTP/WS сервера
--api-token DCHAIN_API_TOKEN Bearer-токен для submit tx. Пустой = публичная нода
--api-private DCHAIN_API_PRIVATE false Требовать токен также на чтение
--disable-ui DCHAIN_DISABLE_UI false Отключить HTML-explorer (JSON API остаётся)
--disable-swagger DCHAIN_DISABLE_SWAGGER false Отключить /swagger*

Resource caps (новое в v2.1.0)

Флаг Env Default Назначение
--max-cpu DCHAIN_MAX_CPU 0 Сколько CPU-ядер Go-runtime'у (GOMAXPROCS). 0 = все
--max-ram-mb DCHAIN_MAX_RAM_MB 0 Soft-лимит Go-хипа в MiB (GOMEMLIMIT). 0 = без лимита. Не OOM-kill'ит — усиливает GC при приближении
--feed-disk-limit-mb DCHAIN_FEED_DISK_LIMIT_MB 0 Жёсткая квота на feed-БД. При превышении /feed/publish отвечает 507. Существующие посты продолжают отдаваться
--chain-disk-limit-mb DCHAIN_CHAIN_DISK_LIMIT_MB 0 Advisory-квота на блокчейн-БД. Превышение → WARN в лог раз в минуту (жёстко не отказываем — сломали бы консенсус)

Для реального sandboxing (hard-kill при OOM, hard CPU throttling) используйте docker run --cpus --memory или systemd CPUQuota / MemoryMax поверх этих флагов.

Update / versioning

Флаг Env Default Назначение
--update-source-url DCHAIN_UPDATE_SOURCE_URL Gitea /api/v1/repos/{owner}/{repo}/releases/latest для /api/update-check
--update-source-token DCHAIN_UPDATE_SOURCE_TOKEN PAT для приватного репо
--log-format DCHAIN_LOG_FORMAT text text (human) или json (Loki/ELK)
--governance-contract DCHAIN_GOVERNANCE_CONTRACT ID governance-контракта для динамических параметров
--version Печатает версию и выходит

Минимальные чек-листы

Первая нода (открытая): --genesis=true + --key + --announce на внешний IP + --stats-addr + опционально --register-relay=true --relay-fee=... чтобы сразу монетизировать relay-трафик.

Joiner: --join=<url-любой-живой-ноды> + --key + --announce + --stats-addr. После синка попросите действующего валидатора поднять add-validator (иначе остаётесь observer'ом до принятия — это нормально и безопасно).

Приватная/домашняя нода без публичного эксплорера: добавьте --api-token=<random>, --api-private=true, --disable-ui=true, --disable-swagger=true. Clients передают Authorization: Bearer <token>.

Слабое железо: --max-cpu=2 --max-ram-mb=1024 --feed-disk-limit-mb=2048 --chain-disk-limit-mb=10240.

Docker-обёртка с теми же флагами — в deploy/single/README.md.

Продакшен деплой

Два варианта, по масштабу.

🔸 Single-node (deploy/single/)

Рекомендуется для личного/первого узла. Один узел + Caddy TLS + опциональный Prometheus. Полный runbook с 6 сценариями (публичная с UI, headless, полностью приватная, genesis, joiner, auto-update) — в deploy/single/README.md.

Модели доступа

Режим DCHAIN_API_TOKEN DCHAIN_API_PRIVATE Поведение
Public (default) не задан Все могут читать и писать
Token writes задан false Читать — любой; submit tx — только с токеном
Fully private задан true Всё требует Authorization: Bearer <token>

UI / Swagger — включать или нет?

Нужно DCHAIN_DISABLE_UI DCHAIN_DISABLE_SWAGGER Открыто
Публичная с эксплорером + docs не задано не задано / Explorer, /swagger, /api/*, /metrics
Headless API-нода с OpenAPI true не задано /swagger, /api/*, /metrics
Личная hardened true true /api/* + /metrics

Флаги читаются и из CLI (--disable-ui, --disable-swagger), и из env. /api/* JSON-поверхность регистрируется всегда — отключить её можно только на уровне Caddy / firewall.

Auto-update

# node.env — когда проект в Gitea
DCHAIN_UPDATE_SOURCE_URL=https://gitea.example.com/api/v1/repos/OWNER/REPO/releases/latest
UPDATE_ALLOW_MAJOR=false
# Hourly systemd timer с 15-мин jitter
sudo cp deploy/single/systemd/dchain-update.{service,timer} /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now dchain-update.timer

Подробно: docs/update-system.md + deploy/UPDATE_STRATEGY.md.

🔹 Multi-validator (deploy/prod/)

3 validator'а в PBFT-кворуме, Caddy с ip_hash для WS-стикинесса и least-conn для REST. Для федераций / консорциумов — см. deploy/prod/README.md и docs/node/multi-server.md.

Архитектура

┌────────────┐    libp2p     ┌────────────┐    libp2p     ┌────────────┐
│   node-A   │◄─────pubsub──►│   node-B   │◄─────pubsub──►│   node-C   │
│ validator  │               │ validator  │               │ validator  │
│  + relay   │               │  + relay   │               │  + relay   │
└─────┬──────┘               └─────┬──────┘               └─────┬──────┘
      │                            │                            │
      │ HTTPS / wss (via Caddy)    │                            │
      ▼                            ▼                            ▼
    mobile / web / CLI clients   (ip_hash для WS, least-conn для REST)

Четыре слоя: network → chain → transport → app. Детали — в docs/architecture.md.

Основные пакеты:

Путь Роль
blockchain/ Блочная машина, applyTx, native-контракты, schema-migrations
consensus/ PBFT (Pre-prepare → Prepare → Commit), мемпул, equivocation
p2p/ libp2p host, gossipsub, sync протокол, peer-version gossip
node/ HTTP + WS API, SSE, metrics, access control
node/version/ Build-time version metadata (ldflags-инжектимый)
vm/ wazero runtime для WASM-контрактов + gas model
relay/ E2E mailbox (1:1 envelopes) + public feed-mailbox (post bodies, view counter, hashtag index)
media/ Server-side metadata scrubber (EXIF strip + FFmpeg sidecar client)
identity/ Ed25519 + X25519 keypair, tx signing
economy/ Fee model, rewards
wallet/ Optional payout wallet (отдельный ключ)

REST / WebSocket API

Обзор (полный reference — docs/api/README.md):

Chain + stats

Endpoint Описание
GET /api/netstats height, total_txs, supply, validator_count
GET /api/network-info chain_id, genesis, peers, validators, contracts
GET /api/blocks?limit=N / /api/block/{index} Блоки
GET /api/tx/{id} / /api/txs/recent?limit=N Транзакции
GET /api/address/{pub_or_addr} Баланс + история
GET /api/validators Validator set
GET /api/peers Connected peers + их версии
POST /api/tx Submit signed tx (rate-limited, size-capped)

Discovery + update

Endpoint Описание
GET /api/well-known-version {node_version, build{}, protocol_version, features[], chain_id}
GET /api/well-known-contracts {name → contract_id} map
GET /api/update-check Diff с Gitea release (см. docs/update-system.md)

Real-time

  • GET /api/wsWebSocket (recommended). Ops: auth, subscribe, unsubscribe, submit_tx, typing, ping. Push: block, tx, inbox, typing, submit_ack.
  • GET /api/events — SSE legacy one-way.

Scoped WS-топики (addr:, inbox:, typing:) требуют auth через Ed25519-nonce; публичные (blocks, tx, contract_log) — без.

Relay (E2E messaging)

Endpoint Описание
POST /relay/broadcast Опубликовать pre-sealed envelope (E2E-путь, рекомендован)
GET /relay/inbox?pub=<x25519> Прочитать входящие конверты
DELETE /relay/inbox/{id} Удалить envelope (требует Ed25519-подписи владельца)

Детали — docs/api/relay.md. /relay/send оставлен для backward-compat, но ломает E2E (nod-релей запечатывает своим ключом) и помечен как non-recommended.

Social feed (v2.0.0)

Endpoint Описание
POST /feed/publish Загрузить тело поста + EXIF-скраб + вернуть fee
GET /feed/post/{id} Тело поста
GET /feed/post/{id}/attachment Сырые байты картинки/видео (cache'able)
GET /feed/post/{id}/stats?me=<pub> {views, likes, liked_by_me?}
POST /feed/post/{id}/view Бамп off-chain счётчика просмотров
GET /feed/author/{pub}?before=<ts>&limit=N Посты автора (пагинация before)
GET /feed/timeline?follower=<pub>&before=<ts>&limit=N Merged лента подписок
GET /feed/trending?window=24&limit=N Топ по likes × 3 + views за окно
GET /feed/foryou?pub=<pub>&limit=N Рекомендации (неподписанные авторы)
GET /feed/hashtag/{tag}?limit=N Посты по хэштегу

Детали + спецификация — docs/api/feed.md.

Docs / UI

  • GET /swaggerSwagger UI (рендерится через swagger-ui-dist).
  • GET /swagger/openapi.json — сырая OpenAPI 3.0 спека.
  • GET / — block-explorer HTML (выключается DCHAIN_DISABLE_UI=true).

CLI

client keygen               --out key.json
client --version            # → dchain-client vX.Y.Z (commit=... date=...)
client balance              --key key.json --node URL
client transfer             --key key.json --to <pub> --amount <µT> --node URL
client call-contract        --key key.json --contract native:username_registry \
                            --method register --arg alice --amount 10000 \
                            --node URL
client add-validator        --key key.json --target <pub> --cosigs pub:sig,pub:sig
client admit-sign           --key validator.json --target <candidate-pub>
client deploy-contract      --key key.json --wasm ./my.wasm --abi ./my_abi.json --node URL
# полный список — `client` без аргументов, или в docs/cli/README.md

Все флаги node — в docs/node/README.md, либо node --help.

Мониторинг

Prometheus endpoint /metrics на каждой ноде. Ключевые метрики:

dchain_blocks_total                  # committed blocks count
dchain_txs_total                     # tx count
dchain_tx_submit_accepted_total
dchain_tx_submit_rejected_total
dchain_ws_connections                # current WS sockets
dchain_peer_count_live               # live libp2p peer count
dchain_max_missed_blocks             # worst validator liveness gap
dchain_block_commit_seconds          # histogram of AddBlock time

Grafana + Prometheus поднимаются вместе с нодой через docker compose --profile monitor up -d (см. deploy/single/docker-compose.yml).

Тесты

# Unit + integration
go test ./...                       # blockchain + consensus + identity + relay + vm
go vet ./...                        # static checks

# End-to-end load (3-node dev cluster должен быть поднят):
docker compose up --build -d
go run ./cmd/loadtest \
  --node http://localhost:8081 \
  --funder testdata/node1.json \
  --clients 50 --duration 60s

Документация

Полный справочник в docs/ — см. docs/README.md как оглавление. Ключевые документы:

Документ О чём
docs/quickstart.md Три пути: локально, single-node prod, федерация
docs/architecture.md 4 слоя, consensus, storage, gas model
docs/update-system.md Versioning + update-check + semver guard
docs/api/README.md REST + WebSocket reference
docs/cli/README.md CLI команды и флаги
docs/node/README.md Запуск ноды (native + Docker)
docs/contracts/README.md Системные + WASM контракты
docs/development/README.md SDK для своих контрактов
deploy/single/README.md Single-node operator runbook
deploy/prod/README.md Multi-validator federation
deploy/UPDATE_STRATEGY.md Forward-compat design (4 слоя)
Description
No description provided
Readme 2.4 MiB
Languages
Go 49.9%
TypeScript 37.6%
JavaScript 4.9%
HTML 2.7%
CSS 2.2%
Other 2.7%