Files
dchain/deploy/UPDATE_STRATEGY.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

16 KiB
Raw Permalink Blame History

DChain node — update & seamless-upgrade strategy

Этот документ отвечает на два вопроса:

  1. Как оператор ноды обновляет её от git-сервера (pull → build → restart) без простоя и без потери данных.
  2. Как мы сохраняем бесшовную совместимость между версиями ноды, чтобы не пришлось "ломать" старых клиентов, чужие ноды или собственную историю.

Читается в связке с 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):

  1. Релиз N: нода подписана на ОБА топика dchain/blocks/v1 и dchain/blocks/v2. Публикует в v2, читает из обоих.
  2. Релиз N+1 (после того, как оператор видит в /api/netstats что все 100% пиров ≥ N): нода перестаёт читать v1.
  3. Релиз N+2: v1 удаляется из кода.

Между N и N+2 должно пройти минимум 30 дней. За это время у каждого оператора хоть раз сработает auto-update.


4. API versioning

Уже есть:

  • /api/* — v1, "explorer API", stable contract
  • /v2/chain/* — специальная секция для tonapi-подобных клиентов (tonapi совместимость)

Правило на будущее:

  1. Только добавляем поля к существующим ответам. JSON-клиент не падает от незнакомого поля — Go unmarshal игнорирует, TypeScript через unknown каст тоже. Никогда не переименовываем и не удаляем.
  2. Если нужна breaking-change — новый префикс. Например, если CreateChannelPayload меняет формат, появляется /v2/channels/*. Старый /api/channels/* сохраняется как read-only adapter поверх нового стораджа.
  3. Deprecation header: когда старый эндпоинт переведён на адаптер, добавить Warning: 299 - "use /v2/channels/* instead, this will be removed 2026-06-01".
  4. Клиент сам определяет версию через /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.tsnetworkInfo()), если нода сменила 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 добавляется в два этапа:

  1. Release A: бинарь умеет EventChannelBan, но не пропускает его в мемпул, пока не увидит в chain state запись feature:channel_ban:enabled. Эту запись создаёт одна "activation tx" от валидаторов (multi-sig).
  2. Release B (через 30+ дней): операторы, у которых автопуллится, получили Release A. Один валидатор подаёт activation tx — она пишет в state, все остальные validate её, ОК.
  3. С этого момента 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-version endpoint (§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/checkpoint endpoint (за 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. Короткий ответ на вопрос

надо подумать на счёт синхронизации и обновления ноды с гит сервера, а так же бесшовности, чтобы не пришлось ничего ломать в будущем

  1. Синхронизация с git: deploy/single/update.sh + systemd timer раз в час, ~5-8 секунд даунтайма на single-node.
  2. Бесшовность: 4 слоя, каждый со своим правилом расширения без ломания — versioned topics, additive-only API, feature-flag activation для новых EventType, schema-versioned БД.
  3. P0-тикеты выше (4 штуки, маленькие) закрывают "семплинг worst case": unknown event как no-op, version endpoint, schema-version key в БД, update-скрипт. Этого достаточно чтобы следующие 3-5 релизов прошли без breaking-change.