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
16 KiB
DChain node — update & seamless-upgrade strategy
Этот документ отвечает на два вопроса:
- Как оператор ноды обновляет её от git-сервера (pull → build → restart) без простоя и без потери данных.
- Как мы сохраняем бесшовную совместимость между версиями ноды, чтобы не пришлось "ломать" старых клиентов, чужие ноды или собственную историю.
Читается в связке с deploy/single/README.md (операционный runbook) и
CHANGELOG.md (что уже зашипплено).
1. Слои, которые надо развести
| Слой | Что ломает совместимость | Кто страдает | Как закрыто |
|---|---|---|---|
| Wire-протокол | gossipsub topic name, tx encoding, PBFT message format | P2P-сеть целиком | §3. Versioned topics |
| HTTP/WS API | эндпоинт меняет схему, WS op исчезает | Клиенты (mobile, web) | §4. API versioning |
| Chain state | новый EventType в блоке, новое поле в TxRecord | Joiner'ы, валидаторы | §5. Chain upgrade |
| Storage layout | BadgerDB prefix переименован, ключи перемешались | Сам бинарь при старте | §6. DB migrations |
| Docker image | пересобрать образ, поменять флаги | Только локально | §2. Rolling restart |
Главный принцип: любое изменение проходит как минимум два релиза — сначала "понимаем оба формата, пишем новый", потом "не умеем старый". Между ними время, за которое оператор обновляется.
2. Rolling-restart от git-сервера (single-node)
2.1. Скрипт deploy/single/update.sh
Оператор ставит один cron/systemd-timer который дергает этот скрипт:
#!/usr/bin/env bash
# deploy/single/update.sh — pull-and-restart update for a single DChain node.
# Safe to run unattended: no-op if git HEAD didn't move.
set -euo pipefail
REPO_DIR="${REPO_DIR:-/opt/dchain}"
IMAGE_TAG="${IMAGE_TAG:-dchain-node-slim}"
CONTAINER="${CONTAINER:-dchain_node}"
cd "$REPO_DIR"
git fetch --quiet origin main
local=$(git rev-parse HEAD)
remote=$(git rev-parse origin/main)
[[ "$local" = "$remote" ]] && { echo "up to date: $local"; exit 0; }
echo "updating $local → $remote"
# 1. rebuild
docker build --quiet -t "$IMAGE_TAG:$remote" -t "$IMAGE_TAG:latest" \
-f deploy/prod/Dockerfile.slim .
# 2. smoke-test new image BEFORE killing the running one
docker run --rm --entrypoint /usr/local/bin/node "$IMAGE_TAG:$remote" --version \
>/dev/null || { echo "new image fails smoke test"; exit 1; }
# 3. checkpoint the DB (cheap cp-on-write snapshot via badger)
curl -fs "http://127.0.0.1:8080/api/admin/checkpoint" \
-H "Authorization: Bearer $DCHAIN_API_TOKEN" \
|| echo "checkpoint failed, continuing anyway"
# 4. stop-start with the SAME volume + env
git log -1 --pretty='update: %h %s' > .last-update
docker compose -f deploy/single/docker-compose.yml up -d --force-recreate node
# 5. wait for health
for i in {1..30}; do
curl -fsS http://127.0.0.1:8080/api/netstats >/dev/null && { echo ok; exit 0; }
sleep 2
done
echo "new container did not become healthy"
docker logs "$CONTAINER" | tail -40
exit 1
2.2. systemd таймер
# /etc/systemd/system/dchain-update.service
[Unit]
Description=DChain node pull-and-restart
[Service]
Type=oneshot
EnvironmentFile=/opt/dchain/deploy/single/node.env
ExecStart=/opt/dchain/deploy/single/update.sh
# /etc/systemd/system/dchain-update.timer
[Unit]
Description=Pull DChain updates hourly
[Timer]
OnCalendar=hourly
RandomizedDelaySec=15min
Persistent=true
[Install]
WantedBy=timers.target
RandomizedDelaySec=15min — чтобы куча нод на одной сети не перезапускалась
одновременно, иначе на момент обновления PBFT quorum может упасть.
2.3. Даунтайм одной ноды
| Шаг | Время | Можно ли слать tx? |
|---|---|---|
| docker build | 30-90s | да (старая ещё работает) |
| docker compose up | 2-5s | нет (transition) |
| DB open + replay | 1-3s | нет |
| healthy | — | да |
Итого ~5-8 секунд простоя на одну ноду. Клиент (React Native) уже
реконнектится по WS автоматически (см. client-app/lib/ws.ts — retry loop,
max 30s backoff).
2.4. Multi-node rolling (для будущего кластера)
Когда появится 3+ валидаторов: update скрипт должен обновлять по одному с
паузой между ними больше, чем health-check interval. В deploy/prod/ есть
docker-compose.yml с тремя нодами — там эквивалент выглядит как:
for n in node1 node2 node3; do
docker compose up -d --force-recreate "$n"
for i in {1..30}; do
curl -fs "http://127.0.0.1:808${n: -1}/api/netstats" >/dev/null && break
sleep 2
done
done
Пока в сети 2/3 валидатора живы, PBFT quorum не падает и блоки продолжают
коммититься. Единственная нода, которая обновляется, пропустит 1-2 блока и
догонит их через gossip gap-fill (уже работает, см. p2p/host.go → GetBlocks).
3. Wire-протокол: versioned topics
Текущие gossipsub-топики:
dchain/tx/v1
dchain/blocks/v1
dchain/relay/v1
/v1 суффикс — это не формальность, это рельса под миграцию. Когда
появится несовместимое изменение (напр. новый PBFT round format):
- Релиз N: нода подписана на ОБА топика
dchain/blocks/v1иdchain/blocks/v2. Публикует в v2, читает из обоих. - Релиз N+1 (после того, как оператор видит в
/api/netstatsчто все 100% пиров ≥ N): нода перестаёт читать v1. - Релиз N+2: v1 удаляется из кода.
Между N и N+2 должно пройти минимум 30 дней. За это время у каждого оператора хоть раз сработает auto-update.
4. API versioning
Уже есть:
/api/*— v1, "explorer API", stable contract/v2/chain/*— специальная секция для tonapi-подобных клиентов (tonapi совместимость)
Правило на будущее:
- Только добавляем поля к существующим ответам. JSON-клиент не падает от
незнакомого поля — Go unmarshal игнорирует, TypeScript через
unknownкаст тоже. Никогда не переименовываем и не удаляем. - Если нужна breaking-change — новый префикс. Например, если CreateChannelPayload
меняет формат, появляется
/v2/channels/*. Старый/api/channels/*сохраняется как read-only adapter поверх нового стораджа. - Deprecation header: когда старый эндпоинт переведён на адаптер, добавить
Warning: 299 - "use /v2/channels/* instead, this will be removed 2026-06-01". - Клиент сам определяет версию через
/api/well-known-version:Клиент в{ "node_version": "0.5.0", "protocol_version": 3, "features": ["channels_v1", "fan_out"] }client-app/lib/api.tsкеширует ответ и знает, что можно звать. Уже есть/api/well-known-contractsкак прецедент;/api/well-known-versionдобавляется одной функцией.
Клиентская сторона — graceful degradation:
- WebSocket: если op
submit_txвернул{error: "unknown_op"}, fallback на HTTP POST /api/tx. - HTTP: fetch'и обёрнуты в try/catch в
api.ts, 404 на новом эндпоинте → скрыть фичу в UI (feature-flag), не падать. - Chain-ID check: уже есть (
client-app/lib/api.ts→networkInfo()), если нода сменила chain_id — клиент очищает кеш и пересинкается.
5. Chain state upgrade
Самый болезненный слой: если блок N+1 содержит EventType, который старая нода не умеет обрабатывать, она отклонит весь блок и отвалится от консенсуса.
5.1. Strict forward-compatibility правила для EventType
// ApplyTx в blockchain/chain.go
switch ev.Type {
case EventTransfer: ...
case EventRegisterRelay: ...
case EventCreateChannel: ...
// ...
case EventFutureFeatureWeDontHave:
// ← НЕ возвращать error! ЭТО крашнет валидатор на своём же блоке
// иначе.
// Правило: неизвестный event type === no-op + warn. Tx включается в блок,
// fee списывается, результат = ничего не изменилось.
chain.log.Warn("unknown event type", "type", ev.Type, "tx", tx.ID)
return nil
}
Проверить: сейчас ApplyTx в blockchain/chain.go падает на unknown event.
Это приоритетный fix для seamless — добавить в план.
5.2. Feature activation flags
Новый EventType добавляется в два этапа:
- Release A: бинарь умеет
EventChannelBan, но не пропускает его в мемпул, пока не увидит в chain state записьfeature:channel_ban:enabled. Эту запись создаёт одна "activation tx" от валидаторов (multi-sig). - Release B (через 30+ дней): операторы, у которых автопуллится, получили Release A. Один валидатор подаёт activation tx — она пишет в state, все остальные validate её, ОК.
- С этого момента
EventChannelBanлегален. Старые ноды (кто не обновился) отклонят activation tx → отвалятся от консенсуса. Это сознательно: они и так не понимают новый event, лучше явная ошибка "обновись", чем silent divergence.
Прототип в blockchain/types.go уже есть — chain.GovernanceContract может
хранить feature flags. Нужен конкретный helper chain.FeatureEnabled(name).
5.3. Genesis hash pin
Новая нода при --join скачивает /api/network-info, читает genesis_hash,
сравнивает со своим (пустым, т.к. чистый старт). Если в сети уже есть другой
genesis — ошибка FATAL: genesis hash mismatch. Это защита от случайного
фарка при опечатке в DCHAIN_JOIN. Работает сейчас, не трогать.
6. DB migrations (BadgerDB)
Правила работы с префиксами:
const (
prefixTx = "tx:"
prefixChannel = "chan:"
prefixSchemaVer = "schema:v" // ← meta-ключ, хранит текущую версию схемы
)
При старте:
cur := chain.ReadSchemaVersion() // default 0 если ключ отсутствует
for cur < TargetSchemaVersion {
switch cur {
case 0:
// migration 0→1: rename prefix "member:" → "chan_mem:"
migrate_0_to_1(db)
case 1:
// migration 1→2: add x25519_pub column to IdentityInfo
migrate_1_to_2(db)
}
cur++
chain.WriteSchemaVersion(cur)
}
Свойства миграции:
- Идемпотентна: если упала посередине, повторный старт доделает.
- Однонаправлена: downgrade → надо восстанавливать из backup. Это OK, документируется.
- Бэкап перед миграцией:
update.shиз §2.1 делает/api/admin/checkpointдо перезапуска. (Этот endpoint надо ещё реализовать — сейчас его нет.) - Первая миграция, которую надо сделать — завести сам mechanism, даже если
TargetSchemaVersion = 0. Чтобы следующая breaking-change могла им воспользоваться.
7. Что сделать сейчас, чтобы "не пришлось ничего ломать в будущем"
Минимальный чек-лист, отсортирован по приоритету:
P0 (до следующего release):
ApplyTx: unknown EventType → warn + no-op, НЕ error. (§5.1)/api/well-known-versionendpoint (§4). Тривиально, 20 строк.- Schema version meta-ключ в BadgerDB, даже если
current = 0. (§6) deploy/single/update.sh+ systemd timer примеры. (§2)
P1 (до 1.0):
chain.FeatureEnabled(name)helper + документ activation flow. (§5.2)/api/admin/checkpointendpoint (за token-guard), делаетdb.Flatten+ создаёт snapshot в/data/snapshots/<timestamp>/. (§2.1)- Deprecation-header механизм в HTTP middleware. (§4)
- CI smoke-test: "новый бинарь поверх старого volume" — проверяет что миграции не ломают данные.
P2 (nice-to-have):
- Multi-version e2e test в
cmd/loadtest: два процесса на разных HEAD, убедиться что они в консенсусе. go-blockchain/pkg/migrate/отдельный пакет с registry migrations.
8. Короткий ответ на вопрос
надо подумать на счёт синхронизации и обновления ноды с гит сервера, а так же бесшовности, чтобы не пришлось ничего ломать в будущем
- Синхронизация с git:
deploy/single/update.sh+ systemd timer раз в час, ~5-8 секунд даунтайма на single-node. - Бесшовность: 4 слоя, каждый со своим правилом расширения без ломания — versioned topics, additive-only API, feature-flag activation для новых EventType, schema-versioned БД.
- P0-тикеты выше (4 штуки, маленькие) закрывают "семплинг worst case": unknown event как no-op, version endpoint, schema-version key в БД, update-скрипт. Этого достаточно чтобы следующие 3-5 релизов прошли без breaking-change.