diff --git a/Dev_Docs/API/05_Technical_Requests_API.md b/Dev_Docs/API/05_Technical_Requests_API.md index 0314891..8b3198b 100644 --- a/Dev_Docs/API/05_Technical_Requests_API.md +++ b/Dev_Docs/API/05_Technical_Requests_API.md @@ -2,11 +2,12 @@ Этот файл описывает технические WebSocket-запросы, которые нужны для служебной работы клиента с сервером. Часть операций доступна без авторизации, часть требует успешной авторизованной сессии. -Сейчас здесь семь методов: +Сейчас здесь восемь методов: - `Ping` — keep-alive запрос для поддержания живого WebSocket-соединения; - `GetServerInfo` — запрос базовой публичной информации о сервере для выбора узла в децентрализованной сети; - `ListBlockchainHeads` — краткая сводка по всем локальным блокчейнам сервера для межсерверной синхронизации; +- `GetSyncUserProfile` — межсерверный профиль пользователя для создания локальной цепочки без Solana RPC; - `GetCallIceConfig` — выдача STUN/TURN конфигурации для звонков; - `ClientErrorLog` — отправка клиентской ошибки в серверный лог; - `ClientDebugLog` — отправка клиентского debug-события в серверный буфер; @@ -17,6 +18,7 @@ - `Ping` нужен для регулярной проверки, что соединение всё ещё живо; - `GetServerInfo` нужен до авторизации и до работы с данными, чтобы клиент понял, что сервер доступен, и показал пользователю краткую карточку этого узла. - `ListBlockchainHeads` нужен для сервер-сервер сверки: партнёр получает список heads по всем цепочкам, сравнивает его со своим состоянием и затем добирает недостающие блоки по диапазону. +- `GetSyncUserProfile` нужен для server-to-server режима, когда принимающий сервер хочет создать у себя локальные `solana_users + blockchain_state` без прямого обращения в Solana. Это используется как временный обход ограничений внешнего Solana RPC. Ниже сначала описаны назначение методов, затем точные форматы запросов и ответов. @@ -197,7 +199,90 @@ --- -## 4. `GetCallIceConfig` +## 4. `GetSyncUserProfile` + +### Назначение + +Запрос минимального профиля пользователя для межсерверной синхронизации. + +Нужен в сценарии, когда сервер во время periodic sync увидел чужой блокчейн, которого у него локально ещё нет. Вместо обращения в Solana PDA он может запросить у партнёра: + +- `login` +- `blockchainName` +- `solanaKey` +- `blockchainKey` +- `clientKey` +- `blockchainSizeLimitBytes` + +После этого принимающий сервер может локально создать записи в `solana_users` и `blockchain_state`, а затем уже докачивать блоки через `GetBlockchainBlock`. + +Этот запрос доступен без авторизации и предназначен именно для server-to-server sync. + +### Запрос + +```json +{ + "op": "GetSyncUserProfile", + "requestId": "sync-user-001", + "payload": { + "login": "alice" + } +} +``` + +### Успешный ответ: пользователь не найден + +```json +{ + "op": "GetSyncUserProfile", + "requestId": "sync-user-001", + "status": 200, + "ok": true, + "payload": { + "exists": false + } +} +``` + +### Успешный ответ: пользователь найден + +```json +{ + "op": "GetSyncUserProfile", + "requestId": "sync-user-001", + "status": 200, + "ok": true, + "payload": { + "exists": true, + "login": "alice", + "blockchainName": "alice-001", + "solanaKey": "BASE64_32", + "blockchainKey": "BASE64_32", + "clientKey": "BASE64_32", + "blockchainSizeLimitBytes": 100000 + } +} +``` + +### Поля ответа + +- `exists` — найден ли пользователь на сервере-партнёре. +- `login` — канонический login из БД сервера-партнёра. +- `blockchainName` — имя основной цепочки пользователя. +- `solanaKey` — публичный ключ логина. +- `blockchainKey` — публичный ключ блокчейна. +- `clientKey` — публичный клиентский ключ, который в текущей модели используется при создании локальной записи. +- `blockchainSizeLimitBytes` — лимит размера файла блокчейна, который будет записан в локальный `blockchain_state`. + +### Специфические коды ошибок `GetSyncUserProfile` + +- `400 / BAD_FIELDS` — пустой или некорректный `login`. +- `404 / BLOCKCHAIN_STATE_NOT_FOUND` — пользователь найден, но на сервере-партнёре отсутствует `blockchain_state` для его цепочки. +- При непредвиденной ошибке сервер вернёт общую ошибку из раздела `00`, обычно `500 / INTERNAL_ERROR`. + +--- + +## 5. `GetCallIceConfig` Доступно только после успешной авторизации. @@ -247,7 +332,7 @@ --- -## 5. `ClientErrorLog` +## 6. `ClientErrorLog` ### Запрос @@ -294,7 +379,7 @@ --- -## 6. `ClientDebugLog` +## 7. `ClientDebugLog` ### Запрос @@ -332,7 +417,7 @@ --- -## 7. `CallDeliveryReport` +## 8. `CallDeliveryReport` ### Запрос diff --git a/Dev_Docs/API/09_Operations_Index.md b/Dev_Docs/API/09_Operations_Index.md index dc1fd25..1800691 100644 --- a/Dev_Docs/API/09_Operations_Index.md +++ b/Dev_Docs/API/09_Operations_Index.md @@ -36,6 +36,7 @@ | `Ping` | `05_Technical_Requests_API.md` | keep-alive | | `GetServerInfo` | `05_Technical_Requests_API.md` | публичная информация о сервере | | `ListBlockchainHeads` | `05_Technical_Requests_API.md` | список heads всех локальных блокчейнов | +| `GetSyncUserProfile` | `05_Technical_Requests_API.md` | межсерверный профиль пользователя для синхронизации | | `GetCallIceConfig` | `05_Technical_Requests_API.md` | STUN/TURN конфигурация звонков | | `ClientErrorLog` | `05_Technical_Requests_API.md` | логирование клиентской ошибки | | `ClientDebugLog` | `05_Technical_Requests_API.md` | клиентский debug-лог | diff --git a/Dev_Docs/Blockchain/sync-between-servers.md b/Dev_Docs/Blockchain/sync-between-servers.md index dfdabd8..3d908c4 100644 --- a/Dev_Docs/Blockchain/sync-between-servers.md +++ b/Dev_Docs/Blockchain/sync-between-servers.md @@ -39,33 +39,94 @@ - Порядок блоков сохраняется (по глобальному номеру блока и хэшу). - Дедупликация по глобальному номеру блока и хэшу. -## 4. Протокол синхронизации (целевой, не реализован) +## 4. Текущая реализованная схема -### 4.1 Межсерверное соединение +На текущем этапе сервер уже умеет базовую межсерверную синхронизацию пользовательских блокчейнов. + +### 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`. + +### 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-server `GetSyncUserProfile`. + +Настройка влияет именно на этап подготовки отсутствующей локальной цепочки во время periodic sync. + +## 5. Целевой протокол следующего этапа + +### 5.1 Межсерверное соединение - Серверы устанавливают постоянное WebSocket-соединение друг с другом. - Адрес партнёра определяется по `server_address` из его Solana PDA. - Аутентификация: подпись Ed25519 корневым ключом сервера (`root_key` из PDA). - При разрыве — переподключение с экспоненциальным backoff. -### 4.2 Доставка новых данных (push) +### 5.2 Доставка новых данных (push) - При получении нового блока или DM сервер немедленно пушит его всем подключённым партнёрам. - Партнёр подтверждает приём (ACK). Без ACK — повтор с backoff. -### 4.3 Начальная синхронизация (backfill) +### 5.3 Начальная синхронизация (backfill) - При первом подключении к партнёру серверы обмениваются «курсорами» состояния: последний глобальный номер блока, последний известный DM-ключ. - Сервер с более полной историей досылает недостающее партнёру. -### 4.4 Разрешение конфликтов +### 5.4 Разрешение конфликтов - Блоки пользовательского блокчейна: порядок определяется глобальным номером блока. Конфликтующие ветки (fork) разрешаются по правилам `AddBlock` (см. `Dev_Docs/Blockchain/README.md`). - DM: конфликтов нет, `message_key` уникален. -## 5. Маршрутизация DM между серверами +## 6. Маршрутизация DM между серверами При отправке DM от пользователя A к пользователю B: @@ -78,23 +139,30 @@ Кэш адресов серверов: обновляется раз в сессию (при ошибке соединения). -## 6. Безопасность +## 7. Безопасность - Все блоки подписаны ключами пользователя на клиенте — сервер не может подделать содержимое. - Серверы не расшифровывают DM-контент (шифрование — задача следующего этапа). - При синхронизации каждый блок проходит валидацию подписи на принимающем сервере. -## 7. Статус реализации +## 8. Статус реализации | Компонент | Статус | |-----------|--------| | Регистрация серверной PDA в Solana | ✅ Реализовано | | Чтение `sync_servers` из PDA | ✅ Реализовано | -| Межсерверный WebSocket-канал | Нужна реализация | +| Локальная таблица `sync_servers` | ✅ Реализовано | +| Публичный `ListBlockchainHeads` | ✅ Реализовано | +| Публичный `GetBlockchainBlock` | ✅ Реализовано | +| Публичный `GetSyncUserProfile` | ✅ Реализовано | +| Плановый blockchain sync при старте + каждые 12 часов | ✅ Реализовано | +| Обход Solana RPC через `sync.importUserProfileFromPartner.enabled` | ✅ Реализовано | +| Межсерверный постоянный WebSocket-канал | Нужна реализация | | Push новых DM партнёрам | Нужна реализация | | Push блоков блокчейна партнёрам | ✅ Реализована базовая one-shot версия | -| Backfill при первом подключении | Нужна реализация | +| Periodic backfill отсутствующего хвоста | ✅ Реализовано | +| Разрешение рассинхрона / divergence | Нужна реализация | | Маршрутизация DM через access_servers | Нужна реализация (заглушка) | -Текущая версия сервера работает без межсерверной синхронизации. -Синхронизация — задача следующего этапа разработки. +Текущая версия сервера уже умеет базовую синхронизацию блокчейнов между партнёрами. +Не реализованы ещё DM-sync, постоянные server-to-server соединения и автоматическое исправление рассинхрона цепочек. diff --git a/Dev_Docs/Pending_Features/2026-06-25_1200_periodic_blockchain_sync.md b/Dev_Docs/Pending_Features/2026-06-25_1200_periodic_blockchain_sync.md index 8ef4241..c9d647a 100644 --- a/Dev_Docs/Pending_Features/2026-06-25_1200_periodic_blockchain_sync.md +++ b/Dev_Docs/Pending_Features/2026-06-25_1200_periodic_blockchain_sync.md @@ -1,21 +1,28 @@ # Периодическая межсерверная синхронизация блокчейнов - Краткое описание: + - При старте сервер подтягивает `sync_servers` из server PDA и сохраняет адреса партнёров в локальную таблицу. + - После успешного локального `AddBlock` работает фоновая one-shot отправка нового блока партнёрам. - Добавлен публичный `GetBlockchainBlock` для чтения одного блока. + - Добавлен публичный `GetSyncUserProfile` для подготовки отсутствующей локальной цепочки без прямого Solana RPC. - Добавлен плановый sync блокчейнов при старте сервера и затем каждые `12` часов. - Синхронизация пока умеет только докачивать отсутствующий хвост цепочки. + - Добавлена настройка `sync.importUserProfileFromPartner.enabled`, которая включает создание локального `solana_users + blockchain_state` по ответу сервера-партнёра вместо Solana PDA. - Случай рассинхрона цепочек пока не исправляется автоматически: он только логируется как не реализованный сценарий. - Что именно проверять: - После старта сервера в логах появляется запуск периодического sync. - Сервер может запросить у партнёра `ListBlockchainHeads`. + - Сервер может запросить у партнёра `GetSyncUserProfile`, если включён `sync.importUserProfileFromPartner.enabled=true`. - Сервер может запросить у партнёра `GetBlockchainBlock` и локально применить блок через существующий `AddBlock`. - На чистом тестовом сервере после удаления БД и файлов блокчейнов сервер сам подтягивает блоки при старте. + - При включённом режиме обхода Solana сервер восстанавливает локальные цепочки без запросов в Solana RPC. - После первичного старта новые блоки продолжают догоняться без ручного вмешательства. - При рассинхроне цепочек в логах появляется явное сообщение, что reconciliation пока не реализован. - Ожидаемый результат: - Чистый сервер после старта сам восстанавливает локальные цепочки от партнёра синхронизации. + - В режиме обхода Solana чистый сервер не упирается в `Solana RPC 429` при создании локальных chain-state для уже существующих на партнёре пользователей. - Периодический sync не мешает обычной работе сервера и не ломает локальный `AddBlock`. - Нереализованный случай рассинхрона не приводит к падению сервера и явно отражается в логах. diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/JsonHandlerRegistry.java b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/JsonHandlerRegistry.java index 70b870e..4d42cbc 100644 --- a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/JsonHandlerRegistry.java +++ b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/JsonHandlerRegistry.java @@ -106,6 +106,7 @@ import server.logic.ws_protocol.JSON.messages.entyties.Net_UpsertPushToken_Reque // --- NEW: Ping --- import server.logic.ws_protocol.JSON.handlers.system.Net_GetServerInfo_Handler; import server.logic.ws_protocol.JSON.handlers.system.Net_GetCallIceConfig_Handler; +import server.logic.ws_protocol.JSON.handlers.system.Net_GetSyncUserProfile_Handler; import server.logic.ws_protocol.JSON.handlers.system.Net_ClientErrorLog_Handler; import server.logic.ws_protocol.JSON.handlers.system.Net_ClientDebugLog_Handler; import server.logic.ws_protocol.JSON.handlers.system.Net_ListBlockchainHeads_Handler; @@ -116,6 +117,7 @@ import server.logic.ws_protocol.JSON.handlers.system.entyties.Net_ClientErrorLog import server.logic.ws_protocol.JSON.handlers.system.entyties.Net_ClientDebugLog_Request; import server.logic.ws_protocol.JSON.handlers.system.entyties.Net_GetCallIceConfig_Request; import server.logic.ws_protocol.JSON.handlers.system.entyties.Net_GetServerInfo_Request; +import server.logic.ws_protocol.JSON.handlers.system.entyties.Net_GetSyncUserProfile_Request; import server.logic.ws_protocol.JSON.handlers.system.entyties.Net_ListBlockchainHeads_Request; import server.logic.ws_protocol.JSON.handlers.system.entyties.Net_Ping_Request; @@ -199,6 +201,7 @@ public final class JsonHandlerRegistry { Map.entry("Ping", new Net_Ping_Handler()), Map.entry("GetServerInfo", new Net_GetServerInfo_Handler()), Map.entry("ListBlockchainHeads", new Net_ListBlockchainHeads_Handler()), + Map.entry("GetSyncUserProfile", new Net_GetSyncUserProfile_Handler()), Map.entry("GetCallIceConfig", new Net_GetCallIceConfig_Handler()), Map.entry("ClientErrorLog", new Net_ClientErrorLog_Handler()), Map.entry("ClientDebugLog", new Net_ClientDebugLog_Handler()), @@ -276,6 +279,7 @@ public final class JsonHandlerRegistry { Map.entry("Ping", Net_Ping_Request.class), Map.entry("GetServerInfo", Net_GetServerInfo_Request.class), Map.entry("ListBlockchainHeads", Net_ListBlockchainHeads_Request.class), + Map.entry("GetSyncUserProfile", Net_GetSyncUserProfile_Request.class), Map.entry("GetCallIceConfig", Net_GetCallIceConfig_Request.class), Map.entry("ClientErrorLog", Net_ClientErrorLog_Request.class), Map.entry("ClientDebugLog", Net_ClientDebugLog_Request.class), diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/system/Net_GetSyncUserProfile_Handler.java b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/system/Net_GetSyncUserProfile_Handler.java new file mode 100644 index 0000000..9bf9c58 --- /dev/null +++ b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/system/Net_GetSyncUserProfile_Handler.java @@ -0,0 +1,86 @@ +package server.logic.ws_protocol.JSON.handlers.system; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import server.logic.ws_protocol.JSON.ConnectionContext; +import server.logic.ws_protocol.JSON.entyties.Net_Request; +import server.logic.ws_protocol.JSON.entyties.Net_Response; +import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler; +import server.logic.ws_protocol.JSON.handlers.system.entyties.Net_GetSyncUserProfile_Request; +import server.logic.ws_protocol.JSON.handlers.system.entyties.Net_GetSyncUserProfile_Response; +import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory; +import server.logic.ws_protocol.WireCodes; +import shine.db.dao.BlockchainStateDAO; +import shine.db.dao.SolanaUsersDAO; +import shine.db.entities.BlockchainStateEntry; +import shine.db.entities.SolanaUserEntry; + +/** + * GetSyncUserProfile — server-to-server профиль пользователя для межсерверной синхронизации. + * Нужен, чтобы принимающий сервер мог создать локальные solana_users + blockchain_state + * без прямого запроса в Solana RPC. + */ +public final class Net_GetSyncUserProfile_Handler implements JsonMessageHandler { + + private static final Logger log = LoggerFactory.getLogger(Net_GetSyncUserProfile_Handler.class); + private final SolanaUsersDAO usersDAO = SolanaUsersDAO.getInstance(); + private final BlockchainStateDAO stateDAO = BlockchainStateDAO.getInstance(); + + @Override + public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) { + Net_GetSyncUserProfile_Request req = (Net_GetSyncUserProfile_Request) baseRequest; + + String login = req.getLogin() == null ? "" : req.getLogin().trim(); + if (login.isEmpty()) { + return NetExceptionResponseFactory.error( + req, + WireCodes.Status.BAD_REQUEST, + "BAD_FIELDS", + "Некорректные поля: login" + ); + } + + try { + SolanaUserEntry user = usersDAO.getByLogin(login); + + Net_GetSyncUserProfile_Response resp = new Net_GetSyncUserProfile_Response(); + resp.setOp(req.getOp()); + resp.setRequestId(req.getRequestId()); + resp.setStatus(WireCodes.Status.OK); + + if (user == null) { + resp.setExists(false); + return resp; + } + + BlockchainStateEntry state = stateDAO.getByBlockchainName(user.getBlockchainName()); + if (state == null) { + log.warn("GetSyncUserProfile: blockchain_state not found for login={} blockchainName={}", + user.getLogin(), user.getBlockchainName()); + return NetExceptionResponseFactory.error( + req, + WireCodes.Status.NOT_FOUND, + "BLOCKCHAIN_STATE_NOT_FOUND", + "Состояние блокчейна пользователя не найдено" + ); + } + + resp.setExists(true); + resp.setLogin(user.getLogin()); + resp.setBlockchainName(user.getBlockchainName()); + resp.setSolanaKey(user.getSolanaKey()); + resp.setBlockchainKey(user.getBlockchainKey()); + resp.setClientKey(user.getClientKey()); + resp.setBlockchainSizeLimitBytes(state.getSizeLimit()); + return resp; + } catch (Exception e) { + log.error("❌ Internal error GetSyncUserProfile login={}", login, e); + return NetExceptionResponseFactory.error( + req, + WireCodes.Status.INTERNAL_ERROR, + "INTERNAL_ERROR", + NetExceptionResponseFactory.detailedMessage("Внутренняя ошибка сервера при GetSyncUserProfile", e) + ); + } + } +} diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/system/entyties/Net_GetSyncUserProfile_Request.java b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/system/entyties/Net_GetSyncUserProfile_Request.java new file mode 100644 index 0000000..8de85e6 --- /dev/null +++ b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/system/entyties/Net_GetSyncUserProfile_Request.java @@ -0,0 +1,14 @@ +package server.logic.ws_protocol.JSON.handlers.system.entyties; + +import server.logic.ws_protocol.JSON.entyties.Net_Request; + +/** + * Запрос межсерверного профиля пользователя для синхронизации. + */ +public class Net_GetSyncUserProfile_Request extends Net_Request { + + private String login; + + public String getLogin() { return login; } + public void setLogin(String login) { this.login = login; } +} diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/system/entyties/Net_GetSyncUserProfile_Response.java b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/system/entyties/Net_GetSyncUserProfile_Response.java new file mode 100644 index 0000000..aed4a26 --- /dev/null +++ b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/system/entyties/Net_GetSyncUserProfile_Response.java @@ -0,0 +1,38 @@ +package server.logic.ws_protocol.JSON.handlers.system.entyties; + +import server.logic.ws_protocol.JSON.entyties.Net_Response; + +/** + * Ответ межсерверного профиля пользователя для синхронизации. + */ +public class Net_GetSyncUserProfile_Response extends Net_Response { + + private Boolean exists; + private String login; + private String blockchainName; + private String solanaKey; + private String blockchainKey; + private String clientKey; + private Long blockchainSizeLimitBytes; + + public Boolean getExists() { return exists; } + public void setExists(Boolean exists) { this.exists = exists; } + + public String getLogin() { return login; } + public void setLogin(String login) { this.login = login; } + + public String getBlockchainName() { return blockchainName; } + public void setBlockchainName(String blockchainName) { this.blockchainName = blockchainName; } + + public String getSolanaKey() { return solanaKey; } + public void setSolanaKey(String solanaKey) { this.solanaKey = solanaKey; } + + public String getBlockchainKey() { return blockchainKey; } + public void setBlockchainKey(String blockchainKey) { this.blockchainKey = blockchainKey; } + + public String getClientKey() { return clientKey; } + public void setClientKey(String clientKey) { this.clientKey = clientKey; } + + public Long getBlockchainSizeLimitBytes() { return blockchainSizeLimitBytes; } + public void setBlockchainSizeLimitBytes(Long blockchainSizeLimitBytes) { this.blockchainSizeLimitBytes = blockchainSizeLimitBytes; } +} diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/sync/RemoteBlockchainSyncClient.java b/SHiNE-server/shine-server-net-protocol/src/main/java/server/sync/RemoteBlockchainSyncClient.java index f987a69..1ac52c9 100644 --- a/SHiNE-server/shine-server-net-protocol/src/main/java/server/sync/RemoteBlockchainSyncClient.java +++ b/SHiNE-server/shine-server-net-protocol/src/main/java/server/sync/RemoteBlockchainSyncClient.java @@ -61,6 +61,41 @@ public final class RemoteBlockchainSyncClient { return result; } + public RemoteSyncUserProfile getSyncUserProfile(String serverAddressRaw, String login) throws Exception { + String safeLogin = MAPPER.writeValueAsString(login); + JsonNode response = send(serverAddressRaw, """ + { + "op":"GetSyncUserProfile", + "requestId":%s, + "payload":{ + "login":%s + } + } + """.formatted("%s", safeLogin)); + + int status = response.path("status").asInt(500); + if (status == 404) { + return null; + } + if (status < 200 || status >= 300) { + throw new IllegalStateException("GetSyncUserProfile failed: status=" + status + " code=" + errorCode(response)); + } + + JsonNode payload = response.path("payload"); + if (!payload.path("exists").asBoolean(false)) { + return null; + } + + return new RemoteSyncUserProfile( + payload.path("login").asText(login), + payload.path("blockchainName").asText(""), + payload.path("solanaKey").asText(""), + payload.path("blockchainKey").asText(""), + payload.path("clientKey").asText(""), + payload.path("blockchainSizeLimitBytes").asLong(0L) + ); + } + public RemoteBlockchainBlock getBlockchainBlock(String serverAddressRaw, String blockchainName, int blockNumber) throws Exception { String safeBlockchainName = MAPPER.writeValueAsString(blockchainName); JsonNode response = send(serverAddressRaw, """ @@ -176,6 +211,15 @@ public final class RemoteBlockchainSyncClient { String blockBytesB64 ) {} + public record RemoteSyncUserProfile( + String login, + String blockchainName, + String solanaKey, + String blockchainKey, + String clientKey, + long blockchainSizeLimitBytes + ) {} + private static final class SyncWsListener implements WebSocket.Listener { private final CompletableFuture responseFuture; private final CountDownLatch openLatch; diff --git a/SHiNE-server/src/main/java/server/sync/PeriodicBlockchainSyncService.java b/SHiNE-server/src/main/java/server/sync/PeriodicBlockchainSyncService.java index ffcbc6a..ae903ff 100644 --- a/SHiNE-server/src/main/java/server/sync/PeriodicBlockchainSyncService.java +++ b/SHiNE-server/src/main/java/server/sync/PeriodicBlockchainSyncService.java @@ -9,9 +9,11 @@ import server.logic.ws_protocol.JSON.handlers.blockchain.Net_AddBlock_Handler; import server.logic.ws_protocol.JSON.handlers.blockchain.entyties.Net_AddBlock_Request; import shine.db.dao.BlockchainStateDAO; import shine.db.dao.SyncServersDAO; +import shine.db.dao.UserCreateDAO; import shine.db.entities.BlockchainStateEntry; import shine.db.entities.SyncServerEntry; import utils.blockchain.BlockchainNameUtil; +import utils.config.AppConfig; import java.util.List; import java.util.Locale; @@ -44,6 +46,8 @@ public final class PeriodicBlockchainSyncService { private static final Net_AddBlock_Handler ADD_BLOCK_HANDLER = new Net_AddBlock_Handler(); private static final BlockchainStateDAO STATE_DAO = BlockchainStateDAO.getInstance(); private static final SyncServersDAO SYNC_SERVERS_DAO = SyncServersDAO.getInstance(); + private static final UserCreateDAO USER_CREATE_DAO = UserCreateDAO.getInstance(); + private static final String CONFIG_IMPORT_PROFILE_FROM_PARTNER = "sync.importUserProfileFromPartner.enabled"; private PeriodicBlockchainSyncService() {} @@ -140,7 +144,7 @@ public final class PeriodicBlockchainSyncService { String localHash ) throws Exception { String partnerLogin = normalize(partner.getLogin()); - if (!ensureLocalChainExists(remoteHead.blockchainName())) { + if (!ensureLocalChainExists(partner, remoteHead.blockchainName())) { log.warn("Periodic blockchain sync: cannot prepare local chain. partner={} blockchainName={}", partnerLogin, remoteHead.blockchainName()); return; @@ -198,7 +202,7 @@ public final class PeriodicBlockchainSyncService { return new LocalAddBlockApplyResult(false, error.getCode(), error.getMessage(), ""); } - private static boolean ensureLocalChainExists(String blockchainName) { + private static boolean ensureLocalChainExists(SyncServerEntry partner, String blockchainName) { try { if (STATE_DAO.getByBlockchainName(blockchainName) != null) { return true; @@ -207,6 +211,9 @@ public final class PeriodicBlockchainSyncService { if (login == null || login.isBlank()) { return false; } + if (AppConfig.getInstance().getBoolean(CONFIG_IMPORT_PROFILE_FROM_PARTNER, false)) { + return importUserProfileFromPartner(partner, login); + } SolanaUserPdaImportService.findOrImportByLogin(login); return STATE_DAO.getByBlockchainName(blockchainName) != null; } catch (Exception e) { @@ -216,6 +223,37 @@ public final class PeriodicBlockchainSyncService { } } + private static boolean importUserProfileFromPartner(SyncServerEntry partner, String login) throws Exception { + if (partner == null || partner.getServerAddress() == null || partner.getServerAddress().isBlank()) { + return false; + } + + RemoteBlockchainSyncClient.RemoteSyncUserProfile profile = + REMOTE.getSyncUserProfile(partner.getServerAddress(), login); + if (profile == null) { + log.warn("Periodic blockchain sync: partner has no sync profile for login={} partner={}", + login, normalize(partner.getLogin())); + return false; + } + + long now = System.currentTimeMillis(); + long sizeLimit = profile.blockchainSizeLimitBytes() > 0 ? profile.blockchainSizeLimitBytes() : 100_000L; + boolean inserted = USER_CREATE_DAO.insertUserWithBlockchain( + profile.login(), + profile.blockchainName(), + profile.solanaKey(), + profile.blockchainKey(), + profile.clientKey(), + sizeLimit, + now + ); + + if (!inserted) { + return STATE_DAO.getByBlockchainName(profile.blockchainName()) != null; + } + return STATE_DAO.getByBlockchainName(profile.blockchainName()) != null; + } + private static String normalize(String value) { if (value == null) return null; String s = value.trim().toLowerCase(Locale.ROOT); diff --git a/SHiNE-server/src/main/resources/application.properties b/SHiNE-server/src/main/resources/application.properties index 592f4b9..f258963 100644 --- a/SHiNE-server/src/main/resources/application.properties +++ b/SHiNE-server/src/main/resources/application.properties @@ -2,6 +2,19 @@ server.1port=7070 db.path=data/shine.sqlite server.SHiNE.login=shineupme +# ------------------------------------------------------------ +# Межсерверная синхронизация: как создавать локальную запись пользователя, +# если во время sync пришла чужая цепочка, а у нас такого login ещё нет. +# false - брать профиль пользователя напрямую из Solana PDA (обычный режим). +# true - не ходить в Solana RPC, а запрашивать у сервера-партнёра специальный +# sync-профиль пользователя и по нему локально создавать +# solana_users + blockchain_state. +# Эта настройка нужна как временный обход лимитов Solana RPC (например 429), +# чтобы чистый сервер мог восстановить цепочки от партнёра без зависимости +# от внешнего Solana endpoint. +# ------------------------------------------------------------ +sync.importUserProfileFromPartner.enabled=false + # ------------------------------------------------------------ # Server public info # Эти поля используются JSON-операцией GetServerInfo. diff --git a/VERSION.properties b/VERSION.properties index de131e3..5f18382 100644 --- a/VERSION.properties +++ b/VERSION.properties @@ -1,2 +1,2 @@ -client.version=1.2.272 -server.version=1.2.252 +client.version=1.2.273 +server.version=1.2.253 diff --git a/deploy_shine-server_test2.sh b/deploy_shine-server_test2.sh index 32ddcac..e5a1cf5 100644 --- a/deploy_shine-server_test2.sh +++ b/deploy_shine-server_test2.sh @@ -1,7 +1,6 @@ #!/usr/bin/env bash set -euo pipefail -PROD_HOST="${PROD_HOST:-player@shineup.me}" TARGET_HOST="${TARGET_HOST:-player@193.8.215.70}" TARGET_DOMAIN="${TARGET_DOMAIN:-t.shineup.me}" REMOTE_BASE="${REMOTE_BASE:-/home/player/SHiNE}" @@ -11,8 +10,6 @@ REMOTE_LOGS_DIR="${REMOTE_LOGS_DIR:-$REMOTE_SERVER_DIR/logs}" REMOTE_UI_DIR="${REMOTE_UI_DIR:-$REMOTE_BASE/shine-ui}" REMOTE_SERVICE_NAME="${REMOTE_SERVICE_NAME:-shine-server}" LOCAL_JAR="${LOCAL_JAR:-SHiNE-server/build/libs/shine-server.jar}" -PROD_DATA_DIR="${PROD_DATA_DIR:-/home/player/SHiNE/shine-server/data}" -PROD_APP_PROPS="${PROD_APP_PROPS:-/home/player/SHiNE/shine-server/application.properties}" TMP_DIR="$(mktemp -d)" cleanup() { @@ -25,25 +22,10 @@ if [[ ! -f "$LOCAL_JAR" ]]; then exit 1 fi -ssh -o BatchMode=yes -o ConnectTimeout=20 "$PROD_HOST" "echo SSH OK" >/dev/null ssh -o BatchMode=yes -o ConnectTimeout=20 "$TARGET_HOST" "echo SSH OK" >/dev/null ssh "$TARGET_HOST" "sudo -n true" ssh "$TARGET_HOST" "java -version >/dev/null 2>&1" -mkdir -p "$TMP_DIR/data" -rsync -az --delete "$PROD_HOST:$PROD_DATA_DIR/" "$TMP_DIR/data/" -rsync -az "$PROD_HOST:$PROD_APP_PROPS" "$TMP_DIR/application.properties" -if grep -q '^server\.ui\.indexPath=' "$TMP_DIR/application.properties"; then - perl -0pi -e 's@^server\.ui\.indexPath=.*$@server.ui.indexPath=/home/player/SHiNE/shine-ui/index.html@m' "$TMP_DIR/application.properties" -else - printf '\nserver.ui.indexPath=/home/player/SHiNE/shine-ui/index.html\n' >>"$TMP_DIR/application.properties" -fi -if grep -q '^server\.SHiNE\.login=' "$TMP_DIR/application.properties"; then - perl -0pi -e 's@^server\.SHiNE\.login=.*$@server.SHiNE.login=tshineupme@m' "$TMP_DIR/application.properties" -else - printf '\nserver.SHiNE.login=tshineupme\n' >>"$TMP_DIR/application.properties" -fi - cat >"$TMP_DIR/shine-server.service" <