Сервер: починить восстановление chain-state из PostgreSQL sync-таблиц

This commit is contained in:
AidarKC
2026-07-24 22:22:37 +04:00
parent 1406111f22
commit ce5595bc16
9 changed files with 292 additions and 9 deletions
@@ -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;
}
}
@@ -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; }
@@ -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);
@@ -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;
}
}
@@ -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;
}
@@ -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)
@@ -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;
}
}
@@ -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;
+1 -1
View File
@@ -1,2 +1,2 @@
client.version=1.2.354
server.version=1.2.322
server.version=1.2.323