From a309b6f3ef19830b8710a06f6378c78db68092c53b57946170fedaf91954467f Mon Sep 17 00:00:00 2001 From: AidarKC Date: Wed, 24 Dec 2025 16:42:26 +0300 Subject: [PATCH] =?UTF-8?q?24=2012=2025=20=D0=94=D0=BE=D1=80=D0=B0=D0=B1?= =?UTF-8?q?=D0=B0=D1=82=D1=8B=D0=B2=D0=B0=D1=8E=20=D0=B4=D0=BE=D0=B1=D0=B0?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=B1=D0=BB=D0=BE=D0=BA?= =?UTF-8?q?=D0=BE=D0=B2.=20=D0=A3=D1=80=D0=B0=20=D0=B4=D0=BE=D0=B1=D0=B0?= =?UTF-8?q?=D0=B2=D0=B8=D0=BB=D0=BE=D1=81=D1=8C.=20=D0=9E=D0=B1=D1=8A?= =?UTF-8?q?=D0=B5=D0=B4=D0=B5=D0=BD=D0=B8=D0=BB=20=D0=B2=20=D0=BE=D0=B4?= =?UTF-8?q?=D0=B8=D0=BD=20=D0=A5=D1=8D=D0=BD=D0=B4=D0=BB=D0=B5=D1=80=20?= =?UTF-8?q?=D0=B8=20=D1=81=D0=B4=D0=B5=D0=BB=D0=B0=D0=BB=20=D0=B0=D1=82?= =?UTF-8?q?=D0=BE=D0=BC=D0=B0=D1=80=D0=BD=D1=83=D1=8E=20=D0=B7=D0=B0=D0=BF?= =?UTF-8?q?=D0=B8=D1=81=D1=8C=20=D0=B2=20=D0=91=D0=94.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../blockchain/BlockchainDbWriter.java | 81 +++++- .../blockchain/BlockchainStateService.java | 243 ---------------- .../blockchain/Net_AddBlock_Handler.java | 268 +++++++++++++++++- 3 files changed, 340 insertions(+), 252 deletions(-) delete mode 100644 shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/BlockchainStateService.java diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/BlockchainDbWriter.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/BlockchainDbWriter.java index c1b4c78..60b9f62 100644 --- a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/BlockchainDbWriter.java +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/BlockchainDbWriter.java @@ -1,5 +1,6 @@ package server.logic.ws_protocol.JSON.handlers.blockchain; +import shine.db.SqliteDbController; import shine.db.dao.BlockchainStateDAO; import shine.db.dao.BlocksDAO; import shine.db.entities.BlockEntry; @@ -8,16 +9,76 @@ import shine.db.entities.BlockchainStateEntry; import java.sql.Connection; import java.sql.SQLException; +/** + * BlockchainDbWriter — единая точка записи блока + состояния в БД. + * + * Важно: + * - Здесь обеспечивается атомарность записи: либо вставился блок и обновилось состояние, либо не вставилось ничего. + * - Соединение открывается/закрывается внутри (удобно для хэндлера). + * - При необходимости можно вызвать appendBlockAndState(Connection, ...) и управлять транзакцией снаружи. + */ public final class BlockchainDbWriter { + private final SqliteDbController db; private final BlocksDAO blocksDAO; private final BlockchainStateDAO stateDAO; public BlockchainDbWriter(BlocksDAO blocksDAO, BlockchainStateDAO stateDAO) { + this.db = SqliteDbController.getInstance(); this.blocksDAO = blocksDAO; this.stateDAO = stateDAO; } + /** + * Публичный метод: сам открывает соединение, делает транзакцию и закрывает соединение. + * + * @return true если всё записалось успешно, иначе кидает SQLException (или IllegalStateException выше по коду). + */ + public void appendBlockAndState( + String login, + String blockchainName, + int globalNumber, + String prevGlobalHashHex, + byte[] blockBytes, + BlockchainStateEntry stOrNull, + String newHashHex + ) throws SQLException { + + // 1) Открываем соединение (try-with-resources гарантирует закрытие) + try (Connection c = db.getConnection()) { + + // 2) Включаем ручное управление транзакцией + boolean oldAutoCommit = c.getAutoCommit(); + c.setAutoCommit(false); + + try { + // 3) Внутри одной транзакции: + // - вставляем строку блока + // - обновляем/создаём blockchain_state + appendBlockAndState(c, login, blockchainName, globalNumber, prevGlobalHashHex, blockBytes, stOrNull, newHashHex); + + // 4) Фиксируем транзакцию + c.commit(); + + } catch (Exception e) { + // 5) Если что-то упало — откатываем транзакцию, чтобы не было "полузаписей" + try { c.rollback(); } catch (SQLException ignore) {} + + // Пробрасываем как SQLException (чтобы вызывающий код мог отдать internal_error и т.п.) + if (e instanceof SQLException se) throw se; + throw new SQLException("appendBlockAndState failed", e); + + } finally { + // 6) Возвращаем autoCommit как было + try { c.setAutoCommit(oldAutoCommit); } catch (SQLException ignore) {} + } + } + } + + /** + * Внутренний/расширенный метод: запись в рамках УЖЕ открытого соединения. + * Удобно если снаружи хотят объединить несколько действий в одну транзакцию. + */ public void appendBlockAndState( Connection c, String login, @@ -29,24 +90,34 @@ public final class BlockchainDbWriter { String newHashHex ) throws SQLException { + // A) Вставляем блок (строка в таблицу blocks) insertBlockRow(c, login, blockchainName, globalNumber, prevGlobalHashHex, blockBytes); + // B) Обновляем состояние blockchain_state (создаём если отсутствует) BlockchainStateEntry st = stOrNull; if (st == null) { st = new BlockchainStateEntry(); st.setBlockchainName(blockchainName); } + // Последний глобальный блок st.setLastGlobalNumber(globalNumber); st.setLastGlobalHash(newHashHex); + // Пока линии не используются: lineIndex=0 и lineHash = globalHash st.setLastLineNumber(0, globalNumber); st.setLastLineHash(0, newHashHex); + // Метка времени обновления st.setUpdatedAtMs(System.currentTimeMillis()); + + // UPSERT состояния stateDAO.upsert(c, st); } + /** + * Вставка/апдейт строки блока в blocks. + */ private void insertBlockRow( Connection c, String login, @@ -58,24 +129,32 @@ public final class BlockchainDbWriter { BlockEntry e = new BlockEntry(); + // Кому принадлежит блок (логин владельца цепочки) e.setLogin(login); e.setBchName(blockchainName); + // Глобальная нумерация e.setBlockGlobalNumber(globalNumber); e.setBlockGlobalPreHashe(prevGlobalHashHex); + // Линии пока не используются: lineIndex=0, lineNumber=globalNumber e.setBlockLineIndex(0); e.setBlockLineNumber(globalNumber); e.setBlockLinePreHashe(prevGlobalHashHex); + // msgType у тебя пока 0 (при желании позже можно ставить по Body/type) e.setMsgType(0); + + // Сырые байты полного блока e.setBlockByte(blockBytes); + // Поля "кому" (для сообщений/трансферов) пока пустые e.setToLogin(null); e.setToBchName(null); e.setToBlockGlobalNumber(null); e.setToBlockHashe(null); + // UPSERT блока blocksDAO.upsert(c, e); } -} \ No newline at end of file +} diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/BlockchainStateService.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/BlockchainStateService.java deleted file mode 100644 index 2bb5e65..0000000 --- a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/BlockchainStateService.java +++ /dev/null @@ -1,243 +0,0 @@ -package server.logic.ws_protocol.JSON.handlers.blockchain; - -import blockchain.BchBlockEntry; -import blockchain.BchCryptoVerifier; -import blockchain.body.BodyRecordParser; -import server.logic.ws_protocol.WireCodes; -import shine.db.SqliteDbController; -import shine.db.dao.BlockchainStateDAO; -import shine.db.dao.BlocksDAO; -import shine.db.dao.SolanaUsersDAO; -import shine.db.entities.BlockchainStateEntry; -import shine.db.entities.SolanaUserEntry; -import utils.blockchain.BlockchainNameUtil; - -import java.sql.Connection; -import java.sql.SQLException; -import java.util.Base64; - -/** - * BlockchainStateService_new — атомарное добавление блока (НОВЫЙ формат): - * - decode Base64 -> FULL block bytes - * - parse block (recordSize must match) - * - взять loginKey (publicKey32) пользователя - * - взять prevGlobalHash / prevLineHash из DB-состояния - * - собрать preimage -> sha256 -> verify signature - * - вставить blocks - * - обновить blockchain_state: lastGlobalNumber/lastGlobalHash (и позже line stuff) - * - * Ответ наружу: только reasonCode + serverLastGlobalNumber/serverLastGlobalHash - */ -public final class BlockchainStateService { - - /** Результат атомарного addBlock */ - public static final class AddBlockResult { - public final int httpStatus; // WireCodes.Status.* - public final String reasonCode; // null если ok - public final int serverLastGlobalNumber; - public final String serverLastGlobalHash; - - public AddBlockResult(int httpStatus, String reasonCode, int serverLastGlobalNumber, String serverLastGlobalHash) { - this.httpStatus = httpStatus; - this.reasonCode = reasonCode; - this.serverLastGlobalNumber = serverLastGlobalNumber; - this.serverLastGlobalHash = serverLastGlobalHash; - } - - public boolean isOk() { - return httpStatus == WireCodes.Status.OK; - } - } - - private static volatile BlockchainStateService instance; - - private final SqliteDbController db = SqliteDbController.getInstance(); - private final BlocksDAO blocksDAO = BlocksDAO.getInstance(); - private final BlockchainStateDAO stateDAO = BlockchainStateDAO.getInstance(); - private final SolanaUsersDAO solanaUsersDAO = SolanaUsersDAO.getInstance(); - private final BlockchainDbWriter dbWriter = new BlockchainDbWriter(blocksDAO, stateDAO); - - private BlockchainStateService() {} - - public static BlockchainStateService getInstance() { - if (instance == null) { - synchronized (BlockchainStateService.class) { - if (instance == null) instance = new BlockchainStateService(); - } - } - return instance; - } - - public AddBlockResult addBlockAtomically( - String blockchainName, - int globalNumber, - String prevGlobalHashHex, - String blockBytesB64 - ) { - - if (blockchainName == null || blockchainName.isBlank()) - return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "empty_blockchain_name", 0, ""); - - String login = BlockchainNameUtil.loginFromBlockchainName(blockchainName); - if (login == null || login.isBlank()) - return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_blockchain_name", 0, ""); - - byte[] blockBytes; - try { - blockBytes = decodeBase64(blockBytesB64); - } catch (Exception e) { - return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_block_base64", 0, ""); - } - - final BchBlockEntry block; - try { - block = new BchBlockEntry(blockBytes); - } catch (Exception e) { - return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_block_format", 0, ""); - } - - try { - BodyRecordParser.parse(block.bodyBytes).check(); - } catch (Exception e) { - return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_block_body", 0, ""); - } - - if (block.recordNumber != globalNumber) { - return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "global_number_mismatch", 0, ""); - } - - try (Connection c = db.getConnection()) { - boolean oldAutoCommit = c.getAutoCommit(); - c.setAutoCommit(false); - try { - SolanaUserEntry u = solanaUsersDAO.getByLogin(c, login); - if (u == null) { - c.rollback(); - return new AddBlockResult(WireCodes.Status.NOT_FOUND, "user_not_found", 0, ""); - } - - byte[] loginKey32 = u.getLoginKeyByte(); - if (loginKey32 == null || loginKey32.length != 32) { - c.rollback(); - return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_user_login_key", 0, ""); - } - - BlockchainStateEntry st = stateDAO.getByBlockchainName(c, blockchainName); - - int serverLastNum; - String serverLastHash; - - if (st == null) { - if (globalNumber != 0) { - c.rollback(); - return new AddBlockResult(WireCodes.Status.NOT_FOUND, "blockchain_state_not_found", 0, ""); - } - serverLastNum = -1; - serverLastHash = ""; - } else { - serverLastNum = st.getLastGlobalNumber(); - serverLastHash = nn(st.getLastGlobalHash()); - } - - int expected = serverLastNum + 1; - if (globalNumber != expected) { - c.rollback(); - return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_global_number", serverLastNum, serverLastHash); - } - - byte[] prevGlobalHash32 = hexTo32(nn(prevGlobalHashHex)); - byte[] serverPrevGlobal32 = (st == null) ? new byte[32] : hexTo32(nn(st.getLastGlobalHash())); - - if (!bytesEq(prevGlobalHash32, serverPrevGlobal32)) { - c.rollback(); - return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_prev_global_hash", serverLastNum, serverLastHash); - } - - byte[] prevLineHash32 = prevGlobalHash32; - - boolean ok = BchCryptoVerifier.verifyAll( - login, - prevGlobalHash32, - prevLineHash32, - block.getRawBytes(), - block.getSignature64(), - loginKey32, - block.getHash32() - ); - - if (!ok) { - c.rollback(); - return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_signature_or_hash", serverLastNum, serverLastHash); - } - - String newHashHex = toHex(block.getHash32()); - - dbWriter.appendBlockAndState( - c, - login, - blockchainName, - globalNumber, - nn(prevGlobalHashHex), - blockBytes, - st, - newHashHex - ); - - c.commit(); - return new AddBlockResult(WireCodes.Status.OK, null, globalNumber, newHashHex); - - } catch (Exception e) { - try { c.rollback(); } catch (SQLException ignore) {} - return new AddBlockResult(WireCodes.Status.INTERNAL_ERROR, "internal_error", 0, ""); - } finally { - try { c.setAutoCommit(oldAutoCommit); } catch (SQLException ignore) {} - } - } catch (Exception e) { - return new AddBlockResult(WireCodes.Status.INTERNAL_ERROR, "db_error", 0, ""); - } - } - - // -------------------- utils -------------------- - - private static String nn(String s) { return s == null ? "" : s; } - - private static byte[] decodeBase64(String s) { - if (s == null || s.isBlank()) throw new IllegalArgumentException("empty base64"); - return Base64.getDecoder().decode(s); - } - - /** hex(64) -> 32 bytes; пустой -> 32 нуля */ - private static byte[] hexTo32(String hex) { - if (hex == null || hex.isBlank()) return new byte[32]; - String h = hex.trim(); - if (h.length() != 64) throw new IllegalArgumentException("hex hash must be 64 chars"); - byte[] out = new byte[32]; - for (int i = 0; i < 32; i++) { - int hi = Character.digit(h.charAt(i * 2), 16); - int lo = Character.digit(h.charAt(i * 2 + 1), 16); - if (hi < 0 || lo < 0) throw new IllegalArgumentException("bad hex"); - out[i] = (byte)((hi << 4) | lo); - } - return out; - } - - private static boolean bytesEq(byte[] a, byte[] b) { - if (a == b) return true; - if (a == null || b == null) return false; - if (a.length != b.length) return false; - int x = 0; - for (int i = 0; i < a.length; i++) x |= (a[i] ^ b[i]); - return x == 0; - } - - private static String toHex(byte[] bytes) { - char[] HEX = "0123456789abcdef".toCharArray(); - char[] out = new char[bytes.length * 2]; - for (int i = 0; i < bytes.length; i++) { - int v = bytes[i] & 0xFF; - out[i * 2] = HEX[v >>> 4]; - out[i * 2 + 1] = HEX[v & 0x0F]; - } - return new String(out); - } -} \ No newline at end of file diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/Net_AddBlock_Handler.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/Net_AddBlock_Handler.java index 2dd6e4e..303310f 100644 --- a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/Net_AddBlock_Handler.java +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/Net_AddBlock_Handler.java @@ -1,5 +1,8 @@ package server.logic.ws_protocol.JSON.handlers.blockchain; +import blockchain.BchBlockEntry; +import blockchain.BchCryptoVerifier; +import blockchain.body.BodyRecordParser; import server.logic.ws_protocol.JSON.ConnectionContext; import server.logic.ws_protocol.JSON.entyties.Net_Request; import server.logic.ws_protocol.JSON.entyties.Net_Response; @@ -7,27 +10,54 @@ import server.logic.ws_protocol.JSON.entyties.blockchain.Net_AddBlock_Request; import server.logic.ws_protocol.JSON.entyties.blockchain.Net_AddBlock_Response; import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler; import server.logic.ws_protocol.WireCodes; +import shine.db.dao.BlockchainStateDAO; +import shine.db.dao.BlocksDAO; +import shine.db.dao.SolanaUsersDAO; +import shine.db.entities.BlockchainStateEntry; +import shine.db.entities.SolanaUserEntry; +import utils.blockchain.BlockchainNameUtil; +import java.util.Base64; import java.util.concurrent.locks.ReentrantLock; +/** + * Net_AddBlock_Handler — единый хэндлер добавления блока (JSON). + * + * Задачи: + * 1) Лочим добавление блоков для конкретного blockchainName (защита от гонок в одном процессе). + * 2) Декодируем блок из Base64 и парсим его структуру. + * 3) Парсим body и валидируем (type/version + содержимое). + * 4) Проверяем globalNumber и prevGlobalHash относительно server state. + * 5) Проверяем подпись/хэш (Ed25519 над hash32, hash32=sha256(preimage)). + * 6) Делаем запись в БД через BlockchainDbWriter (атомарность реализуется там). + * 7) Возвращаем клиенту serverLastGlobalNumber/serverLastGlobalHash. + */ public final class Net_AddBlock_Handler implements JsonMessageHandler { + // DAO (перегрузки сами создают/закрывают Connection внутри) + private final BlocksDAO blocksDAO = BlocksDAO.getInstance(); + private final BlockchainStateDAO stateDAO = BlockchainStateDAO.getInstance(); + private final SolanaUsersDAO solanaUsersDAO = SolanaUsersDAO.getInstance(); + + // Writer отвечает за транзакции/атомарность и консистентность БД + private final BlockchainDbWriter dbWriter = new BlockchainDbWriter(blocksDAO, stateDAO); + @Override - public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) throws Exception { + public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) { Net_AddBlock_Request req = (Net_AddBlock_Request) baseReq; - String bchName = req.getBlockchainName(); - ReentrantLock lock = BlockchainLocks.lockFor(bchName); + // 0) Берём имя цепочки и лочим операции добавления для неё + String blockchainName = req.getBlockchainName(); + ReentrantLock lock = BlockchainLocks.lockFor(blockchainName); lock.lock(); try { - var r = BlockchainStateService.getInstance().addBlockAtomically( - req.getBlockchainName(), + AddBlockResult r = addBlock(blockchainName, req.getGlobalNumber(), req.getPrevGlobalHash(), - req.getBlockBytesB64() - ); + req.getBlockBytesB64()); + // 7) Формируем стандартный Net_AddBlock_Response Net_AddBlock_Response resp = new Net_AddBlock_Response(); resp.setOp(req.getOp()); resp.setRequestId(req.getRequestId()); @@ -40,6 +70,7 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler { resp.setReasonCode(r.reasonCode); } + // Возвращаем актуальное состояние сервера (даже при ошибках, где уместно) resp.setServerLastGlobalNumber(r.serverLastGlobalNumber); if (r.serverLastGlobalHash != null) { resp.setServerLastGlobalHash(r.serverLastGlobalHash); @@ -51,4 +82,225 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler { lock.unlock(); } } -} \ No newline at end of file + + /* ===================================================================== */ + /* ========================== Основная логика =========================== */ + /* ===================================================================== */ + + /** + * Внутренняя логика добавления блока (без ручного управления Connection/tx). + * Все атомарные записи — внутри BlockchainDbWriter. + */ + private AddBlockResult addBlock( + String blockchainName, + int globalNumber, + String prevGlobalHashHex, + String blockBytesB64 + ) { + // 1) Быстрая валидация входных параметров + if (blockchainName == null || blockchainName.isBlank()) { + return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "empty_blockchain_name", 0, ""); + } + + // 2) Из имени блокчейна вытаскиваем login (как ты и хотел — через util) + String login = BlockchainNameUtil.loginFromBlockchainName(blockchainName); + if (login == null || login.isBlank()) { + return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_blockchain_name", 0, ""); + } + + // 3) Декодируем блок из Base64 + final byte[] blockBytes; + try { + blockBytes = decodeBase64(blockBytesB64); + } catch (Exception e) { + return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_block_base64", 0, ""); + } + + // 4) Парсим блок (проверяется recordSize и минимальная длина) + final BchBlockEntry block; + try { + block = new BchBlockEntry(blockBytes); + } catch (Exception e) { + return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_block_format", 0, ""); + } + + // 5) Парсим и валидируем body (type/version + содержимое) + try { + BodyRecordParser.parse(block.bodyBytes).check(); + } catch (Exception e) { + return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_block_body", 0, ""); + } + + // 6) Защита от рассинхрона: recordNumber внутри блока должен совпадать с заявленным globalNumber + if (block.recordNumber != globalNumber) { + return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "global_number_mismatch", 0, ""); + } + + // 7) Получаем пользователя и его loginKey (публичный ключ 32 байта) + SolanaUserEntry u; + try { + u = solanaUsersDAO.getByLogin(login); // перегрузка: сама открывает/закрывает соединение + } catch (Exception e) { + return new AddBlockResult(WireCodes.Status.INTERNAL_ERROR, "db_error", 0, ""); + } + + if (u == null) { + return new AddBlockResult(WireCodes.Status.NOT_FOUND, "user_not_found", 0, ""); + } + + byte[] loginKey32 = u.getLoginKeyByte(); + if (loginKey32 == null || loginKey32.length != 32) { + return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_user_login_key", 0, ""); + } + + // 8) Читаем текущее состояние блокчейна с сервера + BlockchainStateEntry st; + try { + st = stateDAO.getByBlockchainName(blockchainName); // перегрузка: сама открывает/закрывает соединение + } catch (Exception e) { + return new AddBlockResult(WireCodes.Status.INTERNAL_ERROR, "db_error", 0, ""); + } + + // 9) Определяем serverLastNum/serverLastHash (если state ещё нет — ожидаем genesis с globalNumber=0) + final int serverLastNum; + final String serverLastHash; + if (st == null) { + if (globalNumber != 0) { + return new AddBlockResult(WireCodes.Status.NOT_FOUND, "blockchain_state_not_found", 0, ""); + } + serverLastNum = -1; + serverLastHash = ""; + } else { + serverLastNum = st.getLastGlobalNumber(); + serverLastHash = nn(st.getLastGlobalHash()); + } + + // 10) Проверяем, что клиент присылает следующий блок ровно (last+1) + int expected = serverLastNum + 1; + if (globalNumber != expected) { + return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_global_number", serverLastNum, serverLastHash); + } + + // 11) Проверяем prevGlobalHash: клиент должен ссылаться на текущий serverLastHash + final byte[] prevGlobalHash32; + final byte[] serverPrevGlobal32; + try { + prevGlobalHash32 = hexTo32(nn(prevGlobalHashHex)); + serverPrevGlobal32 = (st == null) ? new byte[32] : hexTo32(nn(st.getLastGlobalHash())); + } catch (Exception e) { + return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_prev_global_hash_format", serverLastNum, serverLastHash); + } + + if (!bytesEq(prevGlobalHash32, serverPrevGlobal32)) { + return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_prev_global_hash", serverLastNum, serverLastHash); + } + + // 12) Пока линии не используем — prevLineHash равен prevGlobalHash (как ты писал) + byte[] prevLineHash32 = prevGlobalHash32; + + // 13) Криптопроверка: hash в блоке + подпись над hash + boolean ok = BchCryptoVerifier.verifyAll( + login, + prevGlobalHash32, + prevLineHash32, + block.getRawBytes(), // только RAW (без signature/hash) + block.getSignature64(), // подпись Ed25519 + loginKey32, // public key пользователя + block.getHash32() // ожидаемый hash32 из самого блока + ); + + if (!ok) { + return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_signature_or_hash", serverLastNum, serverLastHash); + } + + // 14) Новый hash блока (hex) — то, что будет записано как lastGlobalHash + String newHashHex = toHex(block.getHash32()); + + // 15) Запись блока + обновление состояния (атомарность/транзакции — внутри dbWriter) + try { + dbWriter.appendBlockAndState( + login, + blockchainName, + globalNumber, + nn(prevGlobalHashHex), + blockBytes, + st, + newHashHex + ); + } catch (Exception e) { + return new AddBlockResult(WireCodes.Status.INTERNAL_ERROR, "internal_error", serverLastNum, serverLastHash); + } + + // 16) Успех + return new AddBlockResult(WireCodes.Status.OK, null, globalNumber, newHashHex); + } + + /* ===================================================================== */ + /* ============================= Result ================================= */ + /* ===================================================================== */ + + /** Результат обработки addBlock */ + private static final class AddBlockResult { + final int httpStatus; // WireCodes.Status.* + final String reasonCode; // null если ok + final int serverLastGlobalNumber; + final String serverLastGlobalHash; + + AddBlockResult(int httpStatus, String reasonCode, int serverLastGlobalNumber, String serverLastGlobalHash) { + this.httpStatus = httpStatus; + this.reasonCode = reasonCode; + this.serverLastGlobalNumber = serverLastGlobalNumber; + this.serverLastGlobalHash = serverLastGlobalHash; + } + + boolean isOk() { + return httpStatus == WireCodes.Status.OK; + } + } + + /* ===================================================================== */ + /* ============================== Utils ================================= */ + /* ===================================================================== */ + + private static String nn(String s) { return s == null ? "" : s; } + + private static byte[] decodeBase64(String s) { + if (s == null || s.isBlank()) throw new IllegalArgumentException("empty base64"); + return Base64.getDecoder().decode(s); + } + + /** hex(64) -> 32 bytes; пустой -> 32 нуля */ + private static byte[] hexTo32(String hex) { + if (hex == null || hex.isBlank()) return new byte[32]; + String h = hex.trim(); + if (h.length() != 64) throw new IllegalArgumentException("hex hash must be 64 chars"); + byte[] out = new byte[32]; + for (int i = 0; i < 32; i++) { + int hi = Character.digit(h.charAt(i * 2), 16); + int lo = Character.digit(h.charAt(i * 2 + 1), 16); + if (hi < 0 || lo < 0) throw new IllegalArgumentException("bad hex"); + out[i] = (byte)((hi << 4) | lo); + } + return out; + } + + private static boolean bytesEq(byte[] a, byte[] b) { + if (a == b) return true; + if (a == null || b == null) return false; + if (a.length != b.length) return false; + int x = 0; + for (int i = 0; i < a.length; i++) x |= (a[i] ^ b[i]); + return x == 0; + } + + private static String toHex(byte[] bytes) { + char[] HEX = "0123456789abcdef".toCharArray(); + char[] out = new char[bytes.length * 2]; + for (int i = 0; i < bytes.length; i++) { + int v = bytes[i] & 0xFF; + out[i * 2] = HEX[v >>> 4]; + out[i * 2 + 1] = HEX[v & 0x0F]; + } + return new String(out); + } +}