From ce5595bc16727bd701fb37af061e391e5eb73d39c57480a86d58498d34543043 Mon Sep 17 00:00:00 2001 From: AidarKC Date: Fri, 24 Jul 2026 22:22:37 +0400 Subject: [PATCH] =?UTF-8?q?=D0=A1=D0=B5=D1=80=D0=B2=D0=B5=D1=80:=20=D0=BF?= =?UTF-8?q?=D0=BE=D1=87=D0=B8=D0=BD=D0=B8=D1=82=D1=8C=20=D0=B2=D0=BE=D1=81?= =?UTF-8?q?=D1=81=D1=82=D0=B0=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D0=B5=20chain-state=20=D0=B8=D0=B7=20PostgreSQL=20sync-=D1=82?= =?UTF-8?q?=D0=B0=D0=B1=D0=BB=D0=B8=D1=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/shine/db/KeyEncodingUtil.java | 115 ++++++++++++++++++ .../java/shine/db/dao/BlockchainStateDAO.java | 2 +- .../src/main/java/shine/db/dao/BlocksDAO.java | 4 +- .../shine/db/dao/SolanaUserPdaCurrentDAO.java | 78 ++++++++++++ .../java/shine/db/dao/SolanaUsersDAO.java | 7 +- .../main/java/shine/db/dao/UserCreateDAO.java | 2 +- .../entities/SolanaUserPdaCurrentEntry.java | 46 +++++++ .../sync/PeriodicBlockchainSyncService.java | 45 ++++++- VERSION.properties | 2 +- 9 files changed, 292 insertions(+), 9 deletions(-) create mode 100644 SHiNE-server/shine-server-db/src/main/java/shine/db/KeyEncodingUtil.java create mode 100644 SHiNE-server/shine-server-db/src/main/java/shine/db/dao/SolanaUserPdaCurrentDAO.java create mode 100644 SHiNE-server/shine-server-db/src/main/java/shine/db/entities/SolanaUserPdaCurrentEntry.java diff --git a/SHiNE-server/shine-server-db/src/main/java/shine/db/KeyEncodingUtil.java b/SHiNE-server/shine-server-db/src/main/java/shine/db/KeyEncodingUtil.java new file mode 100644 index 00000000..867bd394 --- /dev/null +++ b/SHiNE-server/shine-server-db/src/main/java/shine/db/KeyEncodingUtil.java @@ -0,0 +1,115 @@ +package shine.db; + +import java.util.Base64; + +/** + * Нормализация ключей из разных storage-слоёв к каноническому Base64(32). + */ +public final class KeyEncodingUtil { + + private static final String BASE58_ALPHABET = + "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; + + private static final int[] BASE58_INDEXES = new int[128]; + + static { + for (int i = 0; i < BASE58_INDEXES.length; i++) { + BASE58_INDEXES[i] = -1; + } + for (int i = 0; i < BASE58_ALPHABET.length(); i++) { + BASE58_INDEXES[BASE58_ALPHABET.charAt(i)] = i; + } + } + + private KeyEncodingUtil() {} + + public static String normalizeKeyToBase64_32(String rawKey) { + if (rawKey == null) { + return null; + } + + String value = rawKey.trim(); + if (value.isEmpty()) { + return value; + } + + byte[] asBase64 = tryDecodeBase64_32(value); + if (asBase64 != null) { + return Base64.getEncoder().encodeToString(asBase64); + } + + byte[] asBase58 = tryDecodeBase58_32(value); + if (asBase58 != null) { + return Base64.getEncoder().encodeToString(asBase58); + } + + return value; + } + + private static byte[] tryDecodeBase64_32(String value) { + try { + byte[] decoded = Base64.getDecoder().decode(value); + return decoded.length == 32 ? decoded : null; + } catch (IllegalArgumentException ignore) { + return null; + } + } + + private static byte[] tryDecodeBase58_32(String value) { + byte[] decoded = decodeBase58(value); + return decoded.length == 32 ? decoded : null; + } + + private static byte[] decodeBase58(String input) { + if (input.isEmpty()) { + return new byte[0]; + } + + byte[] input58 = new byte[input.length()]; + for (int i = 0; i < input.length(); i++) { + char c = input.charAt(i); + if (c >= BASE58_INDEXES.length || BASE58_INDEXES[c] < 0) { + return new byte[0]; + } + input58[i] = (byte) BASE58_INDEXES[c]; + } + + int zeros = 0; + while (zeros < input58.length && input58[zeros] == 0) { + zeros++; + } + + byte[] decoded = new byte[input.length()]; + int outputStart = decoded.length; + int inputStart = zeros; + while (inputStart < input58.length) { + int mod = divmod256(input58, inputStart); + if (input58[inputStart] == 0) { + inputStart++; + } + decoded[--outputStart] = (byte) mod; + } + + while (outputStart < decoded.length && decoded[outputStart] == 0) { + outputStart++; + } + + byte[] result = new byte[decoded.length - outputStart + zeros]; + for (int i = 0; i < zeros; i++) { + result[i] = 0; + } + System.arraycopy(decoded, outputStart, result, zeros, decoded.length - outputStart); + return result; + } + + private static int divmod256(byte[] number58, int startAt) { + int remainder = 0; + for (int i = startAt; i < number58.length; i++) { + int digit58 = number58[i] & 0xFF; + int temp = remainder * 58 + digit58; + number58[i] = (byte) (temp / 256); + remainder = temp % 256; + } + return remainder; + } +} diff --git a/SHiNE-server/shine-server-db/src/main/java/shine/db/dao/BlockchainStateDAO.java b/SHiNE-server/shine-server-db/src/main/java/shine/db/dao/BlockchainStateDAO.java index 88bf71a9..b11e29c7 100644 --- a/SHiNE-server/shine-server-db/src/main/java/shine/db/dao/BlockchainStateDAO.java +++ b/SHiNE-server/shine-server-db/src/main/java/shine/db/dao/BlockchainStateDAO.java @@ -229,7 +229,7 @@ public final class BlockchainStateDAO { private static void setBytesNullable(PreparedStatement ps, int index, byte[] b) throws SQLException { if (b != null) ps.setBytes(index, b); - else ps.setNull(index, Types.BLOB); + else ps.setNull(index, Types.BINARY); } private static String nn(String s) { return s == null ? "" : s; } diff --git a/SHiNE-server/shine-server-db/src/main/java/shine/db/dao/BlocksDAO.java b/SHiNE-server/shine-server-db/src/main/java/shine/db/dao/BlocksDAO.java index f61c75ce..2719c31c 100644 --- a/SHiNE-server/shine-server-db/src/main/java/shine/db/dao/BlocksDAO.java +++ b/SHiNE-server/shine-server-db/src/main/java/shine/db/dao/BlocksDAO.java @@ -90,7 +90,7 @@ public final class BlocksDAO { else ps.setNull(i++, Types.INTEGER); if (e.getToBlockHash() != null) ps.setBytes(i++, e.getToBlockHash()); - else ps.setNull(i++, Types.BLOB); + else ps.setNull(i++, Types.BINARY); ps.setBytes(i++, e.getBlockHash()); ps.setBytes(i++, e.getBlockSignature()); @@ -106,7 +106,7 @@ public final class BlocksDAO { else ps.setNull(i++, Types.INTEGER); if (e.getPrevLineHash() != null) ps.setBytes(i++, e.getPrevLineHash()); - else ps.setNull(i++, Types.BLOB); + else ps.setNull(i++, Types.BINARY); if (e.getThisLineNumber() != null) ps.setInt(i++, e.getThisLineNumber()); else ps.setNull(i++, Types.INTEGER); diff --git a/SHiNE-server/shine-server-db/src/main/java/shine/db/dao/SolanaUserPdaCurrentDAO.java b/SHiNE-server/shine-server-db/src/main/java/shine/db/dao/SolanaUserPdaCurrentDAO.java new file mode 100644 index 00000000..c66e975f --- /dev/null +++ b/SHiNE-server/shine-server-db/src/main/java/shine/db/dao/SolanaUserPdaCurrentDAO.java @@ -0,0 +1,78 @@ +package shine.db.dao; + +import shine.db.DbController; +import shine.db.KeyEncodingUtil; +import shine.db.entities.SolanaUserPdaCurrentEntry; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +/** + * Доступ к актуальной PostgreSQL-таблице пользователей, поддерживаемой sync-модулем. + */ +public final class SolanaUserPdaCurrentDAO { + + private static volatile SolanaUserPdaCurrentDAO instance; + + private final DbController db = DbController.getInstance(); + + private SolanaUserPdaCurrentDAO() {} + + public static SolanaUserPdaCurrentDAO getInstance() { + if (instance == null) { + synchronized (SolanaUserPdaCurrentDAO.class) { + if (instance == null) { + instance = new SolanaUserPdaCurrentDAO(); + } + } + } + return instance; + } + + public SolanaUserPdaCurrentEntry getByBlockchainName(String blockchainName) throws SQLException { + if (!db.isPostgres()) { + return null; + } + try (Connection c = db.getConnection()) { + return getByBlockchainName(c, blockchainName); + } + } + + public SolanaUserPdaCurrentEntry getByBlockchainName(Connection c, String blockchainName) throws SQLException { + if (!db.isPostgres()) { + return null; + } + + String sql = """ + SELECT + login, + blockchain_name, + blockchain_key, + paid_limit_bytes + FROM solana_user_pda_current + WHERE blockchain_name = ? + LIMIT 1 + """; + + try (PreparedStatement ps = c.prepareStatement(sql)) { + ps.setString(1, blockchainName); + try (ResultSet rs = ps.executeQuery()) { + if (!rs.next()) { + return null; + } + return mapRow(rs); + } + } + } + + private SolanaUserPdaCurrentEntry mapRow(ResultSet rs) throws SQLException { + SolanaUserPdaCurrentEntry entry = new SolanaUserPdaCurrentEntry(); + entry.setLogin(rs.getString("login")); + entry.setBlockchainName(rs.getString("blockchain_name")); + entry.setBlockchainKey(KeyEncodingUtil.normalizeKeyToBase64_32(rs.getString("blockchain_key"))); + entry.setPaidLimitBytes(rs.getLong("paid_limit_bytes")); + return entry; + } +} diff --git a/SHiNE-server/shine-server-db/src/main/java/shine/db/dao/SolanaUsersDAO.java b/SHiNE-server/shine-server-db/src/main/java/shine/db/dao/SolanaUsersDAO.java index 8395e1e0..d1f588eb 100644 --- a/SHiNE-server/shine-server-db/src/main/java/shine/db/dao/SolanaUsersDAO.java +++ b/SHiNE-server/shine-server-db/src/main/java/shine/db/dao/SolanaUsersDAO.java @@ -1,6 +1,7 @@ package shine.db.dao; import shine.db.DbController; +import shine.db.KeyEncodingUtil; import shine.db.entities.SolanaUserEntry; import java.sql.*; @@ -242,9 +243,9 @@ public final class SolanaUsersDAO { e.setLogin(rs.getString("login")); e.setBlockchainName(rs.getString("blockchain_name")); - e.setSolanaKey(rs.getString("solana_key")); - e.setBlockchainKey(rs.getString("blockchain_key")); - e.setClientKey(rs.getString("client_key")); + e.setSolanaKey(KeyEncodingUtil.normalizeKeyToBase64_32(rs.getString("solana_key"))); + e.setBlockchainKey(KeyEncodingUtil.normalizeKeyToBase64_32(rs.getString("blockchain_key"))); + e.setClientKey(KeyEncodingUtil.normalizeKeyToBase64_32(rs.getString("client_key"))); return e; } diff --git a/SHiNE-server/shine-server-db/src/main/java/shine/db/dao/UserCreateDAO.java b/SHiNE-server/shine-server-db/src/main/java/shine/db/dao/UserCreateDAO.java index 86065d2e..6c71d37e 100644 --- a/SHiNE-server/shine-server-db/src/main/java/shine/db/dao/UserCreateDAO.java +++ b/SHiNE-server/shine-server-db/src/main/java/shine/db/dao/UserCreateDAO.java @@ -119,7 +119,7 @@ public final class UserCreateDAO { ps.setLong(i++, 0L); ps.setInt(i++, -1); - ps.setNull(i++, Types.BLOB); // старт: блоков ещё нет + ps.setNull(i++, Types.BINARY); // старт: блоков ещё нет ps.setLong(i++, nowMs); ps.executeUpdate(); // если blockchainName занят -> constraint (PK) diff --git a/SHiNE-server/shine-server-db/src/main/java/shine/db/entities/SolanaUserPdaCurrentEntry.java b/SHiNE-server/shine-server-db/src/main/java/shine/db/entities/SolanaUserPdaCurrentEntry.java new file mode 100644 index 00000000..33fcce29 --- /dev/null +++ b/SHiNE-server/shine-server-db/src/main/java/shine/db/entities/SolanaUserPdaCurrentEntry.java @@ -0,0 +1,46 @@ +package shine.db.entities; + +/** + * Минимальный срез текущей записи пользователя из PostgreSQL sync-таблицы. + * + * Источник: solana_user_pda_current + */ +public final class SolanaUserPdaCurrentEntry { + + private String login; + private String blockchainName; + private String blockchainKey; + private long paidLimitBytes; + + 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 getBlockchainKey() { + return blockchainKey; + } + + public void setBlockchainKey(String blockchainKey) { + this.blockchainKey = blockchainKey; + } + + public long getPaidLimitBytes() { + return paidLimitBytes; + } + + public void setPaidLimitBytes(long paidLimitBytes) { + this.paidLimitBytes = paidLimitBytes; + } +} diff --git a/SHiNE-server/src/main/java/server/sync/PeriodicBlockchainSyncService.java b/SHiNE-server/src/main/java/server/sync/PeriodicBlockchainSyncService.java index ddf7f7e2..f88f49bd 100644 --- a/SHiNE-server/src/main/java/server/sync/PeriodicBlockchainSyncService.java +++ b/SHiNE-server/src/main/java/server/sync/PeriodicBlockchainSyncService.java @@ -13,7 +13,9 @@ import shine.db.dao.BlockchainStateDAO; import shine.db.dao.SyncServersDAO; import shine.db.dao.UserCreateDAO; import shine.db.dao.SolanaUsersDAO; +import shine.db.dao.SolanaUserPdaCurrentDAO; import shine.db.entities.BlockchainStateEntry; +import shine.db.entities.SolanaUserPdaCurrentEntry; import shine.db.entities.SyncServerEntry; import server.sync.BlockchainResyncGuard; import utils.files.FileStoreUtil; @@ -57,6 +59,7 @@ public final class PeriodicBlockchainSyncService { private static final BlockchainResyncCleanupDAO RESYNC_CLEANUP_DAO = BlockchainResyncCleanupDAO.getInstance(); private static final FileStoreUtil FILE_STORE = FileStoreUtil.getInstance(); private static final SolanaUsersDAO SOLANA_USERS_DAO = SolanaUsersDAO.getInstance(); + private static final SolanaUserPdaCurrentDAO SOLANA_USER_PDA_CURRENT_DAO = SolanaUserPdaCurrentDAO.getInstance(); private static final String CONFIG_IMPORT_PROFILE_FROM_PARTNER = "sync.importUserProfileFromPartner.enabled"; private PeriodicBlockchainSyncService() {} @@ -354,9 +357,15 @@ public final class PeriodicBlockchainSyncService { private static boolean ensureLocalChainExists(SyncServerEntry partner, String blockchainName) { try { - if (STATE_DAO.getByBlockchainName(blockchainName) != null) { + BlockchainStateEntry existingState = STATE_DAO.getByBlockchainName(blockchainName); + if (hasValidBlockchainKey(existingState)) { return true; } + + if (ensureLocalChainStateFromCurrentPostgresSnapshot(blockchainName, existingState)) { + return true; + } + String login = BlockchainNameUtil.loginFromBlockchainName(blockchainName); if (login == null || login.isBlank()) { return false; @@ -373,6 +382,40 @@ public final class PeriodicBlockchainSyncService { } } + private static boolean ensureLocalChainStateFromCurrentPostgresSnapshot(String blockchainName, + BlockchainStateEntry existingState) throws Exception { + SolanaUserPdaCurrentEntry currentUser = SOLANA_USER_PDA_CURRENT_DAO.getByBlockchainName(blockchainName); + if (currentUser == null) { + return false; + } + + BlockchainStateEntry state = new BlockchainStateEntry(); + state.setBlockchainName(currentUser.getBlockchainName()); + state.setLogin(currentUser.getLogin()); + state.setBlockchainKey(currentUser.getBlockchainKey()); + state.setSizeLimit(currentUser.getPaidLimitBytes() > 0 ? currentUser.getPaidLimitBytes() : 100_000L); + state.setFileSizeBytes(existingState == null ? 0L : existingState.getFileSizeBytes()); + state.setLastBlockNumber(existingState == null ? -1 : existingState.getLastBlockNumber()); + state.setLastBlockHash(existingState == null ? null : existingState.getLastBlockHash()); + state.setUpdatedAtMs(System.currentTimeMillis()); + + if (existingState == null) { + STATE_DAO.insertIfMissing(state); + } else { + STATE_DAO.upsert(state); + } + + return hasValidBlockchainKey(STATE_DAO.getByBlockchainName(blockchainName)); + } + + private static boolean hasValidBlockchainKey(BlockchainStateEntry state) { + if (state == null) { + return false; + } + byte[] keyBytes = state.getBlockchainKeyBytes(); + return keyBytes != null && keyBytes.length == 32; + } + private static boolean importUserProfileFromPartner(SyncServerEntry partner, String login) throws Exception { if (partner == null || partner.getServerAddress() == null || partner.getServerAddress().isBlank()) { return false; diff --git a/VERSION.properties b/VERSION.properties index ef92e756..e63d5594 100644 --- a/VERSION.properties +++ b/VERSION.properties @@ -1,2 +1,2 @@ client.version=1.2.354 -server.version=1.2.322 +server.version=1.2.323