SHA256
Добавить sync-профиль пользователя и обход Solana RPC
This commit is contained in:
+4
@@ -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),
|
||||
|
||||
+86
@@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
+14
@@ -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; }
|
||||
}
|
||||
+38
@@ -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; }
|
||||
}
|
||||
+44
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user