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 0e540c0..d8b0dbb 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,12 +1,12 @@ 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; @@ -34,14 +34,23 @@ public final class BlockchainStateService_new { public static BlockchainStateService_new getInstance() { return INSTANCE; } private BlockchainStateService_new() {} - // --- MVP: локи в памяти по blockchainId --- + // ===== locks per blockchainId (MVP: один сервер) ===== private static final ConcurrentHashMap LOCKS = new ConcurrentHashMap<>(); private static ReentrantLock lockFor(long blockchainId) { return LOCKS.computeIfAbsent(blockchainId, id -> new ReentrantLock()); } - public Result addBlockAtomically( + // ===== constants ===== + private static final String ZERO64 = "0".repeat(64); + + // MVP: “заглавный блок” + // (пока без парсинга тела, просто по номеру) + private static boolean isHeaderBlock(int globalNumber, int lineNumber) { + return globalNumber == 0 && lineNumber == 0; + } + + public Result addBlock( String login, long blockchainId, int globalNumber, @@ -78,79 +87,141 @@ public final class BlockchainStateService_new { ReentrantLock lock = lockFor(blockchainId); lock.lock(); - try (Connection conn = SqliteDbController.getInstance().getConnection()) { + try { + BlockchainStateEntry state = BlockchainStateDAO.getInstance().getByBlockchainId(blockchainId); - // Транзакция — норм, но БЕЗ "BEGIN IMMEDIATE". - boolean oldAuto = conn.getAutoCommit(); - conn.setAutoCommit(false); - - try { - BlockchainStateEntry state = - BlockchainStateDAO.getInstance().getByBlockchainId(conn, blockchainId); - - if (state == null) { - conn.rollback(); + // ===== GENESIS ветка: state ещё нет ===== + if (state == null) { + // разрешаем только заглавный блок + if (!isHeaderBlock(globalNumber, block.lineNumber)) { return new Result(404, "UNKNOWN_BLOCKCHAIN", null, lineIndex); } - if (!login.equals(state.getUserLogin())) { - conn.rollback(); - return new Result(403, "LOGIN_MISMATCH", state, 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); } - int expectedGlobal = state.getLastGlobalNumber() + 1; - if (globalNumber != expectedGlobal) { - conn.rollback(); - return new Result(409, "OUT_OF_SEQUENCE_GLOBAL", state, lineIndex); - } - - String dbPrevGlobalHash = nn(state.getLastGlobalHash()); - if (!eqHash(prevGlobalHashHex, dbPrevGlobalHash)) { - conn.rollback(); - return new Result(409, "GLOBAL_HASH_MISMATCH", state, lineIndex); - } - - int expectedLineNumber = state.getLastLineNumber(lineIndex) + 1; - if (block.lineNumber != expectedLineNumber) { - conn.rollback(); - return new Result(409, "OUT_OF_SEQUENCE_LINE", state, lineIndex); - } - - // prevLineHash (пока просто читаем, дальше пригодится для крипто-проверки) - String dbPrevLineHashHex = nn(state.getLastLineHash(lineIndex)); - - // TODO crypto check (потом подключим) - - // 1) пишем в файл - FileStoreUtil.getInstance().addDataToBlockchain(blockchainId, block.toBytes()); - - // 2) обновляем state в БД - state.setLastGlobalNumber(globalNumber); - state.setLastGlobalHash(bytesToHex(block.getHash32())); - - state.setLastLineNumber(lineIndex, block.lineNumber); - state.setLastLineHash(lineIndex, bytesToHex(block.getHash32())); - - state.setSizeBytes(state.getSizeBytes() + fullBytes.length); - state.setUpdatedAtMs(System.currentTimeMillis()); - - BlockchainStateDAO.getInstance().upsert(conn, state); - - conn.commit(); - return new Result(200, null, state, lineIndex); - - } catch (SQLException e) { - conn.rollback(); - throw e; - } finally { - conn.setAutoCommit(oldAuto); + // сохраняем стартовую строку + BlockchainStateDAO.getInstance().upsert(state); } + // 1) защита от подмены логина + if (!login.equals(state.getUserLogin())) { + return new Result(403, "LOGIN_MISMATCH", state, lineIndex); + } + + // 2) expected global: last_global + 1 (у нас last_global стартует -1) + int expectedGlobal = state.getLastGlobalNumber() + 1; + if (globalNumber != expectedGlobal) { + return new Result(409, "OUT_OF_SEQUENCE_GLOBAL", state, lineIndex); + } + + // 3) 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 + 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); + } + } + + // 5) prevLineHash берём из БД (пока просто читаем) + String dbPrevLineHashHex = nn(state.getLastLineHash(lineIndex)); + // (можешь позже сравнивать с тем, что внутри блока, если там есть prevLineHash) + + // 6) крипто-проверка (позже) + // TODO: + // - восстановить preimage + // - sha256(preimage) == block.hash32 + // - Ed25519 verify signature + // если не ок: return new Result(422, "CRYPTO_INVALID", state, lineIndex); + + // 7) запись блока в файл + FileStoreUtil.getInstance().addDataToBlockchain(blockchainId, block.toBytes()); + + // 8) апдейт состояния + state.setLastGlobalNumber(globalNumber); + state.setLastGlobalHash(bytesToHex(block.getHash32())); + + // line number: + // - для заглавного блока оставляем 0 + // - для остальных двигаем как обычно + 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.setUpdatedAtMs(System.currentTimeMillis()); + + BlockchainStateDAO.getInstance().upsert(state); + + return new Result(200, null, state, lineIndex); + + } catch (SQLException e) { + throw e; } finally { lock.unlock(); } } + /** + * Создаёт стартовое состояние по данным пользователя: + * - проверяем, что 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; + + BlockchainStateEntry s = new BlockchainStateEntry(); + s.setBlockchainId(blockchainId); + s.setUserLogin(login); + + // публичный ключ для блокчейна = loginKey (как ты и хочешь) + s.setPublicKeyBase64(nn(u.getLoginKey())); + + // лимит (пока тестовый / из пользователя) + int limit = (u.getBchLimit() != null) ? u.getBchLimit() : 1_000_000; + 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()); + return s; + } + 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 097a347..0629c6f 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().addBlockAtomically( + var r = BlockchainStateService_new.getInstance().addBlock( req.getLogin(), req.getBlockchainId(), req.getGlobalNumber(), @@ -32,8 +32,6 @@ public final class Net_AddBlock_new_Handler implements JsonMessageHandler { resp.setStatus(WireCodes.Status.OK); resp.setReasonCode(null); } else { - // 409 / 422 / 403 / 404... - // у тебя WireCodes.Status — это “HTTP-подобное”? тогда маппим: resp.setStatus(r.httpStatus); resp.setReasonCode(r.reasonCode); } @@ -49,4 +47,4 @@ public final class Net_AddBlock_new_Handler implements JsonMessageHandler { return resp; } -} +} \ No newline at end of file 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 9effece..e9b6eb4 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,56 +10,45 @@ 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; -/** - * Временный хэндлер AddUser (тестовая регистрация локального пользователя). - * - * Ожидаемый запрос (все поля в payload): - * { - * "op": "AddUser", - * "requestId": "...", - * "payload": { - * "login": "anya", - * "loginId": 100211, - * "bchId": 4222, - * "loginKey": "base64-pubkey-login", - * "deviceKey": "base64-pubkey-device", - * "bchLimit": 1000000 - * } - * } - * - * При успехе: - * - пользователь сохраняется в таблицу solana_users; - * - возвращается status=200 и пустой payload. - */ public class Net_AddUser_Handler implements JsonMessageHandler { private static final Logger log = LoggerFactory.getLogger(Net_AddUser_Handler.class); + // ====== TEST CONST (пока так) ====== + 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.getBchLimit() == null) { + || req.getDeviceKey() == null || req.getDeviceKey().isBlank()) { return NetExceptionResponseFactory.error( req, WireCodes.Status.BAD_REQUEST, "BAD_FIELDS", - "Некорректные или пустые поля: login, loginKey, deviceKey, bchLimit" + "Некорректные или пустые поля: login, loginKey, deviceKey" ); } + // bchLimit: если клиент не прислал — ставим тестовую константу + Integer limit = req.getBchLimit(); + if (limit == null || limit <= 0) limit = TEST_BCH_LIMIT; + try { - SolanaUsersDAO dao = SolanaUsersDAO.getInstance(); + SolanaUsersDAO users = SolanaUsersDAO.getInstance(); + BlockchainStateDAO stateDao = BlockchainStateDAO.getInstance(); SolanaUserEntry user = new SolanaUserEntry( req.getLoginId(), @@ -67,21 +56,47 @@ public class Net_AddUser_Handler implements JsonMessageHandler { req.getBchId(), req.getLoginKey(), req.getDeviceKey(), - req.getBchLimit() + limit ); - dao.insert(user); + 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); Net_AddUser_Response resp = new Net_AddUser_Response(); resp.setOp(req.getOp()); resp.setRequestId(req.getRequestId()); resp.setStatus(WireCodes.Status.OK); - // payload станет {} через JsonInboundProcessor - log.info("✅ Пользователь добавлен: login={}, loginId={}", req.getLogin(), req.getLoginId()); + + log.info("✅ AddUser ok: login={}, loginId={}, bchId={}, limit={}", + req.getLogin(), req.getLoginId(), req.getBchId(), limit); + return resp; } catch (SQLException e) { - log.error("❌ Ошибка при вставке пользователя в БД", e); + log.error("❌ DB error in AddUser", e); return NetExceptionResponseFactory.error( req, WireCodes.Status.SERVER_DATA_ERROR, @@ -89,7 +104,7 @@ public class Net_AddUser_Handler implements JsonMessageHandler { "Ошибка доступа к базе данных" ); } catch (Exception e) { - log.error("❌ Неожиданная ошибка в AddUser", e); + log.error("❌ Internal error in AddUser", e); return NetExceptionResponseFactory.error( req, WireCodes.Status.INTERNAL_ERROR, @@ -98,4 +113,4 @@ public class Net_AddUser_Handler implements JsonMessageHandler { ); } } -} +} \ No newline at end of file