SHiNE-server/src/test/java/test/it/blockchain/AddBlockSender.java
AidarKC 580695b486 23 01 25
Сделал ещё более два поля в общем формате блоков блокчейна (перед самим блоком данных) и перед его цп

(все тесты проходят)
2026-01-23 17:49:13 +03:00

241 lines
9.1 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package test.it.blockchain;
import blockchain.BchBlockEntry;
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.time.Duration;
import java.util.Base64;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
/**
* AddBlockSender — под новый формат BchBlockEntry (Frame v0):
* - blockBytes = preimage + sigMarker(2) + signature64
* - preimage начинается с frameCode(2) = 0x0000
* - hash32 вычисляется как sha256(preimage)
* - signature = Ed25519.sign(hash32)
*
* ВАЖНО:
* - Линии по ТЗ ведём на стороне сервера/БД (триггеры), а в тестах считаем локально.
*/
public final class AddBlockSender {
private static final String ZERO64 = "0".repeat(64);
private final WsSession ws;
private final ChainState state;
private final String login;
private final String blockchainName;
private final byte[] loginPrivKey;
public AddBlockSender(WsSession ws, ChainState state, String login, String blockchainName, byte[] loginPrivKey) {
this.ws = ws;
this.state = state;
this.login = login;
this.blockchainName = blockchainName;
this.loginPrivKey = (loginPrivKey == null ? null : loginPrivKey.clone());
if (this.ws == null) throw new IllegalArgumentException("ws == null");
if (this.state == null) throw new IllegalArgumentException("state == null");
if (this.loginPrivKey == null) throw new IllegalArgumentException("loginPrivKey == null");
}
public ChainState state() { return state; }
public void send(BodyRecord body, Duration timeout) {
if (body == null) throw new IllegalArgumentException("body == null");
body.check();
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("Нельзя слать блоки до HEADER (нет headerHash32)");
}
}
int blockNumber = state.nextBlockNumber();
byte[] prevHash32 = state.prevHash32ForNext();
long tsSec = System.currentTimeMillis() / 1000L;
short type = typeOf(body);
short subType = subTypeOf(body);
short version = versionOf(body);
byte[] bodyBytes = body.toBytes();
// ВАЖНО: preimage должен быть БАЙТ-В-БАЙТ таким же, как в BchBlockEntry
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(
prevHash32,
blockNumber,
tsSec,
type,
subType,
version,
bodyBytes,
signature64
);
String prevHashHexForReq = (blockNumber == 0) ? ZERO64 : state.lastBlockHashHex();
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 serverLastHash = JsonMini.extractPayloadString(resp, "serverLastBlockHash");
if (serverLastHash == null) {
serverLastHash = JsonMini.extractPayloadString(resp, "serverLastGlobalHash");
}
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 + ": serverLastBlockHash=" + serverLastHash);
}
assertEquals(localHashHex, serverLastHash, op + ": serverLastBlockHash must match local hash");
state.applyAppendedBlock(blockNumber, entry.getHash32(), isHeader, type, body);
if (TestConfig.DEBUG()) TestLog.info(op + ": state updated");
}
// ---------- request JSON ----------
private static String buildAddBlockJson(String blockchainName, int blockNumber, String prevBlockHashHex, String blockBytesB64) {
String requestId = TestIds.next("addblock");
return """
{
"op": "AddBlock",
"requestId": "%s",
"payload": {
"blockchainName": "%s",
"blockNumber": %d,
"prevBlockHash": "%s",
"blockBytesB64": "%s"
}
}
""".formatted(requestId, blockchainName, blockNumber, prevBlockHashHex, blockBytesB64);
}
private static void assert200(String op, String resp) {
int st = JsonParsers.status(resp);
assertEquals(200, st, op + ": expected status=200, but got=" + st + ", resp=" + resp);
TestLog.ok(op + ": status=200");
}
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);
}
// ---------- header extraction from body ----------
private static short typeOf(BodyRecord body) {
if (body instanceof HeaderBody) return HeaderBody.TYPE;
if (body instanceof CreateChannelBody) return CreateChannelBody.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 CreateChannelBody cb) return cb.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 CreateChannelBody cb) return cb.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 Frame v0) ----------
private static byte[] buildPreimage(byte[] prevHash32,
int blockNumber,
long tsSec,
short type,
short subType,
short version,
byte[] bodyBytes) {
if (prevHash32 == null || prevHash32.length != 32) {
throw new IllegalArgumentException("prevHash32 must be 32 bytes");
}
int bodyLen = (bodyBytes == null ? 0 : bodyBytes.length);
int blockSize = BchBlockEntry.PREIMAGE_HEADER_SIZE + bodyLen;
java.nio.ByteBuffer bb = java.nio.ByteBuffer.allocate(blockSize).order(java.nio.ByteOrder.BIG_ENDIAN);
// [2] frameCode (v0)
bb.putShort((short) (BchBlockEntry.FRAME_CODE_V0 & 0xFFFF));
// [32] prevHash32
bb.put(prevHash32);
// [4] blockSize (preimage size)
bb.putInt(blockSize);
// [4] blockNumber
bb.putInt(blockNumber);
// [8] timestamp
bb.putLong(tsSec);
// [2][2][2] type/subType/version
bb.putShort(type);
bb.putShort(subType);
bb.putShort(version);
// [N] bodyBytes
if (bodyBytes != null) bb.put(bodyBytes);
return bb.array();
}
}