Добавить sync-профиль пользователя и обход Solana RPC

This commit is contained in:
AidarKC
2026-06-25 18:46:47 +04:00
parent f3e4233285
commit 23edad416c
13 changed files with 419 additions and 41 deletions
@@ -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),
@@ -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)
);
}
}
}
@@ -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; }
}
@@ -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; }
}
@@ -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<String> responseFuture;
private final CountDownLatch openLatch;
@@ -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);
@@ -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.