13 01 25
Переписал код кучи классов перешёл на новый надеюсь теперь подходящий формат блоков и тесты переделал. Но пока остались баги и тесты не проходят (в частности пользователи не создаются - ошибка в бд)
This commit is contained in:
parent
b7025dde59
commit
e9e05c1192
@ -1,3 +1,6 @@
|
|||||||
|
// =======================
|
||||||
|
// blockchain/BchBlockEntry.java (НОВАЯ ВЕРСИЯ под ТЗ)
|
||||||
|
// =======================
|
||||||
package blockchain;
|
package blockchain;
|
||||||
|
|
||||||
import blockchain.body.BodyRecord;
|
import blockchain.body.BodyRecord;
|
||||||
@ -9,100 +12,66 @@ import java.time.Instant;
|
|||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
/**
|
|
||||||
* старый формат -его надо поменять на новый формат
|
|
||||||
*
|
|
||||||
* RAW (BigEndian):
|
|
||||||
* [4] recordSize (int) = размер RAW (включая этот заголовок), БЕЗ signature+hash
|
|
||||||
* [4] recordNumber (int) глобальный номер блока
|
|
||||||
* [8] timestamp (long) unix seconds
|
|
||||||
[2] lineIndex (short)
|
|
||||||
* [4] lineNumber (int)
|
|
||||||
* [N] bodyBytes (body, начинается с [type][version])
|
|
||||||
*
|
|
||||||
* TAIL (НЕ входит в recordSize):
|
|
||||||
* [64] signature64 (Ed25519)
|
|
||||||
* [32] hash32 (SHA-256)
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* BchBlockEntry — универсальный блок нового формата.
|
* BchBlockEntry — универсальный блок нового формата.
|
||||||
*
|
*
|
||||||
* RAW (BigEndian):
|
* RAW (BigEndian) = preimage:
|
||||||
* Неизменное заглавие
|
* [32] prevHash32 (SHA-256) hash предыдущего блока (цепочка)
|
||||||
* [32] prevHash32 (SHA-256) ХЭЩ ПРИВЕДУЩЕГО
|
* [4] blockSize (int) = размер preimage (в байтах), БЕЗ signature64
|
||||||
* [4] blockSize (int) = размер RAW (включая этот заголовок), БЕЗ signature
|
* [4] blockNumber (int) глобальный номер блока
|
||||||
* [4] blockNumber (int) номер блока
|
|
||||||
* [8] timestamp (long) unix seconds
|
* [8] timestamp (long) unix seconds
|
||||||
|
|
||||||
*
|
*
|
||||||
* [2] type - тип соощения
|
* [2] type (short) тип сообщения
|
||||||
* [2] Sиbtype - субтип сообщения
|
* [2] subType (short) подтип сообщения
|
||||||
* [2] version - версия формата соощения
|
* [2] version (short) версия формата сообщения
|
||||||
*
|
*
|
||||||
|
* [N] bodyBytes (bytes) тело сообщения (БЕЗ type/subType/version)
|
||||||
*
|
*
|
||||||
* Дальше Само сообщение (может быть разным)
|
* TAIL (НЕ входит в blockSize):
|
||||||
* [4] prevLineNumber НОМЕР ПРИВЕДУЩЕГО СООБЩЕНИЯ В ЛИНИИ - может быть а может и небыть в зависимости от типа сообщения
|
* [64] signature64 (Ed25519) подпись над hash32
|
||||||
* [32] prevLineHash ХЭШ ПРИВЕДУЩЕГО СООБЩЕНИЯ В ЛИНИИ - может быть а может и небыть в зависимости от типа сообщения
|
|
||||||
* [4] номер самого сообщения в этой линии
|
|
||||||
* [N] bodyBytes (ОСТАЛЬНЫЕ БАЙТЫ])
|
|
||||||
|
|
||||||
* TAIL (НЕ входит в recordSize):
|
|
||||||
* [64] signature64 (Ed25519)
|
|
||||||
* И хэш в конце блока мы не храним, тк он будет в начале следующего блока. А для проверки блока оно не нужно тк мы каждый раз провеяем подпись . А она основана на хэше
|
|
||||||
*
|
*
|
||||||
|
* hash32 ВНУТРИ БЛОКА НЕ ХРАНИМ.
|
||||||
|
* hash32 вычисляется при парсинге:
|
||||||
* [32] hash32 (SHA-256)
|
* preimage = первые blockSize байт
|
||||||
|
* hash32 = SHA-256(preimage)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public final class BchBlockEntry {
|
public final class BchBlockEntry {
|
||||||
|
|
||||||
public static final int SIGNATURE_LEN = 64;
|
public static final int SIGNATURE_LEN = 64;
|
||||||
public static final int HASH_LEN = 32;
|
public static final int HASH_LEN = 32;
|
||||||
|
|
||||||
/** Размер фиксированного RAW-заголовка без body */
|
/** Размер фиксированного RAW-заголовка без body */
|
||||||
public static final int RAW_HEADER_SIZE = 4 + 4 + 8 + 2 + 4;
|
public static final int RAW_HEADER_SIZE =
|
||||||
|
32 // prevHash32
|
||||||
|
+ 4 // blockSize
|
||||||
|
+ 4 // blockNumber
|
||||||
|
+ 8 // timestamp
|
||||||
|
+ 2 // type
|
||||||
|
+ 2 // subType
|
||||||
|
+ 2; // version
|
||||||
|
|
||||||
// --- RAW ---
|
// --- HEADER (RAW) ---
|
||||||
public final int recordSize; // только RAW, без signature+hash
|
public final byte[] prevHash32; // 32
|
||||||
public final int recordNumber;
|
public final int blockSize; // preimage size
|
||||||
|
public final int blockNumber;
|
||||||
public final long timestamp;
|
public final long timestamp;
|
||||||
public final short lineIndex;
|
public final short type;
|
||||||
public final int lineNumber;
|
public final short subType;
|
||||||
|
public final short version;
|
||||||
|
|
||||||
|
// --- BODY (RAW) ---
|
||||||
public final byte[] bodyBytes;
|
public final byte[] bodyBytes;
|
||||||
|
|
||||||
/** Распарсенное тело (создаётся сразу при парсинге блока). */
|
/** Распарсенное тело (создаётся сразу при парсинге блока). */
|
||||||
public final BodyRecord body;
|
public final BodyRecord body;
|
||||||
|
|
||||||
// --- TAIL ---
|
// --- TAIL ---
|
||||||
private final byte[] signature64;
|
private final byte[] signature64; // 64
|
||||||
private final byte[] hash32;
|
|
||||||
|
|
||||||
// --- cached ---
|
// --- derived ---
|
||||||
private final byte[] fullBytes;
|
private final byte[] hash32; // 32, computed
|
||||||
|
private final byte[] preimage; // blockSize bytes
|
||||||
|
private final byte[] fullBytes; // preimage + signature
|
||||||
|
|
||||||
/* ===================================================================== */
|
/* ===================================================================== */
|
||||||
/* ====================== Конструктор из байт ========================== */
|
/* ====================== Конструктор из байт ========================== */
|
||||||
@ -110,113 +79,113 @@ public final class BchBlockEntry {
|
|||||||
|
|
||||||
public BchBlockEntry(byte[] fullBytes) {
|
public BchBlockEntry(byte[] fullBytes) {
|
||||||
Objects.requireNonNull(fullBytes, "fullBytes == null");
|
Objects.requireNonNull(fullBytes, "fullBytes == null");
|
||||||
if (fullBytes.length < RAW_HEADER_SIZE + SIGNATURE_LEN + HASH_LEN)
|
|
||||||
|
if (fullBytes.length < RAW_HEADER_SIZE + SIGNATURE_LEN) {
|
||||||
throw new IllegalArgumentException("Block too short");
|
throw new IllegalArgumentException("Block too short");
|
||||||
|
}
|
||||||
|
|
||||||
ByteBuffer bb = ByteBuffer.wrap(fullBytes).order(ByteOrder.BIG_ENDIAN);
|
ByteBuffer bb = ByteBuffer.wrap(fullBytes).order(ByteOrder.BIG_ENDIAN);
|
||||||
|
|
||||||
this.recordSize = bb.getInt();
|
this.prevHash32 = new byte[32];
|
||||||
if (recordSize + SIGNATURE_LEN + HASH_LEN != fullBytes.length)
|
bb.get(this.prevHash32);
|
||||||
throw new IllegalArgumentException("recordSize mismatch");
|
|
||||||
|
|
||||||
this.recordNumber = bb.getInt();
|
this.blockSize = bb.getInt();
|
||||||
|
if (blockSize < RAW_HEADER_SIZE) {
|
||||||
|
throw new IllegalArgumentException("blockSize too small: " + blockSize);
|
||||||
|
}
|
||||||
|
if (blockSize + SIGNATURE_LEN != fullBytes.length) {
|
||||||
|
throw new IllegalArgumentException("blockSize mismatch: blockSize=" + blockSize + " fullLen=" + fullBytes.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.blockNumber = bb.getInt();
|
||||||
this.timestamp = bb.getLong();
|
this.timestamp = bb.getLong();
|
||||||
this.lineIndex = bb.getShort();
|
|
||||||
this.lineNumber = bb.getInt();
|
|
||||||
|
|
||||||
int bodyLen = recordSize - RAW_HEADER_SIZE;
|
this.type = bb.getShort();
|
||||||
if (bodyLen <= 0)
|
this.subType = bb.getShort();
|
||||||
throw new IllegalArgumentException("Invalid body length");
|
this.version = bb.getShort();
|
||||||
|
|
||||||
|
int bodyLen = blockSize - RAW_HEADER_SIZE;
|
||||||
|
if (bodyLen < 0) throw new IllegalArgumentException("Invalid body length: " + bodyLen);
|
||||||
|
|
||||||
this.bodyBytes = new byte[bodyLen];
|
this.bodyBytes = new byte[bodyLen];
|
||||||
bb.get(this.bodyBytes);
|
bb.get(this.bodyBytes);
|
||||||
|
|
||||||
// ✅ Сразу парсим BodyRecord (и если неизвестный type/version — тут же упадём)
|
|
||||||
this.body = BodyRecordParser.parse(this.bodyBytes);
|
|
||||||
|
|
||||||
// ✅ УРОВЕНЬ B: проверка ожидаемой линии по типу body
|
|
||||||
short expectedLine = this.body.expectedLineIndex();
|
|
||||||
if (this.lineIndex != expectedLine) {
|
|
||||||
throw new IllegalArgumentException(
|
|
||||||
"Body is in wrong lineIndex: expected=" + expectedLine + " actual=" + this.lineIndex +
|
|
||||||
" (type=" + this.body.type() + " ver=" + this.body.version() + ")"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.signature64 = new byte[SIGNATURE_LEN];
|
this.signature64 = new byte[SIGNATURE_LEN];
|
||||||
bb.get(this.signature64);
|
bb.get(this.signature64);
|
||||||
|
|
||||||
this.hash32 = new byte[HASH_LEN];
|
// preimage = первые blockSize байт
|
||||||
bb.get(this.hash32);
|
this.preimage = Arrays.copyOfRange(fullBytes, 0, blockSize);
|
||||||
|
|
||||||
|
// hash32 = sha256(preimage)
|
||||||
|
this.hash32 = BchCryptoVerifier.sha256(preimage);
|
||||||
|
|
||||||
|
// parse body по header.type/subType/version
|
||||||
|
this.body = BodyRecordParser.parse(this.type, this.subType, this.version, this.bodyBytes);
|
||||||
|
|
||||||
this.fullBytes = Arrays.copyOf(fullBytes, fullBytes.length);
|
this.fullBytes = Arrays.copyOf(fullBytes, fullBytes.length);
|
||||||
|
|
||||||
|
// запрет мусора
|
||||||
|
if (bb.remaining() != 0) {
|
||||||
|
throw new IllegalArgumentException("Unexpected tail bytes, remaining=" + bb.remaining());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===================================================================== */
|
/* ===================================================================== */
|
||||||
/* ====================== Конструктор сборки ============================ */
|
/* ====================== Конструктор сборки ============================ */
|
||||||
/* ===================================================================== */
|
/* ===================================================================== */
|
||||||
|
|
||||||
public BchBlockEntry(int recordNumber,
|
public BchBlockEntry(byte[] prevHash32,
|
||||||
|
int blockNumber,
|
||||||
long timestamp,
|
long timestamp,
|
||||||
short lineIndex,
|
short type,
|
||||||
int lineNumber,
|
short subType,
|
||||||
|
short version,
|
||||||
byte[] bodyBytes,
|
byte[] bodyBytes,
|
||||||
byte[] signature64,
|
byte[] signature64) {
|
||||||
byte[] hash32) {
|
|
||||||
|
|
||||||
|
Objects.requireNonNull(prevHash32, "prevHash32 == null");
|
||||||
Objects.requireNonNull(bodyBytes, "bodyBytes == null");
|
Objects.requireNonNull(bodyBytes, "bodyBytes == null");
|
||||||
Objects.requireNonNull(signature64, "signature64 == null");
|
Objects.requireNonNull(signature64, "signature64 == null");
|
||||||
Objects.requireNonNull(hash32, "hash32 == null");
|
|
||||||
|
|
||||||
if (signature64.length != SIGNATURE_LEN)
|
if (prevHash32.length != 32) throw new IllegalArgumentException("prevHash32 != 32");
|
||||||
throw new IllegalArgumentException("signature64 != 64");
|
if (signature64.length != SIGNATURE_LEN) throw new IllegalArgumentException("signature64 != 64");
|
||||||
if (hash32.length != HASH_LEN)
|
|
||||||
throw new IllegalArgumentException("hash32 != 32");
|
|
||||||
|
|
||||||
this.recordNumber = recordNumber;
|
this.prevHash32 = Arrays.copyOf(prevHash32, 32);
|
||||||
|
this.blockNumber = blockNumber;
|
||||||
this.timestamp = timestamp;
|
this.timestamp = timestamp;
|
||||||
this.lineIndex = lineIndex;
|
this.type = type;
|
||||||
this.lineNumber = lineNumber;
|
this.subType = subType;
|
||||||
|
this.version = version;
|
||||||
this.bodyBytes = Arrays.copyOf(bodyBytes, bodyBytes.length);
|
this.bodyBytes = Arrays.copyOf(bodyBytes, bodyBytes.length);
|
||||||
|
|
||||||
// ✅ И при сборке — тоже сразу парсим body (чтобы объект был цельным)
|
|
||||||
this.body = BodyRecordParser.parse(this.bodyBytes);
|
|
||||||
|
|
||||||
// ✅ УРОВЕНЬ B: проверка ожидаемой линии по типу body
|
|
||||||
short expectedLine = this.body.expectedLineIndex();
|
|
||||||
if (this.lineIndex != expectedLine) {
|
|
||||||
throw new IllegalArgumentException(
|
|
||||||
"Body is in wrong lineIndex: expected=" + expectedLine + " actual=" + this.lineIndex +
|
|
||||||
" (type=" + this.body.type() + " ver=" + this.body.version() + ")"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.signature64 = Arrays.copyOf(signature64, SIGNATURE_LEN);
|
this.signature64 = Arrays.copyOf(signature64, SIGNATURE_LEN);
|
||||||
this.hash32 = Arrays.copyOf(hash32, HASH_LEN);
|
|
||||||
|
|
||||||
// recordSize теперь только RAW (header + body), без signature+hash
|
this.blockSize = RAW_HEADER_SIZE + this.bodyBytes.length;
|
||||||
this.recordSize = RAW_HEADER_SIZE + bodyBytes.length;
|
|
||||||
|
|
||||||
int fullLen = this.recordSize + SIGNATURE_LEN + HASH_LEN;
|
// parse body по header
|
||||||
|
this.body = BodyRecordParser.parse(this.type, this.subType, this.version, this.bodyBytes);
|
||||||
|
|
||||||
ByteBuffer bb = ByteBuffer.allocate(fullLen).order(ByteOrder.BIG_ENDIAN);
|
// build preimage
|
||||||
bb.putInt(this.recordSize);
|
ByteBuffer pre = ByteBuffer.allocate(blockSize).order(ByteOrder.BIG_ENDIAN);
|
||||||
bb.putInt(recordNumber);
|
pre.put(this.prevHash32);
|
||||||
bb.putLong(timestamp);
|
pre.putInt(this.blockSize);
|
||||||
bb.putShort(lineIndex);
|
pre.putInt(this.blockNumber);
|
||||||
bb.putInt(lineNumber);
|
pre.putLong(this.timestamp);
|
||||||
bb.put(bodyBytes);
|
pre.putShort(this.type);
|
||||||
bb.put(this.signature64);
|
pre.putShort(this.subType);
|
||||||
bb.put(this.hash32);
|
pre.putShort(this.version);
|
||||||
|
pre.put(this.bodyBytes);
|
||||||
|
|
||||||
this.fullBytes = bb.array();
|
this.preimage = pre.array();
|
||||||
|
this.hash32 = BchCryptoVerifier.sha256(preimage);
|
||||||
|
|
||||||
|
ByteBuffer full = ByteBuffer.allocate(blockSize + SIGNATURE_LEN).order(ByteOrder.BIG_ENDIAN);
|
||||||
|
full.put(this.preimage);
|
||||||
|
full.put(this.signature64);
|
||||||
|
this.fullBytes = full.array();
|
||||||
}
|
}
|
||||||
|
|
||||||
public byte[] getRawBytes() {
|
public byte[] getPreimageBytes() {
|
||||||
int rawLen = recordSize; // ровно RAW, без signature+hash
|
return Arrays.copyOf(preimage, preimage.length);
|
||||||
byte[] raw = new byte[rawLen];
|
|
||||||
System.arraycopy(fullBytes, 0, raw, 0, rawLen);
|
|
||||||
return raw;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public byte[] getSignature64() {
|
public byte[] getSignature64() {
|
||||||
@ -241,26 +210,18 @@ public final class BchBlockEntry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return "BchBlockEntry{"
|
return "BchBlockEntry{"
|
||||||
+ "RAW{"
|
+ "HDR{"
|
||||||
+ "recordSize=" + recordSize
|
+ "blockSize=" + blockSize
|
||||||
+ ", recordNumber=" + recordNumber
|
+ ", blockNumber=" + blockNumber
|
||||||
+ ", timestamp=" + timestamp + " (" + timeIso + ")"
|
+ ", timestamp=" + timestamp + " (" + timeIso + ")"
|
||||||
+ ", lineIndex=" + lineIndex
|
+ ", type=" + (type & 0xFFFF)
|
||||||
+ ", lineNumber=" + lineNumber
|
+ ", subType=" + (subType & 0xFFFF)
|
||||||
+ ", bodyLen=" + (bodyBytes == null ? -1 : bodyBytes.length)
|
+ ", version=" + (version & 0xFFFF)
|
||||||
+ ", bodyType=" + (body == null ? "?" : (body.type() & 0xFFFF))
|
+ ", prevHash32(hex)=" + toHex(prevHash32)
|
||||||
+ ", bodyVer=" + (body == null ? "?" : (body.version() & 0xFFFF))
|
|
||||||
+ "}"
|
+ "}"
|
||||||
+ ", TAIL{"
|
+ ", BODY{len=" + (bodyBytes == null ? -1 : bodyBytes.length) + "}"
|
||||||
+ "signature64(hex)=" + toHex(signature64)
|
+ ", TAIL{signature64(hex)=" + toHex(signature64) + "}"
|
||||||
+ ", hash32(hex)=" + toHex(hash32)
|
+ ", DERIVED{hash32(hex)=" + toHex(hash32) + "}"
|
||||||
+ "}"
|
|
||||||
+ ", FULL{"
|
|
||||||
+ "fullLen=" + (fullBytes == null ? -1 : fullBytes.length)
|
|
||||||
+ ", rawLen=" + recordSize
|
|
||||||
+ "}"
|
|
||||||
+ ", body=" + (body == null ? "null" : body.toString())
|
|
||||||
+ ", bodyBytesPreview(hex32)=" + toHexPreview(bodyBytes, 32)
|
|
||||||
+ "}";
|
+ "}";
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -275,14 +236,4 @@ public final class BchBlockEntry {
|
|||||||
}
|
}
|
||||||
return new String(out);
|
return new String(out);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String toHexPreview(byte[] bytes, int maxBytes) {
|
|
||||||
if (bytes == null) return "null";
|
|
||||||
if (maxBytes <= 0) return "";
|
|
||||||
int n = Math.min(bytes.length, maxBytes);
|
|
||||||
byte[] cut = Arrays.copyOf(bytes, n);
|
|
||||||
String hex = toHex(cut);
|
|
||||||
if (bytes.length > n) hex += "…(+" + (bytes.length - n) + " байт)";
|
|
||||||
return hex;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -1,66 +1,26 @@
|
|||||||
|
// =======================
|
||||||
|
// blockchain/BchCryptoVerifier.java (НОВАЯ ВЕРСИЯ под ТЗ)
|
||||||
|
// =======================
|
||||||
package blockchain;
|
package blockchain;
|
||||||
|
|
||||||
import utils.config.ShineSignatureConstants;
|
|
||||||
import utils.crypto.Ed25519Util;
|
import utils.crypto.Ed25519Util;
|
||||||
|
|
||||||
import java.nio.ByteBuffer;
|
|
||||||
import java.nio.ByteOrder;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Новый верификатор по ТЗ:
|
||||||
|
*
|
||||||
|
* preimage = все байты блока без signature64
|
||||||
|
* hash32 = SHA-256(preimage)
|
||||||
|
* verify = Ed25519.verify(hash32, signature64, pubKey32)
|
||||||
|
*/
|
||||||
public final class BchCryptoVerifier {
|
public final class BchCryptoVerifier {
|
||||||
|
|
||||||
private BchCryptoVerifier() {}
|
private BchCryptoVerifier() {}
|
||||||
|
|
||||||
// ✅ строка — из констант; байты/длина — локально, “на месте”
|
|
||||||
private static final String DOMAIN_STR = ShineSignatureConstants.BLOCK_HASH_DOMAIN;
|
|
||||||
private static final byte[] DOMAIN = DOMAIN_STR.getBytes(StandardCharsets.US_ASCII);
|
|
||||||
private static final int DOMAIN_LEN = DOMAIN.length;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* preimage =
|
|
||||||
* DOMAIN +
|
|
||||||
* [1] loginLen + loginBytes +
|
|
||||||
* prevGlobalHash32 +
|
|
||||||
* prevLineHash32 +
|
|
||||||
* rawBytes
|
|
||||||
*/
|
|
||||||
public static byte[] buildPreimage(String userLogin,
|
|
||||||
byte[] prevGlobalHash32,
|
|
||||||
byte[] prevLineHash32,
|
|
||||||
byte[] rawBytes) {
|
|
||||||
|
|
||||||
Objects.requireNonNull(userLogin, "userLogin == null");
|
|
||||||
Objects.requireNonNull(prevGlobalHash32, "prevGlobalHash32 == null");
|
|
||||||
Objects.requireNonNull(prevLineHash32, "prevLineHash32 == null");
|
|
||||||
Objects.requireNonNull(rawBytes, "rawBytes == null");
|
|
||||||
|
|
||||||
if (prevGlobalHash32.length != 32 || prevLineHash32.length != 32)
|
|
||||||
throw new IllegalArgumentException("hash len != 32");
|
|
||||||
|
|
||||||
byte[] loginBytes = userLogin.getBytes(StandardCharsets.UTF_8);
|
|
||||||
if (loginBytes.length > 255)
|
|
||||||
throw new IllegalArgumentException("login >255 bytes");
|
|
||||||
|
|
||||||
ByteBuffer bb = ByteBuffer.allocate(
|
|
||||||
DOMAIN_LEN +
|
|
||||||
1 + loginBytes.length +
|
|
||||||
32 + 32 +
|
|
||||||
rawBytes.length
|
|
||||||
).order(ByteOrder.BIG_ENDIAN);
|
|
||||||
|
|
||||||
bb.put(DOMAIN);
|
|
||||||
bb.put((byte) loginBytes.length);
|
|
||||||
bb.put(loginBytes);
|
|
||||||
bb.put(prevGlobalHash32);
|
|
||||||
bb.put(prevLineHash32);
|
|
||||||
bb.put(rawBytes);
|
|
||||||
|
|
||||||
return bb.array();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static byte[] sha256(byte[] data) {
|
public static byte[] sha256(byte[] data) {
|
||||||
|
Objects.requireNonNull(data, "data == null");
|
||||||
try {
|
try {
|
||||||
MessageDigest d = MessageDigest.getInstance("SHA-256");
|
MessageDigest d = MessageDigest.getInstance("SHA-256");
|
||||||
return d.digest(data);
|
return d.digest(data);
|
||||||
@ -69,34 +29,15 @@ public final class BchCryptoVerifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public static boolean verifyBlock(BchBlockEntry block, byte[] publicKey32) {
|
||||||
* Проверка подписи Ed25519:
|
Objects.requireNonNull(block, "block == null");
|
||||||
*/
|
|
||||||
public static boolean verifyAll(String userLogin,
|
|
||||||
byte[] prevGlobalHash32,
|
|
||||||
byte[] prevLineHash32,
|
|
||||||
byte[] rawBytes,
|
|
||||||
byte[] signature64,
|
|
||||||
byte[] publicKey32,
|
|
||||||
byte[] expectedHash32FromBlock) {
|
|
||||||
|
|
||||||
Objects.requireNonNull(signature64, "signature64 == null");
|
|
||||||
Objects.requireNonNull(publicKey32, "publicKey32 == null");
|
Objects.requireNonNull(publicKey32, "publicKey32 == null");
|
||||||
Objects.requireNonNull(expectedHash32FromBlock, "expectedHash32FromBlock == null");
|
|
||||||
|
|
||||||
if (signature64.length != 64) throw new IllegalArgumentException("signature64 != 64");
|
|
||||||
if (publicKey32.length != 32) throw new IllegalArgumentException("publicKey32 != 32");
|
if (publicKey32.length != 32) throw new IllegalArgumentException("publicKey32 != 32");
|
||||||
if (expectedHash32FromBlock.length != 32) throw new IllegalArgumentException("hash32 != 32");
|
|
||||||
|
|
||||||
byte[] preimage = buildPreimage(userLogin, prevGlobalHash32, prevLineHash32, rawBytes);
|
byte[] hash32 = block.getHash32();
|
||||||
byte[] hash32 = sha256(preimage);
|
byte[] sig64 = block.getSignature64();
|
||||||
|
|
||||||
// 1) сверяем hash, который лежит в блоке
|
return Ed25519Util.verify(hash32, sig64, publicKey32);
|
||||||
if (!java.util.Arrays.equals(hash32, expectedHash32FromBlock)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2) проверяем подпись (Ed25519 над hash32)
|
|
||||||
return Ed25519Util.verify(hash32, signature64, publicKey32);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,7 +1,10 @@
|
|||||||
|
// =======================
|
||||||
|
// blockchain/body/BodyHasTarget.java (без изменений, оставляю как есть)
|
||||||
|
// =======================
|
||||||
package blockchain.body;
|
package blockchain.body;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* BodyToFields — дополнительный интерфейс для body, которые "ссылаются" на цель (to-поля).
|
* BodyHasTarget — дополнительный интерфейс для body, которые "ссылаются" на цель (to-поля).
|
||||||
*
|
*
|
||||||
* Идея:
|
* Идея:
|
||||||
* - Не все body имеют "to".
|
* - Не все body имеют "to".
|
||||||
@ -10,11 +13,6 @@ package blockchain.body;
|
|||||||
*
|
*
|
||||||
* Важно:
|
* Важно:
|
||||||
* - Все методы могут возвращать null.
|
* - Все методы могут возвращать null.
|
||||||
* - toLogin может отсутствовать в самом формате body (например, ReactionBody, TextBody reply/repost),
|
|
||||||
* но в БД мы пишем toLogin "про запас".
|
|
||||||
* Поэтому writer может:
|
|
||||||
* - взять toLogin из body (если есть),
|
|
||||||
* - либо попытаться вычислить из toBchName.
|
|
||||||
*/
|
*/
|
||||||
public interface BodyHasTarget {
|
public interface BodyHasTarget {
|
||||||
|
|
||||||
|
|||||||
@ -1,56 +1,29 @@
|
|||||||
|
// =======================
|
||||||
|
// blockchain/body/BodyRecord.java (ИЗМЕНЁННЫЙ контракт под ТЗ)
|
||||||
|
// =======================
|
||||||
package blockchain.body;
|
package blockchain.body;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* BodyRecord_new — общий контракт для всех типов body (тела блока).
|
* BodyRecord — общий контракт для всех типов body (тела блока).
|
||||||
*
|
*
|
||||||
* Идея:
|
* ВАЖНО (новый формат):
|
||||||
* - На каждый тип body (Header, Text, Reaction, ...) — отдельный класс.
|
* - type/subType/version НЕ лежат в bodyBytes.
|
||||||
* - Десериализация из байтов делается КОНСТРУКТОРОМ:
|
* - type/subType/version читаются из заголовка блока (BchBlockEntry).
|
||||||
* new XxxBody_new(byte[] bodyBytes)
|
|
||||||
* (конструктор обязан распарсить байты или кинуть IllegalArgumentException).
|
|
||||||
*
|
*
|
||||||
* - Валидация делается методом check().
|
* Поэтому из интерфейса УБРАНЫ:
|
||||||
* check() должен:
|
* - type()
|
||||||
* - вернуть this, если всё корректно
|
* - subType()
|
||||||
* - кинуть IllegalArgumentException, если данные некорректны
|
* - version()
|
||||||
*
|
* - expectedLineIndex()
|
||||||
* - Сериализация обратно в байты делается методом toBytes().
|
|
||||||
*
|
|
||||||
* - type() и version() — это идентификаторы формата body.
|
|
||||||
* Они должны быть константами для класса (например TYPE=1, VERSION=1).
|
|
||||||
*
|
|
||||||
* ДОПОЛНЕНИЕ (ЛИНИИ):
|
|
||||||
* - Каждый тип body знает, в какой lineIndex он ДОЛЖЕН находиться.
|
|
||||||
* Это проверяется в валидаторе блока (уровень B).
|
|
||||||
*
|
|
||||||
* ДОПОЛНЕНИЕ (SUBTYPE):
|
|
||||||
* - У каждого body есть subType (uint16).
|
|
||||||
* - Для HeaderBody он всегда 0 (служебная совместимость).
|
|
||||||
* - Для TextBody это тип сообщения (NEW/REPLY/REPOST).
|
|
||||||
* - Для ReactionBody это тип реакции (LIKE и т.п.).
|
|
||||||
*/
|
*/
|
||||||
public interface BodyRecord {
|
public interface BodyRecord {
|
||||||
|
|
||||||
/** Код типа записи (совпадает с type в bodyBytes). */
|
|
||||||
short type();
|
|
||||||
|
|
||||||
/** Версия формата записи (совпадает с version в bodyBytes). */
|
|
||||||
short version();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Подтип записи (uint16).
|
|
||||||
*/
|
|
||||||
short subType();
|
|
||||||
|
|
||||||
/** Ожидаемый индекс линии для этого body. */
|
|
||||||
short expectedLineIndex();
|
|
||||||
|
|
||||||
/** Проверить корректность содержимого и вернуть этот объект (или кинуть исключение). */
|
/** Проверить корректность содержимого и вернуть этот объект (или кинуть исключение). */
|
||||||
BodyRecord check();
|
BodyRecord check();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Сериализовать тело записи в байты (ровно то, что кладётся в block.body).
|
* Сериализовать тело записи в байты (ровно то, что кладётся в block.bodyBytes).
|
||||||
* Важно: включает type/version/subType и весь payload.
|
* Важно: НЕ включает type/subType/version.
|
||||||
*/
|
*/
|
||||||
byte[] toBytes();
|
byte[] toBytes();
|
||||||
}
|
}
|
||||||
@ -1,31 +1,34 @@
|
|||||||
|
// =======================
|
||||||
|
// blockchain/body/BodyRecordParser.java (ИЗМЕНЁННЫЙ под новый формат)
|
||||||
|
// =======================
|
||||||
package blockchain.body;
|
package blockchain.body;
|
||||||
|
|
||||||
import java.nio.ByteBuffer;
|
/**
|
||||||
import java.nio.ByteOrder;
|
* Парсер body теперь выбирает класс по header: type/subType/version,
|
||||||
|
* потому что bodyBytes больше НЕ содержат type/subType/version.
|
||||||
|
*/
|
||||||
public final class BodyRecordParser {
|
public final class BodyRecordParser {
|
||||||
|
|
||||||
private BodyRecordParser() {}
|
private BodyRecordParser() {}
|
||||||
|
|
||||||
public static BodyRecord parse(byte[] bodyBytes) {
|
public static BodyRecord parse(short type, short subType, short version, byte[] bodyBytes) {
|
||||||
if (bodyBytes == null) throw new IllegalArgumentException("bodyBytes == null");
|
if (bodyBytes == null) throw new IllegalArgumentException("bodyBytes == null");
|
||||||
if (bodyBytes.length < 4) throw new IllegalArgumentException("bodyBytes too short (<4)");
|
|
||||||
|
|
||||||
ByteBuffer bb = ByteBuffer.wrap(bodyBytes).order(ByteOrder.BIG_ENDIAN);
|
int t = type & 0xFFFF;
|
||||||
short type = bb.getShort();
|
int v = version & 0xFFFF;
|
||||||
short ver = bb.getShort();
|
|
||||||
|
|
||||||
int key = ((type & 0xFFFF) << 16) | (ver & 0xFFFF);
|
// ключ = (type<<16)|version (как раньше по смыслу), но берём из HEADER
|
||||||
|
int key = (t << 16) | v;
|
||||||
|
|
||||||
return switch (key) {
|
return switch (key) {
|
||||||
case HeaderBody.KEY -> new HeaderBody(bodyBytes); // type=0, ver=1 заглавие блокчейна
|
case HeaderBody.KEY -> new HeaderBody(subType, version, bodyBytes);
|
||||||
case TextBody.KEY -> new TextBody(bodyBytes); // type=1, ver=1 текст
|
case TextBody.KEY -> new TextBody(subType, version, bodyBytes);
|
||||||
case ReactionBody.KEY -> new ReactionBody(bodyBytes); // type=2, ver=1 реакции
|
case ReactionBody.KEY -> new ReactionBody(subType, version, bodyBytes);
|
||||||
case ConnectionBody.KEY -> new ConnectionBody(bodyBytes); // type=3, ver=1 связи
|
case ConnectionBody.KEY -> new ConnectionBody(subType, version, bodyBytes);
|
||||||
case UserParamBody.KEY -> new UserParamBody(bodyBytes); // type=4, ver=1 параметры пользователя
|
case UserParamBody.KEY -> new UserParamBody(subType, version, bodyBytes);
|
||||||
default -> throw new IllegalArgumentException(String.format(
|
default -> throw new IllegalArgumentException(String.format(
|
||||||
"Unknown body type/version: type=%d ver=%d (key=0x%08X)",
|
"Unknown body type/version from header: type=%d ver=%d subType=%d",
|
||||||
(type & 0xFFFF), (ver & 0xFFFF), key
|
t, v, (subType & 0xFFFF)
|
||||||
));
|
));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,9 @@
|
|||||||
|
// =======================
|
||||||
|
// blockchain/body/ConnectionBody.java (ИЗМЕНЁННЫЙ: bodyBytes без type/subType/version, + line fields)
|
||||||
|
// =======================
|
||||||
package blockchain.body;
|
package blockchain.body;
|
||||||
|
|
||||||
import blockchain.LineIndex;
|
import shine.db.MsgSubType;
|
||||||
|
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.nio.ByteOrder;
|
import java.nio.ByteOrder;
|
||||||
@ -9,112 +12,75 @@ import java.util.Arrays;
|
|||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ConnectionBody — type=3, ver=1. (Связь/отношение)
|
* ConnectionBody — type=3, ver=1 (в заголовке блока).
|
||||||
*
|
*
|
||||||
* Идея:
|
* subType (в заголовке блока) как MsgSubType:
|
||||||
* - Это запись "у меня есть связь с X" ИЛИ "я отменяю связь с X".
|
* FRIEND=10, UNFRIEND=11
|
||||||
* - subType определяет вид связи и действие.
|
* CONTACT=20, UNCONTACT=21
|
||||||
|
* FOLLOW=30, UNFOLLOW=31
|
||||||
*
|
*
|
||||||
* subType (uint16):
|
* bodyBytes (BigEndian), новый формат:
|
||||||
* УСТАНОВИТЬ связь:
|
* [4] prevLineNumber
|
||||||
* 10 = FRIEND (друг)
|
* [32] prevLineHash32
|
||||||
* 20 = CONTACT (контакт)
|
* [4] thisLineNumber
|
||||||
* 30 = FOLLOW (подписан на кого-то)
|
|
||||||
*
|
|
||||||
* ОТМЕНИТЬ связь (событие, которое “снимает” прошлую связь):
|
|
||||||
* 11 = UNFRIEND (больше не друг)
|
|
||||||
* 21 = UNCONTACT (больше не контакт)
|
|
||||||
* 31 = UNFOLLOW (больше не подписан)
|
|
||||||
*
|
|
||||||
* Важно про смысл:
|
|
||||||
* - Состояние связи вычисляется по последнему блоку данной “категории”:
|
|
||||||
* (toLogin, kind=FRIEND/CONTACT/FOLLOW)
|
|
||||||
* Если последний subType — 10/20/30 => связь активна
|
|
||||||
* Если последний subType — 11/21/31 => связь снята
|
|
||||||
*
|
|
||||||
* Формат bodyBytes (BigEndian):
|
|
||||||
* [2] type=3
|
|
||||||
* [2] ver=1
|
|
||||||
*
|
|
||||||
* [2] subType (uint16) — вид связи (10,20/30)
|
|
||||||
*
|
*
|
||||||
* [1] toLoginLen (uint8)
|
* [1] toLoginLen (uint8)
|
||||||
* [N] toLogin UTF-8
|
* [N] toLogin UTF-8
|
||||||
* ВАЖНО: toLogin — это "с кем связь" (ключевой смысл этой записи).
|
|
||||||
*
|
*
|
||||||
* [1] toBlockchainNameLen (uint8)
|
* [1] toBlockchainNameLen (uint8)
|
||||||
* [M] toBlockchainName UTF-8
|
* [M] toBlockchainName UTF-8
|
||||||
* [4] toBlockGlobalNumber (int32)
|
* [4] toBlockGlobalNumber (int32)
|
||||||
* [32] toBlockHash32 (raw 32 bytes)
|
* [32] toBlockHash32 (raw 32 bytes)
|
||||||
*
|
|
||||||
* ВАЖНО: поля toBlockchainName/toBlockGlobalNumber/toBlockHash32 — это
|
|
||||||
* "последний известный блок" того человека (снимок/якорь состояния).
|
|
||||||
*
|
|
||||||
* ЛИНИЯ:
|
|
||||||
* - строго lineIndex=3 (выделяем отдельную линию под связи).
|
|
||||||
*/
|
*/
|
||||||
public final class ConnectionBody implements BodyRecord, BodyHasTarget {
|
public final class ConnectionBody implements BodyRecord, BodyHasTarget, BodyHasLine {
|
||||||
|
|
||||||
public static final short TYPE = 3;
|
public static final short TYPE = 3;
|
||||||
public static final short VER = 1;
|
public static final short VER = 1;
|
||||||
|
|
||||||
/** Удобный ключ для BodyRecordParser: (type<<16)|ver */
|
|
||||||
public static final int KEY = ((TYPE & 0xFFFF) << 16) | (VER & 0xFFFF);
|
public static final int KEY = ((TYPE & 0xFFFF) << 16) | (VER & 0xFFFF);
|
||||||
|
|
||||||
// --- subType: SET ---
|
public final short subType; // из header
|
||||||
public static final short SUB_FRIEND = 10;
|
public final short version; // из header
|
||||||
public static final short SUB_CONTACT = 20;
|
|
||||||
public static final short SUB_FOLLOW = 30;
|
|
||||||
|
|
||||||
// --- subType: UNSET (снятие/отмена связи) ---
|
// line
|
||||||
public static final short SUB_UNFRIEND = 11; // больше не друг
|
public final int prevLineNumber;
|
||||||
public static final short SUB_UNCONTACT = 21; // больше не контакт
|
public final byte[] prevLineHash32;
|
||||||
public static final short SUB_UNFOLLOW = 31; // больше не подписан
|
public final int thisLineNumber;
|
||||||
|
|
||||||
public final short subType;
|
// payload
|
||||||
|
|
||||||
/** С кем связь (главное поле). */
|
|
||||||
public final String toLogin;
|
public final String toLogin;
|
||||||
|
|
||||||
/** Блокчейн того человека (снимок/якорь). */
|
|
||||||
public final String toBlockchainName;
|
public final String toBlockchainName;
|
||||||
|
|
||||||
/** Номер последнего известного блока у того человека (снимок/якорь). */
|
|
||||||
public final int toBlockGlobalNumber;
|
public final int toBlockGlobalNumber;
|
||||||
|
|
||||||
/** Хэш последнего известного блока у того человека (снимок/якорь). */
|
|
||||||
public final byte[] toBlockHash32;
|
public final byte[] toBlockHash32;
|
||||||
|
|
||||||
/* ===================================================================== */
|
public ConnectionBody(short subType, short version, byte[] bodyBytes) {
|
||||||
/* ====================== Конструктор из байт =========================== */
|
|
||||||
/* ===================================================================== */
|
|
||||||
|
|
||||||
public ConnectionBody(byte[] bodyBytes) {
|
|
||||||
Objects.requireNonNull(bodyBytes, "bodyBytes == null");
|
Objects.requireNonNull(bodyBytes, "bodyBytes == null");
|
||||||
|
|
||||||
|
this.subType = subType;
|
||||||
|
this.version = version;
|
||||||
|
|
||||||
|
if ((this.version & 0xFFFF) != (VER & 0xFFFF)) {
|
||||||
|
throw new IllegalArgumentException("ConnectionBody version must be 1, got=" + (this.version & 0xFFFF));
|
||||||
|
}
|
||||||
|
if (!isValidSubType(this.subType)) {
|
||||||
|
throw new IllegalArgumentException("Bad connection subType: " + (this.subType & 0xFFFF));
|
||||||
|
}
|
||||||
|
|
||||||
// минимум:
|
// минимум:
|
||||||
// type[2]+ver[2]+subType[2] +
|
// line(4+32+4) + toLoginLen[1]+toLogin[1] + toBchLen[1]+toBch[1] + global[4] + hash[32]
|
||||||
// toLoginLen[1]+toLogin[1] +
|
if (bodyBytes.length < (4 + 32 + 4) + 1 + 1 + 1 + 1 + 4 + 32) {
|
||||||
// toBchLen[1]+toBch[1] +
|
|
||||||
// global[4] + hash[32]
|
|
||||||
if (bodyBytes.length < 2 + 2 + 2 + 1 + 1 + 1 + 1 + 4 + 32) {
|
|
||||||
throw new IllegalArgumentException("ConnectionBody too short");
|
throw new IllegalArgumentException("ConnectionBody too short");
|
||||||
}
|
}
|
||||||
|
|
||||||
ByteBuffer bb = ByteBuffer.wrap(bodyBytes).order(ByteOrder.BIG_ENDIAN);
|
ByteBuffer bb = ByteBuffer.wrap(bodyBytes).order(ByteOrder.BIG_ENDIAN);
|
||||||
|
|
||||||
short type = bb.getShort();
|
this.prevLineNumber = bb.getInt();
|
||||||
short ver = bb.getShort();
|
|
||||||
if (type != TYPE || ver != VER) {
|
|
||||||
throw new IllegalArgumentException("Not ConnectionBody: type=" + type + " ver=" + ver);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.subType = bb.getShort();
|
this.prevLineHash32 = new byte[32];
|
||||||
if (!isValidSubType(this.subType)) {
|
bb.get(this.prevLineHash32);
|
||||||
throw new IllegalArgumentException("Bad connection subType: " + (this.subType & 0xFFFF));
|
|
||||||
}
|
this.thisLineNumber = bb.getInt();
|
||||||
|
|
||||||
// --- toLogin ---
|
|
||||||
int toLoginLen = Byte.toUnsignedInt(bb.get());
|
int toLoginLen = Byte.toUnsignedInt(bb.get());
|
||||||
if (toLoginLen <= 0) throw new IllegalArgumentException("toLoginLen is 0");
|
if (toLoginLen <= 0) throw new IllegalArgumentException("toLoginLen is 0");
|
||||||
if (bb.remaining() < toLoginLen) throw new IllegalArgumentException("toLogin payload too short");
|
if (bb.remaining() < toLoginLen) throw new IllegalArgumentException("toLogin payload too short");
|
||||||
@ -123,8 +89,6 @@ public final class ConnectionBody implements BodyRecord, BodyHasTarget {
|
|||||||
bb.get(toLoginBytes);
|
bb.get(toLoginBytes);
|
||||||
this.toLogin = new String(toLoginBytes, StandardCharsets.UTF_8);
|
this.toLogin = new String(toLoginBytes, StandardCharsets.UTF_8);
|
||||||
|
|
||||||
// --- toBlockchainName + snapshot блока ---
|
|
||||||
if (bb.remaining() < 1) throw new IllegalArgumentException("Missing toBlockchainNameLen");
|
|
||||||
int bchLen = Byte.toUnsignedInt(bb.get());
|
int bchLen = Byte.toUnsignedInt(bb.get());
|
||||||
if (bchLen <= 0) throw new IllegalArgumentException("toBlockchainNameLen is 0");
|
if (bchLen <= 0) throw new IllegalArgumentException("toBlockchainNameLen is 0");
|
||||||
if (bb.remaining() < bchLen + 4 + 32) throw new IllegalArgumentException("Connection payload too short");
|
if (bb.remaining() < bchLen + 4 + 32) throw new IllegalArgumentException("Connection payload too short");
|
||||||
@ -138,17 +102,13 @@ public final class ConnectionBody implements BodyRecord, BodyHasTarget {
|
|||||||
this.toBlockHash32 = new byte[32];
|
this.toBlockHash32 = new byte[32];
|
||||||
bb.get(this.toBlockHash32);
|
bb.get(this.toBlockHash32);
|
||||||
|
|
||||||
// запрет мусора в конце
|
if (bb.remaining() != 0) throw new IllegalArgumentException("Unexpected tail bytes, remaining=" + bb.remaining());
|
||||||
if (bb.remaining() != 0) {
|
|
||||||
throw new IllegalArgumentException("Unexpected tail bytes, remaining=" + bb.remaining());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===================================================================== */
|
public ConnectionBody(int prevLineNumber,
|
||||||
/* ====================== Конструктор “вручную” ========================= */
|
byte[] prevLineHash32,
|
||||||
/* ===================================================================== */
|
int thisLineNumber,
|
||||||
|
short subType,
|
||||||
public ConnectionBody(short subType,
|
|
||||||
String toLogin,
|
String toLogin,
|
||||||
String toBlockchainName,
|
String toBlockchainName,
|
||||||
int toBlockGlobalNumber,
|
int toBlockGlobalNumber,
|
||||||
@ -158,19 +118,21 @@ public final class ConnectionBody implements BodyRecord, BodyHasTarget {
|
|||||||
Objects.requireNonNull(toBlockchainName, "toBlockchainName == null");
|
Objects.requireNonNull(toBlockchainName, "toBlockchainName == null");
|
||||||
Objects.requireNonNull(toBlockHash32, "toBlockHash32 == null");
|
Objects.requireNonNull(toBlockHash32, "toBlockHash32 == null");
|
||||||
|
|
||||||
if (!isValidSubType(subType)) {
|
if (!isValidSubType(subType)) throw new IllegalArgumentException("Bad connection subType: " + (subType & 0xFFFF));
|
||||||
throw new IllegalArgumentException("Unknown connection subType: " + (subType & 0xFFFF));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (toLogin.isBlank()) throw new IllegalArgumentException("toLogin is blank");
|
if (toLogin.isBlank()) throw new IllegalArgumentException("toLogin is blank");
|
||||||
if (!toLogin.matches("^[A-Za-z0-9_]+$"))
|
if (!toLogin.matches("^[A-Za-z0-9_]+$")) throw new IllegalArgumentException("toLogin must match ^[A-Za-z0-9_]+$");
|
||||||
throw new IllegalArgumentException("toLogin must match ^[A-Za-z0-9_]+$");
|
|
||||||
|
|
||||||
if (toBlockchainName.isBlank()) throw new IllegalArgumentException("toBlockchainName is blank");
|
if (toBlockchainName.isBlank()) throw new IllegalArgumentException("toBlockchainName is blank");
|
||||||
if (toBlockGlobalNumber < 0) throw new IllegalArgumentException("toBlockGlobalNumber < 0");
|
if (toBlockGlobalNumber < 0) throw new IllegalArgumentException("toBlockGlobalNumber < 0");
|
||||||
if (toBlockHash32.length != 32) throw new IllegalArgumentException("toBlockHash32 != 32");
|
if (toBlockHash32.length != 32) throw new IllegalArgumentException("toBlockHash32 != 32");
|
||||||
|
|
||||||
|
this.prevLineNumber = prevLineNumber;
|
||||||
|
this.prevLineHash32 = (prevLineHash32 == null ? new byte[32] : Arrays.copyOf(prevLineHash32, 32));
|
||||||
|
this.thisLineNumber = thisLineNumber;
|
||||||
|
|
||||||
this.subType = subType;
|
this.subType = subType;
|
||||||
|
this.version = VER;
|
||||||
|
|
||||||
this.toLogin = toLogin;
|
this.toLogin = toLogin;
|
||||||
this.toBlockchainName = toBlockchainName;
|
this.toBlockchainName = toBlockchainName;
|
||||||
this.toBlockGlobalNumber = toBlockGlobalNumber;
|
this.toBlockGlobalNumber = toBlockGlobalNumber;
|
||||||
@ -178,62 +140,33 @@ public final class ConnectionBody implements BodyRecord, BodyHasTarget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static boolean isValidSubType(short st) {
|
private static boolean isValidSubType(short st) {
|
||||||
return st == SUB_FRIEND || st == SUB_CONTACT || st == SUB_FOLLOW
|
int v = st & 0xFFFF;
|
||||||
|| st == SUB_UNFRIEND || st == SUB_UNCONTACT || st == SUB_UNFOLLOW;
|
return v == (MsgSubType.CONNECTION_FRIEND & 0xFFFF)
|
||||||
}
|
|| v == (MsgSubType.CONNECTION_UNFRIEND & 0xFFFF)
|
||||||
|
|| v == (MsgSubType.CONNECTION_CONTACT & 0xFFFF)
|
||||||
/** true если это событие установки связи (10/20/30). */
|
|| v == (MsgSubType.CONNECTION_UNCONTACT & 0xFFFF)
|
||||||
public boolean isSetAction() {
|
|| v == (MsgSubType.CONNECTION_FOLLOW & 0xFFFF)
|
||||||
return subType == SUB_FRIEND || subType == SUB_CONTACT || subType == SUB_FOLLOW;
|
|| v == (MsgSubType.CONNECTION_UNFOLLOW & 0xFFFF);
|
||||||
}
|
|
||||||
|
|
||||||
/** true если это событие снятия связи (11/21/31). */
|
|
||||||
public boolean isUnsetAction() {
|
|
||||||
return subType == SUB_UNFRIEND || subType == SUB_UNCONTACT || subType == SUB_UNFOLLOW;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Нормализованный “вид связи” без действия:
|
|
||||||
* FRIEND / CONTACT / FOLLOW
|
|
||||||
*/
|
|
||||||
public short kind() {
|
|
||||||
return switch (subType) {
|
|
||||||
case SUB_FRIEND, SUB_UNFRIEND -> SUB_FRIEND;
|
|
||||||
case SUB_CONTACT, SUB_UNCONTACT -> SUB_CONTACT;
|
|
||||||
case SUB_FOLLOW, SUB_UNFOLLOW -> SUB_FOLLOW;
|
|
||||||
default -> throw new IllegalStateException("Unexpected subType: " + (subType & 0xFFFF));
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ===================================================================== */
|
|
||||||
/* ====================== BodyRecord контракт =========================== */
|
|
||||||
/* ===================================================================== */
|
|
||||||
|
|
||||||
@Override public short type() { return TYPE; }
|
|
||||||
@Override public short version() { return VER; }
|
|
||||||
@Override public short subType() { return subType; }
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public short expectedLineIndex() {
|
|
||||||
return LineIndex.CONNECTION;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ConnectionBody check() {
|
public ConnectionBody check() {
|
||||||
if (!isValidSubType(subType))
|
if (!isValidSubType(subType)) throw new IllegalArgumentException("Bad connection subType: " + (subType & 0xFFFF));
|
||||||
throw new IllegalArgumentException("Bad connection subType: " + (subType & 0xFFFF));
|
|
||||||
|
|
||||||
if (toLogin == null || toLogin.isBlank())
|
// line rule
|
||||||
throw new IllegalArgumentException("toLogin is blank");
|
if (prevLineNumber == -1) {
|
||||||
if (!toLogin.matches("^[A-Za-z0-9_]+$"))
|
if (!isAllZero32(prevLineHash32)) throw new IllegalArgumentException("prevLineHash32 must be zero when prevLineNumber=-1");
|
||||||
throw new IllegalArgumentException("toLogin must match ^[A-Za-z0-9_]+$");
|
if (thisLineNumber != -1) throw new IllegalArgumentException("thisLineNumber must be -1 when prevLineNumber=-1");
|
||||||
|
} else {
|
||||||
|
if (prevLineHash32 == null || prevLineHash32.length != 32) throw new IllegalArgumentException("prevLineHash32 invalid");
|
||||||
|
}
|
||||||
|
|
||||||
if (toBlockchainName == null || toBlockchainName.isBlank())
|
if (toLogin == null || toLogin.isBlank()) throw new IllegalArgumentException("toLogin is blank");
|
||||||
throw new IllegalArgumentException("toBlockchainName is blank");
|
if (!toLogin.matches("^[A-Za-z0-9_]+$")) throw new IllegalArgumentException("toLogin must match ^[A-Za-z0-9_]+$");
|
||||||
if (toBlockGlobalNumber < 0)
|
|
||||||
throw new IllegalArgumentException("toBlockGlobalNumber < 0");
|
if (toBlockchainName == null || toBlockchainName.isBlank()) throw new IllegalArgumentException("toBlockchainName is blank");
|
||||||
if (toBlockHash32 == null || toBlockHash32.length != 32)
|
if (toBlockGlobalNumber < 0) throw new IllegalArgumentException("toBlockGlobalNumber < 0");
|
||||||
throw new IllegalArgumentException("toBlockHash32 invalid");
|
if (toBlockHash32 == null || toBlockHash32.length != 32) throw new IllegalArgumentException("toBlockHash32 invalid");
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
@ -248,26 +181,19 @@ public final class ConnectionBody implements BodyRecord, BodyHasTarget {
|
|||||||
if (bchBytes.length == 0 || bchBytes.length > 255)
|
if (bchBytes.length == 0 || bchBytes.length > 255)
|
||||||
throw new IllegalArgumentException("toBlockchainName utf8 len must be 1..255");
|
throw new IllegalArgumentException("toBlockchainName utf8 len must be 1..255");
|
||||||
|
|
||||||
if (!isValidSubType(subType))
|
|
||||||
throw new IllegalArgumentException("Bad connection subType: " + (subType & 0xFFFF));
|
|
||||||
if (toBlockHash32 == null || toBlockHash32.length != 32)
|
if (toBlockHash32 == null || toBlockHash32.length != 32)
|
||||||
throw new IllegalArgumentException("toBlockHash32 != 32");
|
throw new IllegalArgumentException("toBlockHash32 != 32");
|
||||||
|
|
||||||
// type[2]+ver[2]+subType[2]
|
int cap = (4 + 32 + 4)
|
||||||
// + toLoginLen[1]+toLogin[N]
|
|
||||||
// + toBchLen[1]+toBch[M]
|
|
||||||
// + global[4]+hash[32]
|
|
||||||
int cap = 2 + 2 + 2
|
|
||||||
+ 1 + toLoginBytes.length
|
+ 1 + toLoginBytes.length
|
||||||
+ 1 + bchBytes.length
|
+ 1 + bchBytes.length
|
||||||
+ 4 + 32;
|
+ 4 + 32;
|
||||||
|
|
||||||
ByteBuffer bb = ByteBuffer.allocate(cap).order(ByteOrder.BIG_ENDIAN);
|
ByteBuffer bb = ByteBuffer.allocate(cap).order(ByteOrder.BIG_ENDIAN);
|
||||||
|
|
||||||
bb.putShort(TYPE);
|
bb.putInt(prevLineNumber);
|
||||||
bb.putShort(VER);
|
bb.put(prevLineHash32 == null ? new byte[32] : Arrays.copyOf(prevLineHash32, 32));
|
||||||
|
bb.putInt(thisLineNumber);
|
||||||
bb.putShort(subType);
|
|
||||||
|
|
||||||
bb.put((byte) toLoginBytes.length);
|
bb.put((byte) toLoginBytes.length);
|
||||||
bb.put(toLoginBytes);
|
bb.put(toLoginBytes);
|
||||||
@ -281,69 +207,20 @@ public final class ConnectionBody implements BodyRecord, BodyHasTarget {
|
|||||||
return bb.array();
|
return bb.array();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
private static boolean isAllZero32(byte[] b) {
|
||||||
public String toString() {
|
if (b == null || b.length != 32) return true;
|
||||||
String st = switch (subType) {
|
for (int i = 0; i < 32; i++) if (b[i] != 0) return false;
|
||||||
case SUB_FRIEND -> "FRIEND (10)";
|
return true;
|
||||||
case SUB_CONTACT -> "CONTACT (20)";
|
|
||||||
case SUB_FOLLOW -> "FOLLOW (30)";
|
|
||||||
case SUB_UNFRIEND -> "UNFRIEND (11)";
|
|
||||||
case SUB_UNCONTACT -> "UNCONTACT (21)";
|
|
||||||
case SUB_UNFOLLOW -> "UNFOLLOW (31)";
|
|
||||||
default -> "UNKNOWN";
|
|
||||||
};
|
|
||||||
|
|
||||||
String action = isSetAction() ? "SET" : (isUnsetAction() ? "UNSET" : "?");
|
|
||||||
String kindStr = switch (kind()) {
|
|
||||||
case SUB_FRIEND -> "FRIEND";
|
|
||||||
case SUB_CONTACT -> "CONTACT";
|
|
||||||
case SUB_FOLLOW -> "FOLLOW";
|
|
||||||
default -> "?";
|
|
||||||
};
|
|
||||||
|
|
||||||
return """
|
|
||||||
ConnectionBody {
|
|
||||||
тип записи : CONNECTION (type=3, ver=1)
|
|
||||||
ожидаемая линия : 3
|
|
||||||
subType : %s
|
|
||||||
действие : %s
|
|
||||||
вид связи : %s
|
|
||||||
связь с login : "%s"
|
|
||||||
блокчейн друга/цели : "%s"
|
|
||||||
lastKnown globalNumber : %d
|
|
||||||
lastKnown hash (hex) : %s
|
|
||||||
}
|
|
||||||
""".formatted(
|
|
||||||
st,
|
|
||||||
action,
|
|
||||||
kindStr,
|
|
||||||
toLogin,
|
|
||||||
toBlockchainName,
|
|
||||||
toBlockGlobalNumber,
|
|
||||||
toBlockHashHex()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public String toBlockHashHex() {
|
/* ====================== BodyHasLine ====================== */
|
||||||
char[] HEX = "0123456789abcdef".toCharArray();
|
@Override public int prevLineNumber() { return prevLineNumber; }
|
||||||
char[] out = new char[64];
|
@Override public byte[] prevLineHash32() { return prevLineHash32 == null ? null : Arrays.copyOf(prevLineHash32, 32); }
|
||||||
for (int i = 0; i < 32; i++) {
|
@Override public int thisLineNumber() { return thisLineNumber; }
|
||||||
int v = toBlockHash32[i] & 0xFF;
|
|
||||||
out[i * 2] = HEX[v >>> 4];
|
|
||||||
out[i * 2 + 1] = HEX[v & 0x0F];
|
|
||||||
}
|
|
||||||
return new String(out);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ===================================================================== */
|
|
||||||
/* ====================== BodyHasTarget контракт ========================= */
|
|
||||||
/* ===================================================================== */
|
|
||||||
|
|
||||||
|
/* ====================== BodyHasTarget ===================== */
|
||||||
@Override public String toLogin() { return toLogin; }
|
@Override public String toLogin() { return toLogin; }
|
||||||
|
|
||||||
@Override public String toBchName() { return toBlockchainName; }
|
@Override public String toBchName() { return toBlockchainName; }
|
||||||
|
|
||||||
@Override public Integer toBlockGlobalNumber() { return toBlockGlobalNumber; }
|
@Override public Integer toBlockGlobalNumber() { return toBlockGlobalNumber; }
|
||||||
|
|
||||||
@Override public byte[] toBlockHasheBytes() { return toBlockHash32; }
|
@Override public byte[] toBlockHasheBytes() { return toBlockHash32; }
|
||||||
}
|
}
|
||||||
@ -1,6 +1,8 @@
|
|||||||
|
// =======================
|
||||||
|
// blockchain/body/HeaderBody.java (ИЗМЕНЁННЫЙ: bodyBytes без type/subType/version)
|
||||||
|
// =======================
|
||||||
package blockchain.body;
|
package blockchain.body;
|
||||||
|
|
||||||
import blockchain.LineIndex;
|
|
||||||
import utils.config.ShineSignatureConstants;
|
import utils.config.ShineSignatureConstants;
|
||||||
|
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
@ -11,18 +13,13 @@ import java.util.Objects;
|
|||||||
/**
|
/**
|
||||||
* HeaderBody — type=0, version=1.
|
* HeaderBody — type=0, version=1.
|
||||||
*
|
*
|
||||||
* Полный bodyBytes (BigEndian):
|
* В новом формате type/subType/version живут в HEADER блока,
|
||||||
* [2] type=0
|
* поэтому bodyBytes для HeaderBody содержат только payload:
|
||||||
* [2] version=1
|
|
||||||
*
|
|
||||||
* [2] subType (uint16) = 0
|
|
||||||
*
|
*
|
||||||
|
* bodyBytes (BigEndian):
|
||||||
* [TAG_LEN] tag ASCII "SHiNE"
|
* [TAG_LEN] tag ASCII "SHiNE"
|
||||||
* [1] loginLength=N (uint8)
|
* [1] loginLength=N (uint8)
|
||||||
* [N] login UTF-8
|
* [N] login UTF-8
|
||||||
*
|
|
||||||
* ЛИНИЯ:
|
|
||||||
* - строго lineIndex=0 (genesis)
|
|
||||||
*/
|
*/
|
||||||
public final class HeaderBody implements BodyRecord {
|
public final class HeaderBody implements BodyRecord {
|
||||||
|
|
||||||
@ -31,40 +28,39 @@ public final class HeaderBody implements BodyRecord {
|
|||||||
|
|
||||||
public static final int KEY = ((TYPE & 0xFFFF) << 16) | (VER & 0xFFFF);
|
public static final int KEY = ((TYPE & 0xFFFF) << 16) | (VER & 0xFFFF);
|
||||||
|
|
||||||
/** Для header всегда 0 (служебная совместимость). */
|
/** Для header subType всегда 0 (служебная совместимость). */
|
||||||
public static final short SUBTYPE_COMPAT = 0;
|
public static final short SUBTYPE_COMPAT = 0;
|
||||||
|
|
||||||
/** TAG формата (ASCII). Значение берём из общих строковых констант. */
|
/** TAG формата (ASCII). */
|
||||||
public static final String TAG = ShineSignatureConstants.BLOCKCHAIN_HEADER_TAG;
|
public static final String TAG = ShineSignatureConstants.BLOCKCHAIN_HEADER_TAG;
|
||||||
|
|
||||||
// ✅ производные значения считаем "на месте", а не в константах
|
|
||||||
private static final byte[] TAG_ASCII = TAG.getBytes(StandardCharsets.US_ASCII);
|
private static final byte[] TAG_ASCII = TAG.getBytes(StandardCharsets.US_ASCII);
|
||||||
private static final int TAG_LEN = TAG_ASCII.length;
|
private static final int TAG_LEN = TAG_ASCII.length;
|
||||||
|
|
||||||
public final short subType; // всегда 0
|
public final short subType; // всегда 0 (из заголовка блока)
|
||||||
|
public final short version; // из заголовка блока
|
||||||
public final String tag; // "SHiNE"
|
public final String tag; // "SHiNE"
|
||||||
public final String login;
|
public final String login;
|
||||||
|
|
||||||
/** Десериализация из полного bodyBytes (включая type/version/subType). */
|
/** Десериализация из payload bodyBytes (без type/subType/version). */
|
||||||
public HeaderBody(byte[] bodyBytes) {
|
public HeaderBody(short subType, short version, byte[] bodyBytes) {
|
||||||
Objects.requireNonNull(bodyBytes, "bodyBytes == null");
|
Objects.requireNonNull(bodyBytes, "bodyBytes == null");
|
||||||
if (bodyBytes.length < 4 + 2) throw new IllegalArgumentException("HeaderBody too short (<6)");
|
|
||||||
|
this.subType = subType;
|
||||||
|
this.version = version;
|
||||||
|
|
||||||
|
if ((this.subType & 0xFFFF) != (SUBTYPE_COMPAT & 0xFFFF)) {
|
||||||
|
throw new IllegalArgumentException("HeaderBody subType must be 0, got=" + (this.subType & 0xFFFF));
|
||||||
|
}
|
||||||
|
if ((this.version & 0xFFFF) != (VER & 0xFFFF)) {
|
||||||
|
throw new IllegalArgumentException("HeaderBody version must be 1, got=" + (this.version & 0xFFFF));
|
||||||
|
}
|
||||||
|
|
||||||
|
// минимум: tag[TAG_LEN] + loginLen[1]
|
||||||
|
if (bodyBytes.length < TAG_LEN + 1) throw new IllegalArgumentException("HeaderBody too short");
|
||||||
|
|
||||||
ByteBuffer bb = ByteBuffer.wrap(bodyBytes).order(ByteOrder.BIG_ENDIAN);
|
ByteBuffer bb = ByteBuffer.wrap(bodyBytes).order(ByteOrder.BIG_ENDIAN);
|
||||||
|
|
||||||
short type = bb.getShort();
|
|
||||||
short ver = bb.getShort();
|
|
||||||
if (type != TYPE || ver != VER)
|
|
||||||
throw new IllegalArgumentException("Not HeaderBody: type=" + type + " ver=" + ver);
|
|
||||||
|
|
||||||
this.subType = bb.getShort();
|
|
||||||
if (this.subType != SUBTYPE_COMPAT)
|
|
||||||
throw new IllegalArgumentException("HeaderBody subType must be 0, got=" + (this.subType & 0xFFFF));
|
|
||||||
|
|
||||||
// дальше: tag[TAG_LEN] + loginLen[1] минимум
|
|
||||||
if (bb.remaining() < TAG_LEN + 1)
|
|
||||||
throw new IllegalArgumentException("Header payload too short");
|
|
||||||
|
|
||||||
byte[] tagBytes = new byte[TAG_LEN];
|
byte[] tagBytes = new byte[TAG_LEN];
|
||||||
bb.get(tagBytes);
|
bb.get(tagBytes);
|
||||||
String t = new String(tagBytes, StandardCharsets.US_ASCII);
|
String t = new String(tagBytes, StandardCharsets.US_ASCII);
|
||||||
@ -79,59 +75,43 @@ public final class HeaderBody implements BodyRecord {
|
|||||||
bb.get(loginBytes);
|
bb.get(loginBytes);
|
||||||
this.login = new String(loginBytes, StandardCharsets.UTF_8);
|
this.login = new String(loginBytes, StandardCharsets.UTF_8);
|
||||||
|
|
||||||
if (bb.remaining() != 0) {
|
if (bb.remaining() != 0) throw new IllegalArgumentException("Unexpected tail bytes, remaining=" + bb.remaining());
|
||||||
throw new IllegalArgumentException("Unexpected tail bytes, remaining=" + bb.remaining());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Создание “вручную” (для генерации первого блока). */
|
/** Создание “вручную”. */
|
||||||
public HeaderBody(String login) {
|
public HeaderBody(String login) {
|
||||||
Objects.requireNonNull(login, "login == null");
|
Objects.requireNonNull(login, "login == null");
|
||||||
this.subType = SUBTYPE_COMPAT;
|
this.subType = SUBTYPE_COMPAT;
|
||||||
|
this.version = VER;
|
||||||
this.tag = TAG;
|
this.tag = TAG;
|
||||||
this.login = login;
|
this.login = login;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override public short type() { return TYPE; }
|
|
||||||
@Override public short version() { return VER; }
|
|
||||||
@Override public short subType() { return subType; }
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public short expectedLineIndex() {
|
|
||||||
return LineIndex.HEADER;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public HeaderBody check() {
|
public HeaderBody check() {
|
||||||
if (subType != SUBTYPE_COMPAT)
|
if ((subType & 0xFFFF) != (SUBTYPE_COMPAT & 0xFFFF))
|
||||||
throw new IllegalArgumentException("HeaderBody subType must be 0");
|
throw new IllegalArgumentException("HeaderBody subType must be 0");
|
||||||
|
|
||||||
if (login == null || login.isBlank())
|
if (login == null || login.isBlank())
|
||||||
throw new IllegalArgumentException("Login is blank");
|
throw new IllegalArgumentException("Login is blank");
|
||||||
if (!login.matches("^[A-Za-z0-9_]+$"))
|
if (!login.matches("^[A-Za-z0-9_]+$"))
|
||||||
throw new IllegalArgumentException("Login must match ^[A-Za-z0-9_]+$");
|
throw new IllegalArgumentException("Login must match ^[A-Za-z0-9_]+$");
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public byte[] toBytes() {
|
public byte[] toBytes() {
|
||||||
byte[] loginUtf8 = login.getBytes(StandardCharsets.UTF_8);
|
byte[] loginUtf8 = login.getBytes(StandardCharsets.UTF_8);
|
||||||
if (loginUtf8.length > 255)
|
if (loginUtf8.length == 0 || loginUtf8.length > 255)
|
||||||
throw new IllegalArgumentException("Login too long (>255 bytes)");
|
throw new IllegalArgumentException("Login utf8 len must be 1..255");
|
||||||
|
|
||||||
// type[2] + ver[2] + subType[2] + tag[TAG_LEN] + loginLen[1] + login[N]
|
int cap = TAG_LEN + 1 + loginUtf8.length;
|
||||||
int cap = 2 + 2 + 2 + TAG_LEN + 1 + loginUtf8.length;
|
|
||||||
|
|
||||||
ByteBuffer bb = ByteBuffer.allocate(cap).order(ByteOrder.BIG_ENDIAN);
|
ByteBuffer bb = ByteBuffer.allocate(cap).order(ByteOrder.BIG_ENDIAN);
|
||||||
|
bb.put(TAG_ASCII);
|
||||||
bb.putShort(TYPE);
|
bb.put((byte) loginUtf8.length);
|
||||||
bb.putShort(VER);
|
bb.put(loginUtf8);
|
||||||
|
|
||||||
bb.putShort(SUBTYPE_COMPAT);
|
|
||||||
|
|
||||||
bb.put(TAG_ASCII); // [TAG_LEN]
|
|
||||||
bb.put((byte) loginUtf8.length); // [1]
|
|
||||||
bb.put(loginUtf8); // [N]
|
|
||||||
|
|
||||||
return bb.array();
|
return bb.array();
|
||||||
}
|
}
|
||||||
@ -140,8 +120,7 @@ public final class HeaderBody implements BodyRecord {
|
|||||||
public String toString() {
|
public String toString() {
|
||||||
return """
|
return """
|
||||||
HeaderBody {
|
HeaderBody {
|
||||||
тип записи : HEADER (type=0, ver=1)
|
тип записи : HEADER (type=0, ver=1) [в заголовке блока]
|
||||||
ожидаемая линия : 0 (genesis)
|
|
||||||
subType : 0 (compat)
|
subType : 0 (compat)
|
||||||
тег формата : "%s"
|
тег формата : "%s"
|
||||||
login владельца : "%s"
|
login владельца : "%s"
|
||||||
|
|||||||
@ -1,6 +1,9 @@
|
|||||||
|
// =======================
|
||||||
|
// blockchain/body/ReactionBody.java (ИЗМЕНЁННЫЙ: bodyBytes без type/subType/version, НЕТ линейных полей)
|
||||||
|
// =======================
|
||||||
package blockchain.body;
|
package blockchain.body;
|
||||||
|
|
||||||
import blockchain.LineIndex;
|
import shine.db.MsgSubType;
|
||||||
|
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.nio.ByteOrder;
|
import java.nio.ByteOrder;
|
||||||
@ -9,22 +12,18 @@ import java.util.Arrays;
|
|||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ReactionBody — type=2, version=1.
|
* ReactionBody — type=2, version=1 (в заголовке блока).
|
||||||
*
|
*
|
||||||
* Формат bodyBytes (BigEndian):
|
* subType (в заголовке блока):
|
||||||
* [2] type=2
|
* 1 = LIKE
|
||||||
* [2] ver=1
|
|
||||||
*
|
|
||||||
* [2] subType (uint16) — подтип реакции
|
|
||||||
* 1 = LIKE (лайк)
|
|
||||||
*
|
*
|
||||||
|
* bodyBytes (BigEndian), новый формат:
|
||||||
* [1] toBlockchainNameLen (uint8)
|
* [1] toBlockchainNameLen (uint8)
|
||||||
* [N] toBlockchainName UTF-8
|
* [N] toBlockchainName UTF-8
|
||||||
* [4] toBlockGlobalNumber (int32)
|
* [4] toBlockGlobalNumber (int32)
|
||||||
* [32] toBlockHash32 (raw 32 bytes)
|
* [32] toBlockHash32 (raw 32 bytes)
|
||||||
*
|
*
|
||||||
* ЛИНИЯ:
|
* ЛИНИИ НЕТ.
|
||||||
* - строго lineIndex=2
|
|
||||||
*/
|
*/
|
||||||
public final class ReactionBody implements BodyRecord, BodyHasTarget {
|
public final class ReactionBody implements BodyRecord, BodyHasTarget {
|
||||||
|
|
||||||
@ -33,40 +32,34 @@ public final class ReactionBody implements BodyRecord, BodyHasTarget {
|
|||||||
|
|
||||||
public static final int KEY = ((TYPE & 0xFFFF) << 16) | (VER & 0xFFFF);
|
public static final int KEY = ((TYPE & 0xFFFF) << 16) | (VER & 0xFFFF);
|
||||||
|
|
||||||
// subType:
|
public final short subType; // из header
|
||||||
public static final short SUB_LIKE = 1;
|
public final short version; // из header
|
||||||
|
|
||||||
public final short subType;
|
|
||||||
|
|
||||||
public final String toBlockchainName;
|
public final String toBlockchainName;
|
||||||
public final int toBlockGlobalNumber;
|
public final int toBlockGlobalNumber;
|
||||||
public final byte[] toBlockHash32;
|
public final byte[] toBlockHash32;
|
||||||
|
|
||||||
/** Десериализация из полного bodyBytes (включая type/version/subType). */
|
public ReactionBody(short subType, short version, byte[] bodyBytes) {
|
||||||
public ReactionBody(byte[] bodyBytes) {
|
|
||||||
Objects.requireNonNull(bodyBytes, "bodyBytes == null");
|
Objects.requireNonNull(bodyBytes, "bodyBytes == null");
|
||||||
|
|
||||||
// минимум: type[2]+ver[2]+subType[2]+nameLen[1]+name[1]+global[4]+hash[32]
|
this.subType = subType;
|
||||||
if (bodyBytes.length < 2 + 2 + 2 + 1 + 1 + 4 + 32) {
|
this.version = version;
|
||||||
throw new IllegalArgumentException("ReactionBody too short");
|
|
||||||
|
if ((this.version & 0xFFFF) != (VER & 0xFFFF)) {
|
||||||
|
throw new IllegalArgumentException("ReactionBody version must be 1, got=" + (this.version & 0xFFFF));
|
||||||
}
|
}
|
||||||
|
if ((this.subType & 0xFFFF) != (MsgSubType.REACTION_LIKE & 0xFFFF)) {
|
||||||
ByteBuffer bb = ByteBuffer.wrap(bodyBytes).order(ByteOrder.BIG_ENDIAN);
|
|
||||||
|
|
||||||
short type = bb.getShort();
|
|
||||||
short ver = bb.getShort();
|
|
||||||
if (type != TYPE || ver != VER)
|
|
||||||
throw new IllegalArgumentException("Not ReactionBody: type=" + type + " ver=" + ver);
|
|
||||||
|
|
||||||
this.subType = bb.getShort();
|
|
||||||
if (this.subType != SUB_LIKE) {
|
|
||||||
throw new IllegalArgumentException("Bad reaction subType: " + (this.subType & 0xFFFF));
|
throw new IllegalArgumentException("Bad reaction subType: " + (this.subType & 0xFFFF));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// минимум: nameLen[1]+name[1]+global[4]+hash[32]
|
||||||
|
if (bodyBytes.length < 1 + 1 + 4 + 32) throw new IllegalArgumentException("ReactionBody too short");
|
||||||
|
|
||||||
|
ByteBuffer bb = ByteBuffer.wrap(bodyBytes).order(ByteOrder.BIG_ENDIAN);
|
||||||
|
|
||||||
int nameLen = Byte.toUnsignedInt(bb.get());
|
int nameLen = Byte.toUnsignedInt(bb.get());
|
||||||
if (nameLen <= 0) throw new IllegalArgumentException("toBlockchainNameLen is 0");
|
if (nameLen <= 0) throw new IllegalArgumentException("toBlockchainNameLen is 0");
|
||||||
if (bb.remaining() < nameLen + 4 + 32)
|
if (bb.remaining() < nameLen + 4 + 32) throw new IllegalArgumentException("ReactionBody payload too short");
|
||||||
throw new IllegalArgumentException("ReactionBody payload too short");
|
|
||||||
|
|
||||||
byte[] nameBytes = new byte[nameLen];
|
byte[] nameBytes = new byte[nameLen];
|
||||||
bb.get(nameBytes);
|
bb.get(nameBytes);
|
||||||
@ -77,46 +70,28 @@ public final class ReactionBody implements BodyRecord, BodyHasTarget {
|
|||||||
this.toBlockHash32 = new byte[32];
|
this.toBlockHash32 = new byte[32];
|
||||||
bb.get(this.toBlockHash32);
|
bb.get(this.toBlockHash32);
|
||||||
|
|
||||||
// запрет мусора в конце
|
if (bb.remaining() != 0) throw new IllegalArgumentException("Unexpected tail bytes, remaining=" + bb.remaining());
|
||||||
if (bb.remaining() != 0) {
|
|
||||||
throw new IllegalArgumentException("Unexpected tail bytes, remaining=" + bb.remaining());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Создание “вручную”. */
|
public ReactionBody(String toBlockchainName, int toBlockGlobalNumber, byte[] toBlockHash32) {
|
||||||
public ReactionBody(short subType,
|
|
||||||
String toBlockchainName,
|
|
||||||
int toBlockGlobalNumber,
|
|
||||||
byte[] toBlockHash32) {
|
|
||||||
|
|
||||||
Objects.requireNonNull(toBlockchainName, "toBlockchainName == null");
|
Objects.requireNonNull(toBlockchainName, "toBlockchainName == null");
|
||||||
Objects.requireNonNull(toBlockHash32, "toBlockHash32 == null");
|
Objects.requireNonNull(toBlockHash32, "toBlockHash32 == null");
|
||||||
|
|
||||||
if (subType != SUB_LIKE)
|
this.subType = MsgSubType.REACTION_LIKE;
|
||||||
throw new IllegalArgumentException("Unknown reaction subType: " + (subType & 0xFFFF));
|
this.version = VER;
|
||||||
|
|
||||||
if (toBlockchainName.isBlank()) throw new IllegalArgumentException("toBlockchainName is blank");
|
if (toBlockchainName.isBlank()) throw new IllegalArgumentException("toBlockchainName is blank");
|
||||||
if (toBlockGlobalNumber < 0) throw new IllegalArgumentException("toBlockGlobalNumber < 0");
|
if (toBlockGlobalNumber < 0) throw new IllegalArgumentException("toBlockGlobalNumber < 0");
|
||||||
if (toBlockHash32.length != 32) throw new IllegalArgumentException("toBlockHash32 != 32");
|
if (toBlockHash32.length != 32) throw new IllegalArgumentException("toBlockHash32 != 32");
|
||||||
|
|
||||||
this.subType = subType;
|
|
||||||
this.toBlockchainName = toBlockchainName;
|
this.toBlockchainName = toBlockchainName;
|
||||||
this.toBlockGlobalNumber = toBlockGlobalNumber;
|
this.toBlockGlobalNumber = toBlockGlobalNumber;
|
||||||
this.toBlockHash32 = Arrays.copyOf(toBlockHash32, 32);
|
this.toBlockHash32 = Arrays.copyOf(toBlockHash32, 32);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override public short type() { return TYPE; }
|
|
||||||
@Override public short version() { return VER; }
|
|
||||||
@Override public short subType() { return subType; }
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public short expectedLineIndex() {
|
|
||||||
return LineIndex.REACTION;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ReactionBody check() {
|
public ReactionBody check() {
|
||||||
if (subType != SUB_LIKE)
|
if ((subType & 0xFFFF) != (MsgSubType.REACTION_LIKE & 0xFFFF))
|
||||||
throw new IllegalArgumentException("Bad reaction subType: " + (subType & 0xFFFF));
|
throw new IllegalArgumentException("Bad reaction subType: " + (subType & 0xFFFF));
|
||||||
|
|
||||||
if (toBlockchainName == null || toBlockchainName.isBlank())
|
if (toBlockchainName == null || toBlockchainName.isBlank())
|
||||||
@ -135,16 +110,9 @@ public final class ReactionBody implements BodyRecord, BodyHasTarget {
|
|||||||
if (nameBytes.length == 0 || nameBytes.length > 255)
|
if (nameBytes.length == 0 || nameBytes.length > 255)
|
||||||
throw new IllegalArgumentException("toBlockchainName utf8 len must be 1..255");
|
throw new IllegalArgumentException("toBlockchainName utf8 len must be 1..255");
|
||||||
|
|
||||||
// type[2]+ver[2]+subType[2] + nameLen[1]+name[N] + global[4] + hash[32]
|
int cap = 1 + nameBytes.length + 4 + 32;
|
||||||
int cap = 2 + 2 + 2 + 1 + nameBytes.length + 4 + 32;
|
|
||||||
|
|
||||||
ByteBuffer bb = ByteBuffer.allocate(cap).order(ByteOrder.BIG_ENDIAN);
|
ByteBuffer bb = ByteBuffer.allocate(cap).order(ByteOrder.BIG_ENDIAN);
|
||||||
|
|
||||||
bb.putShort(TYPE);
|
|
||||||
bb.putShort(VER);
|
|
||||||
|
|
||||||
bb.putShort(subType);
|
|
||||||
|
|
||||||
bb.put((byte) nameBytes.length);
|
bb.put((byte) nameBytes.length);
|
||||||
bb.put(nameBytes);
|
bb.put(nameBytes);
|
||||||
bb.putInt(toBlockGlobalNumber);
|
bb.putInt(toBlockGlobalNumber);
|
||||||
@ -153,43 +121,8 @@ public final class ReactionBody implements BodyRecord, BodyHasTarget {
|
|||||||
return bb.array();
|
return bb.array();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
/* ====================== BodyHasTarget ====================== */
|
||||||
public String toString() {
|
|
||||||
String st = (subType == SUB_LIKE) ? "LIKE (1)" : "UNKNOWN";
|
|
||||||
|
|
||||||
return """
|
|
||||||
ReactionBody {
|
|
||||||
тип записи : REACTION (type=2, ver=1)
|
|
||||||
ожидаемая линия : 2
|
|
||||||
subType : %s
|
|
||||||
целевой блокчейн : "%s"
|
|
||||||
globalNumber цели : %d
|
|
||||||
hash цели (hex) : %s
|
|
||||||
}
|
|
||||||
""".formatted(
|
|
||||||
st,
|
|
||||||
toBlockchainName,
|
|
||||||
toBlockGlobalNumber,
|
|
||||||
toBlockHashHex()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public String toBlockHashHex() {
|
|
||||||
char[] HEX = "0123456789abcdef".toCharArray();
|
|
||||||
char[] out = new char[64];
|
|
||||||
for (int i = 0; i < 32; i++) {
|
|
||||||
int v = toBlockHash32[i] & 0xFF;
|
|
||||||
out[i * 2] = HEX[v >>> 4];
|
|
||||||
out[i * 2 + 1] = HEX[v & 0x0F];
|
|
||||||
}
|
|
||||||
return new String(out);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ===================================================================== */
|
|
||||||
/* ====================== BodyHasTarget контракт ========================= */
|
|
||||||
/* ===================================================================== */
|
|
||||||
|
|
||||||
/** В самом формате ReactionBody login цели не хранится => null. */
|
|
||||||
@Override public String toLogin() { return null; }
|
@Override public String toLogin() { return null; }
|
||||||
|
|
||||||
@Override public String toBchName() { return toBlockchainName; }
|
@Override public String toBchName() { return toBlockchainName; }
|
||||||
|
|||||||
@ -1,6 +1,9 @@
|
|||||||
|
// =======================
|
||||||
|
// blockchain/body/TextBody.java (ИЗМЕНЁННЫЙ: header содержит type/subType/version, body содержит line fields)
|
||||||
|
// =======================
|
||||||
package blockchain.body;
|
package blockchain.body;
|
||||||
|
|
||||||
import blockchain.LineIndex;
|
import shine.db.MsgSubType;
|
||||||
|
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.nio.ByteOrder;
|
import java.nio.ByteOrder;
|
||||||
@ -11,98 +14,87 @@ import java.util.Arrays;
|
|||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TextBody — type=1, ver=1.
|
* TextBody — type=1, ver=1 (в заголовке блока).
|
||||||
*
|
*
|
||||||
* Формат bodyBytes (BigEndian):
|
* subType (в заголовке блока):
|
||||||
* [2] type=1
|
* 1 = NEW
|
||||||
* [2] ver=1
|
* 2 = REPLY
|
||||||
|
* 3 = REPOST
|
||||||
|
* 10 = EDIT
|
||||||
*
|
*
|
||||||
* [2] subType (uint16): подтип текстового сообщения
|
* bodyBytes (BigEndian), новый формат:
|
||||||
* 1 = новое сообщение (начало ветки)
|
* [4] prevLineNumber
|
||||||
* 2 = ответ на сообщение (reply)
|
* [32] prevLineHash32
|
||||||
* 3 = репост (repost)
|
* [4] thisLineNumber
|
||||||
* 10 = редактирование (edit) <-- ВАЖНО: как на сервере/в БД-триггере
|
|
||||||
*
|
*
|
||||||
* [2] textLenBytes (uint16) — длина текста в байтах UTF-8
|
* [2] textLenBytes (uint16)
|
||||||
* [N] text UTF-8
|
* [N] text UTF-8
|
||||||
*
|
*
|
||||||
* Далее ТОЛЬКО если subType == 2 или subType == 3 или subType == 10:
|
* Далее ТОЛЬКО если subType == REPLY/REPOST/EDIT:
|
||||||
* [1] toBlockchainNameLen (uint8)
|
* [1] toBlockchainNameLen (uint8)
|
||||||
* [N] toBlockchainName UTF-8
|
* [N] toBlockchainName UTF-8
|
||||||
* [4] toBlockGlobalNumber (int32)
|
* [4] toBlockGlobalNumber (int32)
|
||||||
* [32] toBlockHash32 (raw 32 bytes)
|
* [32] toBlockHash32 (raw 32 bytes)
|
||||||
*
|
|
||||||
* ЛИНИЯ:
|
|
||||||
* - строго lineIndex=1
|
|
||||||
*/
|
*/
|
||||||
public final class TextBody implements BodyRecord, BodyHasTarget {
|
public final class TextBody implements BodyRecord, BodyHasTarget, BodyHasLine {
|
||||||
|
|
||||||
public static final short TYPE = 1;
|
public static final short TYPE = 1;
|
||||||
public static final short VER = 1;
|
public static final short VER = 1;
|
||||||
|
|
||||||
public static final int KEY = ((TYPE & 0xFFFF) << 16) | (VER & 0xFFFF);
|
public static final int KEY = ((TYPE & 0xFFFF) << 16) | (VER & 0xFFFF);
|
||||||
|
|
||||||
// subType:
|
public final short subType; // из header
|
||||||
public static final short SUB_NEW = 1;
|
public final short version; // из header
|
||||||
public static final short SUB_REPLY = 2;
|
|
||||||
public static final short SUB_REPOST = 3;
|
|
||||||
|
|
||||||
/** ВАЖНО: EDIT как на сервере (и как ожидает trg_blocks_edit_apply_ai). */
|
// линейные поля
|
||||||
public static final short SUB_EDIT = 10;
|
public final int prevLineNumber;
|
||||||
|
public final byte[] prevLineHash32; // 32
|
||||||
|
public final int thisLineNumber;
|
||||||
|
|
||||||
/** Подтип текстового сообщения (1/2/3/10). */
|
// payload
|
||||||
public final short subType;
|
|
||||||
|
|
||||||
/** Текст сообщения (строго валидный UTF-8, не пустой/не blank). */
|
|
||||||
public final String message;
|
public final String message;
|
||||||
|
|
||||||
// Заполняются только если subType == SUB_REPLY || SUB_REPOST || SUB_EDIT
|
// target (только для reply/repost/edit)
|
||||||
public final String toBlockchainName;
|
public final String toBlockchainName;
|
||||||
public final int toBlockGlobalNumber;
|
public final int toBlockGlobalNumber;
|
||||||
public final byte[] toBlockHash32;
|
public final byte[] toBlockHash32;
|
||||||
|
|
||||||
/* ===================================================================== */
|
public TextBody(short subType, short version, byte[] bodyBytes) {
|
||||||
/* ====================== Конструктор из байт =========================== */
|
|
||||||
/* ===================================================================== */
|
|
||||||
|
|
||||||
/** Десериализация из полного bodyBytes (включая type/version). */
|
|
||||||
public TextBody(byte[] bodyBytes) {
|
|
||||||
Objects.requireNonNull(bodyBytes, "bodyBytes == null");
|
Objects.requireNonNull(bodyBytes, "bodyBytes == null");
|
||||||
|
|
||||||
// минимум: type+ver (4) + subType(2) + textLen(2)
|
this.subType = subType;
|
||||||
if (bodyBytes.length < 4 + 2 + 2) {
|
this.version = version;
|
||||||
|
|
||||||
|
if ((this.version & 0xFFFF) != (VER & 0xFFFF)) {
|
||||||
|
throw new IllegalArgumentException("TextBody version must be 1, got=" + (this.version & 0xFFFF));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isValidSubType(this.subType)) {
|
||||||
|
throw new IllegalArgumentException("Bad Text subType: " + (this.subType & 0xFFFF));
|
||||||
|
}
|
||||||
|
|
||||||
|
// минимум: line(4+32+4) + textLen(2)
|
||||||
|
if (bodyBytes.length < 4 + 32 + 4 + 2) {
|
||||||
throw new IllegalArgumentException("TextBody too short");
|
throw new IllegalArgumentException("TextBody too short");
|
||||||
}
|
}
|
||||||
|
|
||||||
ByteBuffer bb = ByteBuffer.wrap(bodyBytes).order(ByteOrder.BIG_ENDIAN);
|
ByteBuffer bb = ByteBuffer.wrap(bodyBytes).order(ByteOrder.BIG_ENDIAN);
|
||||||
|
|
||||||
short type = bb.getShort();
|
this.prevLineNumber = bb.getInt();
|
||||||
short ver = bb.getShort();
|
|
||||||
if (type != TYPE || ver != VER) {
|
|
||||||
throw new IllegalArgumentException("Not TextBody: type=" + type + " ver=" + ver);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.subType = bb.getShort();
|
this.prevLineHash32 = new byte[32];
|
||||||
if (this.subType != SUB_NEW
|
bb.get(this.prevLineHash32);
|
||||||
&& this.subType != SUB_REPLY
|
|
||||||
&& this.subType != SUB_REPOST
|
this.thisLineNumber = bb.getInt();
|
||||||
&& this.subType != SUB_EDIT) {
|
|
||||||
throw new IllegalArgumentException("Bad subType: " + (this.subType & 0xFFFF));
|
|
||||||
}
|
|
||||||
|
|
||||||
int textLen = Short.toUnsignedInt(bb.getShort());
|
int textLen = Short.toUnsignedInt(bb.getShort());
|
||||||
if (textLen <= 0) {
|
if (textLen <= 0) throw new IllegalArgumentException("Text payload is empty");
|
||||||
throw new IllegalArgumentException("Text payload is empty");
|
if (bb.remaining() < textLen) throw new IllegalArgumentException("Text payload too short (len=" + textLen + ")");
|
||||||
}
|
|
||||||
if (bb.remaining() < textLen) {
|
|
||||||
throw new IllegalArgumentException("Text payload too short (len=" + textLen + ")");
|
|
||||||
}
|
|
||||||
|
|
||||||
byte[] textBytes = new byte[textLen];
|
byte[] textBytes = new byte[textLen];
|
||||||
bb.get(textBytes);
|
bb.get(textBytes);
|
||||||
|
|
||||||
var decoder = StandardCharsets.UTF_8
|
var decoder = StandardCharsets.UTF_8.newDecoder()
|
||||||
.newDecoder()
|
|
||||||
.onMalformedInput(CodingErrorAction.REPORT)
|
.onMalformedInput(CodingErrorAction.REPORT)
|
||||||
.onUnmappableCharacter(CodingErrorAction.REPORT);
|
.onUnmappableCharacter(CodingErrorAction.REPORT);
|
||||||
|
|
||||||
@ -112,22 +104,16 @@ public final class TextBody implements BodyRecord, BodyHasTarget {
|
|||||||
throw new IllegalArgumentException("Text payload is not valid UTF-8", e);
|
throw new IllegalArgumentException("Text payload is not valid UTF-8", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.message.isBlank()) {
|
if (this.message.isBlank()) throw new IllegalArgumentException("Text message is blank");
|
||||||
throw new IllegalArgumentException("Text message is blank");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Поля ссылки — только для reply/repost/edit
|
// target only for reply/repost/edit
|
||||||
if (this.subType == SUB_REPLY || this.subType == SUB_REPOST || this.subType == SUB_EDIT) {
|
if (isHasTargetSubType(this.subType)) {
|
||||||
|
if (bb.remaining() < 1) throw new IllegalArgumentException("Missing toBlockchainNameLen");
|
||||||
if (bb.remaining() < 1) {
|
|
||||||
throw new IllegalArgumentException("Missing toBlockchainNameLen");
|
|
||||||
}
|
|
||||||
|
|
||||||
int nameLen = Byte.toUnsignedInt(bb.get());
|
int nameLen = Byte.toUnsignedInt(bb.get());
|
||||||
if (nameLen <= 0) throw new IllegalArgumentException("toBlockchainNameLen is 0");
|
if (nameLen <= 0) throw new IllegalArgumentException("toBlockchainNameLen is 0");
|
||||||
if (bb.remaining() < nameLen + 4 + 32) {
|
if (bb.remaining() < nameLen + 4 + 32)
|
||||||
throw new IllegalArgumentException("Reply/Repost/Edit payload too short");
|
throw new IllegalArgumentException("Reply/Repost/Edit payload too short");
|
||||||
}
|
|
||||||
|
|
||||||
byte[] nameBytes = new byte[nameLen];
|
byte[] nameBytes = new byte[nameLen];
|
||||||
bb.get(nameBytes);
|
bb.get(nameBytes);
|
||||||
@ -138,110 +124,92 @@ public final class TextBody implements BodyRecord, BodyHasTarget {
|
|||||||
this.toBlockHash32 = new byte[32];
|
this.toBlockHash32 = new byte[32];
|
||||||
bb.get(this.toBlockHash32);
|
bb.get(this.toBlockHash32);
|
||||||
|
|
||||||
// Запрет мусора в конце
|
if (bb.remaining() != 0) throw new IllegalArgumentException("Unexpected tail bytes, remaining=" + bb.remaining());
|
||||||
if (bb.remaining() != 0) {
|
|
||||||
throw new IllegalArgumentException("Unexpected tail bytes, remaining=" + bb.remaining());
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// SUB_NEW
|
|
||||||
this.toBlockchainName = null;
|
this.toBlockchainName = null;
|
||||||
this.toBlockGlobalNumber = 0;
|
this.toBlockGlobalNumber = 0;
|
||||||
this.toBlockHash32 = null;
|
this.toBlockHash32 = null;
|
||||||
|
|
||||||
// если кто-то подсунул хвост — лучше упасть, чтобы формат не “плыл”
|
if (bb.remaining() != 0) throw new IllegalArgumentException("Unexpected tail for subType=NEW, remaining=" + bb.remaining());
|
||||||
if (bb.remaining() != 0) {
|
|
||||||
throw new IllegalArgumentException("Unexpected tail for subType=NEW, remaining=" + bb.remaining());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===================================================================== */
|
public TextBody(int prevLineNumber,
|
||||||
/* ====================== Конструкторы “для тестов” ====================== */
|
byte[] prevLineHash32,
|
||||||
/* ===================================================================== */
|
int thisLineNumber,
|
||||||
|
short subType,
|
||||||
public TextBody(String message) {
|
|
||||||
this(SUB_NEW, message);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Сообщение subType=NEW (1). */
|
|
||||||
public TextBody(short subType, String message) {
|
|
||||||
Objects.requireNonNull(message, "message == null");
|
|
||||||
|
|
||||||
if (subType != SUB_NEW) {
|
|
||||||
throw new IllegalArgumentException("This constructor is only for SUB_NEW");
|
|
||||||
}
|
|
||||||
if (message.isBlank()) {
|
|
||||||
throw new IllegalArgumentException("message is blank");
|
|
||||||
}
|
|
||||||
|
|
||||||
this.subType = subType;
|
|
||||||
this.message = message;
|
|
||||||
|
|
||||||
this.toBlockchainName = null;
|
|
||||||
this.toBlockGlobalNumber = 0;
|
|
||||||
this.toBlockHash32 = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Сообщение subType=REPLY (2) или subType=REPOST (3) или subType=EDIT (10) со ссылкой на блок. */
|
|
||||||
public TextBody(short subType,
|
|
||||||
String message,
|
String message,
|
||||||
String toBlockchainName,
|
String toBlockchainName,
|
||||||
int toBlockGlobalNumber,
|
Integer toBlockGlobalNumber,
|
||||||
byte[] toBlockHash32) {
|
byte[] toBlockHash32) {
|
||||||
|
|
||||||
Objects.requireNonNull(message, "message == null");
|
Objects.requireNonNull(message, "message == null");
|
||||||
Objects.requireNonNull(toBlockchainName, "toBlockchainName == null");
|
if (!isValidSubType(subType)) throw new IllegalArgumentException("Bad Text subType: " + (subType & 0xFFFF));
|
||||||
Objects.requireNonNull(toBlockHash32, "toBlockHash32 == null");
|
|
||||||
|
|
||||||
if (subType != SUB_REPLY && subType != SUB_REPOST && subType != SUB_EDIT) {
|
|
||||||
throw new IllegalArgumentException("subType must be SUB_REPLY or SUB_REPOST or SUB_EDIT for this constructor");
|
|
||||||
}
|
|
||||||
if (message.isBlank()) throw new IllegalArgumentException("message is blank");
|
if (message.isBlank()) throw new IllegalArgumentException("message is blank");
|
||||||
|
|
||||||
|
this.prevLineNumber = prevLineNumber;
|
||||||
|
this.prevLineHash32 = (prevLineHash32 == null ? new byte[32] : Arrays.copyOf(prevLineHash32, 32));
|
||||||
|
this.thisLineNumber = thisLineNumber;
|
||||||
|
|
||||||
|
this.subType = subType;
|
||||||
|
this.version = VER;
|
||||||
|
|
||||||
|
this.message = message;
|
||||||
|
|
||||||
|
if (isHasTargetSubType(subType)) {
|
||||||
|
Objects.requireNonNull(toBlockchainName, "toBlockchainName == null");
|
||||||
|
Objects.requireNonNull(toBlockGlobalNumber, "toBlockGlobalNumber == null");
|
||||||
|
Objects.requireNonNull(toBlockHash32, "toBlockHash32 == null");
|
||||||
if (toBlockchainName.isBlank()) throw new IllegalArgumentException("toBlockchainName is blank");
|
if (toBlockchainName.isBlank()) throw new IllegalArgumentException("toBlockchainName is blank");
|
||||||
if (toBlockGlobalNumber < 0) throw new IllegalArgumentException("toBlockGlobalNumber < 0");
|
if (toBlockGlobalNumber < 0) throw new IllegalArgumentException("toBlockGlobalNumber < 0");
|
||||||
if (toBlockHash32.length != 32) throw new IllegalArgumentException("toBlockHash32 != 32");
|
if (toBlockHash32.length != 32) throw new IllegalArgumentException("toBlockHash32 != 32");
|
||||||
|
|
||||||
this.subType = subType;
|
|
||||||
this.message = message;
|
|
||||||
this.toBlockchainName = toBlockchainName;
|
this.toBlockchainName = toBlockchainName;
|
||||||
this.toBlockGlobalNumber = toBlockGlobalNumber;
|
this.toBlockGlobalNumber = toBlockGlobalNumber;
|
||||||
this.toBlockHash32 = Arrays.copyOf(toBlockHash32, 32);
|
this.toBlockHash32 = Arrays.copyOf(toBlockHash32, 32);
|
||||||
|
} else {
|
||||||
|
this.toBlockchainName = null;
|
||||||
|
this.toBlockGlobalNumber = 0;
|
||||||
|
this.toBlockHash32 = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===================================================================== */
|
private static boolean isValidSubType(short st) {
|
||||||
/* ====================== BodyRecord контракт =========================== */
|
int v = st & 0xFFFF;
|
||||||
/* ===================================================================== */
|
return v == (MsgSubType.TEXT_NEW & 0xFFFF)
|
||||||
|
|| v == (MsgSubType.TEXT_REPLY & 0xFFFF)
|
||||||
|
|| v == (MsgSubType.TEXT_REPOST & 0xFFFF)
|
||||||
|
|| v == (MsgSubType.TEXT_EDIT & 0xFFFF);
|
||||||
|
}
|
||||||
|
|
||||||
@Override public short type() { return TYPE; }
|
private static boolean isHasTargetSubType(short st) {
|
||||||
@Override public short version() { return VER; }
|
int v = st & 0xFFFF;
|
||||||
@Override public short subType() { return subType; }
|
return v == (MsgSubType.TEXT_REPLY & 0xFFFF)
|
||||||
|
|| v == (MsgSubType.TEXT_REPOST & 0xFFFF)
|
||||||
@Override
|
|| v == (MsgSubType.TEXT_EDIT & 0xFFFF);
|
||||||
public short expectedLineIndex() {
|
|
||||||
return LineIndex.TEXT;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public TextBody check() {
|
public TextBody check() {
|
||||||
if (subType != SUB_NEW && subType != SUB_REPLY && subType != SUB_REPOST && subType != SUB_EDIT) {
|
if (!isValidSubType(subType)) throw new IllegalArgumentException("Bad Text subType: " + (subType & 0xFFFF));
|
||||||
throw new IllegalArgumentException("Bad subType: " + (subType & 0xFFFF));
|
if (message == null || message.isBlank()) throw new IllegalArgumentException("Text message is blank");
|
||||||
}
|
|
||||||
|
|
||||||
if (message == null || message.isBlank()) {
|
// line fields rule:
|
||||||
throw new IllegalArgumentException("Text message is blank");
|
if (prevLineNumber == -1) {
|
||||||
}
|
if (!isAllZero32(prevLineHash32)) throw new IllegalArgumentException("prevLineHash32 must be zero when prevLineNumber=-1");
|
||||||
|
if (thisLineNumber != -1) throw new IllegalArgumentException("thisLineNumber must be -1 when prevLineNumber=-1");
|
||||||
if (subType == SUB_REPLY || subType == SUB_REPOST || subType == SUB_EDIT) {
|
|
||||||
if (toBlockchainName == null || toBlockchainName.isBlank())
|
|
||||||
throw new IllegalArgumentException("toBlockchainName is blank");
|
|
||||||
if (toBlockGlobalNumber < 0)
|
|
||||||
throw new IllegalArgumentException("toBlockGlobalNumber < 0");
|
|
||||||
if (toBlockHash32 == null || toBlockHash32.length != 32)
|
|
||||||
throw new IllegalArgumentException("toBlockHash32 invalid");
|
|
||||||
} else {
|
} else {
|
||||||
if (toBlockchainName != null) throw new IllegalArgumentException("toBlockchainName must be null for SUB_NEW");
|
if (prevLineHash32 == null || prevLineHash32.length != 32) throw new IllegalArgumentException("prevLineHash32 invalid");
|
||||||
if (toBlockHash32 != null) throw new IllegalArgumentException("toBlockHash32 must be null for SUB_NEW");
|
// thisLineNumber сервер пока не проверяет (принимаем как есть)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isHasTargetSubType(subType)) {
|
||||||
|
if (toBlockchainName == null || toBlockchainName.isBlank()) throw new IllegalArgumentException("toBlockchainName is blank");
|
||||||
|
if (toBlockGlobalNumber < 0) throw new IllegalArgumentException("toBlockGlobalNumber < 0");
|
||||||
|
if (toBlockHash32 == null || toBlockHash32.length != 32) throw new IllegalArgumentException("toBlockHash32 invalid");
|
||||||
|
} else {
|
||||||
|
if (toBlockchainName != null || toBlockHash32 != null) throw new IllegalArgumentException("SUB_NEW must not contain target fields");
|
||||||
}
|
}
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
@ -250,46 +218,34 @@ public final class TextBody implements BodyRecord, BodyHasTarget {
|
|||||||
@Override
|
@Override
|
||||||
public byte[] toBytes() {
|
public byte[] toBytes() {
|
||||||
byte[] msgUtf8 = message.getBytes(StandardCharsets.UTF_8);
|
byte[] msgUtf8 = message.getBytes(StandardCharsets.UTF_8);
|
||||||
if (msgUtf8.length == 0) {
|
if (msgUtf8.length == 0) throw new IllegalArgumentException("Text payload is empty");
|
||||||
throw new IllegalArgumentException("Text payload is empty");
|
if (msgUtf8.length > 65535) throw new IllegalArgumentException("Text too long (>65535 bytes)");
|
||||||
}
|
|
||||||
if (msgUtf8.length > 65535) {
|
|
||||||
throw new IllegalArgumentException("Text too long (>65535 bytes)");
|
|
||||||
}
|
|
||||||
|
|
||||||
// base: type+ver + subType + textLen + textBytes
|
int cap = 4 + 32 + 4 // line fields
|
||||||
int cap = 4 + 2 + 2 + msgUtf8.length;
|
+ 2 + msgUtf8.length; // text
|
||||||
|
|
||||||
byte[] nameBytes = null;
|
byte[] nameBytes = null;
|
||||||
|
|
||||||
if (subType == SUB_REPLY || subType == SUB_REPOST || subType == SUB_EDIT) {
|
if (isHasTargetSubType(subType)) {
|
||||||
nameBytes = toBlockchainName.getBytes(StandardCharsets.UTF_8);
|
nameBytes = toBlockchainName.getBytes(StandardCharsets.UTF_8);
|
||||||
if (nameBytes.length == 0 || nameBytes.length > 255) {
|
if (nameBytes.length == 0 || nameBytes.length > 255)
|
||||||
throw new IllegalArgumentException("toBlockchainName utf8 len must be 1..255");
|
throw new IllegalArgumentException("toBlockchainName utf8 len must be 1..255");
|
||||||
}
|
if (toBlockHash32 == null || toBlockHash32.length != 32)
|
||||||
if (toBlockHash32 == null || toBlockHash32.length != 32) {
|
|
||||||
throw new IllegalArgumentException("toBlockHash32 != 32");
|
throw new IllegalArgumentException("toBlockHash32 != 32");
|
||||||
}
|
|
||||||
|
|
||||||
cap += 1 + nameBytes.length + 4 + 32;
|
cap += 1 + nameBytes.length + 4 + 32;
|
||||||
|
|
||||||
} else {
|
|
||||||
if (toBlockchainName != null || toBlockHash32 != null) {
|
|
||||||
throw new IllegalArgumentException("SUB_NEW must not contain reply/repost/edit fields");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ByteBuffer bb = ByteBuffer.allocate(cap).order(ByteOrder.BIG_ENDIAN);
|
ByteBuffer bb = ByteBuffer.allocate(cap).order(ByteOrder.BIG_ENDIAN);
|
||||||
|
|
||||||
bb.putShort(TYPE);
|
bb.putInt(prevLineNumber);
|
||||||
bb.putShort(VER);
|
bb.put(prevLineHash32 == null ? new byte[32] : Arrays.copyOf(prevLineHash32, 32));
|
||||||
|
bb.putInt(thisLineNumber);
|
||||||
bb.putShort(subType);
|
|
||||||
|
|
||||||
bb.putShort((short) msgUtf8.length);
|
bb.putShort((short) msgUtf8.length);
|
||||||
bb.put(msgUtf8);
|
bb.put(msgUtf8);
|
||||||
|
|
||||||
if (subType == SUB_REPLY || subType == SUB_REPOST || subType == SUB_EDIT) {
|
if (isHasTargetSubType(subType)) {
|
||||||
bb.put((byte) nameBytes.length);
|
bb.put((byte) nameBytes.length);
|
||||||
bb.put(nameBytes);
|
bb.put(nameBytes);
|
||||||
bb.putInt(toBlockGlobalNumber);
|
bb.putInt(toBlockGlobalNumber);
|
||||||
@ -299,83 +255,32 @@ public final class TextBody implements BodyRecord, BodyHasTarget {
|
|||||||
return bb.array();
|
return bb.array();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
private static boolean isAllZero32(byte[] b) {
|
||||||
public String toString() {
|
if (b == null || b.length != 32) return true;
|
||||||
String st = switch (subType) {
|
for (int i = 0; i < 32; i++) if (b[i] != 0) return false;
|
||||||
case SUB_NEW -> "NEW (1)";
|
return true;
|
||||||
case SUB_REPLY -> "REPLY (2)";
|
|
||||||
case SUB_REPOST -> "REPOST (3)";
|
|
||||||
case SUB_EDIT -> "EDIT (10)";
|
|
||||||
default -> "UNKNOWN";
|
|
||||||
};
|
|
||||||
|
|
||||||
if (subType == SUB_REPLY || subType == SUB_REPOST || subType == SUB_EDIT) {
|
|
||||||
return """
|
|
||||||
TextBody {
|
|
||||||
тип записи : TEXT (type=1, ver=1)
|
|
||||||
ожидаемая линия : 1
|
|
||||||
subType : %s
|
|
||||||
длина сообщения : %d байт
|
|
||||||
текст сообщения : "%s"
|
|
||||||
ссылка на блок : "%s" #%d
|
|
||||||
hash цели (hex) : %s
|
|
||||||
}
|
|
||||||
""".formatted(
|
|
||||||
st,
|
|
||||||
message.getBytes(StandardCharsets.UTF_8).length,
|
|
||||||
message,
|
|
||||||
toBlockchainName,
|
|
||||||
toBlockGlobalNumber,
|
|
||||||
toBlockHashHex()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return """
|
/* ====================== BodyHasLine ====================== */
|
||||||
TextBody {
|
@Override public int prevLineNumber() { return prevLineNumber; }
|
||||||
тип записи : TEXT (type=1, ver=1)
|
@Override public byte[] prevLineHash32() { return prevLineHash32 == null ? null : Arrays.copyOf(prevLineHash32, 32); }
|
||||||
ожидаемая линия : 1
|
@Override public int thisLineNumber() { return thisLineNumber; }
|
||||||
subType : %s
|
|
||||||
длина сообщения : %d байт
|
|
||||||
текст сообщения : "%s"
|
|
||||||
}
|
|
||||||
""".formatted(
|
|
||||||
st,
|
|
||||||
message.getBytes(StandardCharsets.UTF_8).length,
|
|
||||||
message
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public String toBlockHashHex() {
|
/* ====================== BodyHasTarget ===================== */
|
||||||
if (toBlockHash32 == null) return "null";
|
|
||||||
char[] HEX = "0123456789abcdef".toCharArray();
|
|
||||||
char[] out = new char[64];
|
|
||||||
for (int i = 0; i < 32; i++) {
|
|
||||||
int v = toBlockHash32[i] & 0xFF;
|
|
||||||
out[i * 2] = HEX[v >>> 4];
|
|
||||||
out[i * 2 + 1] = HEX[v & 0x0F];
|
|
||||||
}
|
|
||||||
return new String(out);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ===================================================================== */
|
|
||||||
/* ====================== BodyHasTarget контракт ========================= */
|
|
||||||
/* ===================================================================== */
|
|
||||||
|
|
||||||
/** В формате TextBody login цели не хранится => null. */
|
|
||||||
@Override public String toLogin() { return null; }
|
@Override public String toLogin() { return null; }
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toBchName() {
|
public String toBchName() {
|
||||||
return (subType == SUB_REPLY || subType == SUB_REPOST || subType == SUB_EDIT) ? toBlockchainName : null;
|
return isHasTargetSubType(subType) ? toBlockchainName : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Integer toBlockGlobalNumber() {
|
public Integer toBlockGlobalNumber() {
|
||||||
return (subType == SUB_REPLY || subType == SUB_REPOST || subType == SUB_EDIT) ? toBlockGlobalNumber : null;
|
return isHasTargetSubType(subType) ? toBlockGlobalNumber : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public byte[] toBlockHasheBytes() {
|
public byte[] toBlockHasheBytes() {
|
||||||
return (subType == SUB_REPLY || subType == SUB_REPOST || subType == SUB_EDIT) ? toBlockHash32 : null;
|
return isHasTargetSubType(subType) ? toBlockHash32 : null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,87 +1,79 @@
|
|||||||
|
// =======================
|
||||||
|
// blockchain/body/UserParamBody.java (ИЗМЕНЁННЫЙ: bodyBytes без type/subType/version, + line fields)
|
||||||
|
// =======================
|
||||||
package blockchain.body;
|
package blockchain.body;
|
||||||
|
|
||||||
import blockchain.LineIndex;
|
import shine.db.MsgSubType;
|
||||||
|
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.nio.ByteOrder;
|
import java.nio.ByteOrder;
|
||||||
import java.nio.charset.CharacterCodingException;
|
import java.nio.charset.CharacterCodingException;
|
||||||
import java.nio.charset.CodingErrorAction;
|
import java.nio.charset.CodingErrorAction;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* UserParamBody — type=4, ver=1. (Параметр профиля / данные пользователя о себе)
|
* UserParamBody — type=4, ver=1 (в заголовке блока).
|
||||||
*
|
*
|
||||||
* Идея:
|
* subType (в заголовке блока):
|
||||||
* - Это "пользователь сам заявил параметр X со значением Y".
|
* 1 = TEXT_TEXT
|
||||||
* - Один блок = один параметр (одна пара key/value).
|
|
||||||
* (Если нужно больше параметров — просто добавляешь несколько блоков подряд).
|
|
||||||
*
|
*
|
||||||
* Формат bodyBytes (BigEndian):
|
* bodyBytes (BigEndian), новый формат:
|
||||||
* [2] type=4
|
* [4] prevLineNumber
|
||||||
* [2] ver=1
|
* [32] prevLineHash32
|
||||||
|
* [4] thisLineNumber
|
||||||
*
|
*
|
||||||
* [2] subType (uint16)
|
* [2] keyLenBytes (uint16)
|
||||||
* 1 = TEXT_TEXT (ключ-значение, обе строки UTF-8)
|
|
||||||
*
|
|
||||||
* [2] keyLenBytes (uint16) — длина ключа в байтах UTF-8
|
|
||||||
* [N] keyUtf8
|
* [N] keyUtf8
|
||||||
*
|
*
|
||||||
* [2] valueLenBytes (uint16) — длина значения в байтах UTF-8
|
* [2] valueLenBytes (uint16)
|
||||||
* [M] valueUtf8
|
* [M] valueUtf8
|
||||||
*
|
|
||||||
* ВАЖНО:
|
|
||||||
* - длины именно В БАЙТАХ UTF-8 (не в символах)
|
|
||||||
* - ключ и значение обязаны быть валидным UTF-8
|
|
||||||
* - ключ запрещаем пустым/blank (иначе нельзя идентифицировать параметр)
|
|
||||||
* - значение может быть пустым? (реши сам)
|
|
||||||
* сейчас: запрещаем пустое (len>0) и запрещаем blank, чтобы не мусорить цепочку
|
|
||||||
*
|
|
||||||
* ЛИНИЯ:
|
|
||||||
* - строго lineIndex=4 (выделенная линия под пользовательские параметры/профиль).
|
|
||||||
*/
|
*/
|
||||||
public final class UserParamBody implements BodyRecord {
|
public final class UserParamBody implements BodyRecord, BodyHasLine {
|
||||||
|
|
||||||
public static final short TYPE = 4;
|
public static final short TYPE = 4;
|
||||||
public static final short VER = 1;
|
public static final short VER = 1;
|
||||||
|
|
||||||
public static final int KEY = ((TYPE & 0xFFFF) << 16) | (VER & 0xFFFF);
|
public static final int KEY = ((TYPE & 0xFFFF) << 16) | (VER & 0xFFFF);
|
||||||
|
|
||||||
// subType:
|
public final short subType; // из header
|
||||||
public static final short SUB_TEXT_TEXT = 1;
|
public final short version; // из header
|
||||||
|
|
||||||
public final short subType;
|
// line
|
||||||
|
public final int prevLineNumber;
|
||||||
|
public final byte[] prevLineHash32;
|
||||||
|
public final int thisLineNumber;
|
||||||
|
|
||||||
/** Название параметра (пример: "firstName", "lastName", "address", "about"). */
|
|
||||||
public final String paramKey;
|
public final String paramKey;
|
||||||
|
|
||||||
/** Значение параметра (пример: "Aidar", "Gareev", "..."). */
|
|
||||||
public final String paramValue;
|
public final String paramValue;
|
||||||
|
|
||||||
/* ===================================================================== */
|
public UserParamBody(short subType, short version, byte[] bodyBytes) {
|
||||||
/* ====================== Конструктор из байт =========================== */
|
|
||||||
/* ===================================================================== */
|
|
||||||
|
|
||||||
public UserParamBody(byte[] bodyBytes) {
|
|
||||||
Objects.requireNonNull(bodyBytes, "bodyBytes == null");
|
Objects.requireNonNull(bodyBytes, "bodyBytes == null");
|
||||||
|
|
||||||
// минимум: type[2]+ver[2]+subType[2]+keyLen[2]+key[1]+valLen[2]+val[1]
|
this.subType = subType;
|
||||||
if (bodyBytes.length < 2 + 2 + 2 + 2 + 1 + 2 + 1) {
|
this.version = version;
|
||||||
|
|
||||||
|
if ((this.version & 0xFFFF) != (VER & 0xFFFF)) {
|
||||||
|
throw new IllegalArgumentException("UserParamBody version must be 1, got=" + (this.version & 0xFFFF));
|
||||||
|
}
|
||||||
|
if ((this.subType & 0xFFFF) != (MsgSubType.USER_PARAM_TEXT_TEXT & 0xFFFF)) {
|
||||||
|
throw new IllegalArgumentException("Bad UserParam subType: " + (this.subType & 0xFFFF));
|
||||||
|
}
|
||||||
|
|
||||||
|
// минимум: line(4+32+4) + keyLen(2)+key(1) + valLen(2)+val(1)
|
||||||
|
if (bodyBytes.length < (4 + 32 + 4) + 2 + 1 + 2 + 1) {
|
||||||
throw new IllegalArgumentException("UserParamBody too short");
|
throw new IllegalArgumentException("UserParamBody too short");
|
||||||
}
|
}
|
||||||
|
|
||||||
ByteBuffer bb = ByteBuffer.wrap(bodyBytes).order(ByteOrder.BIG_ENDIAN);
|
ByteBuffer bb = ByteBuffer.wrap(bodyBytes).order(ByteOrder.BIG_ENDIAN);
|
||||||
|
|
||||||
short type = bb.getShort();
|
this.prevLineNumber = bb.getInt();
|
||||||
short ver = bb.getShort();
|
|
||||||
if (type != TYPE || ver != VER) {
|
|
||||||
throw new IllegalArgumentException("Not UserParamBody: type=" + type + " ver=" + ver);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.subType = bb.getShort();
|
this.prevLineHash32 = new byte[32];
|
||||||
if (this.subType != SUB_TEXT_TEXT) {
|
bb.get(this.prevLineHash32);
|
||||||
throw new IllegalArgumentException("Bad UserParam subType: " + (this.subType & 0xFFFF));
|
|
||||||
}
|
this.thisLineNumber = bb.getInt();
|
||||||
|
|
||||||
int keyLen = Short.toUnsignedInt(bb.getShort());
|
int keyLen = Short.toUnsignedInt(bb.getShort());
|
||||||
if (keyLen <= 0) throw new IllegalArgumentException("paramKeyLen is 0");
|
if (keyLen <= 0) throw new IllegalArgumentException("paramKeyLen is 0");
|
||||||
@ -97,31 +89,30 @@ public final class UserParamBody implements BodyRecord {
|
|||||||
byte[] valBytes = new byte[valLen];
|
byte[] valBytes = new byte[valLen];
|
||||||
bb.get(valBytes);
|
bb.get(valBytes);
|
||||||
|
|
||||||
// запрет мусора в конце
|
if (bb.remaining() != 0) throw new IllegalArgumentException("Unexpected tail bytes, remaining=" + bb.remaining());
|
||||||
if (bb.remaining() != 0) {
|
|
||||||
throw new IllegalArgumentException("Unexpected tail bytes, remaining=" + bb.remaining());
|
|
||||||
}
|
|
||||||
|
|
||||||
this.paramKey = strictUtf8(keyBytes, "paramKey");
|
this.paramKey = strictUtf8(keyBytes, "paramKey");
|
||||||
this.paramValue = strictUtf8(valBytes, "paramValue");
|
this.paramValue = strictUtf8(valBytes, "paramValue");
|
||||||
|
|
||||||
if (this.paramKey.isBlank()) {
|
if (this.paramKey.isBlank()) throw new IllegalArgumentException("paramKey is blank");
|
||||||
throw new IllegalArgumentException("paramKey is blank");
|
if (this.paramValue.isBlank()) throw new IllegalArgumentException("paramValue is blank");
|
||||||
}
|
|
||||||
if (this.paramValue.isBlank()) {
|
|
||||||
throw new IllegalArgumentException("paramValue is blank");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===================================================================== */
|
public UserParamBody(int prevLineNumber,
|
||||||
/* ====================== Конструктор “вручную” ========================= */
|
byte[] prevLineHash32,
|
||||||
/* ===================================================================== */
|
int thisLineNumber,
|
||||||
|
String paramKey,
|
||||||
|
String paramValue) {
|
||||||
|
|
||||||
public UserParamBody(String paramKey, String paramValue) {
|
|
||||||
Objects.requireNonNull(paramKey, "paramKey == null");
|
Objects.requireNonNull(paramKey, "paramKey == null");
|
||||||
Objects.requireNonNull(paramValue, "paramValue == null");
|
Objects.requireNonNull(paramValue, "paramValue == null");
|
||||||
|
|
||||||
this.subType = SUB_TEXT_TEXT;
|
this.subType = MsgSubType.USER_PARAM_TEXT_TEXT;
|
||||||
|
this.version = VER;
|
||||||
|
|
||||||
|
this.prevLineNumber = prevLineNumber;
|
||||||
|
this.prevLineHash32 = (prevLineHash32 == null ? new byte[32] : Arrays.copyOf(prevLineHash32, 32));
|
||||||
|
this.thisLineNumber = thisLineNumber;
|
||||||
|
|
||||||
if (paramKey.isBlank()) throw new IllegalArgumentException("paramKey is blank");
|
if (paramKey.isBlank()) throw new IllegalArgumentException("paramKey is blank");
|
||||||
if (paramValue.isBlank()) throw new IllegalArgumentException("paramValue is blank");
|
if (paramValue.isBlank()) throw new IllegalArgumentException("paramValue is blank");
|
||||||
@ -130,55 +121,41 @@ public final class UserParamBody implements BodyRecord {
|
|||||||
this.paramValue = paramValue;
|
this.paramValue = paramValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===================================================================== */
|
|
||||||
/* ====================== BodyRecord контракт =========================== */
|
|
||||||
/* ===================================================================== */
|
|
||||||
|
|
||||||
@Override public short type() { return TYPE; }
|
|
||||||
@Override public short version() { return VER; }
|
|
||||||
@Override public short subType() { return subType; }
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public short expectedLineIndex() {
|
|
||||||
return LineIndex.USER_PARAM;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public UserParamBody check() {
|
public UserParamBody check() {
|
||||||
if (subType != SUB_TEXT_TEXT)
|
if ((subType & 0xFFFF) != (MsgSubType.USER_PARAM_TEXT_TEXT & 0xFFFF))
|
||||||
throw new IllegalArgumentException("Bad UserParam subType: " + (subType & 0xFFFF));
|
throw new IllegalArgumentException("Bad UserParam subType: " + (subType & 0xFFFF));
|
||||||
|
|
||||||
if (paramKey == null || paramKey.isBlank())
|
if (prevLineNumber == -1) {
|
||||||
throw new IllegalArgumentException("paramKey is blank");
|
if (!isAllZero32(prevLineHash32)) throw new IllegalArgumentException("prevLineHash32 must be zero when prevLineNumber=-1");
|
||||||
if (paramValue == null || paramValue.isBlank())
|
if (thisLineNumber != -1) throw new IllegalArgumentException("thisLineNumber must be -1 when prevLineNumber=-1");
|
||||||
throw new IllegalArgumentException("paramValue is blank");
|
} else {
|
||||||
|
if (prevLineHash32 == null || prevLineHash32.length != 32) throw new IllegalArgumentException("prevLineHash32 invalid");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (paramKey == null || paramKey.isBlank()) throw new IllegalArgumentException("paramKey is blank");
|
||||||
|
if (paramValue == null || paramValue.isBlank()) throw new IllegalArgumentException("paramValue is blank");
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public byte[] toBytes() {
|
public byte[] toBytes() {
|
||||||
if (subType != SUB_TEXT_TEXT)
|
|
||||||
throw new IllegalArgumentException("Bad UserParam subType: " + (subType & 0xFFFF));
|
|
||||||
|
|
||||||
byte[] keyUtf8 = paramKey.getBytes(StandardCharsets.UTF_8);
|
byte[] keyUtf8 = paramKey.getBytes(StandardCharsets.UTF_8);
|
||||||
byte[] valUtf8 = paramValue.getBytes(StandardCharsets.UTF_8);
|
byte[] valUtf8 = paramValue.getBytes(StandardCharsets.UTF_8);
|
||||||
|
|
||||||
if (keyUtf8.length == 0) throw new IllegalArgumentException("paramKey utf8 len is 0");
|
if (keyUtf8.length == 0 || keyUtf8.length > 65535) throw new IllegalArgumentException("paramKey utf8 len must be 1..65535");
|
||||||
if (valUtf8.length == 0) throw new IllegalArgumentException("paramValue utf8 len is 0");
|
if (valUtf8.length == 0 || valUtf8.length > 65535) throw new IllegalArgumentException("paramValue utf8 len must be 1..65535");
|
||||||
|
|
||||||
if (keyUtf8.length > 65535) throw new IllegalArgumentException("paramKey too long (>65535 bytes)");
|
int cap = (4 + 32 + 4)
|
||||||
if (valUtf8.length > 65535) throw new IllegalArgumentException("paramValue too long (>65535 bytes)");
|
+ 2 + keyUtf8.length
|
||||||
|
+ 2 + valUtf8.length;
|
||||||
// type[2]+ver[2]+subType[2] + keyLen[2]+key[N] + valLen[2]+val[M]
|
|
||||||
int cap = 2 + 2 + 2 + 2 + keyUtf8.length + 2 + valUtf8.length;
|
|
||||||
|
|
||||||
ByteBuffer bb = ByteBuffer.allocate(cap).order(ByteOrder.BIG_ENDIAN);
|
ByteBuffer bb = ByteBuffer.allocate(cap).order(ByteOrder.BIG_ENDIAN);
|
||||||
|
|
||||||
bb.putShort(TYPE);
|
bb.putInt(prevLineNumber);
|
||||||
bb.putShort(VER);
|
bb.put(prevLineHash32 == null ? new byte[32] : Arrays.copyOf(prevLineHash32, 32));
|
||||||
|
bb.putInt(thisLineNumber);
|
||||||
bb.putShort(SUB_TEXT_TEXT);
|
|
||||||
|
|
||||||
bb.putShort((short) keyUtf8.length);
|
bb.putShort((short) keyUtf8.length);
|
||||||
bb.put(keyUtf8);
|
bb.put(keyUtf8);
|
||||||
@ -189,28 +166,8 @@ public final class UserParamBody implements BodyRecord {
|
|||||||
return bb.array();
|
return bb.array();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
String st = (subType == SUB_TEXT_TEXT) ? "TEXT_TEXT (1)" : "UNKNOWN";
|
|
||||||
|
|
||||||
return """
|
|
||||||
UserParamBody {
|
|
||||||
тип записи : USER_PARAM (type=4, ver=1)
|
|
||||||
ожидаемая линия : 4
|
|
||||||
subType : %s
|
|
||||||
paramKey : "%s"
|
|
||||||
paramValue : "%s"
|
|
||||||
}
|
|
||||||
""".formatted(st, paramKey, paramValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ===================================================================== */
|
|
||||||
/* =========================== Helpers ================================== */
|
|
||||||
/* ===================================================================== */
|
|
||||||
|
|
||||||
private static String strictUtf8(byte[] bytes, String fieldName) {
|
private static String strictUtf8(byte[] bytes, String fieldName) {
|
||||||
var decoder = StandardCharsets.UTF_8
|
var decoder = StandardCharsets.UTF_8.newDecoder()
|
||||||
.newDecoder()
|
|
||||||
.onMalformedInput(CodingErrorAction.REPORT)
|
.onMalformedInput(CodingErrorAction.REPORT)
|
||||||
.onUnmappableCharacter(CodingErrorAction.REPORT);
|
.onUnmappableCharacter(CodingErrorAction.REPORT);
|
||||||
|
|
||||||
@ -220,4 +177,15 @@ public final class UserParamBody implements BodyRecord {
|
|||||||
throw new IllegalArgumentException(fieldName + " is not valid UTF-8", e);
|
throw new IllegalArgumentException(fieldName + " is not valid UTF-8", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static boolean isAllZero32(byte[] b) {
|
||||||
|
if (b == null || b.length != 32) return true;
|
||||||
|
for (int i = 0; i < 32; i++) if (b[i] != 0) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ====================== BodyHasLine ====================== */
|
||||||
|
@Override public int prevLineNumber() { return prevLineNumber; }
|
||||||
|
@Override public byte[] prevLineHash32() { return prevLineHash32 == null ? null : Arrays.copyOf(prevLineHash32, 32); }
|
||||||
|
@Override public int thisLineNumber() { return thisLineNumber; }
|
||||||
}
|
}
|
||||||
@ -1,5 +1,5 @@
|
|||||||
// =======================
|
// =======================
|
||||||
// BlockchainStateDAO.java (НОВАЯ ВЕРСИЯ)
|
// shine/db/dao/BlockchainStateDAO.java (ИЗМЕНЁННАЯ: убраны line0..7, last_block_*)
|
||||||
// =======================
|
// =======================
|
||||||
package shine.db.dao;
|
package shine.db.dao;
|
||||||
|
|
||||||
@ -40,17 +40,9 @@ public final class BlockchainStateDAO {
|
|||||||
blockchain_key,
|
blockchain_key,
|
||||||
size_limit,
|
size_limit,
|
||||||
file_size_bytes,
|
file_size_bytes,
|
||||||
last_global_number,
|
last_block_number,
|
||||||
last_global_hash,
|
last_block_hash,
|
||||||
updated_at_ms,
|
updated_at_ms
|
||||||
line0_last_number, line0_last_hash,
|
|
||||||
line1_last_number, line1_last_hash,
|
|
||||||
line2_last_number, line2_last_hash,
|
|
||||||
line3_last_number, line3_last_hash,
|
|
||||||
line4_last_number, line4_last_hash,
|
|
||||||
line5_last_number, line5_last_hash,
|
|
||||||
line6_last_number, line6_last_hash,
|
|
||||||
line7_last_number, line7_last_hash
|
|
||||||
FROM blockchain_state
|
FROM blockchain_state
|
||||||
WHERE blockchain_name = ?
|
WHERE blockchain_name = ?
|
||||||
""";
|
""";
|
||||||
@ -73,10 +65,6 @@ public final class BlockchainStateDAO {
|
|||||||
|
|
||||||
/** UPSERT с внешним соединением. Соединение НЕ закрывает. */
|
/** UPSERT с внешним соединением. Соединение НЕ закрывает. */
|
||||||
public void upsert(Connection c, BlockchainStateEntry e) throws SQLException {
|
public void upsert(Connection c, BlockchainStateEntry e) throws SQLException {
|
||||||
|
|
||||||
// Колонок ровно 24:
|
|
||||||
// 8 основных + (8 линий * 2 поля) = 24
|
|
||||||
|
|
||||||
String sql = """
|
String sql = """
|
||||||
INSERT INTO blockchain_state (
|
INSERT INTO blockchain_state (
|
||||||
blockchain_name,
|
blockchain_name,
|
||||||
@ -84,53 +72,19 @@ public final class BlockchainStateDAO {
|
|||||||
blockchain_key,
|
blockchain_key,
|
||||||
size_limit,
|
size_limit,
|
||||||
file_size_bytes,
|
file_size_bytes,
|
||||||
last_global_number,
|
last_block_number,
|
||||||
last_global_hash,
|
last_block_hash,
|
||||||
updated_at_ms,
|
updated_at_ms
|
||||||
line0_last_number, line0_last_hash,
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
line1_last_number, line1_last_hash,
|
|
||||||
line2_last_number, line2_last_hash,
|
|
||||||
line3_last_number, line3_last_hash,
|
|
||||||
line4_last_number, line4_last_hash,
|
|
||||||
line5_last_number, line5_last_hash,
|
|
||||||
line6_last_number, line6_last_hash,
|
|
||||||
line7_last_number, line7_last_hash
|
|
||||||
) VALUES (
|
|
||||||
?,?,?,?,?,?,?,?,
|
|
||||||
?,?,
|
|
||||||
?,?,
|
|
||||||
?,?,
|
|
||||||
?,?,
|
|
||||||
?,?,
|
|
||||||
?,?,
|
|
||||||
?,?,
|
|
||||||
?,?
|
|
||||||
)
|
|
||||||
ON CONFLICT(blockchain_name)
|
ON CONFLICT(blockchain_name)
|
||||||
DO UPDATE SET
|
DO UPDATE SET
|
||||||
login = excluded.login,
|
login = excluded.login,
|
||||||
blockchain_key = excluded.blockchain_key,
|
blockchain_key = excluded.blockchain_key,
|
||||||
size_limit = excluded.size_limit,
|
size_limit = excluded.size_limit,
|
||||||
file_size_bytes = excluded.file_size_bytes,
|
file_size_bytes = excluded.file_size_bytes,
|
||||||
last_global_number = excluded.last_global_number,
|
last_block_number= excluded.last_block_number,
|
||||||
last_global_hash = excluded.last_global_hash,
|
last_block_hash = excluded.last_block_hash,
|
||||||
updated_at_ms = excluded.updated_at_ms,
|
updated_at_ms = excluded.updated_at_ms
|
||||||
line0_last_number = excluded.line0_last_number,
|
|
||||||
line0_last_hash = excluded.line0_last_hash,
|
|
||||||
line1_last_number = excluded.line1_last_number,
|
|
||||||
line1_last_hash = excluded.line1_last_hash,
|
|
||||||
line2_last_number = excluded.line2_last_number,
|
|
||||||
line2_last_hash = excluded.line2_last_hash,
|
|
||||||
line3_last_number = excluded.line3_last_number,
|
|
||||||
line3_last_hash = excluded.line3_last_hash,
|
|
||||||
line4_last_number = excluded.line4_last_number,
|
|
||||||
line4_last_hash = excluded.line4_last_hash,
|
|
||||||
line5_last_number = excluded.line5_last_number,
|
|
||||||
line5_last_hash = excluded.line5_last_hash,
|
|
||||||
line6_last_number = excluded.line6_last_number,
|
|
||||||
line6_last_hash = excluded.line6_last_hash,
|
|
||||||
line7_last_number = excluded.line7_last_number,
|
|
||||||
line7_last_hash = excluded.line7_last_hash
|
|
||||||
""";
|
""";
|
||||||
|
|
||||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
@ -143,14 +97,10 @@ public final class BlockchainStateDAO {
|
|||||||
ps.setLong(i++, e.getSizeLimit());
|
ps.setLong(i++, e.getSizeLimit());
|
||||||
ps.setLong(i++, e.getFileSizeBytes());
|
ps.setLong(i++, e.getFileSizeBytes());
|
||||||
|
|
||||||
ps.setInt(i++, e.getLastGlobalNumber());
|
ps.setInt(i++, e.getLastBlockNumber());
|
||||||
setBytesNullable(ps, i++, e.getLastGlobalHash());
|
setBytesNullable(ps, i++, e.getLastBlockHash());
|
||||||
ps.setLong(i++, e.getUpdatedAtMs());
|
|
||||||
|
|
||||||
for (int line = 0; line < 8; line++) {
|
ps.setLong(i++, e.getUpdatedAtMs());
|
||||||
ps.setInt(i++, e.getLastLineNumber(line));
|
|
||||||
setBytesNullable(ps, i++, e.getLastLineHash(line));
|
|
||||||
}
|
|
||||||
|
|
||||||
ps.executeUpdate();
|
ps.executeUpdate();
|
||||||
}
|
}
|
||||||
@ -175,24 +125,10 @@ public final class BlockchainStateDAO {
|
|||||||
ps.setLong(2, nowMs);
|
ps.setLong(2, nowMs);
|
||||||
ps.setString(3, blockchainName);
|
ps.setString(3, blockchainName);
|
||||||
ps.setLong(4, deltaBytes);
|
ps.setLong(4, deltaBytes);
|
||||||
int updated = ps.executeUpdate();
|
return ps.executeUpdate() > 0;
|
||||||
return updated > 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Удобная проверка для HEADER: запись должна быть и last_global_number должен быть -1. */
|
|
||||||
public BlockchainStateEntry requireExistingAtGenesis(Connection c, String blockchainName) throws SQLException {
|
|
||||||
BlockchainStateEntry st = getByBlockchainName(c, blockchainName);
|
|
||||||
if (st == null) {
|
|
||||||
throw new IllegalStateException("Blockchain state not found for blockchainName=" + blockchainName);
|
|
||||||
}
|
|
||||||
if (st.getLastGlobalNumber() != -1) {
|
|
||||||
throw new IllegalStateException("Blockchain state is not at genesis (-1). blockchainName=" + blockchainName +
|
|
||||||
" last_global_number=" + st.getLastGlobalNumber());
|
|
||||||
}
|
|
||||||
return st;
|
|
||||||
}
|
|
||||||
|
|
||||||
private BlockchainStateEntry mapRow(ResultSet rs) throws SQLException {
|
private BlockchainStateEntry mapRow(ResultSet rs) throws SQLException {
|
||||||
BlockchainStateEntry e = new BlockchainStateEntry();
|
BlockchainStateEntry e = new BlockchainStateEntry();
|
||||||
|
|
||||||
@ -203,16 +139,11 @@ public final class BlockchainStateDAO {
|
|||||||
e.setSizeLimit(rs.getLong("size_limit"));
|
e.setSizeLimit(rs.getLong("size_limit"));
|
||||||
e.setFileSizeBytes(rs.getLong("file_size_bytes"));
|
e.setFileSizeBytes(rs.getLong("file_size_bytes"));
|
||||||
|
|
||||||
e.setLastGlobalNumber(rs.getInt("last_global_number"));
|
e.setLastBlockNumber(rs.getInt("last_block_number"));
|
||||||
e.setLastGlobalHash(rs.getBytes("last_global_hash")); // может быть null
|
e.setLastBlockHash(rs.getBytes("last_block_hash")); // nullable
|
||||||
|
|
||||||
e.setUpdatedAtMs(rs.getLong("updated_at_ms"));
|
e.setUpdatedAtMs(rs.getLong("updated_at_ms"));
|
||||||
|
|
||||||
for (int line = 0; line < 8; line++) {
|
|
||||||
e.setLastLineNumber(line, rs.getInt("line" + line + "_last_number"));
|
|
||||||
e.setLastLineHash(line, rs.getBytes("line" + line + "_last_hash")); // может быть null
|
|
||||||
}
|
|
||||||
|
|
||||||
return e;
|
return e;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,6 @@
|
|||||||
|
// =======================
|
||||||
|
// shine/db/dao/BlocksDAO.java (ИЗМЕНЁННЫЙ под новый blocks формат + линейная проверка)
|
||||||
|
// =======================
|
||||||
package shine.db.dao;
|
package shine.db.dao;
|
||||||
|
|
||||||
import shine.db.SqliteDbController;
|
import shine.db.SqliteDbController;
|
||||||
@ -6,14 +9,14 @@ import shine.db.entities.BlockEntry;
|
|||||||
import java.sql.*;
|
import java.sql.*;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DAO для таблицы blocks.
|
* DAO для таблицы blocks (новый формат).
|
||||||
*
|
*
|
||||||
* Правило:
|
* Правило:
|
||||||
* - методы с Connection НЕ закрывают соединение
|
* - методы с Connection НЕ закрывают соединение
|
||||||
* - методы без Connection сами открывают и закрывают соединение
|
* - методы без Connection сами открывают и закрывают соединение
|
||||||
*
|
*
|
||||||
* Важно:
|
* Ключ:
|
||||||
* - PRIMARY KEY удалён (временно), поэтому "upsert" сделан через UPDATE->INSERT.
|
* - (bch_name, block_number) — уникальная пара в рамках общей БД сервера.
|
||||||
*/
|
*/
|
||||||
public final class BlocksDAO {
|
public final class BlocksDAO {
|
||||||
|
|
||||||
@ -39,26 +42,62 @@ public final class BlocksDAO {
|
|||||||
INSERT INTO blocks (
|
INSERT INTO blocks (
|
||||||
login,
|
login,
|
||||||
bch_name,
|
bch_name,
|
||||||
block_global_number,
|
block_number,
|
||||||
block_global_pre_hashe,
|
|
||||||
block_line_index,
|
|
||||||
block_line_number,
|
|
||||||
block_line_pre_hashe,
|
|
||||||
msg_type,
|
msg_type,
|
||||||
msg_sub_type,
|
msg_sub_type,
|
||||||
block_byte,
|
block_bytes,
|
||||||
to_login,
|
to_login,
|
||||||
to_bch_name,
|
to_bch_name,
|
||||||
to_block_global_number,
|
to_block_number,
|
||||||
to_block_hashe,
|
to_block_hash,
|
||||||
block_hash,
|
block_hash,
|
||||||
block_signature,
|
block_signature,
|
||||||
edited_by_block_global_number
|
edited_by_block_number,
|
||||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
prev_line_number,
|
||||||
|
prev_line_hash,
|
||||||
|
this_line_number
|
||||||
|
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||||
""";
|
""";
|
||||||
|
|
||||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
bindAll(ps, e);
|
int i = 1;
|
||||||
|
|
||||||
|
ps.setString(i++, e.getLogin());
|
||||||
|
ps.setString(i++, e.getBchName());
|
||||||
|
ps.setInt(i++, e.getBlockNumber());
|
||||||
|
|
||||||
|
ps.setInt(i++, e.getMsgType());
|
||||||
|
ps.setInt(i++, e.getMsgSubType());
|
||||||
|
|
||||||
|
ps.setBytes(i++, e.getBlockBytes());
|
||||||
|
|
||||||
|
if (e.getToLogin() != null) ps.setString(i++, e.getToLogin());
|
||||||
|
else ps.setNull(i++, Types.VARCHAR);
|
||||||
|
|
||||||
|
if (e.getToBchName() != null) ps.setString(i++, e.getToBchName());
|
||||||
|
else ps.setNull(i++, Types.VARCHAR);
|
||||||
|
|
||||||
|
if (e.getToBlockNumber() != null) ps.setInt(i++, e.getToBlockNumber());
|
||||||
|
else ps.setNull(i++, Types.INTEGER);
|
||||||
|
|
||||||
|
if (e.getToBlockHash() != null) ps.setBytes(i++, e.getToBlockHash());
|
||||||
|
else ps.setNull(i++, Types.BLOB);
|
||||||
|
|
||||||
|
ps.setBytes(i++, e.getBlockHash());
|
||||||
|
ps.setBytes(i++, e.getBlockSignature());
|
||||||
|
|
||||||
|
if (e.getEditedByBlockNumber() != null) ps.setInt(i++, e.getEditedByBlockNumber());
|
||||||
|
else ps.setNull(i++, Types.INTEGER);
|
||||||
|
|
||||||
|
if (e.getPrevLineNumber() != null) ps.setInt(i++, e.getPrevLineNumber());
|
||||||
|
else ps.setNull(i++, Types.INTEGER);
|
||||||
|
|
||||||
|
if (e.getPrevLineHash() != null) ps.setBytes(i++, e.getPrevLineHash());
|
||||||
|
else ps.setNull(i++, Types.BLOB);
|
||||||
|
|
||||||
|
if (e.getThisLineNumber() != null) ps.setInt(i++, e.getThisLineNumber());
|
||||||
|
else ps.setNull(i++, Types.INTEGER);
|
||||||
|
|
||||||
ps.executeUpdate();
|
ps.executeUpdate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -70,63 +109,62 @@ public final class BlocksDAO {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------- UPSERT (UPDATE -> INSERT) --------------------
|
// -------------------- SELECT: HASH BY NUMBER --------------------
|
||||||
|
|
||||||
public void upsert(Connection c, BlockEntry e) throws SQLException {
|
|
||||||
int updated = update(c, e);
|
|
||||||
if (updated == 0) insert(c, e);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void upsert(BlockEntry e) throws SQLException {
|
|
||||||
try (Connection c = db.getConnection()) {
|
|
||||||
upsert(c, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------------------- SELECT --------------------
|
|
||||||
|
|
||||||
public BlockEntry getByPk(Connection c,
|
|
||||||
String login,
|
|
||||||
String bchName,
|
|
||||||
int blockGlobalNumber,
|
|
||||||
int blockLineIndex,
|
|
||||||
int blockLineNumber) throws SQLException {
|
|
||||||
|
|
||||||
|
/** Получить block_hash по (bch_name, block_number). Нужен для линейной проверки. */
|
||||||
|
public byte[] getHashByNumber(Connection c, String bchName, int blockNumber) throws SQLException {
|
||||||
String sql = """
|
String sql = """
|
||||||
SELECT
|
SELECT block_hash
|
||||||
login,
|
|
||||||
bch_name,
|
|
||||||
block_global_number,
|
|
||||||
block_global_pre_hashe,
|
|
||||||
block_line_index,
|
|
||||||
block_line_number,
|
|
||||||
block_line_pre_hashe,
|
|
||||||
msg_type,
|
|
||||||
msg_sub_type,
|
|
||||||
block_byte,
|
|
||||||
to_login,
|
|
||||||
to_bch_name,
|
|
||||||
to_block_global_number,
|
|
||||||
to_block_hashe,
|
|
||||||
block_hash,
|
|
||||||
block_signature,
|
|
||||||
edited_by_block_global_number
|
|
||||||
FROM blocks
|
FROM blocks
|
||||||
WHERE
|
WHERE bch_name = ? AND block_number = ?
|
||||||
login = ?
|
|
||||||
AND bch_name = ?
|
|
||||||
AND block_global_number = ?
|
|
||||||
AND block_line_index = ?
|
|
||||||
AND block_line_number = ?
|
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
""";
|
""";
|
||||||
|
|
||||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
ps.setString(1, login);
|
ps.setString(1, bchName);
|
||||||
ps.setString(2, bchName);
|
ps.setInt(2, blockNumber);
|
||||||
ps.setInt(3, blockGlobalNumber);
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
ps.setInt(4, blockLineIndex);
|
if (!rs.next()) return null;
|
||||||
ps.setInt(5, blockLineNumber);
|
return rs.getBytes("block_hash");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getHashByNumber(String bchName, int blockNumber) throws SQLException {
|
||||||
|
try (Connection c = db.getConnection()) {
|
||||||
|
return getHashByNumber(c, bchName, blockNumber);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------- SELECT: FULL ENTRY --------------------
|
||||||
|
|
||||||
|
public BlockEntry getByNumber(Connection c, String bchName, int blockNumber) throws SQLException {
|
||||||
|
String sql = """
|
||||||
|
SELECT
|
||||||
|
login,
|
||||||
|
bch_name,
|
||||||
|
block_number,
|
||||||
|
msg_type,
|
||||||
|
msg_sub_type,
|
||||||
|
block_bytes,
|
||||||
|
to_login,
|
||||||
|
to_bch_name,
|
||||||
|
to_block_number,
|
||||||
|
to_block_hash,
|
||||||
|
block_hash,
|
||||||
|
block_signature,
|
||||||
|
edited_by_block_number,
|
||||||
|
prev_line_number,
|
||||||
|
prev_line_hash,
|
||||||
|
this_line_number
|
||||||
|
FROM blocks
|
||||||
|
WHERE bch_name = ? AND block_number = ?
|
||||||
|
LIMIT 1
|
||||||
|
""";
|
||||||
|
|
||||||
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
ps.setString(1, bchName);
|
||||||
|
ps.setInt(2, blockNumber);
|
||||||
|
|
||||||
try (ResultSet rs = ps.executeQuery()) {
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
if (!rs.next()) return null;
|
if (!rs.next()) return null;
|
||||||
@ -135,205 +173,57 @@ public final class BlocksDAO {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public BlockEntry getByPk(String login,
|
public BlockEntry getByNumber(String bchName, int blockNumber) throws SQLException {
|
||||||
String bchName,
|
|
||||||
int blockGlobalNumber,
|
|
||||||
int blockLineIndex,
|
|
||||||
int blockLineNumber) throws SQLException {
|
|
||||||
try (Connection c = db.getConnection()) {
|
try (Connection c = db.getConnection()) {
|
||||||
return getByPk(c, login, bchName, blockGlobalNumber, blockLineIndex, blockLineNumber);
|
return getByNumber(c, bchName, blockNumber);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------------------- UPDATE --------------------
|
|
||||||
|
|
||||||
public int update(Connection c, BlockEntry e) throws SQLException {
|
|
||||||
String sql = """
|
|
||||||
UPDATE blocks
|
|
||||||
SET
|
|
||||||
block_global_pre_hashe = ?,
|
|
||||||
block_line_pre_hashe = ?,
|
|
||||||
msg_type = ?,
|
|
||||||
msg_sub_type = ?,
|
|
||||||
block_byte = ?,
|
|
||||||
to_login = ?,
|
|
||||||
to_bch_name = ?,
|
|
||||||
to_block_global_number = ?,
|
|
||||||
to_block_hashe = ?,
|
|
||||||
block_hash = ?,
|
|
||||||
block_signature = ?,
|
|
||||||
edited_by_block_global_number = ?
|
|
||||||
WHERE
|
|
||||||
login = ?
|
|
||||||
AND bch_name = ?
|
|
||||||
AND block_global_number = ?
|
|
||||||
AND block_line_index = ?
|
|
||||||
AND block_line_number = ?
|
|
||||||
""";
|
|
||||||
|
|
||||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
|
||||||
int i = 1;
|
|
||||||
|
|
||||||
ps.setBytes(i++, bb(e.getBlockGlobalPreHashe()));
|
|
||||||
ps.setBytes(i++, bb(e.getBlockLinePreHashe()));
|
|
||||||
ps.setInt(i++, e.getMsgType());
|
|
||||||
ps.setInt(i++, e.getMsgSubType());
|
|
||||||
|
|
||||||
byte[] bytes = e.getBlockByte();
|
|
||||||
if (bytes != null) ps.setBytes(i++, bytes);
|
|
||||||
else ps.setNull(i++, Types.BLOB);
|
|
||||||
|
|
||||||
if (e.getToLogin() != null) ps.setString(i++, e.getToLogin());
|
|
||||||
else ps.setNull(i++, Types.VARCHAR);
|
|
||||||
|
|
||||||
if (e.getToBchName() != null) ps.setString(i++, e.getToBchName());
|
|
||||||
else ps.setNull(i++, Types.VARCHAR);
|
|
||||||
|
|
||||||
if (e.getToBlockGlobalNumber() != null) ps.setInt(i++, e.getToBlockGlobalNumber());
|
|
||||||
else ps.setNull(i++, Types.INTEGER);
|
|
||||||
|
|
||||||
if (e.getToBlockHashe() != null) ps.setBytes(i++, e.getToBlockHashe());
|
|
||||||
else ps.setNull(i++, Types.BLOB);
|
|
||||||
|
|
||||||
ps.setBytes(i++, bb(e.getBlockHash()));
|
|
||||||
ps.setBytes(i++, bb(e.getBlockSignature()));
|
|
||||||
|
|
||||||
if (e.getEditedByBlockGlobalNumber() != null) ps.setInt(i++, e.getEditedByBlockGlobalNumber());
|
|
||||||
else ps.setNull(i++, Types.INTEGER);
|
|
||||||
|
|
||||||
ps.setString(i++, e.getLogin());
|
|
||||||
ps.setString(i++, e.getBchName());
|
|
||||||
ps.setInt(i++, e.getBlockGlobalNumber());
|
|
||||||
ps.setInt(i++, e.getBlockLineIndex());
|
|
||||||
ps.setInt(i++, e.getBlockLineNumber());
|
|
||||||
|
|
||||||
return ps.executeUpdate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public int update(BlockEntry e) throws SQLException {
|
|
||||||
try (Connection c = db.getConnection()) {
|
|
||||||
return update(c, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------------------- DELETE --------------------
|
|
||||||
|
|
||||||
public int deleteByPk(Connection c,
|
|
||||||
String login,
|
|
||||||
String bchName,
|
|
||||||
int blockGlobalNumber,
|
|
||||||
int blockLineIndex,
|
|
||||||
int blockLineNumber) throws SQLException {
|
|
||||||
|
|
||||||
String sql = """
|
|
||||||
DELETE FROM blocks
|
|
||||||
WHERE
|
|
||||||
login = ?
|
|
||||||
AND bch_name = ?
|
|
||||||
AND block_global_number = ?
|
|
||||||
AND block_line_index = ?
|
|
||||||
AND block_line_number = ?
|
|
||||||
""";
|
|
||||||
|
|
||||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
|
||||||
ps.setString(1, login);
|
|
||||||
ps.setString(2, bchName);
|
|
||||||
ps.setInt(3, blockGlobalNumber);
|
|
||||||
ps.setInt(4, blockLineIndex);
|
|
||||||
ps.setInt(5, blockLineNumber);
|
|
||||||
return ps.executeUpdate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public int deleteByPk(String login,
|
|
||||||
String bchName,
|
|
||||||
int blockGlobalNumber,
|
|
||||||
int blockLineIndex,
|
|
||||||
int blockLineNumber) throws SQLException {
|
|
||||||
try (Connection c = db.getConnection()) {
|
|
||||||
return deleteByPk(c, login, bchName, blockGlobalNumber, blockLineIndex, blockLineNumber);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------- INTERNAL --------------------
|
// -------------------- INTERNAL --------------------
|
||||||
|
|
||||||
private static void bindAll(PreparedStatement ps, BlockEntry e) throws SQLException {
|
|
||||||
int i = 1;
|
|
||||||
|
|
||||||
ps.setString(i++, e.getLogin());
|
|
||||||
ps.setString(i++, e.getBchName());
|
|
||||||
ps.setInt(i++, e.getBlockGlobalNumber());
|
|
||||||
ps.setBytes(i++, bb(e.getBlockGlobalPreHashe()));
|
|
||||||
|
|
||||||
ps.setInt(i++, e.getBlockLineIndex());
|
|
||||||
ps.setInt(i++, e.getBlockLineNumber());
|
|
||||||
ps.setBytes(i++, bb(e.getBlockLinePreHashe()));
|
|
||||||
|
|
||||||
ps.setInt(i++, e.getMsgType());
|
|
||||||
ps.setInt(i++, e.getMsgSubType());
|
|
||||||
|
|
||||||
byte[] bytes = e.getBlockByte();
|
|
||||||
if (bytes != null) ps.setBytes(i++, bytes);
|
|
||||||
else ps.setNull(i++, Types.BLOB);
|
|
||||||
|
|
||||||
if (e.getToLogin() != null) ps.setString(i++, e.getToLogin());
|
|
||||||
else ps.setNull(i++, Types.VARCHAR);
|
|
||||||
|
|
||||||
if (e.getToBchName() != null) ps.setString(i++, e.getToBchName());
|
|
||||||
else ps.setNull(i++, Types.VARCHAR);
|
|
||||||
|
|
||||||
if (e.getToBlockGlobalNumber() != null) ps.setInt(i++, e.getToBlockGlobalNumber());
|
|
||||||
else ps.setNull(i++, Types.INTEGER);
|
|
||||||
|
|
||||||
if (e.getToBlockHashe() != null) ps.setBytes(i++, e.getToBlockHashe());
|
|
||||||
else ps.setNull(i++, Types.BLOB);
|
|
||||||
|
|
||||||
ps.setBytes(i++, bb(e.getBlockHash()));
|
|
||||||
ps.setBytes(i++, bb(e.getBlockSignature()));
|
|
||||||
|
|
||||||
if (e.getEditedByBlockGlobalNumber() != null) ps.setInt(i++, e.getEditedByBlockGlobalNumber());
|
|
||||||
else ps.setNull(i++, Types.INTEGER);
|
|
||||||
}
|
|
||||||
|
|
||||||
private BlockEntry mapRow(ResultSet rs) throws SQLException {
|
private BlockEntry mapRow(ResultSet rs) throws SQLException {
|
||||||
BlockEntry e = new BlockEntry();
|
BlockEntry e = new BlockEntry();
|
||||||
|
|
||||||
e.setLogin(rs.getString("login"));
|
e.setLogin(rs.getString("login"));
|
||||||
e.setBchName(rs.getString("bch_name"));
|
e.setBchName(rs.getString("bch_name"));
|
||||||
e.setBlockGlobalNumber(rs.getInt("block_global_number"));
|
e.setBlockNumber(rs.getInt("block_number"));
|
||||||
e.setBlockGlobalPreHashe(rs.getBytes("block_global_pre_hashe"));
|
|
||||||
|
|
||||||
e.setBlockLineIndex(rs.getInt("block_line_index"));
|
|
||||||
e.setBlockLineNumber(rs.getInt("block_line_number"));
|
|
||||||
e.setBlockLinePreHashe(rs.getBytes("block_line_pre_hashe"));
|
|
||||||
|
|
||||||
e.setMsgType(rs.getInt("msg_type"));
|
e.setMsgType(rs.getInt("msg_type"));
|
||||||
e.setMsgSubType(rs.getInt("msg_sub_type"));
|
e.setMsgSubType(rs.getInt("msg_sub_type"));
|
||||||
|
|
||||||
e.setBlockByte(rs.getBytes("block_byte"));
|
e.setBlockBytes(rs.getBytes("block_bytes"));
|
||||||
|
|
||||||
e.setToLogin(rs.getString("to_login"));
|
String toLogin = rs.getString("to_login");
|
||||||
|
if (rs.wasNull()) toLogin = null;
|
||||||
|
e.setToLogin(toLogin);
|
||||||
|
|
||||||
String toBchName = rs.getString("to_bch_name");
|
String toBchName = rs.getString("to_bch_name");
|
||||||
if (rs.wasNull()) toBchName = null;
|
if (rs.wasNull()) toBchName = null;
|
||||||
e.setToBchName(toBchName);
|
e.setToBchName(toBchName);
|
||||||
|
|
||||||
Integer toBlockGlobalNumber = (Integer) rs.getObject("to_block_global_number");
|
Integer toBlockNumber = (Integer) rs.getObject("to_block_number");
|
||||||
e.setToBlockGlobalNumber(toBlockGlobalNumber);
|
e.setToBlockNumber(toBlockNumber);
|
||||||
|
|
||||||
byte[] toBlockHashe = rs.getBytes("to_block_hashe");
|
byte[] toHash = rs.getBytes("to_block_hash");
|
||||||
if (rs.wasNull()) toBlockHashe = null;
|
if (rs.wasNull()) toHash = null;
|
||||||
e.setToBlockHashe(toBlockHashe);
|
e.setToBlockHash(toHash);
|
||||||
|
|
||||||
e.setBlockHash(rs.getBytes("block_hash"));
|
e.setBlockHash(rs.getBytes("block_hash"));
|
||||||
e.setBlockSignature(rs.getBytes("block_signature"));
|
e.setBlockSignature(rs.getBytes("block_signature"));
|
||||||
|
|
||||||
Integer editedBy = (Integer) rs.getObject("edited_by_block_global_number");
|
Integer editedBy = (Integer) rs.getObject("edited_by_block_number");
|
||||||
e.setEditedByBlockGlobalNumber(editedBy);
|
e.setEditedByBlockNumber(editedBy);
|
||||||
|
|
||||||
|
Integer prevLn = (Integer) rs.getObject("prev_line_number");
|
||||||
|
e.setPrevLineNumber(prevLn);
|
||||||
|
|
||||||
|
byte[] prevLh = rs.getBytes("prev_line_hash");
|
||||||
|
if (rs.wasNull()) prevLh = null;
|
||||||
|
e.setPrevLineHash(prevLh);
|
||||||
|
|
||||||
|
Integer thisLn = (Integer) rs.getObject("this_line_number");
|
||||||
|
e.setThisLineNumber(thisLn);
|
||||||
|
|
||||||
return e;
|
return e;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static byte[] bb(byte[] b) { return b == null ? new byte[0] : b; }
|
|
||||||
}
|
}
|
||||||
@ -9,7 +9,7 @@ import java.sql.*;
|
|||||||
/**
|
/**
|
||||||
* UserCreateDAO — атомарное добавление пользователя:
|
* UserCreateDAO — атомарное добавление пользователя:
|
||||||
* - solana_users (login, device_key)
|
* - solana_users (login, device_key)
|
||||||
* - blockchain_state (blockchain_name, login, blockchain_key, size_limit, ... last_global_number=-1 ...)
|
* - blockchain_state (blockchain_name, login, blockchain_key, size_limit, ... last_block_number=-1 ...)
|
||||||
*
|
*
|
||||||
* ВАЖНО:
|
* ВАЖНО:
|
||||||
* - только INSERT/UPSERT
|
* - только INSERT/UPSERT
|
||||||
@ -67,14 +67,9 @@ public final class UserCreateDAO {
|
|||||||
st.setSizeLimit(sizeLimit);
|
st.setSizeLimit(sizeLimit);
|
||||||
st.setFileSizeBytes(0L);
|
st.setFileSizeBytes(0L);
|
||||||
|
|
||||||
// старт: глобальных блоков ещё нет
|
// старт: блоков ещё нет
|
||||||
st.setLastGlobalNumber(-1);
|
st.setLastBlockNumber(-1);
|
||||||
st.setLastGlobalHash(null);
|
st.setLastBlockHash(null);
|
||||||
|
|
||||||
for (int line = 0; line < 8; line++) {
|
|
||||||
st.setLastLineNumber(line, 0);
|
|
||||||
st.setLastLineHash(line, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
st.setUpdatedAtMs(nowMs);
|
st.setUpdatedAtMs(nowMs);
|
||||||
|
|
||||||
|
|||||||
@ -1,93 +1,62 @@
|
|||||||
|
// =======================
|
||||||
|
// shine/db/entities/BlockEntry.java (ИЗМЕНЁННАЯ под новый blocks формат)
|
||||||
|
// =======================
|
||||||
package shine.db.entities;
|
package shine.db.entities;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Запись блока (таблица blocks).
|
* Запись блока (таблица blocks) — обновлённая модель под новый формат.
|
||||||
|
*
|
||||||
|
* Храним:
|
||||||
|
* - login, bch_name (как было в проекте, чтобы не ломать общую БД)
|
||||||
|
* - block_number (глобальный номер в этой цепочке)
|
||||||
|
* - block_bytes (полный блок: preimage + signature)
|
||||||
|
* - block_hash (32 байта вычисленный SHA-256(preimage))
|
||||||
|
* - block_signature (64 байта)
|
||||||
|
*
|
||||||
|
* Опционально:
|
||||||
|
* - prev_line_number / prev_line_hash / this_line_number
|
||||||
|
*
|
||||||
|
* Плюс поля индексации (как раньше было удобно):
|
||||||
|
* - msg_type / msg_sub_type
|
||||||
|
* - to_* (если есть target)
|
||||||
|
* - edited_by_block_number (для TEXT_EDIT)
|
||||||
*/
|
*/
|
||||||
public class BlockEntry {
|
public class BlockEntry {
|
||||||
|
|
||||||
private String login;
|
private String login;
|
||||||
private String bchName;
|
private String bchName;
|
||||||
|
|
||||||
private int blockGlobalNumber;
|
private int blockNumber;
|
||||||
private byte[] blockGlobalPreHashe;
|
|
||||||
|
|
||||||
private int blockLineIndex;
|
|
||||||
private int blockLineNumber;
|
|
||||||
private byte[] blockLinePreHashe;
|
|
||||||
|
|
||||||
private int msgType;
|
private int msgType;
|
||||||
private int msgSubType;
|
private int msgSubType;
|
||||||
|
|
||||||
private byte[] blockByte;
|
private byte[] blockBytes;
|
||||||
|
|
||||||
private String toLogin;
|
private String toLogin;
|
||||||
private String toBchName;
|
private String toBchName;
|
||||||
private Integer toBlockGlobalNumber;
|
private Integer toBlockNumber;
|
||||||
private byte[] toBlockHashe;
|
private byte[] toBlockHash;
|
||||||
|
|
||||||
// новое
|
|
||||||
private byte[] blockHash;
|
private byte[] blockHash;
|
||||||
private byte[] blockSignature;
|
private byte[] blockSignature;
|
||||||
private Integer editedByBlockGlobalNumber;
|
|
||||||
|
private Integer editedByBlockNumber;
|
||||||
|
|
||||||
|
private Integer prevLineNumber;
|
||||||
|
private byte[] prevLineHash;
|
||||||
|
private Integer thisLineNumber;
|
||||||
|
|
||||||
public BlockEntry() {}
|
public BlockEntry() {}
|
||||||
|
|
||||||
public BlockEntry(String login,
|
|
||||||
String bchName,
|
|
||||||
int blockGlobalNumber,
|
|
||||||
byte[] blockGlobalPreHashe,
|
|
||||||
int blockLineIndex,
|
|
||||||
int blockLineNumber,
|
|
||||||
byte[] blockLinePreHashe,
|
|
||||||
int msgType,
|
|
||||||
int msgSubType,
|
|
||||||
byte[] blockByte,
|
|
||||||
String toLogin,
|
|
||||||
String toBchName,
|
|
||||||
Integer toBlockGlobalNumber,
|
|
||||||
byte[] toBlockHashe,
|
|
||||||
byte[] blockHash,
|
|
||||||
byte[] blockSignature,
|
|
||||||
Integer editedByBlockGlobalNumber) {
|
|
||||||
this.login = login;
|
|
||||||
this.bchName = bchName;
|
|
||||||
this.blockGlobalNumber = blockGlobalNumber;
|
|
||||||
this.blockGlobalPreHashe = blockGlobalPreHashe;
|
|
||||||
this.blockLineIndex = blockLineIndex;
|
|
||||||
this.blockLineNumber = blockLineNumber;
|
|
||||||
this.blockLinePreHashe = blockLinePreHashe;
|
|
||||||
this.msgType = msgType;
|
|
||||||
this.msgSubType = msgSubType;
|
|
||||||
this.blockByte = blockByte;
|
|
||||||
this.toLogin = toLogin;
|
|
||||||
this.toBchName = toBchName;
|
|
||||||
this.toBlockGlobalNumber = toBlockGlobalNumber;
|
|
||||||
this.toBlockHashe = toBlockHashe;
|
|
||||||
this.blockHash = blockHash;
|
|
||||||
this.blockSignature = blockSignature;
|
|
||||||
this.editedByBlockGlobalNumber = editedByBlockGlobalNumber;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getLogin() { return login; }
|
public String getLogin() { return login; }
|
||||||
public void setLogin(String login) { this.login = login; }
|
public void setLogin(String login) { this.login = login; }
|
||||||
|
|
||||||
public String getBchName() { return bchName; }
|
public String getBchName() { return bchName; }
|
||||||
public void setBchName(String bchName) { this.bchName = bchName; }
|
public void setBchName(String bchName) { this.bchName = bchName; }
|
||||||
|
|
||||||
public int getBlockGlobalNumber() { return blockGlobalNumber; }
|
public int getBlockNumber() { return blockNumber; }
|
||||||
public void setBlockGlobalNumber(int blockGlobalNumber) { this.blockGlobalNumber = blockGlobalNumber; }
|
public void setBlockNumber(int blockNumber) { this.blockNumber = blockNumber; }
|
||||||
|
|
||||||
public byte[] getBlockGlobalPreHashe() { return blockGlobalPreHashe; }
|
|
||||||
public void setBlockGlobalPreHashe(byte[] blockGlobalPreHashe) { this.blockGlobalPreHashe = blockGlobalPreHashe; }
|
|
||||||
|
|
||||||
public int getBlockLineIndex() { return blockLineIndex; }
|
|
||||||
public void setBlockLineIndex(int blockLineIndex) { this.blockLineIndex = blockLineIndex; }
|
|
||||||
|
|
||||||
public int getBlockLineNumber() { return blockLineNumber; }
|
|
||||||
public void setBlockLineNumber(int blockLineNumber) { this.blockLineNumber = blockLineNumber; }
|
|
||||||
|
|
||||||
public byte[] getBlockLinePreHashe() { return blockLinePreHashe; }
|
|
||||||
public void setBlockLinePreHashe(byte[] blockLinePreHashe) { this.blockLinePreHashe = blockLinePreHashe; }
|
|
||||||
|
|
||||||
public int getMsgType() { return msgType; }
|
public int getMsgType() { return msgType; }
|
||||||
public void setMsgType(int msgType) { this.msgType = msgType; }
|
public void setMsgType(int msgType) { this.msgType = msgType; }
|
||||||
@ -95,8 +64,8 @@ public class BlockEntry {
|
|||||||
public int getMsgSubType() { return msgSubType; }
|
public int getMsgSubType() { return msgSubType; }
|
||||||
public void setMsgSubType(int msgSubType) { this.msgSubType = msgSubType; }
|
public void setMsgSubType(int msgSubType) { this.msgSubType = msgSubType; }
|
||||||
|
|
||||||
public byte[] getBlockByte() { return blockByte; }
|
public byte[] getBlockBytes() { return blockBytes; }
|
||||||
public void setBlockByte(byte[] blockByte) { this.blockByte = blockByte; }
|
public void setBlockBytes(byte[] blockBytes) { this.blockBytes = blockBytes; }
|
||||||
|
|
||||||
public String getToLogin() { return toLogin; }
|
public String getToLogin() { return toLogin; }
|
||||||
public void setToLogin(String toLogin) { this.toLogin = toLogin; }
|
public void setToLogin(String toLogin) { this.toLogin = toLogin; }
|
||||||
@ -104,11 +73,11 @@ public class BlockEntry {
|
|||||||
public String getToBchName() { return toBchName; }
|
public String getToBchName() { return toBchName; }
|
||||||
public void setToBchName(String toBchName) { this.toBchName = toBchName; }
|
public void setToBchName(String toBchName) { this.toBchName = toBchName; }
|
||||||
|
|
||||||
public Integer getToBlockGlobalNumber() { return toBlockGlobalNumber; }
|
public Integer getToBlockNumber() { return toBlockNumber; }
|
||||||
public void setToBlockGlobalNumber(Integer toBlockGlobalNumber) { this.toBlockGlobalNumber = toBlockGlobalNumber; }
|
public void setToBlockNumber(Integer toBlockNumber) { this.toBlockNumber = toBlockNumber; }
|
||||||
|
|
||||||
public byte[] getToBlockHashe() { return toBlockHashe; }
|
public byte[] getToBlockHash() { return toBlockHash; }
|
||||||
public void setToBlockHashe(byte[] toBlockHashe) { this.toBlockHashe = toBlockHashe; }
|
public void setToBlockHash(byte[] toBlockHash) { this.toBlockHash = toBlockHash; }
|
||||||
|
|
||||||
public byte[] getBlockHash() { return blockHash; }
|
public byte[] getBlockHash() { return blockHash; }
|
||||||
public void setBlockHash(byte[] blockHash) { this.blockHash = blockHash; }
|
public void setBlockHash(byte[] blockHash) { this.blockHash = blockHash; }
|
||||||
@ -116,6 +85,15 @@ public class BlockEntry {
|
|||||||
public byte[] getBlockSignature() { return blockSignature; }
|
public byte[] getBlockSignature() { return blockSignature; }
|
||||||
public void setBlockSignature(byte[] blockSignature) { this.blockSignature = blockSignature; }
|
public void setBlockSignature(byte[] blockSignature) { this.blockSignature = blockSignature; }
|
||||||
|
|
||||||
public Integer getEditedByBlockGlobalNumber() { return editedByBlockGlobalNumber; }
|
public Integer getEditedByBlockNumber() { return editedByBlockNumber; }
|
||||||
public void setEditedByBlockGlobalNumber(Integer editedByBlockGlobalNumber) { this.editedByBlockGlobalNumber = editedByBlockGlobalNumber; }
|
public void setEditedByBlockNumber(Integer editedByBlockNumber) { this.editedByBlockNumber = editedByBlockNumber; }
|
||||||
|
|
||||||
|
public Integer getPrevLineNumber() { return prevLineNumber; }
|
||||||
|
public void setPrevLineNumber(Integer prevLineNumber) { this.prevLineNumber = prevLineNumber; }
|
||||||
|
|
||||||
|
public byte[] getPrevLineHash() { return prevLineHash; }
|
||||||
|
public void setPrevLineHash(byte[] prevLineHash) { this.prevLineHash = prevLineHash; }
|
||||||
|
|
||||||
|
public Integer getThisLineNumber() { return thisLineNumber; }
|
||||||
|
public void setThisLineNumber(Integer thisLineNumber) { this.thisLineNumber = thisLineNumber; }
|
||||||
}
|
}
|
||||||
@ -1,72 +1,38 @@
|
|||||||
// =======================
|
// =======================
|
||||||
// BlockchainStateEntry.java (НОВАЯ ВЕРСИЯ)
|
// shine/db/entities/BlockchainStateEntry.java (ИЗМЕНЁННАЯ: убраны line0..7, переименовано last_block_*)
|
||||||
// =======================
|
// =======================
|
||||||
package shine.db.entities;
|
package shine.db.entities;
|
||||||
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Агрегатная сущность текущего состояния блокчейна.
|
* Агрегатная сущность текущего состояния блокчейна.
|
||||||
* 1 строка = 1 blockchain_name, плюс состояние линий 0..7.
|
|
||||||
*
|
*
|
||||||
* ВАЖНО:
|
* ВАЖНО:
|
||||||
* - hash-поля теперь храним как byte[] и допускаем NULL:
|
* - Убраны все поля линий line0..7 (они больше не нужны).
|
||||||
* * NULL = "ещё не было ни одного блока" (genesis и т.п.)
|
* - Оставляем:
|
||||||
* * не подменяем на new byte[0], чтобы не терять смысл
|
* last_block_number
|
||||||
|
* last_block_hash
|
||||||
|
*
|
||||||
|
* Остальные поля (login, blockchain_key, лимиты) оставлены как в проекте,
|
||||||
|
* потому что серверу они реально нужны (ключ подписи/лимит файла).
|
||||||
*/
|
*/
|
||||||
public final class BlockchainStateEntry {
|
public final class BlockchainStateEntry {
|
||||||
|
|
||||||
private String blockchainName;
|
private String blockchainName;
|
||||||
private String login;
|
private String login;
|
||||||
|
|
||||||
private String blockchainKey;
|
private String blockchainKey; // Base64(32)
|
||||||
|
|
||||||
private long sizeLimit;
|
private long sizeLimit;
|
||||||
private long fileSizeBytes;
|
private long fileSizeBytes;
|
||||||
|
|
||||||
private int lastGlobalNumber;
|
private int lastBlockNumber; // было last_global_number
|
||||||
private byte[] lastGlobalHash; // nullable
|
private byte[] lastBlockHash; // было last_global_hash (nullable)
|
||||||
|
|
||||||
private final int[] lastLineNumbers = new int[8];
|
|
||||||
private final byte[][] lastLineHashes = new byte[8][]; // nullable elements
|
|
||||||
|
|
||||||
private long updatedAtMs;
|
private long updatedAtMs;
|
||||||
|
|
||||||
public BlockchainStateEntry() {
|
public BlockchainStateEntry() {}
|
||||||
// hashes остаются null по умолчанию (genesis)
|
|
||||||
}
|
|
||||||
|
|
||||||
public BlockchainStateEntry(String blockchainName,
|
|
||||||
String login,
|
|
||||||
String blockchainKey,
|
|
||||||
long sizeLimit,
|
|
||||||
long fileSizeBytes,
|
|
||||||
int lastGlobalNumber,
|
|
||||||
byte[] lastGlobalHash,
|
|
||||||
int[] lastLineNumbers,
|
|
||||||
byte[][] lastLineHashes,
|
|
||||||
long updatedAtMs) {
|
|
||||||
this.blockchainName = blockchainName;
|
|
||||||
this.login = login;
|
|
||||||
this.blockchainKey = blockchainKey;
|
|
||||||
this.sizeLimit = sizeLimit;
|
|
||||||
this.fileSizeBytes = fileSizeBytes;
|
|
||||||
this.lastGlobalNumber = lastGlobalNumber;
|
|
||||||
this.lastGlobalHash = lastGlobalHash;
|
|
||||||
|
|
||||||
if (lastLineNumbers != null) {
|
|
||||||
if (lastLineNumbers.length != 8) throw new IllegalArgumentException("lastLineNumbers must be len=8");
|
|
||||||
System.arraycopy(lastLineNumbers, 0, this.lastLineNumbers, 0, 8);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lastLineHashes != null) {
|
|
||||||
if (lastLineHashes.length != 8) throw new IllegalArgumentException("lastLineHashes must be len=8");
|
|
||||||
System.arraycopy(lastLineHashes, 0, this.lastLineHashes, 0, 8);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.updatedAtMs = updatedAtMs;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getBlockchainName() { return blockchainName; }
|
public String getBlockchainName() { return blockchainName; }
|
||||||
public void setBlockchainName(String blockchainName) { this.blockchainName = blockchainName; }
|
public void setBlockchainName(String blockchainName) { this.blockchainName = blockchainName; }
|
||||||
@ -95,42 +61,12 @@ public final class BlockchainStateEntry {
|
|||||||
public long getFileSizeBytes() { return fileSizeBytes; }
|
public long getFileSizeBytes() { return fileSizeBytes; }
|
||||||
public void setFileSizeBytes(long fileSizeBytes) { this.fileSizeBytes = fileSizeBytes; }
|
public void setFileSizeBytes(long fileSizeBytes) { this.fileSizeBytes = fileSizeBytes; }
|
||||||
|
|
||||||
public int getLastGlobalNumber() { return lastGlobalNumber; }
|
public int getLastBlockNumber() { return lastBlockNumber; }
|
||||||
public void setLastGlobalNumber(int lastGlobalNumber) { this.lastGlobalNumber = lastGlobalNumber; }
|
public void setLastBlockNumber(int lastBlockNumber) { this.lastBlockNumber = lastBlockNumber; }
|
||||||
|
|
||||||
public byte[] getLastGlobalHash() { return lastGlobalHash; }
|
public byte[] getLastBlockHash() { return lastBlockHash; }
|
||||||
public void setLastGlobalHash(byte[] lastGlobalHash) { this.lastGlobalHash = lastGlobalHash; }
|
public void setLastBlockHash(byte[] lastBlockHash) { this.lastBlockHash = lastBlockHash; }
|
||||||
|
|
||||||
public int getLastLineNumber(int line) {
|
|
||||||
checkLine(line);
|
|
||||||
return lastLineNumbers[line];
|
|
||||||
}
|
|
||||||
public void setLastLineNumber(int line, int value) {
|
|
||||||
checkLine(line);
|
|
||||||
lastLineNumbers[line] = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public byte[] getLastLineHash(int line) {
|
|
||||||
checkLine(line);
|
|
||||||
return lastLineHashes[line];
|
|
||||||
}
|
|
||||||
public void setLastLineHash(int line, byte[] value) {
|
|
||||||
checkLine(line);
|
|
||||||
lastLineHashes[line] = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int[] getLastLineNumbersCopy() {
|
|
||||||
return Arrays.copyOf(lastLineNumbers, 8);
|
|
||||||
}
|
|
||||||
|
|
||||||
public byte[][] getLastLineHashesCopy() {
|
|
||||||
return Arrays.copyOf(lastLineHashes, 8);
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getUpdatedAtMs() { return updatedAtMs; }
|
public long getUpdatedAtMs() { return updatedAtMs; }
|
||||||
public void setUpdatedAtMs(long updatedAtMs) { this.updatedAtMs = updatedAtMs; }
|
public void setUpdatedAtMs(long updatedAtMs) { this.updatedAtMs = updatedAtMs; }
|
||||||
|
|
||||||
private static void checkLine(int line) {
|
|
||||||
if (line < 0 || line > 7) throw new IllegalArgumentException("line must be 0..7");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -24,7 +24,7 @@ import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_ListUserPa
|
|||||||
import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_UpsertUserParam_Request;
|
import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_UpsertUserParam_Request;
|
||||||
|
|
||||||
// !!! подставь реальные пакеты/имена, как у тебя в проекте:
|
// !!! подставь реальные пакеты/имена, как у тебя в проекте:
|
||||||
import server.logic.ws_protocol.JSON.handlers.subscriptions.Net_GetSubscribedChannels_Handler;
|
//import server.logic.ws_protocol.JSON.handlers.subscriptions.Net_GetSubscribedChannels_Handler;
|
||||||
import server.logic.ws_protocol.JSON.handlers.subscriptions.entyties.Net_GetSubscribedChannels_Request;
|
import server.logic.ws_protocol.JSON.handlers.subscriptions.entyties.Net_GetSubscribedChannels_Request;
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@ -48,10 +48,10 @@ public final class JsonHandlerRegistry {
|
|||||||
// --- userParams ---
|
// --- userParams ---
|
||||||
Map.entry("UpsertUserParam", new Net_UpsertUserParam_Handler()),
|
Map.entry("UpsertUserParam", new Net_UpsertUserParam_Handler()),
|
||||||
Map.entry("GetUserParam", new Net_GetUserParam_Handler()),
|
Map.entry("GetUserParam", new Net_GetUserParam_Handler()),
|
||||||
Map.entry("ListUserParams", new Net_ListUserParams_Handler()),
|
Map.entry("ListUserParams", new Net_ListUserParams_Handler())
|
||||||
|
|
||||||
// --- subscriptions ---
|
// --- subscriptions ---
|
||||||
Map.entry("ListSubscribedChannels", new Net_GetSubscribedChannels_Handler())
|
// Map.entry("ListSubscribedChannels", new Net_GetSubscribedChannels_Handler())
|
||||||
);
|
);
|
||||||
|
|
||||||
private static final Map<String, Class<? extends Net_Request>> REQUEST_TYPES = Map.ofEntries(
|
private static final Map<String, Class<? extends Net_Request>> REQUEST_TYPES = Map.ofEntries(
|
||||||
|
|||||||
@ -1,40 +1,52 @@
|
|||||||
|
// =======================
|
||||||
|
// server/logic/ws_protocol/JSON/handlers/blockchain/Net_AddBlock_Handler.java (ИЗМЕНЁННЫЙ под ТЗ)
|
||||||
|
// =======================
|
||||||
package server.logic.ws_protocol.JSON.handlers.blockchain;
|
package server.logic.ws_protocol.JSON.handlers.blockchain;
|
||||||
|
|
||||||
import blockchain.BchBlockEntry;
|
import blockchain.BchBlockEntry;
|
||||||
import blockchain.BchCryptoVerifier;
|
import blockchain.BchCryptoVerifier;
|
||||||
|
import blockchain.body.BodyHasLine;
|
||||||
|
import blockchain.body.BodyHasTarget;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import server.logic.ws_protocol.JSON.ConnectionContext;
|
import server.logic.ws_protocol.JSON.ConnectionContext;
|
||||||
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||||
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
||||||
|
import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
|
||||||
import server.logic.ws_protocol.JSON.handlers.blockchain.Net_AddBlock_Handler_utils.BlockchainLocks;
|
import server.logic.ws_protocol.JSON.handlers.blockchain.Net_AddBlock_Handler_utils.BlockchainLocks;
|
||||||
import server.logic.ws_protocol.JSON.handlers.blockchain.Net_AddBlock_Handler_utils.BlockchainWriter;
|
import server.logic.ws_protocol.JSON.handlers.blockchain.Net_AddBlock_Handler_utils.BlockchainWriter;
|
||||||
import server.logic.ws_protocol.JSON.handlers.blockchain.entyties.Net_AddBlock_Request;
|
import server.logic.ws_protocol.JSON.handlers.blockchain.entyties.Net_AddBlock_Request;
|
||||||
import server.logic.ws_protocol.JSON.handlers.blockchain.entyties.Net_AddBlock_Response;
|
import server.logic.ws_protocol.JSON.handlers.blockchain.entyties.Net_AddBlock_Response;
|
||||||
import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
|
|
||||||
import server.logic.ws_protocol.WireCodes;
|
import server.logic.ws_protocol.WireCodes;
|
||||||
import shine.db.dao.BlockchainStateDAO;
|
import shine.db.dao.BlockchainStateDAO;
|
||||||
import shine.db.dao.BlocksDAO;
|
import shine.db.dao.BlocksDAO;
|
||||||
import shine.db.entities.BlockchainStateEntry;
|
import shine.db.entities.BlockchainStateEntry;
|
||||||
|
import shine.db.entities.BlockEntry;
|
||||||
import utils.blockchain.BlockchainNameUtil;
|
import utils.blockchain.BlockchainNameUtil;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
import java.util.concurrent.locks.ReentrantLock;
|
import java.util.concurrent.locks.ReentrantLock;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Net_AddBlock_Handler — единый хэндлер добавления блока (JSON).
|
* Net_AddBlock_Handler — единый хэндлер добавления блока (JSON).
|
||||||
*
|
*
|
||||||
* Задачи:
|
* Новый порядок валидации (ТЗ):
|
||||||
* 1) Лочим добавление блоков для конкретного blockchainName (защита от гонок в одном процессе).
|
* 1) Достаём из blockchain_state: last_block_number, last_block_hash
|
||||||
* 2) Декодируем блок из Base64 и парсим его структуру.
|
* 2) Проверяем:
|
||||||
* 3) Валидируем body (type/version + содержимое).
|
* - incoming.blockNumber == last+1
|
||||||
* 4) Проверяем globalNumber и prevGlobalHash относительно server state.
|
* - incoming.prevHash32 == last_hash (для genesis last_hash = 32 нулей)
|
||||||
* 5) Проверяем линии:
|
* 3) Считаем hash32 = SHA-256(preimage) (preimage = block_bytes без signature64)
|
||||||
* - genesis: global=0, lineIndex=0, lineNumber=0
|
* 4) Проверяем подпись Ed25519.verify(hash32, signature64, pubKey)
|
||||||
* - остальные: lineIndex=1..7, lineNumber по счётчику линии
|
* 5) Если тип имеет линию:
|
||||||
* 6) Проверяем подпись/хэш (Ed25519 над hash32, hash32=sha256(preimage)).
|
* - если prevLineNumber != -1:
|
||||||
* preimage включает prevLineHash32 (берём из state по lineIndex).
|
* достаём hash блока prevLineNumber из blocks
|
||||||
* 7) Пишем в БД+файл через BlockchainWriter (атомарность там).
|
* сравниваем с prevLineHash32 из body
|
||||||
|
* 6) Сохраняем блок в blocks + обновляем blockchain_state
|
||||||
|
*
|
||||||
|
* Важно:
|
||||||
|
* - Сетевой протокол AddBlock пока оставляем старые поля (globalNumber/prevGlobalHash),
|
||||||
|
* но внутренняя логика использует НОВЫЙ формат блока.
|
||||||
*/
|
*/
|
||||||
public final class Net_AddBlock_Handler implements JsonMessageHandler {
|
public final class Net_AddBlock_Handler implements JsonMessageHandler {
|
||||||
|
|
||||||
@ -56,8 +68,8 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
|
|||||||
try {
|
try {
|
||||||
AddBlockResult r = addBlock(
|
AddBlockResult r = addBlock(
|
||||||
blockchainName,
|
blockchainName,
|
||||||
req.getGlobalNumber(),
|
req.getGlobalNumber(), // старое поле, пока оставляем
|
||||||
req.getPrevGlobalHash(),
|
req.getPrevGlobalHash(), // старое поле, пока оставляем
|
||||||
req.getBlockBytesB64()
|
req.getBlockBytesB64()
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -73,8 +85,8 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
|
|||||||
resp.setReasonCode(r.reasonCode);
|
resp.setReasonCode(r.reasonCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
resp.setServerLastGlobalNumber(r.serverLastGlobalNumber);
|
resp.setServerLastGlobalNumber(r.serverLastBlockNumber);
|
||||||
resp.setServerLastGlobalHash(r.serverLastGlobalHashHex);
|
resp.setServerLastGlobalHash(r.serverLastBlockHashHex);
|
||||||
|
|
||||||
return resp;
|
return resp;
|
||||||
|
|
||||||
@ -85,313 +97,237 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
|
|||||||
|
|
||||||
private AddBlockResult addBlock(
|
private AddBlockResult addBlock(
|
||||||
String blockchainName,
|
String blockchainName,
|
||||||
int globalNumber,
|
int globalNumberFromReq,
|
||||||
String prevGlobalHashHex,
|
String prevGlobalHashHexFromReq,
|
||||||
String blockBytesB64
|
String blockBytesB64
|
||||||
) {
|
) {
|
||||||
if (blockchainName == null || blockchainName.isBlank()) {
|
if (blockchainName == null || blockchainName.isBlank()) {
|
||||||
log.warn("AddBlock: пустой blockchainName (globalNumber={})", globalNumber);
|
log.warn("AddBlock: пустой blockchainName (reqGlobalNumber={})", globalNumberFromReq);
|
||||||
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "empty_blockchain_name", 0, "");
|
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "empty_blockchain_name", 0, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
String login = BlockchainNameUtil.loginFromBlockchainName(blockchainName);
|
String login = BlockchainNameUtil.loginFromBlockchainName(blockchainName);
|
||||||
if (login == null || login.isBlank()) {
|
if (login == null || login.isBlank()) {
|
||||||
log.warn("AddBlock: плохой blockchainName='{}' => login не получился (globalNumber={})",
|
log.warn("AddBlock: плохой blockchainName='{}' => login не получился (reqGlobalNumber={})",
|
||||||
blockchainName, globalNumber);
|
blockchainName, globalNumberFromReq);
|
||||||
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_blockchain_name", 0, "");
|
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_blockchain_name", 0, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------
|
// 1) state обязателен
|
||||||
// ✅ 1) state теперь ОБЯЗАТЕЛЕН (и ключ подписи берём из него)
|
|
||||||
// -------------------------------------------------------------------
|
|
||||||
final BlockchainStateEntry st;
|
final BlockchainStateEntry st;
|
||||||
try {
|
try {
|
||||||
st = stateDAO.getByBlockchainName(blockchainName);
|
st = stateDAO.getByBlockchainName(blockchainName);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("AddBlock: ошибка БД при чтении blockchain_state (login={}, blockchainName={}, globalNumber={})",
|
log.error("AddBlock: ошибка БД при чтении blockchain_state (login={}, blockchainName={}, reqGlobalNumber={})",
|
||||||
login, blockchainName, globalNumber, e);
|
login, blockchainName, globalNumberFromReq, e);
|
||||||
return new AddBlockResult(WireCodes.Status.INTERNAL_ERROR, "db_error", 0, "");
|
return new AddBlockResult(WireCodes.Status.INTERNAL_ERROR, "db_error", 0, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (st == null) {
|
if (st == null) {
|
||||||
log.warn("AddBlock: blockchain_state_not_found (login={}, blockchainName={}, globalNumber={})",
|
log.warn("AddBlock: blockchain_state_not_found (login={}, blockchainName={}, reqGlobalNumber={})",
|
||||||
login, blockchainName, globalNumber);
|
login, blockchainName, globalNumberFromReq);
|
||||||
return new AddBlockResult(WireCodes.Status.NOT_FOUND, "blockchain_state_not_found", -1, "");
|
return new AddBlockResult(WireCodes.Status.NOT_FOUND, "blockchain_state_not_found", -1, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
final int serverLastNum = st.getLastGlobalNumber();
|
final int serverLastNum = st.getLastBlockNumber();
|
||||||
final String serverLastHashHex = toHex(nnBytes(st.getLastGlobalHash()));
|
final byte[] serverLastHash32 = (serverLastNum < 0)
|
||||||
|
? new byte[32]
|
||||||
|
: require32OrThrow(st.getLastBlockHash(), "state.last_block_hash is null/invalid");
|
||||||
|
|
||||||
// ✅ для genesis ожидаем, что state уже в начальном состоянии (-1)
|
final String serverLastHashHex = toHex(serverLastHash32);
|
||||||
if (globalNumber == 0 && serverLastNum != -1) {
|
|
||||||
log.warn("AddBlock: genesis_but_state_not_initial (login={}, blockchainName={}, stateLastGlobalNumber={})",
|
|
||||||
login, blockchainName, serverLastNum);
|
|
||||||
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "genesis_but_state_not_initial", serverLastNum, serverLastHashHex);
|
|
||||||
}
|
|
||||||
|
|
||||||
// следующий global строго
|
// 2) decode block
|
||||||
int expectedGlobal = serverLastNum + 1;
|
|
||||||
if (globalNumber != expectedGlobal) {
|
|
||||||
log.warn("AddBlock: bad_global_number (login={}, blockchainName={}, пришёл={}, ожидали={}, serverLastNum={}, serverLastHash={})",
|
|
||||||
login, blockchainName, globalNumber, expectedGlobal, serverLastNum, serverLastHashHex);
|
|
||||||
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_global_number", serverLastNum, serverLastHashHex);
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------------------------------------------------------------------
|
|
||||||
// ✅ 2) Декодируем блок
|
|
||||||
// -------------------------------------------------------------------
|
|
||||||
final byte[] blockBytes;
|
final byte[] blockBytes;
|
||||||
try {
|
try {
|
||||||
blockBytes = decodeBase64(blockBytesB64);
|
blockBytes = decodeBase64(blockBytesB64);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("AddBlock: некорректный base64 блока (login={}, blockchainName={}, globalNumber={})",
|
log.warn("AddBlock: некорректный base64 блока (login={}, blockchainName={}, reqGlobalNumber={})",
|
||||||
login, blockchainName, globalNumber, e);
|
login, blockchainName, globalNumberFromReq, e);
|
||||||
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_block_base64", serverLastNum, serverLastHashHex);
|
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_block_base64", serverLastNum, serverLastHashHex);
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------
|
// 3) лимит (оставляем как было)
|
||||||
// ✅ 3) Ранняя проверка лимита
|
|
||||||
// -------------------------------------------------------------------
|
|
||||||
try {
|
try {
|
||||||
long oldSize = st.getFileSizeBytes();
|
long oldSize = st.getFileSizeBytes();
|
||||||
long limit = st.getSizeLimit();
|
long limit = st.getSizeLimit();
|
||||||
long newSize = safeAdd(oldSize, blockBytes.length);
|
long newSize = safeAdd(oldSize, blockBytes.length);
|
||||||
|
|
||||||
if (limit > 0 && newSize > limit) {
|
if (limit > 0 && newSize > limit) {
|
||||||
log.warn("AddBlock: limit_exceeded (login={}, blockchainName={}, globalNumber={}, oldSize={}, addLen={}, newSize={}, limit={})",
|
log.warn("AddBlock: limit_exceeded (login={}, blockchainName={}, oldSize={}, addLen={}, newSize={}, limit={})",
|
||||||
login, blockchainName, globalNumber, oldSize, blockBytes.length, newSize, limit);
|
login, blockchainName, oldSize, blockBytes.length, newSize, limit);
|
||||||
return new AddBlockResult(413, "limit_exceeded", serverLastNum, serverLastHashHex);
|
return new AddBlockResult(413, "limit_exceeded", serverLastNum, serverLastHashHex);
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("AddBlock: limit_check_failed (login={}, blockchainName={}, globalNumber={})",
|
log.error("AddBlock: limit_check_failed (login={}, blockchainName={})", login, blockchainName, e);
|
||||||
login, blockchainName, globalNumber, e);
|
|
||||||
return new AddBlockResult(WireCodes.Status.INTERNAL_ERROR, "limit_check_failed", serverLastNum, serverLastHashHex);
|
return new AddBlockResult(WireCodes.Status.INTERNAL_ERROR, "limit_check_failed", serverLastNum, serverLastHashHex);
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------
|
// 4) parse block
|
||||||
// ✅ 4) Парсим блок
|
|
||||||
// -------------------------------------------------------------------
|
|
||||||
final BchBlockEntry block;
|
final BchBlockEntry block;
|
||||||
try {
|
try {
|
||||||
block = new BchBlockEntry(blockBytes);
|
block = new BchBlockEntry(blockBytes);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("AddBlock: не удалось распарсить BchBlockEntry (login={}, blockchainName={}, globalNumber={}, bytesLen={})",
|
log.warn("AddBlock: не удалось распарсить BchBlockEntry (login={}, blockchainName={}, bytesLen={})",
|
||||||
login, blockchainName, globalNumber, blockBytes.length, e);
|
login, blockchainName, blockBytes.length, e);
|
||||||
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_block_format", serverLastNum, serverLastHashHex);
|
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_block_format", serverLastNum, serverLastHashHex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// body.check()
|
||||||
try {
|
try {
|
||||||
block.body.check();
|
block.body.check();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("AddBlock: body.check() не прошёл (login={}, blockchainName={}, globalNumber={}, bodyType={}, bodyVersion={})",
|
log.warn("AddBlock: body.check() не прошёл (login={}, blockchainName={}, blockNumber={}, type={}, ver={})",
|
||||||
login, blockchainName, globalNumber, safeBodyType(block), safeBodyVersion(block), e);
|
login, blockchainName, block.blockNumber, (block.type & 0xFFFF), (block.version & 0xFFFF), e);
|
||||||
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_block_body", serverLastNum, serverLastHashHex);
|
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_block_body", serverLastNum, serverLastHashHex);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (block.recordNumber != globalNumber) {
|
// 4.2) запрет дырок: blockNumber строго last+1
|
||||||
log.warn("AddBlock: global_number_mismatch (login={}, blockchainName={}, заявлен={}, внутриБлока={})",
|
int expectedBlockNumber = serverLastNum + 1;
|
||||||
login, blockchainName, globalNumber, block.recordNumber);
|
if (block.blockNumber != expectedBlockNumber) {
|
||||||
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "global_number_mismatch", serverLastNum, serverLastHashHex);
|
log.warn("AddBlock: bad_block_number (login={}, blockchainName={}, пришёл={}, ожидали={}, serverLastNum={})",
|
||||||
|
login, blockchainName, block.blockNumber, expectedBlockNumber, serverLastNum);
|
||||||
|
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_block_number", serverLastNum, serverLastHashHex);
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------
|
// (временная совместимость) req.globalNumber должен совпасть с block.blockNumber
|
||||||
// ✅ 5) Ключ подписи берём из blockchain_state.blockchainKey (Base64(32))
|
if (globalNumberFromReq != block.blockNumber) {
|
||||||
// -------------------------------------------------------------------
|
log.warn("AddBlock: req_global_mismatch (login={}, blockchainName={}, reqGlobal={}, blockNumber={})",
|
||||||
final byte[] solanaKey32;
|
login, blockchainName, globalNumberFromReq, block.blockNumber);
|
||||||
try {
|
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "req_global_mismatch", serverLastNum, serverLastHashHex);
|
||||||
solanaKey32 = st.getBlockchainKeyBytes();
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.warn("AddBlock: bad_blockchain_key_in_state (login={}, blockchainName={}, globalNumber={})",
|
|
||||||
login, blockchainName, globalNumber, e);
|
|
||||||
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_blockchain_key_in_state", serverLastNum, serverLastHashHex);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (solanaKey32 == null || solanaKey32.length != 32) {
|
// 4.3) проверка цепочки по prevHash32
|
||||||
log.warn("AddBlock: bad_blockchain_key_len (login={}, blockchainName={}, globalNumber={}, keyLen={})",
|
if (!Arrays.equals(block.prevHash32, serverLastHash32)) {
|
||||||
login, blockchainName, globalNumber, (solanaKey32 == null ? -1 : solanaKey32.length));
|
log.warn("AddBlock: bad_prev_hash (login={}, blockchainName={}, blockNumber={}, clientPrev={}, serverPrev={})",
|
||||||
|
login, blockchainName, block.blockNumber, toHex(block.prevHash32), serverLastHashHex);
|
||||||
|
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_prev_hash", serverLastNum, serverLastHashHex);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5) pubKey
|
||||||
|
final byte[] pubKey32 = st.getBlockchainKeyBytes();
|
||||||
|
if (pubKey32 == null || pubKey32.length != 32) {
|
||||||
|
log.warn("AddBlock: bad_blockchain_key_len (login={}, blockchainName={}, blockNumber={}, keyLen={})",
|
||||||
|
login, blockchainName, block.blockNumber, (pubKey32 == null ? -1 : pubKey32.length));
|
||||||
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_blockchain_key_len", serverLastNum, serverLastHashHex);
|
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_blockchain_key_len", serverLastNum, serverLastHashHex);
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------
|
// 6) подпись по hash32(preimage)
|
||||||
// ✅ 6) prevGlobalHash сравниваем со state.lastGlobalHash (оба byte[32])
|
boolean sigOk;
|
||||||
// -------------------------------------------------------------------
|
|
||||||
final byte[] prevGlobalHash32;
|
|
||||||
try {
|
try {
|
||||||
prevGlobalHash32 = hexTo32(nn(prevGlobalHashHex)); // "" -> 32 нуля
|
sigOk = BchCryptoVerifier.verifyBlock(block, pubKey32);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("AddBlock: bad_prev_global_hash_format (login={}, blockchainName={}, globalNumber={}, prevGlobalHashHex='{}')",
|
log.warn("AddBlock: signature_verify_failed (login={}, blockchainName={}, blockNumber={})",
|
||||||
login, blockchainName, globalNumber, nn(prevGlobalHashHex), e);
|
login, blockchainName, block.blockNumber, e);
|
||||||
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_prev_global_hash_format", serverLastNum, serverLastHashHex);
|
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_signature", serverLastNum, serverLastHashHex);
|
||||||
}
|
}
|
||||||
|
|
||||||
final byte[] serverPrevGlobal32 = serverLastNum < 0 ? new byte[32] : nnBytes(st.getLastGlobalHash());
|
if (!sigOk) {
|
||||||
if (!bytesEq(prevGlobalHash32, serverPrevGlobal32)) {
|
log.warn("AddBlock: bad_signature (login={}, blockchainName={}, blockNumber={})",
|
||||||
log.warn("AddBlock: bad_prev_global_hash (login={}, blockchainName={}, globalNumber={}, clientPrev='{}', serverPrev='{}')",
|
login, blockchainName, block.blockNumber);
|
||||||
login, blockchainName, globalNumber, nn(prevGlobalHashHex), toHex(serverPrevGlobal32));
|
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_signature", serverLastNum, serverLastHashHex);
|
||||||
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_prev_global_hash", serverLastNum, serverLastHashHex);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===========================
|
// 7) линейная проверка (только для типов с линией)
|
||||||
// ЛИНИИ (строго)
|
Integer prevLineNumber = null;
|
||||||
// ===========================
|
byte[] prevLineHash32 = null;
|
||||||
int li = block.lineIndex;
|
Integer thisLineNumber = null;
|
||||||
int ln = block.lineNumber;
|
|
||||||
|
|
||||||
if (globalNumber == 0) {
|
if (block.body instanceof BodyHasLine bl) {
|
||||||
if (li != 0 || ln != 0) {
|
prevLineNumber = bl.prevLineNumber();
|
||||||
log.warn("AddBlock: bad_genesis_line_fields (login={}, blockchainName={}, lineIndex={}, lineNumber={})",
|
prevLineHash32 = bl.prevLineHash32();
|
||||||
login, blockchainName, li, ln);
|
thisLineNumber = bl.thisLineNumber();
|
||||||
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_genesis_line_fields", serverLastNum, serverLastHashHex);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (li == 0) {
|
|
||||||
log.warn("AddBlock: line0_only_genesis (login={}, blockchainName={}, globalNumber={}, lineIndex={})",
|
|
||||||
login, blockchainName, globalNumber, li);
|
|
||||||
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "line0_only_genesis", serverLastNum, serverLastHashHex);
|
|
||||||
}
|
|
||||||
if (li < 1 || li > 7) {
|
|
||||||
log.warn("AddBlock: bad_line_index (login={}, blockchainName={}, globalNumber={}, lineIndex={})",
|
|
||||||
login, blockchainName, globalNumber, li);
|
|
||||||
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_line_index", serverLastNum, serverLastHashHex);
|
|
||||||
}
|
|
||||||
|
|
||||||
int expectedLineNumber = st.getLastLineNumber(li) + 1;
|
if (prevLineNumber != null && prevLineNumber != -1) {
|
||||||
if (ln != expectedLineNumber) {
|
|
||||||
log.warn("AddBlock: bad_line_number (login={}, blockchainName={}, globalNumber={}, lineIndex={}, пришёлLineNumber={}, ожидалиLineNumber={}, lastLineNumber={})",
|
|
||||||
login, blockchainName, globalNumber, li, ln, expectedLineNumber, st.getLastLineNumber(li));
|
|
||||||
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_line_number", serverLastNum, serverLastHashHex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final byte[] prevLineHash32;
|
|
||||||
try {
|
try {
|
||||||
prevLineHash32 = computePrevLineHash32(st, li);
|
byte[] dbPrevHash = blocksDAO.getHashByNumber(blockchainName, prevLineNumber);
|
||||||
|
if (dbPrevHash == null) {
|
||||||
|
log.warn("AddBlock: prev_line_block_not_found (login={}, blockchainName={}, blockNumber={}, prevLineNumber={})",
|
||||||
|
login, blockchainName, block.blockNumber, prevLineNumber);
|
||||||
|
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "prev_line_block_not_found", serverLastNum, serverLastHashHex);
|
||||||
|
}
|
||||||
|
if (!Arrays.equals(dbPrevHash, require32OrThrow(prevLineHash32, "prevLineHash32 invalid"))) {
|
||||||
|
log.warn("AddBlock: bad_prev_line_hash (login={}, blockchainName={}, blockNumber={}, prevLineNumber={})",
|
||||||
|
login, blockchainName, block.blockNumber, prevLineNumber);
|
||||||
|
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_prev_line_hash", serverLastNum, serverLastHashHex);
|
||||||
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("AddBlock: bad_prev_line_hash_in_state (login={}, blockchainName={}, globalNumber={}, lineIndex={})",
|
log.error("AddBlock: db_error_prev_line_check (login={}, blockchainName={}, blockNumber={})",
|
||||||
login, blockchainName, globalNumber, li, e);
|
login, blockchainName, block.blockNumber, e);
|
||||||
return new AddBlockResult(WireCodes.Status.INTERNAL_ERROR, "bad_prev_line_hash_in_state", serverLastNum, serverLastHashHex);
|
return new AddBlockResult(WireCodes.Status.INTERNAL_ERROR, "db_error_prev_line_check", serverLastNum, serverLastHashHex);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean ok = BchCryptoVerifier.verifyAll(
|
// 8) сформировать запись и записать (DB + state + файл)
|
||||||
login,
|
|
||||||
prevGlobalHash32,
|
|
||||||
prevLineHash32,
|
|
||||||
block.getRawBytes(),
|
|
||||||
block.getSignature64(),
|
|
||||||
solanaKey32,
|
|
||||||
block.getHash32()
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!ok) {
|
|
||||||
log.warn("AddBlock: bad_signature_or_hash (login={}, blockchainName={}, globalNumber={}, lineIndex={}, lineNumber={})",
|
|
||||||
login, blockchainName, globalNumber, li, ln);
|
|
||||||
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_signature_or_hash", serverLastNum, serverLastHashHex);
|
|
||||||
}
|
|
||||||
|
|
||||||
// write
|
|
||||||
try {
|
try {
|
||||||
dbWriter.appendBlockAndState(
|
BlockEntry be = new BlockEntry();
|
||||||
login,
|
be.setLogin(login);
|
||||||
blockchainName,
|
be.setBchName(blockchainName);
|
||||||
prevGlobalHash32,
|
|
||||||
prevLineHash32,
|
be.setBlockNumber(block.blockNumber);
|
||||||
block,
|
be.setMsgType(block.type & 0xFFFF);
|
||||||
st
|
be.setMsgSubType(block.subType & 0xFFFF);
|
||||||
);
|
|
||||||
|
be.setBlockBytes(block.toBytes());
|
||||||
|
be.setBlockHash(block.getHash32());
|
||||||
|
be.setBlockSignature(block.getSignature64());
|
||||||
|
|
||||||
|
// line columns (optional)
|
||||||
|
be.setPrevLineNumber(prevLineNumber);
|
||||||
|
be.setPrevLineHash(prevLineHash32);
|
||||||
|
be.setThisLineNumber(thisLineNumber);
|
||||||
|
|
||||||
|
// target columns (optional)
|
||||||
|
if (block.body instanceof BodyHasTarget t) {
|
||||||
|
be.setToLogin(t.toLogin());
|
||||||
|
be.setToBchName(t.toBchName());
|
||||||
|
be.setToBlockNumber(t.toBlockGlobalNumber());
|
||||||
|
be.setToBlockHash(t.toBlockHasheBytes());
|
||||||
|
}
|
||||||
|
|
||||||
|
// edit helper (optional): если TEXT_EDIT — это "редактирование блока цели"
|
||||||
|
if ((block.type & 0xFFFF) == 1 && (block.subType & 0xFFFF) == 10 && be.getToBlockNumber() != null) {
|
||||||
|
be.setEditedByBlockNumber(be.getToBlockNumber());
|
||||||
|
}
|
||||||
|
|
||||||
|
dbWriter.appendBlockAndState(blockchainName, block, st, be);
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("AddBlock: внутренняя ошибка при записи блока (login={}, blockchainName={}, globalNumber={})",
|
log.error("AddBlock: внутренняя ошибка при записи блока (login={}, blockchainName={}, blockNumber={})",
|
||||||
login, blockchainName, globalNumber, e);
|
login, blockchainName, block.blockNumber, e);
|
||||||
return new AddBlockResult(WireCodes.Status.INTERNAL_ERROR, "internal_error", serverLastNum, serverLastHashHex);
|
return new AddBlockResult(WireCodes.Status.INTERNAL_ERROR, "internal_error", serverLastNum, serverLastHashHex);
|
||||||
}
|
}
|
||||||
|
|
||||||
String newHashHex = toHex(block.getHash32());
|
String newHashHex = toHex(block.getHash32());
|
||||||
|
|
||||||
log.info("✅ AddBlock ok: login={}, blockchainName={}, globalNumber={}, lineIndex={}, lineNumber={}, newHash={}",
|
log.info("✅ AddBlock ok: login={}, blockchainName={}, blockNumber={}, newHash={}",
|
||||||
login, blockchainName, globalNumber, li, ln, newHashHex);
|
login, blockchainName, block.blockNumber, newHashHex);
|
||||||
|
|
||||||
return new AddBlockResult(WireCodes.Status.OK, null, globalNumber, newHashHex);
|
return new AddBlockResult(WireCodes.Status.OK, null, block.blockNumber, newHashHex);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/* ===================================================================== */
|
||||||
* ✅ Правило:
|
/* ====================== Helpers ====================================== */
|
||||||
* - lineIndex=0: prevLineHash = 32 нулей
|
/* ===================================================================== */
|
||||||
* - lineIndex>0:
|
|
||||||
* - если в этой линии ещё нет блоков (lastLineNumber==0) => prevLineHash = hash(genesis) (line0 hash)
|
private static byte[] decodeBase64(String b64) {
|
||||||
* - иначе => prevLineHash = lastLineHash(lineIndex)
|
if (b64 == null) throw new IllegalArgumentException("blockBytesB64 == null");
|
||||||
*/
|
return Base64.getDecoder().decode(b64);
|
||||||
private static byte[] computePrevLineHash32(BlockchainStateEntry st, int lineIndex) {
|
|
||||||
if (lineIndex == 0) {
|
|
||||||
return new byte[32];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int lastLn = st.getLastLineNumber(lineIndex);
|
private static long safeAdd(long a, long b) {
|
||||||
if (lastLn == 0) {
|
long r = a + b;
|
||||||
byte[] genesis = nnBytes(st.getLastLineHash(0));
|
if (((a ^ r) & (b ^ r)) < 0) throw new ArithmeticException("long overflow");
|
||||||
if (genesis.length == 32) return genesis;
|
return r;
|
||||||
|
|
||||||
byte[] g = nnBytes(st.getLastGlobalHash());
|
|
||||||
if (g.length == 32) return g;
|
|
||||||
|
|
||||||
return new byte[32];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
byte[] last = nnBytes(st.getLastLineHash(lineIndex));
|
private static byte[] require32OrThrow(byte[] b, String msg) {
|
||||||
return last.length == 32 ? last : new byte[32];
|
if (b == null || b.length != 32) throw new IllegalArgumentException(msg);
|
||||||
}
|
return b;
|
||||||
|
|
||||||
private static final class AddBlockResult {
|
|
||||||
final int httpStatus;
|
|
||||||
final String reasonCode;
|
|
||||||
final int serverLastGlobalNumber;
|
|
||||||
final String serverLastGlobalHashHex;
|
|
||||||
|
|
||||||
AddBlockResult(int httpStatus, String reasonCode, int serverLastGlobalNumber, String serverLastGlobalHashHex) {
|
|
||||||
this.httpStatus = httpStatus;
|
|
||||||
this.reasonCode = reasonCode;
|
|
||||||
this.serverLastGlobalNumber = serverLastGlobalNumber;
|
|
||||||
this.serverLastGlobalHashHex = serverLastGlobalHashHex;
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean isOk() {
|
|
||||||
return httpStatus == WireCodes.Status.OK;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String nn(String s) { return s == null ? "" : s; }
|
|
||||||
|
|
||||||
private static byte[] nnBytes(byte[] b) { return b == null ? new byte[0] : b; }
|
|
||||||
|
|
||||||
private static byte[] decodeBase64(String s) {
|
|
||||||
if (s == null || s.isBlank()) throw new IllegalArgumentException("empty base64");
|
|
||||||
return Base64.getDecoder().decode(s);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** hex(64) -> 32 bytes; пустой -> 32 нуля */
|
|
||||||
private static byte[] hexTo32(String hex) {
|
|
||||||
if (hex == null || hex.isBlank()) return new byte[32];
|
|
||||||
String h = hex.trim();
|
|
||||||
if (h.length() != 64) throw new IllegalArgumentException("hex hash must be 64 chars");
|
|
||||||
byte[] out = new byte[32];
|
|
||||||
for (int i = 0; i < 32; i++) {
|
|
||||||
int hi = Character.digit(h.charAt(i * 2), 16);
|
|
||||||
int lo = Character.digit(h.charAt(i * 2 + 1), 16);
|
|
||||||
if (hi < 0 || lo < 0) throw new IllegalArgumentException("bad hex");
|
|
||||||
out[i] = (byte)((hi << 4) | lo);
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean bytesEq(byte[] a, byte[] b) {
|
|
||||||
if (a == b) return true;
|
|
||||||
if (a == null || b == null) return false;
|
|
||||||
if (a.length != b.length) return false;
|
|
||||||
int x = 0;
|
|
||||||
for (int i = 0; i < a.length; i++) x |= (a[i] ^ b[i]);
|
|
||||||
return x == 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String toHex(byte[] bytes) {
|
private static String toHex(byte[] bytes) {
|
||||||
if (bytes == null || bytes.length == 0) return "";
|
if (bytes == null) return "null";
|
||||||
char[] HEX = "0123456789abcdef".toCharArray();
|
char[] HEX = "0123456789abcdef".toCharArray();
|
||||||
char[] out = new char[bytes.length * 2];
|
char[] out = new char[bytes.length * 2];
|
||||||
for (int i = 0; i < bytes.length; i++) {
|
for (int i = 0; i < bytes.length; i++) {
|
||||||
@ -402,19 +338,19 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
|
|||||||
return new String(out);
|
return new String(out);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String safeBodyType(BchBlockEntry b) {
|
private static final class AddBlockResult {
|
||||||
try { return String.valueOf(b.body.type()); } catch (Exception e) { return "unknown"; }
|
final int httpStatus;
|
||||||
|
final String reasonCode;
|
||||||
|
final int serverLastBlockNumber;
|
||||||
|
final String serverLastBlockHashHex;
|
||||||
|
|
||||||
|
AddBlockResult(int httpStatus, String reasonCode, int serverLastBlockNumber, String serverLastBlockHashHex) {
|
||||||
|
this.httpStatus = httpStatus;
|
||||||
|
this.reasonCode = reasonCode;
|
||||||
|
this.serverLastBlockNumber = serverLastBlockNumber;
|
||||||
|
this.serverLastBlockHashHex = serverLastBlockHashHex;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String safeBodyVersion(BchBlockEntry b) {
|
boolean isOk() { return httpStatus == WireCodes.Status.OK; }
|
||||||
try { return String.valueOf(b.body.version()); } catch (Exception e) { return "unknown"; }
|
|
||||||
}
|
|
||||||
|
|
||||||
private static long safeAdd(long x, long y) {
|
|
||||||
long r = x + y;
|
|
||||||
if (((x ^ r) & (y ^ r)) < 0) {
|
|
||||||
throw new IllegalArgumentException("overflow: " + x + " + " + y);
|
|
||||||
}
|
|
||||||
return r;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,342 +1,71 @@
|
|||||||
// =======================
|
// =======================
|
||||||
// BlockchainWriter.java (НОВАЯ ВЕРСИЯ)
|
// server/logic/ws_protocol/JSON/handlers/blockchain/Net_AddBlock_Handler_utils/BlockchainWriter.java
|
||||||
|
// (НОВАЯ ВЕРСИЯ — чтобы AddBlock работал с новым blocks/state)
|
||||||
// =======================
|
// =======================
|
||||||
package server.logic.ws_protocol.JSON.handlers.blockchain.Net_AddBlock_Handler_utils;
|
package server.logic.ws_protocol.JSON.handlers.blockchain.Net_AddBlock_Handler_utils;
|
||||||
|
|
||||||
import blockchain.BchBlockEntry;
|
import blockchain.BchBlockEntry;
|
||||||
import blockchain.body.BodyHasTarget;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import shine.db.SqliteDbController;
|
|
||||||
import shine.db.dao.BlockchainStateDAO;
|
import shine.db.dao.BlockchainStateDAO;
|
||||||
import shine.db.dao.BlocksDAO;
|
import shine.db.dao.BlocksDAO;
|
||||||
import shine.db.entities.BlockEntry;
|
|
||||||
import shine.db.entities.BlockchainStateEntry;
|
import shine.db.entities.BlockchainStateEntry;
|
||||||
import utils.blockchain.BlockchainNameUtil;
|
import shine.db.entities.BlockEntry;
|
||||||
import utils.files.FileStoreUtil;
|
import utils.files.FileStoreUtil;
|
||||||
import shine.log.BlockchainAdminNotifier;
|
|
||||||
|
|
||||||
import java.sql.Connection;
|
import java.sql.Connection;
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
import java.sql.Types;
|
|
||||||
import java.util.Base64;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* BlockchainWriter — единая точка записи:
|
* BlockchainWriter — запись блока в DB + обновление state + запись в файл.
|
||||||
* 1) создаём новый файл <name>.tmp_bch = oldFileBytes + newBlockBytes
|
*
|
||||||
* 2) атомарно фиксируем в БД:
|
* ВАЖНО:
|
||||||
* - blocks (строка блока)
|
* - Это минимальный рабочий вариант под новый формат.
|
||||||
* - blockchain_state (включая новый fileSizeBytes)
|
* - Если у тебя уже есть "атомарность" сложнее (tmp_bch + commit/recovery) — можно усилить потом.
|
||||||
* 3) атомарно заменяем файл:
|
|
||||||
* - удаляем/замещаем старый <name>.bch
|
|
||||||
* - переименовываем <name>.tmp_bch -> <name>.bch
|
|
||||||
*/
|
*/
|
||||||
public final class BlockchainWriter {
|
public final class BlockchainWriter {
|
||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(BlockchainWriter.class);
|
|
||||||
|
|
||||||
private final SqliteDbController db;
|
|
||||||
private final BlocksDAO blocksDAO;
|
private final BlocksDAO blocksDAO;
|
||||||
private final BlockchainStateDAO stateDAO;
|
private final BlockchainStateDAO stateDAO;
|
||||||
private final FileStoreUtil fs;
|
private final FileStoreUtil fs = FileStoreUtil.getInstance();
|
||||||
|
|
||||||
public BlockchainWriter(BlocksDAO blocksDAO, BlockchainStateDAO stateDAO) {
|
public BlockchainWriter(BlocksDAO blocksDAO, BlockchainStateDAO stateDAO) {
|
||||||
this.db = SqliteDbController.getInstance();
|
|
||||||
this.blocksDAO = blocksDAO;
|
this.blocksDAO = blocksDAO;
|
||||||
this.stateDAO = stateDAO;
|
this.stateDAO = stateDAO;
|
||||||
this.fs = FileStoreUtil.getInstance();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void appendBlockAndState(
|
public void appendBlockAndState(String blockchainName,
|
||||||
String login,
|
|
||||||
String blockchainName,
|
|
||||||
byte[] prevGlobalHash32,
|
|
||||||
byte[] prevLineHash32,
|
|
||||||
BchBlockEntry block,
|
BchBlockEntry block,
|
||||||
BlockchainStateEntry stOrNull
|
BlockchainStateEntry st,
|
||||||
) throws SQLException {
|
BlockEntry be) throws SQLException {
|
||||||
|
|
||||||
if (stOrNull == null) {
|
long nowMs = System.currentTimeMillis();
|
||||||
throw new SQLException("blockchain_state not found for blockchainName=" + blockchainName + " (state обязателен)");
|
|
||||||
}
|
|
||||||
|
|
||||||
verifyMainFileSizeMatchesStateOrAlert(login, blockchainName, block, stOrNull);
|
try (Connection c = shine.db.SqliteDbController.getInstance().getConnection()) {
|
||||||
|
|
||||||
// bytes FULL блока (raw+sig+hash)
|
|
||||||
final byte[] newBlockFullBytes = block.toBytes();
|
|
||||||
|
|
||||||
final long oldFileSize = stOrNull.getFileSizeBytes();
|
|
||||||
final long newFileSize = safeAdd(oldFileSize, newBlockFullBytes.length);
|
|
||||||
|
|
||||||
// tmp = old + new
|
|
||||||
final byte[] tmpBytes;
|
|
||||||
if (oldFileSize == 0) {
|
|
||||||
tmpBytes = newBlockFullBytes;
|
|
||||||
} else {
|
|
||||||
byte[] oldBytes;
|
|
||||||
try {
|
|
||||||
oldBytes = fs.readBlockchain(blockchainName);
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("Ошибка чтения старого файла блокчейна перед записью tmp (login={}, blockchainName={}, oldFileSize={}, blockNumber={})",
|
|
||||||
login, blockchainName, oldFileSize, block.recordNumber, e);
|
|
||||||
throw new SQLException("Cannot read old blockchain file for: " + blockchainName, e);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (oldBytes.length != (int) oldFileSize) {
|
|
||||||
String msg =
|
|
||||||
"Несовпадение размера файла блокчейна при чтении: " +
|
|
||||||
"state ожидал oldFileSize=" + oldFileSize +
|
|
||||||
", а реально прочитали oldBytes.length=" + oldBytes.length +
|
|
||||||
" (login=" + login +
|
|
||||||
", blockchainName=" + blockchainName +
|
|
||||||
", blockNumber=" + block.recordNumber + ").";
|
|
||||||
BlockchainAdminNotifier.critical(msg, null);
|
|
||||||
throw new SQLException(msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
tmpBytes = concat(oldBytes, newBlockFullBytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
fs.writeBlockchainTmp(blockchainName, tmpBytes);
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("Ошибка записи tmp файла блокчейна (login={}, blockchainName={}, tmpBytesLen={}, oldFileSize={}, newFileSize={}, blockNumber={})",
|
|
||||||
login, blockchainName, tmpBytes.length, oldFileSize, newFileSize, block.recordNumber, e);
|
|
||||||
throw new SQLException("Cannot write tmp blockchain file for: " + blockchainName, e);
|
|
||||||
}
|
|
||||||
|
|
||||||
// атомарно БД
|
|
||||||
try (Connection c = db.getConnection()) {
|
|
||||||
|
|
||||||
boolean oldAutoCommit = c.getAutoCommit();
|
|
||||||
c.setAutoCommit(false);
|
c.setAutoCommit(false);
|
||||||
|
|
||||||
boolean committed = false;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
insertBlockRow(c, login, blockchainName, prevGlobalHash32, prevLineHash32, block);
|
// 1) insert block
|
||||||
appendState(c, blockchainName, block, stOrNull, newFileSize);
|
blocksDAO.insert(c, be);
|
||||||
|
|
||||||
c.commit();
|
// 2) update state
|
||||||
committed = true;
|
st.setLastBlockNumber(block.blockNumber);
|
||||||
|
st.setLastBlockHash(block.getHash32());
|
||||||
} catch (Exception e) {
|
st.setFileSizeBytes(st.getFileSizeBytes() + block.toBytes().length);
|
||||||
try { c.rollback(); } catch (SQLException ignore) {}
|
st.setUpdatedAtMs(nowMs);
|
||||||
|
|
||||||
log.error("Ошибка транзакции БД при добавлении блока (rollback выполнен) (login={}, blockchainName={}, blockNumber={}, oldFileSize={}, newFileSize={})",
|
|
||||||
login, blockchainName, block.recordNumber, oldFileSize, newFileSize, e);
|
|
||||||
|
|
||||||
if (e instanceof SQLException se) throw se;
|
|
||||||
throw new SQLException("appendBlockAndState failed (db tx)", e);
|
|
||||||
|
|
||||||
} finally {
|
|
||||||
try { c.setAutoCommit(oldAutoCommit); } catch (SQLException ignore) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
// после коммита БД — атомарно заменяем файл
|
|
||||||
if (committed) {
|
|
||||||
try {
|
|
||||||
fs.atomicReplaceBlockchainFile(blockchainName);
|
|
||||||
} catch (Exception moveError) {
|
|
||||||
log.error("БД закоммичена, но атомарная замена файла блокчейна не удалась. tmp оставлен для recovery. (login={}, blockchainName={}, blockNumber={})",
|
|
||||||
login, blockchainName, block.recordNumber, moveError);
|
|
||||||
|
|
||||||
throw new SQLException(
|
|
||||||
"DB committed but file replace failed; tmp kept for recovery. blockchainName=" + blockchainName,
|
|
||||||
moveError
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void verifyMainFileSizeMatchesStateOrAlert(
|
|
||||||
String login,
|
|
||||||
String blockchainName,
|
|
||||||
BchBlockEntry block,
|
|
||||||
BlockchainStateEntry stOrNull
|
|
||||||
) throws SQLException {
|
|
||||||
|
|
||||||
if (stOrNull == null) return;
|
|
||||||
|
|
||||||
long expected = stOrNull.getFileSizeBytes();
|
|
||||||
if (expected <= 0) return;
|
|
||||||
|
|
||||||
String mainFileName = fs.buildBlockchainFileName(blockchainName);
|
|
||||||
|
|
||||||
if (!fs.exists(mainFileName)) {
|
|
||||||
String msg =
|
|
||||||
"КРИТИЧЕСКАЯ ОШИБКА КОНСИСТЕНТНОСТИ: state ожидает основной файл, но его нет. " +
|
|
||||||
"login=" + login +
|
|
||||||
", blockchainName=" + blockchainName +
|
|
||||||
", expectedSizeFromState=" + expected +
|
|
||||||
", blockNumber=" + (block != null ? block.recordNumber : -1) + ".";
|
|
||||||
BlockchainAdminNotifier.critical(msg, null);
|
|
||||||
throw new SQLException(msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
long real;
|
|
||||||
try {
|
|
||||||
real = fs.size(mainFileName);
|
|
||||||
} catch (Exception e) {
|
|
||||||
String msg =
|
|
||||||
"КРИТИЧЕСКАЯ ОШИБКА: не удалось получить размер основного файла блокчейна. " +
|
|
||||||
"login=" + login +
|
|
||||||
", blockchainName=" + blockchainName +
|
|
||||||
", expectedSizeFromState=" + expected +
|
|
||||||
", blockNumber=" + (block != null ? block.recordNumber : -1) + ".";
|
|
||||||
BlockchainAdminNotifier.critical(msg, e);
|
|
||||||
throw new SQLException(msg, e);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (real != expected) {
|
|
||||||
String msg =
|
|
||||||
"КРИТИЧЕСКАЯ ОШИБКА КОНСИСТЕНТНОСТИ: размер файла блокчейна НЕ СОВПАДАЕТ с state. " +
|
|
||||||
"login=" + login +
|
|
||||||
", blockchainName=" + blockchainName +
|
|
||||||
", expectedSizeFromState=" + expected +
|
|
||||||
", realMainFileSize=" + real +
|
|
||||||
", blockNumber=" + (block != null ? block.recordNumber : -1) + ". " +
|
|
||||||
"Похоже на внешнее вмешательство/порчу файла. Запись нового блока остановлена.";
|
|
||||||
BlockchainAdminNotifier.critical(msg, null);
|
|
||||||
throw new SQLException(msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void appendState(
|
|
||||||
Connection c,
|
|
||||||
String blockchainName,
|
|
||||||
BchBlockEntry block,
|
|
||||||
BlockchainStateEntry stOrNull,
|
|
||||||
long newFileSizeBytes
|
|
||||||
) throws SQLException {
|
|
||||||
|
|
||||||
BlockchainStateEntry st = stOrNull;
|
|
||||||
if (st == null) {
|
|
||||||
throw new SQLException("blockchain_state not found for blockchainName=" + blockchainName);
|
|
||||||
}
|
|
||||||
|
|
||||||
// глобальная цепочка
|
|
||||||
st.setLastGlobalNumber(block.recordNumber);
|
|
||||||
st.setLastGlobalHash(block.getHash32());
|
|
||||||
|
|
||||||
// линия
|
|
||||||
int li = block.lineIndex;
|
|
||||||
st.setLastLineNumber(li, block.lineNumber);
|
|
||||||
st.setLastLineHash(li, block.getHash32());
|
|
||||||
|
|
||||||
// file size
|
|
||||||
st.setFileSizeBytes(newFileSizeBytes);
|
|
||||||
|
|
||||||
// timestamp
|
|
||||||
st.setUpdatedAtMs(System.currentTimeMillis());
|
|
||||||
|
|
||||||
stateDAO.upsert(c, st);
|
stateDAO.upsert(c, st);
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
c.commit();
|
||||||
* Вставка/апдейт строки блока в blocks (BLOB-вариант).
|
} catch (Exception e) {
|
||||||
*/
|
try { c.rollback(); } catch (Exception ignored) {}
|
||||||
private void insertBlockRow(
|
if (e instanceof SQLException se) throw se;
|
||||||
Connection c,
|
throw new SQLException("appendBlockAndState failed", e);
|
||||||
String login,
|
} finally {
|
||||||
String blockchainName,
|
try { c.setAutoCommit(true); } catch (Exception ignored) {}
|
||||||
byte[] prevGlobalHash32,
|
|
||||||
byte[] prevLineHash32,
|
|
||||||
BchBlockEntry block
|
|
||||||
) throws SQLException {
|
|
||||||
|
|
||||||
BlockEntry e = new BlockEntry();
|
|
||||||
|
|
||||||
e.setLogin(login);
|
|
||||||
e.setBchName(blockchainName);
|
|
||||||
|
|
||||||
e.setBlockGlobalNumber(block.recordNumber);
|
|
||||||
e.setBlockGlobalPreHashe(prevGlobalHash32);
|
|
||||||
|
|
||||||
e.setBlockLineIndex(block.lineIndex);
|
|
||||||
e.setBlockLineNumber(block.lineNumber);
|
|
||||||
e.setBlockLinePreHashe(prevLineHash32);
|
|
||||||
|
|
||||||
e.setMsgType(block.body.type());
|
|
||||||
e.setMsgSubType(block.body.subType());
|
|
||||||
|
|
||||||
// ВАЖНО: здесь ты кладёшь FULL bytes (raw+sig+hash). Это ок, ты так задумал.
|
|
||||||
e.setBlockByte(block.toBytes());
|
|
||||||
|
|
||||||
// to-поля
|
|
||||||
e.setToLogin(null);
|
|
||||||
e.setToBchName(null);
|
|
||||||
e.setToBlockGlobalNumber(null);
|
|
||||||
e.setToBlockHashe(null);
|
|
||||||
|
|
||||||
if (block.body instanceof BodyHasTarget tf) {
|
|
||||||
e.setToLogin(tf.toLogin());
|
|
||||||
e.setToBchName(tf.toBchName());
|
|
||||||
e.setToBlockGlobalNumber(tf.toBlockGlobalNumber());
|
|
||||||
e.setToBlockHashe(tf.toBlockHasheBytes());
|
|
||||||
|
|
||||||
// если to_login не пришёл, но есть to_bch_name — восстановим логин из имени цепочки
|
|
||||||
if (e.getToLogin() == null && e.getToBchName() != null) {
|
|
||||||
String toLogin = BlockchainNameUtil.loginFromBlockchainName(e.getToBchName());
|
|
||||||
if (toLogin != null && !toLogin.isBlank()) {
|
|
||||||
e.setToLogin(toLogin);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// новое: хэш и подпись самого блока
|
// 3) append to file (минимально: просто дописать)
|
||||||
e.setBlockHash(block.getHash32());
|
// Если у тебя уже есть логика tmp_bch+atomicReplace — можно заменить тут.
|
||||||
e.setBlockSignature(block.getSignature64());
|
String fileName = fs.buildBlockchainFileName(blockchainName);
|
||||||
|
fs.addDataToFile(fileName, block.toBytes());
|
||||||
// новое: не трогаем (NULL); триггер пометит исходный блок
|
|
||||||
e.setEditedByBlockGlobalNumber(null);
|
|
||||||
|
|
||||||
blocksDAO.upsert(c, e);
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------------------- utils --------------------
|
|
||||||
|
|
||||||
private static byte[] concat(byte[] a, byte[] b) {
|
|
||||||
byte[] out = new byte[a.length + b.length];
|
|
||||||
System.arraycopy(a, 0, out, 0, a.length);
|
|
||||||
System.arraycopy(b, 0, out, a.length, b.length);
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static long safeAdd(long x, long y) {
|
|
||||||
long r = x + y;
|
|
||||||
if (((x ^ r) & (y ^ r)) < 0) {
|
|
||||||
throw new IllegalArgumentException("fileSizeBytes overflow: " + x + " + " + y);
|
|
||||||
}
|
|
||||||
return r;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Если у тебя где-то ещё остался String-хэш (legacy), используй это в месте парсинга JSON,
|
|
||||||
// но НЕ в writer. Оставляю тут только на всякий случай для миграции:
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
private static byte[] decodeHashStringLenient(String s) {
|
|
||||||
if (s == null) return null;
|
|
||||||
String t = s.trim();
|
|
||||||
if (t.isEmpty()) return null;
|
|
||||||
|
|
||||||
// hex 64
|
|
||||||
if (t.length() == 64 && t.matches("^[0-9a-fA-F]+$")) {
|
|
||||||
byte[] out = new byte[32];
|
|
||||||
for (int i = 0; i < 32; i++) {
|
|
||||||
int hi = Character.digit(t.charAt(i * 2), 16);
|
|
||||||
int lo = Character.digit(t.charAt(i * 2 + 1), 16);
|
|
||||||
out[i] = (byte) ((hi << 4) | lo);
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
// base64 (часто у тебя так)
|
|
||||||
try {
|
|
||||||
byte[] b = Base64.getDecoder().decode(t);
|
|
||||||
return (b != null && b.length == 32) ? b : b;
|
|
||||||
} catch (IllegalArgumentException ignore) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,147 +1,147 @@
|
|||||||
package server.logic.ws_protocol.JSON.handlers.subscriptions;
|
//package server.logic.ws_protocol.JSON.handlers.subscriptions;
|
||||||
|
//
|
||||||
import blockchain.BchBlockEntry;
|
//import blockchain.BchBlockEntry;
|
||||||
import blockchain.body.TextBody;
|
//import blockchain.body.TextBody;
|
||||||
import org.slf4j.Logger;
|
//import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
//import org.slf4j.LoggerFactory;
|
||||||
import server.logic.ws_protocol.JSON.ConnectionContext;
|
//import server.logic.ws_protocol.JSON.ConnectionContext;
|
||||||
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
//import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||||
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
//import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
||||||
import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
|
//import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
|
||||||
import server.logic.ws_protocol.JSON.handlers.subscriptions.entyties.Net_GetSubscribedChannels_Request;
|
//import server.logic.ws_protocol.JSON.handlers.subscriptions.entyties.Net_GetSubscribedChannels_Request;
|
||||||
import server.logic.ws_protocol.JSON.handlers.subscriptions.entyties.Net_GetSubscribedChannels_Response;
|
//import server.logic.ws_protocol.JSON.handlers.subscriptions.entyties.Net_GetSubscribedChannels_Response;
|
||||||
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
//import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||||
import server.logic.ws_protocol.WireCodes;
|
//import server.logic.ws_protocol.WireCodes;
|
||||||
import shine.db.SqliteDbController;
|
//import shine.db.SqliteDbController;
|
||||||
import shine.db.dao.SubscriptionsDAO;
|
//import shine.db.dao.SubscriptionsDAO;
|
||||||
|
//
|
||||||
import java.sql.Connection;
|
//import java.sql.Connection;
|
||||||
import java.sql.SQLException;
|
//import java.sql.SQLException;
|
||||||
import java.util.ArrayList;
|
//import java.util.ArrayList;
|
||||||
import java.util.List;
|
//import java.util.List;
|
||||||
|
//
|
||||||
/**
|
///**
|
||||||
* Handler: GetSubscribedChannels
|
// * Handler: GetSubscribedChannels
|
||||||
*
|
// *
|
||||||
* Логика:
|
// * Логика:
|
||||||
* - DAO возвращает last publication orig bytes (+ edit bytes если есть)
|
// * - DAO возвращает last publication orig bytes (+ edit bytes если есть)
|
||||||
* - Handler парсит FULL bytes блока:
|
// * - Handler парсит FULL bytes блока:
|
||||||
* timestamp берём из ОРИГИНАЛА (publication)
|
// * timestamp берём из ОРИГИНАЛА (publication)
|
||||||
* текст берём из EDIT (если есть) иначе из оригинала
|
// * текст берём из EDIT (если есть) иначе из оригинала
|
||||||
* - формируем превью первых 50 символов
|
// * - формируем превью первых 50 символов
|
||||||
*/
|
// */
|
||||||
public class Net_GetSubscribedChannels_Handler implements JsonMessageHandler {
|
//public class Net_GetSubscribedChannels_Handler implements JsonMessageHandler {
|
||||||
|
//
|
||||||
private static final Logger log = LoggerFactory.getLogger(Net_GetSubscribedChannels_Handler.class);
|
// private static final Logger log = LoggerFactory.getLogger(Net_GetSubscribedChannels_Handler.class);
|
||||||
|
//
|
||||||
@Override
|
// @Override
|
||||||
public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) {
|
// public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) {
|
||||||
Net_GetSubscribedChannels_Request req = (Net_GetSubscribedChannels_Request) baseRequest;
|
// Net_GetSubscribedChannels_Request req = (Net_GetSubscribedChannels_Request) baseRequest;
|
||||||
|
//
|
||||||
if (req.getLogin() == null || req.getLogin().isBlank()) {
|
// if (req.getLogin() == null || req.getLogin().isBlank()) {
|
||||||
return NetExceptionResponseFactory.error(
|
// return NetExceptionResponseFactory.error(
|
||||||
req,
|
// req,
|
||||||
WireCodes.Status.BAD_REQUEST,
|
// WireCodes.Status.BAD_REQUEST,
|
||||||
"BAD_FIELDS",
|
// "BAD_FIELDS",
|
||||||
"Некорректное поле: login"
|
// "Некорректное поле: login"
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
// Если хочешь жёстче:
|
// // Если хочешь жёстче:
|
||||||
// if (!req.getLogin().matches("^[A-Za-z0-9_]+$")) ...
|
// // if (!req.getLogin().matches("^[A-Za-z0-9_]+$")) ...
|
||||||
|
//
|
||||||
SubscriptionsDAO dao = SubscriptionsDAO.getInstance();
|
// SubscriptionsDAO dao = SubscriptionsDAO.getInstance();
|
||||||
SqliteDbController db = SqliteDbController.getInstance();
|
// SqliteDbController db = SqliteDbController.getInstance();
|
||||||
|
//
|
||||||
try (Connection c = db.getConnection()) {
|
// try (Connection c = db.getConnection()) {
|
||||||
|
//
|
||||||
List<SubscriptionsDAO.ChannelRow> rows = dao.getSubscribedChannels(c, req.getLogin());
|
// List<SubscriptionsDAO.ChannelRow> rows = dao.getSubscribedChannels(c, req.getLogin());
|
||||||
List<Net_GetSubscribedChannels_Response.ChannelInfo> out = new ArrayList<>(rows.size());
|
// List<Net_GetSubscribedChannels_Response.ChannelInfo> out = new ArrayList<>(rows.size());
|
||||||
|
//
|
||||||
for (SubscriptionsDAO.ChannelRow r : rows) {
|
// for (SubscriptionsDAO.ChannelRow r : rows) {
|
||||||
Net_GetSubscribedChannels_Response.ChannelInfo dto =
|
// Net_GetSubscribedChannels_Response.ChannelInfo dto =
|
||||||
new Net_GetSubscribedChannels_Response.ChannelInfo();
|
// new Net_GetSubscribedChannels_Response.ChannelInfo();
|
||||||
|
//
|
||||||
dto.setChannelLogin(r.getChannelLogin());
|
// dto.setChannelLogin(r.getChannelLogin());
|
||||||
dto.setChannelBchName(r.getChannelBchName());
|
// dto.setChannelBchName(r.getChannelBchName());
|
||||||
dto.setPublicationsCount(r.getPublicationsCount());
|
// dto.setPublicationsCount(r.getPublicationsCount());
|
||||||
|
//
|
||||||
byte[] pubBytes = r.getLastPublicationBlockBytes();
|
// byte[] pubBytes = r.getLastPublicationBlockBytes();
|
||||||
byte[] editBytes = r.getLastEditBlockBytes();
|
// byte[] editBytes = r.getLastEditBlockBytes();
|
||||||
|
//
|
||||||
if (pubBytes == null || pubBytes.length == 0) {
|
// if (pubBytes == null || pubBytes.length == 0) {
|
||||||
dto.setLastPublicationTimestampSec(null);
|
// dto.setLastPublicationTimestampSec(null);
|
||||||
dto.setLastTextPreview(null);
|
// dto.setLastTextPreview(null);
|
||||||
out.add(dto);
|
// out.add(dto);
|
||||||
continue;
|
// continue;
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
// 1) timestamp берём из ОРИГИНАЛЬНОЙ публикации
|
// // 1) timestamp берём из ОРИГИНАЛЬНОЙ публикации
|
||||||
BchBlockEntry pubBlock = new BchBlockEntry(pubBytes);
|
// BchBlockEntry pubBlock = new BchBlockEntry(pubBytes);
|
||||||
dto.setLastPublicationTimestampSec(pubBlock.timestamp);
|
// dto.setLastPublicationTimestampSec(pubBlock.timestamp);
|
||||||
|
//
|
||||||
// 2) текст — из EDIT (если есть) иначе из оригинала
|
// // 2) текст — из EDIT (если есть) иначе из оригинала
|
||||||
byte[] actualBytes = (editBytes != null && editBytes.length > 0) ? editBytes : pubBytes;
|
// byte[] actualBytes = (editBytes != null && editBytes.length > 0) ? editBytes : pubBytes;
|
||||||
BchBlockEntry actualBlock = new BchBlockEntry(actualBytes);
|
// BchBlockEntry actualBlock = new BchBlockEntry(actualBytes);
|
||||||
|
//
|
||||||
if (!(actualBlock.body instanceof TextBody)) {
|
// if (!(actualBlock.body instanceof TextBody)) {
|
||||||
// Это уже нарушение данных: last publication должен быть текстовым блоком.
|
// // Это уже нарушение данных: last publication должен быть текстовым блоком.
|
||||||
throw new IllegalStateException("Last publication is not TextBody: type="
|
// throw new IllegalStateException("Last publication is not TextBody: type="
|
||||||
+ (actualBlock.body == null ? "null" : (actualBlock.body.type() & 0xFFFF)));
|
// + (actualBlock.body == null ? "null" : (actualBlock.body.type() & 0xFFFF)));
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
String msg = ((TextBody) actualBlock.body).message;
|
// String msg = ((TextBody) actualBlock.body).message;
|
||||||
dto.setLastTextPreview(firstNCharsSafe(msg, 50));
|
// dto.setLastTextPreview(firstNCharsSafe(msg, 50));
|
||||||
|
//
|
||||||
out.add(dto);
|
// out.add(dto);
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
Net_GetSubscribedChannels_Response resp = new Net_GetSubscribedChannels_Response();
|
// Net_GetSubscribedChannels_Response resp = new Net_GetSubscribedChannels_Response();
|
||||||
resp.setOp(req.getOp());
|
// resp.setOp(req.getOp());
|
||||||
resp.setRequestId(req.getRequestId());
|
// resp.setRequestId(req.getRequestId());
|
||||||
resp.setStatus(WireCodes.Status.OK);
|
// resp.setStatus(WireCodes.Status.OK);
|
||||||
resp.setChannels(out);
|
// resp.setChannels(out);
|
||||||
|
//
|
||||||
return resp;
|
// return resp;
|
||||||
|
//
|
||||||
} catch (SQLException e) {
|
// } catch (SQLException e) {
|
||||||
log.error("❌ DB error GetSubscribedChannels", e);
|
// log.error("❌ DB error GetSubscribedChannels", e);
|
||||||
return NetExceptionResponseFactory.error(
|
// return NetExceptionResponseFactory.error(
|
||||||
req,
|
// req,
|
||||||
WireCodes.Status.SERVER_DATA_ERROR,
|
// WireCodes.Status.SERVER_DATA_ERROR,
|
||||||
"DB_ERROR",
|
// "DB_ERROR",
|
||||||
"Ошибка БД"
|
// "Ошибка БД"
|
||||||
);
|
// );
|
||||||
} catch (IllegalArgumentException e) {
|
// } catch (IllegalArgumentException e) {
|
||||||
// сюда попадёт, например, если BchBlockEntry не смог распарсить block_byte
|
// // сюда попадёт, например, если BchBlockEntry не смог распарсить block_byte
|
||||||
log.error("❌ Bad block bytes in DB (cannot parse BchBlockEntry)", e);
|
// log.error("❌ Bad block bytes in DB (cannot parse BchBlockEntry)", e);
|
||||||
return NetExceptionResponseFactory.error(
|
// return NetExceptionResponseFactory.error(
|
||||||
req,
|
// req,
|
||||||
WireCodes.Status.SERVER_DATA_ERROR,
|
// WireCodes.Status.SERVER_DATA_ERROR,
|
||||||
"BAD_BLOCK_BYTES",
|
// "BAD_BLOCK_BYTES",
|
||||||
"В БД обнаружен повреждённый блок"
|
// "В БД обнаружен повреждённый блок"
|
||||||
);
|
// );
|
||||||
} catch (Exception e) {
|
// } catch (Exception e) {
|
||||||
log.error("❌ Internal error GetSubscribedChannels", e);
|
// log.error("❌ Internal error GetSubscribedChannels", e);
|
||||||
return NetExceptionResponseFactory.error(
|
// return NetExceptionResponseFactory.error(
|
||||||
req,
|
// req,
|
||||||
WireCodes.Status.INTERNAL_ERROR,
|
// WireCodes.Status.INTERNAL_ERROR,
|
||||||
"INTERNAL_ERROR",
|
// "INTERNAL_ERROR",
|
||||||
"Внутренняя ошибка сервера"
|
// "Внутренняя ошибка сервера"
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
/**
|
// /**
|
||||||
* Берём первые N "символов" безопасно для emoji/суррогатных пар:
|
// * Берём первые N "символов" безопасно для emoji/суррогатных пар:
|
||||||
* режем по code points.
|
// * режем по code points.
|
||||||
*/
|
// */
|
||||||
private static String firstNCharsSafe(String s, int n) {
|
// private static String firstNCharsSafe(String s, int n) {
|
||||||
if (s == null) return null;
|
// if (s == null) return null;
|
||||||
if (n <= 0) return "";
|
// if (n <= 0) return "";
|
||||||
int cp = s.codePointCount(0, s.length());
|
// int cp = s.codePointCount(0, s.length());
|
||||||
if (cp <= n) return s;
|
// if (cp <= n) return s;
|
||||||
int end = s.offsetByCodePoints(0, n);
|
// int end = s.offsetByCodePoints(0, n);
|
||||||
return s.substring(0, end);
|
// return s.substring(0, end);
|
||||||
}
|
// }
|
||||||
}
|
//}
|
||||||
@ -113,8 +113,8 @@ public class Net_AddUser_Handler implements JsonMessageHandler {
|
|||||||
st.setBlockchainName(req.getBlockchainName());
|
st.setBlockchainName(req.getBlockchainName());
|
||||||
st.setLogin(req.getLogin());
|
st.setLogin(req.getLogin());
|
||||||
st.setBlockchainKey(req.getBlockchainKey()); // Base64(32)
|
st.setBlockchainKey(req.getBlockchainKey()); // Base64(32)
|
||||||
st.setLastGlobalNumber(-1);
|
st.setLastBlockNumber(-1);
|
||||||
st.setLastGlobalHash(new byte[32]);
|
st.setLastBlockHash(new byte[32]);
|
||||||
st.setFileSizeBytes(0);
|
st.setFileSizeBytes(0);
|
||||||
st.setSizeLimit(limit);
|
st.setSizeLimit(limit);
|
||||||
st.setUpdatedAtMs(System.currentTimeMillis());
|
st.setUpdatedAtMs(System.currentTimeMillis());
|
||||||
|
|||||||
@ -1,16 +1,13 @@
|
|||||||
package test.it.blockchain;
|
package test.it.blockchain;
|
||||||
|
|
||||||
import blockchain.BchBlockEntry;
|
import blockchain.BchBlockEntry;
|
||||||
import blockchain.BchCryptoVerifier;
|
import blockchain.body.*;
|
||||||
import blockchain.body.BodyRecord;
|
|
||||||
import test.it.utils.json.JsonParsers;
|
|
||||||
import test.it.utils.TestConfig;
|
import test.it.utils.TestConfig;
|
||||||
import test.it.utils.TestIds;
|
import test.it.utils.TestIds;
|
||||||
|
import test.it.utils.json.JsonParsers;
|
||||||
import test.it.utils.log.TestLog;
|
import test.it.utils.log.TestLog;
|
||||||
import test.it.utils.ws.WsSession;
|
import test.it.utils.ws.WsSession;
|
||||||
|
|
||||||
import java.nio.ByteBuffer;
|
|
||||||
import java.nio.ByteOrder;
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
|
|
||||||
@ -18,16 +15,13 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
|
|||||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AddBlockSender — отправка AddBlock поверх одного WsSession:
|
* AddBlockSender — под новый формат BchBlockEntry:
|
||||||
* - берёт номера/prev-hash из ChainState
|
* - block хранит только preimage + signature
|
||||||
* - строит raw/hash/signature
|
* - hash32 вычисляется как sha256(preimage)
|
||||||
* - отправляет AddBlock
|
* - signature = Ed25519.sign(hash32)
|
||||||
* - проверяет serverLastGlobalHash == localHash
|
|
||||||
* - обновляет ChainState
|
|
||||||
*/
|
*/
|
||||||
public final class AddBlockSender {
|
public final class AddBlockSender {
|
||||||
|
|
||||||
private static final byte[] ZERO32 = new byte[32];
|
|
||||||
private static final String ZERO64 = "0".repeat(64);
|
private static final String ZERO64 = "0".repeat(64);
|
||||||
|
|
||||||
private final WsSession ws;
|
private final WsSession ws;
|
||||||
@ -52,69 +46,89 @@ public final class AddBlockSender {
|
|||||||
public void send(BodyRecord body, Duration timeout) {
|
public void send(BodyRecord body, Duration timeout) {
|
||||||
if (body == null) throw new IllegalArgumentException("body == null");
|
if (body == null) throw new IllegalArgumentException("body == null");
|
||||||
|
|
||||||
short lineIndex = body.expectedLineIndex();
|
body.check();
|
||||||
|
|
||||||
if (lineIndex == 0) {
|
boolean isHeader = (body instanceof HeaderBody);
|
||||||
if (state.globalLastNumber() != -1) throw new IllegalStateException("HEADER должен быть первым: globalLastNumber уже " + state.globalLastNumber());
|
|
||||||
|
if (isHeader) {
|
||||||
|
if (state.lastBlockNumber() != -1) {
|
||||||
|
throw new IllegalStateException("HEADER должен быть первым: lastBlockNumber уже " + state.lastBlockNumber());
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if (!state.hasHeader()) throw new IllegalStateException("Нельзя слать line=" + lineIndex + " до HEADER (нет headerHash32)");
|
if (!state.hasHeader()) {
|
||||||
|
throw new IllegalStateException("Нельзя слать блоки до HEADER (нет headerHash32)");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int globalNumber = state.nextGlobalNumber();
|
int blockNumber = state.nextBlockNumber();
|
||||||
int lineNumber = state.nextLineNumber(lineIndex);
|
byte[] prevHash32 = state.prevHash32ForNext();
|
||||||
|
long tsSec = System.currentTimeMillis() / 1000L;
|
||||||
|
|
||||||
byte[] prevGlobalHash32 = (lineIndex == 0) ? ZERO32 : state.prevGlobalHash32ForNext(lineIndex);
|
short type = typeOf(body);
|
||||||
byte[] prevLineHash32 = (lineIndex == 0) ? ZERO32 : state.prevLineHash32ForNext(lineIndex);
|
short subType = subTypeOf(body);
|
||||||
|
short version = versionOf(body);
|
||||||
|
|
||||||
long ts = System.currentTimeMillis() / 1000L;
|
|
||||||
byte[] bodyBytes = body.toBytes();
|
byte[] bodyBytes = body.toBytes();
|
||||||
|
|
||||||
int recordSize = BchBlockEntry.RAW_HEADER_SIZE + bodyBytes.length;
|
// preimage -> hash32 -> signature
|
||||||
|
byte[] preimage = buildPreimage(prevHash32, blockNumber, tsSec, type, subType, version, bodyBytes);
|
||||||
byte[] rawBytes = ByteBuffer.allocate(recordSize)
|
byte[] hash32 = blockchain.BchCryptoVerifier.sha256(preimage);
|
||||||
.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);
|
|
||||||
byte[] signature64 = utils.crypto.Ed25519Util.sign(hash32, loginPrivKey);
|
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 reqJson = buildAddBlockJson(blockchainName, blockNumber, prevHashHexForReq, base64(entry.toBytes()));
|
||||||
String op = "AddBlock(user=" + login + ", global=" + globalNumber + ", line=" + lineIndex + ", lineNum=" + lineNumber + ")";
|
String op = "AddBlock(user=" + login + ", block=" + blockNumber + ", type=" + (type & 0xFFFF) + ", sub=" + (subType & 0xFFFF) + ")";
|
||||||
|
|
||||||
String resp = ws.call(op, reqJson, timeout);
|
String resp = ws.call(op, reqJson, timeout);
|
||||||
|
|
||||||
assert200(op, resp);
|
assert200(op, resp);
|
||||||
|
|
||||||
String serverLastGlobalHash = JsonMini.extractPayloadString(resp, "serverLastGlobalHash");
|
String serverLastHash = JsonMini.extractPayloadString(resp, "serverLastBlockHash");
|
||||||
assertNotNull(serverLastGlobalHash, op + ": payload.serverLastGlobalHash must not be null");
|
if (serverLastHash == null) {
|
||||||
assertEquals(64, serverLastGlobalHash.trim().length(), op + ": serverLastGlobalHash must be 64 hex chars");
|
// на случай старого имени, но по твоей просьбе мы на это больше не опираемся
|
||||||
|
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()) {
|
if (TestConfig.DEBUG()) {
|
||||||
TestLog.info(op + ": localHash=" + localHashHex);
|
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");
|
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");
|
String requestId = TestIds.next("addblock");
|
||||||
return """
|
return """
|
||||||
{
|
{
|
||||||
@ -122,12 +136,12 @@ public final class AddBlockSender {
|
|||||||
"requestId": "%s",
|
"requestId": "%s",
|
||||||
"payload": {
|
"payload": {
|
||||||
"blockchainName": "%s",
|
"blockchainName": "%s",
|
||||||
"globalNumber": %d,
|
"blockNumber": %d,
|
||||||
"prevGlobalHash": "%s",
|
"prevBlockHash": "%s",
|
||||||
"blockBytesB64": "%s"
|
"blockBytesB64": "%s"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
""".formatted(requestId, blockchainName, globalNumber, prevGlobalHashHex, blockBytesB64);
|
""".formatted(requestId, blockchainName, blockNumber, prevBlockHashHex, blockBytesB64);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void assert200(String op, String resp) {
|
private static void assert200(String op, String resp) {
|
||||||
@ -150,4 +164,70 @@ public final class AddBlockSender {
|
|||||||
}
|
}
|
||||||
return new String(out);
|
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;
|
package test.it.blockchain;
|
||||||
|
|
||||||
|
import blockchain.LineIndex;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ChainState — только состояние цепочки (номера/хэши).
|
* ChainState — состояние цепочки + состояние линий (только тех, где они нужны):
|
||||||
*
|
*
|
||||||
* Хранит:
|
* Глобальная цепочка:
|
||||||
* - last globalNumber / last globalHash
|
* - lastBlockNumber / lastBlockHashHex
|
||||||
* - last lineNum / last lineHash по каждой линии
|
* - map blockNumber -> hash32 (для ссылок reply/edit/reaction)
|
||||||
* - hash32 нулевого блока (headerHash32) — нужен как prevLineHash для первого блока каждой линии
|
*
|
||||||
* - map globalNumber -> hash32 (для ссылок reply/reaction на старые блоки)
|
* Линии (по ТЗ нужны):
|
||||||
|
* - TEXT (1)
|
||||||
|
* - CONNECTION (3)
|
||||||
|
* - USER_PARAM (4)
|
||||||
|
*
|
||||||
|
* prevLineNumber по ТЗ — это GLOBAL blockNumber предыдущего блока линии.
|
||||||
|
* thisLineNumber — внутренний номер линии (мы ведём локально: 1,2,3...)
|
||||||
*/
|
*/
|
||||||
public final class ChainState {
|
public final class ChainState {
|
||||||
|
|
||||||
@ -20,134 +28,157 @@ public final class ChainState {
|
|||||||
private static final byte[] ZERO32 = new byte[32];
|
private static final byte[] ZERO32 = new byte[32];
|
||||||
private static final String ZERO64 = "0".repeat(64);
|
private static final String ZERO64 = "0".repeat(64);
|
||||||
|
|
||||||
private final int[] lineLastNumber = new int[LINES_MAX];
|
// global chain
|
||||||
private final String[] lineLastHashHex = new String[LINES_MAX];
|
private int lastBlockNumber = -1;
|
||||||
|
private String lastBlockHashHex = ZERO64;
|
||||||
private int globalLastNumber = -1;
|
|
||||||
private String globalLastHashHex = ZERO64;
|
|
||||||
|
|
||||||
|
// header (block#0)
|
||||||
private byte[] headerHash32 = null;
|
private byte[] headerHash32 = null;
|
||||||
|
|
||||||
// Для удобства тестов: чтобы можно было делать reply/like на любой уже отправленный globalNumber
|
// per-line state (только для LineIndex.TEXT/CONNECTION/USER_PARAM)
|
||||||
private final Map<Integer, byte[]> globalHash32ByNumber = new HashMap<>();
|
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() {
|
public ChainState() {
|
||||||
|
Arrays.fill(lineLastGlobalNumber, -1);
|
||||||
Arrays.fill(lineLastHashHex, "");
|
Arrays.fill(lineLastHashHex, "");
|
||||||
// lineLastNumber по умолчанию = 0
|
Arrays.fill(lineLastThisLineNumber, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------- getters --------------------
|
// -------------------- global getters --------------------
|
||||||
|
|
||||||
public int globalLastNumber() { return globalLastNumber; }
|
public int lastBlockNumber() { return lastBlockNumber; }
|
||||||
public String globalLastHashHex() { return globalLastHashHex; }
|
public String lastBlockHashHex() { return lastBlockHashHex; }
|
||||||
|
|
||||||
public int lineLastNumber(short line) { return lineLastNumber[line]; }
|
public boolean hasHeader() {
|
||||||
public String lineLastHashHex(short line) { return lineLastHashHex[line]; }
|
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) {
|
public byte[] prevHash32ForNext() {
|
||||||
byte[] h = globalHash32ByNumber.get(globalNumber);
|
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();
|
return h == null ? null : h.clone();
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------- state helpers --------------------
|
// -------------------- line helpers --------------------
|
||||||
|
|
||||||
public boolean hasHeader() {
|
public static final class NextLine {
|
||||||
return headerHash32 != null && headerHash32.length == 32 && globalLastNumber >= 0;
|
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. */
|
/** Следующие line-поля для указанной линии (только TEXT/CONNECTION/USER_PARAM). */
|
||||||
public int nextGlobalNumber() {
|
public NextLine nextLine(short lineIndex) {
|
||||||
return globalLastNumber + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Следующий lineNumber: для line>0 — last+1. Для line0 — всегда 0 (header). */
|
|
||||||
public int nextLineNumber(short lineIndex) {
|
|
||||||
checkLine(lineIndex);
|
checkLine(lineIndex);
|
||||||
if (lineIndex == 0) return 0;
|
if (!isLineUsed(lineIndex)) {
|
||||||
return lineLastNumber[lineIndex] + 1;
|
throw new IllegalArgumentException("Line " + lineIndex + " не используется для BodyHasLine по ТЗ");
|
||||||
|
}
|
||||||
|
if (!hasHeader()) {
|
||||||
|
throw new IllegalStateException("Нельзя формировать line-поля до HEADER (нет headerHash32)");
|
||||||
}
|
}
|
||||||
|
|
||||||
/** prevGlobalHash32: для header это ZERO32, иначе hash последнего глобального блока. */
|
int lastGlobal = lineLastGlobalNumber[lineIndex];
|
||||||
public byte[] prevGlobalHash32ForNext(short nextLineIndex) {
|
int lastThis = lineLastThisLineNumber[lineIndex];
|
||||||
// Для genesis/header prevGlobalHash = ZERO32
|
|
||||||
if (globalLastNumber < 0) return ZERO32;
|
|
||||||
return hexToBytes32(globalLastHashHex);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
if (lastGlobal == -1) {
|
||||||
* prevLineHash32 по твоему правилу:
|
// первый блок линии ссылается на HEADER (block#0)
|
||||||
* - для line0 (header) — ZERO32
|
return new NextLine(0, headerHash32.clone(), 1);
|
||||||
* - для первого блока линии (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];
|
String lastHex = lineLastHashHex[lineIndex];
|
||||||
if (lastHex == null || lastHex.isBlank()) {
|
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) {
|
if (hash32 == null || hash32.length != 32) {
|
||||||
throw new IllegalArgumentException("hash32 must be 32 bytes");
|
throw new IllegalArgumentException("hash32 must be 32 bytes");
|
||||||
}
|
}
|
||||||
|
if (blockNumber != lastBlockNumber + 1) {
|
||||||
// базовые ожидания по номерам (для тестов строго)
|
throw new IllegalStateException("blockNumber sequence broken: expected=" + (lastBlockNumber + 1) + " got=" + blockNumber);
|
||||||
if (globalNumber != globalLastNumber + 1) {
|
|
||||||
throw new IllegalStateException("globalNumber sequence broken: expected=" + (globalLastNumber + 1) + " got=" + globalNumber);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
checkLine(lineIndex);
|
if (isHeader) {
|
||||||
|
if (blockNumber != 0) throw new IllegalStateException("HEADER must be blockNumber=0");
|
||||||
if (lineIndex == 0) {
|
|
||||||
if (globalNumber != 0 || lineNumber != 0) {
|
|
||||||
throw new IllegalStateException("Header must be global=0 line=0 lineNum=0");
|
|
||||||
}
|
|
||||||
headerHash32 = hash32.clone();
|
headerHash32 = hash32.clone();
|
||||||
} else {
|
} else {
|
||||||
int expectedLineNum = lineLastNumber[lineIndex] + 1;
|
if (blockNumber == 0) throw new IllegalStateException("Non-header block can't be blockNumber=0");
|
||||||
if (lineNumber != expectedLineNum) {
|
if (headerHash32 == null) throw new IllegalStateException("Header must be sent before non-header blocks");
|
||||||
throw new IllegalStateException("lineNumber sequence broken for line=" + lineIndex +
|
|
||||||
": expected=" + expectedLineNum + " got=" + lineNumber);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
String hex64 = bytesToHex64(hash32);
|
String hex64 = bytesToHex64(hash32);
|
||||||
|
|
||||||
globalLastNumber = globalNumber;
|
lastBlockNumber = blockNumber;
|
||||||
globalLastHashHex = hex64;
|
lastBlockHashHex = hex64;
|
||||||
|
|
||||||
lineLastNumber[lineIndex] = lineNumber;
|
hash32ByNumber.put(blockNumber, hash32.clone());
|
||||||
|
|
||||||
|
// обновляем line-state только для линий, которые "надо" по ТЗ
|
||||||
|
short lineIndex = lineIndexByType(type);
|
||||||
|
if (lineIndex != -1 && isLineUsed(lineIndex)) {
|
||||||
|
lineLastGlobalNumber[lineIndex] = blockNumber;
|
||||||
lineLastHashHex[lineIndex] = hex64;
|
lineLastHashHex[lineIndex] = hex64;
|
||||||
|
|
||||||
globalHash32ByNumber.put(globalNumber, hash32.clone());
|
// 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) {
|
private static void checkLine(short lineIndex) {
|
||||||
if (lineIndex < 0 || lineIndex >= LINES_MAX) {
|
if (lineIndex < 0 || lineIndex >= LINES_MAX) {
|
||||||
@ -155,6 +186,8 @@ public final class ChainState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -------------------- utils --------------------
|
||||||
|
|
||||||
private static byte[] hexToBytes32(String hex) {
|
private static byte[] hexToBytes32(String hex) {
|
||||||
if (hex == null) throw new IllegalArgumentException("hex is null");
|
if (hex == null) throw new IllegalArgumentException("hex is null");
|
||||||
String s = hex.trim();
|
String s = hex.trim();
|
||||||
|
|||||||
@ -1,10 +1,8 @@
|
|||||||
package test.it.cases;
|
package test.it.cases;
|
||||||
|
|
||||||
import blockchain.body.ConnectionBody;
|
import blockchain.LineIndex;
|
||||||
import blockchain.body.HeaderBody;
|
import blockchain.body.*;
|
||||||
import blockchain.body.ReactionBody;
|
import shine.db.MsgSubType;
|
||||||
import blockchain.body.TextBody;
|
|
||||||
import blockchain.body.UserParamBody;
|
|
||||||
import test.it.blockchain.AddBlockSender;
|
import test.it.blockchain.AddBlockSender;
|
||||||
import test.it.blockchain.ChainState;
|
import test.it.blockchain.ChainState;
|
||||||
import test.it.utils.TestConfig;
|
import test.it.utils.TestConfig;
|
||||||
@ -17,11 +15,7 @@ import java.time.Duration;
|
|||||||
import static org.junit.jupiter.api.Assertions.*;
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* IT_03_AddBlock_NoAuth
|
* IT_03_AddBlock_NoAuth — обновлён под новый формат блоков (ТЗ).
|
||||||
*
|
|
||||||
* ВАЖНО:
|
|
||||||
* - пользователей НЕ создаём (их создаёт IT_01)
|
|
||||||
* - ключи берём только из TestConfig по login
|
|
||||||
*/
|
*/
|
||||||
public class 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);
|
sender1.send(new HeaderBody(u1), t);
|
||||||
assertTrue(st1.hasHeader());
|
assertTrue(st1.hasHeader());
|
||||||
|
|
||||||
sender1.send(new TextBody(TextBody.SUB_NEW, "Hello #1 (NEW) from IT_03 test"), t);
|
// TEXT_NEW x3 (с line)
|
||||||
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);
|
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[] text1Hash = st1.getHash32(1);
|
||||||
byte[] text2Hash = st1.getGlobalHash32(2);
|
byte[] text2Hash = st1.getHash32(2);
|
||||||
byte[] text3Hash = st1.getGlobalHash32(3);
|
byte[] text3Hash = st1.getHash32(3);
|
||||||
assertNotNull(text1Hash);
|
assertNotNull(text1Hash);
|
||||||
assertNotNull(text2Hash);
|
assertNotNull(text2Hash);
|
||||||
assertNotNull(text3Hash);
|
assertNotNull(text3Hash);
|
||||||
|
|
||||||
sender1.send(new TextBody(TextBody.SUB_REPLY, "Reply to TEXT#1", bch1, 1, text1Hash), t);
|
// TEXT_REPLY x2 (с line + target)
|
||||||
sender1.send(new TextBody(TextBody.SUB_REPLY, "Reply to TEXT#3", bch1, 3, text3Hash), t);
|
{
|
||||||
|
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);
|
// REACTION_LIKE x2 (без line)
|
||||||
sender1.send(new ReactionBody(ReactionBody.SUB_LIKE, bch1, 2, text2Hash), t);
|
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);
|
// TEXT_EDIT x3 (с line + target)
|
||||||
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);
|
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(10, st1.lastBlockNumber(), "USER1: lastBlockNumber должен быть 10 (всего 11 блоков включая HEADER)");
|
||||||
assertEquals(8, st1.lineLastNumber((short) 1), "USER1: line=1 должно быть 8 TEXT блоков");
|
|
||||||
assertEquals(2, st1.lineLastNumber((short) 2), "USER1: line=2 должно быть 2 REACTION блока");
|
|
||||||
|
|
||||||
// USER2
|
// USER2
|
||||||
ChainState st2 = new ChainState();
|
ChainState st2 = new ChainState();
|
||||||
@ -95,7 +147,13 @@ public class IT_03_AddBlock_NoAuth {
|
|||||||
sender2.send(new HeaderBody(u2), t);
|
sender2.send(new HeaderBody(u2), t);
|
||||||
assertTrue(st2.hasHeader());
|
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 мог подписаться на существующий блокчейн)
|
// USER3 (нужен, чтобы u1 мог подписаться на существующий блокчейн)
|
||||||
ChainState st3 = new ChainState();
|
ChainState st3 = new ChainState();
|
||||||
@ -105,27 +163,70 @@ public class IT_03_AddBlock_NoAuth {
|
|||||||
assertTrue(st3.hasHeader());
|
assertTrue(st3.hasHeader());
|
||||||
|
|
||||||
// -----------------------------------------------------------------
|
// -----------------------------------------------------------------
|
||||||
// Подписки (как ты просил):
|
// Подписки:
|
||||||
// - u1 follows u2 и u3
|
// - u1 follows u2 и u3
|
||||||
// - u2 follows только u1
|
// - u2 follows только u1
|
||||||
|
// Все CONNECTION идут по линии CONNECTION (по ТЗ "да надо")
|
||||||
// -----------------------------------------------------------------
|
// -----------------------------------------------------------------
|
||||||
|
|
||||||
// u1 -> follow u2
|
// 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
|
// 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
|
// 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 как было — но они уже не обязательны для подписок)
|
// friend/unfriend как было, но тоже по CONNECTION линии
|
||||||
sender2.send(new ConnectionBody(ConnectionBody.SUB_FRIEND, 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_FRIEND,
|
||||||
|
u1, bch1, 0, new byte[32]
|
||||||
|
), t);
|
||||||
|
}
|
||||||
|
|
||||||
sender1.send(new UserParamBody("Anna", "Gareeva"), t);
|
// user1 param + friend to u2
|
||||||
sender1.send(new ConnectionBody(ConnectionBody.SUB_FRIEND, u2, bch2, 0, new byte[32]), t);
|
{
|
||||||
|
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 сценарий блоков выполнен");
|
r.ok("IT_03 сценарий блоков выполнен");
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user