# Синхронизация блоков и 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 `.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. Удаляются файлы `.bch` и `.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 на реальном тестовом прогоне после ручного удаления БД/файлов.