18 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 Как именно работает full resync
Full resync запускается только тогда, когда:
- локальная chain отстаёт и обычная докачка хвоста упирается в
bad_prev_hashилиbad_block_number; - либо высота цепочек одинаковая, но удалённая версия сильнее по правилу:
lastBlockNumber;fileSizeBytes;lastBlockHash.
Порядок действий:
- Ставится in-memory guard на
blockchainName. - Создаётся marker-file
<blockchainName>.resync_pending. - Обычный
AddBlockна эту chain временно получаетchain_resync_in_progress. - Вызывается атомарный SQL cleanup одной chain:
- уменьшаются чужие
likes_countиreplies_count; - удаляются локальные derived-state записи этой chain;
- удаляются
blocksиblockchain_stateэтой chain.
- уменьшаются чужие
- Удаляются файлы
<blockchainName>.bchи<blockchainName>.tmp_bch. - Локальная chain создаётся заново через
GetSyncUserProfileили через Solana import, еслиsync.importUserProfileFromPartner.enabled=false. - Chain replay-ится с
0черезGetBlockchainBlock. - Если всё прошло успешно, marker-file удаляется.
- Если на любом шаге произошёл сбой, marker-file остаётся на диске, и сервер добивает эту chain при следующем старте.
Важно:
- full resync не делает умный rollback по одному блоку;
- full resync не трогает DM-таблицы и
solana_users; - висячие cross-chain ссылки считаются допустимым поведением системы.
4.5 Как работает обычный AddBlock и его recovery
Обычная запись блока теперь тоже идёт через временные артефакты:
- собирается
<blockchainName>.tmp_bchкак полный кандидат на замену основного файла; - пишется маленький sidecar
<blockchainName>.write_checkсblockNumberиblockHash; - только после этого создаётся пустой marker
<blockchainName>.write_pending; - выполняется SQL-транзакция;
- после
committmp атомарно ставится на место основного.bch; - marker и sidecar удаляются.
На старте BlockchainTmpRecoveryOnStartup смотрит именно на эту пару:
- если
write_pendingесть, recovery проверяет sidecar и БД, а затем либо завершает swap, либо чистит временные файлы; - если
write_pendingнет, аtmp_bchилиwrite_checkостались, это мусор и он удаляется; resync_pendingсюда не относится, это отдельный recovery-поток.
4.6 Startup recovery по marker-file
При старте сервер идёт в таком порядке:
BlockchainTmpRecoveryOnStartupдля*.write_pendingи orphan*.tmp_bch/*.write_check;BlockchainResyncRecoveryOnStartupдля*.resync_pending;- только потом поднимается обычный сервер и запускается
PeriodicBlockchainSyncService.
Если marker-file существует:
- сервер не должен начинать обычную работу поверх этой chain;
- recovery снова выполняет cleanup и replay с нуля;
- если recovery не завершился, marker остаётся, и сервер не переходит к обычному режиму для этой chain.
4.7 Зачем понадобился GetSyncUserProfile
Изначально подготовка локальной цепочки делалась через Solana:
- из
blockchainNameизвлекалсяlogin; - сервер вызывал import пользователя из Solana PDA;
- по данным PDA локально создавались
solana_users + blockchain_state.
На практике это упёрлось в ограничение внешнего Solana RPC: при чистом старте и массовой подтяжке чужих цепочек сервер мог получать HTTP 429.
Поэтому добавлен отдельный обходной режим:
- настройка
sync.importUserProfileFromPartner.enabled=true - в этом режиме сервер не ходит в Solana RPC для создания локальной цепочки во время sync;
- вместо этого он запрашивает у сервера-партнёра
GetSyncUserProfileи создаёт локальную запись по данным партнёра. - если локальный
solana_usersуже существует, sync восстанавливает толькоblockchain_stateи не трогает identity-слой.
Это временная практическая заплатка, чтобы clean-start sync не зависел от rate limit внешнего Solana endpoint.
4.8 Что делает настройка 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 |
✅ Реализовано |
Обычный AddBlock через tmp_bch/write_check/write_pending |
✅ Реализовано |
| Межсерверный постоянный 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 соединения.
Следующие отдельные шаги после текущего этапа:
- отдельно проверить full-resync и startup-recovery на реальном тестовом прогоне после ручного удаления БД/файлов.
8.1 Практическая проверка на тестовом сервере
Проверка на t.shineup.me показала, что текущая схема действительно поднимает цепочку при старте:
- после рестарта сервер сначала проходит
BlockchainTmpRecovery; - затем обрабатывает
BlockchainResyncRecovery; - после этого сам догружает цепочку
aidartest-001сshineup.me; - итоговое состояние на тестовом сервере:
blockchain_state.last_block_number = 13blocksпоaidartest-001=14записей
Это подтверждает, что startup sync и full-resync flow работают в живом сценарии, а не только в коде.