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