13 KiB
Синхронизация блоков и DM между серверами SHiNE
Документ описывает архитектуру и протокол синхронизации данных между партнёрскими серверами SHiNE.
1. Зачем нужна синхронизация
Пользователи SHiNE могут быть «приписаны» к разным серверам. Когда пользователь A (на сервере X) пишет пользователю B (на сервере Y):
- Сервер X принимает сообщение;
- Сервер X должен переслать DM-блок серверу Y;
- Сервер 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 Что уже сделано
- При старте сервер читает свой
server.SHiNE.login. - По этому логину он загружает из Solana свою server PDA.
- Из неё вытаскивает список
sync_servers. - Для каждого логина партнёра сервер читает его PDA и сохраняет локально:
loginserver_address
- После этого:
- новые локальные
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:
- запрашивается
ListBlockchainHeads; - для каждой удалённой цепочки сравниваются:
lastBlockNumberlastBlockHash- локальное состояние;
- если локальная цепочка слабее, сервер по одному блоку вызывает
GetBlockchainBlock; - каждый скачанный блок локально применяется через существующий
AddBlock; - если у сервера ещё нет локальной записи пользователя/цепочки, перед этим подготавливается локальный
solana_users + blockchain_state. - если во время replay обнаруживается рассинхрон или на одинаковой высоте удалённая цепочка сильнее, запускается полный resync:
- цепочка помечается in-memory как
resync in progress; - создаётся marker-file в
data/; - в одной SQL-транзакции очищаются локальные данные цепочки и корректируются чужие счётчики;
- удаляются
.bchи.tmp_bch; - цепочка подтягивается заново с
0черезGetBlockchainBlock. - обычный
AddBlockна эту цепочку в этот момент возвращаетchain_resync_in_progress.
- цепочка помечается in-memory как
4.4 Зачем понадобился 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.5 Что делает настройка sync.importUserProfileFromPartner.enabled
false— стандартный режим, подготовка локального пользователя идёт через Solana PDA;true— sync-режим обхода Solana, локальный пользователь создаётся по server-to-serverGetSyncUserProfile.
Настройка влияет именно на этап подготовки отсутствующей локальной цепочки во время 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:
- Клиент A отправляет пару блоков на свой сервер X.
- Сервер X определяет, на каком сервере зарегистрирован пользователь B.
- Сначала проверяет локально (если B зарегистрирован на X).
- Иначе читает PDA пользователя B из Solana и смотрит
access_servers. - Выбирает первый доступный сервер из
access_serversи перенаправляет туда DM.
- Сервер Y (из
access_serversB) сохраняет и доставляет блоки.
Кэш адресов серверов: обновляется раз в сессию (при ошибке соединения).
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 |
| Маршрутизация DM через access_servers | Нужна реализация (заглушка) |
Текущая версия сервера уже умеет базовую синхронизацию блокчейнов между партнёрами. Не реализованы ещё DM-sync, постоянные server-to-server соединения и recovery при старте по marker-file для resync.
Следующие отдельные шаги после текущего этапа:
- добавить startup recovery по marker-file для resync-цепочек;
- вернуть обычному
AddBlockнастоящуюtmp_bch-схему записи и recovery при резком рестарте.