Two bugs reported by the user:
1. After accepting a contact request on the desktop, the requester's
"Send message" call errored with "no encryption key published" for
the newly-accepted contact. Root cause: desktop never ran the
device-registry bootstrap (mobile does it from _layout.tsx on
sign-in) — so the desktop's X25519 pub was never published via
LINK_DEVICE, and resolveRecipientKeys returned an empty list.
2. On the accepting device, the new chat didn't appear in Messages
after tapping Accept — accept wrote the contact to store + disk
but didn't switch sections, so the user was stuck in Contacts
watching nothing happen.
Fixes:
* hooks/useDeviceBootstrap — direct port of mobile's _layout.tsx
bootstrap effect. On every sign-in:
- fetchDevices(master) → if our X25519 is listed, mark local
registered flag.
- not listed + was registered before → REVOKED → wipe state +
bounce to Welcome.
- not listed + never registered → submit LINK_DEVICE. Tx may
bounce if balance is zero; next launch retries.
Mounted from App.tsx so it runs once per authenticated session.
* RequestsList.accept — after submitting ACCEPT_CONTACT, check if
OUR X25519 is in the on-chain registry. If not, submit LINK_DEVICE
immediately (balance is now covered by the contact fee the peer
paid us). This closes the window where the peer couldn't encrypt
to us because our key wasn't published yet.
Also: after a successful accept, setSection('messages') +
setActiveChat(requester_pub), matching mobile's
router.replace('/chats/<pub>') flow.
* Conversation.send — nicer error copy when
resolveRecipientKeys returns []. Was: "recipient has no
encryption key published". Now: actionable text asking the peer
to re-open their app so the LINK_DEVICE tx commits.
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
Содержание
- Быстрый старт
- Поднятие ноды — пошагово
- Продакшен деплой
- Архитектура
- REST / WebSocket API
- CLI
- Мониторинг
- Тесты
- Документация
Быстрый старт
Одна нода в 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/ws— WebSocket (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 /swagger— Swagger 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 слоя) |