From 3f5f94a53f21177ad086f04a0840bf367260c8b0262c711a82eefd3e9c4e8aee Mon Sep 17 00:00:00 2001 From: AidarKC Date: Thu, 22 Jan 2026 01:57:02 +0300 Subject: [PATCH] 22 01 25 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Да вроде всё работает и тесты проходят. И блоки добавляются все что надо для MVP --- .../blockchain/body/BodyRecordParser.java | 20 +- .../java/blockchain/body/TextLineBody.java | 265 +++++++++++++++ .../java/blockchain/body/TextReplyBody.java | 244 ++++++++++++++ .../java/shine/db/DatabaseInitializer.java | 313 +----------------- .../blockchain/Net_AddBlock_Handler.java | 34 +- 5 files changed, 566 insertions(+), 310 deletions(-) create mode 100644 shine-server-blockchain/src/main/java/blockchain/body/TextLineBody.java create mode 100644 shine-server-blockchain/src/main/java/blockchain/body/TextReplyBody.java diff --git a/shine-server-blockchain/src/main/java/blockchain/body/BodyRecordParser.java b/shine-server-blockchain/src/main/java/blockchain/body/BodyRecordParser.java index 121a199..38b6669 100644 --- a/shine-server-blockchain/src/main/java/blockchain/body/BodyRecordParser.java +++ b/shine-server-blockchain/src/main/java/blockchain/body/BodyRecordParser.java @@ -1,5 +1,7 @@ package blockchain.body; +import blockchain.MsgSubType; + /** * Парсер body выбирает класс по header: type/subType/version, * потому что bodyBytes больше НЕ содержат type/subType/version. @@ -28,7 +30,23 @@ public final class BodyRecordParser { throw new IllegalArgumentException("Unknown TECH subType for type=0 ver=1: subType=" + st); } - case TextBody.KEY -> new TextBody(subType, version, bodyBytes); + // TEXT type=1 ver=1: выбираем класс по subType + case TextBody.KEY -> { + int st = subType & 0xFFFF; + + if (st == (MsgSubType.TEXT_POST & 0xFFFF) + || st == (MsgSubType.TEXT_EDIT_POST & 0xFFFF)) { + yield new TextLineBody(subType, version, bodyBytes); + } + + if (st == (MsgSubType.TEXT_REPLY & 0xFFFF) + || st == (MsgSubType.TEXT_EDIT_REPLY & 0xFFFF)) { + yield new TextReplyBody(subType, version, bodyBytes); + } + + throw new IllegalArgumentException("Unknown TEXT subType for type=1 ver=1: subType=" + st); + } + case ReactionBody.KEY -> new ReactionBody(subType, version, bodyBytes); case ConnectionBody.KEY -> new ConnectionBody(subType, version, bodyBytes); case UserParamBody.KEY -> new UserParamBody(subType, version, bodyBytes); diff --git a/shine-server-blockchain/src/main/java/blockchain/body/TextLineBody.java b/shine-server-blockchain/src/main/java/blockchain/body/TextLineBody.java new file mode 100644 index 0000000..4a8f224 --- /dev/null +++ b/shine-server-blockchain/src/main/java/blockchain/body/TextLineBody.java @@ -0,0 +1,265 @@ +package blockchain.body; + +import blockchain.MsgSubType; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.charset.CharacterCodingException; +import java.nio.charset.CodingErrorAction; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Objects; + +/** + * TextLineBody — type=1, ver=1. + * + * subType: + * - POST (10) + * - EDIT_POST (11) + * + * Формат bodyBytes (BigEndian): + * + * POST: + * [4] lineCode + * [4] prevLineNumber + * [32] prevLineHash32 + * [4] thisLineNumber + * [2] textLenBytes (uint16) + * [N] text UTF-8 + * + * EDIT_POST: + * [4] lineCode + * [4] prevLineNumber + * [32] prevLineHash32 + * [4] thisLineNumber + * [4] toBlockGlobalNumber (int32) + * [32] toBlockHash32 + * [2] textLenBytes (uint16) + * [N] text UTF-8 + */ +public final class TextLineBody implements BodyRecord, BodyHasLine, BodyHasTarget { + + public static final short TYPE = 1; + public static final short VER = 1; + + public static final int KEY = ((TYPE & 0xFFFF) << 16) | (VER & 0xFFFF); + + public final short subType; // из header + public final short version; // из header (=1) + + // line + public final int lineCode; + public final int prevLineNumber; + public final byte[] prevLineHash32; // 32 (может быть нули) + public final int thisLineNumber; + + // target (только для EDIT_POST) + public final Integer toBlockGlobalNumber; // nullable для POST + public final byte[] toBlockHash32; // nullable для POST + + // text + public final String message; + + /* ====================== parse from bytes ====================== */ + + public TextLineBody(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("TextLineBody version must be 1, got=" + (this.version & 0xFFFF)); + } + + int st = this.subType & 0xFFFF; + if (st != (MsgSubType.TEXT_POST & 0xFFFF) && st != (MsgSubType.TEXT_EDIT_POST & 0xFFFF)) { + throw new IllegalArgumentException("TextLineBody supports only POST/EDIT_POST, got subType=" + st); + } + + ByteBuffer bb = ByteBuffer.wrap(bodyBytes).order(ByteOrder.BIG_ENDIAN); + + // минимум line + textLen(2) + ensureMin(bb, (4 + 4 + 32 + 4) + 2, "TextLineBody too short"); + + this.lineCode = bb.getInt(); + this.prevLineNumber = bb.getInt(); + + this.prevLineHash32 = new byte[32]; + bb.get(this.prevLineHash32); + + this.thisLineNumber = bb.getInt(); + + if (st == (MsgSubType.TEXT_EDIT_POST & 0xFFFF)) { + // нужен target + ensureMin(bb, (4 + 32) + 2, "EDIT_POST missing target"); + int tgtNum = bb.getInt(); + byte[] tgtHash = new byte[32]; + bb.get(tgtHash); + + this.toBlockGlobalNumber = tgtNum; + this.toBlockHash32 = tgtHash; + + } else { + this.toBlockGlobalNumber = null; + this.toBlockHash32 = null; + } + + this.message = readStrictUtf8Len16(bb, "TextLineBody text"); + + ensureNoTail(bb, "TextLineBody"); + } + + /* ====================== manual ctor ====================== */ + + public TextLineBody(int lineCode, + int prevLineNumber, + byte[] prevLineHash32, + int thisLineNumber, + short subType, + Integer toBlockGlobalNumber, + byte[] toBlockHash32, + String message) { + + Objects.requireNonNull(message, "message == null"); + + int st = subType & 0xFFFF; + if (st != (MsgSubType.TEXT_POST & 0xFFFF) && st != (MsgSubType.TEXT_EDIT_POST & 0xFFFF)) { + throw new IllegalArgumentException("TextLineBody supports only POST/EDIT_POST"); + } + + if (lineCode < 0) throw new IllegalArgumentException("lineCode < 0"); + if (message.isBlank()) throw new IllegalArgumentException("message is blank"); + + this.subType = subType; + this.version = VER; + + this.lineCode = lineCode; + this.prevLineNumber = prevLineNumber; + this.prevLineHash32 = (prevLineHash32 == null ? new byte[32] : Arrays.copyOf(prevLineHash32, 32)); + this.thisLineNumber = thisLineNumber; + + if (st == (MsgSubType.TEXT_EDIT_POST & 0xFFFF)) { + Objects.requireNonNull(toBlockGlobalNumber, "toBlockGlobalNumber == null"); + Objects.requireNonNull(toBlockHash32, "toBlockHash32 == null"); + if (toBlockGlobalNumber < 0) throw new IllegalArgumentException("toBlockGlobalNumber < 0"); + if (toBlockHash32.length != 32) throw new IllegalArgumentException("toBlockHash32 != 32"); + + this.toBlockGlobalNumber = toBlockGlobalNumber; + this.toBlockHash32 = Arrays.copyOf(toBlockHash32, 32); + } else { + this.toBlockGlobalNumber = null; + this.toBlockHash32 = null; + } + + this.message = message; + } + + @Override + public TextLineBody check() { + int st = subType & 0xFFFF; + if (st != (MsgSubType.TEXT_POST & 0xFFFF) && st != (MsgSubType.TEXT_EDIT_POST & 0xFFFF)) + throw new IllegalArgumentException("Bad TextLineBody subType: " + st); + + if (lineCode < 0) throw new IllegalArgumentException("lineCode < 0"); + if (prevLineHash32 == null || prevLineHash32.length != 32) + throw new IllegalArgumentException("prevLineHash32 invalid"); + + if (message == null || message.isBlank()) + throw new IllegalArgumentException("Text message is blank"); + + if (st == (MsgSubType.TEXT_EDIT_POST & 0xFFFF)) { + if (toBlockGlobalNumber == null || toBlockGlobalNumber < 0) + throw new IllegalArgumentException("EDIT_POST toBlockGlobalNumber invalid"); + if (toBlockHash32 == null || toBlockHash32.length != 32) + throw new IllegalArgumentException("EDIT_POST toBlockHash32 invalid"); + } else { + if (toBlockGlobalNumber != null || toBlockHash32 != null) + throw new IllegalArgumentException("POST must not contain target fields"); + } + + return this; + } + + @Override + public byte[] toBytes() { + byte[] msgUtf8 = message.getBytes(StandardCharsets.UTF_8); + if (msgUtf8.length == 0) throw new IllegalArgumentException("Text payload is empty"); + if (msgUtf8.length > 65535) throw new IllegalArgumentException("Text too long (>65535 bytes)"); + + int st = subType & 0xFFFF; + + int cap; + if (st == (MsgSubType.TEXT_POST & 0xFFFF)) { + cap = (4 + 4 + 32 + 4) + 2 + msgUtf8.length; + } else { + // EDIT_POST + if (toBlockGlobalNumber == null) throw new IllegalArgumentException("EDIT_POST missing toBlockGlobalNumber"); + if (toBlockHash32 == null || toBlockHash32.length != 32) throw new IllegalArgumentException("EDIT_POST toBlockHash32 != 32"); + 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); + + if (st == (MsgSubType.TEXT_EDIT_POST & 0xFFFF)) { + bb.putInt(toBlockGlobalNumber); + bb.put(toBlockHash32); + } + + bb.putShort((short) msgUtf8.length); + bb.put(msgUtf8); + + return bb.array(); + } + + /* ====================== BodyHasLine ====================== */ + @Override public int lineCode() { return lineCode; } + @Override public int prevLineNumber() { return prevLineNumber; } + @Override public byte[] prevLineHash32() { return Arrays.copyOf(prevLineHash32, 32); } + @Override public int thisLineNumber() { return thisLineNumber; } + + /* ====================== BodyHasTarget ===================== */ + @Override public String toBchName() { return null; } // по ТЗ: не хранить + @Override public Integer toBlockGlobalNumber() { return toBlockGlobalNumber; } + @Override public byte[] toBlockHashBytes() { return toBlockHash32; } + + /* ====================== helpers ====================== */ + + public boolean isEditPost() { + return (subType & 0xFFFF) == (MsgSubType.TEXT_EDIT_POST & 0xFFFF); + } + + private static String readStrictUtf8Len16(ByteBuffer bb, String fieldName) { + int len = Short.toUnsignedInt(bb.getShort()); + if (len <= 0) throw new IllegalArgumentException(fieldName + " is empty"); + if (bb.remaining() < len) throw new IllegalArgumentException(fieldName + " payload too short (len=" + len + ")"); + + byte[] bytes = new byte[len]; + bb.get(bytes); + + var decoder = StandardCharsets.UTF_8.newDecoder() + .onMalformedInput(CodingErrorAction.REPORT) + .onUnmappableCharacter(CodingErrorAction.REPORT); + + try { + String s = decoder.decode(ByteBuffer.wrap(bytes)).toString(); + if (s.isBlank()) throw new IllegalArgumentException(fieldName + " is blank"); + return s; + } catch (CharacterCodingException e) { + throw new IllegalArgumentException(fieldName + " is not valid UTF-8", e); + } + } + + private static void ensureMin(ByteBuffer bb, int need, String msg) { + if (bb.remaining() < need) throw new IllegalArgumentException(msg + " (need=" + need + ", remaining=" + bb.remaining() + ")"); + } + + private static void ensureNoTail(ByteBuffer bb, String ctx) { + if (bb.remaining() != 0) throw new IllegalArgumentException("Unexpected tail bytes for " + ctx + ", remaining=" + bb.remaining()); + } +} \ No newline at end of file diff --git a/shine-server-blockchain/src/main/java/blockchain/body/TextReplyBody.java b/shine-server-blockchain/src/main/java/blockchain/body/TextReplyBody.java new file mode 100644 index 0000000..b2131ff --- /dev/null +++ b/shine-server-blockchain/src/main/java/blockchain/body/TextReplyBody.java @@ -0,0 +1,244 @@ +package blockchain.body; + +import blockchain.MsgSubType; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.charset.CharacterCodingException; +import java.nio.charset.CodingErrorAction; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Objects; + +/** + * TextReplyBody — type=1, ver=1. + * + * subType: + * - REPLY (20) + * - EDIT_REPLY (21) + * + * Форматы bodyBytes (BigEndian): + * + * REPLY: + * [1] toBlockchainNameLen (uint8) + * [N] toBlockchainName UTF-8 + * [4] toBlockGlobalNumber + * [32] toBlockHash32 + * [2] textLenBytes (uint16) + * [M] text UTF-8 + * + * EDIT_REPLY: + * [4] toBlockGlobalNumber + * [32] toBlockHash32 + * [2] textLenBytes (uint16) + * [N] text UTF-8 + */ +public final class TextReplyBody implements BodyRecord, BodyHasTarget { + + public static final short TYPE = 1; + public static final short VER = 1; + + public static final int KEY = ((TYPE & 0xFFFF) << 16) | (VER & 0xFFFF); + + public final short subType; // из header + public final short version; // (=1) + + // target + public final String toBlockchainName; // nullable для EDIT_REPLY + public final int toBlockGlobalNumber; + public final byte[] toBlockHash32; // 32 + + // text + public final String message; + + public TextReplyBody(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("TextReplyBody version must be 1, got=" + (this.version & 0xFFFF)); + } + + int st = this.subType & 0xFFFF; + if (st != (MsgSubType.TEXT_REPLY & 0xFFFF) && st != (MsgSubType.TEXT_EDIT_REPLY & 0xFFFF)) { + throw new IllegalArgumentException("TextReplyBody supports only REPLY/EDIT_REPLY, got subType=" + st); + } + + ByteBuffer bb = ByteBuffer.wrap(bodyBytes).order(ByteOrder.BIG_ENDIAN); + + if (st == (MsgSubType.TEXT_REPLY & 0xFFFF)) { + // минимум: nameLen[1]+name[1]+global[4]+hash[32]+textLen[2] + ensureMin(bb, 1 + 1 + 4 + 32 + 2, "REPLY too short"); + + int nameLen = Byte.toUnsignedInt(bb.get()); + if (nameLen <= 0) throw new IllegalArgumentException("REPLY toBlockchainNameLen is 0"); + ensureMin(bb, nameLen + 4 + 32 + 2, "REPLY payload too short"); + + byte[] nameBytes = new byte[nameLen]; + bb.get(nameBytes); + this.toBlockchainName = new String(nameBytes, StandardCharsets.UTF_8); + + this.toBlockGlobalNumber = bb.getInt(); + + this.toBlockHash32 = new byte[32]; + bb.get(this.toBlockHash32); + + } else { + // EDIT_REPLY: target без имени + ensureMin(bb, (4 + 32) + 2, "EDIT_REPLY too short"); + + this.toBlockchainName = null; + this.toBlockGlobalNumber = bb.getInt(); + + this.toBlockHash32 = new byte[32]; + bb.get(this.toBlockHash32); + } + + this.message = readStrictUtf8Len16(bb, "TextReplyBody text"); + ensureNoTail(bb, "TextReplyBody"); + } + + public TextReplyBody(short subType, + int toBlockGlobalNumber, + byte[] toBlockHash32, + String toBlockchainName, + String message) { + + Objects.requireNonNull(message, "message == null"); + Objects.requireNonNull(toBlockHash32, "toBlockHash32 == null"); + + int st = subType & 0xFFFF; + if (st != (MsgSubType.TEXT_REPLY & 0xFFFF) && st != (MsgSubType.TEXT_EDIT_REPLY & 0xFFFF)) { + throw new IllegalArgumentException("TextReplyBody supports only REPLY/EDIT_REPLY"); + } + + if (message.isBlank()) throw new IllegalArgumentException("message is blank"); + if (toBlockGlobalNumber < 0) throw new IllegalArgumentException("toBlockGlobalNumber < 0"); + if (toBlockHash32.length != 32) throw new IllegalArgumentException("toBlockHash32 != 32"); + + if (st == (MsgSubType.TEXT_REPLY & 0xFFFF)) { + Objects.requireNonNull(toBlockchainName, "toBlockchainName == null"); + if (toBlockchainName.isBlank()) throw new IllegalArgumentException("toBlockchainName is blank"); + this.toBlockchainName = toBlockchainName; + } else { + // EDIT_REPLY: имя не хранить + this.toBlockchainName = null; + } + + this.subType = subType; + this.version = VER; + + this.toBlockGlobalNumber = toBlockGlobalNumber; + this.toBlockHash32 = Arrays.copyOf(toBlockHash32, 32); + + this.message = message; + } + + @Override + public TextReplyBody check() { + int st = subType & 0xFFFF; + if (st != (MsgSubType.TEXT_REPLY & 0xFFFF) && st != (MsgSubType.TEXT_EDIT_REPLY & 0xFFFF)) + throw new IllegalArgumentException("Bad TextReplyBody subType: " + st); + + if (message == null || message.isBlank()) + throw new IllegalArgumentException("Text message is blank"); + + if (toBlockGlobalNumber < 0) + throw new IllegalArgumentException("toBlockGlobalNumber < 0"); + if (toBlockHash32 == null || toBlockHash32.length != 32) + throw new IllegalArgumentException("toBlockHash32 invalid"); + + if (st == (MsgSubType.TEXT_REPLY & 0xFFFF)) { + if (toBlockchainName == null || toBlockchainName.isBlank()) + throw new IllegalArgumentException("REPLY toBlockchainName is blank"); + } else { + if (toBlockchainName != null) + throw new IllegalArgumentException("EDIT_REPLY must not contain toBlockchainName"); + } + + return this; + } + + @Override + public byte[] toBytes() { + byte[] msgUtf8 = message.getBytes(StandardCharsets.UTF_8); + if (msgUtf8.length == 0) throw new IllegalArgumentException("Text payload is empty"); + if (msgUtf8.length > 65535) throw new IllegalArgumentException("Text too long (>65535 bytes)"); + + int st = subType & 0xFFFF; + + if (st == (MsgSubType.TEXT_REPLY & 0xFFFF)) { + if (toBlockchainName == null) throw new IllegalArgumentException("REPLY missing toBlockchainName"); + + byte[] nameUtf8 = toBlockchainName.getBytes(StandardCharsets.UTF_8); + if (nameUtf8.length == 0 || nameUtf8.length > 255) + throw new IllegalArgumentException("REPLY toBlockchainName utf8 len must be 1..255"); + + int cap = 1 + nameUtf8.length + 4 + 32 + 2 + msgUtf8.length; + + ByteBuffer bb = ByteBuffer.allocate(cap).order(ByteOrder.BIG_ENDIAN); + bb.put((byte) nameUtf8.length); + bb.put(nameUtf8); + bb.putInt(toBlockGlobalNumber); + bb.put(toBlockHash32); + bb.putShort((short) msgUtf8.length); + bb.put(msgUtf8); + + return bb.array(); + } + + // EDIT_REPLY + int cap = (4 + 32) + 2 + msgUtf8.length; + + ByteBuffer bb = ByteBuffer.allocate(cap).order(ByteOrder.BIG_ENDIAN); + bb.putInt(toBlockGlobalNumber); + bb.put(toBlockHash32); + bb.putShort((short) msgUtf8.length); + bb.put(msgUtf8); + + return bb.array(); + } + + /* ====================== BodyHasTarget ====================== */ + + @Override public String toBchName() { return toBlockchainName; } + @Override public Integer toBlockGlobalNumber() { return toBlockGlobalNumber; } + @Override public byte[] toBlockHashBytes() { return toBlockHash32; } + + public boolean isEditReply() { + return (subType & 0xFFFF) == (MsgSubType.TEXT_EDIT_REPLY & 0xFFFF); + } + + /* ====================== helpers ====================== */ + + private static String readStrictUtf8Len16(ByteBuffer bb, String fieldName) { + int len = Short.toUnsignedInt(bb.getShort()); + if (len <= 0) throw new IllegalArgumentException(fieldName + " is empty"); + if (bb.remaining() < len) throw new IllegalArgumentException(fieldName + " payload too short (len=" + len + ")"); + + byte[] bytes = new byte[len]; + bb.get(bytes); + + var decoder = StandardCharsets.UTF_8.newDecoder() + .onMalformedInput(CodingErrorAction.REPORT) + .onUnmappableCharacter(CodingErrorAction.REPORT); + + try { + String s = decoder.decode(ByteBuffer.wrap(bytes)).toString(); + if (s.isBlank()) throw new IllegalArgumentException(fieldName + " is blank"); + return s; + } catch (CharacterCodingException e) { + throw new IllegalArgumentException(fieldName + " is not valid UTF-8", e); + } + } + + private static void ensureMin(ByteBuffer bb, int need, String msg) { + if (bb.remaining() < need) throw new IllegalArgumentException(msg + " (need=" + need + ", remaining=" + bb.remaining() + ")"); + } + + private static void ensureNoTail(ByteBuffer bb, String ctx) { + if (bb.remaining() != 0) throw new IllegalArgumentException("Unexpected tail bytes for " + ctx + ", remaining=" + bb.remaining()); + } +} \ No newline at end of file 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 afd5a7c..00b6b4f 100644 --- a/shine-server-db/src/main/java/shine/db/DatabaseInitializer.java +++ b/shine-server-db/src/main/java/shine/db/DatabaseInitializer.java @@ -14,17 +14,17 @@ import java.sql.Statement; /** * DatabaseInitializer — создание новой SQLite-БД по схеме SHiNE. * - * Таблицы: - * - solana_users - * - active_sessions - * - users_params - * - ip_geo_cache - * - blockchain_state - * - blocks - * - connections_state - * - message_stats + * В этой версии: + * - создаём ТОЛЬКО таблицы/индексы + * - в конце вызываем DatabaseTriggersInstaller.createAllTriggers(st) + * + * Зачем так: + * - триггеры часто ломают совместимость с внешними SQLite-просмотрщиками/сборками + * - проще поддерживать/мигрировать */ -public class DatabaseInitializer { +public final class DatabaseInitializer { + + private DatabaseInitializer() {} /* ===================== TEXT (msg_type=1) ===================== */ @@ -46,7 +46,6 @@ public class DatabaseInitializer { public static final short REACTION_LIKE = 1; /* ===================== CONNECTION (msg_type=3) ===================== */ - // Приведено к твоему shine.db.MsgSubType: // FRIEND=10/11, CONTACT=20/21, FOLLOW=30/31 public static final short CONNECTION_FRIEND = 10; public static final short CONNECTION_UNFRIEND = 11; @@ -264,138 +263,6 @@ public class DatabaseInitializer { 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 ( @@ -427,59 +294,7 @@ public class DatabaseInitializer { ON connections_state (login, to_login); """); - // 8) Trigger: connection state - st.executeUpdate(""" - CREATE TRIGGER IF NOT EXISTS trg_blocks_connection_state_ai - AFTER INSERT ON blocks - WHEN NEW.msg_type = 3 - BEGIN - - INSERT INTO connections_state ( - login, rel_type, to_login, to_bch_name, to_block_number, to_block_hash - ) - SELECT - NEW.login, - NEW.msg_sub_type, - NEW.to_login, - NEW.to_bch_name, - NEW.to_block_number, - NEW.to_block_hash - WHERE NEW.msg_sub_type IN (%d, %d, %d) - AND NEW.to_login IS NOT NULL - AND NEW.to_bch_name IS NOT NULL - ON CONFLICT(login, rel_type, to_login) - DO UPDATE SET - to_bch_name = excluded.to_bch_name, - to_block_number = excluded.to_block_number, - to_block_hash = excluded.to_block_hash; - - DELETE FROM connections_state - WHERE login = NEW.login - AND to_login = NEW.to_login - AND rel_type = CASE NEW.msg_sub_type - WHEN %d THEN %d - WHEN %d THEN %d - WHEN %d THEN %d - ELSE rel_type - END - AND NEW.msg_sub_type IN (%d, %d, %d); - - END; - """.formatted( - (int) CONNECTION_FRIEND, - (int) CONNECTION_CONTACT, - (int) CONNECTION_FOLLOW, - - (int) CONNECTION_UNFRIEND, (int) CONNECTION_FRIEND, - (int) CONNECTION_UNCONTACT, (int) CONNECTION_CONTACT, - (int) CONNECTION_UNFOLLOW, (int) CONNECTION_FOLLOW, - - (int) CONNECTION_UNFRIEND, - (int) CONNECTION_UNCONTACT, - (int) CONNECTION_UNFOLLOW - )); - - // 9) message_stats + // 8) message_stats st.executeUpdate(""" CREATE TABLE IF NOT EXISTS message_stats ( to_login TEXT NOT NULL, @@ -510,110 +325,8 @@ public class DatabaseInitializer { ON message_stats (to_login); """); - // 10) Trigger: LIKE - st.executeUpdate(""" - CREATE TRIGGER IF NOT EXISTS trg_blocks_message_stats_like_ai - AFTER INSERT ON blocks - WHEN NEW.msg_type = 2 AND NEW.msg_sub_type = %d - BEGIN - INSERT INTO message_stats ( - to_login, - to_bch_name, - to_block_number, - to_block_hash, - likes_count, - replies_count, - edits_count - ) - SELECT - NEW.to_login, - NEW.to_bch_name, - NEW.to_block_number, - NEW.to_block_hash, - 1, - 0, - 0 - WHERE NEW.to_login IS NOT NULL - AND NEW.to_bch_name IS NOT NULL - AND NEW.to_block_number IS NOT NULL - AND NEW.to_block_hash IS NOT NULL - ON CONFLICT(to_login, to_bch_name, to_block_number, to_block_hash) - DO UPDATE SET - likes_count = message_stats.likes_count + 1; - END; - """.formatted((int) REACTION_LIKE)); - - // 11) Trigger: REPLY - st.executeUpdate(""" - CREATE TRIGGER IF NOT EXISTS trg_blocks_message_stats_reply_ai - AFTER INSERT ON blocks - WHEN NEW.msg_type = 1 AND NEW.msg_sub_type = %d - BEGIN - INSERT INTO message_stats ( - to_login, - to_bch_name, - to_block_number, - to_block_hash, - likes_count, - replies_count, - edits_count - ) - SELECT - NEW.to_login, - NEW.to_bch_name, - NEW.to_block_number, - NEW.to_block_hash, - 0, - 1, - 0 - WHERE NEW.to_login IS NOT NULL - AND NEW.to_bch_name IS NOT NULL - AND NEW.to_block_number IS NOT NULL - AND NEW.to_block_hash IS NOT NULL - ON CONFLICT(to_login, to_bch_name, to_block_number, to_block_hash) - DO UPDATE SET - replies_count = message_stats.replies_count + 1; - END; - """.formatted((int) TEXT_REPLY)); - - // 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 - 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; - - INSERT INTO message_stats ( - to_login, - to_bch_name, - to_block_number, - to_block_hash, - likes_count, - replies_count, - edits_count - ) - SELECT - NEW.to_login, - NEW.to_bch_name, - NEW.to_block_number, - NEW.to_block_hash, - 0, - 0, - 1 - WHERE NEW.to_login IS NOT NULL - AND NEW.to_bch_name IS NOT NULL - AND NEW.to_block_number IS NOT NULL - AND NEW.to_block_hash IS NOT NULL - ON CONFLICT(to_login, to_bch_name, to_block_number, to_block_hash) - DO UPDATE SET - edits_count = message_stats.edits_count + 1; - END; - """.formatted((int) TEXT_EDIT)); + // ВАЖНО: триггеры ставим отдельно + DatabaseTriggersInstaller.createAllTriggers(st); } } } \ No newline at end of file diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/Net_AddBlock_Handler.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/Net_AddBlock_Handler.java index 48f71bf..596a982 100644 --- a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/Net_AddBlock_Handler.java +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/Net_AddBlock_Handler.java @@ -2,6 +2,7 @@ package server.logic.ws_protocol.JSON.handlers.blockchain; import blockchain.BchBlockEntry; import blockchain.BchCryptoVerifier; +import blockchain.MsgSubType; import blockchain.body.BodyHasLine; import blockchain.body.BodyHasTarget; import org.slf4j.Logger; @@ -33,13 +34,12 @@ import java.util.concurrent.locks.ReentrantLock; * 2) Проверяем: * - incoming.blockNumber == last+1 * - incoming.prevHash32 == last_hash (для genesis last_hash = 32 нулей) - * 3) Считаем hash32 = SHA-256(preimage) (preimage = block_bytes без signature64) - * 4) Проверяем подпись Ed25519.verify(hash32, signature64, pubKey) - * 5) Если тип имеет линию: - * - если prevLineNumber != -1: + * 3) Проверяем подпись Ed25519.verify(hash32(preimage), signature64, pubKey) + * 4) Если тип имеет линию: + * - если prevLineNumber != null: * достаём hash блока prevLineNumber из blocks * сравниваем с prevLineHash32 из body - * 6) Сохраняем блок в blocks + обновляем blockchain_state + * 5) Сохраняем блок в blocks + обновляем blockchain_state * * Важно: * - Сетевой протокол AddBlock пока оставляем старые поля (globalNumber/prevGlobalHash), @@ -224,17 +224,27 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler { return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_signature", serverLastNum, serverLastHashHex); } - // 7) линейная проверка (только для типов с линией) + // 7) line columns (only for BodyHasLine) + Integer lineCode = null; Integer prevLineNumber = null; byte[] prevLineHash32 = null; Integer thisLineNumber = null; if (block.body instanceof BodyHasLine bl) { + lineCode = bl.lineCode(); prevLineNumber = bl.prevLineNumber(); prevLineHash32 = bl.prevLineHash32(); thisLineNumber = bl.thisLineNumber(); - if (prevLineNumber != null && prevLineNumber != -1) { + // Нормализация: -1 не пишем в БД (для совместимости со старым TextBody) + if (prevLineNumber != null && prevLineNumber == -1) { + prevLineNumber = null; + prevLineHash32 = null; + thisLineNumber = null; + } + + // Если prevLineNumber задан — проверяем его хэш + if (prevLineNumber != null) { try { byte[] dbPrevHash = blocksDAO.getHashByNumber(blockchainName, prevLineNumber); if (dbPrevHash == null) { @@ -270,6 +280,7 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler { be.setBlockSignature(block.getSignature64()); // line columns (optional) + be.setLineCode(lineCode); be.setPrevLineNumber(prevLineNumber); be.setPrevLineHash(prevLineHash32); be.setThisLineNumber(thisLineNumber); @@ -282,8 +293,13 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler { be.setToBlockHash(t.toBlockHashBytes()); } - // edit helper (optional): если TEXT_EDIT — это "редактирование блока цели" - if ((block.type & 0xFFFF) == 1 && (block.subType & 0xFFFF) == 10 && be.getToBlockNumber() != null) { + // edit helper (optional): если TEXT_EDIT_* — это "редактирование блока цели" + int type = block.type & 0xFFFF; + int sub = block.subType & 0xFFFF; + + if (type == 1 + && (sub == (MsgSubType.TEXT_EDIT_POST & 0xFFFF) || sub == (MsgSubType.TEXT_EDIT_REPLY & 0xFFFF)) + && be.getToBlockNumber() != null) { be.setEditedByBlockNumber(be.getToBlockNumber()); }