From eb37b43de4372ede3e0b105ec4766ab3aab269c9493c1acbe9caec8257924abb Mon Sep 17 00:00:00 2001 From: AidarKC Date: Wed, 17 Dec 2025 13:32:06 +0300 Subject: [PATCH] =?UTF-8?q?17=2012=2025=20=D0=9F=D1=80=D0=BE=D0=BC=D0=B5?= =?UTF-8?q?=D0=B6=D1=83=D1=82=D0=BE=D1=87=D0=BD=D1=8B=D0=B9=20=D0=BA=D0=BE?= =?UTF-8?q?=D0=BC=D0=B8=D1=82=20=D0=B2=D0=B5=D1=80=D0=B8=D0=B8=20=D0=BA?= =?UTF-8?q?=D0=BE=D1=82=D0=BE=D1=80=D0=B0=D1=8F=20=D0=BC=D0=BE=D0=B6=D0=B5?= =?UTF-8?q?=D1=82=20=D0=B1=D1=8B=D1=82=D1=8C=20=D1=80=D0=B0=D0=B1=D0=BE?= =?UTF-8?q?=D1=82=D0=B0=D0=B5=D1=82=20:)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../blockchain_new/BchBlockEntry_new.java | 8 + shine-server-net-protocol/build.gradle | 4 +- .../Blockchain/Net_AddBlock_new_Request.java | 42 +-- .../Blockchain/Net_AddBlock_new_Response.java | 46 ++-- .../blockchain/AddBlock_new_Handler.java | 63 +++++ .../BlockchainStateService_new.java | 245 ++++++++++++++++++ 6 files changed, 376 insertions(+), 32 deletions(-) create mode 100644 shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/AddBlock_new_Handler.java create mode 100644 shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/BlockchainStateService_new.java diff --git a/shine-server-blockchain/src/main/java/blockchain_new/BchBlockEntry_new.java b/shine-server-blockchain/src/main/java/blockchain_new/BchBlockEntry_new.java index a69bbf2..0c0ae1e 100644 --- a/shine-server-blockchain/src/main/java/blockchain_new/BchBlockEntry_new.java +++ b/shine-server-blockchain/src/main/java/blockchain_new/BchBlockEntry_new.java @@ -127,6 +127,14 @@ public final class BchBlockEntry_new { this.fullBytes = bb.array(); } + + public byte[] getRawBytes() { + int rawLen = recordSize - SIGNATURE_LEN - HASH_LEN; + byte[] raw = new byte[rawLen]; + System.arraycopy(fullBytes, 0, raw, 0, rawLen); + return raw; + } + /* ===================================================================== */ public byte[] getSignature64() { diff --git a/shine-server-net-protocol/build.gradle b/shine-server-net-protocol/build.gradle index f74dbdd..da15a35 100644 --- a/shine-server-net-protocol/build.gradle +++ b/shine-server-net-protocol/build.gradle @@ -33,11 +33,11 @@ dependencies { } - +вапва java { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } - +пвпва 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 index c2ae59a..4b46eb2 100644 --- 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 @@ -2,37 +2,49 @@ 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 int globalNumber; - private String prevGlobalHash; // HEX(64) or "" - - private int lineNumber; // 0..7 + private short line; private int lineBlockNumber; - private String prevLineHash; // HEX(64) or "" - private String blockBase64; // base64url of raw .bch bytes + 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 getGlobalNumber() { return globalNumber; } - public void setGlobalNumber(int globalNumber) { this.globalNumber = globalNumber; } + public int getGlobalBlockNumber() { return globalBlockNumber; } + public void setGlobalBlockNumber(int globalBlockNumber) { this.globalBlockNumber = globalBlockNumber; } - public String getPrevGlobalHash() { return prevGlobalHash; } - public void setPrevGlobalHash(String prevGlobalHash) { this.prevGlobalHash = prevGlobalHash; } + public String getPrevGlobalHashHex() { return prevGlobalHashHex; } + public void setPrevGlobalHashHex(String prevGlobalHashHex) { this.prevGlobalHashHex = prevGlobalHashHex; } - public int getLineNumber() { return lineNumber; } - public void setLineNumber(int lineNumber) { this.lineNumber = lineNumber; } + 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 getPrevLineHash() { return prevLineHash; } - public void setPrevLineHash(String prevLineHash) { this.prevLineHash = prevLineHash; } - 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 index b0a219e..09230a9 100644 --- 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 @@ -2,28 +2,44 @@ 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 int serverLastGlobalNumber; - private String serverLastGlobalHash; + private boolean accepted; - private int serverLastLineNumber; - private String serverLastLineHash; + private int newGlobalNumber; + private String newGlobalHashHex; - private String reasonCode; // "OUT_OF_SEQUENCE", "HASH_MISMATCH", ... + private int newLineNumber; + private String newLineHashHex; - public int getServerLastGlobalNumber() { return serverLastGlobalNumber; } - public void setServerLastGlobalNumber(int v) { this.serverLastGlobalNumber = v; } + private int sizeBytes; - public String getServerLastGlobalHash() { return serverLastGlobalHash; } - public void setServerLastGlobalHash(String v) { this.serverLastGlobalHash = v; } + public boolean isAccepted() { return accepted; } + public void setAccepted(boolean accepted) { this.accepted = accepted; } - public int getServerLastLineNumber() { return serverLastLineNumber; } - public void setServerLastLineNumber(int v) { this.serverLastLineNumber = v; } + public int getNewGlobalNumber() { return newGlobalNumber; } + public void setNewGlobalNumber(int newGlobalNumber) { this.newGlobalNumber = newGlobalNumber; } - public String getServerLastLineHash() { return serverLastLineHash; } - public void setServerLastLineHash(String v) { this.serverLastLineHash = v; } + public String getNewGlobalHashHex() { return newGlobalHashHex; } + public void setNewGlobalHashHex(String newGlobalHashHex) { this.newGlobalHashHex = newGlobalHashHex; } - public String getReasonCode() { return reasonCode; } - public void setReasonCode(String reasonCode) { this.reasonCode = reasonCode; } + 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/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 new file mode 100644 index 0000000..9614464 --- /dev/null +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/AddBlock_new_Handler.java @@ -0,0 +1,63 @@ +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 new file mode 100644 index 0000000..f219c7d --- /dev/null +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/BlockchainStateService_new.java @@ -0,0 +1,245 @@ +package server.logic.blockchain_new; + +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 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 ApplyResult applyAddBlock( + String userLogin, + long blockchainId, + int globalBlockNumber, + String prevGlobalHashHexFromClient, + short lineIndex, + int lineBlockNumber, + String blockBase64 + ) throws Exception { + + 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"); + + byte[] fullBytes; + try { + fullBytes = Base64.getDecoder().decode(blockBase64); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("blockBase64 is not valid Base64", e); + } + + BchBlockEntry_new block = new BchBlockEntry_new(fullBytes); + + // Быстрая проверка: что клиентские “в шапке запроса” совпадают с тем, что внутри блока. + 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); + + Object lock = locks.computeIfAbsent(blockchainId, k -> new Object()); + + synchronized (lock) { + Connection conn = db.getConnection(); + boolean prevAutoCommit = conn.getAutoCommit(); + + 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) {} + } + } + } + + // ---------------- 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)); + } + + 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) { + 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