# 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** который дергает этот скрипт: ```bash #!/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 таймер ```ini # /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` с тремя нодами — там эквивалент выглядит как: ```bash 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`: ```json { "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 ```go // 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) Правила работы с префиксами: ```go const ( prefixTx = "tx:" prefixChannel = "chan:" prefixSchemaVer = "schema:v" // ← meta-ключ, хранит текущую версию схемы ) ``` При старте: ```go 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//`. (§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.