226 lines
16 KiB
Markdown
226 lines
16 KiB
Markdown
# Синхронизация блоков и DM между серверами SHiNE
|
||
|
||
Документ описывает архитектуру и протокол синхронизации данных между партнёрскими серверами SHiNE.
|
||
|
||
## 1. Зачем нужна синхронизация
|
||
|
||
Пользователи SHiNE могут быть «приписаны» к разным серверам.
|
||
Когда пользователь A (на сервере X) пишет пользователю B (на сервере Y):
|
||
|
||
1. Сервер X принимает сообщение;
|
||
2. Сервер X должен переслать DM-блок серверу Y;
|
||
3. Сервер Y сохраняет блок и доставляет в активные сессии пользователя B.
|
||
|
||
Аналогично, блоки пользовательского блокчейна (записи `AddBlock`) должны синхронизироваться,
|
||
чтобы любой партнёрский сервер мог отдать полную историю пользователя.
|
||
|
||
## 2. Список серверов синхронизации (`sync_servers`)
|
||
|
||
Каждый сервер регистрирует в своей Solana PDA список `sync_servers` —
|
||
логины SHiNE-аккаунтов партнёрских серверов, с которыми он синхронизируется.
|
||
|
||
- Список хранится в блоке `ServerProfileBlock` внутри `user_pda` сервера.
|
||
- Адрес каждого партнёрского сервера читается из его PDA на Solana.
|
||
- Синхронизация двусторонняя: оба сервера должны иметь друг друга в `sync_servers`.
|
||
|
||
## 3. Что синхронизируется
|
||
|
||
### 3.1 Личные сообщения (DM)
|
||
|
||
- Все DM-блоки форматов типов `1/2` (текст) и `3/4` (read-receipt).
|
||
- Сервер-отправитель: при получении пары блоков от клиента перенаправляет их серверу получателя.
|
||
- Сервер-получатель: сохраняет блоки в `signed_messages_v2`, доставляет в активные сессии.
|
||
- Дедупликация по уникальному `message_key = from|to|timeMs|nonce|type`.
|
||
|
||
### 3.2 Блоки пользовательского блокчейна
|
||
|
||
- Все блоки `AddBlock` пользователей, зарегистрированных на сервере или синхронизирующихся через него.
|
||
- Синхронизируются в обе стороны между всеми партнёрами из `sync_servers`.
|
||
- Порядок блоков сохраняется (по глобальному номеру блока и хэшу).
|
||
- Дедупликация по глобальному номеру блока и хэшу.
|
||
|
||
## 4. Текущая реализованная схема
|
||
|
||
На текущем этапе сервер уже умеет базовую межсерверную синхронизацию пользовательских блокчейнов.
|
||
|
||
### 4.1 Что уже сделано
|
||
|
||
1. При старте сервер читает свой `server.SHiNE.login`.
|
||
2. По этому логину он загружает из Solana свою server PDA.
|
||
3. Из неё вытаскивает список `sync_servers`.
|
||
4. Для каждого логина партнёра сервер читает его PDA и сохраняет локально:
|
||
- `login`
|
||
- `server_address`
|
||
5. После этого:
|
||
- новые локальные `AddBlock` рассылаются партнёрам в фоне;
|
||
- при старте запускается periodic sync;
|
||
- periodic sync повторяется каждые `12` часов после старта.
|
||
|
||
### 4.2 Какие server-to-server API уже используются
|
||
|
||
- `ListBlockchainHeads` — список heads всех локальных цепочек партнёра;
|
||
- `GetBlockchainBlock` — чтение одного конкретного блока партнёра;
|
||
- `GetSyncUserProfile` — минимальный профиль пользователя для локального создания `solana_users + blockchain_state` без обращения в Solana RPC.
|
||
|
||
### 4.3 Как сейчас работает periodic sync
|
||
|
||
Для каждого сервера из локальной таблицы `sync_servers`:
|
||
|
||
1. запрашивается `ListBlockchainHeads`;
|
||
2. для каждой удалённой цепочки сравниваются:
|
||
- `lastBlockNumber`
|
||
- `lastBlockHash`
|
||
- локальное состояние;
|
||
3. если локальная цепочка слабее, сервер по одному блоку вызывает `GetBlockchainBlock`;
|
||
4. каждый скачанный блок локально применяется через существующий `AddBlock`;
|
||
5. если у сервера ещё нет локальной записи пользователя/цепочки, перед этим подготавливается локальный `solana_users + blockchain_state`.
|
||
6. если во время replay обнаруживается рассинхрон или на одинаковой высоте удалённая цепочка сильнее, запускается полный resync:
|
||
- цепочка помечается in-memory как `resync in progress`;
|
||
- создаётся marker-file в `data/`;
|
||
- в одной SQL-транзакции очищаются локальные данные цепочки и корректируются чужие счётчики;
|
||
- удаляются `.bch` и `.tmp_bch`;
|
||
- цепочка подтягивается заново с `0` через `GetBlockchainBlock`.
|
||
- обычный `AddBlock` на эту цепочку в этот момент возвращает `chain_resync_in_progress`.
|
||
|
||
### 4.4 Как именно работает full resync
|
||
|
||
Full resync запускается только тогда, когда:
|
||
|
||
- локальная chain отстаёт и обычная докачка хвоста упирается в `bad_prev_hash` или `bad_block_number`;
|
||
- либо высота цепочек одинаковая, но удалённая версия сильнее по правилу:
|
||
- `lastBlockNumber`;
|
||
- `fileSizeBytes`;
|
||
- `lastBlockHash`.
|
||
|
||
Порядок действий:
|
||
|
||
1. Ставится in-memory guard на `blockchainName`.
|
||
2. Создаётся marker-file `<blockchainName>.resync_pending`.
|
||
3. Обычный `AddBlock` на эту chain временно получает `chain_resync_in_progress`.
|
||
4. Вызывается атомарный SQL cleanup одной chain:
|
||
- уменьшаются чужие `likes_count` и `replies_count`;
|
||
- удаляются локальные derived-state записи этой chain;
|
||
- удаляются `blocks` и `blockchain_state` этой chain.
|
||
5. Удаляются файлы `<blockchainName>.bch` и `<blockchainName>.tmp_bch`.
|
||
6. Локальная chain создаётся заново через `GetSyncUserProfile` или через Solana import, если `sync.importUserProfileFromPartner.enabled=false`.
|
||
7. Chain replay-ится с `0` через `GetBlockchainBlock`.
|
||
8. Если всё прошло успешно, marker-file удаляется.
|
||
9. Если на любом шаге произошёл сбой, marker-file остаётся на диске, и сервер добивает эту chain при следующем старте.
|
||
|
||
Важно:
|
||
|
||
- full resync не делает умный rollback по одному блоку;
|
||
- full resync не трогает DM-таблицы и `solana_users`;
|
||
- висячие cross-chain ссылки считаются допустимым поведением системы.
|
||
|
||
### 4.5 Startup recovery по marker-file
|
||
|
||
При старте сервер идёт в таком порядке:
|
||
|
||
1. `BlockchainTmpRecoveryOnStartup` для `*.tmp_bch`;
|
||
2. `BlockchainResyncRecoveryOnStartup` для `*.resync_pending`;
|
||
3. только потом поднимается обычный сервер и запускается `PeriodicBlockchainSyncService`.
|
||
|
||
Если marker-file существует:
|
||
|
||
- сервер не должен начинать обычную работу поверх этой chain;
|
||
- recovery снова выполняет cleanup и replay с нуля;
|
||
- если recovery не завершился, marker остаётся, и сервер не переходит к обычному режиму для этой chain.
|
||
|
||
### 4.6 Зачем понадобился `GetSyncUserProfile`
|
||
|
||
Изначально подготовка локальной цепочки делалась через Solana:
|
||
|
||
- из `blockchainName` извлекался `login`;
|
||
- сервер вызывал import пользователя из Solana PDA;
|
||
- по данным PDA локально создавались `solana_users + blockchain_state`.
|
||
|
||
На практике это упёрлось в ограничение внешнего Solana RPC: при чистом старте и массовой подтяжке чужих цепочек сервер мог получать `HTTP 429`.
|
||
|
||
Поэтому добавлен отдельный обходной режим:
|
||
|
||
- настройка `sync.importUserProfileFromPartner.enabled=true`
|
||
- в этом режиме сервер **не ходит в Solana RPC** для создания локальной цепочки во время sync;
|
||
- вместо этого он запрашивает у сервера-партнёра `GetSyncUserProfile` и создаёт локальную запись по данным партнёра.
|
||
|
||
Это временная практическая заплатка, чтобы clean-start sync не зависел от rate limit внешнего Solana endpoint.
|
||
|
||
### 4.7 Что делает настройка `sync.importUserProfileFromPartner.enabled`
|
||
|
||
- `false` — стандартный режим, подготовка локального пользователя идёт через Solana PDA;
|
||
- `true` — sync-режим обхода Solana, локальный пользователь создаётся по server-to-server `GetSyncUserProfile`.
|
||
|
||
Настройка влияет именно на этап подготовки отсутствующей локальной цепочки во время periodic sync.
|
||
|
||
## 5. Целевой протокол следующего этапа
|
||
|
||
### 5.1 Межсерверное соединение
|
||
|
||
- Серверы устанавливают постоянное WebSocket-соединение друг с другом.
|
||
- Адрес партнёра определяется по `server_address` из его Solana PDA.
|
||
- Аутентификация: подпись Ed25519 корневым ключом сервера (`root_key` из PDA).
|
||
- При разрыве — переподключение с экспоненциальным backoff.
|
||
|
||
### 5.2 Доставка новых данных (push)
|
||
|
||
- При получении нового блока или DM сервер немедленно пушит его всем подключённым партнёрам.
|
||
- Партнёр подтверждает приём (ACK). Без ACK — повтор с backoff.
|
||
|
||
### 5.3 Начальная синхронизация (backfill)
|
||
|
||
- При первом подключении к партнёру серверы обмениваются «курсорами» состояния:
|
||
последний глобальный номер блока, последний известный DM-ключ.
|
||
- Сервер с более полной историей досылает недостающее партнёру.
|
||
|
||
### 5.4 Разрешение конфликтов
|
||
|
||
- Блоки пользовательского блокчейна: порядок определяется глобальным номером блока.
|
||
Конфликтующие ветки (fork) разрешаются по правилам `AddBlock` (см. `Dev_Docs/Blockchain/README.md`).
|
||
- DM: конфликтов нет, `message_key` уникален.
|
||
|
||
## 6. Маршрутизация DM между серверами
|
||
|
||
При отправке DM от пользователя A к пользователю B:
|
||
|
||
1. Клиент A отправляет пару блоков на свой сервер X.
|
||
2. Сервер X определяет, на каком сервере зарегистрирован пользователь B.
|
||
- Сначала проверяет локально (если B зарегистрирован на X).
|
||
- Иначе читает PDA пользователя B из Solana и смотрит `access_servers`.
|
||
- Выбирает первый доступный сервер из `access_servers` и перенаправляет туда DM.
|
||
3. Сервер Y (из `access_servers` B) сохраняет и доставляет блоки.
|
||
|
||
Кэш адресов серверов: обновляется раз в сессию (при ошибке соединения).
|
||
|
||
## 7. Безопасность
|
||
|
||
- Все блоки подписаны ключами пользователя на клиенте — сервер не может подделать содержимое.
|
||
- Серверы не расшифровывают DM-контент (шифрование — задача следующего этапа).
|
||
- При синхронизации каждый блок проходит валидацию подписи на принимающем сервере.
|
||
|
||
## 8. Статус реализации
|
||
|
||
| Компонент | Статус |
|
||
|-----------|--------|
|
||
| Регистрация серверной PDA в Solana | ✅ Реализовано |
|
||
| Чтение `sync_servers` из PDA | ✅ Реализовано |
|
||
| Локальная таблица `sync_servers` | ✅ Реализовано |
|
||
| Публичный `ListBlockchainHeads` | ✅ Реализовано |
|
||
| Публичный `GetBlockchainBlock` | ✅ Реализовано |
|
||
| Публичный `GetSyncUserProfile` | ✅ Реализовано |
|
||
| Плановый blockchain sync при старте + каждые 12 часов | ✅ Реализовано |
|
||
| Обход Solana RPC через `sync.importUserProfileFromPartner.enabled` | ✅ Реализовано |
|
||
| Межсерверный постоянный WebSocket-канал | Нужна реализация |
|
||
| Push новых DM партнёрам | Нужна реализация |
|
||
| Push блоков блокчейна партнёрам | ✅ Реализована базовая one-shot версия |
|
||
| Periodic backfill отсутствующего хвоста | ✅ Реализовано |
|
||
| Разрешение рассинхрона / divergence | ✅ Реализована базовая full-resync схема во время periodic sync |
|
||
| Startup recovery по `*.resync_pending` marker-file | ✅ Реализовано |
|
||
| Маршрутизация DM через access_servers | Нужна реализация (заглушка) |
|
||
|
||
Текущая версия сервера уже умеет базовую синхронизацию блокчейнов между партнёрами.
|
||
Не реализованы ещё DM-sync и постоянные server-to-server соединения.
|
||
|
||
Следующие отдельные шаги после текущего этапа:
|
||
- вернуть обычному `AddBlock` настоящую `tmp_bch`-схему записи и recovery при резком рестарте.
|
||
- отдельно проверить full-resync и startup-recovery на реальном тестовом прогоне после ручного удаления БД/файлов.
|