Ещё промежуточный комит верии - не работает :)   4
This commit is contained in:
AidarKC 2025-12-17 18:17:21 +03:00
parent 45a862b11f
commit 2037ebaa8b
5 changed files with 111 additions and 154 deletions

View File

@ -21,8 +21,14 @@ public final class BlockchainStateDAO {
return instance; return instance;
} }
// --- Новый вариант: работа на переданном соединении --- // старый метод оставим
public BlockchainStateEntry getByBlockchainId(Connection conn, long blockchainId) throws SQLException { public BlockchainStateEntry getByBlockchainId(long blockchainId) throws SQLException {
try (Connection c = db.getConnection()) {
return getByBlockchainId(c, blockchainId);
}
}
public BlockchainStateEntry getByBlockchainId(Connection c, long blockchainId) throws SQLException {
String sql = """ String sql = """
SELECT SELECT
blockchain_id, blockchain_id,
@ -45,7 +51,7 @@ public final class BlockchainStateDAO {
WHERE blockchain_id = ? WHERE blockchain_id = ?
"""; """;
try (PreparedStatement ps = conn.prepareStatement(sql)) { try (PreparedStatement ps = c.prepareStatement(sql)) {
ps.setLong(1, blockchainId); ps.setLong(1, blockchainId);
try (ResultSet rs = ps.executeQuery()) { try (ResultSet rs = ps.executeQuery()) {
if (!rs.next()) return null; if (!rs.next()) return null;
@ -54,18 +60,14 @@ public final class BlockchainStateDAO {
} }
} }
// Старый вариант: сам открывает/закрывает conn // старый метод оставим
public BlockchainStateEntry getByBlockchainId(long blockchainId) throws SQLException { public void upsert(BlockchainStateEntry e) throws SQLException {
try (Connection conn = db.getConnection()) { try (Connection c = db.getConnection()) {
return getByBlockchainId(conn, blockchainId); upsert(c, e);
} }
} }
// --- Новый вариант: UPSERT на переданном соединении --- public void upsert(Connection c, BlockchainStateEntry e) throws SQLException {
public void upsert(Connection conn, BlockchainStateEntry e) throws SQLException {
long now = System.currentTimeMillis();
if (e.getUpdatedAtMs() <= 0) e.setUpdatedAtMs(now);
String sql = """ String sql = """
INSERT INTO blockchain_state ( INSERT INTO blockchain_state (
blockchain_id, blockchain_id,
@ -104,7 +106,6 @@ public final class BlockchainStateDAO {
size_bytes = excluded.size_bytes, size_bytes = excluded.size_bytes,
last_global_number = excluded.last_global_number, last_global_number = excluded.last_global_number,
last_global_hash = excluded.last_global_hash, last_global_hash = excluded.last_global_hash,
line0_last_number = excluded.line0_last_number, line0_last_number = excluded.line0_last_number,
line0_last_hash = excluded.line0_last_hash, line0_last_hash = excluded.line0_last_hash,
line1_last_number = excluded.line1_last_number, line1_last_number = excluded.line1_last_number,
@ -121,11 +122,10 @@ public final class BlockchainStateDAO {
line6_last_hash = excluded.line6_last_hash, line6_last_hash = excluded.line6_last_hash,
line7_last_number = excluded.line7_last_number, line7_last_number = excluded.line7_last_number,
line7_last_hash = excluded.line7_last_hash, line7_last_hash = excluded.line7_last_hash,
updated_at_ms = excluded.updated_at_ms updated_at_ms = excluded.updated_at_ms
"""; """;
try (PreparedStatement ps = conn.prepareStatement(sql)) { try (PreparedStatement ps = c.prepareStatement(sql)) {
int i = 1; int i = 1;
ps.setLong(i++, e.getBlockchainId()); ps.setLong(i++, e.getBlockchainId());
ps.setString(i++, nn(e.getUserLogin())); ps.setString(i++, nn(e.getUserLogin()));
@ -145,13 +145,6 @@ public final class BlockchainStateDAO {
} }
} }
// Старый вариант: сам открывает/закрывает conn
public void upsert(BlockchainStateEntry e) throws SQLException {
try (Connection conn = db.getConnection()) {
upsert(conn, e);
}
}
private BlockchainStateEntry mapRow(ResultSet rs) throws SQLException { private BlockchainStateEntry mapRow(ResultSet rs) throws SQLException {
BlockchainStateEntry e = new BlockchainStateEntry(); BlockchainStateEntry e = new BlockchainStateEntry();
e.setBlockchainId(rs.getLong("blockchain_id")); e.setBlockchainId(rs.getLong("blockchain_id"));
@ -173,7 +166,5 @@ public final class BlockchainStateDAO {
return e; return e;
} }
private static String nn(String s) { private static String nn(String s) { return s == null ? "" : s; }
return s == null ? "" : s;
}
} }

View File

@ -76,6 +76,23 @@ public final class SolanaUsersDAO {
} }
} }
// добавь рядом со старым методом
public SolanaUserEntry getByLogin(Connection c, String login) throws SQLException {
String sql = """
SELECT login, loginId, bchId, loginKey, deviceKey, bchLimit
FROM solana_users
WHERE LOWER(login) = LOWER(?)
""";
try (PreparedStatement ps = c.prepareStatement(sql)) {
ps.setString(1, login);
try (ResultSet rs = ps.executeQuery()) {
if (!rs.next()) return null;
return mapRow(rs);
}
}
}
public SolanaUserEntry getByLogin(String login) throws SQLException { public SolanaUserEntry getByLogin(String login) throws SQLException {
String sql = """ String sql = """
SELECT login, loginId, bchId, loginKey, deviceKey, bchLimit SELECT login, loginId, bchId, loginKey, deviceKey, bchLimit

View File

@ -1,15 +1,18 @@
package server.logic.ws_protocol.JSON.handlers.blockchain; package server.logic.ws_protocol.JSON.handlers.blockchain;
import blockchain_new.BchBlockEntry_new; import blockchain_new.BchBlockEntry_new;
import shine.db.SqliteDbController;
import shine.db.dao.BlockchainStateDAO; import shine.db.dao.BlockchainStateDAO;
import shine.db.dao.SolanaUsersDAO; import shine.db.dao.SolanaUsersDAO;
import shine.db.entities.BlockchainStateEntry; import shine.db.entities.BlockchainStateEntry;
import shine.db.entities.SolanaUserEntry; import shine.db.entities.SolanaUserEntry;
import utils.files.FileStoreUtil; import utils.files.FileStoreUtil;
import java.sql.Connection;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.Base64; import java.util.Base64;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.ReentrantLock;
public final class BlockchainStateService_new { public final class BlockchainStateService_new {
@ -26,7 +29,6 @@ public final class BlockchainStateService_new {
this.stateAfter = stateAfter; this.stateAfter = stateAfter;
this.lineIndex = lineIndex; this.lineIndex = lineIndex;
} }
public boolean isOk() { return reasonCode == null && httpStatus == 200; } public boolean isOk() { return reasonCode == null && httpStatus == 200; }
} }
@ -34,23 +36,16 @@ public final class BlockchainStateService_new {
public static BlockchainStateService_new getInstance() { return INSTANCE; } public static BlockchainStateService_new getInstance() { return INSTANCE; }
private BlockchainStateService_new() {} private BlockchainStateService_new() {}
// ===== locks per blockchainId (MVP: один сервер) =====
private static final ConcurrentHashMap<Long, ReentrantLock> LOCKS = new ConcurrentHashMap<>();
private static ReentrantLock lockFor(long blockchainId) {
return LOCKS.computeIfAbsent(blockchainId, id -> new ReentrantLock());
}
// ===== constants =====
private static final String ZERO64 = "0".repeat(64); private static final String ZERO64 = "0".repeat(64);
// MVP: заглавный блок // Локи по blockchainId (MVP, один сервер)
// (пока без парсинга тела, просто по номеру) private final ConcurrentMap<Long, ReentrantLock> locks = new ConcurrentHashMap<>();
private static boolean isHeaderBlock(int globalNumber, int lineNumber) {
return globalNumber == 0 && lineNumber == 0; private ReentrantLock lockFor(long blockchainId) {
return locks.computeIfAbsent(blockchainId, k -> new ReentrantLock());
} }
public Result addBlock( public Result addBlockAtomically(
String login, String login,
long blockchainId, long blockchainId,
int globalNumber, int globalNumber,
@ -81,101 +76,90 @@ public final class BlockchainStateService_new {
return new Result(400, "BAD_BLOCK_FORMAT", null, -1); return new Result(400, "BAD_BLOCK_FORMAT", null, -1);
} }
int lineIndex = block.line; // short -> int int lineIndex = block.line;
if (lineIndex < 0 || lineIndex > 7) if (lineIndex < 0 || lineIndex > 7)
return new Result(400, "BAD_LINE_INDEX", null, lineIndex); return new Result(400, "BAD_LINE_INDEX", null, lineIndex);
ReentrantLock lock = lockFor(blockchainId); ReentrantLock lock = lockFor(blockchainId);
lock.lock(); lock.lock();
try { try (Connection conn = SqliteDbController.getInstance().getConnection()) {
BlockchainStateEntry state = BlockchainStateDAO.getInstance().getByBlockchainId(blockchainId);
BlockchainStateDAO stateDao = BlockchainStateDAO.getInstance();
SolanaUsersDAO usersDao = SolanaUsersDAO.getInstance();
// читаем state В ЭТОМ ЖЕ conn
BlockchainStateEntry state = stateDao.getByBlockchainId(conn, blockchainId);
boolean isHeaderBlock = (globalNumber == 0 && lineIndex == 0 && block.lineNumber == 0);
// ===== GENESIS ветка: state ещё нет =====
if (state == null) { if (state == null) {
// разрешаем только заглавный блок // state отсутствует разрешаем ТОЛЬКО header-блок
if (!isHeaderBlock(globalNumber, block.lineNumber)) { if (!isHeaderBlock) {
return new Result(404, "UNKNOWN_BLOCKCHAIN", null, lineIndex); return new Result(404, "UNKNOWN_BLOCKCHAIN", null, lineIndex);
} }
// создаём первичное состояние (last_global=-1, hash=ZERO64, lines=0/ZERO64) // Проверяем пользователя и соответствие bchId
state = createInitialStateFromUser(login, blockchainId); SolanaUserEntry u = usersDao.getByLogin(conn, login);
if (state == null) { if (u == null) {
// нет такого юзера / не его bchId return new Result(404, "UNKNOWN_USER", null, lineIndex);
return new Result(404, "UNKNOWN_BLOCKCHAIN", null, lineIndex); }
if (u.getBchId() != blockchainId) {
return new Result(403, "BCHID_MISMATCH", null, lineIndex);
} }
// сохраняем стартовую строку // prevGlobalHash для header должен быть нулевой
BlockchainStateDAO.getInstance().upsert(state); if (!eqHash(prevGlobalHashHex, ZERO64)) {
return new Result(409, "GLOBAL_HASH_MISMATCH", null, lineIndex);
}
// Создаём нулевой state ДО записи header (last_global_number = -1)
state = createInitialState(blockchainId, login, u.getLoginKey(), safeLimit(u.getBchLimit()));
// stateDao.upsert(conn, state); //TODO так здесь наверное его в БД сохранять не надо если всё верно то потом дополненный сохраниться
} else {
// state есть обычная проверка login
if (!login.equals(state.getUserLogin())) {
return new Result(403, "LOGIN_MISMATCH", state, lineIndex);
}
} }
// 1) защита от подмены логина // Перечитывать не надо, state актуален в переменной.
if (!login.equals(state.getUserLogin())) {
return new Result(403, "LOGIN_MISMATCH", state, lineIndex);
}
// 2) expected global: last_global + 1 (у нас last_global стартует -1) // expected global
int expectedGlobal = state.getLastGlobalNumber() + 1; int expectedGlobal = state.getLastGlobalNumber() + 1;
if (globalNumber != expectedGlobal) { if (globalNumber != expectedGlobal) {
return new Result(409, "OUT_OF_SEQUENCE_GLOBAL", state, lineIndex); return new Result(409, "OUT_OF_SEQUENCE_GLOBAL", state, lineIndex);
} }
// 3) prev global hash // prev global hash сверяем
String dbPrevGlobalHash = nn(state.getLastGlobalHash()); String dbPrevGlobalHash = nn(state.getLastGlobalHash());
if (!eqHash(prevGlobalHashHex, dbPrevGlobalHash)) { if (!eqHash(prevGlobalHashHex, dbPrevGlobalHash)) {
return new Result(409, "GLOBAL_HASH_MISMATCH", state, lineIndex); return new Result(409, "GLOBAL_HASH_MISMATCH", state, lineIndex);
} }
// 4) lineNumber // expected line number
// Нормально: первый обычный блок по линии должен быть lineNumber=1 при lastLine=0
// Исключение: заглавный блок имеет lineNumber=0
int expectedLineNumber = state.getLastLineNumber(lineIndex) + 1; int expectedLineNumber = state.getLastLineNumber(lineIndex) + 1;
boolean header = isHeaderBlock(globalNumber, block.lineNumber); if (block.lineNumber != expectedLineNumber) {
return new Result(409, "OUT_OF_SEQUENCE_LINE", state, lineIndex);
if (!header) {
if (block.lineNumber != expectedLineNumber) {
return new Result(409, "OUT_OF_SEQUENCE_LINE", state, lineIndex);
}
} else {
// заглавный блок допускаем только если текущий lastLineNumber == 0 и пришёл 0
if (state.getLastLineNumber(lineIndex) != 0 || block.lineNumber != 0) {
return new Result(409, "BAD_HEADER_LINE_NUMBER", state, lineIndex);
}
} }
// 5) prevLineHash берём из БД (пока просто читаем) // TODO: крипто-проверка (потом подключим)
String dbPrevLineHashHex = nn(state.getLastLineHash(lineIndex));
// (можешь позже сравнивать с тем, что внутри блока, если там есть prevLineHash)
// 6) крипто-проверка (позже) // 1) запись блока в файл
// TODO:
// - восстановить preimage
// - sha256(preimage) == block.hash32
// - Ed25519 verify signature
// если не ок: return new Result(422, "CRYPTO_INVALID", state, lineIndex);
// 7) запись блока в файл
FileStoreUtil.getInstance().addDataToBlockchain(blockchainId, block.toBytes()); FileStoreUtil.getInstance().addDataToBlockchain(blockchainId, block.toBytes());
// 8) апдейт состояния // 2) апдейт state
String newHashHex = bytesToHex(block.getHash32());
state.setLastGlobalNumber(globalNumber); state.setLastGlobalNumber(globalNumber);
state.setLastGlobalHash(bytesToHex(block.getHash32())); state.setLastGlobalHash(newHashHex);
// line number: state.setLastLineNumber(lineIndex, block.lineNumber);
// - для заглавного блока оставляем 0 state.setLastLineHash(lineIndex, newHashHex);
// - для остальных двигаем как обычно
if (!header) {
state.setLastLineNumber(lineIndex, block.lineNumber);
} else {
state.setLastLineNumber(lineIndex, 0);
}
// line hash обновляем в любом случае (так проще для цепочки)
state.setLastLineHash(lineIndex, bytesToHex(block.getHash32()));
state.setSizeBytes(state.getSizeBytes() + fullBytes.length); state.setSizeBytes(state.getSizeBytes() + fullBytes.length);
state.setUpdatedAtMs(System.currentTimeMillis()); state.setUpdatedAtMs(System.currentTimeMillis());
BlockchainStateDAO.getInstance().upsert(state); stateDao.upsert(conn, state);
return new Result(200, null, state, lineIndex); return new Result(200, null, state, lineIndex);
@ -186,30 +170,19 @@ public final class BlockchainStateService_new {
} }
} }
/** private static BlockchainStateEntry createInitialState(long blockchainId,
* Создаёт стартовое состояние по данным пользователя: String login,
* - проверяем, что login существует и что bchId совпадает с blockchainId String loginKeyBase64,
* - public_key_base64 берём из loginKey int sizeLimit) {
*/
private static BlockchainStateEntry createInitialStateFromUser(String login, long blockchainId) throws SQLException {
SolanaUserEntry u = SolanaUsersDAO.getInstance().getByLogin(login);
if (u == null) return null;
if (u.getBchId() != blockchainId) return null;
BlockchainStateEntry s = new BlockchainStateEntry(); BlockchainStateEntry s = new BlockchainStateEntry();
s.setBlockchainId(blockchainId); s.setBlockchainId(blockchainId);
s.setUserLogin(login); s.setUserLogin(login);
s.setPublicKeyBase64(nn(loginKeyBase64));
// публичный ключ для блокчейна = loginKey (как ты и хочешь) s.setSizeLimit(sizeLimit);
s.setPublicKeyBase64(nn(u.getLoginKey()));
// лимит (пока тестовый / из пользователя)
int limit = (u.getBchLimit() != null) ? u.getBchLimit() : 1_000_000;
s.setSizeLimit(limit);
s.setSizeBytes(0); s.setSizeBytes(0);
// ВАЖНО: стартовые значения // как ты хочешь:
s.setLastGlobalNumber(-1); s.setLastGlobalNumber(-1);
s.setLastGlobalHash(ZERO64); s.setLastGlobalHash(ZERO64);
@ -222,6 +195,11 @@ public final class BlockchainStateService_new {
return s; return s;
} }
private static int safeLimit(Integer limit) {
if (limit == null || limit <= 0) return 1_000_000; // fallback (test)
return limit;
}
private static String nn(String s) { return s == null ? "" : s; } private static String nn(String s) { return s == null ? "" : s; }
private static boolean eqHash(String a, String b) { private static boolean eqHash(String a, String b) {

View File

@ -14,7 +14,7 @@ public final class Net_AddBlock_new_Handler implements JsonMessageHandler {
public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) throws Exception { public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) throws Exception {
Net_AddBlock_new_Request req = (Net_AddBlock_new_Request) baseReq; Net_AddBlock_new_Request req = (Net_AddBlock_new_Request) baseReq;
var r = BlockchainStateService_new.getInstance().addBlock( var r = BlockchainStateService_new.getInstance().addBlockAtomically(
req.getLogin(), req.getLogin(),
req.getBlockchainId(), req.getBlockchainId(),
req.getGlobalNumber(), req.getGlobalNumber(),
@ -25,7 +25,6 @@ public final class Net_AddBlock_new_Handler implements JsonMessageHandler {
Net_AddBlock_new_Response resp = new Net_AddBlock_new_Response(); Net_AddBlock_new_Response resp = new Net_AddBlock_new_Response();
resp.setOp(req.getOp()); resp.setOp(req.getOp());
resp.setRequestId(req.getRequestId()); resp.setRequestId(req.getRequestId());
resp.setLineIndex(r.lineIndex); resp.setLineIndex(r.lineIndex);
if (r.isOk()) { if (r.isOk()) {

View File

@ -10,9 +10,7 @@ import server.logic.ws_protocol.JSON.entyties.tempToTest.Net_AddUser_Response;
import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler; import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory; import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
import server.logic.ws_protocol.WireCodes; import server.logic.ws_protocol.WireCodes;
import shine.db.dao.BlockchainStateDAO;
import shine.db.dao.SolanaUsersDAO; import shine.db.dao.SolanaUsersDAO;
import shine.db.entities.BlockchainStateEntry;
import shine.db.entities.SolanaUserEntry; import shine.db.entities.SolanaUserEntry;
import java.sql.SQLException; import java.sql.SQLException;
@ -21,34 +19,32 @@ public class Net_AddUser_Handler implements JsonMessageHandler {
private static final Logger log = LoggerFactory.getLogger(Net_AddUser_Handler.class); private static final Logger log = LoggerFactory.getLogger(Net_AddUser_Handler.class);
// ====== TEST CONST (пока так) ====== /** TEST ONLY: лимит блокчейна по умолчанию. Потом заменишь на норм логику. */
private static final int TEST_BCH_LIMIT = 1_000_000; private static final int TEST_BCH_LIMIT = 1_000_000;
private static final String ZERO64 = "0".repeat(64);
@Override @Override
public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) throws Exception { public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) throws Exception {
Net_AddUser_Request req = (Net_AddUser_Request) baseRequest; Net_AddUser_Request req = (Net_AddUser_Request) baseRequest;
if (req.getLogin() == null || req.getLogin().isBlank() if (req.getLogin() == null || req.getLogin().isBlank()
|| req.getLoginKey() == null || req.getLoginKey().isBlank() || req.getLoginKey() == null || req.getLoginKey().isBlank()
|| req.getDeviceKey() == null || req.getDeviceKey().isBlank()) { || req.getDeviceKey() == null || req.getDeviceKey().isBlank()
|| req.getLoginId() <= 0
|| req.getBchId() <= 0) {
return NetExceptionResponseFactory.error( return NetExceptionResponseFactory.error(
req, req,
WireCodes.Status.BAD_REQUEST, WireCodes.Status.BAD_REQUEST,
"BAD_FIELDS", "BAD_FIELDS",
"Некорректные или пустые поля: login, loginKey, deviceKey" "Некорректные поля: login/loginId/bchId/loginKey/deviceKey"
); );
} }
// bchLimit: если клиент не прислал ставим тестовую константу
Integer limit = req.getBchLimit(); Integer limit = req.getBchLimit();
if (limit == null || limit <= 0) limit = TEST_BCH_LIMIT; if (limit == null || limit <= 0) limit = TEST_BCH_LIMIT;
try { try {
SolanaUsersDAO users = SolanaUsersDAO.getInstance(); SolanaUsersDAO dao = SolanaUsersDAO.getInstance();
BlockchainStateDAO stateDao = BlockchainStateDAO.getInstance();
SolanaUserEntry user = new SolanaUserEntry( SolanaUserEntry user = new SolanaUserEntry(
req.getLoginId(), req.getLoginId(),
@ -59,31 +55,7 @@ public class Net_AddUser_Handler implements JsonMessageHandler {
limit limit
); );
users.insert(user); dao.insert(user);
// Создаём стартовую запись blockchain_state
BlockchainStateEntry s = new BlockchainStateEntry();
s.setBlockchainId(req.getBchId());
s.setUserLogin(req.getLogin());
// В блокчейн-стейте храним loginKey как основной pubkey
s.setPublicKeyBase64(req.getLoginKey());
s.setSizeLimit(limit);
s.setSizeBytes(0);
// ВАЖНО: твои стартовые значения
s.setLastGlobalNumber(-1);
s.setLastGlobalHash(ZERO64);
for (int i = 0; i < 8; i++) {
s.setLastLineNumber(i, 0);
s.setLastLineHash(i, ZERO64);
}
s.setUpdatedAtMs(System.currentTimeMillis());
stateDao.upsert(s);
Net_AddUser_Response resp = new Net_AddUser_Response(); Net_AddUser_Response resp = new Net_AddUser_Response();
resp.setOp(req.getOp()); resp.setOp(req.getOp());
@ -96,7 +68,7 @@ public class Net_AddUser_Handler implements JsonMessageHandler {
return resp; return resp;
} catch (SQLException e) { } catch (SQLException e) {
log.error("❌ DB error in AddUser", e); log.error("❌ DB error AddUser", e);
return NetExceptionResponseFactory.error( return NetExceptionResponseFactory.error(
req, req,
WireCodes.Status.SERVER_DATA_ERROR, WireCodes.Status.SERVER_DATA_ERROR,
@ -104,7 +76,7 @@ public class Net_AddUser_Handler implements JsonMessageHandler {
"Ошибка доступа к базе данных" "Ошибка доступа к базе данных"
); );
} catch (Exception e) { } catch (Exception e) {
log.error("❌ Internal error in AddUser", e); log.error("❌ Internal error AddUser", e);
return NetExceptionResponseFactory.error( return NetExceptionResponseFactory.error(
req, req,
WireCodes.Status.INTERNAL_ERROR, WireCodes.Status.INTERNAL_ERROR,