Дорабатываю добавление блоков. Поставил todo что доделать
This commit is contained in:
AidarKC 2025-12-24 14:08:40 +03:00
parent 33635886e0
commit 5ecaf67bcb
8 changed files with 659 additions and 345 deletions

View File

@ -12,7 +12,7 @@ import java.util.Objects;
* [4] recordSize (int) = размер RAW (включая этот заголовок), БЕЗ signature+hash * [4] recordSize (int) = размер RAW (включая этот заголовок), БЕЗ signature+hash
* [4] recordNumber (int) глобальный номер блока * [4] recordNumber (int) глобальный номер блока
* [8] timestamp (long) unix seconds * [8] timestamp (long) unix seconds
* [2] line (short) * [2] lineIndex (short)
* [4] lineNumber (int) * [4] lineNumber (int)
* [N] bodyBytes (body, начинается с [type][version]) * [N] bodyBytes (body, начинается с [type][version])
* *
@ -32,7 +32,7 @@ public final class BchBlockEntry {
public final int recordSize; // только RAW, без signature+hash public final int recordSize; // только RAW, без signature+hash
public final int recordNumber; public final int recordNumber;
public final long timestamp; public final long timestamp;
public final short line; public final short lineIndex;
public final int lineNumber; public final int lineNumber;
public final byte[] bodyBytes; public final byte[] bodyBytes;
@ -60,7 +60,7 @@ public final class BchBlockEntry {
this.recordNumber = bb.getInt(); this.recordNumber = bb.getInt();
this.timestamp = bb.getLong(); this.timestamp = bb.getLong();
this.line = bb.getShort(); this.lineIndex = bb.getShort();
this.lineNumber = bb.getInt(); this.lineNumber = bb.getInt();
int bodyLen = recordSize - RAW_HEADER_SIZE; int bodyLen = recordSize - RAW_HEADER_SIZE;
@ -85,7 +85,7 @@ public final class BchBlockEntry {
public BchBlockEntry(int recordNumber, public BchBlockEntry(int recordNumber,
long timestamp, long timestamp,
short line, short lineIndex,
int lineNumber, int lineNumber,
byte[] bodyBytes, byte[] bodyBytes,
byte[] signature64, byte[] signature64,
@ -102,7 +102,7 @@ public final class BchBlockEntry {
this.recordNumber = recordNumber; this.recordNumber = recordNumber;
this.timestamp = timestamp; this.timestamp = timestamp;
this.line = line; this.lineIndex = lineIndex;
this.lineNumber = lineNumber; this.lineNumber = lineNumber;
this.bodyBytes = Arrays.copyOf(bodyBytes, bodyBytes.length); this.bodyBytes = Arrays.copyOf(bodyBytes, bodyBytes.length);
this.signature64 = Arrays.copyOf(signature64, SIGNATURE_LEN); this.signature64 = Arrays.copyOf(signature64, SIGNATURE_LEN);
@ -117,7 +117,7 @@ public final class BchBlockEntry {
bb.putInt(this.recordSize); bb.putInt(this.recordSize);
bb.putInt(recordNumber); bb.putInt(recordNumber);
bb.putLong(timestamp); bb.putLong(timestamp);
bb.putShort(line); bb.putShort(lineIndex);
bb.putInt(lineNumber); bb.putInt(lineNumber);
bb.put(bodyBytes); bb.put(bodyBytes);
bb.put(this.signature64); bb.put(this.signature64);

View File

@ -3,13 +3,13 @@ package utils.blockchain;
public final class BlockchainNameUtil { public final class BlockchainNameUtil {
/** Сколько символов отрезаем с конца blockchainName, чтобы получить login. */ /** Сколько символов отрезаем с конца blockchainName, чтобы получить login. */
public static final int BLOCKCHAIN_NAME_LOGIN_SUFFIX_LEN = 4; public static final int BLOCKCHAIN_NAME_LOGIN_SUFFIX_LEN = 3;
private BlockchainNameUtil() {} private BlockchainNameUtil() {}
/** /**
* Извлечь login из blockchainName: отрезаем последние 4 символа. * Извлечь login из blockchainName: отрезаем последние 3 символа.
* Пример: "aidar.bch" -> "aidar" * Пример: "Dima001" -> "Dima"
*/ */
public static String loginFromBlockchainName(String blockchainName) { public static String loginFromBlockchainName(String blockchainName) {
if (blockchainName == null) return null; if (blockchainName == null) return null;

View File

@ -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_RefreshSession_Handler;
import server.logic.ws_protocol.JSON.handlers.auth.Net_CloseActiveSession_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.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 server.logic.ws_protocol.JSON.handlers.tempToTest.Net_AddUser_Handler;
import java.util.Map; import java.util.Map;
@ -37,7 +37,7 @@ public final class JsonHandlerRegistry {
"CreateAuthSession", new Net_CreateAuthSession__Handler(), "CreateAuthSession", new Net_CreateAuthSession__Handler(),
"CloseActiveSession", new Net_CloseActiveSession_Handler(), "CloseActiveSession", new Net_CloseActiveSession_Handler(),
"ListSessions", new Net_ListSessions_Handler(), "ListSessions", new Net_ListSessions_Handler(),
"AddBlock", new Net_AddBlock_new_Handler() "AddBlock", new Net_AddBlock_Handler()
// сюда потом добавишь другие операции // сюда потом добавишь другие операции
); );

View File

@ -28,7 +28,7 @@ import java.util.Base64;
* *
* Ответ наружу: только reasonCode + serverLastGlobalNumber/serverLastGlobalHash * Ответ наружу: только reasonCode + serverLastGlobalNumber/serverLastGlobalHash
*/ */
public final class BlockchainStateService_new { public final class BlockchainStateService {
/** Результат атомарного addBlock */ /** Результат атомарного addBlock */
public static final class AddBlockResult { 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 SqliteDbController db = SqliteDbController.getInstance();
private final BlocksDAO blocksDAO = BlocksDAO.getInstance(); private final BlocksDAO blocksDAO = BlocksDAO.getInstance();
private final BlockchainStateDAO stateDAO = BlockchainStateDAO.getInstance(); private final BlockchainStateDAO stateDAO = BlockchainStateDAO.getInstance();
private final SolanaUsersDAO solanaUsersDAO = SolanaUsersDAO.getInstance(); private final SolanaUsersDAO solanaUsersDAO = SolanaUsersDAO.getInstance();
private BlockchainStateService_new() {} private BlockchainStateService() {}
public static BlockchainStateService_new getInstance() { public static BlockchainStateService getInstance() {
if (instance == null) { if (instance == null) {
synchronized (BlockchainStateService_new.class) { synchronized (BlockchainStateService.class) {
if (instance == null) instance = new BlockchainStateService_new(); if (instance == null) instance = new BlockchainStateService();
} }
} }
return instance; return instance;
@ -80,10 +80,12 @@ public final class BlockchainStateService_new {
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "empty_blockchain_name", 0, ""); return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "empty_blockchain_name", 0, "");
// Можно быстро проверить, что login согласован с blockchainName (если хочешь строгость) // Можно быстро проверить, что login согласован с blockchainName (если хочешь строгость)
String loginFromName = BlockchainNameUtil.loginFromBlockchainName(blockchainName); String loginFromBlockchainName = BlockchainNameUtil.loginFromBlockchainName(blockchainName);
if (loginFromName == null || loginFromName.isBlank()) if (loginFromBlockchainName == null || loginFromBlockchainName.isBlank())
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_blockchain_name", 0, ""); return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_blockchain_name", 0, "");
// todo действительно давай прото брать логин из имени блокчена и не передавать его отдельно в запросе!
if (login == null || login.isBlank()) { if (login == null || login.isBlank()) {
// раз уж у нас есть loginFromName можно принимать login пустым, // раз уж у нас есть loginFromName можно принимать login пустым,
// но ты явно передаёшь 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, ""); 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, ""); return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_block_base64", 0, "");
} }
// todo ну и ещё тут нужно проверить что не только сам формат блока верный, но и запись в этом блоке верная - тоесть что её можно распарсить!
final BchBlockEntry block; final BchBlockEntry block;
try { try {
block = new BchBlockEntry(blockBytes); block = new BchBlockEntry(blockBytes);
@ -133,6 +139,8 @@ public final class BlockchainStateService_new {
// 2) состояние блокчейна // 2) состояние блокчейна
BlockchainStateEntry st = stateDAO.getByBlockchainName(c, blockchainName); BlockchainStateEntry st = stateDAO.getByBlockchainName(c, blockchainName);
//todo тут надо учесть тот случай что если это 0 блок тоесть начало блокчейна то логично что ещё нет самого файла блокчейна и по этому нет и BlockchainStateEntry
if (st == null) { if (st == null) {
c.rollback(); c.rollback();
return new AddBlockResult(WireCodes.Status.NOT_FOUND, "blockchain_state_not_found", 0, ""); return new AddBlockResult(WireCodes.Status.NOT_FOUND, "blockchain_state_not_found", 0, "");
@ -156,6 +164,9 @@ public final class BlockchainStateService_new {
} }
byte[] prevLineHash32 = prevGlobalHash32; // пока линии не используем byte[] prevLineHash32 = prevGlobalHash32; // пока линии не используем
//todo точно так же как и глобальный проверяем преведущий хэш по линии. он пока не используется, в том плане что у нас везде линия 0 и приведущий хэш по линии по сути равен приведущему глобальному хэшу, но его тоже надо проверять.
// по сути можно сказать он используется просто пока везде только одна линия с индексом 0
// 5) крипто-проверка // 5) крипто-проверка
boolean ok = BchCryptoVerifier.verifyAll( 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())); return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_signature_or_hash", st.getLastGlobalNumber(), nn(st.getLastGlobalHash()));
} }
//todo сам код добавление блока вынести в отдельный класс - тк там надо потом усложнить действие
// 6) вставка блока // 6) вставка блока
insertBlockRow(c, login, blockchainName, globalNumber, prevGlobalHashHex, blockBytes); insertBlockRow(c, login, blockchainName, globalNumber, prevGlobalHashHex, blockBytes);

View File

@ -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.JSON.handlers.JsonMessageHandler;
import server.logic.ws_protocol.WireCodes; import server.logic.ws_protocol.WireCodes;
public final class Net_AddBlock_new_Handler implements JsonMessageHandler { public final class Net_AddBlock_Handler implements JsonMessageHandler {
@Override @Override
public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) throws Exception { public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) throws Exception {
Net_AddBlock_Request req = (Net_AddBlock_Request) baseReq; Net_AddBlock_Request req = (Net_AddBlock_Request) baseReq;
var r = BlockchainStateService_new.getInstance().addBlockAtomically( var r = BlockchainStateService.getInstance().addBlockAtomically(
req.getLogin(), req.getLogin(),
req.getBlockchainName(), req.getBlockchainName(),
req.getGlobalNumber(), req.getGlobalNumber(),
@ -22,6 +23,10 @@ public final class Net_AddBlock_new_Handler implements JsonMessageHandler {
req.getBlockBytesB64() req.getBlockBytesB64()
); );
// todo если пришёл запрос на добавление то надо блочить работу с этим блокчейном по req.getBlockchainName(),
// с помощью класса BlockchainLocks и разлочивать работу только в конце завершения работы этого хэндлера, что бы не случилось паралельной работы двух потоков с одним и тем же блокчейном!
Net_AddBlock_Response resp = new Net_AddBlock_Response(); Net_AddBlock_Response resp = new Net_AddBlock_Response();
resp.setOp(req.getOp()); resp.setOp(req.getOp());
resp.setRequestId(req.getRequestId()); resp.setRequestId(req.getRequestId());

View File

@ -1,322 +1,317 @@
//package Test; package Test;
//
//import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
//import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
//import utils.crypto.Ed25519Util; import utils.crypto.Ed25519Util;
//import blockchain.body.HeaderBody; import blockchain.body.HeaderBody;
//import blockchain.body.TextBody; import blockchain.body.TextBody;
//import blockchain_new.BchCryptoVerifier_new; import blockchain.BchCryptoVerifier;
//import blockchain_new.BchBlockEntry_new; import blockchain.BchBlockEntry;
//
//import java.net.URI; import java.net.URI;
//import java.net.http.HttpClient; import java.net.http.HttpClient;
//import java.net.http.WebSocket; import java.net.http.WebSocket;
//import java.nio.ByteBuffer; import java.nio.ByteBuffer;
//import java.nio.ByteOrder; import java.nio.ByteOrder;
//import java.util.Base64; import java.util.Base64;
//import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
//import java.util.concurrent.CompletionStage; import java.util.concurrent.CompletionStage;
//import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
//
//public class Test_AddBlock_new_NoAuth { public class Test_AddBlock_new_NoAuth {
//
// private static final String WS_URI = "ws://localhost:7070/ws"; private static final String WS_URI = "ws://localhost:7070/ws";
// private static final ObjectMapper JSON = new ObjectMapper(); private static final ObjectMapper JSON = new ObjectMapper();
//
// private static final String TEST_LOGIN = "anya24"; private static final String TEST_LOGIN = "anya24";
// // По твоему правилу: blockchainName = login + 4 цифры // По твоему правилу: blockchainName = login + 4 цифры
// private static final String TEST_BCH_NAME = TEST_LOGIN + "0001"; private static final String TEST_BCH_NAME = TEST_LOGIN + "0001";
//
// private static final byte[] LOGIN_PRIV_KEY; private static final byte[] LOGIN_PRIV_KEY;
// private static final byte[] LOGIN_PUB_KEY; private static final byte[] LOGIN_PUB_KEY;
//
// static { static {
// LOGIN_PRIV_KEY = Ed25519Util.generatePrivateKeyFromString("test-ed25519-login-11" + TEST_LOGIN); LOGIN_PRIV_KEY = Ed25519Util.generatePrivateKeyFromString("test-ed25519-login-11" + TEST_LOGIN);
// LOGIN_PUB_KEY = Ed25519Util.derivePublicKey(LOGIN_PRIV_KEY); LOGIN_PUB_KEY = Ed25519Util.derivePublicKey(LOGIN_PRIV_KEY);
// } }
//
// private static final byte[] ZERO32 = new byte[32]; private static final byte[] ZERO32 = new byte[32];
// private static final String ZERO64 = "0".repeat(64); private static final String ZERO64 = "0".repeat(64);
//
// public static void main(String[] args) throws Exception { public static void main(String[] args) throws Exception {
// CountDownLatch latch = new CountDownLatch(1); CountDownLatch latch = new CountDownLatch(1);
// HttpClient client = HttpClient.newHttpClient(); HttpClient client = HttpClient.newHttpClient();
//
// client.newWebSocketBuilder() client.newWebSocketBuilder()
// .buildAsync(URI.create(WS_URI), new WebSocket.Listener() { .buildAsync(URI.create(WS_URI), new WebSocket.Listener() {
//
// private int step = 0; private int step = 0;
//
// // Эти значения обновим ПО ОТВЕТУ сервера на header // Эти значения обновим ПО ОТВЕТУ сервера на header
// private String lastGlobalHashHex = ZERO64; private String lastGlobalHashHex = ZERO64;
// private String lastLineHashHex = ZERO64; private String lastLineHashHex = ZERO64;
//
// @Override @Override
// public void onOpen(WebSocket ws) { public void onOpen(WebSocket ws) {
// System.out.println("✅ WS connected: " + WS_URI); System.out.println("✅ WS connected: " + WS_URI);
// ws.request(1); ws.request(1);
//
// // 1) HEADER (global=0, line=0, lineNumber=0) // 1) HEADER (global=0, line=0, lineNumber=0)
// byte[] headerFull = buildHeaderBlockFullBytes( byte[] headerFull = buildHeaderBlockFullBytes(
// /*global*/0, /*global*/0,
// /*lineIndex*/(short)0, /*lineIndex*/(short)0,
// /*lineBlock*/0, /*lineBlock*/0,
// /*prevGlobal*/ZERO32, /*prevGlobal*/ZERO32,
// /*prevLine*/ZERO32 /*prevLine*/ZERO32
// ); );
//
// String json = buildAddBlockJson( String json = buildAddBlockJson(
// "test-add-header", "test-add-header",
// TEST_BCH_NAME, TEST_BCH_NAME,
// 0, 0,
// ZERO64, // prevGlobalHash для первого блока нули ZERO64, // prevGlobalHash для первого блока нули
// base64(headerFull) base64(headerFull)
// ); );
//
// System.out.println("\n📤 SEND #1 (HEADER):\n" + json); System.out.println("\n📤 SEND #1 (HEADER):\n" + json);
// ws.sendText(json, true); ws.sendText(json, true);
// } }
//
// @Override @Override
// public CompletionStage<?> onText(WebSocket ws, CharSequence data, boolean last) { public CompletionStage<?> onText(WebSocket ws, CharSequence data, boolean last) {
// String msg = data.toString(); String msg = data.toString();
// System.out.println("\n📥 RECV:\n" + msg); System.out.println("\n📥 RECV:\n" + msg);
// System.out.println("-----------------------------------------------------"); System.out.println("-----------------------------------------------------");
//
// try { try {
// int status = extractStatus(msg); int status = extractStatus(msg);
//
// if (step == 0) { if (step == 0) {
// if (status != 200) { if (status != 200) {
// System.out.println("❌ HEADER rejected, status=" + status); System.out.println("❌ HEADER rejected, status=" + status);
// ws.sendClose(WebSocket.NORMAL_CLOSURE, "fail"); ws.sendClose(WebSocket.NORMAL_CLOSURE, "fail");
// return CompletableFuture.completedFuture(null); return CompletableFuture.completedFuture(null);
// } }
//
// // Берём ИМЕННО ТОТ хэш, который сервер сохранил в state // Берём ИМЕННО ТОТ хэш, который сервер сохранил в state
// String serverLastGlobalHash = extractPayloadString(msg, "serverLastGlobalHash"); String serverLastGlobalHash = extractPayloadString(msg, "serverLastGlobalHash");
// String serverLastLineHash = extractPayloadString(msg, "serverLastLineHash"); String serverLastLineHash = extractPayloadString(msg, "serverLastLineHash");
//
// if (serverLastGlobalHash == null || serverLastGlobalHash.isBlank()) { if (serverLastGlobalHash == null || serverLastGlobalHash.isBlank()) {
// System.out.println("❌ No serverLastGlobalHash in response"); System.out.println("❌ No serverLastGlobalHash in response");
// ws.sendClose(WebSocket.NORMAL_CLOSURE, "bad-response"); ws.sendClose(WebSocket.NORMAL_CLOSURE, "bad-response");
// return CompletableFuture.completedFuture(null); return CompletableFuture.completedFuture(null);
// } }
// if (serverLastLineHash == null || serverLastLineHash.isBlank()) { if (serverLastLineHash == null || serverLastLineHash.isBlank()) {
// // fallback: пусть будет как global (если сервер так хранит) // fallback: пусть будет как global (если сервер так хранит)
// serverLastLineHash = serverLastGlobalHash; serverLastLineHash = serverLastGlobalHash;
// } }
//
// lastGlobalHashHex = serverLastGlobalHash; lastGlobalHashHex = serverLastGlobalHash;
// lastLineHashHex = serverLastLineHash; lastLineHashHex = serverLastLineHash;
//
// byte[] prevGlobal32 = hexToBytes32(lastGlobalHashHex); byte[] prevGlobal32 = hexToBytes32(lastGlobalHashHex);
// byte[] prevLine32 = hexToBytes32(lastLineHashHex); byte[] prevLine32 = hexToBytes32(lastLineHashHex);
//
// // 2) TEXT (global=1, line=0, lineNumber=1) // 2) TEXT (global=1, line=0, lineNumber=1)
// byte[] textFull = buildTextBlockFullBytes( byte[] textFull = buildTextBlockFullBytes(
// /*global*/1, /*global*/1,
// /*lineIndex*/(short)0, /*lineIndex*/(short)0,
// /*lineBlock*/1, /*lineBlock*/1,
// prevGlobal32, prevGlobal32,
// prevLine32, prevLine32,
// "Hello from test client" "Hello from test client"
// ); );
//
// String json2 = buildAddBlockJson( String json2 = buildAddBlockJson(
// "test-add-text", "test-add-text",
// TEST_BCH_NAME, TEST_BCH_NAME,
// 1, 1,
// lastGlobalHashHex, // prevGlobalHash = хэш header'а из ответа сервера lastGlobalHashHex, // prevGlobalHash = хэш header'а из ответа сервера
// base64(textFull) base64(textFull)
// ); );
//
// System.out.println("\n📤 SEND #2 (TEXT):\n" + json2); System.out.println("\n📤 SEND #2 (TEXT):\n" + json2);
// step = 1; step = 1;
// ws.sendText(json2, true); ws.sendText(json2, true);
//
// } else if (step == 1) { } else if (step == 1) {
// if (status != 200) { if (status != 200) {
// System.out.println("❌ TEXT rejected, status=" + status); System.out.println("❌ TEXT rejected, status=" + status);
// } else { } else {
// System.out.println("✅ Done. Closing."); System.out.println("✅ Done. Closing.");
// } }
// ws.sendClose(WebSocket.NORMAL_CLOSURE, "ok"); ws.sendClose(WebSocket.NORMAL_CLOSURE, "ok");
// } }
//
// } catch (Exception e) { } catch (Exception e) {
// e.printStackTrace(System.out); e.printStackTrace(System.out);
// ws.sendClose(WebSocket.NORMAL_CLOSURE, "exception"); ws.sendClose(WebSocket.NORMAL_CLOSURE, "exception");
// } }
//
// ws.request(1); ws.request(1);
// return CompletableFuture.completedFuture(null); return CompletableFuture.completedFuture(null);
// } }
//
// @Override @Override
// public void onError(WebSocket ws, Throwable error) { public void onError(WebSocket ws, Throwable error) {
// System.out.println("❌ WS error: " + error.getMessage()); System.out.println("❌ WS error: " + error.getMessage());
// error.printStackTrace(System.out); error.printStackTrace(System.out);
// latch.countDown(); latch.countDown();
// } }
//
// @Override @Override
// public CompletionStage<?> onClose(WebSocket ws, int statusCode, String reason) { public CompletionStage<?> onClose(WebSocket ws, int statusCode, String reason) {
// System.out.println("🔚 WS closed. code=" + statusCode + " reason=" + reason); System.out.println("🔚 WS closed. code=" + statusCode + " reason=" + reason);
// latch.countDown(); latch.countDown();
// return CompletableFuture.completedFuture(null); return CompletableFuture.completedFuture(null);
// } }
// }).join(); }).join();
//
// latch.await(); latch.await();
// } }
//
// // ================================================================================= // =================================================================================
// // BUILD BLOCKS // BUILD BLOCKS
// // ================================================================================= // =================================================================================
//
// private static byte[] buildHeaderBlockFullBytes(int globalNumber, private static byte[] buildHeaderBlockFullBytes(int globalNumber,
// short lineIndex, short lineIndex,
// int lineBlockNumber, int lineBlockNumber,
// byte[] prevGlobalHash32, byte[] prevGlobalHash32,
// byte[] prevLineHash32) { byte[] prevLineHash32) {
//
// HeaderBody body = new HeaderBody( HeaderBody body = new HeaderBody(
// TEST_BCH_NAME, // было TEST_BCH_ID (long), теперь имя блокчейна (String) TEST_LOGIN
// TEST_LOGIN, );
// 0, 0, byte[] bodyBytes = body.toBytes();
// (short) 1,
// 0L, return buildSignedBlockFullBytes(globalNumber, lineIndex, lineBlockNumber, bodyBytes, prevGlobalHash32, prevLineHash32);
// LOGIN_PUB_KEY }
// );
// byte[] bodyBytes = body.toBytes(); private static byte[] buildTextBlockFullBytes(int globalNumber,
// short lineIndex,
// return buildSignedBlockFullBytes(globalNumber, lineIndex, lineBlockNumber, bodyBytes, prevGlobalHash32, prevLineHash32); int lineBlockNumber,
// } byte[] prevGlobalHash32,
// byte[] prevLineHash32,
// private static byte[] buildTextBlockFullBytes(int globalNumber, String text) {
// short lineIndex,
// int lineBlockNumber, TextBody body = new TextBody(text);
// byte[] prevGlobalHash32, byte[] bodyBytes = body.toBytes();
// byte[] prevLineHash32,
// String text) { return buildSignedBlockFullBytes(globalNumber, lineIndex, lineBlockNumber, bodyBytes, prevGlobalHash32, prevLineHash32);
// }
// TextBody body = new TextBody(text);
// byte[] bodyBytes = body.toBytes(); private static byte[] buildSignedBlockFullBytes(int globalNumber,
// short lineIndex,
// return buildSignedBlockFullBytes(globalNumber, lineIndex, lineBlockNumber, bodyBytes, prevGlobalHash32, prevLineHash32); int lineBlockNumber,
// } byte[] bodyBytes,
// byte[] prevGlobalHash32,
// private static byte[] buildSignedBlockFullBytes(int globalNumber, byte[] prevLineHash32) {
// short lineIndex,
// int lineBlockNumber, long ts = System.currentTimeMillis() / 1000L;
// byte[] bodyBytes,
// byte[] prevGlobalHash32, int recordSize =
// byte[] prevLineHash32) { BchBlockEntry.RAW_HEADER_SIZE +
// bodyBytes.length +
// long ts = System.currentTimeMillis() / 1000L; BchBlockEntry.SIGNATURE_LEN +
// BchBlockEntry.HASH_LEN;
// int recordSize =
// BchBlockEntry_new.RAW_HEADER_SIZE + byte[] rawBytes = ByteBuffer.allocate(BchBlockEntry.RAW_HEADER_SIZE + bodyBytes.length)
// bodyBytes.length + .order(ByteOrder.BIG_ENDIAN)
// BchBlockEntry_new.SIGNATURE_LEN + .putInt(recordSize)
// BchBlockEntry_new.HASH_LEN; .putInt(globalNumber)
// .putLong(ts)
// byte[] rawBytes = ByteBuffer.allocate(BchBlockEntry_new.RAW_HEADER_SIZE + bodyBytes.length) .putShort(lineIndex)
// .order(ByteOrder.BIG_ENDIAN) .putInt(lineBlockNumber)
// .putInt(recordSize) .put(bodyBytes)
// .putInt(globalNumber) .array();
// .putLong(ts)
// .putShort(lineIndex) byte[] preimage = BchCryptoVerifier.buildPreimage(
// .putInt(lineBlockNumber) TEST_LOGIN,
// .put(bodyBytes) prevGlobalHash32,
// .array(); prevLineHash32,
// rawBytes
// byte[] preimage = BchCryptoVerifier_new.buildPreimage( );
// TEST_LOGIN,
// prevGlobalHash32, byte[] hash32 = BchCryptoVerifier.sha256(preimage);
// prevLineHash32,
// rawBytes // если у тебя подпись должна быть по preimage меняй тут
// ); byte[] signature64 = Ed25519Util.sign(hash32, LOGIN_PRIV_KEY);
//
// byte[] hash32 = BchCryptoVerifier_new.sha256(preimage); return new BchBlockEntry(
// globalNumber,
// // если у тебя подпись должна быть по preimage меняй тут ts,
// byte[] signature64 = Ed25519Util.sign(hash32, LOGIN_PRIV_KEY); lineIndex,
// lineBlockNumber,
// return new BchBlockEntry_new( bodyBytes,
// globalNumber, signature64,
// ts, hash32
// lineIndex, ).toBytes();
// lineBlockNumber, }
// bodyBytes,
// signature64, // =================================================================================
// hash32 // JSON BUILD
// ).toBytes(); // =================================================================================
// }
// private static String buildAddBlockJson(String requestId,
// // ================================================================================= String blockchainName,
// // JSON BUILD int globalNumber,
// // ================================================================================= String prevGlobalHashHex,
// String blockBytesB64) {
// private static String buildAddBlockJson(String requestId, return """
// String blockchainName, {
// int globalNumber, "op": "AddBlock",
// String prevGlobalHashHex, "requestId": "%s",
// String blockBytesB64) { "payload": {
// return """ "login": "%s",
// { "blockchainName": "%s",
// "op": "AddBlock", "globalNumber": %d,
// "requestId": "%s", "prevGlobalHash": "%s",
// "payload": { "blockBytesB64": "%s"
// "login": "%s", }
// "blockchainName": "%s", }
// "globalNumber": %d, """.formatted(requestId, TEST_LOGIN, blockchainName, globalNumber, prevGlobalHashHex, blockBytesB64);
// "prevGlobalHash": "%s", }
// "blockBytesB64": "%s"
// } // =================================================================================
// } // HELPERS
// """.formatted(requestId, TEST_LOGIN, blockchainName, globalNumber, prevGlobalHashHex, blockBytesB64); // =================================================================================
// }
// private static int extractStatus(String json) {
// // ================================================================================= try {
// // HELPERS JsonNode root = JSON.readTree(json);
// // ================================================================================= if (root.has("status")) return root.get("status").asInt();
// } catch (Exception ignore) {}
// private static int extractStatus(String json) { return -1;
// try { }
// JsonNode root = JSON.readTree(json);
// if (root.has("status")) return root.get("status").asInt(); private static String extractPayloadString(String json, String field) {
// } catch (Exception ignore) {} try {
// return -1; JsonNode root = JSON.readTree(json);
// } JsonNode payload = root.get("payload");
// if (payload != null && payload.has(field)) {
// private static String extractPayloadString(String json, String field) { return payload.get(field).asText();
// try { }
// JsonNode root = JSON.readTree(json); } catch (Exception ignore) {}
// JsonNode payload = root.get("payload"); return null;
// if (payload != null && payload.has(field)) { }
// return payload.get(field).asText();
// } private static String base64(byte[] bytes) {
// } catch (Exception ignore) {} return Base64.getEncoder().encodeToString(bytes);
// return null; }
// }
// private static byte[] hexToBytes32(String hex) {
// private static String base64(byte[] bytes) { if (hex == null) throw new IllegalArgumentException("hex is null");
// return Base64.getEncoder().encodeToString(bytes); String s = hex.trim();
// } if (s.length() != 64) throw new IllegalArgumentException("hex must be 64 chars, got " + s.length());
// byte[] out = new byte[32];
// private static byte[] hexToBytes32(String hex) { for (int i = 0; i < 32; i++) {
// if (hex == null) throw new IllegalArgumentException("hex is null"); int hi = Character.digit(s.charAt(i * 2), 16);
// String s = hex.trim(); int lo = Character.digit(s.charAt(i * 2 + 1), 16);
// if (s.length() != 64) throw new IllegalArgumentException("hex must be 64 chars, got " + s.length()); if (hi < 0 || lo < 0) throw new IllegalArgumentException("bad hex at pos " + (i * 2));
// byte[] out = new byte[32]; out[i] = (byte) ((hi << 4) | lo);
// for (int i = 0; i < 32; i++) { }
// int hi = Character.digit(s.charAt(i * 2), 16); return out;
// 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;
// }
//}

View File

@ -9,7 +9,7 @@ public final class TestConfig {
public static final String WS_URI = "ws://localhost:7070/ws"; public static final String WS_URI = "ws://localhost:7070/ws";
public static final String TEST_LOGIN = "anya24"; 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 int TEST_BCH_LIMIT = 1_000_000;
public static final String TEST_CLIENT_INFO = "JavaTestClient/1.0"; public static final String TEST_CLIENT_INFO = "JavaTestClient/1.0";

View File

@ -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;
}
}