SHA256
Сервер: починить восстановление chain-state из PostgreSQL sync-таблиц
This commit is contained in:
@@ -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 {
|
private static void setBytesNullable(PreparedStatement ps, int index, byte[] b) throws SQLException {
|
||||||
if (b != null) ps.setBytes(index, b);
|
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; }
|
private static String nn(String s) { return s == null ? "" : s; }
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ public final class BlocksDAO {
|
|||||||
else ps.setNull(i++, Types.INTEGER);
|
else ps.setNull(i++, Types.INTEGER);
|
||||||
|
|
||||||
if (e.getToBlockHash() != null) ps.setBytes(i++, e.getToBlockHash());
|
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.getBlockHash());
|
||||||
ps.setBytes(i++, e.getBlockSignature());
|
ps.setBytes(i++, e.getBlockSignature());
|
||||||
@@ -106,7 +106,7 @@ public final class BlocksDAO {
|
|||||||
else ps.setNull(i++, Types.INTEGER);
|
else ps.setNull(i++, Types.INTEGER);
|
||||||
|
|
||||||
if (e.getPrevLineHash() != null) ps.setBytes(i++, e.getPrevLineHash());
|
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());
|
if (e.getThisLineNumber() != null) ps.setInt(i++, e.getThisLineNumber());
|
||||||
else ps.setNull(i++, Types.INTEGER);
|
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;
|
package shine.db.dao;
|
||||||
|
|
||||||
import shine.db.DbController;
|
import shine.db.DbController;
|
||||||
|
import shine.db.KeyEncodingUtil;
|
||||||
import shine.db.entities.SolanaUserEntry;
|
import shine.db.entities.SolanaUserEntry;
|
||||||
|
|
||||||
import java.sql.*;
|
import java.sql.*;
|
||||||
@@ -242,9 +243,9 @@ public final class SolanaUsersDAO {
|
|||||||
|
|
||||||
e.setLogin(rs.getString("login"));
|
e.setLogin(rs.getString("login"));
|
||||||
e.setBlockchainName(rs.getString("blockchain_name"));
|
e.setBlockchainName(rs.getString("blockchain_name"));
|
||||||
e.setSolanaKey(rs.getString("solana_key"));
|
e.setSolanaKey(KeyEncodingUtil.normalizeKeyToBase64_32(rs.getString("solana_key")));
|
||||||
e.setBlockchainKey(rs.getString("blockchain_key"));
|
e.setBlockchainKey(KeyEncodingUtil.normalizeKeyToBase64_32(rs.getString("blockchain_key")));
|
||||||
e.setClientKey(rs.getString("client_key"));
|
e.setClientKey(KeyEncodingUtil.normalizeKeyToBase64_32(rs.getString("client_key")));
|
||||||
|
|
||||||
return e;
|
return e;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ public final class UserCreateDAO {
|
|||||||
ps.setLong(i++, 0L);
|
ps.setLong(i++, 0L);
|
||||||
|
|
||||||
ps.setInt(i++, -1);
|
ps.setInt(i++, -1);
|
||||||
ps.setNull(i++, Types.BLOB); // старт: блоков ещё нет
|
ps.setNull(i++, Types.BINARY); // старт: блоков ещё нет
|
||||||
ps.setLong(i++, nowMs);
|
ps.setLong(i++, nowMs);
|
||||||
|
|
||||||
ps.executeUpdate(); // если blockchainName занят -> constraint (PK)
|
ps.executeUpdate(); // если blockchainName занят -> constraint (PK)
|
||||||
|
|||||||
+46
@@ -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.SyncServersDAO;
|
||||||
import shine.db.dao.UserCreateDAO;
|
import shine.db.dao.UserCreateDAO;
|
||||||
import shine.db.dao.SolanaUsersDAO;
|
import shine.db.dao.SolanaUsersDAO;
|
||||||
|
import shine.db.dao.SolanaUserPdaCurrentDAO;
|
||||||
import shine.db.entities.BlockchainStateEntry;
|
import shine.db.entities.BlockchainStateEntry;
|
||||||
|
import shine.db.entities.SolanaUserPdaCurrentEntry;
|
||||||
import shine.db.entities.SyncServerEntry;
|
import shine.db.entities.SyncServerEntry;
|
||||||
import server.sync.BlockchainResyncGuard;
|
import server.sync.BlockchainResyncGuard;
|
||||||
import utils.files.FileStoreUtil;
|
import utils.files.FileStoreUtil;
|
||||||
@@ -57,6 +59,7 @@ public final class PeriodicBlockchainSyncService {
|
|||||||
private static final BlockchainResyncCleanupDAO RESYNC_CLEANUP_DAO = BlockchainResyncCleanupDAO.getInstance();
|
private static final BlockchainResyncCleanupDAO RESYNC_CLEANUP_DAO = BlockchainResyncCleanupDAO.getInstance();
|
||||||
private static final FileStoreUtil FILE_STORE = FileStoreUtil.getInstance();
|
private static final FileStoreUtil FILE_STORE = FileStoreUtil.getInstance();
|
||||||
private static final SolanaUsersDAO SOLANA_USERS_DAO = SolanaUsersDAO.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 static final String CONFIG_IMPORT_PROFILE_FROM_PARTNER = "sync.importUserProfileFromPartner.enabled";
|
||||||
|
|
||||||
private PeriodicBlockchainSyncService() {}
|
private PeriodicBlockchainSyncService() {}
|
||||||
@@ -354,9 +357,15 @@ public final class PeriodicBlockchainSyncService {
|
|||||||
|
|
||||||
private static boolean ensureLocalChainExists(SyncServerEntry partner, String blockchainName) {
|
private static boolean ensureLocalChainExists(SyncServerEntry partner, String blockchainName) {
|
||||||
try {
|
try {
|
||||||
if (STATE_DAO.getByBlockchainName(blockchainName) != null) {
|
BlockchainStateEntry existingState = STATE_DAO.getByBlockchainName(blockchainName);
|
||||||
|
if (hasValidBlockchainKey(existingState)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ensureLocalChainStateFromCurrentPostgresSnapshot(blockchainName, existingState)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
String login = BlockchainNameUtil.loginFromBlockchainName(blockchainName);
|
String login = BlockchainNameUtil.loginFromBlockchainName(blockchainName);
|
||||||
if (login == null || login.isBlank()) {
|
if (login == null || login.isBlank()) {
|
||||||
return false;
|
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 {
|
private static boolean importUserProfileFromPartner(SyncServerEntry partner, String login) throws Exception {
|
||||||
if (partner == null || partner.getServerAddress() == null || partner.getServerAddress().isBlank()) {
|
if (partner == null || partner.getServerAddress() == null || partner.getServerAddress().isBlank()) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
+1
-1
@@ -1,2 +1,2 @@
|
|||||||
client.version=1.2.354
|
client.version=1.2.354
|
||||||
server.version=1.2.322
|
server.version=1.2.323
|
||||||
|
|||||||
Reference in New Issue
Block a user