From 69cd33479b0b878ab3a17fdb7440beac72fe2b4e0c87d52eb982856145c79edf Mon Sep 17 00:00:00 2001 From: AidarKC Date: Wed, 21 Jan 2026 18:37:05 +0300 Subject: [PATCH] 15 01 25 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Потч работает добавление линий - ситуация сложная тест падает --- .../main/java/blockchain/BchBlockEntry.java | 12 +- .../java/blockchain/body/BodyHasLine.java | 13 +- .../java/blockchain/body/ConnectionBody.java | 23 ++- .../blockchain/body/CreateChannelBody.java | 24 ++- .../main/java/blockchain/body/TextBody.java | 85 +++++---- .../java/blockchain/body/UserParamBody.java | 21 ++- .../java/shine/db/DatabaseInitializer.java | 165 ++++++++++++++++-- .../src/main/java/shine/db/dao/BlocksDAO.java | 12 +- .../java/shine/db/entities/BlockEntry.java | 11 +- src/test/addblocks.sh | 39 +++++ src/test/concat_to_file.sh | 20 --- .../java/test/it/blockchain/ChainState.java | 99 +++++++---- .../test/it/cases/IT_03_AddBlock_NoAuth.java | 21 ++- 13 files changed, 396 insertions(+), 149 deletions(-) create mode 100755 src/test/addblocks.sh delete mode 100755 src/test/concat_to_file.sh diff --git a/shine-server-blockchain/src/main/java/blockchain/BchBlockEntry.java b/shine-server-blockchain/src/main/java/blockchain/BchBlockEntry.java index c565102..2e5367d 100644 --- a/shine-server-blockchain/src/main/java/blockchain/BchBlockEntry.java +++ b/shine-server-blockchain/src/main/java/blockchain/BchBlockEntry.java @@ -15,7 +15,7 @@ import java.util.Objects; * RAW (BigEndian) = preimage: * [32] prevHash32 (SHA-256) hash предыдущего блока (цепочка) * [4] blockSize (int) = размер preimage (в байтах), БЕЗ signature64 - * [4] blockNumber (int) глобальный номер блока + * [4] blockNumber (int) глобальный номер блока (>=0) * [8] timestamp (long) unix seconds * * [2] type (short) тип сообщения @@ -62,7 +62,7 @@ public final class BchBlockEntry { // --- HEADER (RAW) --- public final byte[] prevHash32; // 32 public final int blockSize; // preimage size - public final int blockNumber; + public final int blockNumber; // >=0 public final long timestamp; public final short type; public final short subType; @@ -113,6 +113,10 @@ public final class BchBlockEntry { } this.blockNumber = bb.getInt(); + if (this.blockNumber < 0) { + throw new IllegalArgumentException("blockNumber < 0: " + this.blockNumber); + } + this.timestamp = bb.getLong(); // запрет “в будущее” больше чем на 1 минуту @@ -171,6 +175,10 @@ public final class BchBlockEntry { if (prevHash32.length != 32) throw new IllegalArgumentException("prevHash32 != 32"); if (signature64.length != SIGNATURE_LEN) throw new IllegalArgumentException("signature64 != 64"); + if (blockNumber < 0) { + throw new IllegalArgumentException("blockNumber < 0: " + blockNumber); + } + // запрет “в будущее” больше чем на 1 минуту long now = Instant.now().getEpochSecond(); if (timestamp > now + MAX_FUTURE_SECONDS) { diff --git a/shine-server-blockchain/src/main/java/blockchain/body/BodyHasLine.java b/shine-server-blockchain/src/main/java/blockchain/body/BodyHasLine.java index d5655af..7f130c1 100644 --- a/shine-server-blockchain/src/main/java/blockchain/body/BodyHasLine.java +++ b/shine-server-blockchain/src/main/java/blockchain/body/BodyHasLine.java @@ -3,13 +3,10 @@ package blockchain.body; /** * BodyHasLine — для типов, которые имеют линейные поля в body. * - * В проекте hasLine встречается, например, у: - * - TECH: CREATE_CHANNEL (type=0, subType=1) — идёт по тех-линии - * - TEXT: POST / EDIT_POST (type=1, subType=10/11) — линия канала - * - CONNECTION (type=3) - * - USER_PARAM (type=4) - * - * Формат линейных полей (BigEndian) в НАЧАЛЕ bodyBytes: + * Новый префикс для line-сообщений (BigEndian) в НАЧАЛЕ bodyBytes: + * [4] lineCode код линии: + * - 0 для нулевой линии + * - для каналов: blockNumber "заглавия линии" (CREATE_CHANNEL или HEADER/0) * [4] prevLineNumber * [32] prevLineHash32 * [4] thisLineNumber @@ -20,6 +17,8 @@ package blockchain.body; */ public interface BodyHasLine { + int lineCode(); + int prevLineNumber(); byte[] prevLineHash32(); diff --git a/shine-server-blockchain/src/main/java/blockchain/body/ConnectionBody.java b/shine-server-blockchain/src/main/java/blockchain/body/ConnectionBody.java index 9278046..0cb5ff7 100644 --- a/shine-server-blockchain/src/main/java/blockchain/body/ConnectionBody.java +++ b/shine-server-blockchain/src/main/java/blockchain/body/ConnectionBody.java @@ -18,6 +18,7 @@ import java.util.Objects; * FOLLOW=30, UNFOLLOW=31 * * bodyBytes (BigEndian), новый формат (toLogin НЕ ХРАНИМ): + * [4] lineCode * [4] prevLineNumber * [32] prevLineHash32 * [4] thisLineNumber @@ -41,6 +42,7 @@ public final class ConnectionBody implements BodyRecord, BodyHasTarget, BodyHasL public final short version; // из header // line + public final int lineCode; public final int prevLineNumber; public final byte[] prevLineHash32; public final int thisLineNumber; @@ -64,13 +66,15 @@ public final class ConnectionBody implements BodyRecord, BodyHasTarget, BodyHasL } // минимум: - // line(4+32+4) + toBchLen[1]+toBch[1] + global[4] + hash[32] - if (bodyBytes.length < (4 + 32 + 4) + 1 + 1 + 4 + 32) { + // lineCode(4) + line(4+32+4) + toBchLen[1]+toBch[1] + global[4] + hash[32] + if (bodyBytes.length < 4 + (4 + 32 + 4) + 1 + 1 + 4 + 32) { throw new IllegalArgumentException("ConnectionBody too short"); } ByteBuffer bb = ByteBuffer.wrap(bodyBytes).order(ByteOrder.BIG_ENDIAN); + this.lineCode = bb.getInt(); + this.prevLineNumber = bb.getInt(); this.prevLineHash32 = new byte[32]; @@ -94,7 +98,8 @@ public final class ConnectionBody implements BodyRecord, BodyHasTarget, BodyHasL if (bb.remaining() != 0) throw new IllegalArgumentException("Unexpected tail bytes, remaining=" + bb.remaining()); } - public ConnectionBody(int prevLineNumber, + public ConnectionBody(int lineCode, + int prevLineNumber, byte[] prevLineHash32, int thisLineNumber, short subType, @@ -105,6 +110,7 @@ public final class ConnectionBody implements BodyRecord, BodyHasTarget, BodyHasL Objects.requireNonNull(toBlockchainName, "toBlockchainName == null"); Objects.requireNonNull(toBlockHash32, "toBlockHash32 == null"); + if (lineCode < 0) throw new IllegalArgumentException("lineCode < 0"); if (!isValidSubType(subType)) throw new IllegalArgumentException("Bad connection subType: " + (subType & 0xFFFF)); if (toBlockchainName.isBlank()) throw new IllegalArgumentException("toBlockchainName is blank"); @@ -116,6 +122,8 @@ public final class ConnectionBody implements BodyRecord, BodyHasTarget, BodyHasL if (toBlockGlobalNumber < 0) throw new IllegalArgumentException("toBlockGlobalNumber < 0"); if (toBlockHash32.length != 32) throw new IllegalArgumentException("toBlockHash32 != 32"); + this.lineCode = lineCode; + this.prevLineNumber = prevLineNumber; this.prevLineHash32 = (prevLineHash32 == null ? new byte[32] : Arrays.copyOf(prevLineHash32, 32)); this.thisLineNumber = thisLineNumber; @@ -140,9 +148,10 @@ public final class ConnectionBody implements BodyRecord, BodyHasTarget, BodyHasL @Override public ConnectionBody check() { + if (lineCode < 0) throw new IllegalArgumentException("lineCode < 0"); if (!isValidSubType(subType)) throw new IllegalArgumentException("Bad connection subType: " + (subType & 0xFFFF)); - // line rule + // 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"); @@ -172,12 +181,14 @@ public final class ConnectionBody implements BodyRecord, BodyHasTarget, BodyHasL if (toBlockHash32 == null || toBlockHash32.length != 32) throw new IllegalArgumentException("toBlockHash32 != 32"); - int cap = (4 + 32 + 4) + int cap = 4 + (4 + 32 + 4) + 1 + bchBytes.length + 4 + 32; ByteBuffer bb = ByteBuffer.allocate(cap).order(ByteOrder.BIG_ENDIAN); + bb.putInt(lineCode); + bb.putInt(prevLineNumber); bb.put(prevLineHash32 == null ? new byte[32] : Arrays.copyOf(prevLineHash32, 32)); bb.putInt(thisLineNumber); @@ -198,12 +209,12 @@ public final class ConnectionBody implements BodyRecord, BodyHasTarget, BodyHasL } /* ====================== BodyHasLine ====================== */ + @Override public int lineCode() { return lineCode; } @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 ===================== */ - // toLogin() теперь default в интерфейсе и вычисляется из toBchName() @Override public String toBchName() { return toBlockchainName; } @Override public Integer toBlockGlobalNumber() { return toBlockGlobalNumber; } @Override public byte[] toBlockHashBytes() { return toBlockHash32; } diff --git a/shine-server-blockchain/src/main/java/blockchain/body/CreateChannelBody.java b/shine-server-blockchain/src/main/java/blockchain/body/CreateChannelBody.java index 2c9c6c5..7578be5 100644 --- a/shine-server-blockchain/src/main/java/blockchain/body/CreateChannelBody.java +++ b/shine-server-blockchain/src/main/java/blockchain/body/CreateChannelBody.java @@ -18,7 +18,8 @@ import java.util.Objects; * - prevLineNumber/hash указывают на предыдущее TECH-сообщение (HEADER или прошлый CREATE_CHANNEL) * - thisLineNumber: 1,2,3... (тех-нумерация) * - * bodyBytes (BigEndian): + * bodyBytes (BigEndian), новый формат line-prefix: + * [4] lineCode (для TECH линии обычно 0) * [4] prevLineNumber * [32] prevLineHash32 * [4] thisLineNumber @@ -43,6 +44,7 @@ public final class CreateChannelBody implements BodyRecord, BodyHasLine { public final short version; // из header // line + public final int lineCode; public final int prevLineNumber; public final byte[] prevLineHash32; // 32 public final int thisLineNumber; @@ -63,12 +65,15 @@ public final class CreateChannelBody implements BodyRecord, BodyHasLine { throw new IllegalArgumentException("CreateChannelBody subType must be TECH_CREATE_CHANNEL(1), got=" + (this.subType & 0xFFFF)); } - if (bodyBytes.length < (4 + 32 + 4) + 1 + 1) { + // минимум: lineCode(4) + line(4+32+4) + nameLen(1) + name(1) + if (bodyBytes.length < 4 + (4 + 32 + 4) + 1 + 1) { throw new IllegalArgumentException("CreateChannelBody too short"); } ByteBuffer bb = ByteBuffer.wrap(bodyBytes).order(ByteOrder.BIG_ENDIAN); + this.lineCode = bb.getInt(); + this.prevLineNumber = bb.getInt(); this.prevLineHash32 = new byte[32]; @@ -90,12 +95,18 @@ public final class CreateChannelBody implements BodyRecord, BodyHasLine { if (bb.remaining() != 0) throw new IllegalArgumentException("Unexpected tail bytes, remaining=" + bb.remaining()); } - public CreateChannelBody(int prevLineNumber, byte[] prevLineHash32, int thisLineNumber, String channelName) { + public CreateChannelBody(int lineCode, + int prevLineNumber, + byte[] prevLineHash32, + int thisLineNumber, + String channelName) { Objects.requireNonNull(channelName, "channelName == null"); + if (lineCode < 0) throw new IllegalArgumentException("lineCode < 0"); this.subType = SUBTYPE; this.version = VER; + this.lineCode = lineCode; this.prevLineNumber = prevLineNumber; this.prevLineHash32 = (prevLineHash32 == null ? ZERO32 : Arrays.copyOf(prevLineHash32, 32)); this.thisLineNumber = thisLineNumber; @@ -105,6 +116,8 @@ public final class CreateChannelBody implements BodyRecord, BodyHasLine { @Override public CreateChannelBody check() { + if (lineCode < 0) throw new IllegalArgumentException("lineCode < 0"); + if ((subType & 0xFFFF) != (SUBTYPE & 0xFFFF)) throw new IllegalArgumentException("CreateChannelBody subType must be TECH_CREATE_CHANNEL(1)"); @@ -134,9 +147,11 @@ public final class CreateChannelBody implements BodyRecord, BodyHasLine { if (nameUtf8.length == 0 || nameUtf8.length > 255) throw new IllegalArgumentException("channelName utf8 len must be 1..255"); - int cap = (4 + 32 + 4) + 1 + nameUtf8.length; + int cap = 4 + (4 + 32 + 4) + 1 + nameUtf8.length; ByteBuffer bb = ByteBuffer.allocate(cap).order(ByteOrder.BIG_ENDIAN); + bb.putInt(lineCode); + bb.putInt(prevLineNumber); bb.put(prevLineHash32 == null ? ZERO32 : Arrays.copyOf(prevLineHash32, 32)); bb.putInt(thisLineNumber); @@ -148,6 +163,7 @@ public final class CreateChannelBody implements BodyRecord, BodyHasLine { } /* ====================== BodyHasLine ====================== */ + @Override public int lineCode() { return lineCode; } @Override public int prevLineNumber() { return prevLineNumber; } @Override public byte[] prevLineHash32() { return prevLineHash32 == null ? null : Arrays.copyOf(prevLineHash32, 32); } @Override public int thisLineNumber() { return thisLineNumber; } diff --git a/shine-server-blockchain/src/main/java/blockchain/body/TextBody.java b/shine-server-blockchain/src/main/java/blockchain/body/TextBody.java index 5f5c51b..b51a46c 100644 --- a/shine-server-blockchain/src/main/java/blockchain/body/TextBody.java +++ b/shine-server-blockchain/src/main/java/blockchain/body/TextBody.java @@ -22,32 +22,29 @@ import java.util.Objects; * ========================================================================= * КОНЦЕПЦИЯ ЛИНИЙ ДЛЯ ТЕКСТОВЫХ СООБЩЕНИЙ: * - * POST и EDIT_POST принадлежат ЛИНИИ КАНАЛА и имеют hasLine: - * [4] prevLineNumber - * [32] prevLineHash32 - * [4] thisLineNumber + * POST и EDIT_POST принадлежат ЛИНИИ КАНАЛА и имеют hasLine. + * В новом формате добавлен lineCode: + * lineCode = 0 для канала "0" + * lineCode = blockNumber "заглавия линии/канала" (например CREATE_CHANNEL) * - * Канал в POST/EDIT_POST НЕ хранится (channelName не лежит в bodyBytes). - * Канал определяется логически через lineRootBlockNumber: - * - канал "0": lineRootBlockNumber = blockNumber заголовка (HEADER) - * - канал "X": lineRootBlockNumber = blockNumber тех-сообщения CREATE_CHANNEL("X") - * - * REPLY и EDIT_REPLY НЕ имеют линии (нет hasLine). + * REPLY и EDIT_REPLY НЕ имеют линии (нет hasLine в байтах). * * ========================================================================= * ФОРМАТЫ bodyBytes (BigEndian): * * 1) POST (subType=10): + * [4] lineCode * [4] prevLineNumber * [32] prevLineHash32 - * [4] thisLineNumber // 0,1,2... + * [4] thisLineNumber * [2] textLenBytes (uint16) * [N] text UTF-8 * * 2) EDIT_POST (subType=11): + * [4] lineCode * [4] prevLineNumber * [32] prevLineHash32 - * [4] thisLineNumber // равен thisLineNumber предыдущего сообщения линии + * [4] thisLineNumber * * hasTarget (на ОРИГИНАЛЬНЫЙ POST, toBchName НЕ хранить): * [4] toBlockGlobalNumber @@ -57,7 +54,7 @@ import java.util.Objects; * [N] text UTF-8 * * 3) REPLY (subType=20) — НЕ в линии: - * hasTarget (может быть на чужой блокчейн; существование НЕ проверяем): + * hasTarget: * [1] toBlockchainNameLen (uint8) * [N] toBlockchainName UTF-8 * [4] toBlockGlobalNumber @@ -73,15 +70,6 @@ import java.util.Objects; * * [2] textLenBytes (uint16) * [N] text UTF-8 - * - * ========================================================================= - * ВАЖНО: - * - Body.check() НЕ имеет доступа к БД, поэтому: - * - не проверяет существование prevLineNumber/hash - * - не проверяет согласование thisLineNumber относительно prev - * - не проверяет существование target для REPLY - * - * Эти проверки выполняются на сервере/в БД при вставке. */ public final class TextBody implements BodyRecord, BodyHasTarget, BodyHasLine { @@ -95,6 +83,7 @@ public final class TextBody implements BodyRecord, BodyHasTarget, BodyHasLine { // ===== line fields (только для POST/EDIT_POST) ===== // Для REPLY/EDIT_REPLY эти поля НЕ сериализуются; значения держим как "пустые". + public final int lineCode; // только для line-message; иначе -1 public final int prevLineNumber; public final byte[] prevLineHash32; // 32 or null public final int thisLineNumber; @@ -105,9 +94,9 @@ public final class TextBody implements BodyRecord, BodyHasTarget, BodyHasLine { // ===== target fields ===== // REPLY: toBlockchainName + globalNumber + hash32 // EDIT_POST / EDIT_REPLY: только globalNumber + hash32 (без toBlockchainName) - public final String toBlockchainName; // nullable + public final String toBlockchainName; // nullable public final Integer toBlockGlobalNumber; // nullable - public final byte[] toBlockHash32; // nullable(но если target есть -> 32) + public final byte[] toBlockHash32; // nullable (но если target есть -> 32) /* ===================================================================== */ /* ====================== Конструктор из байт ========================== */ @@ -131,9 +120,10 @@ public final class TextBody implements BodyRecord, BodyHasTarget, BodyHasLine { int st = this.subType & 0xFFFF; if (st == (MsgSubType.TEXT_POST & 0xFFFF)) { - // POST: hasLine + text - ensureMin(bb, (4 + 32 + 4) + 2, "POST too short"); + // POST: hasLine(lineCode+line) + text + ensureMin(bb, (4 + 4 + 32 + 4) + 2, "POST too short"); + this.lineCode = bb.getInt(); this.prevLineNumber = bb.getInt(); this.prevLineHash32 = new byte[32]; bb.get(this.prevLineHash32); @@ -148,9 +138,10 @@ public final class TextBody implements BodyRecord, BodyHasTarget, BodyHasLine { ensureNoTail(bb, "POST"); } else if (st == (MsgSubType.TEXT_EDIT_POST & 0xFFFF)) { - // EDIT_POST: hasLine + target(no bch) + text - ensureMin(bb, (4 + 32 + 4) + (4 + 32) + 2, "EDIT_POST too short"); + // EDIT_POST: hasLine(lineCode+line) + target(no bch) + text + ensureMin(bb, (4 + 4 + 32 + 4) + (4 + 32) + 2, "EDIT_POST too short"); + this.lineCode = bb.getInt(); this.prevLineNumber = bb.getInt(); this.prevLineHash32 = new byte[32]; bb.get(this.prevLineHash32); @@ -169,7 +160,7 @@ public final class TextBody implements BodyRecord, BodyHasTarget, BodyHasLine { ensureNoTail(bb, "EDIT_POST"); } else if (st == (MsgSubType.TEXT_REPLY & 0xFFFF)) { - // REPLY: target(with bch) + text + // REPLY: target(with bch) + text (без line) ensureMin(bb, 1 + 1 + 4 + 32 + 2, "REPLY too short"); int nameLen = Byte.toUnsignedInt(bb.get()); @@ -188,6 +179,7 @@ public final class TextBody implements BodyRecord, BodyHasTarget, BodyHasLine { this.message = readStrictUtf8Len16(bb, "REPLY text"); // line fields отсутствуют в байтах + this.lineCode = -1; this.prevLineNumber = -1; this.prevLineHash32 = null; this.thisLineNumber = -1; @@ -195,7 +187,7 @@ public final class TextBody implements BodyRecord, BodyHasTarget, BodyHasLine { ensureNoTail(bb, "REPLY"); } else if (st == (MsgSubType.TEXT_EDIT_REPLY & 0xFFFF)) { - // EDIT_REPLY: target(no bch) + text + // EDIT_REPLY: target(no bch) + text (без line) ensureMin(bb, (4 + 32) + 2, "EDIT_REPLY too short"); int tgtNum = bb.getInt(); @@ -209,6 +201,7 @@ public final class TextBody implements BodyRecord, BodyHasTarget, BodyHasLine { this.message = readStrictUtf8Len16(bb, "EDIT_REPLY text"); // line fields отсутствуют в байтах + this.lineCode = -1; this.prevLineNumber = -1; this.prevLineHash32 = null; this.thisLineNumber = -1; @@ -216,7 +209,6 @@ public final class TextBody implements BodyRecord, BodyHasTarget, BodyHasLine { ensureNoTail(bb, "EDIT_REPLY"); } else { - // недостижимо из-за isValidSubType, но пусть будет throw new IllegalArgumentException("Unsupported Text subType: " + st); } } @@ -225,25 +217,25 @@ public final class TextBody implements BodyRecord, BodyHasTarget, BodyHasLine { /* ====================== Фабрики (удобно) ============================= */ /* ===================================================================== */ - public static TextBody newPost(int prevLineNumber, byte[] prevLineHash32, int thisLineNumber, String message) { - return new TextBody(MsgSubType.TEXT_POST, prevLineNumber, prevLineHash32, thisLineNumber, + public static TextBody newPost(int lineCode, int prevLineNumber, byte[] prevLineHash32, int thisLineNumber, String message) { + return new TextBody(MsgSubType.TEXT_POST, lineCode, prevLineNumber, prevLineHash32, thisLineNumber, message, null, null, null); } - public static TextBody newEditPost(int prevLineNumber, byte[] prevLineHash32, int thisLineNumber, + public static TextBody newEditPost(int lineCode, int prevLineNumber, byte[] prevLineHash32, int thisLineNumber, int targetBlockNumber, byte[] targetHash32, String message) { - return new TextBody(MsgSubType.TEXT_EDIT_POST, prevLineNumber, prevLineHash32, thisLineNumber, + return new TextBody(MsgSubType.TEXT_EDIT_POST, lineCode, prevLineNumber, prevLineHash32, thisLineNumber, message, null, targetBlockNumber, targetHash32); } public static TextBody newReply(String toBlockchainName, int targetBlockNumber, byte[] targetHash32, String message) { - return new TextBody(MsgSubType.TEXT_REPLY, -1, null, -1, + return new TextBody(MsgSubType.TEXT_REPLY, -1, -1, null, -1, message, toBlockchainName, targetBlockNumber, targetHash32); } public static TextBody newEditReply(int targetBlockNumber, byte[] targetHash32, String message) { - return new TextBody(MsgSubType.TEXT_EDIT_REPLY, -1, null, -1, + return new TextBody(MsgSubType.TEXT_EDIT_REPLY, -1, -1, null, -1, message, null, targetBlockNumber, targetHash32); } @@ -252,6 +244,7 @@ public final class TextBody implements BodyRecord, BodyHasTarget, BodyHasLine { * Для REPLY/EDIT_REPLY line поля игнорируются при сериализации (их в формате нет). */ public TextBody(short subType, + int lineCode, int prevLineNumber, byte[] prevLineHash32, int thisLineNumber, @@ -272,10 +265,13 @@ public final class TextBody implements BodyRecord, BodyHasTarget, BodyHasLine { // line применима только к POST/EDIT_POST if (st == (MsgSubType.TEXT_POST & 0xFFFF) || st == (MsgSubType.TEXT_EDIT_POST & 0xFFFF)) { + if (lineCode < 0) throw new IllegalArgumentException("lineCode < 0 for line message"); + this.lineCode = lineCode; this.prevLineNumber = prevLineNumber; this.prevLineHash32 = (prevLineHash32 == null ? new byte[32] : Arrays.copyOf(prevLineHash32, 32)); this.thisLineNumber = thisLineNumber; } else { + this.lineCode = -1; this.prevLineNumber = -1; this.prevLineHash32 = null; this.thisLineNumber = -1; @@ -322,7 +318,6 @@ public final class TextBody implements BodyRecord, BodyHasTarget, BodyHasLine { this.toBlockHash32 = Arrays.copyOf(toBlockHash32, 32); } else { - // недостижимо this.toBlockchainName = null; this.toBlockGlobalNumber = null; this.toBlockHash32 = null; @@ -349,6 +344,7 @@ public final class TextBody implements BodyRecord, BodyHasTarget, BodyHasLine { // локальные проверки line (БД не трогаем) if (st == (MsgSubType.TEXT_POST & 0xFFFF) || st == (MsgSubType.TEXT_EDIT_POST & 0xFFFF)) { + if (lineCode < 0) throw new IllegalArgumentException("lineCode < 0 for line message"); if (prevLineHash32 == null || prevLineHash32.length != 32) throw new IllegalArgumentException("prevLineHash32 invalid"); } else { @@ -399,10 +395,11 @@ public final class TextBody implements BodyRecord, BodyHasTarget, BodyHasLine { int st = subType & 0xFFFF; if (st == (MsgSubType.TEXT_POST & 0xFFFF)) { - // hasLine + text - int cap = (4 + 32 + 4) + 2 + msgUtf8.length; + // hasLine(lineCode+line) + text + int cap = (4 + 4 + 32 + 4) + 2 + msgUtf8.length; ByteBuffer bb = ByteBuffer.allocate(cap).order(ByteOrder.BIG_ENDIAN); + bb.putInt(lineCode); bb.putInt(prevLineNumber); bb.put(prevLineHash32 == null ? new byte[32] : Arrays.copyOf(prevLineHash32, 32)); bb.putInt(thisLineNumber); @@ -411,13 +408,14 @@ public final class TextBody implements BodyRecord, BodyHasTarget, BodyHasLine { return bb.array(); } else if (st == (MsgSubType.TEXT_EDIT_POST & 0xFFFF)) { - // hasLine + target(no bch) + text + // hasLine(lineCode+line) + target(no bch) + text if (toBlockGlobalNumber == null) throw new IllegalArgumentException("EDIT_POST missing toBlockGlobalNumber"); if (toBlockHash32 == null || toBlockHash32.length != 32) throw new IllegalArgumentException("EDIT_POST toBlockHash32 != 32"); - int cap = (4 + 32 + 4) + (4 + 32) + 2 + msgUtf8.length; + int cap = (4 + 4 + 32 + 4) + (4 + 32) + 2 + msgUtf8.length; ByteBuffer bb = ByteBuffer.allocate(cap).order(ByteOrder.BIG_ENDIAN); + bb.putInt(lineCode); bb.putInt(prevLineNumber); bb.put(prevLineHash32 == null ? new byte[32] : Arrays.copyOf(prevLineHash32, 32)); bb.putInt(thisLineNumber); @@ -506,6 +504,7 @@ public final class TextBody implements BodyRecord, BodyHasTarget, BodyHasLine { } /* ====================== BodyHasLine ====================== */ + @Override public int lineCode() { return lineCode; } @Override public int prevLineNumber() { return prevLineNumber; } @Override public byte[] prevLineHash32() { if (prevLineHash32 == null) return null; @@ -518,8 +517,6 @@ public final class TextBody implements BodyRecord, BodyHasTarget, BodyHasLine { @Override public Integer toBlockGlobalNumber() { return toBlockGlobalNumber; } @Override public byte[] toBlockHashBytes() { return toBlockHash32; } - - /* ===================================================================== */ /* ===================== Удобные хелперы (для ChainState) =============== */ /* ===================================================================== */ diff --git a/shine-server-blockchain/src/main/java/blockchain/body/UserParamBody.java b/shine-server-blockchain/src/main/java/blockchain/body/UserParamBody.java index 852abe1..95beda6 100644 --- a/shine-server-blockchain/src/main/java/blockchain/body/UserParamBody.java +++ b/shine-server-blockchain/src/main/java/blockchain/body/UserParamBody.java @@ -17,6 +17,7 @@ import java.util.Objects; * 1 = TEXT_TEXT * * bodyBytes (BigEndian), новый формат: + * [4] lineCode * [4] prevLineNumber * [32] prevLineHash32 * [4] thisLineNumber @@ -38,6 +39,7 @@ public final class UserParamBody implements BodyRecord, BodyHasLine { public final short version; // из header // line + public final int lineCode; public final int prevLineNumber; public final byte[] prevLineHash32; public final int thisLineNumber; @@ -58,13 +60,15 @@ public final class UserParamBody implements BodyRecord, BodyHasLine { 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) { + // минимум: lineCode(4)+line(4+32+4) + keyLen(2)+key(1) + valLen(2)+val(1) + if (bodyBytes.length < 4 + (4 + 32 + 4) + 2 + 1 + 2 + 1) { throw new IllegalArgumentException("UserParamBody too short"); } ByteBuffer bb = ByteBuffer.wrap(bodyBytes).order(ByteOrder.BIG_ENDIAN); + this.lineCode = bb.getInt(); + this.prevLineNumber = bb.getInt(); this.prevLineHash32 = new byte[32]; @@ -95,7 +99,8 @@ public final class UserParamBody implements BodyRecord, BodyHasLine { if (this.paramValue.isBlank()) throw new IllegalArgumentException("paramValue is blank"); } - public UserParamBody(int prevLineNumber, + public UserParamBody(int lineCode, + int prevLineNumber, byte[] prevLineHash32, int thisLineNumber, String paramKey, @@ -104,9 +109,12 @@ public final class UserParamBody implements BodyRecord, BodyHasLine { Objects.requireNonNull(paramKey, "paramKey == null"); Objects.requireNonNull(paramValue, "paramValue == null"); + if (lineCode < 0) throw new IllegalArgumentException("lineCode < 0"); + this.subType = MsgSubType.USER_PARAM_TEXT_TEXT; this.version = VER; + this.lineCode = lineCode; this.prevLineNumber = prevLineNumber; this.prevLineHash32 = (prevLineHash32 == null ? new byte[32] : Arrays.copyOf(prevLineHash32, 32)); this.thisLineNumber = thisLineNumber; @@ -120,6 +128,8 @@ public final class UserParamBody implements BodyRecord, BodyHasLine { @Override public UserParamBody check() { + if (lineCode < 0) throw new IllegalArgumentException("lineCode < 0"); + if ((subType & 0xFFFF) != (MsgSubType.USER_PARAM_TEXT_TEXT & 0xFFFF)) throw new IllegalArgumentException("Bad UserParam subType: " + (subType & 0xFFFF)); @@ -144,12 +154,14 @@ public final class UserParamBody implements BodyRecord, BodyHasLine { if (keyUtf8.length == 0 || keyUtf8.length > 65535) throw new IllegalArgumentException("paramKey utf8 len must be 1..65535"); if (valUtf8.length == 0 || valUtf8.length > 65535) throw new IllegalArgumentException("paramValue utf8 len must be 1..65535"); - int cap = (4 + 32 + 4) + int cap = 4 + (4 + 32 + 4) + 2 + keyUtf8.length + 2 + valUtf8.length; ByteBuffer bb = ByteBuffer.allocate(cap).order(ByteOrder.BIG_ENDIAN); + bb.putInt(lineCode); + bb.putInt(prevLineNumber); bb.put(prevLineHash32 == null ? new byte[32] : Arrays.copyOf(prevLineHash32, 32)); bb.putInt(thisLineNumber); @@ -182,6 +194,7 @@ public final class UserParamBody implements BodyRecord, BodyHasLine { } /* ====================== BodyHasLine ====================== */ + @Override public int lineCode() { return lineCode; } @Override public int prevLineNumber() { return prevLineNumber; } @Override public byte[] prevLineHash32() { return prevLineHash32 == null ? null : Arrays.copyOf(prevLineHash32, 32); } @Override public int thisLineNumber() { return thisLineNumber; } diff --git a/shine-server-db/src/main/java/shine/db/DatabaseInitializer.java b/shine-server-db/src/main/java/shine/db/DatabaseInitializer.java index e7e3b45..639212f 100644 --- a/shine-server-db/src/main/java/shine/db/DatabaseInitializer.java +++ b/shine-server-db/src/main/java/shine/db/DatabaseInitializer.java @@ -182,8 +182,7 @@ public class DatabaseInitializer { ON ip_geo_cache (updated_at_ms); """); - // 5. blockchain_state (НОВЫЙ формат под BlockchainStateDAO/Entry) - // ВАЖНО: last_block_number / last_block_hash (а не last_global_*) + // 5. blockchain_state st.executeUpdate(""" CREATE TABLE IF NOT EXISTS blockchain_state ( blockchain_name TEXT NOT NULL PRIMARY KEY, @@ -212,13 +211,12 @@ public class DatabaseInitializer { ON blockchain_state (updated_at_ms); """); - // 6. blocks (НОВЫЙ формат под BlocksDAO/BlockEntry) - // Ключ: (bch_name, block_number) + // 6. blocks (+ line_code) st.executeUpdate(""" CREATE TABLE IF NOT EXISTS blocks ( login TEXT NOT NULL, bch_name TEXT NOT NULL, - block_number INTEGER NOT NULL, + block_number INTEGER NOT NULL CHECK(block_number >= 0), msg_type INTEGER NOT NULL, msg_sub_type INTEGER NOT NULL, @@ -228,7 +226,7 @@ public class DatabaseInitializer { -- target (reply/like/edit и т.д.) to_login TEXT, to_bch_name TEXT, - to_block_number INTEGER, + to_block_number INTEGER CHECK(to_block_number IS NULL OR to_block_number >= 0), to_block_hash BLOB, -- собственные данные @@ -236,12 +234,13 @@ public class DatabaseInitializer { block_signature BLOB NOT NULL, -- если этот блок был изменён последним edit'ом - edited_by_block_number INTEGER, + edited_by_block_number INTEGER CHECK(edited_by_block_number IS NULL OR edited_by_block_number >= 0), -- линейность (опционально) - prev_line_number INTEGER, - prev_line_hash BLOB, - this_line_number INTEGER, + line_code INTEGER CHECK(line_code IS NULL OR line_code >= 0), + prev_line_number INTEGER CHECK(prev_line_number IS NULL OR prev_line_number >= 0), + prev_line_hash BLOB, + this_line_number INTEGER CHECK(this_line_number IS NULL OR this_line_number >= 0), FOREIGN KEY (login) REFERENCES solana_users(login), FOREIGN KEY (bch_name) REFERENCES blockchain_state(blockchain_name), @@ -260,7 +259,143 @@ public class DatabaseInitializer { ON blocks (to_login, to_bch_name, to_block_number); """); - // 7) connections_state (под SubscriptionsDAO: rel_type + to_login/to_bch_name) + st.executeUpdate(""" + CREATE INDEX IF NOT EXISTS idx_blocks_by_line + ON blocks (bch_name, line_code, this_line_number); + """); + + // 6.1) TRIGGER: проверка целостности линии (только если line-поля реально переданы) + st.executeUpdate(""" + CREATE TRIGGER IF NOT EXISTS trg_blocks_line_integrity_bi + BEFORE INSERT ON blocks + WHEN + NEW.line_code IS NOT NULL + OR NEW.prev_line_number IS NOT NULL + OR NEW.prev_line_hash IS NOT NULL + OR NEW.this_line_number IS NOT NULL + BEGIN + -- ============================================================ + -- LINE-INTEGRITY (BodyHasLine) + -- + -- Этот триггер срабатывает ТОЛЬКО если при вставке передали хотя бы одно line-поле. + -- + -- Типы, которые МОГУТ быть линейными (BodyHasLine в коде проекта): + -- - TECH (msg_type=0): CreateChannelBody (и т.п. тех-блоки с линией) + -- - TEXT (msg_type=1): TextBody в режиме линии (пост/редактирование поста в канале) + -- - CONNECTION (msg_type=3): ConnectionBody + -- - USER_PARAM (msg_type=4): UserParamBody + -- + -- Проверки: + -- 1) Если передали line-поля -> обязаны передать ВСЕ 4: + -- line_code, prev_line_number, prev_line_hash, this_line_number. + -- 2) prev блок линии существует и p.block_hash == NEW.prev_line_hash + -- 3) line_code корректный: + -- - либо NEW.prev_line_number == NEW.line_code (первый шаг после root), + -- - либо у prev блока p.line_code == NEW.line_code + -- 4) this_line_number корректный: + -- - первый шаг после root: + -- TEXT: this=0 + -- TECH/CONNECTION/USER_PARAM: this=1 + -- - дальше: + -- TEXT: допускаем this = prev.this или prev.this + 1 + -- TECH/CONNECTION/USER_PARAM: строго this = prev.this + 1 + -- + -- Ошибки: RAISE(ABORT, 'LINE_ERR_...') — чтобы Java могла понять причину. + -- ============================================================ + + -- 0) line-поля нельзя у неожиданных типов + SELECT RAISE(ABORT, + 'LINE_ERR_UNSUPPORTED_TYPE_WITH_LINE: msg_type=' || NEW.msg_type || ' msg_sub_type=' || NEW.msg_sub_type + ) + WHERE NOT (NEW.msg_type IN (0, 1, 3, 4)); + + -- 1) line-поля должны быть заполнены полностью (без “частично”) + SELECT RAISE(ABORT, + 'LINE_ERR_PARTIAL_FIELDS: all of (line_code, prev_line_number, prev_line_hash, this_line_number) must be NOT NULL' + ) + WHERE NEW.line_code IS NULL + OR NEW.prev_line_number IS NULL + OR NEW.prev_line_hash IS NULL + OR NEW.this_line_number IS NULL; + + -- 2) prev существует? + SELECT RAISE(ABORT, + 'LINE_ERR_NO_PREV: bch=' || NEW.bch_name || ' block=' || NEW.block_number || ' prev=' || NEW.prev_line_number + ) + WHERE NOT EXISTS( + SELECT 1 + FROM blocks p + WHERE p.bch_name = NEW.bch_name + AND p.block_number = NEW.prev_line_number + LIMIT 1 + ); + + -- 3) prev hash совпадает? + SELECT RAISE(ABORT, + 'LINE_ERR_PREV_HASH_MISMATCH: bch=' || NEW.bch_name || ' block=' || NEW.block_number || ' prev=' || NEW.prev_line_number + ) + WHERE NOT EXISTS( + SELECT 1 + FROM blocks p + WHERE p.bch_name = NEW.bch_name + AND p.block_number = NEW.prev_line_number + AND p.block_hash = NEW.prev_line_hash + LIMIT 1 + ); + + -- 4) line_code корректный: + -- либо это первый шаг после root (prev_line_number == line_code), + -- либо prev уже в этой линии (p.line_code == NEW.line_code). + SELECT RAISE(ABORT, + 'LINE_ERR_LINE_CODE_MISMATCH: bch=' || NEW.bch_name || ' block=' || NEW.block_number || + ' line_code=' || NEW.line_code || ' prev=' || NEW.prev_line_number + ) + WHERE NEW.prev_line_number <> NEW.line_code + AND NOT EXISTS( + SELECT 1 + FROM blocks p + WHERE p.bch_name = NEW.bch_name + AND p.block_number = NEW.prev_line_number + AND p.line_code = NEW.line_code + LIMIT 1 + ); + + -- 5) первый шаг после root: this_line_number + SELECT RAISE(ABORT, + 'LINE_ERR_FIRST_STEP_BAD_THIS: expected this_line_number=0 for TEXT or =1 for other types' + ) + WHERE NEW.prev_line_number = NEW.line_code + AND NEW.this_line_number <> (CASE WHEN NEW.msg_type = 1 THEN 0 ELSE 1 END); + + -- 6) обычный шаг: this_line_number относительно prev + SELECT RAISE(ABORT, + 'LINE_ERR_THIS_LINE_BAD_STEP: bch=' || NEW.bch_name || ' block=' || NEW.block_number || + ' this=' || NEW.this_line_number || ' prev=' || NEW.prev_line_number + ) + WHERE NEW.prev_line_number <> NEW.line_code + AND NOT EXISTS( + SELECT 1 + FROM blocks p + WHERE p.bch_name = NEW.bch_name + AND p.block_number = NEW.prev_line_number + AND p.this_line_number IS NOT NULL + AND ( + -- TEXT: допускаем same или +1 (поддерживает “edit не увеличивает thisLineNumber”) + (NEW.msg_type = 1 AND + (NEW.this_line_number = p.this_line_number OR NEW.this_line_number = p.this_line_number + 1) + ) + OR + -- TECH/CONNECTION/USER_PARAM: строго +1 + (NEW.msg_type IN (0,3,4) AND + NEW.this_line_number = p.this_line_number + 1 + ) + ) + LIMIT 1 + ); + END; + """); + + // 7) connections_state st.executeUpdate(""" CREATE TABLE IF NOT EXISTS connections_state ( login TEXT NOT NULL, @@ -291,7 +426,7 @@ public class DatabaseInitializer { ON connections_state (login, to_login); """); - // 8) Trigger: connection state (под новые имена колонок) + // 8) Trigger: connection state st.executeUpdate(""" CREATE TRIGGER IF NOT EXISTS trg_blocks_connection_state_ai AFTER INSERT ON blocks @@ -343,7 +478,7 @@ public class DatabaseInitializer { (int) CONNECTION_UNFOLLOW )); - // 9) message_stats (под новые to_* имена) + edits_count + // 9) message_stats st.executeUpdate(""" CREATE TABLE IF NOT EXISTS message_stats ( to_login TEXT NOT NULL, @@ -440,20 +575,18 @@ public class DatabaseInitializer { END; """.formatted((int) TEXT_REPLY)); - // 12) Trigger: EDIT — пометить исходный блок + увеличить edits_count + // 12) Trigger: EDIT st.executeUpdate(""" CREATE TRIGGER IF NOT EXISTS trg_blocks_edit_apply_ai AFTER INSERT ON blocks WHEN NEW.msg_type = 1 AND NEW.msg_sub_type = %d BEGIN - -- 1) Помечаем исходный блок, что его изменили последним edit'ом UPDATE blocks SET edited_by_block_number = NEW.block_number WHERE login = NEW.login AND bch_name = NEW.bch_name AND block_number = NEW.to_block_number; - -- 2) edits_count +1 в message_stats (upsert) INSERT INTO message_stats ( to_login, to_bch_name, diff --git a/shine-server-db/src/main/java/shine/db/dao/BlocksDAO.java b/shine-server-db/src/main/java/shine/db/dao/BlocksDAO.java index 89701d1..ca261d2 100644 --- a/shine-server-db/src/main/java/shine/db/dao/BlocksDAO.java +++ b/shine-server-db/src/main/java/shine/db/dao/BlocksDAO.java @@ -50,10 +50,11 @@ public final class BlocksDAO { block_hash, block_signature, edited_by_block_number, + line_code, prev_line_number, prev_line_hash, this_line_number - ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) + ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) """; try (PreparedStatement ps = c.prepareStatement(sql)) { @@ -86,6 +87,10 @@ public final class BlocksDAO { if (e.getEditedByBlockNumber() != null) ps.setInt(i++, e.getEditedByBlockNumber()); else ps.setNull(i++, Types.INTEGER); + // NEW: line_code + if (e.getLineCode() != null) ps.setInt(i++, e.getLineCode()); + else ps.setNull(i++, Types.INTEGER); + if (e.getPrevLineNumber() != null) ps.setInt(i++, e.getPrevLineNumber()); else ps.setNull(i++, Types.INTEGER); @@ -151,6 +156,7 @@ public final class BlocksDAO { block_hash, block_signature, edited_by_block_number, + line_code, prev_line_number, prev_line_hash, this_line_number @@ -211,6 +217,10 @@ public final class BlocksDAO { Integer editedBy = (Integer) rs.getObject("edited_by_block_number"); e.setEditedByBlockNumber(editedBy); + // NEW: line_code + Integer lineCode = (Integer) rs.getObject("line_code"); + e.setLineCode(lineCode); + Integer prevLn = (Integer) rs.getObject("prev_line_number"); e.setPrevLineNumber(prevLn); diff --git a/shine-server-db/src/main/java/shine/db/entities/BlockEntry.java b/shine-server-db/src/main/java/shine/db/entities/BlockEntry.java index 6a56b04..17a423c 100644 --- a/shine-server-db/src/main/java/shine/db/entities/BlockEntry.java +++ b/shine-server-db/src/main/java/shine/db/entities/BlockEntry.java @@ -11,9 +11,9 @@ package shine.db.entities; * - block_signature (64 байта) * * Опционально: - * - prev_line_number / prev_line_hash / this_line_number + * - line_code / prev_line_number / prev_line_hash / this_line_number * - * Плюс поля индексации (как раньше было удобно): + * Плюс поля индексации: * - msg_type / msg_sub_type * - to_* (если есть target) * - edited_by_block_number (для TEXT_EDIT) @@ -40,6 +40,9 @@ public class BlockEntry { private Integer editedByBlockNumber; + // NEW: + private Integer lineCode; + private Integer prevLineNumber; private byte[] prevLineHash; private Integer thisLineNumber; @@ -85,6 +88,10 @@ public class BlockEntry { public Integer getEditedByBlockNumber() { return editedByBlockNumber; } public void setEditedByBlockNumber(Integer editedByBlockNumber) { this.editedByBlockNumber = editedByBlockNumber; } + // NEW: + public Integer getLineCode() { return lineCode; } + public void setLineCode(Integer lineCode) { this.lineCode = lineCode; } + public Integer getPrevLineNumber() { return prevLineNumber; } public void setPrevLineNumber(Integer prevLineNumber) { this.prevLineNumber = prevLineNumber; } diff --git a/src/test/addblocks.sh b/src/test/addblocks.sh new file mode 100755 index 0000000..5f8f10c --- /dev/null +++ b/src/test/addblocks.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +set -euo pipefail + +OUTFILE="all_files.txt" + +# === Список файлов (ТОЛЬКО имена без расширений) === +# пример: Main значит Main.java, Utils значит Utils.java +NAMES=( + "IT_04_UserParams_NoAuth" + "AddBlockSender" + "ChainState" + "JsonBuilders" +) + +# очищаем или создаём файл +: > "$OUTFILE" + +# Быстрый фильтр: сделаем хеш-таблицу из имён (ассоц. массив) +declare -A WANT=() +for name in "${NAMES[@]}"; do + WANT["$name"]=1 +done + +# собрать только нужные *.java по базовому имени +find . -type f -name "*.java" | sort | while read -r f; do + base="$(basename "$f" .java)" + if [[ -n "${WANT[$base]+x}" ]]; then + cat "$f" >> "$OUTFILE" + echo >> "$OUTFILE" # пустая строка-разделитель + fi +done + +# скопировать весь файл в буфер обмена (Wayland) +wl-copy < "$OUTFILE" + +echo "Готово!" +echo "Выбрано имён: ${#NAMES[@]}" +echo "Все нужные .java файлы собраны в $OUTFILE" +echo "Содержимое скопировано в буфер обмена (Wayland)" diff --git a/src/test/concat_to_file.sh b/src/test/concat_to_file.sh deleted file mode 100755 index f6db1f1..0000000 --- a/src/test/concat_to_file.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -OUTFILE="all_files.txt" - -# очищаем или создаём файл -: > "$OUTFILE" - -# собрать только *.java файлы и вывести их содержимое в файл -find . -type f -name "*.java" | sort | while read -r f; do - cat "$f" >> "$OUTFILE" - echo >> "$OUTFILE" # пустая строка-разделитель -done - -# скопировать весь файл в буфер обмена (Wayland) -wl-copy < "$OUTFILE" - -echo "Готово!" -echo "Все .java файлы собраны в $OUTFILE" -echo "Содержимое скопировано в буфер обмена (Wayland)" diff --git a/src/test/java/test/it/blockchain/ChainState.java b/src/test/java/test/it/blockchain/ChainState.java index 32d8fdc..2919c87 100644 --- a/src/test/java/test/it/blockchain/ChainState.java +++ b/src/test/java/test/it/blockchain/ChainState.java @@ -1,6 +1,5 @@ package test.it.blockchain; -import blockchain.MsgSubType; import blockchain.body.BodyRecord; import blockchain.body.BodyHasLine; import blockchain.body.CreateChannelBody; @@ -22,9 +21,12 @@ import java.util.Map; * - CONNECTION (type=3): одна линия * - USER_PARAM (type=4): одна линия * - * Важно: + * ВАЖНО: * - prevLineNumber — это GLOBAL blockNumber предыдущего блока линии. * - thisLineNumber — внутренний номер линии (для постов: 0,1,2...; для тех-линии: 1,2,3...) + * - lineCode — код линии: + * * 0 для канала "0" и для "простых" линий (connection/user_param/tech) + * * для каналов !=0: lineCode = blockNumber "заглавия" канала (CREATE_CHANNEL) */ public final class ChainState { @@ -79,14 +81,16 @@ public final class ChainState { // ---------- TEXT channels ---------- public static final class ChannelLineState { - final int rootBlockNumber; + final int lineCode; // для каналов: = rootBlockNumber; для канала 0: 0 + final int rootBlockNumber; // 0 для канала 0, иначе blockNumber CREATE_CHANNEL final String rootHashHex; int lastGlobalNumber; String lastHashHex; int lastThisLineNumber; // перед первым постом = -1, чтобы первый был 0 - ChannelLineState(int rootBlockNumber, String rootHashHex) { + ChannelLineState(int lineCode, int rootBlockNumber, String rootHashHex) { + this.lineCode = lineCode; this.rootBlockNumber = rootBlockNumber; this.rootHashHex = rootHashHex; this.lastGlobalNumber = rootBlockNumber; @@ -95,7 +99,7 @@ public final class ChainState { } } - // rootBlockNumber -> state + // lineCode -> state (для канала 0 lineCode=0) private final Map textChannels = new HashMap<>(); public ChainState() { @@ -134,18 +138,20 @@ public final class ChainState { // -------------------- line helpers -------------------- public static final class NextLine { + public final int lineCode; public final int prevLineNumber; // GLOBAL blockNumber public final byte[] prevLineHash32; // 32 bytes public final int thisLineNumber; // внутр. номер линии - public NextLine(int prevLineNumber, byte[] prevLineHash32, int thisLineNumber) { + public NextLine(int lineCode, int prevLineNumber, byte[] prevLineHash32, int thisLineNumber) { + this.lineCode = lineCode; this.prevLineNumber = prevLineNumber; this.prevLineHash32 = (prevLineHash32 == null ? null : prevLineHash32.clone()); this.thisLineNumber = thisLineNumber; } } - /** Следующие line-поля для TECH/CONNECTION/USER_PARAM. */ + /** Следующие line-поля для TECH/CONNECTION/USER_PARAM. lineCode=0. */ public NextLine nextLineByType(short type) { if (!hasHeader()) { throw new IllegalStateException("Нельзя формировать line-поля до HEADER (нет headerHash32)"); @@ -154,12 +160,15 @@ public final class ChainState { int t = type & 0xFFFF; if (t == TYPE_TECH) { - // tech-line: prev = последний TECH; первый CREATE_CHANNEL -> prev = HEADER if (techLine.lastGlobalNumber == -1) { - // после HEADER мы должны инициализировать techLine (делаем в applyHeader) throw new IllegalStateException("TECH line is not initialized yet"); } - return new NextLine(techLine.lastGlobalNumber, hexToBytes32(techLine.lastHashHex), techLine.lastThisLineNumber + 1); + return new NextLine( + 0, + techLine.lastGlobalNumber, + hexToBytes32(techLine.lastHashHex), + techLine.lastThisLineNumber + 1 + ); } if (t == TYPE_CONNECTION) { @@ -175,35 +184,55 @@ public final class ChainState { private NextLine nextSimpleLine(SimpleLineState ls) { if (ls.lastGlobalNumber == -1) { // первый блок линии ссылается на HEADER (block#0) - return new NextLine(0, headerHash32.clone(), 1); + return new NextLine(0, 0, headerHash32.clone(), 1); } if (ls.lastHashHex == null || ls.lastHashHex.isBlank()) { throw new IllegalStateException("LineState.lastHashHex пуст, но lastGlobalNumber!=-1"); } - return new NextLine(ls.lastGlobalNumber, hexToBytes32(ls.lastHashHex), ls.lastThisLineNumber + 1); + return new NextLine(0, ls.lastGlobalNumber, hexToBytes32(ls.lastHashHex), ls.lastThisLineNumber + 1); } - /** Следующие line-поля для TEXT-канала по rootBlockNumber. */ - public NextLine nextTextLineByRoot(int rootBlockNumber) { + /** + * Следующие line-поля для TEXT-канала по lineCode. + * Для канала 0: lineCode=0. + * Для других каналов: lineCode = rootBlockNumber (CREATE_CHANNEL blockNumber). + */ + public NextLine nextTextLineByCode(int lineCode) { if (!hasHeader()) throw new IllegalStateException("No HEADER"); - ChannelLineState cs = textChannels.get(rootBlockNumber); - if (cs == null) throw new IllegalStateException("Unknown TEXT channel rootBlockNumber=" + rootBlockNumber); + ChannelLineState cs = textChannels.get(lineCode); + if (cs == null) throw new IllegalStateException("Unknown TEXT channel lineCode=" + lineCode); return new NextLine( + lineCode, cs.lastGlobalNumber, hexToBytes32(cs.lastHashHex), cs.lastThisLineNumber + 1 ); } - /** Зарегистрировать новый канал TEXT по root = CREATE_CHANNEL block. */ - public void registerTextChannelRoot(int rootBlockNumber, byte[] rootHash32) { - if (rootBlockNumber <= 0) throw new IllegalArgumentException("rootBlockNumber must be > 0 for custom channel"); - if (rootHash32 == null || rootHash32.length != 32) throw new IllegalArgumentException("rootHash32 invalid"); - textChannels.put(rootBlockNumber, new ChannelLineState(rootBlockNumber, bytesToHex64(rootHash32))); + /** Старое имя — оставил для удобства: rootBlockNumber == lineCode для каналов. */ + public NextLine nextTextLineByRoot(int rootBlockNumber) { + return nextTextLineByCode(rootBlockNumber); } - /** root канала "0" (по умолчанию) — это HEADER block#0. */ + /** + * Зарегистрировать новый канал TEXT: + * - lineCode = rootBlockNumber (blockNumber CREATE_CHANNEL) + * ИДЕМПОТЕНТНО: если уже зарегистрирован — ничего не делаем. + */ + public void registerTextChannelRoot(int rootBlockNumber, byte[] rootHash32) { + if (rootBlockNumber < 0) throw new IllegalArgumentException("rootBlockNumber must be >= 0"); + if (rootHash32 == null || rootHash32.length != 32) throw new IllegalArgumentException("rootHash32 invalid"); + + if (textChannels.containsKey(rootBlockNumber)) { + return; // уже есть — не трогаем, чтобы не сбросить lastThisLineNumber и т.д. + } + + int lineCode = rootBlockNumber; + textChannels.put(lineCode, new ChannelLineState(lineCode, rootBlockNumber, bytesToHex64(rootHash32))); + } + + /** root/lineCode канала "0" (по умолчанию) — это HEADER block#0, lineCode=0. */ public int rootChannel0() { return 0; } @@ -240,8 +269,8 @@ public final class ChainState { techLine.lastHashHex = hex64; techLine.lastThisLineNumber = 0; - // TEXT channel "0" root = HEADER, первый пост будет thisLineNumber=0 - textChannels.put(0, new ChannelLineState(0, hex64)); + // TEXT channel "0" root = HEADER, lineCode=0 + registerTextChannelRoot(0, hash32); return; } @@ -253,6 +282,11 @@ public final class ChainState { techLine.lastGlobalNumber = blockNumber; techLine.lastHashHex = hex64; techLine.lastThisLineNumber = ccb.thisLineNumber; + + // ВАЖНО: CREATE_CHANNEL — это root нового текстового канала: + // lineCode для этого канала = blockNumber CREATE_CHANNEL + registerTextChannelRoot(blockNumber, hash32); + return; } @@ -273,10 +307,14 @@ public final class ChainState { // ---- TEXT channels (POST/EDIT_POST) ---- if (t == TYPE_TEXT && body instanceof TextBody tb) { if (tb.isLineMessage()) { - // ищем канал по совпадению prevLineNumber с lastGlobalNumber канала - ChannelLineState channel = findTextChannelByLastGlobal(tb.prevLineNumber); + int lineCode = tb.lineCode; + + ChannelLineState channel = textChannels.get(lineCode); if (channel == null) { - throw new IllegalStateException("TEXT line message prevLineNumber=" + tb.prevLineNumber + " не привязан ни к одному каналу (канал root не зарегистрирован?)"); + throw new IllegalStateException( + "TEXT line message has unknown lineCode=" + lineCode + + " (канал не зарегистрирован; ждали CREATE_CHANNEL или HEADER)" + ); } channel.lastGlobalNumber = blockNumber; @@ -286,13 +324,6 @@ public final class ChainState { } } - private ChannelLineState findTextChannelByLastGlobal(int prevLineNumber) { - for (ChannelLineState cs : textChannels.values()) { - if (cs.lastGlobalNumber == prevLineNumber) return cs; - } - return null; - } - // -------------------- utils -------------------- private static byte[] hexToBytes32(String hex) { diff --git a/src/test/java/test/it/cases/IT_03_AddBlock_NoAuth.java b/src/test/java/test/it/cases/IT_03_AddBlock_NoAuth.java index f41e9a3..7912597 100644 --- a/src/test/java/test/it/cases/IT_03_AddBlock_NoAuth.java +++ b/src/test/java/test/it/cases/IT_03_AddBlock_NoAuth.java @@ -64,13 +64,14 @@ public class IT_03_AddBlock_NoAuth { assertTrue(st1.hasHeader()); // канал "0" (root=HEADER) — по умолчанию существует - int root0 = st1.rootChannel0(); + int root0 = st1.rootChannel0(); // lineCode для канала "0" = 0 // POST в канал "0" { var ln = st1.nextTextLineByRoot(root0); sender1.send(new TextBody( MsgSubType.TEXT_POST, + root0, // lineCode ln.prevLineNumber, ln.prevLineHash32, ln.thisLineNumber, "U1: story/post in channel 0", null, null, null @@ -87,11 +88,12 @@ public class IT_03_AddBlock_NoAuth { { var ln = st1.nextLineByType(ChainState.TYPE_TECH); sender1.send(new CreateChannelBody( + 0, // lineCode для TECH линии ln.prevLineNumber, ln.prevLineHash32, ln.thisLineNumber, "News" ), t); - newsRootBlock = st1.lastBlockNumber(); + newsRootBlock = st1.lastBlockNumber(); // root канала = blockNumber CREATE_CHANNEL newsRootHash = st1.getHash32(newsRootBlock); assertNotNull(newsRootHash); @@ -106,6 +108,7 @@ public class IT_03_AddBlock_NoAuth { var ln = st1.nextTextLineByRoot(newsRootBlock); sender1.send(new TextBody( MsgSubType.TEXT_POST, + newsRootBlock, // lineCode = root блока канала (CREATE_CHANNEL) ln.prevLineNumber, ln.prevLineHash32, ln.thisLineNumber, "U1: News post #0", null, null, null @@ -121,18 +124,19 @@ public class IT_03_AddBlock_NoAuth { var ln = st1.nextTextLineByRoot(newsRootBlock); sender1.send(new TextBody( MsgSubType.TEXT_POST, + newsRootBlock, // lineCode ln.prevLineNumber, ln.prevLineHash32, ln.thisLineNumber, "U1: News post #1", null, null, null ), t); } - // EDIT_POST (не увеличивает thisLineNumber, но является частью линии) + // EDIT_POST (является частью линии; lineCode обязателен) { var ln = st1.nextTextLineByRoot(newsRootBlock); - // edit должен иметь thisLineNumber как у предыдущего сообщения линии (ChainState это уже даёт) sender1.send(new TextBody( MsgSubType.TEXT_EDIT_POST, + newsRootBlock, // lineCode ln.prevLineNumber, ln.prevLineHash32, ln.thisLineNumber, "U1: News post #0 (EDIT)", null, @@ -151,14 +155,13 @@ public class IT_03_AddBlock_NoAuth { assertTrue(st2.hasHeader()); // REPLY (20): ответ на post в чужом блокчейне/канале + // ВАЖНО: REPLY не имеет line-полей вообще, поэтому используем фабрику newReply(). { - sender2.send(new TextBody( - MsgSubType.TEXT_REPLY, - -1, new byte[32], -1, // для replies линии нет - "U2: reply to U1 News post #0 (cross-chain)", + sender2.send(TextBody.newReply( bch1, newsPost0Block, - newsPost0Hash + newsPost0Hash, + "U2: reply to U1 News post #0 (cross-chain)" ), t); }