Переписал код кучи классов перешёл на новый надеюсь теперь подходящий формат блоков

и тесты переделал.

Но пока остались баги и тесты не проходят (в частности пользователи не создаются - ошибка в бд)
This commit is contained in:
AidarKC
2026-01-13 16:18:38 +03:00
parent b7025dde59
commit e9e05c1192
23 changed files with 1516 additions and 2379 deletions
@@ -1,6 +1,9 @@
// =======================
// blockchain/body/ConnectionBody.java (ИЗМЕНЁННЫЙ: bodyBytes без type/subType/version, + line fields)
// =======================
package blockchain.body;
import blockchain.LineIndex;
import shine.db.MsgSubType;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
@@ -9,112 +12,75 @@ import java.util.Arrays;
import java.util.Objects;
/**
* ConnectionBody — type=3, ver=1. (Связь/отношение)
* ConnectionBody — type=3, ver=1 (в заголовке блока).
*
* Идея:
* - Это запись "у меня есть связь с X" ИЛИ "я отменяю связь с X".
* - subType определяет вид связи и действие.
* subType (в заголовке блока) как MsgSubType:
* FRIEND=10, UNFRIEND=11
* CONTACT=20, UNCONTACT=21
* FOLLOW=30, UNFOLLOW=31
*
* subType (uint16):
* УСТАНОВИТЬ связь:
* 10 = FRIEND (друг)
* 20 = CONTACT (контакт)
* 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)
* bodyBytes (BigEndian), новый формат:
* [4] prevLineNumber
* [32] prevLineHash32
* [4] thisLineNumber
*
* [1] toLoginLen (uint8)
* [N] toLogin UTF-8
* ВАЖНО: toLogin — это "с кем связь" (ключевой смысл этой записи).
*
* [1] toBlockchainNameLen (uint8)
* [M] toBlockchainName UTF-8
* [4] toBlockGlobalNumber (int32)
* [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 VER = 1;
/** Удобный ключ для BodyRecordParser: (type<<16)|ver */
public static final int KEY = ((TYPE & 0xFFFF) << 16) | (VER & 0xFFFF);
// --- subType: SET ---
public static final short SUB_FRIEND = 10;
public static final short SUB_CONTACT = 20;
public static final short SUB_FOLLOW = 30;
public final short subType; // из header
public final short version; // из header
// --- subType: UNSET (снятие/отмена связи) ---
public static final short SUB_UNFRIEND = 11; // больше не друг
public static final short SUB_UNCONTACT = 21; // больше не контакт
public static final short SUB_UNFOLLOW = 31; // больше не подписан
// line
public final int prevLineNumber;
public final byte[] prevLineHash32;
public final int thisLineNumber;
public final short subType;
/** С кем связь (главное поле). */
// payload
public final String toLogin;
/** Блокчейн того человека (снимок/якорь). */
public final String toBlockchainName;
/** Номер последнего известного блока у того человека (снимок/якорь). */
public final int toBlockGlobalNumber;
/** Хэш последнего известного блока у того человека (снимок/якорь). */
public final byte[] toBlockHash32;
/* ===================================================================== */
/* ====================== Конструктор из байт =========================== */
/* ===================================================================== */
public ConnectionBody(byte[] bodyBytes) {
public ConnectionBody(short subType, short version, byte[] bodyBytes) {
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] +
// toLoginLen[1]+toLogin[1] +
// toBchLen[1]+toBch[1] +
// global[4] + hash[32]
if (bodyBytes.length < 2 + 2 + 2 + 1 + 1 + 1 + 1 + 4 + 32) {
// line(4+32+4) + toLoginLen[1]+toLogin[1] + toBchLen[1]+toBch[1] + global[4] + hash[32]
if (bodyBytes.length < (4 + 32 + 4) + 1 + 1 + 1 + 1 + 4 + 32) {
throw new IllegalArgumentException("ConnectionBody too short");
}
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 ConnectionBody: type=" + type + " ver=" + ver);
}
this.prevLineNumber = bb.getInt();
this.subType = bb.getShort();
if (!isValidSubType(this.subType)) {
throw new IllegalArgumentException("Bad connection subType: " + (this.subType & 0xFFFF));
}
this.prevLineHash32 = new byte[32];
bb.get(this.prevLineHash32);
this.thisLineNumber = bb.getInt();
// --- toLogin ---
int toLoginLen = Byte.toUnsignedInt(bb.get());
if (toLoginLen <= 0) throw new IllegalArgumentException("toLoginLen is 0");
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);
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());
if (bchLen <= 0) throw new IllegalArgumentException("toBlockchainNameLen is 0");
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];
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(short subType,
public ConnectionBody(int prevLineNumber,
byte[] prevLineHash32,
int thisLineNumber,
short subType,
String toLogin,
String toBlockchainName,
int toBlockGlobalNumber,
@@ -158,19 +118,21 @@ public final class ConnectionBody implements BodyRecord, BodyHasTarget {
Objects.requireNonNull(toBlockchainName, "toBlockchainName == null");
Objects.requireNonNull(toBlockHash32, "toBlockHash32 == null");
if (!isValidSubType(subType)) {
throw new IllegalArgumentException("Unknown connection subType: " + (subType & 0xFFFF));
}
if (!isValidSubType(subType)) throw new IllegalArgumentException("Bad connection subType: " + (subType & 0xFFFF));
if (toLogin.isBlank()) throw new IllegalArgumentException("toLogin is blank");
if (!toLogin.matches("^[A-Za-z0-9_]+$"))
throw new IllegalArgumentException("toLogin must match ^[A-Za-z0-9_]+$");
if (!toLogin.matches("^[A-Za-z0-9_]+$")) throw new IllegalArgumentException("toLogin must match ^[A-Za-z0-9_]+$");
if (toBlockchainName.isBlank()) throw new IllegalArgumentException("toBlockchainName is blank");
if (toBlockGlobalNumber < 0) throw new IllegalArgumentException("toBlockGlobalNumber < 0");
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.version = VER;
this.toLogin = toLogin;
this.toBlockchainName = toBlockchainName;
this.toBlockGlobalNumber = toBlockGlobalNumber;
@@ -178,62 +140,33 @@ public final class ConnectionBody implements BodyRecord, BodyHasTarget {
}
private static boolean isValidSubType(short st) {
return st == SUB_FRIEND || st == SUB_CONTACT || st == SUB_FOLLOW
|| st == SUB_UNFRIEND || st == SUB_UNCONTACT || st == SUB_UNFOLLOW;
}
/** true если это событие установки связи (10/20/30). */
public boolean isSetAction() {
return subType == SUB_FRIEND || subType == SUB_CONTACT || subType == SUB_FOLLOW;
}
/** 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;
int v = st & 0xFFFF;
return v == (MsgSubType.CONNECTION_FRIEND & 0xFFFF)
|| v == (MsgSubType.CONNECTION_UNFRIEND & 0xFFFF)
|| v == (MsgSubType.CONNECTION_CONTACT & 0xFFFF)
|| v == (MsgSubType.CONNECTION_UNCONTACT & 0xFFFF)
|| v == (MsgSubType.CONNECTION_FOLLOW & 0xFFFF)
|| v == (MsgSubType.CONNECTION_UNFOLLOW & 0xFFFF);
}
@Override
public ConnectionBody check() {
if (!isValidSubType(subType))
throw new IllegalArgumentException("Bad connection subType: " + (subType & 0xFFFF));
if (!isValidSubType(subType)) throw new IllegalArgumentException("Bad connection subType: " + (subType & 0xFFFF));
if (toLogin == null || toLogin.isBlank())
throw new IllegalArgumentException("toLogin is blank");
if (!toLogin.matches("^[A-Za-z0-9_]+$"))
throw new IllegalArgumentException("toLogin must match ^[A-Za-z0-9_]+$");
// line rule
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");
} else {
if (prevLineHash32 == null || prevLineHash32.length != 32) throw new IllegalArgumentException("prevLineHash32 invalid");
}
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");
if (toLogin == null || toLogin.isBlank()) throw new IllegalArgumentException("toLogin is blank");
if (!toLogin.matches("^[A-Za-z0-9_]+$")) throw new IllegalArgumentException("toLogin must match ^[A-Za-z0-9_]+$");
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");
return this;
}
@@ -248,26 +181,19 @@ public final class ConnectionBody implements BodyRecord, BodyHasTarget {
if (bchBytes.length == 0 || bchBytes.length > 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)
throw new IllegalArgumentException("toBlockHash32 != 32");
// type[2]+ver[2]+subType[2]
// + toLoginLen[1]+toLogin[N]
// + toBchLen[1]+toBch[M]
// + global[4]+hash[32]
int cap = 2 + 2 + 2
int cap = (4 + 32 + 4)
+ 1 + toLoginBytes.length
+ 1 + bchBytes.length
+ 4 + 32;
ByteBuffer bb = ByteBuffer.allocate(cap).order(ByteOrder.BIG_ENDIAN);
bb.putShort(TYPE);
bb.putShort(VER);
bb.putShort(subType);
bb.putInt(prevLineNumber);
bb.put(prevLineHash32 == null ? new byte[32] : Arrays.copyOf(prevLineHash32, 32));
bb.putInt(thisLineNumber);
bb.put((byte) toLoginBytes.length);
bb.put(toLoginBytes);
@@ -281,69 +207,20 @@ public final class ConnectionBody implements BodyRecord, BodyHasTarget {
return bb.array();
}
@Override
public String toString() {
String st = switch (subType) {
case SUB_FRIEND -> "FRIEND (10)";
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()
);
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;
}
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 контракт ========================= */
/* ===================================================================== */
/* ====================== 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; }
/* ====================== BodyHasTarget ===================== */
@Override public String toLogin() { return toLogin; }
@Override public String toBchName() { return toBlockchainName; }
@Override public Integer toBlockGlobalNumber() { return toBlockGlobalNumber; }
@Override public byte[] toBlockHasheBytes() { return toBlockHash32; }
}