diff --git a/shine-server-blockchain/src/main/java/blockchain/BchBlockEntry.java b/shine-server-blockchain/src/main/java/blockchain/BchBlockEntry.java index 242c0cc..2f489ad 100644 --- a/shine-server-blockchain/src/main/java/blockchain/BchBlockEntry.java +++ b/shine-server-blockchain/src/main/java/blockchain/BchBlockEntry.java @@ -12,7 +12,7 @@ import java.util.Objects; * [4] recordSize (int) = размер RAW (включая этот заголовок), БЕЗ signature+hash * [4] recordNumber (int) глобальный номер блока * [8] timestamp (long) unix seconds - * [2] line (short) + * [2] lineIndex (short) * [4] lineNumber (int) * [N] bodyBytes (body, начинается с [type][version]) * @@ -32,7 +32,7 @@ public final class BchBlockEntry { public final int recordSize; // только RAW, без signature+hash public final int recordNumber; public final long timestamp; - public final short line; + public final short lineIndex; public final int lineNumber; public final byte[] bodyBytes; @@ -60,7 +60,7 @@ public final class BchBlockEntry { this.recordNumber = bb.getInt(); this.timestamp = bb.getLong(); - this.line = bb.getShort(); + this.lineIndex = bb.getShort(); this.lineNumber = bb.getInt(); int bodyLen = recordSize - RAW_HEADER_SIZE; @@ -85,7 +85,7 @@ public final class BchBlockEntry { public BchBlockEntry(int recordNumber, long timestamp, - short line, + short lineIndex, int lineNumber, byte[] bodyBytes, byte[] signature64, @@ -102,7 +102,7 @@ public final class BchBlockEntry { this.recordNumber = recordNumber; this.timestamp = timestamp; - this.line = line; + this.lineIndex = lineIndex; this.lineNumber = lineNumber; this.bodyBytes = Arrays.copyOf(bodyBytes, bodyBytes.length); this.signature64 = Arrays.copyOf(signature64, SIGNATURE_LEN); @@ -117,7 +117,7 @@ public final class BchBlockEntry { bb.putInt(this.recordSize); bb.putInt(recordNumber); bb.putLong(timestamp); - bb.putShort(line); + bb.putShort(lineIndex); bb.putInt(lineNumber); bb.put(bodyBytes); bb.put(this.signature64); diff --git a/shine-server-blockchain/src/main/java/utils/blockchain/BlockchainNameUtil.java b/shine-server-blockchain/src/main/java/utils/blockchain/BlockchainNameUtil.java index aeb06e7..ba34146 100644 --- a/shine-server-blockchain/src/main/java/utils/blockchain/BlockchainNameUtil.java +++ b/shine-server-blockchain/src/main/java/utils/blockchain/BlockchainNameUtil.java @@ -3,13 +3,13 @@ package utils.blockchain; public final class BlockchainNameUtil { /** Сколько символов отрезаем с конца blockchainName, чтобы получить login. */ - public static final int BLOCKCHAIN_NAME_LOGIN_SUFFIX_LEN = 4; + public static final int BLOCKCHAIN_NAME_LOGIN_SUFFIX_LEN = 3; private BlockchainNameUtil() {} /** - * Извлечь login из blockchainName: отрезаем последние 4 символа. - * Пример: "aidar.bch" -> "aidar" + * Извлечь login из blockchainName: отрезаем последние 3 символа. + * Пример: "Dima001" -> "Dima" */ public static String loginFromBlockchainName(String blockchainName) { if (blockchainName == null) return null; 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 92ddf65..2c70728 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 @@ -14,7 +14,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.blockchain.Net_AddBlock_Handler; import server.logic.ws_protocol.JSON.handlers.tempToTest.Net_AddUser_Handler; import java.util.Map; @@ -37,7 +37,7 @@ public final class JsonHandlerRegistry { "CreateAuthSession", new Net_CreateAuthSession__Handler(), "CloseActiveSession", new Net_CloseActiveSession_Handler(), "ListSessions", new Net_ListSessions_Handler(), - "AddBlock", new Net_AddBlock_new_Handler() + "AddBlock", new Net_AddBlock_Handler() // сюда потом добавишь другие операции ); 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.java similarity index 84% rename from shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/BlockchainStateService_new.java rename to shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/BlockchainStateService.java index 3c95ca9..e15ea91 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.java @@ -28,7 +28,7 @@ import java.util.Base64; * * Ответ наружу: только reasonCode + serverLastGlobalNumber/serverLastGlobalHash */ -public final class BlockchainStateService_new { +public final class BlockchainStateService { /** Результат атомарного addBlock */ public static final class AddBlockResult { @@ -49,19 +49,19 @@ public final class BlockchainStateService_new { } } - private static volatile BlockchainStateService_new instance; + private static volatile BlockchainStateService instance; private final SqliteDbController db = SqliteDbController.getInstance(); private final BlocksDAO blocksDAO = BlocksDAO.getInstance(); private final BlockchainStateDAO stateDAO = BlockchainStateDAO.getInstance(); private final SolanaUsersDAO solanaUsersDAO = SolanaUsersDAO.getInstance(); - private BlockchainStateService_new() {} + private BlockchainStateService() {} - public static BlockchainStateService_new getInstance() { + public static BlockchainStateService getInstance() { if (instance == null) { - synchronized (BlockchainStateService_new.class) { - if (instance == null) instance = new BlockchainStateService_new(); + synchronized (BlockchainStateService.class) { + if (instance == null) instance = new BlockchainStateService(); } } return instance; @@ -80,10 +80,12 @@ public final class BlockchainStateService_new { return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "empty_blockchain_name", 0, ""); // Можно быстро проверить, что login согласован с blockchainName (если хочешь строгость) - String loginFromName = BlockchainNameUtil.loginFromBlockchainName(blockchainName); - if (loginFromName == null || loginFromName.isBlank()) + String loginFromBlockchainName = BlockchainNameUtil.loginFromBlockchainName(blockchainName); + if (loginFromBlockchainName == null || loginFromBlockchainName.isBlank()) return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_blockchain_name", 0, ""); + + // todo действительно давай прото брать логин из имени блокчена и не передавать его отдельно в запросе! if (login == null || login.isBlank()) { // раз уж у нас есть loginFromName — можно принимать login пустым, // но ты явно передаёшь login, поэтому пока так: @@ -91,7 +93,7 @@ public final class BlockchainStateService_new { } // (опционально) сверка - if (!loginFromName.equals(login)) { + if (!loginFromBlockchainName.equals(login)) { return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "login_not_match_blockchain_name", 0, ""); } @@ -102,6 +104,10 @@ public final class BlockchainStateService_new { return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_block_base64", 0, ""); } +// todo ну и ещё тут нужно проверить что не только сам формат блока верный, но и запись в этом блоке верная - тоесть что её можно распарсить! + + + final BchBlockEntry block; try { block = new BchBlockEntry(blockBytes); @@ -133,6 +139,8 @@ public final class BlockchainStateService_new { // 2) состояние блокчейна BlockchainStateEntry st = stateDAO.getByBlockchainName(c, blockchainName); + + //todo тут надо учесть тот случай что если это 0 блок тоесть начало блокчейна то логично что ещё нет самого файла блокчейна и по этому нет и BlockchainStateEntry if (st == null) { c.rollback(); return new AddBlockResult(WireCodes.Status.NOT_FOUND, "blockchain_state_not_found", 0, ""); @@ -156,6 +164,9 @@ public final class BlockchainStateService_new { } byte[] prevLineHash32 = prevGlobalHash32; // пока линии не используем +//todo точно так же как и глобальный проверяем преведущий хэш по линии. он пока не используется, в том плане что у нас везде линия 0 и приведущий хэш по линии по сути равен приведущему глобальному хэшу, но его тоже надо проверять. +// по сути можно сказать он используется просто пока везде только одна линия с индексом 0 + // 5) крипто-проверка boolean ok = BchCryptoVerifier.verifyAll( @@ -173,6 +184,8 @@ public final class BlockchainStateService_new { return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_signature_or_hash", st.getLastGlobalNumber(), nn(st.getLastGlobalHash())); } + + //todo сам код добавление блока вынести в отдельный класс - тк там надо потом усложнить действие // 6) вставка блока insertBlockRow(c, login, blockchainName, globalNumber, prevGlobalHashHex, blockBytes); 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_Handler.java similarity index 70% rename from shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/Net_AddBlock_new_Handler.java rename to shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/Net_AddBlock_Handler.java index 1b865e9..057b36f 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_Handler.java @@ -8,13 +8,14 @@ import server.logic.ws_protocol.JSON.entyties.blockchain.Net_AddBlock_Response; import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler; import server.logic.ws_protocol.WireCodes; -public final class Net_AddBlock_new_Handler implements JsonMessageHandler { +public final class Net_AddBlock_Handler implements JsonMessageHandler { @Override public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) throws Exception { + Net_AddBlock_Request req = (Net_AddBlock_Request) baseReq; - var r = BlockchainStateService_new.getInstance().addBlockAtomically( + var r = BlockchainStateService.getInstance().addBlockAtomically( req.getLogin(), req.getBlockchainName(), req.getGlobalNumber(), @@ -22,6 +23,10 @@ public final class Net_AddBlock_new_Handler implements JsonMessageHandler { req.getBlockBytesB64() ); + // todo если пришёл запрос на добавление то надо блочить работу с этим блокчейном по req.getBlockchainName(), + // с помощью класса BlockchainLocks и разлочивать работу только в конце завершения работы этого хэндлера, что бы не случилось паралельной работы двух потоков с одним и тем же блокчейном! + + Net_AddBlock_Response resp = new Net_AddBlock_Response(); resp.setOp(req.getOp()); resp.setRequestId(req.getRequestId()); diff --git a/src/main/java/Test/Test_AddBlock_new_NoAuth.java b/src/main/java/Test/Test_AddBlock_new_NoAuth.java index 89b382d..29300b4 100644 --- a/src/main/java/Test/Test_AddBlock_new_NoAuth.java +++ b/src/main/java/Test/Test_AddBlock_new_NoAuth.java @@ -1,322 +1,317 @@ -//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.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"; -// // По твоему правилу: blockchainName = login + 4 цифры -// private static final String TEST_BCH_NAME = TEST_LOGIN + "0001"; -// -// 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]; -// private static final String ZERO64 = "0".repeat(64); -// -// 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; -// -// // Эти значения обновим ПО ОТВЕТУ сервера на header -// private String lastGlobalHashHex = ZERO64; -// private String lastLineHashHex = ZERO64; -// -// @Override -// public void onOpen(WebSocket ws) { -// System.out.println("✅ WS connected: " + WS_URI); -// ws.request(1); -// -// // 1) HEADER (global=0, line=0, lineNumber=0) -// byte[] headerFull = buildHeaderBlockFullBytes( -// /*global*/0, -// /*lineIndex*/(short)0, -// /*lineBlock*/0, -// /*prevGlobal*/ZERO32, -// /*prevLine*/ZERO32 -// ); -// -// String json = buildAddBlockJson( -// "test-add-header", -// TEST_BCH_NAME, -// 0, -// ZERO64, // prevGlobalHash для первого блока — нули -// 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); -// } -// -// // Берём ИМЕННО ТОТ хэш, который сервер сохранил в state -// String serverLastGlobalHash = extractPayloadString(msg, "serverLastGlobalHash"); -// String serverLastLineHash = extractPayloadString(msg, "serverLastLineHash"); -// -// if (serverLastGlobalHash == null || serverLastGlobalHash.isBlank()) { -// System.out.println("❌ No serverLastGlobalHash in response"); -// ws.sendClose(WebSocket.NORMAL_CLOSURE, "bad-response"); -// return CompletableFuture.completedFuture(null); -// } -// if (serverLastLineHash == null || serverLastLineHash.isBlank()) { -// // fallback: пусть будет как global (если сервер так хранит) -// serverLastLineHash = serverLastGlobalHash; -// } -// -// lastGlobalHashHex = serverLastGlobalHash; -// lastLineHashHex = serverLastLineHash; -// -// byte[] prevGlobal32 = hexToBytes32(lastGlobalHashHex); -// byte[] prevLine32 = hexToBytes32(lastLineHashHex); -// -// // 2) TEXT (global=1, line=0, lineNumber=1) -// byte[] textFull = buildTextBlockFullBytes( -// /*global*/1, -// /*lineIndex*/(short)0, -// /*lineBlock*/1, -// prevGlobal32, -// prevLine32, -// "Hello from test client" -// ); -// -// String json2 = buildAddBlockJson( -// "test-add-text", -// TEST_BCH_NAME, -// 1, -// lastGlobalHashHex, // prevGlobalHash = хэш header'а из ответа сервера -// base64(textFull) -// ); -// -// System.out.println("\n📤 SEND #2 (TEXT):\n" + json2); -// step = 1; -// ws.sendText(json2, true); -// -// } else if (step == 1) { -// if (status != 200) { -// System.out.println("❌ TEXT rejected, status=" + status); -// } else { -// 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) { -// -// HeaderBody body = new HeaderBody( -// TEST_BCH_NAME, // было TEST_BCH_ID (long), теперь имя блокчейна (String) -// 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; -// -// 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); -// -// // если у тебя подпись должна быть по preimage — меняй тут -// byte[] signature64 = Ed25519Util.sign(hash32, LOGIN_PRIV_KEY); -// -// return new BchBlockEntry_new( -// globalNumber, -// ts, -// lineIndex, -// lineBlockNumber, -// bodyBytes, -// signature64, -// hash32 -// ).toBytes(); -// } -// -// // ================================================================================= -// // JSON BUILD -// // ================================================================================= -// -// private static String buildAddBlockJson(String requestId, -// String blockchainName, -// int globalNumber, -// String prevGlobalHashHex, -// String blockBytesB64) { -// return """ -// { -// "op": "AddBlock", -// "requestId": "%s", -// "payload": { -// "login": "%s", -// "blockchainName": "%s", -// "globalNumber": %d, -// "prevGlobalHash": "%s", -// "blockBytesB64": "%s" -// } -// } -// """.formatted(requestId, TEST_LOGIN, blockchainName, 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 extractPayloadString(String json, String field) { -// try { -// JsonNode root = JSON.readTree(json); -// JsonNode payload = root.get("payload"); -// if (payload != null && payload.has(field)) { -// return payload.get(field).asText(); -// } -// } catch (Exception ignore) {} -// return null; -// } -// -// private static String base64(byte[] bytes) { -// return Base64.getEncoder().encodeToString(bytes); -// } -// -// private static byte[] hexToBytes32(String hex) { -// if (hex == null) throw new IllegalArgumentException("hex is null"); -// String s = hex.trim(); -// if (s.length() != 64) throw new IllegalArgumentException("hex must be 64 chars, got " + s.length()); -// 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); -// if (hi < 0 || lo < 0) throw new IllegalArgumentException("bad hex at pos " + (i * 2)); -// out[i] = (byte) ((hi << 4) | lo); -// } -// return out; -// } -//} +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.BchCryptoVerifier; +import blockchain.BchBlockEntry; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.WebSocket; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +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"; + // По твоему правилу: blockchainName = login + 4 цифры + private static final String TEST_BCH_NAME = TEST_LOGIN + "0001"; + + 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]; + private static final String ZERO64 = "0".repeat(64); + + 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; + + // Эти значения обновим ПО ОТВЕТУ сервера на header + private String lastGlobalHashHex = ZERO64; + private String lastLineHashHex = ZERO64; + + @Override + public void onOpen(WebSocket ws) { + System.out.println("✅ WS connected: " + WS_URI); + ws.request(1); + + // 1) HEADER (global=0, line=0, lineNumber=0) + byte[] headerFull = buildHeaderBlockFullBytes( + /*global*/0, + /*lineIndex*/(short)0, + /*lineBlock*/0, + /*prevGlobal*/ZERO32, + /*prevLine*/ZERO32 + ); + + String json = buildAddBlockJson( + "test-add-header", + TEST_BCH_NAME, + 0, + ZERO64, // prevGlobalHash для первого блока — нули + 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); + } + + // Берём ИМЕННО ТОТ хэш, который сервер сохранил в state + String serverLastGlobalHash = extractPayloadString(msg, "serverLastGlobalHash"); + String serverLastLineHash = extractPayloadString(msg, "serverLastLineHash"); + + if (serverLastGlobalHash == null || serverLastGlobalHash.isBlank()) { + System.out.println("❌ No serverLastGlobalHash in response"); + ws.sendClose(WebSocket.NORMAL_CLOSURE, "bad-response"); + return CompletableFuture.completedFuture(null); + } + if (serverLastLineHash == null || serverLastLineHash.isBlank()) { + // fallback: пусть будет как global (если сервер так хранит) + serverLastLineHash = serverLastGlobalHash; + } + + lastGlobalHashHex = serverLastGlobalHash; + lastLineHashHex = serverLastLineHash; + + byte[] prevGlobal32 = hexToBytes32(lastGlobalHashHex); + byte[] prevLine32 = hexToBytes32(lastLineHashHex); + + // 2) TEXT (global=1, line=0, lineNumber=1) + byte[] textFull = buildTextBlockFullBytes( + /*global*/1, + /*lineIndex*/(short)0, + /*lineBlock*/1, + prevGlobal32, + prevLine32, + "Hello from test client" + ); + + String json2 = buildAddBlockJson( + "test-add-text", + TEST_BCH_NAME, + 1, + lastGlobalHashHex, // prevGlobalHash = хэш header'а из ответа сервера + base64(textFull) + ); + + System.out.println("\n📤 SEND #2 (TEXT):\n" + json2); + step = 1; + ws.sendText(json2, true); + + } else if (step == 1) { + if (status != 200) { + System.out.println("❌ TEXT rejected, status=" + status); + } else { + 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) { + + HeaderBody body = new HeaderBody( + TEST_LOGIN + ); + 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; + + int recordSize = + BchBlockEntry.RAW_HEADER_SIZE + + bodyBytes.length + + BchBlockEntry.SIGNATURE_LEN + + BchBlockEntry.HASH_LEN; + + byte[] rawBytes = ByteBuffer.allocate(BchBlockEntry.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.buildPreimage( + TEST_LOGIN, + prevGlobalHash32, + prevLineHash32, + rawBytes + ); + + byte[] hash32 = BchCryptoVerifier.sha256(preimage); + + // если у тебя подпись должна быть по preimage — меняй тут + byte[] signature64 = Ed25519Util.sign(hash32, LOGIN_PRIV_KEY); + + return new BchBlockEntry( + globalNumber, + ts, + lineIndex, + lineBlockNumber, + bodyBytes, + signature64, + hash32 + ).toBytes(); + } + + // ================================================================================= + // JSON BUILD + // ================================================================================= + + private static String buildAddBlockJson(String requestId, + String blockchainName, + int globalNumber, + String prevGlobalHashHex, + String blockBytesB64) { + return """ + { + "op": "AddBlock", + "requestId": "%s", + "payload": { + "login": "%s", + "blockchainName": "%s", + "globalNumber": %d, + "prevGlobalHash": "%s", + "blockBytesB64": "%s" + } + } + """.formatted(requestId, TEST_LOGIN, blockchainName, 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 extractPayloadString(String json, String field) { + try { + JsonNode root = JSON.readTree(json); + JsonNode payload = root.get("payload"); + if (payload != null && payload.has(field)) { + return payload.get(field).asText(); + } + } catch (Exception ignore) {} + return null; + } + + private static String base64(byte[] bytes) { + return Base64.getEncoder().encodeToString(bytes); + } + + private static byte[] hexToBytes32(String hex) { + if (hex == null) throw new IllegalArgumentException("hex is null"); + String s = hex.trim(); + if (s.length() != 64) throw new IllegalArgumentException("hex must be 64 chars, got " + s.length()); + 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); + if (hi < 0 || lo < 0) throw new IllegalArgumentException("bad hex at pos " + (i * 2)); + out[i] = (byte) ((hi << 4) | lo); + } + return out; + } +} diff --git a/src/test/java/test/it/utils/TestConfig.java b/src/test/java/test/it/utils/TestConfig.java index 01b3650..11cbdff 100644 --- a/src/test/java/test/it/utils/TestConfig.java +++ b/src/test/java/test/it/utils/TestConfig.java @@ -9,7 +9,7 @@ public final class TestConfig { public static final String WS_URI = "ws://localhost:7070/ws"; public static final String TEST_LOGIN = "anya24"; - public static final String TEST_BCH_NAME = TEST_LOGIN + "0001"; + public static final String TEST_BCH_NAME = TEST_LOGIN + "001"; public static final int TEST_BCH_LIMIT = 1_000_000; public static final String TEST_CLIENT_INFO = "JavaTestClient/1.0"; diff --git a/src/test/java/test/it/ws/Test_AddBlock_new_NoAuth.java b/src/test/java/test/it/ws/Test_AddBlock_new_NoAuth.java new file mode 100644 index 0000000..122ec1e --- /dev/null +++ b/src/test/java/test/it/ws/Test_AddBlock_new_NoAuth.java @@ -0,0 +1,301 @@ +package test.it.ws; + +import blockchain.BchBlockEntry; +import blockchain.BchCryptoVerifier; +import blockchain.body.HeaderBody; +import blockchain.body.TextBody; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import test.it.utils.TestConfig; +import utils.crypto.Ed25519Util; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.WebSocket; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +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 ObjectMapper JSON = new ObjectMapper(); + + private static final byte[] ZERO32 = new byte[32]; + private static final String ZERO64 = "0".repeat(64); + + public static void main(String[] args) throws Exception { + CountDownLatch latch = new CountDownLatch(1); + + HttpClient client = HttpClient.newHttpClient(); + client.newWebSocketBuilder() + .buildAsync(URI.create(TestConfig.WS_URI), new WebSocket.Listener() { + + private int step = 0; + + private String lastGlobalHashHex = ZERO64; + private String lastLineHashHex = ZERO64; + + @Override + public void onOpen(WebSocket ws) { + System.out.println("✅ WS connected: " + TestConfig.WS_URI); + ws.request(1); + + // 1) HEADER block: global=0, line=0, lineNumber=0 + byte[] headerFull = buildHeaderBlockFullBytes( + 0, + (short) 0, + 0, + ZERO32, + ZERO32 + ); + + String json = buildAddBlockJson( + "test-add-header", + TestConfig.TEST_LOGIN, + TestConfig.TEST_BCH_NAME, + 0, + ZERO64, + 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); + } + + String serverLastGlobalHash = extractPayloadString(msg, "serverLastGlobalHash"); + String serverLastLineHash = extractPayloadString(msg, "serverLastLineHash"); + + if (serverLastGlobalHash == null || serverLastGlobalHash.isBlank()) { + System.out.println("❌ No serverLastGlobalHash in response"); + ws.sendClose(WebSocket.NORMAL_CLOSURE, "bad-response"); + return CompletableFuture.completedFuture(null); + } + if (serverLastLineHash == null || serverLastLineHash.isBlank()) { + serverLastLineHash = serverLastGlobalHash; + } + + lastGlobalHashHex = serverLastGlobalHash; + lastLineHashHex = serverLastLineHash; + + byte[] prevGlobal32 = hexToBytes32(lastGlobalHashHex); + byte[] prevLine32 = hexToBytes32(lastLineHashHex); + + // 2) TEXT block: global=1, line=0, lineNumber=1 + byte[] textFull = buildTextBlockFullBytes( + 1, + (short) 0, + 1, + prevGlobal32, + prevLine32, + "Hello from test client" + ); + + String json2 = buildAddBlockJson( + "test-add-text", + TestConfig.TEST_LOGIN, + TestConfig.TEST_BCH_NAME, + 1, + lastGlobalHashHex, + base64(textFull) + ); + + System.out.println("\n📤 SEND #2 (TEXT):\n" + json2); + step = 1; + ws.sendText(json2, true); + + } else if (step == 1) { + if (status != 200) { + System.out.println("❌ TEXT rejected, status=" + status); + } else { + 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) { + + // В твоём текущем коде HeaderBody формата type=0 ver=1: + // [type][ver][tag "SHiNE001"][loginLen][login] + HeaderBody body = new HeaderBody(TestConfig.TEST_LOGIN); + 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; + + // recordSize = только RAW = header + body + int recordSize = BchBlockEntry.RAW_HEADER_SIZE + bodyBytes.length; + + byte[] rawBytes = ByteBuffer.allocate(recordSize) + .order(ByteOrder.BIG_ENDIAN) + .putInt(recordSize) + .putInt(globalNumber) + .putLong(ts) + .putShort(lineIndex) + .putInt(lineBlockNumber) + .put(bodyBytes) + .array(); + + byte[] preimage = BchCryptoVerifier.buildPreimage( + TestConfig.TEST_LOGIN, + prevGlobalHash32, + prevLineHash32, + rawBytes + ); + + byte[] hash32 = BchCryptoVerifier.sha256(preimage); + byte[] signature64 = Ed25519Util.sign(hash32, TestConfig.LOGIN_PRIV_KEY); + + return new BchBlockEntry( + globalNumber, + ts, + lineIndex, + lineBlockNumber, + bodyBytes, + signature64, + hash32 + ).toBytes(); + } + + // ================================================================================= + // JSON BUILD + // ================================================================================= + + private static String buildAddBlockJson(String requestId, + String login, + String blockchainName, + int globalNumber, + String prevGlobalHashHex, + String blockBytesB64) { + return """ + { + "op": "AddBlock", + "requestId": "%s", + "payload": { + "login": "%s", + "blockchainName": "%s", + "globalNumber": %d, + "prevGlobalHash": "%s", + "blockBytesB64": "%s" + } + } + """.formatted(requestId, login, blockchainName, 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 extractPayloadString(String json, String field) { + try { + JsonNode root = JSON.readTree(json); + JsonNode payload = root.get("payload"); + if (payload != null && payload.has(field)) { + return payload.get(field).asText(); + } + } catch (Exception ignore) {} + return null; + } + + private static String base64(byte[] bytes) { + return Base64.getEncoder().encodeToString(bytes); + } + + private static byte[] hexToBytes32(String hex) { + if (hex == null) throw new IllegalArgumentException("hex is null"); + String s = hex.trim(); + if (s.length() != 64) throw new IllegalArgumentException("hex must be 64 chars, got " + s.length()); + 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); + if (hi < 0 || lo < 0) throw new IllegalArgumentException("bad hex at pos " + (i * 2)); + out[i] = (byte) ((hi << 4) | lo); + } + return out; + } +}