Files
SHiNE-server/docs/Blockchain/sync-between-servers.md
T
AidarKC 8a0275d962 Убрали старый sync и обновили bundle
Что сделано: вычистили неиспользуемый user-settings sync/DM sync хвост, сохранили сборку, обновили bundle.sh так, чтобы gradle-wrapper.jar всегда попадал в архив.

Проверено: compileJava и deploy на t2 (server + UI).
Не проверяли: полные интеграционные сценарии, ручные UI-флоу и продовый деплой.
2026-08-28 14:51:47 +04:00

21 KiB
Raw Blame History

Синхронизация блокчейнов и доставка 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-аккаунтов партнёрских серверов, с которыми он синхронизируется.

sync_servers относится к серверному узлу и не является списком access-серверов обычного пользователя. У пользователя действует только первый access_servers[0].

  • Список хранится в блоке 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 не реплицируются.
  • Полная актуальная схема: docs/Personal_Messages/Доставка_и_синхронизация_DM.md.

3.2 Блоки пользовательского блокчейна

  • Все блоки AddBlock пользователей, зарегистрированных на сервере или синхронизирующихся через него.
  • Синхронизируются в обе стороны между всеми партнёрами из sync_servers.
  • Порядок блоков сохраняется (по глобальному номеру блока и хэшу).
  • Дедупликация по глобальному номеру блока и хэшу.

3.3 Пользовательские настройки

Пользовательские настройки хранятся локально на единственном access-сервере и между серверами не синхронизируются.

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 — минимальный профиль пользователя для локального создания runtime-проекции пользователя и blockchain_state без обращения в Solana RPC.

4.3 Как сейчас работает periodic sync

Для каждого сервера из локальной таблицы sync_servers:

  1. запрашивается ListBlockchainHeads;
  2. для каждой удалённой цепочки сравниваются:
    • lastBlockNumber
    • lastBlockHash
    • локальное состояние;
  3. если локальная цепочка слабее, сервер по одному блоку вызывает GetBlockchainBlock;
  4. каждый скачанный блок локально применяется через существующий AddBlock;
  5. если у сервера ещё нет локальной записи пользователя/цепочки, перед этим подготавливается локальная runtime-проекция пользователя и 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-таблицы и current users слой;
  • висячие cross-chain ссылки считаются допустимым поведением системы.

4.5 Как работает обычный AddBlock и его recovery

Обычная запись блока теперь тоже идёт через временные артефакты:

  1. собирается <blockchainName>.tmp_bch как полный кандидат на замену основного файла;
  2. пишется маленький sidecar <blockchainName>.write_check с blockNumber и blockHash;
  3. только после этого создаётся пустой marker <blockchainName>.write_pending;
  4. выполняется SQL-транзакция;
  5. после commit tmp атомарно ставится на место основного .bch;
  6. marker и sidecar удаляются.

На старте BlockchainTmpRecoveryOnStartup смотрит именно на эту пару:

  • если write_pending есть, recovery проверяет sidecar и БД, а затем либо завершает swap, либо чистит временные файлы;
  • если write_pending нет, а tmp_bch или write_check остались, это мусор и он удаляется;
  • resync_pending сюда не относится, это отдельный recovery-поток.

4.6 Startup recovery по marker-file

При старте сервер идёт в таком порядке:

  1. BlockchainTmpRecoveryOnStartup для *.write_pending и orphan *.tmp_bch / *.write_check;
  2. BlockchainResyncRecoveryOnStartup для *.resync_pending;
  3. только потом поднимается обычный сервер и запускается 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-server GetSyncUserProfile.

Настройка влияет именно на этап подготовки отсутствующей локальной цепочки во время periodic sync.

5. Реализованный постоянный server-to-server транспорт

Этот раздел не меняет текущую семантику DM, settings и blockchain. Он описывает единый постоянный WSS-транспорт, через который выполняются уже существующие операции.

5.1 Межсерверное соединение

  • Серверы устанавливают постоянное исходящее WebSocket-соединение друг с другом.
  • Адрес партнёра определяется по server_address из его Solana PDA.
  • После подключения отправляется ServerHello с serverLogin, версией протокола и capabilities.
  • На текущем этапе serverLogin принимается на доверии; подпись Ed25519 корневым ключом сервера отложена.
  • При разрыве выполняется переподключение с jitter/backoff до 60 секунд.
  • После 120 секунд отсутствия полезного трафика отправляется WebSocket ping; pong ожидается 15 секунд.
  • Один физический канал переиспользуют доставка DM и blockchain.

5.2 Доставка новых данных (push)

  • При получении нового блока сервер может немедленно пушить его всем подключённым партнёрам.
  • Партнёр подтверждает приём (ACK). Без ACK — повтор с backoff.
  • DM использует отдельное расписание, описанное в docs/Personal_Messages/Доставка_и_синхронизация_DM.md.

5.3 Начальная синхронизация (backfill)

  • При первом подключении к партнёру серверы могут обмениваться курсорами состояния блокчейнов.
  • Сервер с более полной историей досылает недостающее партнёру.
  • DM-history backfill между access-серверами отсутствует.

5.4 Разрешение конфликтов

  • Блоки пользовательского блокчейна: порядок определяется глобальным номером блока. Конфликтующие ветки (fork) разрешаются по правилам AddBlock (см. docs/Blockchain/README.md).
  • DM: конфликтов нет, message_key уникален.

6. Маршрутизация DM между серверами

При отправке DM от пользователя A к пользователю B:

  1. Клиент A отправляет пару блоков на свой сервер X.
  2. Сервер X валидирует и локально сохраняет пару.
  3. До ответа клиенту X отправляет входящую копию на единственный access_servers[0] пользователя B через ReceiveIncomingMessage.
  4. Успешное сохранение на сервере B означает терминальный delivered.
  5. При неудаче выполняются повторы через 30 секунд, 5 минут, 25 минут и 1 час.
  6. После неудачной попытки через час ставится терминальный 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-канал Реализован общий ServerConnectionPool
Асинхронная доставка DM на единственный access-сервер получателя Реализовано
Retry DM до 1 часа + UI-state Реализовано
Репликация DM и настроек между access-серверами Не используется
Push блоков блокчейна партнёрам Выполняется через постоянный WSS-пул
Periodic backfill отсутствующего хвоста Реализовано
Разрешение рассинхрона / divergence Реализована базовая full-resync схема во время periodic sync
Startup recovery по *.resync_pending marker-file Реализовано
Маршрутизация DM только через access_servers[0] Реализовано
Криптографическая server-to-server авторизация DM Нужна реализация

Текущая версия сервера использует постоянный WSS-пул для существующих server-to-server JSON-операций. Отдельной будущей задачей остаётся криптографическая авторизация server-to-server вызовов.

Следующие отдельные шаги после текущего этапа:

  • отдельно проверить full-resync и startup-recovery на реальном тестовом прогоне после ручного удаления БД/файлов.

8.1 Практическая проверка на тестовом сервере

Проверка на server2.shineup.me показала, что текущая схема действительно поднимает цепочку при старте:

  • после рестарта сервер сначала проходит BlockchainTmpRecovery;
  • затем обрабатывает BlockchainResyncRecovery;
  • после этого сам догружает цепочку aidartest-001 с shineup.me;
  • итоговое состояние на тестовом сервере:
    • blockchain_state.last_block_number = 13
    • blocks по aidartest-001 = 14 записей

Это подтверждает, что startup sync и full-resync flow работают в живом сценарии, а не только в коде.