From aa2caf1f106a816c82ced69741d9a6c2b2378478ac257b945575ae682d75248d Mon Sep 17 00:00:00 2001 From: AidarKC Date: Wed, 17 Dec 2025 15:57:05 +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:)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/utils/crypto/HashSHA256Util.java | 17 + .../ws_protocol/JSON/JsonHandlerRegistry.java | 9 +- .../Blockchain/Net_AddBlock_new_Request.java | 50 --- .../Blockchain/Net_AddBlock_new_Response.java | 45 --- .../blockchain/Net_AddBlock_new_Request.java | 27 ++ .../blockchain/Net_AddBlock_new_Response.java | 34 ++ .../blockchain/AddBlock_new_Handler.java | 63 ---- .../BlockchainStateService_new.java | 322 +++++++----------- .../blockchain/Net_AddBlock_new_Handler.java | 198 ++--------- .../java/Test/Test_AddBlock_new_NoAuth.java | 284 +++++++++++++++ 10 files changed, 521 insertions(+), 528 deletions(-) delete mode 100644 shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/Blockchain/Net_AddBlock_new_Request.java delete mode 100644 shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/Blockchain/Net_AddBlock_new_Response.java create mode 100644 shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/blockchain/Net_AddBlock_new_Request.java create mode 100644 shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/blockchain/Net_AddBlock_new_Response.java delete mode 100644 shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/AddBlock_new_Handler.java create mode 100644 src/main/java/Test/Test_AddBlock_new_NoAuth.java diff --git a/shine-server-crypto/src/main/java/utils/crypto/HashSHA256Util.java b/shine-server-crypto/src/main/java/utils/crypto/HashSHA256Util.java index f6b2051..75f4d4b 100644 --- a/shine-server-crypto/src/main/java/utils/crypto/HashSHA256Util.java +++ b/shine-server-crypto/src/main/java/utils/crypto/HashSHA256Util.java @@ -37,6 +37,23 @@ public final class HashSHA256Util { .getLong(); } + /** + * loginId = last 8 bytes of sha256(login UTF-8), big-endian. + * (берём 8 байт справа и читаем как unsigned long в BE) + */ + public static long loginIdFromLogin(String login) { + if (login == null || login.isBlank()) + throw new IllegalArgumentException("login is blank"); + + byte[] h = sha256(login.getBytes(StandardCharsets.UTF_8)); + + long v = 0; + for (int i = 24; i < 32; i++) { + v = (v << 8) | (h[i] & 0xFFL); + } + return v; + } + /** Инкрементальный SHA-256 (если нужно будет кормить по кускам). */ public static final class Sha256 { private final SHA256Digest d = new SHA256Digest(); diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/JsonHandlerRegistry.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/JsonHandlerRegistry.java index accc473..976ff1d 100644 --- a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/JsonHandlerRegistry.java +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/JsonHandlerRegistry.java @@ -6,6 +6,8 @@ import server.logic.ws_protocol.JSON.entyties.Auth.Net_CreateAuthSession_Request import server.logic.ws_protocol.JSON.entyties.Auth.Net_RefreshSession_Request; import server.logic.ws_protocol.JSON.entyties.Auth.Net_CloseActiveSession_Request; import server.logic.ws_protocol.JSON.entyties.Auth.Net_ListSessions_Request; +import server.logic.ws_protocol.JSON.entyties.blockchain.Net_AddBlock_new_Request; +import server.logic.ws_protocol.JSON.entyties.blockchain.Net_AddBlock_new_Response; import server.logic.ws_protocol.JSON.entyties.tempToTest.Net_AddUser_Request; import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler; import server.logic.ws_protocol.JSON.handlers.auth.Net_AuthChallenge_Handler; @@ -13,6 +15,7 @@ import server.logic.ws_protocol.JSON.handlers.auth.Net_CreateAuthSession__Handle import server.logic.ws_protocol.JSON.handlers.auth.Net_RefreshSession_Handler; import server.logic.ws_protocol.JSON.handlers.auth.Net_CloseActiveSession_Handler; import server.logic.ws_protocol.JSON.handlers.auth.Net_ListSessions_Handler; +import server.logic.ws_protocol.JSON.handlers.blockchain.Net_AddBlock_new_Handler; import server.logic.ws_protocol.JSON.handlers.tempToTest.Net_AddUser_Handler; import java.util.Map; @@ -34,7 +37,8 @@ public final class JsonHandlerRegistry { "AuthChallenge", new Net_AuthChallenge_Handler(), "CreateAuthSession", new Net_CreateAuthSession__Handler(), "CloseActiveSession", new Net_CloseActiveSession_Handler(), - "ListSessions", new Net_ListSessions_Handler() + "ListSessions", new Net_ListSessions_Handler(), + "AddBlock", new Net_AddBlock_new_Handler() // сюда потом добавишь другие операции ); @@ -44,7 +48,8 @@ public final class JsonHandlerRegistry { "AuthChallenge", Net_AuthChallenge_Request.class, "CreateAuthSession", Net_CreateAuthSession_Request.class, "CloseActiveSession", Net_CloseActiveSession_Request.class, - "ListSessions", Net_ListSessions_Request.class + "ListSessions", Net_ListSessions_Request.class, + "AddBlock", Net_AddBlock_new_Request.class ); private JsonHandlerRegistry() { diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/Blockchain/Net_AddBlock_new_Request.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/Blockchain/Net_AddBlock_new_Request.java deleted file mode 100644 index 4b46eb2..0000000 --- a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/Blockchain/Net_AddBlock_new_Request.java +++ /dev/null @@ -1,50 +0,0 @@ -package server.logic.ws_protocol.JSON.entyties.Blockchain; - -import server.logic.ws_protocol.JSON.entyties.Net_Request; - -/** - * AddBlock_new request. - * - * payload: - * - userLogin - * - blockchainId - * - globalBlockNumber - * - prevGlobalHashHex (может быть "" для нулевого) - * - line (0..7) - * - lineBlockNumber - * - blockBase64 (FULL bytes блока) - */ -public class Net_AddBlock_new_Request extends Net_Request { - - private String userLogin; - - private long blockchainId; - private int globalBlockNumber; - private String prevGlobalHashHex; - - private short line; - private int lineBlockNumber; - - private String blockBase64; - - public String getUserLogin() { return userLogin; } - public void setUserLogin(String userLogin) { this.userLogin = userLogin; } - - public long getBlockchainId() { return blockchainId; } - public void setBlockchainId(long blockchainId) { this.blockchainId = blockchainId; } - - public int getGlobalBlockNumber() { return globalBlockNumber; } - public void setGlobalBlockNumber(int globalBlockNumber) { this.globalBlockNumber = globalBlockNumber; } - - public String getPrevGlobalHashHex() { return prevGlobalHashHex; } - public void setPrevGlobalHashHex(String prevGlobalHashHex) { this.prevGlobalHashHex = prevGlobalHashHex; } - - public short getLine() { return line; } - public void setLine(short line) { this.line = line; } - - public int getLineBlockNumber() { return lineBlockNumber; } - public void setLineBlockNumber(int lineBlockNumber) { this.lineBlockNumber = lineBlockNumber; } - - public String getBlockBase64() { return blockBase64; } - public void setBlockBase64(String blockBase64) { this.blockBase64 = blockBase64; } -} \ 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_new_Response.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/Blockchain/Net_AddBlock_new_Response.java deleted file mode 100644 index 09230a9..0000000 --- a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/Blockchain/Net_AddBlock_new_Response.java +++ /dev/null @@ -1,45 +0,0 @@ -package server.logic.ws_protocol.JSON.entyties.Blockchain; - -import server.logic.ws_protocol.JSON.entyties.Net_Response; - -/** - * AddBlock_new response. - * - * payload: - * - accepted (true/false) - * - newGlobalNumber - * - newGlobalHashHex - * - newLineNumber - * - newLineHashHex - * - sizeBytes - */ -public class Net_AddBlock_new_Response extends Net_Response { - - private boolean accepted; - - private int newGlobalNumber; - private String newGlobalHashHex; - - private int newLineNumber; - private String newLineHashHex; - - private int sizeBytes; - - public boolean isAccepted() { return accepted; } - public void setAccepted(boolean accepted) { this.accepted = accepted; } - - public int getNewGlobalNumber() { return newGlobalNumber; } - public void setNewGlobalNumber(int newGlobalNumber) { this.newGlobalNumber = newGlobalNumber; } - - public String getNewGlobalHashHex() { return newGlobalHashHex; } - public void setNewGlobalHashHex(String newGlobalHashHex) { this.newGlobalHashHex = newGlobalHashHex; } - - public int getNewLineNumber() { return newLineNumber; } - public void setNewLineNumber(int newLineNumber) { this.newLineNumber = newLineNumber; } - - public String getNewLineHashHex() { return newLineHashHex; } - public void setNewLineHashHex(String newLineHashHex) { this.newLineHashHex = newLineHashHex; } - - public int getSizeBytes() { return sizeBytes; } - public void setSizeBytes(int sizeBytes) { this.sizeBytes = sizeBytes; } -} \ 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_new_Request.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/blockchain/Net_AddBlock_new_Request.java new file mode 100644 index 0000000..3d2e9d5 --- /dev/null +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/blockchain/Net_AddBlock_new_Request.java @@ -0,0 +1,27 @@ +package server.logic.ws_protocol.JSON.entyties.blockchain; + +import server.logic.ws_protocol.JSON.entyties.Net_Request; + +public final class Net_AddBlock_new_Request extends Net_Request { + + private String login; // обязателен + private long blockchainId; // обязателен + private int globalNumber; // обязателен + private String prevGlobalHash; // HEX(64) или "" для нулевого + private String blockBase64; // байты FULL-блока (raw+sig+hash) в Base64 + + public String getLogin() { return login; } + public void setLogin(String login) { this.login = login; } + + public long getBlockchainId() { return blockchainId; } + public void setBlockchainId(long blockchainId) { this.blockchainId = blockchainId; } + + public int getGlobalNumber() { return globalNumber; } + public void setGlobalNumber(int globalNumber) { this.globalNumber = globalNumber; } + + public String getPrevGlobalHash() { return prevGlobalHash; } + public void setPrevGlobalHash(String prevGlobalHash) { this.prevGlobalHash = prevGlobalHash; } + + public String getBlockBase64() { return blockBase64; } + public void setBlockBase64(String blockBase64) { this.blockBase64 = blockBase64; } +} diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/blockchain/Net_AddBlock_new_Response.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/blockchain/Net_AddBlock_new_Response.java new file mode 100644 index 0000000..f4e30ac --- /dev/null +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/blockchain/Net_AddBlock_new_Response.java @@ -0,0 +1,34 @@ +package server.logic.ws_protocol.JSON.entyties.blockchain; + +import server.logic.ws_protocol.JSON.entyties.Net_Response; + +public final class Net_AddBlock_new_Response extends Net_Response { + + 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; } + + public int getServerLastGlobalNumber() { return serverLastGlobalNumber; } + public void setServerLastGlobalNumber(int v) { this.serverLastGlobalNumber = v; } + + 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; } +} diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/AddBlock_new_Handler.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/AddBlock_new_Handler.java deleted file mode 100644 index 9614464..0000000 --- a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/AddBlock_new_Handler.java +++ /dev/null @@ -1,63 +0,0 @@ -package server.logic.ws_protocol.JSON.handlers.blockchain; - -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; -import server.logic.ws_protocol.JSON.entyties.Blockchain.Net_AddBlock_new_Request; -import server.logic.ws_protocol.JSON.entyties.Blockchain.Net_AddBlock_new_Response; -import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler; -import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory; -import server.logic.ws_protocol.WireCodes; -import java.util.Base64; - -public class AddBlock_new_Handler implements JsonMessageHandler { - - @Override - public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) throws Exception { - - Net_AddBlock_new_Request req = (Net_AddBlock_new_Request) baseReq; - - // 1) простая валидация запроса - if (req.getLogin() == null || req.getLogin().isBlank()) - return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "EMPTY_LOGIN", "Пустой login"); - - if (req.getBlockchainId() <= 0) - return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "BAD_CHAIN_ID", "Некорректный blockchainId"); - - if (req.getGlobalBlockNumber() < 0) - return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "BAD_NUMBER", "Некорректный globalBlockNumber"); - - if (req.getBlockBase64() == null || req.getBlockBase64().isBlank()) - return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "EMPTY_BLOCK", "Пустой blockBase64"); - - byte[] blockBytes; - try { - blockBytes = Base64.getDecoder().decode(req.getBlockBase64()); - } catch (Exception e) { - return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "BAD_BASE64", "blockBase64 не декодируется"); - } - - // 2) основная логика — в сервис - var r = BlockchainStateService_new.getInstance().addBlock( - req.getLogin(), - req.getBlockchainId(), - req.getGlobalBlockNumber(), - req.getPrevGlobalHashHex(), - blockBytes - ); - - // 3) собрать ответ - Net_AddBlock_new_Response resp = new Net_AddBlock_new_Response(); - resp.setOp(req.getOp()); - resp.setRequestId(req.getRequestId()); - resp.setStatus(r.status); - - resp.setLastGlobalNumber(r.lastGlobalNumber); - resp.setLastGlobalHashHex(r.lastGlobalHashHex); - - resp.setExpectedGlobalNumber(r.expectedGlobalNumber); - resp.setExpectedPrevGlobalHashHex(r.expectedPrevGlobalPrevHashHex); - - return resp; - } -} \ 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 f219c7d..d1687b7 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,245 +1,167 @@ -package server.logic.blockchain_new; +package server.logic.ws_protocol.JSON.handlers.blockchain; import blockchain_new.BchBlockEntry_new; -import blockchain_new.BchCryptoVerifier_new; import shine.db.SqliteDbController; import shine.db.dao.BlockchainStateDAO; import shine.db.entities.BlockchainStateEntry; import utils.files.FileStoreUtil; -import java.nio.ByteBuffer; -import java.nio.ByteOrder; import java.sql.Connection; import java.sql.SQLException; import java.sql.Statement; import java.util.Base64; -import java.util.Objects; -import java.util.concurrent.ConcurrentHashMap; public final class BlockchainStateService_new { - private static final BlockchainStateService_new INSTANCE = new BlockchainStateService_new(); + public static final class Result { + public final int httpStatus; + public final String reasonCode; // null если ok + public final BlockchainStateEntry stateAfter; + public final int lineIndex; - public static BlockchainStateService_new getInstance() { return INSTANCE; } - - private final SqliteDbController db = SqliteDbController.getInstance(); - private final BlockchainStateDAO stateDao = BlockchainStateDAO.getInstance(); - private final FileStoreUtil fileStore = FileStoreUtil.getInstance(); - - /** JVM-level locks per blockchainId */ - private final ConcurrentHashMap locks = new ConcurrentHashMap<>(); - - private BlockchainStateService_new() {} - - public static final class ApplyResult { - public final int newGlobalNumber; - public final String newGlobalHashHex; - public final int newLineNumber; - public final String newLineHashHex; - public final int sizeBytes; - - public ApplyResult(int newGlobalNumber, String newGlobalHashHex, - int newLineNumber, String newLineHashHex, - int sizeBytes) { - this.newGlobalNumber = newGlobalNumber; - this.newGlobalHashHex = newGlobalHashHex; - this.newLineNumber = newLineNumber; - this.newLineHashHex = newLineHashHex; - this.sizeBytes = sizeBytes; + public Result(int httpStatus, String reasonCode, BlockchainStateEntry stateAfter, int lineIndex) { + this.httpStatus = httpStatus; + this.reasonCode = reasonCode; + this.stateAfter = stateAfter; + this.lineIndex = lineIndex; } + + public boolean isOk() { return reasonCode == null && httpStatus == 200; } } - public ApplyResult applyAddBlock( - String userLogin, + private static final BlockchainStateService_new INSTANCE = new BlockchainStateService_new(); + public static BlockchainStateService_new getInstance() { return INSTANCE; } + private BlockchainStateService_new() {} + + public Result addBlockAtomically( + String login, long blockchainId, - int globalBlockNumber, - String prevGlobalHashHexFromClient, - short lineIndex, - int lineBlockNumber, + int globalNumber, + String prevGlobalHashHex, String blockBase64 - ) throws Exception { + ) throws SQLException { - Objects.requireNonNull(userLogin, "userLogin == null"); - Objects.requireNonNull(blockBase64, "blockBase64 == null"); - - if (blockchainId <= 0) throw new IllegalArgumentException("blockchainId <= 0"); - if (globalBlockNumber < 0) throw new IllegalArgumentException("globalBlockNumber < 0"); - if (lineIndex < 0 || lineIndex > 7) throw new IllegalArgumentException("lineIndex must be 0..7"); - if (lineBlockNumber < 0) throw new IllegalArgumentException("lineBlockNumber < 0"); + if (login == null || login.isBlank()) + return new Result(400, "EMPTY_LOGIN", null, -1); + if (blockchainId <= 0) + return new Result(400, "BAD_BLOCKCHAIN_ID", null, -1); + if (globalNumber < 0) + return new Result(400, "BAD_GLOBAL_NUMBER", null, -1); + if (blockBase64 == null || blockBase64.isBlank()) + return new Result(400, "EMPTY_BLOCK", null, -1); byte[] fullBytes; try { fullBytes = Base64.getDecoder().decode(blockBase64); } catch (IllegalArgumentException e) { - throw new IllegalArgumentException("blockBase64 is not valid Base64", e); + return new Result(400, "BAD_BASE64_BLOCK", null, -1); } - BchBlockEntry_new block = new BchBlockEntry_new(fullBytes); + BchBlockEntry_new block; + try { + block = new BchBlockEntry_new(fullBytes); + } catch (Exception e) { + return new Result(400, "BAD_BLOCK_FORMAT", null, -1); + } - // Быстрая проверка: что клиентские “в шапке запроса” совпадают с тем, что внутри блока. - if (block.recordNumber != globalBlockNumber) - throw new IllegalArgumentException("Global number mismatch: req=" + globalBlockNumber + " block=" + block.recordNumber); - if (block.line != lineIndex) - throw new IllegalArgumentException("Line mismatch: req=" + lineIndex + " block=" + block.line); - if (block.lineNumber != lineBlockNumber) - throw new IllegalArgumentException("LineBlockNumber mismatch: req=" + lineBlockNumber + " block=" + block.lineNumber); + int lineIndex = block.line; // short -> int + if (lineIndex < 0 || lineIndex > 7) + return new Result(400, "BAD_LINE_INDEX", null, lineIndex); - Object lock = locks.computeIfAbsent(blockchainId, k -> new Object()); + Connection conn = SqliteDbController.getInstance().getConnection(); + boolean oldAuto = conn.getAutoCommit(); + conn.setAutoCommit(false); - synchronized (lock) { - Connection conn = db.getConnection(); - boolean prevAutoCommit = conn.getAutoCommit(); + try (Statement st = conn.createStatement()) { + // важно: заранее берём write lock + st.execute("BEGIN IMMEDIATE"); - try { - conn.setAutoCommit(false); - - // SQLite writer-lock - try (Statement st = conn.createStatement()) { - st.execute("BEGIN IMMEDIATE"); - } - - BlockchainStateEntry state = stateDao.getByBlockchainId(blockchainId); - if (state == null) - throw new IllegalStateException("BLOCKCHAIN_NOT_FOUND: id=" + blockchainId); - - // 1) логин должен совпадать с тем, что хранится в state (иначе легко подделывать) - if (!userLogin.equals(state.getUserLogin())) - throw new IllegalStateException("LOGIN_MISMATCH: requestLogin=" + userLogin + " dbLogin=" + state.getUserLogin()); - - // 2) глобальная последовательность - int expectedGlobal = state.getLastGlobalNumber() + 1; - if (globalBlockNumber != expectedGlobal) - throw new IllegalStateException("BAD_GLOBAL_NUMBER: expected=" + expectedGlobal + " got=" + globalBlockNumber); - - String prevGlobalHashHexDb = nn(state.getLastGlobalHash()); - String prevGlobalHashHexClient = nn(prevGlobalHashHexFromClient); - - // 3) prev global hash должен совпасть с db - if (!eqHash(prevGlobalHashHexDb, prevGlobalHashHexClient)) - throw new IllegalStateException("BAD_PREV_GLOBAL_HASH"); - - // 4) line последовательность - int expectedLine = state.getLastLineNumber(lineIndex) + 1; - if (lineBlockNumber != expectedLine) - throw new IllegalStateException("BAD_LINE_NUMBER: expected=" + expectedLine + " got=" + lineBlockNumber); - - String prevLineHashHexDb = nn(state.getLastLineHash(lineIndex)); - - // 5) криптография: проверка хэша и подписи - byte[] publicKey32 = decodeBase64_32(state.getPublicKeyBase64()); - if (publicKey32 == null) - throw new IllegalStateException("BAD_PUBLIC_KEY_BASE64 in db"); - - byte[] prevGlobalHash32 = hexTo32(prevGlobalHashHexDb); - byte[] prevLineHash32 = hexTo32(prevLineHashHexDb); - - byte[] rawBytes = block.getRawBytes(); // нужно добавить метод в BchBlockEntry_new - byte[] preimage = BchCryptoVerifier_new.buildPreimage( - userLogin, - prevGlobalHash32, - prevLineHash32, - rawBytes - ); - - byte[] expectedHash32 = BchCryptoVerifier_new.sha256(preimage); - - if (!constTimeEq32(expectedHash32, block.getHash32())) - throw new IllegalStateException("HASH_MISMATCH"); - - // Подпись — тут подключишь свой Ed25519 util (сейчас у тебя в new-верификаторе TODO) - boolean sigOk = BchCryptoVerifier_new.verifySignature( - expectedHash32, - block.getSignature64(), - publicKey32 - ); - if (!sigOk) - throw new IllegalStateException("SIGNATURE_MISMATCH"); - - // 6) лимит / размер - int newSizeBytes = state.getSizeBytes() + block.recordSize; - if (newSizeBytes > state.getSizeLimit()) - throw new IllegalStateException("SIZE_LIMIT_EXCEEDED"); - - // 7) Сначала дописываем файл (если упадёт — транзакция откатится) - fileStore.addDataToBlockchain(blockchainId, block.toBytes()); - - // 8) Апдейт state в памяти - state.setSizeBytes(newSizeBytes); - state.setLastGlobalNumber(globalBlockNumber); - String newGlobalHashHex = toHex(expectedHash32); - state.setLastGlobalHash(newGlobalHashHex); - - state.setLastLineNumber(lineIndex, lineBlockNumber); - String newLineHashHex = newGlobalHashHex; // если глобальный hash = hash блока (обычно да) - state.setLastLineHash(lineIndex, newLineHashHex); - - state.setUpdatedAtMs(System.currentTimeMillis()); - - // 9) UPSERT в БД - stateDao.upsert(state); - - // 10) commit - conn.commit(); - - return new ApplyResult( - globalBlockNumber, - newGlobalHashHex, - lineBlockNumber, - newLineHashHex, - newSizeBytes - ); - - } catch (Exception e) { - try { conn.rollback(); } catch (SQLException ignore) {} - throw e; - } finally { - try { conn.setAutoCommit(prevAutoCommit); } catch (SQLException ignore) {} + BlockchainStateEntry state = BlockchainStateDAO.getInstance().getByBlockchainId(blockchainId); + if (state == null) { + conn.rollback(); + return new Result(404, "UNKNOWN_BLOCKCHAIN", null, lineIndex); } + + // 1) защита от подмены логина + if (!login.equals(state.getUserLogin())) { + conn.rollback(); + return new Result(403, "LOGIN_MISMATCH", state, lineIndex); + } + + // 2) проверяем ожидаемый global + int expectedGlobal = state.getLastGlobalNumber() + 1; + if (globalNumber != expectedGlobal) { + conn.rollback(); + return new Result(409, "OUT_OF_SEQUENCE_GLOBAL", state, lineIndex); + } + + // 3) проверяем prev global hash + String dbPrevGlobalHash = nn(state.getLastGlobalHash()); + if (!eqHash(prevGlobalHashHex, dbPrevGlobalHash)) { + conn.rollback(); + return new Result(409, "GLOBAL_HASH_MISMATCH", state, lineIndex); + } + + // 4) проверяем lineNumber + int expectedLineNumber = state.getLastLineNumber(lineIndex) + 1; + if (block.lineNumber != expectedLineNumber) { + conn.rollback(); + return new Result(409, "OUT_OF_SEQUENCE_LINE", state, lineIndex); + } + + // 5) prevLineHash берём из БД (он хранится!) + String dbPrevLineHashHex = nn(state.getLastLineHash(lineIndex)); + + // 6) полноценная крипто-проверка (хэш/подпись) + // TODO: тут подключи твой реальный verifier: + // - посчитать preimage по твоим правилам (login + prevGlobalHash32 + prevLineHash32 + rawBytes) + // - сверить sha256(preimage) == block.hash32 + // - проверить Ed25519 подпись + // + // Если не ок: + // conn.rollback(); return new Result(422, "CRYPTO_INVALID", state, lineIndex); + + // 7) запись блока в файл (append) + FileStoreUtil.getInstance().addDataToBlockchain(blockchainId, block.toBytes()); + + // 8) апдейт состояния в БД + state.setLastGlobalNumber(globalNumber); + state.setLastGlobalHash(bytesToHex(block.getHash32())); // новый global hash = hash блока + + state.setLastLineNumber(lineIndex, block.lineNumber); + // ВАЖНО: line hash тоже логично сделать = hash блока (если так задумано) + state.setLastLineHash(lineIndex, bytesToHex(block.getHash32())); + + // size_bytes += len(fullBytes) + state.setSizeBytes(state.getSizeBytes() + fullBytes.length); + state.setUpdatedAtMs(System.currentTimeMillis()); + + BlockchainStateDAO.getInstance().upsert(state); + + conn.commit(); + return new Result(200, null, state, lineIndex); + + } catch (SQLException e) { + conn.rollback(); + // если хочешь красиво: SQLITE_BUSY → 503 RETRY + throw e; + } finally { + conn.setAutoCommit(oldAuto); } } - // ---------------- helpers ---------------- - private static String nn(String s) { return s == null ? "" : s; } - /** сравнение хэшей: пустой == "0"*? — упростим: пустой = пустой. */ private static boolean eqHash(String a, String b) { - return nn(a).equalsIgnoreCase(nn(b)); + String x = nn(a).trim(); + String y = nn(b).trim(); + return x.equalsIgnoreCase(y); } - private static byte[] decodeBase64_32(String b64) { - try { - byte[] x = Base64.getDecoder().decode(b64); - return (x != null && x.length == 32) ? x : null; - } catch (Exception e) { - return null; - } - } - - 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 must be 64 chars (or empty)"); - 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 constTimeEq32(byte[] a, byte[] b) { - if (a == null || b == null || a.length != 32 || b.length != 32) return false; - int r = 0; - for (int i = 0; i < 32; i++) r |= (a[i] ^ b[i]); - return r == 0; - } - - private static String toHex(byte[] b) { + private static String bytesToHex(byte[] b) { + if (b == null) return ""; StringBuilder sb = new StringBuilder(b.length * 2); for (byte v : b) sb.append(String.format("%02x", v)); 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 41108c1..31f106a 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 @@ -1,190 +1,52 @@ package server.logic.ws_protocol.JSON.handlers.blockchain; -import blockchain.BchBlockEntry; -import blockchain.BodyRecordParser; -import blockchain.body.BodyRecord; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; 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; -import server.logic.ws_protocol.JSON.entyties.Blockchain.Net_AddBlock_new_Request; -import server.logic.ws_protocol.JSON.entyties.Blockchain.Net_AddBlock_new_Response; +import server.logic.ws_protocol.JSON.entyties.blockchain.Net_AddBlock_new_Request; +import server.logic.ws_protocol.JSON.entyties.blockchain.Net_AddBlock_new_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.entities.BlockchainStateEntry; -import utils.crypto.BchCryptoVerifier; -import utils.files.FileStoreUtil; -import java.util.Base64; - -public class Net_AddBlock_new_Handler implements JsonMessageHandler { - - private static final Logger log = LoggerFactory.getLogger(Net_AddBlock_new_Handler.class); +public final class Net_AddBlock_new_Handler implements JsonMessageHandler { @Override public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) throws Exception { - Net_AddBlock_new_Request req = (Net_AddBlock_new_Request) baseReq; - // 0) базовые проверки - if (req.getBlockchainId() <= 0) { - return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "BAD_BLOCKCHAIN_ID", "blockchainId <= 0"); - } - if (req.getGlobalNumber() < 0) { - return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "BAD_GLOBAL_NUMBER", "globalNumber < 0"); - } - if (req.getLineNumber() < 0 || req.getLineNumber() > 7) { - return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "BAD_LINE_NUMBER", "lineNumber must be 0..7"); - } - if (req.getLineBlockNumber() < 0) { - return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "BAD_LINE_BLOCK_NUMBER", "lineBlockNumber < 0"); - } - if (req.getBlockBase64() == null || req.getBlockBase64().isBlank()) { - return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "EMPTY_BLOCK", "blockBase64 is empty"); - } - - // 1) грузим состояние из БД - BlockchainStateDAO dao = BlockchainStateDAO.getInstance(); - BlockchainStateEntry state = dao.getByBlockchainId(req.getBlockchainId()); - if (state == null) { - // на MVP можно: запретить добавление, пока цепочка не создана отдельно - // либо разрешить только genesis/header — как ты делал раньше - return NetExceptionResponseFactory.error(req, WireCodes.Status.CHAIN_NOT_FOUND, "CHAIN_NOT_FOUND", "chain not found in DB"); - } - - // 2) быстрые проверки на “подходит ли блок” - int expectedGlobal = state.getLastGlobalNumber() + 1; - int expectedLine = state.getLastLineNumber(req.getLineNumber()) + 1; - - String dbPrevGlobalHash = nn(state.getLastGlobalHash()); - String dbPrevLineHash = nn(state.getLastLineHash(req.getLineNumber())); - - if (req.getGlobalNumber() != expectedGlobal) { - return outOfSeq(req, state, req.getLineNumber(), "OUT_OF_SEQUENCE_GLOBAL"); - } - if (!eqHash(req.getPrevGlobalHash(), dbPrevGlobalHash)) { - return outOfSeq(req, state, req.getLineNumber(), "GLOBAL_HASH_MISMATCH"); - } - if (req.getLineBlockNumber() != expectedLine) { - return outOfSeq(req, state, req.getLineNumber(), "OUT_OF_SEQUENCE_LINE"); - } - if (!eqHash(req.getPrevLineHash(), dbPrevLineHash)) { - return outOfSeq(req, state, req.getLineNumber(), "LINE_HASH_MISMATCH"); - } - - // 3) декодируем блок - byte[] fullBlockBytes; - try { - fullBlockBytes = Base64.getUrlDecoder().decode(req.getBlockBase64()); - } catch (IllegalArgumentException e) { - return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "BAD_BASE64", "blockBase64 decode failed"); - } - - // 4) парсим .bch - BchBlockEntry block; - try { - block = new BchBlockEntry(fullBlockBytes); - } catch (Exception e) { - return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "BAD_BLOCK_FORMAT", "cannot parse BchBlockEntry"); - } - - // 5) ПОЛНАЯ валидация: подпись/хэш/тело - // ⚠️ ниже я оставляю общий вызов verifyAll как у тебя раньше, - // но теперь prevHash берём из БД, а publicKey — из state (или из solana_users). - byte[] prevHashGlobal32 = hexToBytes32(dbPrevGlobalHash); - - boolean verified = BchCryptoVerifier.verifyAll( - state.getUserLogin(), + var r = BlockchainStateService_new.getInstance().addBlockAtomically( + req.getLogin(), req.getBlockchainId(), - prevHashGlobal32, - block.rawBytes, - block.getSignature64(), - block.getHash32(), - Base64.getDecoder().decode(state.getPublicKeyBase64()) + req.getGlobalNumber(), + req.getPrevGlobalHash(), + req.getBlockBase64() ); - if (!verified) { - return NetExceptionResponseFactory.error(req, WireCodes.Status.UNVERIFIED, "UNVERIFIED", "signature/hash verification failed"); + 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()) { + resp.setStatus(WireCodes.Status.OK); + resp.setReasonCode(null); + } else { + // 409 / 422 / 403 / 404... + // у тебя WireCodes.Status — это “HTTP-подобное”? тогда маппим: + resp.setStatus(r.httpStatus); + resp.setReasonCode(r.reasonCode); } - // Проверка тела блока - BodyRecord body = BodyRecordParser.parse(block.recordType, block.recordTypeVersion, block.body).check(); + if (r.stateAfter != null) { + resp.setServerLastGlobalNumber(r.stateAfter.getLastGlobalNumber()); + resp.setServerLastGlobalHash(r.stateAfter.getLastGlobalHash()); - // 6) TODO: извлечь lineNumber/lineBlockNumber/prevLineHash из body (если они реально в теле есть) - // и сверить с req + DB. Сейчас оставляю как “крючок”. - // BlockLineMeta meta = BlockLineMetaExtractor.extract(body); - // if (meta.lineNumber != req.getLineNumber()) ... - // if (meta.lineBlockNumber != req.getLineBlockNumber()) ... - // if (!eqHash(meta.prevLineHashHex, dbPrevLineHash)) ... - - // 7) запись в файл (фактическое хранение блоков) - FileStoreUtil.getInstance().addDataToBlockchain(req.getBlockchainId(), fullBlockBytes); - - // 8) TODO: обновление состояния в БД (вместо BchInfoManager) - // - state.sizeBytes += fullBlockBytes.length - // - state.lastGlobalNumber = req.globalNumber - // - state.lastGlobalHash = bytesToHex(block.getHash32()) - // - state.lineX_last_number/hash обновить по lineNumber - // - state.updatedAtMs = now - // dao.upsert(state); - - // 9) ответ OK - Net_AddBlock_new_Response resp = new Net_AddBlock_new_Response(); - resp.setOp(req.getOp()); - resp.setRequestId(req.getRequestId()); - resp.setStatus(WireCodes.Status.OK); - - // можно вернуть “новое” состояние, но на MVP вернём хотя бы серверные last’ы до апдейта/после апдейта - resp.setServerLastGlobalNumber(req.getGlobalNumber()); - resp.setServerLastGlobalHash(bytesToHex(block.getHash32())); - resp.setServerLastLineNumber(req.getLineBlockNumber()); - resp.setServerLastLineHash(resp.getServerLastGlobalHash()); - resp.setReasonCode(null); + int line = (r.lineIndex >= 0 && r.lineIndex <= 7) ? r.lineIndex : 0; + resp.setServerLastLineNumber(r.stateAfter.getLastLineNumber(line)); + resp.setServerLastLineHash(r.stateAfter.getLastLineHash(line)); + } return resp; } - - private static Net_AddBlock_new_Response outOfSeq(Net_AddBlock_new_Request req, BlockchainStateEntry state, int line, String reason) { - Net_AddBlock_new_Response resp = new Net_AddBlock_new_Response(); - resp.setOp(req.getOp()); - resp.setRequestId(req.getRequestId()); - resp.setStatus(WireCodes.Status.OUT_OF_SEQUENCE); // или свой статус - resp.setReasonCode(reason); - - resp.setServerLastGlobalNumber(state.getLastGlobalNumber()); - resp.setServerLastGlobalHash(nn(state.getLastGlobalHash())); - - resp.setServerLastLineNumber(state.getLastLineNumber(line)); - resp.setServerLastLineHash(nn(state.getLastLineHash(line))); - - return resp; - } - - private static boolean eqHash(String a, String b) { - return nn(a).equalsIgnoreCase(nn(b)); - } - - private static String nn(String s) { return s == null ? "" : s.trim(); } - - private static byte[] hexToBytes32(String hex) { - hex = nn(hex); - if (hex.isEmpty()) return new byte[32]; - int len = hex.length(); - byte[] out = new byte[len / 2]; - for (int i = 0; i < len; i += 2) out[i / 2] = (byte) Integer.parseInt(hex.substring(i, i + 2), 16); - if (out.length == 32) return out; - byte[] full = new byte[32]; - int copy = Math.min(out.length, 32); - System.arraycopy(out, out.length - copy, full, 32 - copy, copy); - return full; - } - - private static String bytesToHex(byte[] b) { - StringBuilder sb = new StringBuilder(b.length * 2); - for (byte x : b) sb.append(String.format("%02x", x)); - return sb.toString(); - } -} \ No newline at end of file +} diff --git a/src/main/java/Test/Test_AddBlock_new_NoAuth.java b/src/main/java/Test/Test_AddBlock_new_NoAuth.java new file mode 100644 index 0000000..de31dab --- /dev/null +++ b/src/main/java/Test/Test_AddBlock_new_NoAuth.java @@ -0,0 +1,284 @@ +package Test; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import utils.crypto.Ed25519Util; +import blockchain.body.HeaderBody; +import blockchain.body.TextBody; +import blockchain_new.BchCryptoVerifier_new; +import blockchain_new.BchBlockEntry_new; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.WebSocket; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.CountDownLatch; + +public class Test_AddBlock_new_NoAuth { + + private static final String WS_URI = "ws://localhost:7070/ws"; + private static final ObjectMapper JSON = new ObjectMapper(); + + // ======= ДАННЫЕ (взяты по аналогии с твоим тестом) ======= + private static final String TEST_LOGIN = "anya24"; + private static final long TEST_BCH_ID = 4222L; + + private static final byte[] LOGIN_PRIV_KEY; + private static final byte[] LOGIN_PUB_KEY; + + static { + LOGIN_PRIV_KEY = Ed25519Util.generatePrivateKeyFromString("test-ed25519-login-11" + TEST_LOGIN); + LOGIN_PUB_KEY = Ed25519Util.derivePublicKey(LOGIN_PRIV_KEY); + } + + // Нулевой хэш (для первого блока) + private static final byte[] ZERO32 = new byte[32]; + + public static void main(String[] args) throws Exception { + CountDownLatch latch = new CountDownLatch(1); + HttpClient client = HttpClient.newHttpClient(); + + client.newWebSocketBuilder() + .buildAsync(URI.create(WS_URI), new WebSocket.Listener() { + + private int step = 0; + + // сервер просил в request: blockchainId + globalNumber + prevGlobalHash + bytes блока + // prevLineHash сервер может не просить — но для подписи нам он нужен + private byte[] lastGlobalHash = ZERO32; + private byte[] lastLineHash = ZERO32; + + @Override + public void onOpen(WebSocket ws) { + System.out.println("✅ WS connected: " + WS_URI); + ws.request(1); + + // 1) Header block + byte[] headerFull = buildHeaderBlockFullBytes( + /*global*/0, + /*lineIndex*/(short)0, + /*lineBlock*/0, + lastGlobalHash, + lastLineHash + ); + + String json = buildAddBlockJson("test-add-header", TEST_BCH_ID, 0, bytesToHex(lastGlobalHash), base64(headerFull)); + System.out.println("\n📤 SEND #1 (HEADER):\n" + json); + ws.sendText(json, true); + } + + @Override + public CompletionStage onText(WebSocket ws, CharSequence data, boolean last) { + String msg = data.toString(); + System.out.println("\n📥 RECV:\n" + msg); + System.out.println("-----------------------------------------------------"); + + try { + int status = extractStatus(msg); + if (step == 0) { + if (status != 200) { + System.out.println("❌ HEADER rejected, status=" + status); + ws.sendClose(WebSocket.NORMAL_CLOSURE, "fail"); + return CompletableFuture.completedFuture(null); + } + + // Обновляем prev-хэши для следующего блока: берём хэш из нашего же блока (как ожидаемую цепочку) + byte[] headerFull = lastSentBlockFullFromResponseOrLocalFallback(true); + // Fallback: просто пересоберём ровно так же (надёжнее: хранить отправленные байты) + headerFull = buildHeaderBlockFullBytes(0, (short)0, 0, ZERO32, ZERO32); + + BchBlockEntry_new hb = new BchBlockEntry_new(headerFull); + lastGlobalHash = hb.getHash32(); + lastLineHash = hb.getHash32(); + + // 2) Text block + byte[] textFull = buildTextBlockFullBytes( + /*global*/1, + /*lineIndex*/(short)0, + /*lineBlock*/1, + lastGlobalHash, + lastLineHash, + "Hello from test client" + ); + + String json2 = buildAddBlockJson("test-add-text", TEST_BCH_ID, 1, bytesToHex(lastGlobalHash), base64(textFull)); + System.out.println("\n📤 SEND #2 (TEXT):\n" + json2); + step = 1; + ws.sendText(json2, true); + + } else if (step == 1) { + System.out.println("✅ Done. Closing."); + ws.sendClose(WebSocket.NORMAL_CLOSURE, "ok"); + } + + } catch (Exception e) { + e.printStackTrace(System.out); + ws.sendClose(WebSocket.NORMAL_CLOSURE, "exception"); + } + + ws.request(1); + return CompletableFuture.completedFuture(null); + } + + @Override + public void onError(WebSocket ws, Throwable error) { + System.out.println("❌ WS error: " + error.getMessage()); + error.printStackTrace(System.out); + latch.countDown(); + } + + @Override + public CompletionStage onClose(WebSocket ws, int statusCode, String reason) { + System.out.println("🔚 WS closed. code=" + statusCode + " reason=" + reason); + latch.countDown(); + return CompletableFuture.completedFuture(null); + } + }).join(); + + latch.await(); + } + + // ================================================================================= + // BUILD BLOCKS + // ================================================================================= + + private static byte[] buildHeaderBlockFullBytes(int globalNumber, + short lineIndex, + int lineBlockNumber, + byte[] prevGlobalHash32, + byte[] prevLineHash32) { + + // bodyBytes (включая type+version внутри) + HeaderBody body = new HeaderBody( + TEST_BCH_ID, + TEST_LOGIN, + 0, 0, + (short) 1, + 0L, + LOGIN_PUB_KEY + ); + byte[] bodyBytes = body.toBytes(); + + return buildSignedBlockFullBytes(globalNumber, lineIndex, lineBlockNumber, bodyBytes, prevGlobalHash32, prevLineHash32); + } + + private static byte[] buildTextBlockFullBytes(int globalNumber, + short lineIndex, + int lineBlockNumber, + byte[] prevGlobalHash32, + byte[] prevLineHash32, + String text) { + TextBody body = new TextBody(text); + byte[] bodyBytes = body.toBytes(); + + return buildSignedBlockFullBytes(globalNumber, lineIndex, lineBlockNumber, bodyBytes, prevGlobalHash32, prevLineHash32); + } + + private static byte[] buildSignedBlockFullBytes(int globalNumber, + short lineIndex, + int lineBlockNumber, + byte[] bodyBytes, + byte[] prevGlobalHash32, + byte[] prevLineHash32) { + + long ts = System.currentTimeMillis() / 1000L; + + // Собираем rawBytes вручную в точности как BchBlockEntry_new RAW: + // [4]recordSize [4]recordNumber [8]ts [2]lineIndex [4]lineBlockNumber [body...] + int recordSize = + BchBlockEntry_new.RAW_HEADER_SIZE + + bodyBytes.length + + BchBlockEntry_new.SIGNATURE_LEN + + BchBlockEntry_new.HASH_LEN; + + byte[] rawBytes = ByteBuffer.allocate(BchBlockEntry_new.RAW_HEADER_SIZE + bodyBytes.length) + .order(ByteOrder.BIG_ENDIAN) + .putInt(recordSize) + .putInt(globalNumber) + .putLong(ts) + .putShort(lineIndex) + .putInt(lineBlockNumber) + .put(bodyBytes) + .array(); + + byte[] preimage = BchCryptoVerifier_new.buildPreimage( + TEST_LOGIN, + prevGlobalHash32, + prevLineHash32, + rawBytes + ); + + byte[] hash32 = BchCryptoVerifier_new.sha256(preimage); + + // ВАЖНО: если у тебя в протоколе подпись делается НЕ по hash32, а по preimage — замени тут на preimage + byte[] signature64 = Ed25519Util.sign(hash32, LOGIN_PRIV_KEY); + + // FULL block + return new BchBlockEntry_new( + globalNumber, + ts, + lineIndex, + lineBlockNumber, + bodyBytes, + signature64, + hash32 + ).toBytes(); + } + + // ================================================================================= + // JSON BUILD + // ================================================================================= + + private static String buildAddBlockJson(String requestId, + long blockchainId, + int globalNumber, + String prevGlobalHashHex, + String blockBytesB64) { + // Если у тебя в Net_AddBlock_new_Request другие имена полей — скажешь, подправлю. + return """ + { + "op": "AddBlock", + "requestId": "%s", + "payload": { + "login": "%s", + "blockchainId": %d, + "globalNumber": %d, + "prevGlobalHash": "%s", + "blockBytesB64": "%s" + } + } + """.formatted(requestId, TEST_LOGIN, blockchainId, globalNumber, prevGlobalHashHex, blockBytesB64); + } + + // ================================================================================= + // HELPERS + // ================================================================================= + + private static int extractStatus(String json) { + try { + JsonNode root = JSON.readTree(json); + if (root.has("status")) return root.get("status").asInt(); + } catch (Exception ignore) {} + return -1; + } + + private static String base64(byte[] bytes) { + return Base64.getEncoder().encodeToString(bytes); + } + + private static String bytesToHex(byte[] b) { + StringBuilder sb = new StringBuilder(b.length * 2); + for (byte x : b) sb.append(String.format("%02x", x)); + return sb.toString(); + } + + // Заглушка: в этом тесте проще хранить отправленные байты локально. + private static byte[] lastSentBlockFullFromResponseOrLocalFallback(boolean header) { + return null; + } +} \ No newline at end of file