From 2037ebaa8b35d1c64a38482467ad38c25e25786736f3f0457158f4bd7e9441a8 Mon Sep 17 00:00:00 2001 From: AidarKC Date: Wed, 17 Dec 2025 18:17:21 +0300 Subject: [PATCH] =?UTF-8?q?17=2012=2025=20=D0=95=D1=89=D1=91=20=D0=BF?= =?UTF-8?q?=D1=80=D0=BE=D0=BC=D0=B5=D0=B6=D1=83=D1=82=D0=BE=D1=87=D0=BD?= =?UTF-8?q?=D1=8B=D0=B9=20=D0=BA=D0=BE=D0=BC=D0=B8=D1=82=20=D0=B2=D0=B5?= =?UTF-8?q?=D1=80=D0=B8=D0=B8=20-=20=D0=BD=D0=B5=20=D1=80=D0=B0=D0=B1?= =?UTF-8?q?=D0=BE=D1=82=D0=B0=D0=B5=D1=82=20:)=20=20=204?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/shine/db/dao/BlockchainStateDAO.java | 41 ++--- .../java/shine/db/dao/SolanaUsersDAO.java | 17 ++ .../BlockchainStateService_new.java | 158 ++++++++---------- .../blockchain/Net_AddBlock_new_Handler.java | 3 +- .../tempToTest/Net_AddUser_Handler.java | 46 +---- 5 files changed, 111 insertions(+), 154 deletions(-) diff --git a/shine-server-db/src/main/java/shine/db/dao/BlockchainStateDAO.java b/shine-server-db/src/main/java/shine/db/dao/BlockchainStateDAO.java index db2adcd..022d8f1 100644 --- a/shine-server-db/src/main/java/shine/db/dao/BlockchainStateDAO.java +++ b/shine-server-db/src/main/java/shine/db/dao/BlockchainStateDAO.java @@ -21,8 +21,14 @@ public final class BlockchainStateDAO { 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 = """ SELECT blockchain_id, @@ -45,7 +51,7 @@ public final class BlockchainStateDAO { WHERE blockchain_id = ? """; - try (PreparedStatement ps = conn.prepareStatement(sql)) { + try (PreparedStatement ps = c.prepareStatement(sql)) { ps.setLong(1, blockchainId); try (ResultSet rs = ps.executeQuery()) { if (!rs.next()) return null; @@ -54,18 +60,14 @@ public final class BlockchainStateDAO { } } - // Старый вариант: сам открывает/закрывает conn - public BlockchainStateEntry getByBlockchainId(long blockchainId) throws SQLException { - try (Connection conn = db.getConnection()) { - return getByBlockchainId(conn, blockchainId); + // старый метод оставим + public void upsert(BlockchainStateEntry e) throws SQLException { + try (Connection c = db.getConnection()) { + upsert(c, e); } } - // --- Новый вариант: UPSERT на переданном соединении --- - public void upsert(Connection conn, BlockchainStateEntry e) throws SQLException { - long now = System.currentTimeMillis(); - if (e.getUpdatedAtMs() <= 0) e.setUpdatedAtMs(now); - + public void upsert(Connection c, BlockchainStateEntry e) throws SQLException { String sql = """ INSERT INTO blockchain_state ( blockchain_id, @@ -104,7 +106,6 @@ public final class BlockchainStateDAO { size_bytes = excluded.size_bytes, last_global_number = excluded.last_global_number, last_global_hash = excluded.last_global_hash, - line0_last_number = excluded.line0_last_number, line0_last_hash = excluded.line0_last_hash, line1_last_number = excluded.line1_last_number, @@ -121,11 +122,10 @@ public final class BlockchainStateDAO { line6_last_hash = excluded.line6_last_hash, line7_last_number = excluded.line7_last_number, line7_last_hash = excluded.line7_last_hash, - updated_at_ms = excluded.updated_at_ms """; - try (PreparedStatement ps = conn.prepareStatement(sql)) { + try (PreparedStatement ps = c.prepareStatement(sql)) { int i = 1; ps.setLong(i++, e.getBlockchainId()); 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 { BlockchainStateEntry e = new BlockchainStateEntry(); e.setBlockchainId(rs.getLong("blockchain_id")); @@ -173,7 +166,5 @@ public final class BlockchainStateDAO { return e; } - private static String nn(String s) { - return s == null ? "" : s; - } + private static String nn(String s) { return s == null ? "" : s; } } \ No newline at end of file diff --git a/shine-server-db/src/main/java/shine/db/dao/SolanaUsersDAO.java b/shine-server-db/src/main/java/shine/db/dao/SolanaUsersDAO.java index f5fda48..6118e05 100644 --- a/shine-server-db/src/main/java/shine/db/dao/SolanaUsersDAO.java +++ b/shine-server-db/src/main/java/shine/db/dao/SolanaUsersDAO.java @@ -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 { String sql = """ SELECT login, loginId, bchId, loginKey, deviceKey, bchLimit diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/BlockchainStateService_new.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/BlockchainStateService_new.java index d8b0dbb..19f970b 100644 --- a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/BlockchainStateService_new.java +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/BlockchainStateService_new.java @@ -1,15 +1,18 @@ package server.logic.ws_protocol.JSON.handlers.blockchain; import blockchain_new.BchBlockEntry_new; +import shine.db.SqliteDbController; import shine.db.dao.BlockchainStateDAO; import shine.db.dao.SolanaUsersDAO; import shine.db.entities.BlockchainStateEntry; import shine.db.entities.SolanaUserEntry; import utils.files.FileStoreUtil; +import java.sql.Connection; import java.sql.SQLException; import java.util.Base64; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; import java.util.concurrent.locks.ReentrantLock; public final class BlockchainStateService_new { @@ -26,7 +29,6 @@ public final class BlockchainStateService_new { this.stateAfter = stateAfter; this.lineIndex = lineIndex; } - public boolean isOk() { return reasonCode == null && httpStatus == 200; } } @@ -34,23 +36,16 @@ public final class BlockchainStateService_new { public static BlockchainStateService_new getInstance() { return INSTANCE; } private BlockchainStateService_new() {} - // ===== locks per blockchainId (MVP: один сервер) ===== - private static final ConcurrentHashMap 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); - // MVP: “заглавный блок” - // (пока без парсинга тела, просто по номеру) - private static boolean isHeaderBlock(int globalNumber, int lineNumber) { - return globalNumber == 0 && lineNumber == 0; + // Локи по blockchainId (MVP, один сервер) + private final ConcurrentMap locks = new ConcurrentHashMap<>(); + + private ReentrantLock lockFor(long blockchainId) { + return locks.computeIfAbsent(blockchainId, k -> new ReentrantLock()); } - public Result addBlock( + public Result addBlockAtomically( String login, long blockchainId, int globalNumber, @@ -81,101 +76,90 @@ public final class BlockchainStateService_new { return new Result(400, "BAD_BLOCK_FORMAT", null, -1); } - int lineIndex = block.line; // short -> int + int lineIndex = block.line; if (lineIndex < 0 || lineIndex > 7) return new Result(400, "BAD_LINE_INDEX", null, lineIndex); ReentrantLock lock = lockFor(blockchainId); lock.lock(); - try { - BlockchainStateEntry state = BlockchainStateDAO.getInstance().getByBlockchainId(blockchainId); + try (Connection conn = SqliteDbController.getInstance().getConnection()) { + + 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 (!isHeaderBlock(globalNumber, block.lineNumber)) { + // state отсутствует — разрешаем ТОЛЬКО header-блок + if (!isHeaderBlock) { return new Result(404, "UNKNOWN_BLOCKCHAIN", null, lineIndex); } - // создаём первичное состояние (last_global=-1, hash=ZERO64, lines=0/ZERO64) - state = createInitialStateFromUser(login, blockchainId); - if (state == null) { - // нет такого юзера / не его bchId - return new Result(404, "UNKNOWN_BLOCKCHAIN", null, lineIndex); + // Проверяем пользователя и соответствие bchId + SolanaUserEntry u = usersDao.getByLogin(conn, login); + if (u == null) { + return new Result(404, "UNKNOWN_USER", null, lineIndex); + } + if (u.getBchId() != blockchainId) { + return new Result(403, "BCHID_MISMATCH", null, lineIndex); } - // сохраняем стартовую строку - BlockchainStateDAO.getInstance().upsert(state); + // prevGlobalHash для header должен быть нулевой + 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) защита от подмены логина - if (!login.equals(state.getUserLogin())) { - return new Result(403, "LOGIN_MISMATCH", state, lineIndex); - } + // Перечитывать не надо, state актуален в переменной. - // 2) expected global: last_global + 1 (у нас last_global стартует -1) + // expected global int expectedGlobal = state.getLastGlobalNumber() + 1; if (globalNumber != expectedGlobal) { return new Result(409, "OUT_OF_SEQUENCE_GLOBAL", state, lineIndex); } - // 3) prev global hash + // prev global hash сверяем String dbPrevGlobalHash = nn(state.getLastGlobalHash()); if (!eqHash(prevGlobalHashHex, dbPrevGlobalHash)) { return new Result(409, "GLOBAL_HASH_MISMATCH", state, lineIndex); } - // 4) lineNumber - // Нормально: первый “обычный” блок по линии должен быть lineNumber=1 при lastLine=0 - // Исключение: заглавный блок имеет lineNumber=0 + // expected line number int expectedLineNumber = state.getLastLineNumber(lineIndex) + 1; - boolean header = isHeaderBlock(globalNumber, block.lineNumber); - - 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); - } + if (block.lineNumber != expectedLineNumber) { + return new Result(409, "OUT_OF_SEQUENCE_LINE", state, lineIndex); } - // 5) prevLineHash берём из БД (пока просто читаем) - String dbPrevLineHashHex = nn(state.getLastLineHash(lineIndex)); - // (можешь позже сравнивать с тем, что внутри блока, если там есть prevLineHash) + // TODO: крипто-проверка (потом подключим) - // 6) крипто-проверка (позже) - // TODO: - // - восстановить preimage - // - sha256(preimage) == block.hash32 - // - Ed25519 verify signature - // если не ок: return new Result(422, "CRYPTO_INVALID", state, lineIndex); - - // 7) запись блока в файл + // 1) запись блока в файл FileStoreUtil.getInstance().addDataToBlockchain(blockchainId, block.toBytes()); - // 8) апдейт состояния + // 2) апдейт state + String newHashHex = bytesToHex(block.getHash32()); + state.setLastGlobalNumber(globalNumber); - state.setLastGlobalHash(bytesToHex(block.getHash32())); + state.setLastGlobalHash(newHashHex); - // line number: - // - для заглавного блока оставляем 0 - // - для остальных двигаем как обычно - if (!header) { - state.setLastLineNumber(lineIndex, block.lineNumber); - } else { - state.setLastLineNumber(lineIndex, 0); - } - - // line hash обновляем в любом случае (так проще для цепочки) - state.setLastLineHash(lineIndex, bytesToHex(block.getHash32())); + state.setLastLineNumber(lineIndex, block.lineNumber); + state.setLastLineHash(lineIndex, newHashHex); state.setSizeBytes(state.getSizeBytes() + fullBytes.length); state.setUpdatedAtMs(System.currentTimeMillis()); - BlockchainStateDAO.getInstance().upsert(state); + stateDao.upsert(conn, state); return new Result(200, null, state, lineIndex); @@ -186,30 +170,19 @@ public final class BlockchainStateService_new { } } - /** - * Создаёт стартовое состояние по данным пользователя: - * - проверяем, что login существует и что bchId совпадает с blockchainId - * - public_key_base64 берём из loginKey - */ - 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; - + private static BlockchainStateEntry createInitialState(long blockchainId, + String login, + String loginKeyBase64, + int sizeLimit) { BlockchainStateEntry s = new BlockchainStateEntry(); s.setBlockchainId(blockchainId); s.setUserLogin(login); + s.setPublicKeyBase64(nn(loginKeyBase64)); - // публичный ключ для блокчейна = loginKey (как ты и хочешь) - s.setPublicKeyBase64(nn(u.getLoginKey())); - - // лимит (пока тестовый / из пользователя) - int limit = (u.getBchLimit() != null) ? u.getBchLimit() : 1_000_000; - s.setSizeLimit(limit); - + s.setSizeLimit(sizeLimit); s.setSizeBytes(0); - // ВАЖНО: стартовые значения + // как ты хочешь: s.setLastGlobalNumber(-1); s.setLastGlobalHash(ZERO64); @@ -222,6 +195,11 @@ public final class BlockchainStateService_new { 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 boolean eqHash(String a, String b) { diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/Net_AddBlock_new_Handler.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/Net_AddBlock_new_Handler.java index 0629c6f..db394c8 100644 --- a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/Net_AddBlock_new_Handler.java +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/Net_AddBlock_new_Handler.java @@ -14,7 +14,7 @@ public final class Net_AddBlock_new_Handler implements JsonMessageHandler { public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) throws Exception { 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.getBlockchainId(), 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(); resp.setOp(req.getOp()); resp.setRequestId(req.getRequestId()); - resp.setLineIndex(r.lineIndex); if (r.isOk()) { diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/tempToTest/Net_AddUser_Handler.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/tempToTest/Net_AddUser_Handler.java index e9b6eb4..3751597 100644 --- a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/tempToTest/Net_AddUser_Handler.java +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/tempToTest/Net_AddUser_Handler.java @@ -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.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; 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); - // ====== TEST CONST (пока так) ====== + /** TEST ONLY: лимит блокчейна по умолчанию. Потом заменишь на норм логику. */ private static final int TEST_BCH_LIMIT = 1_000_000; - private static final String ZERO64 = "0".repeat(64); - @Override public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) throws Exception { Net_AddUser_Request req = (Net_AddUser_Request) baseRequest; if (req.getLogin() == null || req.getLogin().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( req, WireCodes.Status.BAD_REQUEST, "BAD_FIELDS", - "Некорректные или пустые поля: login, loginKey, deviceKey" + "Некорректные поля: login/loginId/bchId/loginKey/deviceKey" ); } - // bchLimit: если клиент не прислал — ставим тестовую константу Integer limit = req.getBchLimit(); if (limit == null || limit <= 0) limit = TEST_BCH_LIMIT; try { - SolanaUsersDAO users = SolanaUsersDAO.getInstance(); - BlockchainStateDAO stateDao = BlockchainStateDAO.getInstance(); + SolanaUsersDAO dao = SolanaUsersDAO.getInstance(); SolanaUserEntry user = new SolanaUserEntry( req.getLoginId(), @@ -59,31 +55,7 @@ public class Net_AddUser_Handler implements JsonMessageHandler { limit ); - users.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); + dao.insert(user); Net_AddUser_Response resp = new Net_AddUser_Response(); resp.setOp(req.getOp()); @@ -96,7 +68,7 @@ public class Net_AddUser_Handler implements JsonMessageHandler { return resp; } catch (SQLException e) { - log.error("❌ DB error in AddUser", e); + log.error("❌ DB error AddUser", e); return NetExceptionResponseFactory.error( req, WireCodes.Status.SERVER_DATA_ERROR, @@ -104,7 +76,7 @@ public class Net_AddUser_Handler implements JsonMessageHandler { "Ошибка доступа к базе данных" ); } catch (Exception e) { - log.error("❌ Internal error in AddUser", e); + log.error("❌ Internal error AddUser", e); return NetExceptionResponseFactory.error( req, WireCodes.Status.INTERNAL_ERROR,