From c3d20ba3386d9621c203c956ef84e261d4db92c8c36c153501f83b203a6c18b8 Mon Sep 17 00:00:00 2001 From: AidarKC Date: Fri, 2 Jan 2026 18:52:19 +0300 Subject: [PATCH] =?UTF-8?q?02=2001=2025=20=D0=94=D0=BE=D0=B4=D0=B5=D0=BB?= =?UTF-8?q?=D0=B0=D0=BB=20=D1=82=D0=B5=D1=81=D1=82=D1=8B=20=D0=B8=20=D0=BD?= =?UTF-8?q?=D0=B0=D0=B7=D0=B2=D0=B0=D0=BD=D0=B8=D1=8F=20=D0=BB=D0=B8=D0=BD?= =?UTF-8?q?=D0=B8=D0=B9=20=D1=81=D0=B4=D0=B5=D0=BB=D0=B0=D0=BB=20=D0=B2=20?= =?UTF-8?q?=D0=BA=D0=BE=D0=BD=D1=81=D1=82=D0=B0=D0=BD=D1=82=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Дальше делать: Описание форматов. Запросы клиент-сервер. Промт на клиента. --- Потом в сервак дописать Синхронизацию серверов. --- .../src/main/java/blockchain/LineIndex.java | 17 ++ .../java/blockchain/body/ConnectionBody.java | 4 +- .../main/java/blockchain/body/HeaderBody.java | 5 +- .../java/blockchain/body/ReactionBody.java | 4 +- .../main/java/blockchain/body/TextBody.java | 4 +- .../java/blockchain/body/UserParamBody.java | 4 +- src/test/concat_to_file.sh | 6 +- .../test/it/addBlockUtils/AddBlockSender.java | 196 ++++++++++++++++++ .../test/it/addBlockUtils/ChainState.java | 182 ++++++++++++++++ .../java/test/it/addBlockUtils/JsonMini.java | 24 +++ src/test/java/test/it/utils/TestConfig.java | 2 +- 11 files changed, 440 insertions(+), 8 deletions(-) create mode 100644 shine-server-blockchain/src/main/java/blockchain/LineIndex.java create mode 100644 src/test/java/test/it/addBlockUtils/AddBlockSender.java create mode 100644 src/test/java/test/it/addBlockUtils/ChainState.java create mode 100644 src/test/java/test/it/addBlockUtils/JsonMini.java diff --git a/shine-server-blockchain/src/main/java/blockchain/LineIndex.java b/shine-server-blockchain/src/main/java/blockchain/LineIndex.java new file mode 100644 index 0000000..2211d4d --- /dev/null +++ b/shine-server-blockchain/src/main/java/blockchain/LineIndex.java @@ -0,0 +1,17 @@ +package blockchain; + +/** + * LineIndex — канонические номера линий блокчейна. + * + * Линия = независимая последовательность блоков внутри одного блокчейна. + */ +public final class LineIndex { + + private LineIndex() {} + + public static final short HEADER = 0; // genesis / идентификация + public static final short TEXT = 1; // сообщения + public static final short REACTION = 2; // реакции + public static final short CONNECTION = 3; // связи (friend/contact/follow) + public static final short USER_PARAM = 4; // параметры профиля +} \ No newline at end of file diff --git a/shine-server-blockchain/src/main/java/blockchain/body/ConnectionBody.java b/shine-server-blockchain/src/main/java/blockchain/body/ConnectionBody.java index 6e7932f..e1aca1b 100644 --- a/shine-server-blockchain/src/main/java/blockchain/body/ConnectionBody.java +++ b/shine-server-blockchain/src/main/java/blockchain/body/ConnectionBody.java @@ -1,5 +1,7 @@ package blockchain.body; +import blockchain.LineIndex; + import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.charset.StandardCharsets; @@ -173,7 +175,7 @@ public final class ConnectionBody implements BodyRecord { @Override public short expectedLineIndex() { - return 3; + return LineIndex.CONNECTION; } @Override diff --git a/shine-server-blockchain/src/main/java/blockchain/body/HeaderBody.java b/shine-server-blockchain/src/main/java/blockchain/body/HeaderBody.java index 973056a..63085c9 100644 --- a/shine-server-blockchain/src/main/java/blockchain/body/HeaderBody.java +++ b/shine-server-blockchain/src/main/java/blockchain/body/HeaderBody.java @@ -1,5 +1,7 @@ package blockchain.body; +import blockchain.LineIndex; + import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.charset.StandardCharsets; @@ -93,8 +95,7 @@ public final class HeaderBody implements BodyRecord { @Override public short expectedLineIndex() { - return 0; - } + return LineIndex.HEADER; } @Override public HeaderBody check() { diff --git a/shine-server-blockchain/src/main/java/blockchain/body/ReactionBody.java b/shine-server-blockchain/src/main/java/blockchain/body/ReactionBody.java index 6698ad8..de0160d 100644 --- a/shine-server-blockchain/src/main/java/blockchain/body/ReactionBody.java +++ b/shine-server-blockchain/src/main/java/blockchain/body/ReactionBody.java @@ -1,5 +1,7 @@ package blockchain.body; +import blockchain.LineIndex; + import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.charset.StandardCharsets; @@ -114,7 +116,7 @@ public final class ReactionBody implements BodyRecord { @Override public short expectedLineIndex() { - return 2; + return LineIndex.REACTION; } @Override diff --git a/shine-server-blockchain/src/main/java/blockchain/body/TextBody.java b/shine-server-blockchain/src/main/java/blockchain/body/TextBody.java index 7a7891d..2fd739e 100644 --- a/shine-server-blockchain/src/main/java/blockchain/body/TextBody.java +++ b/shine-server-blockchain/src/main/java/blockchain/body/TextBody.java @@ -1,5 +1,7 @@ package blockchain.body; +import blockchain.LineIndex; + import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.charset.CharacterCodingException; @@ -223,7 +225,7 @@ public final class TextBody implements BodyRecord { @Override public short expectedLineIndex() { - return 1; + return LineIndex.TEXT; } @Override diff --git a/shine-server-blockchain/src/main/java/blockchain/body/UserParamBody.java b/shine-server-blockchain/src/main/java/blockchain/body/UserParamBody.java index 82400e9..5ec09d6 100644 --- a/shine-server-blockchain/src/main/java/blockchain/body/UserParamBody.java +++ b/shine-server-blockchain/src/main/java/blockchain/body/UserParamBody.java @@ -1,5 +1,7 @@ package blockchain.body; +import blockchain.LineIndex; + import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.charset.CharacterCodingException; @@ -138,7 +140,7 @@ public final class UserParamBody implements BodyRecord { @Override public short expectedLineIndex() { - return 4; + return LineIndex.USER_PARAM; } @Override diff --git a/src/test/concat_to_file.sh b/src/test/concat_to_file.sh index 901712c..f6db1f1 100755 --- a/src/test/concat_to_file.sh +++ b/src/test/concat_to_file.sh @@ -12,5 +12,9 @@ find . -type f -name "*.java" | sort | while read -r f; do echo >> "$OUTFILE" # пустая строка-разделитель done -echo "Готово! Все .java файлы собраны в $OUTFILE" +# скопировать весь файл в буфер обмена (Wayland) +wl-copy < "$OUTFILE" +echo "Готово!" +echo "Все .java файлы собраны в $OUTFILE" +echo "Содержимое скопировано в буфер обмена (Wayland)" diff --git a/src/test/java/test/it/addBlockUtils/AddBlockSender.java b/src/test/java/test/it/addBlockUtils/AddBlockSender.java new file mode 100644 index 0000000..c93ebf0 --- /dev/null +++ b/src/test/java/test/it/addBlockUtils/AddBlockSender.java @@ -0,0 +1,196 @@ +package test.it.addBlockUtils; + +import blockchain.BchBlockEntry; +import blockchain.BchCryptoVerifier; +import blockchain.body.BodyRecord; +import test.it.utils.TestConfig; +import test.it.utils.TestLog; +import utils.crypto.Ed25519Util; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.time.Duration; +import java.util.Base64; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +/** + * AddBlockSender — "одна кнопка": + * - принимает ГОТОВЫЙ Body (HeaderBody/TextBody/ReactionBody) + * - сам берёт номера/prev-hash из ChainState + * - строит raw/hash/signature + * - собирает BchBlockEntry (старый, без изменений) + * - отправляет AddBlock + * - проверяет serverLastGlobalHash == localHash + * - обновляет ChainState + * + * В тестах: + * sender.send(body, timeout); + */ +public final class AddBlockSender { + + private static final byte[] ZERO32 = new byte[32]; + private static final String ZERO64 = "0".repeat(64); + + private final ChainState state; + + public AddBlockSender(ChainState state) { + this.state = state; + } + + public ChainState state() { + return state; + } + + /** + * Отправить следующий блок по body.expectedLineIndex(). + * Ничего не возвращает — состояние хранится в ChainState. + */ + public void send(BodyRecord body, Duration timeout) { + if (body == null) throw new IllegalArgumentException("body == null"); + + short lineIndex = body.expectedLineIndex(); + + // header должен быть первым + if (lineIndex == 0) { + if (state.globalLastNumber() != -1) { + throw new IllegalStateException("HEADER должен быть первым: globalLastNumber уже " + state.globalLastNumber()); + } + } else { + if (!state.hasHeader()) { + throw new IllegalStateException("Нельзя слать line=" + lineIndex + " до HEADER (нет headerHash32)"); + } + } + + int globalNumber = state.nextGlobalNumber(); + int lineNumber = state.nextLineNumber(lineIndex); + + byte[] prevGlobalHash32 = (lineIndex == 0) ? ZERO32 : state.prevGlobalHash32ForNext(lineIndex); + byte[] prevLineHash32 = (lineIndex == 0) ? ZERO32 : state.prevLineHash32ForNext(lineIndex); + + long ts = System.currentTimeMillis() / 1000L; + + byte[] bodyBytes = body.toBytes(); + + // RAW bytes (ровно то, что подписываем/хэшируем) + 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(lineNumber) + .put(bodyBytes) + .array(); + + // preimage -> sha256 -> signature + byte[] preimage = BchCryptoVerifier.buildPreimage( + TestConfig.LOGIN(), + prevGlobalHash32, + prevLineHash32, + rawBytes + ); + byte[] hash32 = BchCryptoVerifier.sha256(preimage); + byte[] signature64 = Ed25519Util.sign(hash32, TestConfig.LOGIN_PRIV_KEY()); + + // Собираем полный блок (BchBlockEntry не меняем) + BchBlockEntry entry = new BchBlockEntry( + globalNumber, + ts, + lineIndex, + lineNumber, + bodyBytes, + signature64, + hash32 + ); + + // отправляем JSON + String prevGlobalHashHex = (globalNumber == 0) ? ZERO64 : state.globalLastHashHex(); + + String req = buildAddBlockJson( + TestConfig.BCH_NAME(), + globalNumber, + prevGlobalHashHex, + base64(entry.toBytes()) + ); + + String op = "AddBlock (global=" + globalNumber + ", line=" + lineIndex + ", lineNum=" + lineNumber + ")"; + String resp = WsJsonOneShot.request(op, req, timeout); + + assert200(op, resp); + + String serverLastGlobalHash = extractPayloadString(resp, "serverLastGlobalHash"); + assertNotNull(serverLastGlobalHash, op + ": payload.serverLastGlobalHash must not be null"); + assertEquals(64, serverLastGlobalHash.trim().length(), op + ": serverLastGlobalHash must be 64 hex chars"); + + String localHashHex = bytesToHex64(hash32); + + if (TestConfig.DEBUG()) { + TestLog.ok(op + ": localHash=" + localHashHex); + TestLog.ok(op + ": serverLastGlobalHash=" + serverLastGlobalHash); + } + + assertEquals(localHashHex, serverLastGlobalHash, op + ": serverLastGlobalHash must match local hash"); + + // обновляем ChainState + state.applyAppendedBlock(globalNumber, lineIndex, lineNumber, hash32); + + if (TestConfig.DEBUG()) { + TestLog.ok(op + ": state updated"); + } + } + + // -------------------- json helpers -------------------- + + private static String buildAddBlockJson(String blockchainName, + int globalNumber, + String prevGlobalHashHex, + String blockBytesB64) { + return """ + { + "op": "AddBlock", + "requestId": "%s", + "payload": { + "blockchainName": "%s", + "globalNumber": %d, + "prevGlobalHash": "%s", + "blockBytesB64": "%s" + } + } + """.formatted(WsJsonOneShot.FIXED_REQUEST_ID, blockchainName, globalNumber, prevGlobalHashHex, blockBytesB64); + } + + private static void assert200(String op, String resp) { + int st = test.it.utils.JsonParsers.status(resp); + assertEquals(200, st, op + ": expected status=200, but got=" + st + ", resp=" + resp); + if (TestConfig.DEBUG()) TestLog.ok(op + ": status=200"); + } + + private static String extractPayloadString(String json, String field) { + try { + com.fasterxml.jackson.databind.JsonNode root = + new com.fasterxml.jackson.databind.ObjectMapper().readTree(json); + com.fasterxml.jackson.databind.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 String bytesToHex64(byte[] b32) { + char[] out = new char[64]; + final char[] HEX = "0123456789abcdef".toCharArray(); + for (int i = 0; i < 32; i++) { + int v = b32[i] & 0xFF; + out[i * 2] = HEX[v >>> 4]; + out[i * 2 + 1] = HEX[v & 0x0F]; + } + return new String(out); + } +} \ No newline at end of file diff --git a/src/test/java/test/it/addBlockUtils/ChainState.java b/src/test/java/test/it/addBlockUtils/ChainState.java new file mode 100644 index 0000000..026f896 --- /dev/null +++ b/src/test/java/test/it/addBlockUtils/ChainState.java @@ -0,0 +1,182 @@ +package test.it.addBlockUtils; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +/** + * ChainState — только состояние цепочки (номера/хэши). + * + * Хранит: + * - last globalNumber / last globalHash + * - last lineNum / last lineHash по каждой линии + * - hash32 нулевого блока (headerHash32) — нужен как prevLineHash для первого блока каждой линии + * - map globalNumber -> hash32 (для ссылок reply/reaction на старые блоки) + */ +public final class ChainState { + + public static final int LINES_MAX = 8; + + private static final byte[] ZERO32 = new byte[32]; + private static final String ZERO64 = "0".repeat(64); + + private final int[] lineLastNumber = new int[LINES_MAX]; + private final String[] lineLastHashHex = new String[LINES_MAX]; + + private int globalLastNumber = -1; + private String globalLastHashHex = ZERO64; + + private byte[] headerHash32 = null; + + // Для удобства тестов: чтобы можно было делать reply/like на любой уже отправленный globalNumber + private final Map globalHash32ByNumber = new HashMap<>(); + + public ChainState() { + Arrays.fill(lineLastHashHex, ""); + // lineLastNumber по умолчанию = 0 + } + + // -------------------- getters -------------------- + + public int globalLastNumber() { return globalLastNumber; } + public String globalLastHashHex() { return globalLastHashHex; } + + public int lineLastNumber(short line) { return lineLastNumber[line]; } + public String lineLastHashHex(short line) { return lineLastHashHex[line]; } + + public byte[] headerHash32() { return headerHash32 == null ? null : headerHash32.clone(); } + + public byte[] getGlobalHash32(int globalNumber) { + byte[] h = globalHash32ByNumber.get(globalNumber); + return h == null ? null : h.clone(); + } + + // -------------------- state helpers -------------------- + + public boolean hasHeader() { + return headerHash32 != null && headerHash32.length == 32 && globalLastNumber >= 0; + } + + /** Следующий globalNumber. */ + public int nextGlobalNumber() { + return globalLastNumber + 1; + } + + /** Следующий lineNumber: для line>0 — last+1. Для line0 — всегда 0 (header). */ + public int nextLineNumber(short lineIndex) { + checkLine(lineIndex); + if (lineIndex == 0) return 0; + return lineLastNumber[lineIndex] + 1; + } + + /** prevGlobalHash32: для header это ZERO32, иначе hash последнего глобального блока. */ + public byte[] prevGlobalHash32ForNext(short nextLineIndex) { + // Для genesis/header prevGlobalHash = ZERO32 + if (globalLastNumber < 0) return ZERO32; + return hexToBytes32(globalLastHashHex); + } + + /** + * prevLineHash32 по твоему правилу: + * - для line0 (header) — ZERO32 + * - для первого блока линии (lineLastNumber[line]==0) — hash нулевого блока (headerHash32) + * - иначе — hash последнего блока этой линии + */ + public byte[] prevLineHash32ForNext(short lineIndex) { + checkLine(lineIndex); + if (lineIndex == 0) return ZERO32; + + if (lineLastNumber[lineIndex] == 0) { + if (headerHash32 == null) { + throw new IllegalStateException("headerHash32 is not set but required for first block of line " + lineIndex); + } + return headerHash32.clone(); + } + + String lastHex = lineLastHashHex[lineIndex]; + if (lastHex == null || lastHex.isBlank()) { + throw new IllegalStateException("lineLastHashHex[" + lineIndex + "] is blank but lineLastNumber>0"); + } + return hexToBytes32(lastHex); + } + + /** + * Применить факт успешного добавления блока: + * - обновить global last + * - обновить line last + * - сохранить globalNumber->hash32 + * - если это header: сохранить headerHash32 + */ + public void applyAppendedBlock(int globalNumber, + short lineIndex, + int lineNumber, + byte[] hash32) { + + if (hash32 == null || hash32.length != 32) { + throw new IllegalArgumentException("hash32 must be 32 bytes"); + } + + // базовые ожидания по номерам (для тестов строго) + if (globalNumber != globalLastNumber + 1) { + throw new IllegalStateException("globalNumber sequence broken: expected=" + (globalLastNumber + 1) + " got=" + globalNumber); + } + + checkLine(lineIndex); + + if (lineIndex == 0) { + if (globalNumber != 0 || lineNumber != 0) { + throw new IllegalStateException("Header must be global=0 line=0 lineNum=0"); + } + headerHash32 = hash32.clone(); + } else { + int expectedLineNum = lineLastNumber[lineIndex] + 1; + if (lineNumber != expectedLineNum) { + throw new IllegalStateException("lineNumber sequence broken for line=" + lineIndex + + ": expected=" + expectedLineNum + " got=" + lineNumber); + } + } + + String hex64 = bytesToHex64(hash32); + + globalLastNumber = globalNumber; + globalLastHashHex = hex64; + + lineLastNumber[lineIndex] = lineNumber; + lineLastHashHex[lineIndex] = hex64; + + globalHash32ByNumber.put(globalNumber, hash32.clone()); + } + + // -------------------- utils -------------------- + + private static void checkLine(short lineIndex) { + if (lineIndex < 0 || lineIndex >= LINES_MAX) { + throw new IllegalArgumentException("lineIndex must be 0.." + (LINES_MAX - 1)); + } + } + + 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; + } + + private static String bytesToHex64(byte[] b32) { + char[] out = new char[64]; + final char[] HEX = "0123456789abcdef".toCharArray(); + for (int i = 0; i < 32; i++) { + int v = b32[i] & 0xFF; + out[i * 2] = HEX[v >>> 4]; + out[i * 2 + 1] = HEX[v & 0x0F]; + } + return new String(out); + } +} \ No newline at end of file diff --git a/src/test/java/test/it/addBlockUtils/JsonMini.java b/src/test/java/test/it/addBlockUtils/JsonMini.java new file mode 100644 index 0000000..309bbfb --- /dev/null +++ b/src/test/java/test/it/addBlockUtils/JsonMini.java @@ -0,0 +1,24 @@ +package test.it.addBlockUtils; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * JsonMini — маленькие утилиты, чтобы не раздувать зависимости. + */ +final class JsonMini { + private static final ObjectMapper M = new ObjectMapper(); + private JsonMini() {} + + static String extractPayloadString(String json, String field) { + try { + JsonNode root = M.readTree(json); + JsonNode payload = root.get("payload"); + if (payload != null && payload.has(field)) { + JsonNode v = payload.get(field); + return (v == null || v.isNull()) ? null : v.asText(); + } + } catch (Exception ignore) {} + return null; + } +} \ No newline at end of file diff --git a/src/test/java/test/it/utils/TestConfig.java b/src/test/java/test/it/utils/TestConfig.java index 8110e23..cb0fd8e 100644 --- a/src/test/java/test/it/utils/TestConfig.java +++ b/src/test/java/test/it/utils/TestConfig.java @@ -27,7 +27,7 @@ public final class TestConfig { public static final String WS_URI = "ws://localhost:7070/ws"; // ======= По умолчанию (можно поменять под свою среду) ======= - public static final String DEFAULT_LOGIN = "anya24"; + public static final String DEFAULT_LOGIN = "Anya"; // Суффикс блокчейна по твоему правилу: login + 3 цифры public static final String DEFAULT_BCH_SUFFIX_3 = "001";