21 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-аккаунтов партнёрских серверов, с которыми он синхронизируется.
Важно: в текущей архитектуре у пользователя одновременно может быть не более
двух sync/access-серверов. Это ограничение считается обязательным для runtime-логики
synced и пользовательских курсоров.
- Список хранится в блоке
ServerProfileBlockвнутриuser_pdaсервера. - Адрес каждого партнёрского сервера читается из его PDA на Solana.
- Синхронизация двусторонняя: оба сервера должны иметь друг друга в
sync_servers.
3. Что синхронизируется
3.1 Личные сообщения (DM)
- Все DM-блоки форматов типов
1/2(текст) и3/4(read-receipt). - Сервер-отправитель: сохраняет пару и ставит асинхронную delivery-задачу.
- Сервер-получатель: сохраняет входящий блок в
signed_messages, затем доставляет его активным сессиям. - Дедупликация по уникальному
message_key = from|to|timeMs|nonce|type. - Репликация между двумя access-серверами пользователя работает через
dm_sync_outbox.synced, без постоянного time-cursor. - Полная актуальная схема:
docs/Personal_Messages/Доставка_и_синхронизация_DM.md.
3.2 Блоки пользовательского блокчейна
- Все блоки
AddBlockпользователей, зарегистрированных на сервере или синхронизирующихся через него. - Синхронизируются в обе стороны между всеми партнёрами из
sync_servers. - Порядок блоков сохраняется (по глобальному номеру блока и хэшу).
- Дедупликация по глобальному номеру блока и хэшу.
3.3 Пользовательские настройки
- Отдельная таблица
user_settings. - Синхронизируются технические настройки пользователя, включая курсор прочитанности каналов.
- Для текущего UI-кейса хранится
setting_type = 1иsetting_key = ownerBlockchainName/channelName. - Синхронизация идёт с учётом
time_msи флагаsynced.
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— минимальный профиль пользователя для локального создания runtime-проекции пользователя иblockchain_stateбез обращения в Solana RPC.
4.3 Как сейчас работает periodic sync
Для каждого сервера из локальной таблицы sync_servers:
- запрашивается
ListBlockchainHeads; - для каждой удалённой цепочки сравниваются:
lastBlockNumberlastBlockHash- локальное состояние;
- если локальная цепочка слабее, сервер по одному блоку вызывает
GetBlockchainBlock; - каждый скачанный блок локально применяется через существующий
AddBlock; - если у сервера ещё нет локальной записи пользователя/цепочки, перед этим подготавливается локальная runtime-проекция пользователя и
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-таблицы и current 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 локально создавались runtime-проекция пользователя и
blockchain_state.
На практике это упёрлось в ограничение внешнего Solana RPC: при чистом старте и массовой подтяжке чужих цепочек сервер мог получать HTTP 429.
Поэтому добавлен отдельный обходной режим:
- настройка
sync.importUserProfileFromPartner.enabled=true - в этом режиме сервер не ходит в Solana RPC для создания локальной цепочки во время sync;
- вместо этого он запрашивает у сервера-партнёра
GetSyncUserProfileи создаёт локальную запись по данным партнёра. - если локальная runtime-проекция пользователя уже существует, sync восстанавливает только
blockchain_stateи не трогает user-layer.
Это временная практическая заплатка, чтобы clean-start sync не зависел от rate limit внешнего Solana endpoint.
4.8 Что делает настройка sync.importUserProfileFromPartner.enabled
false— стандартный режим, подготовка локального пользователя идёт через Solana PDA;true— sync-режим обхода Solana, локальный пользователь создаётся по server-to-serverGetSyncUserProfile.
Настройка влияет именно на этап подготовки отсутствующей локальной цепочки во время periodic sync.
5. Возможное развитие server-to-server транспорта
Этот раздел не описывает текущую DM-доставку. DM уже использует короткие one-shot WebSocket-вызовы, indexed outbox и ACK. Ниже остаётся возможное развитие постоянного транспорта и server-auth.
5.1 Межсерверное соединение
- Серверы устанавливают постоянное WebSocket-соединение друг с другом.
- Адрес партнёра определяется по
server_addressиз его Solana PDA. - Аутентификация: подпись Ed25519 корневым ключом сервера (
root_keyиз PDA). - При разрыве — переподключение с экспоненциальным backoff.
5.2 Доставка новых данных (push)
- При получении нового блока сервер может немедленно пушить его всем подключённым партнёрам.
- Партнёр подтверждает приём (ACK). Без ACK — повтор с backoff.
- DM использует отдельное расписание, описанное в
docs/Personal_Messages/Доставка_и_синхронизация_DM.md.
5.3 Начальная синхронизация (backfill)
- При первом подключении к партнёру серверы могут обмениваться курсорами состояния блокчейнов.
- Сервер с более полной историей досылает недостающее партнёру.
- DM time-cursor удалён: новый peer получает историю после сброса
dm_sync_outbox.synced=false.
5.4 Разрешение конфликтов
- Блоки пользовательского блокчейна: порядок определяется глобальным номером блока.
Конфликтующие ветки (fork) разрешаются по правилам
AddBlock(см.docs/Blockchain/README.md). - DM: конфликтов нет,
message_keyуникален.
6. Маршрутизация DM между серверами
При отправке DM от пользователя A к пользователю B:
- Клиент A отправляет пару блоков на свой сервер X.
- Сервер X валидирует и локально сохраняет пару.
- До ответа клиенту X параллельно отправляет входящую копию максимум двум актуальным
access_serversB. - ACK хотя бы одного маршрута B означает терминальный
delivered; второй сервер B догоняется собственной DM-синхронизацией. - После первой попытки X передаёт полную пару второму access-серверу A старым
ReceiveOutcomingMessage, без delivery-state. - При нулевой доставке выполняются повторы через 30 секунд, 5 минут, 25 минут и 1 час. Перед тремя последними X спрашивает peer A через
GetDmDeliveryStatus(messageKey). - После неудачной попытки через час ставится терминальный
failed.
Изменения routing B учитываются до терминального состояния, потому что список маршрутов перечитывается на каждой попытке.
7. Безопасность
- Все блоки подписаны ключами пользователя на клиенте — сервер не может подделать содержимое.
- Серверы не расшифровывают DM-контент; E2EE уже выполняется клиентами.
- При синхронизации каждый блок проходит валидацию подписи на принимающем сервере.
- Межсерверная авторизация DM-операций пока отложена;
sourceServerLoginвременно считается доверенным.
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-канал | Нужна реализация |
| Асинхронная доставка DM на access-серверы получателя | ✅ Реализовано |
| Retry DM до 1 часа + UI-state | ✅ Реализовано |
Репликация DM на второй access-сервер по synced |
✅ Реализовано |
Read-only GetDmDeliveryStatus |
✅ Реализовано |
| Push блоков блокчейна партнёрам | ✅ Реализована базовая one-shot версия |
| Periodic backfill отсутствующего хвоста | ✅ Реализовано |
| Разрешение рассинхрона / divergence | ✅ Реализована базовая full-resync схема во время periodic sync |
Startup recovery по *.resync_pending marker-file |
✅ Реализовано |
Маршрутизация DM через один/два access_servers |
✅ Реализовано |
| Криптографическая server-to-server авторизация DM | Нужна реализация |
Текущая версия сервера умеет синхронизацию блокчейнов и DM. Постоянные server-to-server соединения не требуются для текущей one-shot WS-реализации; отдельной будущей задачей остаётся криптографическая авторизация DM-вызовов.
Следующие отдельные шаги после текущего этапа:
- отдельно проверить full-resync и startup-recovery на реальном тестовом прогоне после ручного удаления БД/файлов.
8.1 Практическая проверка на тестовом сервере
Проверка на server2.shineup.me показала, что текущая схема действительно поднимает цепочку при старте:
- после рестарта сервер сначала проходит
BlockchainTmpRecovery; - затем обрабатывает
BlockchainResyncRecovery; - после этого сам догружает цепочку
aidartest-001сshineup.me; - итоговое состояние на тестовом сервере:
blockchain_state.last_block_number = 13blocksпоaidartest-001=14записей
Это подтверждает, что startup sync и full-resync flow работают в живом сценарии, а не только в коде.