Сделал ещё более два поля в общем формате блоков блокчейна (перед самим блоком данных) и перед его цп (все тесты проходят)
241 lines
9.1 KiB
Java
241 lines
9.1 KiB
Java
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();
|
||
}
|
||
} |