From 62e4338e88bdfae9a8b5ebff8d9517d4c8fb923694988cbba13b039cbc8cd559 Mon Sep 17 00:00:00 2001 From: AidarKC Date: Tue, 23 Dec 2025 15:48:23 +0300 Subject: [PATCH] =?UTF-8?q?23=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!?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/blockchain/BodyRecordParser.java | 212 ++++++++-------- .../blockchain_new/BchCryptoVerifier_new.java | 26 +- .../shine/db/entities/SolanaUserEntry.java | 37 ++- .../blockchain/Net_AddBlock_Response.java | 23 +- .../BlockchainStateService_new.java | 238 ++++++++++++------ .../blockchain/Net_AddBlock_new_Handler.java | 14 +- 6 files changed, 333 insertions(+), 217 deletions(-) diff --git a/shine-server-blockchain/src/main/java/blockchain/BodyRecordParser.java b/shine-server-blockchain/src/main/java/blockchain/BodyRecordParser.java index 64d8f00..68de929 100644 --- a/shine-server-blockchain/src/main/java/blockchain/BodyRecordParser.java +++ b/shine-server-blockchain/src/main/java/blockchain/BodyRecordParser.java @@ -1,106 +1,106 @@ -package blockchain; - -import blockchain.body.BodyRecord; -import blockchain.body.HeaderBody; -import blockchain.body.TextBody; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * ============================================================================ - * BodyRecordParser — универсальный парсер тел (body) блоков .bch - * ============================================================================ - *. - * 🧩 Назначение: - * Преобразует пару (recordType, recordTypeVersion, bodyBytes) - * в конкретный объект, реализующий интерфейс {@link BodyRecord}. - *. - * 🔹 Особенность: - * Используется объединённый 4-байтовый код: - *. - * fullCode = (recordType << 16) | (recordTypeVersion & 0xFFFF) - *. - * Это позволяет различать версии одного типа блока, - * например: TextBody v1, TextBody v2 и т.д. - *. - * 🔹 Пример: - * BodyRecord body = BodyRecordParser.parse(block.recordType, block.recordTypeVersion, block.body); - *. - * ============================================================================ - */ -public final class BodyRecordParser { - - private static final Logger log = LoggerFactory.getLogger(BodyRecordParser.class); - - private BodyRecordParser() {} - - /** - * Распарсить тело блока по типу и версии записи. - * - * @param recordType код типа записи (0 = Header, 1 = Text, ...) - * @param recordTypeVersion версия формата записи - * @param body массив байт тела записи - * @return объект, реализующий BodyRecord - */ - public static BodyRecord parse(short recordType, short recordTypeVersion, byte[] body) { - if (body == null) - throw new IllegalArgumentException("body == null"); - - // Объединяем тип и версию в 4-байтовый ключ - int fullCode = ((recordType & 0xFFFF) << 16) | (recordTypeVersion & 0xFFFF); - - switch (fullCode) { - - // --------------------------------------------------------- - // TYPE 0, VERSION 1 — HeaderBody v1 - // --------------------------------------------------------- - // Заголовок цепочки пользователя (первый блок). - // - // Формат body (без общих 20 байт заголовка блока): - // [8] ASCII tag = "SHiNE001" - // [8] blockchainId (long, BE) - // [4] blockchainType (int, BE) - // [4] blockchainNumber (int, BE) - // [1] userLoginLength = N (unsigned byte) - // [N] userLogin (UTF-8) - // [2] versionUserBch (short, BE) - // [8] prevUserBchId (long, BE) - // [32] publicKey32 - // - // Назначение: - // Создаёт новую пользовательскую цепочку (блок №0). - case (0x0000_0001): - return new HeaderBody(body); - - // --------------------------------------------------------- - // TYPE 1, VERSION 1 — TextBody v1 - // --------------------------------------------------------- - // Простое текстовое сообщение UTF-8. - // - // Формат body (без общих 20 байт заголовка блока): - // [N] message (UTF-8) - // - // Назначение: - // Текстовые и системные сообщения, описания, комментарии. - case (0x0001_0001): - return new TextBody(body); - - // --------------------------------------------------------- - // РЕЗЕРВ — будущие типы и версии - // --------------------------------------------------------- - // Пример: (0x0001_0002) → TextBody v2 (например, JSON-структура) - // (0x0002_0001) → FileBody v1 - // - // case (0x0001_0002): - // return new TextBodyV2(body); - // - // case (0x0002_0001): - // return new FileBody(body); - - default: - throw new IllegalArgumentException(String.format( - "Неизвестный тип блока: type=%d version=%d (fullCode=0x%08X)", - recordType, recordTypeVersion, fullCode)); - } - } -} +//package blockchain; +// +//import blockchain.body.BodyRecord; +//import blockchain.body.HeaderBody; +//import blockchain.body.TextBody; +//import org.slf4j.Logger; +//import org.slf4j.LoggerFactory; +// +///** +// * ============================================================================ +// * BodyRecordParser — универсальный парсер тел (body) блоков .bch +// * ============================================================================ +// *. +// * 🧩 Назначение: +// * Преобразует пару (recordType, recordTypeVersion, bodyBytes) +// * в конкретный объект, реализующий интерфейс {@link BodyRecord}. +// *. +// * 🔹 Особенность: +// * Используется объединённый 4-байтовый код: +// *. +// * fullCode = (recordType << 16) | (recordTypeVersion & 0xFFFF) +// *. +// * Это позволяет различать версии одного типа блока, +// * например: TextBody v1, TextBody v2 и т.д. +// *. +// * 🔹 Пример: +// * BodyRecord body = BodyRecordParser.parse(block.recordType, block.recordTypeVersion, block.body); +// *. +// * ============================================================================ +// */ +//public final class BodyRecordParser { +// +// private static final Logger log = LoggerFactory.getLogger(BodyRecordParser.class); +// +// private BodyRecordParser() {} +// +// /** +// * Распарсить тело блока по типу и версии записи. +// * +// * @param recordType код типа записи (0 = Header, 1 = Text, ...) +// * @param recordTypeVersion версия формата записи +// * @param body массив байт тела записи +// * @return объект, реализующий BodyRecord +// */ +// public static BodyRecord parse(short recordType, short recordTypeVersion, byte[] body) { +// if (body == null) +// throw new IllegalArgumentException("body == null"); +// +// // Объединяем тип и версию в 4-байтовый ключ +// int fullCode = ((recordType & 0xFFFF) << 16) | (recordTypeVersion & 0xFFFF); +// +// switch (fullCode) { +// +// // --------------------------------------------------------- +// // TYPE 0, VERSION 1 — HeaderBody v1 +// // --------------------------------------------------------- +// // Заголовок цепочки пользователя (первый блок). +// // +// // Формат body (без общих 20 байт заголовка блока): +// // [8] ASCII tag = "SHiNE001" +// // [8] blockchainId (long, BE) +// // [4] blockchainType (int, BE) +// // [4] blockchainNumber (int, BE) +// // [1] userLoginLength = N (unsigned byte) +// // [N] userLogin (UTF-8) +// // [2] versionUserBch (short, BE) +// // [8] prevUserBchId (long, BE) +// // [32] publicKey32 +// // +// // Назначение: +// // Создаёт новую пользовательскую цепочку (блок №0). +// case (0x0000_0001): +// return new HeaderBody(body); +// +// // --------------------------------------------------------- +// // TYPE 1, VERSION 1 — TextBody v1 +// // --------------------------------------------------------- +// // Простое текстовое сообщение UTF-8. +// // +// // Формат body (без общих 20 байт заголовка блока): +// // [N] message (UTF-8) +// // +// // Назначение: +// // Текстовые и системные сообщения, описания, комментарии. +// case (0x0001_0001): +// return new TextBody(body); +// +// // --------------------------------------------------------- +// // РЕЗЕРВ — будущие типы и версии +// // --------------------------------------------------------- +// // Пример: (0x0001_0002) → TextBody v2 (например, JSON-структура) +// // (0x0002_0001) → FileBody v1 +// // +// // case (0x0001_0002): +// // return new TextBodyV2(body); +// // +// // case (0x0002_0001): +// // return new FileBody(body); +// +// default: +// throw new IllegalArgumentException(String.format( +// "Неизвестный тип блока: type=%d version=%d (fullCode=0x%08X)", +// recordType, recordTypeVersion, fullCode)); +// } +// } +//} diff --git a/shine-server-blockchain/src/main/java/blockchain_new/BchCryptoVerifier_new.java b/shine-server-blockchain/src/main/java/blockchain_new/BchCryptoVerifier_new.java index c48c843..972577b 100644 --- a/shine-server-blockchain/src/main/java/blockchain_new/BchCryptoVerifier_new.java +++ b/shine-server-blockchain/src/main/java/blockchain_new/BchCryptoVerifier_new.java @@ -1,5 +1,7 @@ package blockchain_new; +import utils.crypto.Ed25519Util; + import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.charset.StandardCharsets; @@ -39,9 +41,9 @@ public final class BchCryptoVerifier_new { ByteBuffer bb = ByteBuffer.allocate( DOMAIN.length + - 1 + loginBytes.length + - 32 + 32 + - rawBytes.length + 1 + loginBytes.length + + 32 + 32 + + rawBytes.length ).order(ByteOrder.BIG_ENDIAN); bb.put(DOMAIN); @@ -63,11 +65,23 @@ public final class BchCryptoVerifier_new { } } - // TODO: сюда подключается твой Ed25519 util + /** + * Проверка подписи Ed25519: + * verify(hash32, signature64, publicKey32) + */ public static boolean verifySignature(byte[] hash32, byte[] signature64, byte[] publicKey32) { - // TODO: Ed25519.verify(hash32, signature64, publicKey32) - return true; + Objects.requireNonNull(hash32, "hash32 == null"); + Objects.requireNonNull(signature64, "signature64 == null"); + Objects.requireNonNull(publicKey32, "publicKey32 == null"); + + if (hash32.length != 32) throw new IllegalArgumentException("hash32 != 32"); + if (signature64.length != 64) throw new IllegalArgumentException("signature64 != 64"); + if (publicKey32.length != 32) throw new IllegalArgumentException("publicKey32 != 32"); + + // ⚠️ Подстрой под твой Ed25519Util: + // Идея ровно такая: verify(messageHash, signature, publicKey) + return Ed25519Util.verify(hash32, signature64, publicKey32); } } \ No newline at end of file diff --git a/shine-server-db/src/main/java/shine/db/entities/SolanaUserEntry.java b/shine-server-db/src/main/java/shine/db/entities/SolanaUserEntry.java index 95c944f..b23ed6d 100644 --- a/shine-server-db/src/main/java/shine/db/entities/SolanaUserEntry.java +++ b/shine-server-db/src/main/java/shine/db/entities/SolanaUserEntry.java @@ -1,5 +1,7 @@ package shine.db.entities; +import java.util.Base64; + /** * Локальная копия пользователя из Solana. * @@ -18,8 +20,7 @@ public class SolanaUserEntry { private String deviceKey; // TEXT private Integer bchLimit; // INTEGER nullable - public SolanaUserEntry() { - } + public SolanaUserEntry() {} public SolanaUserEntry(String login, String bchName, @@ -49,4 +50,36 @@ public class SolanaUserEntry { public Integer getBchLimit() { return bchLimit; } public void setBchLimit(Integer bchLimit) { this.bchLimit = bchLimit; } + + /** + * Публичный ключ логина в байтах (32 байта) или null, если ключ битый/пустой. + * + * Поддержка форматов: + * - Base64 (предпочтительно) + * - HEX (ровно 64 hex-символа, без пробелов) + */ + public byte[] getLoginKeyByte() { + if (loginKey == null) return null; + String s = loginKey.trim(); + if (s.isEmpty()) return null; + + // 1) пробуем Base64 + try { + byte[] b = Base64.getDecoder().decode(s); + if (b != null && b.length == 32) return b; + } catch (IllegalArgumentException ignore) {} + + // 2) пробуем HEX (64 символа) + if (s.length() == 64 && s.matches("^[0-9a-fA-F]+$")) { + byte[] out = new byte[32]; + for (int i = 0; i < 32; i++) { + int hi = Character.digit(s.charAt(i * 2), 16); + int lo = Character.digit(s.charAt(i * 2 + 1), 16); + out[i] = (byte) ((hi << 4) | lo); + } + return out; + } + + return null; + } } \ No newline at end of file diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/blockchain/Net_AddBlock_Response.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/blockchain/Net_AddBlock_Response.java index b27119d..4adffe5 100644 --- a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/blockchain/Net_AddBlock_Response.java +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/blockchain/Net_AddBlock_Response.java @@ -2,18 +2,18 @@ package server.logic.ws_protocol.JSON.entyties.blockchain; import server.logic.ws_protocol.JSON.entyties.Net_Response; +/** + * Новый укороченный ответ: + * - reasonCode (null если ok) + * - serverLastGlobalNumber / serverLastGlobalHash + */ public final class Net_AddBlock_Response extends Net_Response { - private String reasonCode; // null если ok + private String reasonCode; // null если ok private int serverLastGlobalNumber; private String serverLastGlobalHash; - private int serverLastLineNumber; // для линии блока - private String serverLastLineHash; - - private int lineIndex; // какую линию сервер применил (из блока) - public String getReasonCode() { return reasonCode; } public void setReasonCode(String reasonCode) { this.reasonCode = reasonCode; } @@ -22,13 +22,4 @@ public final class Net_AddBlock_Response extends Net_Response { public String getServerLastGlobalHash() { return serverLastGlobalHash; } public void setServerLastGlobalHash(String v) { this.serverLastGlobalHash = v; } - - public int getServerLastLineNumber() { return serverLastLineNumber; } - public void setServerLastLineNumber(int v) { this.serverLastLineNumber = v; } - - public String getServerLastLineHash() { return serverLastLineHash; } - public void setServerLastLineHash(String v) { this.serverLastLineHash = v; } - - public int getLineIndex() { return lineIndex; } - public void setLineIndex(int lineIndex) { this.lineIndex = lineIndex; } -} +} \ No newline at end of file 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 28da112..21ca255 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,5 +1,7 @@ package server.logic.ws_protocol.JSON.handlers.blockchain; +import blockchain_new.BchBlockEntry_new; +import blockchain_new.BchCryptoVerifier_new; import server.logic.ws_protocol.WireCodes; import shine.db.SqliteDbController; import shine.db.dao.BlockchainStateDAO; @@ -14,29 +16,31 @@ import java.sql.SQLException; import java.util.Base64; /** - * BlockchainStateService_new — атомарное добавление блока: - * - (опционально) проверки - * - вставка строки блока в таблицу blocks - * - обновление агрегатного состояния blockchain_state + * 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) * - * Важно: - * - всё делается в одной транзакции - * - DAO-методы с Connection НЕ закрывают соединение + * Ответ наружу: только reasonCode + serverLastGlobalNumber/serverLastGlobalHash */ public final class BlockchainStateService_new { - /** Результат атомарного addBlock */ public static final class AddBlockResult { - public final int lineIndex; // 0..7 (пока ставим 0) - public final int httpStatus; // WireCodes.Status.* + public final int httpStatus; public final String reasonCode; // null если ok - public final BlockchainStateEntry stateAfter; // состояние после (может быть null) + public final Integer serverLastGlobalNumber; // может быть null при ошибке + public final String serverLastGlobalHash; // может быть null при ошибке - public AddBlockResult(int lineIndex, int httpStatus, String reasonCode, BlockchainStateEntry stateAfter) { - this.lineIndex = lineIndex; + public AddBlockResult(int httpStatus, String reasonCode, + Integer serverLastGlobalNumber, String serverLastGlobalHash) { this.httpStatus = httpStatus; this.reasonCode = reasonCode; - this.stateAfter = stateAfter; + this.serverLastGlobalNumber = serverLastGlobalNumber; + this.serverLastGlobalHash = serverLastGlobalHash; } public boolean isOk() { @@ -62,104 +66,145 @@ public final class BlockchainStateService_new { return instance; } - /** - * Атомарно добавляет блок (в рамках одной транзакции) и возвращает результат, - * чтобы хэндлер мог заполнить ответ клиенту. - */ public AddBlockResult addBlockAtomically( String login, String blockchainName, int globalNumber, - String prevGlobalHash, + String prevGlobalHashFromClient, String blockBytesB64 ) { - - // Пока не парсим lineIndex из блока — ставим 0, чтобы протокол работал. - // Позже сделаем реальный разбор (и это же место будет правильным для вычисления хэшей). - final int lineIndex = 0; - - byte[] blockBytes; + byte[] fullBytes; try { - blockBytes = decodeBase64(blockBytesB64); + fullBytes = decodeBase64(blockBytesB64); } catch (Exception e) { - return new AddBlockResult( - lineIndex, - WireCodes.Status.BAD_REQUEST, - "bad_block_base64", - null - ); + return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_block_base64", null, null); } - if (login == null || login.isBlank()) { - return new AddBlockResult(lineIndex, WireCodes.Status.BAD_REQUEST, "empty_login", null); + if (login == null || login.isBlank()) + return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "empty_login", null, null); + + if (blockchainName == null || blockchainName.isBlank()) + return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "empty_blockchain_name", null, null); + + if (fullBytes == null || fullBytes.length == 0) + return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "empty_block_bytes", null, null); + + // Разбор блока (проверит recordSize == fullBytes.length) + final BchBlockEntry_new block; + try { + block = new BchBlockEntry_new(fullBytes); + } catch (Exception e) { + return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_block_format", null, null); } - if (blockchainName == null || blockchainName.isBlank()) { - return new AddBlockResult(lineIndex, WireCodes.Status.BAD_REQUEST, "empty_blockchain_name", null); - } - if (blockBytes == null || blockBytes.length == 0) { - return new AddBlockResult(lineIndex, WireCodes.Status.BAD_REQUEST, "empty_block_bytes", null); + + // Минимальные sanity-checks запроса vs блока + if (block.recordNumber != globalNumber) { + return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "global_number_mismatch", null, null); } try (Connection c = db.getConnection()) { boolean oldAutoCommit = c.getAutoCommit(); c.setAutoCommit(false); try { - // 1) получаем пользователя по login (если надо валидировать существование) + // 1) user by login (loginKey нужен для подписи) SolanaUserEntry u = solanaUsersDAO.getByLogin(c, login); if (u == null) { c.rollback(); - return new AddBlockResult( - lineIndex, - WireCodes.Status.NOT_FOUND, - "user_not_found", - null - ); + return new AddBlockResult(WireCodes.Status.NOT_FOUND, "user_not_found", null, null); } - // 2) вставляем блок в blocks - insertBlockRow(c, login, blockchainName, globalNumber, prevGlobalHash, blockBytes, lineIndex); + byte[] loginKey32 = u.getLoginKeyByte(); + if (loginKey32 == null || loginKey32.length != 32) { + c.rollback(); + return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_login_key", null, null); + } - // 3) обновляем агрегатное состояние blockchain_state (по blockchainName) + // 2) состояние цепочки по blockchainName BlockchainStateEntry st = stateDAO.getByBlockchainName(c, blockchainName); if (st == null) { c.rollback(); - return new AddBlockResult( - lineIndex, - WireCodes.Status.NOT_FOUND, - "blockchain_state_not_found", - null - ); + return new AddBlockResult(WireCodes.Status.NOT_FOUND, "blockchain_state_not_found", null, null); } - // MVP: обновляем “последний глобальный номер”. + // 3) проверка последовательности globalNumber (по DB, а не по клиенту) + int expected = st.getLastGlobalNumber() + 1; + if (globalNumber != expected) { + c.rollback(); + return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_global_sequence", + st.getLastGlobalNumber(), st.getLastGlobalHash()); + } + + // 4) prev hashes берём с сервера + byte[] prevGlobalHash32 = hexToBytes32(st.getLastGlobalHash()); + short line = block.line; + int lineIndex = normalizeLineIndex(line); + byte[] prevLineHash32 = hexToBytes32(st.getLastLineHash(lineIndex)); + + // (опционально) можно сверить, что клиент прислал то же ожидание: + if (prevGlobalHashFromClient != null && !prevGlobalHashFromClient.isBlank()) { + String a = nn(prevGlobalHashFromClient).trim(); + String b = nn(st.getLastGlobalHash()).trim(); + if (!a.equalsIgnoreCase(b)) { + c.rollback(); + return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "prev_global_hash_mismatch", + st.getLastGlobalNumber(), st.getLastGlobalHash()); + } + } + + // 5) verify signature + byte[] rawBytes = block.getRawBytes(); + byte[] preimage = BchCryptoVerifier_new.buildPreimage( + login, + prevGlobalHash32, + prevLineHash32, + rawBytes + ); + byte[] computedHash32 = BchCryptoVerifier_new.sha256(preimage); + + // hash, присланный в блоке + byte[] blockHash32 = block.getHash32(); + if (!equals32(computedHash32, blockHash32)) { + c.rollback(); + return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_block_hash", + st.getLastGlobalNumber(), st.getLastGlobalHash()); + } + + boolean sigOk = BchCryptoVerifier_new.verifySignature( + computedHash32, + block.getSignature64(), + loginKey32 + ); + if (!sigOk) { + c.rollback(); + return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_block_signature", + st.getLastGlobalNumber(), st.getLastGlobalHash()); + } + + // 6) вставляем блок в blocks (пока line stuff MVP) + insertBlockRow(c, login, blockchainName, globalNumber, st.getLastGlobalHash(), fullBytes, lineIndex, block.lineNumber); + + // 7) обновляем агрегатное состояние st.setLastGlobalNumber(globalNumber); - st.setLastGlobalHash(nn(prevGlobalHash)); // TODO: заменить на hash нового блока + st.setLastGlobalHash(toHexLower(computedHash32)); st.setUpdatedAtMs(System.currentTimeMillis()); - // (линии пока не трогаем — позже внесём логику lineNumber/lineHash) + // линии (пока минимально) + st.setLastLineNumber(lineIndex, block.lineNumber); + st.setLastLineHash(lineIndex, toHexLower(computedHash32)); // пока можно тем же, позже разделим + stateDAO.upsert(c, st); c.commit(); - return new AddBlockResult(lineIndex, WireCodes.Status.OK, null, st); + return new AddBlockResult(WireCodes.Status.OK, null, st.getLastGlobalNumber(), st.getLastGlobalHash()); } catch (Exception e) { try { c.rollback(); } catch (SQLException ignore) {} - return new AddBlockResult( - lineIndex, - WireCodes.Status.INTERNAL_ERROR, - "internal_error", - null - ); + return new AddBlockResult(WireCodes.Status.INTERNAL_ERROR, "internal_error", null, null); } finally { try { c.setAutoCommit(oldAutoCommit); } catch (SQLException ignore) {} } } catch (Exception e) { - return new AddBlockResult( - lineIndex, - WireCodes.Status.INTERNAL_ERROR, - "db_error", - null - ); + return new AddBlockResult(WireCodes.Status.INTERNAL_ERROR, "db_error", null, null); } } @@ -168,9 +213,10 @@ public final class BlockchainStateService_new { String login, String blockchainName, int globalNumber, - String prevGlobalHash, + String prevGlobalHashServer, byte[] blockBytes, - int lineIndex + int lineIndex, + int lineNumber ) throws SQLException { BlockEntry e = new BlockEntry(); @@ -179,18 +225,16 @@ public final class BlockchainStateService_new { e.setBchName(blockchainName); e.setBlockGlobalNumber(globalNumber); - e.setBlockGlobalPreHashe(nn(prevGlobalHash)); + e.setBlockGlobalPreHashe(nn(prevGlobalHashServer)); - // Заглушки под линии — позже заменим на реальную логику из blockBytes. e.setBlockLineIndex(lineIndex); - e.setBlockLineNumber(0); - e.setBlockLinePreHashe(""); + e.setBlockLineNumber(lineNumber); + e.setBlockLinePreHashe(nn("")); // можно потом хранить prevLineHash e.setMsgType(0); - e.setBlockByte(blockBytes); - // NEW: nullable ссылки (не забиваем фейковыми нулями) + // nullable links e.setToLogin(null); e.setToBchName(null); e.setToBlockGlobalNumber(null); @@ -209,4 +253,40 @@ public final class BlockchainStateService_new { if (s == null || s.isBlank()) return null; return Base64.getDecoder().decode(s); } + + private static int normalizeLineIndex(short line) { + int v = line & 0xFFFF; + // пока поддержим 0..7 как “линии” + if (v < 0 || v > 7) return 0; + return v; + } + + private static boolean equals32(byte[] a, byte[] b) { + if (a == null || b == null || a.length != 32 || b.length != 32) return false; + int x = 0; + for (int i = 0; i < 32; i++) x |= (a[i] ^ b[i]); + return x == 0; + } + + private static byte[] hexToBytes32(String hex) { + if (hex == null) return new byte[32]; + String s = hex.trim(); + if (s.isEmpty()) return new byte[32]; + if (s.length() != 64 || !s.matches("^[0-9a-fA-F]{64}$")) return new byte[32]; + + byte[] out = new byte[32]; + for (int i = 0; i < 32; i++) { + int hi = Character.digit(s.charAt(i * 2), 16); + int lo = Character.digit(s.charAt(i * 2 + 1), 16); + out[i] = (byte) ((hi << 4) | lo); + } + return out; + } + + private static String toHexLower(byte[] b32) { + if (b32 == null) return ""; + StringBuilder sb = new StringBuilder(b32.length * 2); + for (byte b : b32) sb.append(String.format("%02x", b)); + return sb.toString(); + } } \ 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_new_Handler.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/Net_AddBlock_new_Handler.java index 04ba90c..771f92b 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 @@ -25,7 +25,6 @@ public final class Net_AddBlock_new_Handler implements JsonMessageHandler { Net_AddBlock_Response resp = new Net_AddBlock_Response(); resp.setOp(req.getOp()); resp.setRequestId(req.getRequestId()); - resp.setLineIndex(r.lineIndex); if (r.isOk()) { resp.setStatus(WireCodes.Status.OK); @@ -35,13 +34,12 @@ public final class Net_AddBlock_new_Handler implements JsonMessageHandler { resp.setReasonCode(r.reasonCode); } - if (r.stateAfter != null) { - resp.setServerLastGlobalNumber(r.stateAfter.getLastGlobalNumber()); - resp.setServerLastGlobalHash(r.stateAfter.getLastGlobalHash()); - - int line = (r.lineIndex >= 0 && r.lineIndex <= 7) ? r.lineIndex : 0; - resp.setServerLastLineNumber(r.stateAfter.getLastLineNumber(line)); - resp.setServerLastLineHash(r.stateAfter.getLastLineHash(line)); + // Даже при ошибке (например bad_global_sequence) можно вернуть “что сервер считает последним” + if (r.serverLastGlobalNumber != null) { + resp.setServerLastGlobalNumber(r.serverLastGlobalNumber); + } + if (r.serverLastGlobalHash != null) { + resp.setServerLastGlobalHash(r.serverLastGlobalHash); } return resp;