02 01 25
Доделал тесты и названия линий сделал в константы Дальше делать: Описание форматов. Запросы клиент-сервер. Промт на клиента. --- Потом в сервак дописать Синхронизацию серверов.
This commit is contained in:
parent
be7a3ab7a6
commit
c3d20ba338
@ -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; // параметры профиля
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)"
|
||||
|
||||
196
src/test/java/test/it/addBlockUtils/AddBlockSender.java
Normal file
196
src/test/java/test/it/addBlockUtils/AddBlockSender.java
Normal file
@ -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);
|
||||
}
|
||||
}
|
||||
182
src/test/java/test/it/addBlockUtils/ChainState.java
Normal file
182
src/test/java/test/it/addBlockUtils/ChainState.java
Normal file
@ -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<Integer, byte[]> 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);
|
||||
}
|
||||
}
|
||||
24
src/test/java/test/it/addBlockUtils/JsonMini.java
Normal file
24
src/test/java/test/it/addBlockUtils/JsonMini.java
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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";
|
||||
|
||||
Loading…
Reference in New Issue
Block a user