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
This commit is contained in:
339
deploy/UPDATE_STRATEGY.md
Normal file
339
deploy/UPDATE_STRATEGY.md
Normal file
@@ -0,0 +1,339 @@
|
||||
# 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/<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.
|
||||
Reference in New Issue
Block a user