SHA256
13 01 25
Переписал код кучи классов перешёл на новый надеюсь теперь подходящий формат блоков и тесты переделал. Но пока остались баги и тесты не проходят (в частности пользователи не создаются - ошибка в бд)
This commit is contained in:
@@ -1,16 +1,13 @@
|
||||
package test.it.blockchain;
|
||||
|
||||
import blockchain.BchBlockEntry;
|
||||
import blockchain.BchCryptoVerifier;
|
||||
import blockchain.body.BodyRecord;
|
||||
import test.it.utils.json.JsonParsers;
|
||||
import blockchain.body.*;
|
||||
import test.it.utils.TestConfig;
|
||||
import test.it.utils.TestIds;
|
||||
import test.it.utils.json.JsonParsers;
|
||||
import test.it.utils.log.TestLog;
|
||||
import test.it.utils.ws.WsSession;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.time.Duration;
|
||||
import java.util.Base64;
|
||||
|
||||
@@ -18,16 +15,13 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
/**
|
||||
* AddBlockSender — отправка AddBlock поверх одного WsSession:
|
||||
* - берёт номера/prev-hash из ChainState
|
||||
* - строит raw/hash/signature
|
||||
* - отправляет AddBlock
|
||||
* - проверяет serverLastGlobalHash == localHash
|
||||
* - обновляет ChainState
|
||||
* AddBlockSender — под новый формат BchBlockEntry:
|
||||
* - block хранит только preimage + signature
|
||||
* - hash32 вычисляется как sha256(preimage)
|
||||
* - signature = Ed25519.sign(hash32)
|
||||
*/
|
||||
public final class AddBlockSender {
|
||||
|
||||
private static final byte[] ZERO32 = new byte[32];
|
||||
private static final String ZERO64 = "0".repeat(64);
|
||||
|
||||
private final WsSession ws;
|
||||
@@ -52,69 +46,89 @@ public final class AddBlockSender {
|
||||
public void send(BodyRecord body, Duration timeout) {
|
||||
if (body == null) throw new IllegalArgumentException("body == null");
|
||||
|
||||
short lineIndex = body.expectedLineIndex();
|
||||
body.check();
|
||||
|
||||
if (lineIndex == 0) {
|
||||
if (state.globalLastNumber() != -1) throw new IllegalStateException("HEADER должен быть первым: globalLastNumber уже " + state.globalLastNumber());
|
||||
boolean isHeader = (body instanceof HeaderBody);
|
||||
|
||||
if (isHeader) {
|
||||
if (state.lastBlockNumber() != -1) {
|
||||
throw new IllegalStateException("HEADER должен быть первым: lastBlockNumber уже " + state.lastBlockNumber());
|
||||
}
|
||||
} else {
|
||||
if (!state.hasHeader()) throw new IllegalStateException("Нельзя слать line=" + lineIndex + " до HEADER (нет headerHash32)");
|
||||
if (!state.hasHeader()) {
|
||||
throw new IllegalStateException("Нельзя слать блоки до HEADER (нет headerHash32)");
|
||||
}
|
||||
}
|
||||
|
||||
int globalNumber = state.nextGlobalNumber();
|
||||
int lineNumber = state.nextLineNumber(lineIndex);
|
||||
int blockNumber = state.nextBlockNumber();
|
||||
byte[] prevHash32 = state.prevHash32ForNext();
|
||||
long tsSec = System.currentTimeMillis() / 1000L;
|
||||
|
||||
byte[] prevGlobalHash32 = (lineIndex == 0) ? ZERO32 : state.prevGlobalHash32ForNext(lineIndex);
|
||||
byte[] prevLineHash32 = (lineIndex == 0) ? ZERO32 : state.prevLineHash32ForNext(lineIndex);
|
||||
short type = typeOf(body);
|
||||
short subType = subTypeOf(body);
|
||||
short version = versionOf(body);
|
||||
|
||||
long ts = System.currentTimeMillis() / 1000L;
|
||||
byte[] bodyBytes = body.toBytes();
|
||||
|
||||
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();
|
||||
|
||||
byte[] preimage = BchCryptoVerifier.buildPreimage(login, prevGlobalHash32, prevLineHash32, rawBytes);
|
||||
byte[] hash32 = BchCryptoVerifier.sha256(preimage);
|
||||
// preimage -> hash32 -> signature
|
||||
byte[] preimage = buildPreimage(prevHash32, blockNumber, tsSec, type, subType, version, bodyBytes);
|
||||
byte[] hash32 = blockchain.BchCryptoVerifier.sha256(preimage);
|
||||
byte[] signature64 = utils.crypto.Ed25519Util.sign(hash32, loginPrivKey);
|
||||
|
||||
BchBlockEntry entry = new BchBlockEntry(globalNumber, ts, lineIndex, lineNumber, bodyBytes, signature64, hash32);
|
||||
BchBlockEntry entry = new BchBlockEntry(
|
||||
prevHash32,
|
||||
blockNumber,
|
||||
tsSec,
|
||||
type,
|
||||
subType,
|
||||
version,
|
||||
bodyBytes,
|
||||
signature64
|
||||
);
|
||||
|
||||
String prevGlobalHashHex = (globalNumber == 0) ? ZERO64 : state.globalLastHashHex();
|
||||
String prevHashHexForReq = (blockNumber == 0) ? ZERO64 : state.lastBlockHashHex();
|
||||
|
||||
String reqJson = buildAddBlockJson(blockchainName, globalNumber, prevGlobalHashHex, base64(entry.toBytes()));
|
||||
String op = "AddBlock(user=" + login + ", global=" + globalNumber + ", line=" + lineIndex + ", lineNum=" + lineNumber + ")";
|
||||
String reqJson = buildAddBlockJson(blockchainName, blockNumber, prevHashHexForReq, base64(entry.toBytes()));
|
||||
String op = "AddBlock(user=" + login + ", block=" + blockNumber + ", type=" + (type & 0xFFFF) + ", sub=" + (subType & 0xFFFF) + ")";
|
||||
|
||||
String resp = ws.call(op, reqJson, timeout);
|
||||
|
||||
assert200(op, resp);
|
||||
|
||||
String serverLastGlobalHash = JsonMini.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 serverLastHash = JsonMini.extractPayloadString(resp, "serverLastBlockHash");
|
||||
if (serverLastHash == null) {
|
||||
// на случай старого имени, но по твоей просьбе мы на это больше не опираемся
|
||||
serverLastHash = JsonMini.extractPayloadString(resp, "serverLastGlobalHash");
|
||||
}
|
||||
|
||||
String localHashHex = bytesToHex64(hash32);
|
||||
assertNotNull(serverLastHash, op + ": payload.serverLastBlockHash must not be null");
|
||||
assertEquals(64, serverLastHash.trim().length(), op + ": serverLastBlockHash must be 64 hex chars");
|
||||
|
||||
String localHashHex = bytesToHex64(entry.getHash32());
|
||||
|
||||
if (TestConfig.DEBUG()) {
|
||||
TestLog.info(op + ": localHash=" + localHashHex);
|
||||
TestLog.info(op + ": serverLastGlobalHash=" + serverLastGlobalHash);
|
||||
TestLog.info(op + ": serverLastBlockHash=" + serverLastHash);
|
||||
}
|
||||
|
||||
assertEquals(localHashHex, serverLastGlobalHash, op + ": serverLastGlobalHash must match local hash");
|
||||
assertEquals(localHashHex, serverLastHash, op + ": serverLastBlockHash must match local hash");
|
||||
|
||||
state.applyAppendedBlock(globalNumber, lineIndex, lineNumber, hash32);
|
||||
state.applyAppendedBlock(blockNumber, entry.getHash32(), isHeader, type);
|
||||
|
||||
// если это line-body — обновим thisLineNumber в state (для nextLine())
|
||||
if (body instanceof BodyHasLine hl) {
|
||||
short lineIndex = lineIndexByType(type);
|
||||
if (lineIndex != -1) {
|
||||
state.applyThisLineNumber(lineIndex, hl.thisLineNumber());
|
||||
}
|
||||
}
|
||||
|
||||
if (TestConfig.DEBUG()) TestLog.info(op + ": state updated");
|
||||
}
|
||||
|
||||
private static String buildAddBlockJson(String blockchainName, int globalNumber, String prevGlobalHashHex, String blockBytesB64) {
|
||||
// ---------- request JSON ----------
|
||||
|
||||
private static String buildAddBlockJson(String blockchainName, int blockNumber, String prevBlockHashHex, String blockBytesB64) {
|
||||
String requestId = TestIds.next("addblock");
|
||||
return """
|
||||
{
|
||||
@@ -122,12 +136,12 @@ public final class AddBlockSender {
|
||||
"requestId": "%s",
|
||||
"payload": {
|
||||
"blockchainName": "%s",
|
||||
"globalNumber": %d,
|
||||
"prevGlobalHash": "%s",
|
||||
"blockNumber": %d,
|
||||
"prevBlockHash": "%s",
|
||||
"blockBytesB64": "%s"
|
||||
}
|
||||
}
|
||||
""".formatted(requestId, blockchainName, globalNumber, prevGlobalHashHex, blockBytesB64);
|
||||
""".formatted(requestId, blockchainName, blockNumber, prevBlockHashHex, blockBytesB64);
|
||||
}
|
||||
|
||||
private static void assert200(String op, String resp) {
|
||||
@@ -150,4 +164,70 @@ public final class AddBlockSender {
|
||||
}
|
||||
return new String(out);
|
||||
}
|
||||
|
||||
// ---------- header extraction from body ----------
|
||||
|
||||
private static short typeOf(BodyRecord body) {
|
||||
if (body instanceof HeaderBody) return HeaderBody.TYPE;
|
||||
if (body instanceof TextBody) return TextBody.TYPE;
|
||||
if (body instanceof ReactionBody) return ReactionBody.TYPE;
|
||||
if (body instanceof ConnectionBody) return ConnectionBody.TYPE;
|
||||
if (body instanceof UserParamBody) return UserParamBody.TYPE;
|
||||
throw new IllegalArgumentException("Unknown body class: " + body.getClass());
|
||||
}
|
||||
|
||||
private static short subTypeOf(BodyRecord body) {
|
||||
if (body instanceof HeaderBody hb) return hb.subType;
|
||||
if (body instanceof TextBody tb) return tb.subType;
|
||||
if (body instanceof ReactionBody rb) return rb.subType;
|
||||
if (body instanceof ConnectionBody cb) return cb.subType;
|
||||
if (body instanceof UserParamBody ub) return ub.subType;
|
||||
throw new IllegalArgumentException("Unknown body class: " + body.getClass());
|
||||
}
|
||||
|
||||
private static short versionOf(BodyRecord body) {
|
||||
if (body instanceof HeaderBody hb) return hb.version;
|
||||
if (body instanceof TextBody tb) return tb.version;
|
||||
if (body instanceof ReactionBody rb) return rb.version;
|
||||
if (body instanceof ConnectionBody cb) return cb.version;
|
||||
if (body instanceof UserParamBody ub) return ub.version;
|
||||
throw new IllegalArgumentException("Unknown body class: " + body.getClass());
|
||||
}
|
||||
|
||||
// ---------- preimage builder (строго по BchBlockEntry) ----------
|
||||
|
||||
private static byte[] buildPreimage(byte[] prevHash32,
|
||||
int blockNumber,
|
||||
long tsSec,
|
||||
short type,
|
||||
short subType,
|
||||
short version,
|
||||
byte[] bodyBytes) {
|
||||
|
||||
int blockSize = BchBlockEntry.RAW_HEADER_SIZE + (bodyBytes == null ? 0 : bodyBytes.length);
|
||||
|
||||
java.nio.ByteBuffer bb = java.nio.ByteBuffer.allocate(blockSize).order(java.nio.ByteOrder.BIG_ENDIAN);
|
||||
|
||||
bb.put(prevHash32);
|
||||
bb.putInt(blockSize);
|
||||
bb.putInt(blockNumber);
|
||||
bb.putLong(tsSec);
|
||||
bb.putShort(type);
|
||||
bb.putShort(subType);
|
||||
bb.putShort(version);
|
||||
if (bodyBytes != null) bb.put(bodyBytes);
|
||||
|
||||
return bb.array();
|
||||
}
|
||||
|
||||
private static short lineIndexByType(short type) {
|
||||
int t = type & 0xFFFF;
|
||||
return switch (t) {
|
||||
case 0 -> blockchain.LineIndex.HEADER;
|
||||
case 1 -> blockchain.LineIndex.TEXT;
|
||||
case 3 -> blockchain.LineIndex.CONNECTION;
|
||||
case 4 -> blockchain.LineIndex.USER_PARAM;
|
||||
default -> (short) -1;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,25 @@
|
||||
package test.it.blockchain;
|
||||
|
||||
import blockchain.LineIndex;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* ChainState — только состояние цепочки (номера/хэши).
|
||||
* ChainState — состояние цепочки + состояние линий (только тех, где они нужны):
|
||||
*
|
||||
* Хранит:
|
||||
* - last globalNumber / last globalHash
|
||||
* - last lineNum / last lineHash по каждой линии
|
||||
* - hash32 нулевого блока (headerHash32) — нужен как prevLineHash для первого блока каждой линии
|
||||
* - map globalNumber -> hash32 (для ссылок reply/reaction на старые блоки)
|
||||
* Глобальная цепочка:
|
||||
* - lastBlockNumber / lastBlockHashHex
|
||||
* - map blockNumber -> hash32 (для ссылок reply/edit/reaction)
|
||||
*
|
||||
* Линии (по ТЗ нужны):
|
||||
* - TEXT (1)
|
||||
* - CONNECTION (3)
|
||||
* - USER_PARAM (4)
|
||||
*
|
||||
* prevLineNumber по ТЗ — это GLOBAL blockNumber предыдущего блока линии.
|
||||
* thisLineNumber — внутренний номер линии (мы ведём локально: 1,2,3...)
|
||||
*/
|
||||
public final class ChainState {
|
||||
|
||||
@@ -20,134 +28,157 @@ public final class ChainState {
|
||||
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;
|
||||
// global chain
|
||||
private int lastBlockNumber = -1;
|
||||
private String lastBlockHashHex = ZERO64;
|
||||
|
||||
// header (block#0)
|
||||
private byte[] headerHash32 = null;
|
||||
|
||||
// Для удобства тестов: чтобы можно было делать reply/like на любой уже отправленный globalNumber
|
||||
private final Map<Integer, byte[]> globalHash32ByNumber = new HashMap<>();
|
||||
// per-line state (только для LineIndex.TEXT/CONNECTION/USER_PARAM)
|
||||
private final int[] lineLastGlobalNumber = new int[LINES_MAX]; // последний GLOBAL номер блока в линии
|
||||
private final String[] lineLastHashHex = new String[LINES_MAX]; // hash последнего блока линии
|
||||
private final int[] lineLastThisLineNumber = new int[LINES_MAX]; // последний thisLineNumber (внутренний)
|
||||
|
||||
private final Map<Integer, byte[]> hash32ByNumber = new HashMap<>();
|
||||
|
||||
public ChainState() {
|
||||
Arrays.fill(lineLastGlobalNumber, -1);
|
||||
Arrays.fill(lineLastHashHex, "");
|
||||
// lineLastNumber по умолчанию = 0
|
||||
Arrays.fill(lineLastThisLineNumber, 0);
|
||||
}
|
||||
|
||||
// -------------------- getters --------------------
|
||||
// -------------------- global getters --------------------
|
||||
|
||||
public int globalLastNumber() { return globalLastNumber; }
|
||||
public String globalLastHashHex() { return globalLastHashHex; }
|
||||
public int lastBlockNumber() { return lastBlockNumber; }
|
||||
public String lastBlockHashHex() { return lastBlockHashHex; }
|
||||
|
||||
public int lineLastNumber(short line) { return lineLastNumber[line]; }
|
||||
public String lineLastHashHex(short line) { return lineLastHashHex[line]; }
|
||||
public boolean hasHeader() {
|
||||
return headerHash32 != null && headerHash32.length == 32 && lastBlockNumber >= 0;
|
||||
}
|
||||
|
||||
public byte[] headerHash32() { return headerHash32 == null ? null : headerHash32.clone(); }
|
||||
public int nextBlockNumber() {
|
||||
return lastBlockNumber + 1;
|
||||
}
|
||||
|
||||
public byte[] getGlobalHash32(int globalNumber) {
|
||||
byte[] h = globalHash32ByNumber.get(globalNumber);
|
||||
public byte[] prevHash32ForNext() {
|
||||
if (lastBlockNumber < 0) return ZERO32;
|
||||
return hexToBytes32(lastBlockHashHex);
|
||||
}
|
||||
|
||||
public byte[] headerHash32() {
|
||||
return headerHash32 == null ? null : headerHash32.clone();
|
||||
}
|
||||
|
||||
public byte[] getHash32(int blockNumber) {
|
||||
byte[] h = hash32ByNumber.get(blockNumber);
|
||||
return h == null ? null : h.clone();
|
||||
}
|
||||
|
||||
// -------------------- state helpers --------------------
|
||||
// -------------------- line helpers --------------------
|
||||
|
||||
public boolean hasHeader() {
|
||||
return headerHash32 != null && headerHash32.length == 32 && globalLastNumber >= 0;
|
||||
public static final class NextLine {
|
||||
public final int prevLineNumber; // GLOBAL blockNumber
|
||||
public final byte[] prevLineHash32; // 32 bytes
|
||||
public final int thisLineNumber; // внутр. номер линии
|
||||
|
||||
public NextLine(int prevLineNumber, byte[] prevLineHash32, int thisLineNumber) {
|
||||
this.prevLineNumber = prevLineNumber;
|
||||
this.prevLineHash32 = (prevLineHash32 == null ? null : prevLineHash32.clone());
|
||||
this.thisLineNumber = thisLineNumber;
|
||||
}
|
||||
}
|
||||
|
||||
/** Следующий globalNumber. */
|
||||
public int nextGlobalNumber() {
|
||||
return globalLastNumber + 1;
|
||||
}
|
||||
|
||||
/** Следующий lineNumber: для line>0 — last+1. Для line0 — всегда 0 (header). */
|
||||
public int nextLineNumber(short lineIndex) {
|
||||
/** Следующие line-поля для указанной линии (только TEXT/CONNECTION/USER_PARAM). */
|
||||
public NextLine nextLine(short lineIndex) {
|
||||
checkLine(lineIndex);
|
||||
if (lineIndex == 0) return 0;
|
||||
return lineLastNumber[lineIndex] + 1;
|
||||
}
|
||||
if (!isLineUsed(lineIndex)) {
|
||||
throw new IllegalArgumentException("Line " + lineIndex + " не используется для BodyHasLine по ТЗ");
|
||||
}
|
||||
if (!hasHeader()) {
|
||||
throw new IllegalStateException("Нельзя формировать line-поля до HEADER (нет headerHash32)");
|
||||
}
|
||||
|
||||
/** prevGlobalHash32: для header это ZERO32, иначе hash последнего глобального блока. */
|
||||
public byte[] prevGlobalHash32ForNext(short nextLineIndex) {
|
||||
// Для genesis/header prevGlobalHash = ZERO32
|
||||
if (globalLastNumber < 0) return ZERO32;
|
||||
return hexToBytes32(globalLastHashHex);
|
||||
}
|
||||
int lastGlobal = lineLastGlobalNumber[lineIndex];
|
||||
int lastThis = lineLastThisLineNumber[lineIndex];
|
||||
|
||||
/**
|
||||
* 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();
|
||||
if (lastGlobal == -1) {
|
||||
// первый блок линии ссылается на HEADER (block#0)
|
||||
return new NextLine(0, headerHash32.clone(), 1);
|
||||
}
|
||||
|
||||
String lastHex = lineLastHashHex[lineIndex];
|
||||
if (lastHex == null || lastHex.isBlank()) {
|
||||
throw new IllegalStateException("lineLastHashHex[" + lineIndex + "] is blank but lineLastNumber>0");
|
||||
throw new IllegalStateException("lineLastHashHex[" + lineIndex + "] пуст, но lastGlobal!=-1");
|
||||
}
|
||||
return hexToBytes32(lastHex);
|
||||
|
||||
return new NextLine(lastGlobal, hexToBytes32(lastHex), lastThis + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Применить факт успешного добавления блока:
|
||||
* - обновить global last
|
||||
* - обновить line last
|
||||
* - сохранить globalNumber->hash32
|
||||
* - если это header: сохранить headerHash32
|
||||
*/
|
||||
public void applyAppendedBlock(int globalNumber,
|
||||
short lineIndex,
|
||||
int lineNumber,
|
||||
byte[] hash32) {
|
||||
// -------------------- apply --------------------
|
||||
|
||||
public void applyAppendedBlock(int blockNumber, byte[] hash32, boolean isHeader, short type) {
|
||||
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);
|
||||
if (blockNumber != lastBlockNumber + 1) {
|
||||
throw new IllegalStateException("blockNumber sequence broken: expected=" + (lastBlockNumber + 1) + " got=" + blockNumber);
|
||||
}
|
||||
|
||||
checkLine(lineIndex);
|
||||
|
||||
if (lineIndex == 0) {
|
||||
if (globalNumber != 0 || lineNumber != 0) {
|
||||
throw new IllegalStateException("Header must be global=0 line=0 lineNum=0");
|
||||
}
|
||||
if (isHeader) {
|
||||
if (blockNumber != 0) throw new IllegalStateException("HEADER must be blockNumber=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);
|
||||
}
|
||||
if (blockNumber == 0) throw new IllegalStateException("Non-header block can't be blockNumber=0");
|
||||
if (headerHash32 == null) throw new IllegalStateException("Header must be sent before non-header blocks");
|
||||
}
|
||||
|
||||
String hex64 = bytesToHex64(hash32);
|
||||
|
||||
globalLastNumber = globalNumber;
|
||||
globalLastHashHex = hex64;
|
||||
lastBlockNumber = blockNumber;
|
||||
lastBlockHashHex = hex64;
|
||||
|
||||
lineLastNumber[lineIndex] = lineNumber;
|
||||
lineLastHashHex[lineIndex] = hex64;
|
||||
hash32ByNumber.put(blockNumber, hash32.clone());
|
||||
|
||||
globalHash32ByNumber.put(globalNumber, hash32.clone());
|
||||
// обновляем line-state только для линий, которые "надо" по ТЗ
|
||||
short lineIndex = lineIndexByType(type);
|
||||
if (lineIndex != -1 && isLineUsed(lineIndex)) {
|
||||
lineLastGlobalNumber[lineIndex] = blockNumber;
|
||||
lineLastHashHex[lineIndex] = hex64;
|
||||
|
||||
// thisLineNumber мы берём из тела, но здесь его нет.
|
||||
// Поэтому thisLineNumber должен обновляться там, где формируются тела (в тестах),
|
||||
// либо AddBlockSender может прокинуть его отдельно.
|
||||
// Чтобы не дублировать контракт — здесь оставляем как есть.
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------- utils --------------------
|
||||
/** В тестах удобно явно обновлять thisLineNumber после успешной отправки line-body. */
|
||||
public void applyThisLineNumber(short lineIndex, int thisLineNumber) {
|
||||
checkLine(lineIndex);
|
||||
if (!isLineUsed(lineIndex)) return;
|
||||
lineLastThisLineNumber[lineIndex] = thisLineNumber;
|
||||
}
|
||||
|
||||
// -------------------- mapping --------------------
|
||||
|
||||
/** По type блока определяем lineIndex. Reaction line по твоему ТЗ "не надо". */
|
||||
private static short lineIndexByType(short type) {
|
||||
int t = type & 0xFFFF;
|
||||
return switch (t) {
|
||||
case 0 -> LineIndex.HEADER;
|
||||
case 1 -> LineIndex.TEXT;
|
||||
case 3 -> LineIndex.CONNECTION;
|
||||
case 4 -> LineIndex.USER_PARAM;
|
||||
default -> (short) -1; // reaction/unknown => line state not used
|
||||
};
|
||||
}
|
||||
|
||||
private static boolean isLineUsed(short lineIndex) {
|
||||
return lineIndex == LineIndex.TEXT
|
||||
|| lineIndex == LineIndex.CONNECTION
|
||||
|| lineIndex == LineIndex.USER_PARAM;
|
||||
}
|
||||
|
||||
private static void checkLine(short lineIndex) {
|
||||
if (lineIndex < 0 || lineIndex >= LINES_MAX) {
|
||||
@@ -155,6 +186,8 @@ public final class ChainState {
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------- utils --------------------
|
||||
|
||||
private static byte[] hexToBytes32(String hex) {
|
||||
if (hex == null) throw new IllegalArgumentException("hex is null");
|
||||
String s = hex.trim();
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
package test.it.cases;
|
||||
|
||||
import blockchain.body.ConnectionBody;
|
||||
import blockchain.body.HeaderBody;
|
||||
import blockchain.body.ReactionBody;
|
||||
import blockchain.body.TextBody;
|
||||
import blockchain.body.UserParamBody;
|
||||
import blockchain.LineIndex;
|
||||
import blockchain.body.*;
|
||||
import shine.db.MsgSubType;
|
||||
import test.it.blockchain.AddBlockSender;
|
||||
import test.it.blockchain.ChainState;
|
||||
import test.it.utils.TestConfig;
|
||||
@@ -17,11 +15,7 @@ import java.time.Duration;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* IT_03_AddBlock_NoAuth
|
||||
*
|
||||
* ВАЖНО:
|
||||
* - пользователей НЕ создаём (их создаёт IT_01)
|
||||
* - ключи берём только из TestConfig по login
|
||||
* IT_03_AddBlock_NoAuth — обновлён под новый формат блоков (ТЗ).
|
||||
*/
|
||||
public class IT_03_AddBlock_NoAuth {
|
||||
|
||||
@@ -63,30 +57,88 @@ public class IT_03_AddBlock_NoAuth {
|
||||
sender1.send(new HeaderBody(u1), t);
|
||||
assertTrue(st1.hasHeader());
|
||||
|
||||
sender1.send(new TextBody(TextBody.SUB_NEW, "Hello #1 (NEW) from IT_03 test"), t);
|
||||
sender1.send(new TextBody(TextBody.SUB_NEW, "Hello #2 (NEW) from IT_03 test"), t);
|
||||
sender1.send(new TextBody(TextBody.SUB_NEW, "Hello #3 (NEW) from IT_03 test"), t);
|
||||
// TEXT_NEW x3 (с line)
|
||||
{
|
||||
var ln = st1.nextLine(LineIndex.TEXT);
|
||||
sender1.send(new TextBody(ln.prevLineNumber, ln.prevLineHash32, ln.thisLineNumber,
|
||||
MsgSubType.TEXT_NEW,
|
||||
"Hello #1 (NEW) from IT_03 test",
|
||||
null, null, null
|
||||
), t);
|
||||
}
|
||||
{
|
||||
var ln = st1.nextLine(LineIndex.TEXT);
|
||||
sender1.send(new TextBody(ln.prevLineNumber, ln.prevLineHash32, ln.thisLineNumber,
|
||||
MsgSubType.TEXT_NEW,
|
||||
"Hello #2 (NEW) from IT_03 test",
|
||||
null, null, null
|
||||
), t);
|
||||
}
|
||||
{
|
||||
var ln = st1.nextLine(LineIndex.TEXT);
|
||||
sender1.send(new TextBody(ln.prevLineNumber, ln.prevLineHash32, ln.thisLineNumber,
|
||||
MsgSubType.TEXT_NEW,
|
||||
"Hello #3 (NEW) from IT_03 test",
|
||||
null, null, null
|
||||
), t);
|
||||
}
|
||||
|
||||
byte[] text1Hash = st1.getGlobalHash32(1);
|
||||
byte[] text2Hash = st1.getGlobalHash32(2);
|
||||
byte[] text3Hash = st1.getGlobalHash32(3);
|
||||
byte[] text1Hash = st1.getHash32(1);
|
||||
byte[] text2Hash = st1.getHash32(2);
|
||||
byte[] text3Hash = st1.getHash32(3);
|
||||
assertNotNull(text1Hash);
|
||||
assertNotNull(text2Hash);
|
||||
assertNotNull(text3Hash);
|
||||
|
||||
sender1.send(new TextBody(TextBody.SUB_REPLY, "Reply to TEXT#1", bch1, 1, text1Hash), t);
|
||||
sender1.send(new TextBody(TextBody.SUB_REPLY, "Reply to TEXT#3", bch1, 3, text3Hash), t);
|
||||
// TEXT_REPLY x2 (с line + target)
|
||||
{
|
||||
var ln = st1.nextLine(LineIndex.TEXT);
|
||||
sender1.send(new TextBody(ln.prevLineNumber, ln.prevLineHash32, ln.thisLineNumber,
|
||||
MsgSubType.TEXT_REPLY,
|
||||
"Reply to TEXT#1",
|
||||
bch1, 1, text1Hash
|
||||
), t);
|
||||
}
|
||||
{
|
||||
var ln = st1.nextLine(LineIndex.TEXT);
|
||||
sender1.send(new TextBody(ln.prevLineNumber, ln.prevLineHash32, ln.thisLineNumber,
|
||||
MsgSubType.TEXT_REPLY,
|
||||
"Reply to TEXT#3",
|
||||
bch1, 3, text3Hash
|
||||
), t);
|
||||
}
|
||||
|
||||
sender1.send(new ReactionBody(ReactionBody.SUB_LIKE, bch1, 1, text1Hash), t);
|
||||
sender1.send(new ReactionBody(ReactionBody.SUB_LIKE, bch1, 2, text2Hash), t);
|
||||
// REACTION_LIKE x2 (без line)
|
||||
sender1.send(new ReactionBody(bch1, 1, text1Hash), t);
|
||||
sender1.send(new ReactionBody(bch1, 2, text2Hash), t);
|
||||
|
||||
sender1.send(new TextBody(TextBody.SUB_EDIT, "Hello #2 (EDIT#1) from IT_03 test", bch1, 2, text2Hash), t);
|
||||
sender1.send(new TextBody(TextBody.SUB_EDIT, "Hello #2 (EDIT#2) from IT_03 test", bch1, 2, text2Hash), t);
|
||||
sender1.send(new TextBody(TextBody.SUB_EDIT, "Hello #3 (EDIT#1) from IT_03 test", bch1, 3, text3Hash), t);
|
||||
// TEXT_EDIT x3 (с line + target)
|
||||
{
|
||||
var ln = st1.nextLine(LineIndex.TEXT);
|
||||
sender1.send(new TextBody(ln.prevLineNumber, ln.prevLineHash32, ln.thisLineNumber,
|
||||
MsgSubType.TEXT_EDIT,
|
||||
"Hello #2 (EDIT#1) from IT_03 test",
|
||||
bch1, 2, text2Hash
|
||||
), t);
|
||||
}
|
||||
{
|
||||
var ln = st1.nextLine(LineIndex.TEXT);
|
||||
sender1.send(new TextBody(ln.prevLineNumber, ln.prevLineHash32, ln.thisLineNumber,
|
||||
MsgSubType.TEXT_EDIT,
|
||||
"Hello #2 (EDIT#2) from IT_03 test",
|
||||
bch1, 2, text2Hash
|
||||
), t);
|
||||
}
|
||||
{
|
||||
var ln = st1.nextLine(LineIndex.TEXT);
|
||||
sender1.send(new TextBody(ln.prevLineNumber, ln.prevLineHash32, ln.thisLineNumber,
|
||||
MsgSubType.TEXT_EDIT,
|
||||
"Hello #3 (EDIT#1) from IT_03 test",
|
||||
bch1, 3, text3Hash
|
||||
), t);
|
||||
}
|
||||
|
||||
assertEquals(10, st1.globalLastNumber(), "USER1: globalLastNumber должен быть 10 (11 блоков)");
|
||||
assertEquals(8, st1.lineLastNumber((short) 1), "USER1: line=1 должно быть 8 TEXT блоков");
|
||||
assertEquals(2, st1.lineLastNumber((short) 2), "USER1: line=2 должно быть 2 REACTION блока");
|
||||
assertEquals(10, st1.lastBlockNumber(), "USER1: lastBlockNumber должен быть 10 (всего 11 блоков включая HEADER)");
|
||||
|
||||
// USER2
|
||||
ChainState st2 = new ChainState();
|
||||
@@ -95,7 +147,13 @@ public class IT_03_AddBlock_NoAuth {
|
||||
sender2.send(new HeaderBody(u2), t);
|
||||
assertTrue(st2.hasHeader());
|
||||
|
||||
sender2.send(new UserParamBody("Anya", "Amsterdam, Example street 10"), t);
|
||||
// USER_PARAM (с line)
|
||||
{
|
||||
var ln = st2.nextLine(LineIndex.USER_PARAM);
|
||||
sender2.send(new UserParamBody(ln.prevLineNumber, ln.prevLineHash32, ln.thisLineNumber,
|
||||
"Anya", "Amsterdam, Example street 10"
|
||||
), t);
|
||||
}
|
||||
|
||||
// USER3 (нужен, чтобы u1 мог подписаться на существующий блокчейн)
|
||||
ChainState st3 = new ChainState();
|
||||
@@ -105,27 +163,70 @@ public class IT_03_AddBlock_NoAuth {
|
||||
assertTrue(st3.hasHeader());
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// Подписки (как ты просил):
|
||||
// Подписки:
|
||||
// - u1 follows u2 и u3
|
||||
// - u2 follows только u1
|
||||
// Все CONNECTION идут по линии CONNECTION (по ТЗ "да надо")
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
// u1 -> follow u2
|
||||
sender1.send(new ConnectionBody(ConnectionBody.SUB_FOLLOW, u2, bch2, 0, new byte[32]), t);
|
||||
{
|
||||
var ln = st1.nextLine(LineIndex.CONNECTION);
|
||||
sender1.send(new ConnectionBody(ln.prevLineNumber, ln.prevLineHash32, ln.thisLineNumber,
|
||||
MsgSubType.CONNECTION_FOLLOW,
|
||||
u2, bch2, 0, new byte[32]
|
||||
), t);
|
||||
}
|
||||
|
||||
// u1 -> follow u3
|
||||
sender1.send(new ConnectionBody(ConnectionBody.SUB_FOLLOW, u3, bch3, 0, new byte[32]), t);
|
||||
{
|
||||
var ln = st1.nextLine(LineIndex.CONNECTION);
|
||||
sender1.send(new ConnectionBody(ln.prevLineNumber, ln.prevLineHash32, ln.thisLineNumber,
|
||||
MsgSubType.CONNECTION_FOLLOW,
|
||||
u3, bch3, 0, new byte[32]
|
||||
), t);
|
||||
}
|
||||
|
||||
// u2 -> follow u1
|
||||
sender2.send(new ConnectionBody(ConnectionBody.SUB_FOLLOW, u1, bch1, 0, new byte[32]), t);
|
||||
{
|
||||
var ln = st2.nextLine(LineIndex.CONNECTION);
|
||||
sender2.send(new ConnectionBody(ln.prevLineNumber, ln.prevLineHash32, ln.thisLineNumber,
|
||||
MsgSubType.CONNECTION_FOLLOW,
|
||||
u1, bch1, 0, new byte[32]
|
||||
), t);
|
||||
}
|
||||
|
||||
// (оставил твои friend/unfriend как было — но они уже не обязательны для подписок)
|
||||
sender2.send(new ConnectionBody(ConnectionBody.SUB_FRIEND, u1, bch1, 0, new byte[32]), t);
|
||||
// friend/unfriend как было, но тоже по CONNECTION линии
|
||||
{
|
||||
var ln = st2.nextLine(LineIndex.CONNECTION);
|
||||
sender2.send(new ConnectionBody(ln.prevLineNumber, ln.prevLineHash32, ln.thisLineNumber,
|
||||
MsgSubType.CONNECTION_FRIEND,
|
||||
u1, bch1, 0, new byte[32]
|
||||
), t);
|
||||
}
|
||||
|
||||
sender1.send(new UserParamBody("Anna", "Gareeva"), t);
|
||||
sender1.send(new ConnectionBody(ConnectionBody.SUB_FRIEND, u2, bch2, 0, new byte[32]), t);
|
||||
// user1 param + friend to u2
|
||||
{
|
||||
var ln = st1.nextLine(LineIndex.USER_PARAM);
|
||||
sender1.send(new UserParamBody(ln.prevLineNumber, ln.prevLineHash32, ln.thisLineNumber,
|
||||
"Anna", "Gareeva"
|
||||
), t);
|
||||
}
|
||||
{
|
||||
var ln = st1.nextLine(LineIndex.CONNECTION);
|
||||
sender1.send(new ConnectionBody(ln.prevLineNumber, ln.prevLineHash32, ln.thisLineNumber,
|
||||
MsgSubType.CONNECTION_FRIEND,
|
||||
u2, bch2, 0, new byte[32]
|
||||
), t);
|
||||
}
|
||||
|
||||
sender2.send(new ConnectionBody(ConnectionBody.SUB_UNFRIEND, u1, bch1, 0, new byte[32]), t);
|
||||
{
|
||||
var ln = st2.nextLine(LineIndex.CONNECTION);
|
||||
sender2.send(new ConnectionBody(ln.prevLineNumber, ln.prevLineHash32, ln.thisLineNumber,
|
||||
MsgSubType.CONNECTION_UNFRIEND,
|
||||
u1, bch1, 0, new byte[32]
|
||||
), t);
|
||||
}
|
||||
|
||||
r.ok("IT_03 сценарий блоков выполнен");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user