From 580695b486aaead669df6118d76ab04e70e8bb0bb30f6db1d5b28f275c3c1638 Mon Sep 17 00:00:00 2001 From: AidarKC Date: Fri, 23 Jan 2026 17:49:13 +0300 Subject: [PATCH] 23 01 25 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Сделал ещё более два поля в общем формате блоков блокчейна (перед самим блоком данных) и перед его цп (все тесты проходят) --- .../main/java/blockchain/BchBlockEntry.java | 198 +++++++++++++----- .../java/blockchain/BchCryptoVerifier.java | 6 +- .../test/it/blockchain/AddBlockSender.java | 30 ++- 3 files changed, 176 insertions(+), 58 deletions(-) diff --git a/shine-server-blockchain/src/main/java/blockchain/BchBlockEntry.java b/shine-server-blockchain/src/main/java/blockchain/BchBlockEntry.java index 0eb78d9..255644d 100644 --- a/shine-server-blockchain/src/main/java/blockchain/BchBlockEntry.java +++ b/shine-server-blockchain/src/main/java/blockchain/BchBlockEntry.java @@ -9,36 +9,65 @@ import java.util.Arrays; import java.util.Objects; /** - * BchBlockEntry — универсальный блок нового формата. + * BchBlockEntry — универсальный блок формата SHiNE (Frame v0). * - * RAW (BigEndian) = preimage: - * [32] prevHash32 (SHA-256) hash предыдущего блока (цепочка) - * [4] blockSize (int) = размер preimage (в байтах), БЕЗ signature64 - * [4] blockNumber (int) глобальный номер блока (>=0) - * [8] timestamp (long) unix seconds + * ========================================================================= + * FRAME v0 — ФИКСИРОВАННЫЙ ФОРМАТ БЛОКА (ДОКУМЕНТ ПРОТОКОЛА) + * ========================================================================= * - * [2] type (short) тип сообщения - * [2] subType (short) подтип сообщения - * [2] version (short) версия формата сообщения + * Все числа BigEndian. * + * PREIMAGE (входит в blockSize, подписывается): + * [2] frameCode (uint16) код/версия рамки: + * - 0x0000 = Frame v0 (текущий) + * [32] prevHash32 (bytes) SHA-256(preimage) предыдущего блока (цепочка) + * [4] blockSize (int32) размер preimage (в байтах), ВКЛЮЧАЯ frameCode, + * НО БЕЗ sigMarker и БЕЗ signature64 + * [4] blockNumber (int32) глобальный номер блока (>=0) + * [8] timestamp (int64) unix seconds + * [2] type (uint16) тип сообщения + * [2] subType (uint16) подтип сообщения + * [2] version (uint16) версия формата сообщения * [N] bodyBytes (bytes) тело сообщения (БЕЗ type/subType/version) * - * TAIL (НЕ входит в blockSize): - * [64] signature64 (Ed25519) подпись над hash32 + * TAIL (НЕ входит в blockSize, НЕ подписывается в Frame v0): + * [2] sigMarker (uint16) маркер подписи: + * - 0x0100 (256) = далее подпись Ed25519 64 байта + * [64] signature64 (bytes) Ed25519 signature над hash32 * - * hash32 ВНУТРИ БЛОКА НЕ ХРАНИМ. + * hash32 НЕ хранится в блоке. * hash32 вычисляется при парсинге: * preimage = первые blockSize байт * hash32 = SHA-256(preimage) + * + * Правила MVP-парсера (Frame v0): + * - frameCode должен быть строго 0x0000, иначе REJECT. + * - sigMarker должен быть строго 0x0100, иначе REJECT. + * - подпись обязана присутствовать всегда (sigMarker+signature64). + * - НИКАКИХ fallback-веток “если маркер другой, то подписи нет/другой хвост”. + * + * Важно по безопасности: + * - sigMarker в v0 не входит в подписываемые байты → его можно подменить, + * поэтому единственная безопасная логика: "если не 0x0100 — reject". + * ========================================================================= */ public final class BchBlockEntry { public static final int SIGNATURE_LEN = 64; public static final int HASH_LEN = 32; + public static final int FRAME_CODE_LEN = 2; + public static final int SIG_MARKER_LEN = 2; + + /** Frame v0 */ + public static final int FRAME_CODE_V0 = 0x0000; + + /** sigMarker: 256 = 0x0100 */ + public static final int SIG_MARKER_ED25519 = 0x0100; + /** - * Максимальный допустимый размер блока (preimage+signature), чтобы не уложить сервер по памяти/диску. - * 4 МБ — нормальный “потолок” под тексты/метаданные, и при этом защищает от мусора/атаки. + * Максимальный допустимый размер блока (fullBytes = preimage + sigMarker + signature), + * чтобы не уложить сервер по памяти/диску. */ public static final int MAX_BLOCK_FULL_BYTES = 4 * 1024 * 1024; @@ -48,9 +77,16 @@ public final class BchBlockEntry { */ public static final long MAX_FUTURE_SECONDS = 60; - /** Размер фиксированного RAW-заголовка без body */ - public static final int RAW_HEADER_SIZE = - 32 // prevHash32 + /** + * Размер фиксированной части PREIMAGE (без bodyBytes). + * + * PREIMAGE header: + * frameCode(2) + prevHash32(32) + blockSize(4) + blockNumber(4) + timestamp(8) + * + type(2) + subType(2) + version(2) + */ + public static final int PREIMAGE_HEADER_SIZE = + 2 // frameCode + + 32 // prevHash32 + 4 // blockSize + 4 // blockNumber + 8 // timestamp @@ -58,28 +94,34 @@ public final class BchBlockEntry { + 2 // subType + 2; // version - // --- HEADER (RAW) --- - public final byte[] prevHash32; // 32 - public final int blockSize; // preimage size - public final int blockNumber; // >=0 + /** Минимальный полный размер блока (без bodyBytes). */ + public static final int MIN_FULL_BYTES = + PREIMAGE_HEADER_SIZE + SIG_MARKER_LEN + SIGNATURE_LEN; + + // --- HEADER (PREIMAGE) --- + public final int frameCode; // uint16 (v0=0) + public final byte[] prevHash32; // 32 + public final int blockSize; // preimage size (включая frameCode) + public final int blockNumber; // >=0 public final long timestamp; public final short type; public final short subType; public final short version; - // --- BODY (RAW) --- + // --- BODY (PREIMAGE) --- public final byte[] bodyBytes; /** Распарсенное тело (создаётся сразу при парсинге блока). */ public final BodyRecord body; // --- TAIL --- - private final byte[] signature64; // 64 + public final int sigMarker; // uint16 (v0: 0x0100) + private final byte[] signature64; // 64 // --- derived --- - private final byte[] hash32; // 32, computed - private final byte[] preimage; // blockSize bytes - private final byte[] fullBytes; // preimage + signature + private final byte[] hash32; // 32, computed + private final byte[] preimage; // blockSize bytes + private final byte[] fullBytes; // preimage + sigMarker + signature /* ===================================================================== */ /* ====================== Конструктор из байт ========================== */ @@ -88,8 +130,8 @@ public final class BchBlockEntry { public BchBlockEntry(byte[] fullBytes) { Objects.requireNonNull(fullBytes, "fullBytes == null"); - if (fullBytes.length < RAW_HEADER_SIZE + SIGNATURE_LEN) { - throw new IllegalArgumentException("Block too short"); + if (fullBytes.length < MIN_FULL_BYTES) { + throw new IllegalArgumentException("Block too short: " + fullBytes.length + " < " + MIN_FULL_BYTES); } if (fullBytes.length > MAX_BLOCK_FULL_BYTES) { throw new IllegalArgumentException("Block too large: " + fullBytes.length + " > " + MAX_BLOCK_FULL_BYTES); @@ -97,47 +139,77 @@ public final class BchBlockEntry { ByteBuffer bb = ByteBuffer.wrap(fullBytes).order(ByteOrder.BIG_ENDIAN); + // [2] frameCode + this.frameCode = Short.toUnsignedInt(bb.getShort()); + if (this.frameCode != FRAME_CODE_V0) { + throw new IllegalArgumentException(String.format( + "Bad frameCode: 0x%04X (expected 0x%04X)", this.frameCode, FRAME_CODE_V0 + )); + } + + // [32] prevHash32 this.prevHash32 = new byte[32]; bb.get(this.prevHash32); + // [4] blockSize 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); - } - if (blockSize + SIGNATURE_LEN > MAX_BLOCK_FULL_BYTES) { - throw new IllegalArgumentException("Block too large by blockSize: " + (blockSize + SIGNATURE_LEN) + " > " + MAX_BLOCK_FULL_BYTES); + if (blockSize < PREIMAGE_HEADER_SIZE) { + throw new IllegalArgumentException("blockSize too small: " + blockSize + " < " + PREIMAGE_HEADER_SIZE); } + // fullLen must match exactly: blockSize + sigMarker(2) + signature(64) + int expectedFullLen = blockSize + SIG_MARKER_LEN + SIGNATURE_LEN; + if (expectedFullLen != fullBytes.length) { + throw new IllegalArgumentException("blockSize mismatch: blockSize=" + blockSize + + " expectedFullLen=" + expectedFullLen + + " fullLen=" + fullBytes.length); + } + if (expectedFullLen > MAX_BLOCK_FULL_BYTES) { + throw new IllegalArgumentException("Block too large by blockSize: " + expectedFullLen + " > " + MAX_BLOCK_FULL_BYTES); + } + + // [4] blockNumber this.blockNumber = bb.getInt(); if (this.blockNumber < 0) { throw new IllegalArgumentException("blockNumber < 0: " + this.blockNumber); } + // [8] timestamp this.timestamp = bb.getLong(); // запрет “в будущее” больше чем на 1 минуту long now = Instant.now().getEpochSecond(); if (this.timestamp > now + MAX_FUTURE_SECONDS) { - throw new IllegalArgumentException("timestamp is too far in future: ts=" + this.timestamp + " now=" + now + " maxFutureSec=" + MAX_FUTURE_SECONDS); + throw new IllegalArgumentException("timestamp is too far in future: ts=" + this.timestamp + + " now=" + now + " maxFutureSec=" + MAX_FUTURE_SECONDS); } + // [2][2][2] type/subType/version 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); - + // [N] bodyBytes + int bodyLen = blockSize - PREIMAGE_HEADER_SIZE; + if (bodyLen < 0) { + throw new IllegalArgumentException("Invalid body length: " + bodyLen); + } this.bodyBytes = new byte[bodyLen]; bb.get(this.bodyBytes); + // TAIL: [2] sigMarker + this.sigMarker = Short.toUnsignedInt(bb.getShort()); + if (this.sigMarker != SIG_MARKER_ED25519) { + throw new IllegalArgumentException(String.format( + "Bad sigMarker: 0x%04X (expected 0x%04X)", this.sigMarker, SIG_MARKER_ED25519 + )); + } + + // TAIL: [64] signature64 this.signature64 = new byte[SIGNATURE_LEN]; bb.get(this.signature64); - // preimage = первые blockSize байт + // preimage = первые blockSize байт (включая frameCode) this.preimage = Arrays.copyOfRange(fullBytes, 0, blockSize); // hash32 = sha256(preimage) @@ -148,7 +220,6 @@ public final class BchBlockEntry { this.fullBytes = Arrays.copyOf(fullBytes, fullBytes.length); - // запрет мусора if (bb.remaining() != 0) { throw new IllegalArgumentException("Unexpected tail bytes, remaining=" + bb.remaining()); } @@ -181,9 +252,11 @@ public final class BchBlockEntry { // запрет “в будущее” больше чем на 1 минуту long now = Instant.now().getEpochSecond(); if (timestamp > now + MAX_FUTURE_SECONDS) { - throw new IllegalArgumentException("timestamp is too far in future: ts=" + timestamp + " now=" + now + " maxFutureSec=" + MAX_FUTURE_SECONDS); + throw new IllegalArgumentException("timestamp is too far in future: ts=" + timestamp + + " now=" + now + " maxFutureSec=" + MAX_FUTURE_SECONDS); } + this.frameCode = FRAME_CODE_V0; this.prevHash32 = Arrays.copyOf(prevHash32, 32); this.blockNumber = blockNumber; this.timestamp = timestamp; @@ -191,11 +264,11 @@ public final class BchBlockEntry { this.subType = subType; this.version = version; this.bodyBytes = Arrays.copyOf(bodyBytes, bodyBytes.length); - this.signature64 = Arrays.copyOf(signature64, SIGNATURE_LEN); - this.blockSize = RAW_HEADER_SIZE + this.bodyBytes.length; + // blockSize = размер preimage (включая frameCode) + this.blockSize = PREIMAGE_HEADER_SIZE + this.bodyBytes.length; - int fullLen = this.blockSize + SIGNATURE_LEN; + int fullLen = this.blockSize + SIG_MARKER_LEN + SIGNATURE_LEN; if (fullLen > MAX_BLOCK_FULL_BYTES) { throw new IllegalArgumentException("Block too large: " + fullLen + " > " + MAX_BLOCK_FULL_BYTES); } @@ -203,8 +276,13 @@ public final class BchBlockEntry { // parse body по header + ОБЯЗАТЕЛЬНЫЙ check() this.body = BodyRecordParser.parse(this.type, this.subType, this.version, this.bodyBytes); + // tail marker фиксирован + this.sigMarker = SIG_MARKER_ED25519; + this.signature64 = Arrays.copyOf(signature64, SIGNATURE_LEN); + // build preimage ByteBuffer pre = ByteBuffer.allocate(blockSize).order(ByteOrder.BIG_ENDIAN); + pre.putShort((short) (FRAME_CODE_V0 & 0xFFFF)); pre.put(this.prevHash32); pre.putInt(this.blockSize); pre.putInt(this.blockNumber); @@ -217,24 +295,33 @@ public final class BchBlockEntry { this.preimage = pre.array(); this.hash32 = BchCryptoVerifier.sha256(preimage); - ByteBuffer full = ByteBuffer.allocate(blockSize + SIGNATURE_LEN).order(ByteOrder.BIG_ENDIAN); + // build fullBytes: preimage + sigMarker + signature64 + ByteBuffer full = ByteBuffer.allocate(fullLen).order(ByteOrder.BIG_ENDIAN); full.put(this.preimage); + full.putShort((short) (SIG_MARKER_ED25519 & 0xFFFF)); full.put(this.signature64); this.fullBytes = full.array(); } + /* ===================================================================== */ + /* ============================ Getters ================================= */ + /* ===================================================================== */ + public byte[] getPreimageBytes() { return Arrays.copyOf(preimage, preimage.length); } + /** Возвращает подпись Ed25519 (64 байта). */ public byte[] getSignature64() { return Arrays.copyOf(signature64, SIGNATURE_LEN); } + /** Возвращает hash32 = SHA-256(preimage). */ public byte[] getHash32() { return Arrays.copyOf(hash32, HASH_LEN); } + /** Возвращает полный блок: preimage + sigMarker + signature. */ public byte[] toBytes() { return Arrays.copyOf(fullBytes, fullBytes.length); } @@ -249,7 +336,8 @@ public final class BchBlockEntry { } return "BchBlockEntry{" - + "HDR{" + + "FRAME{frameCode=0x" + hex4(frameCode) + + "}, HDR{" + "blockSize=" + blockSize + ", blockNumber=" + blockNumber + ", timestamp=" + timestamp + " (" + timeIso + ")" @@ -259,19 +347,25 @@ public final class BchBlockEntry { + ", prevHash32(hex)=" + toHex(prevHash32) + "}" + ", BODY{len=" + (bodyBytes == null ? -1 : bodyBytes.length) + "}" - + ", TAIL{signature64(hex)=" + toHex(signature64) + "}" + + ", TAIL{sigMarker=0x" + hex4(sigMarker) + ", signature64(hex)=" + toHex(signature64) + "}" + ", DERIVED{hash32(hex)=" + toHex(hash32) + "}" + "}"; } + private static String hex4(int v) { + String s = Integer.toHexString(v & 0xFFFF); + while (s.length() < 4) s = "0" + s; + return s; + } + private static String toHex(byte[] bytes) { if (bytes == null) return "null"; char[] HEX = "0123456789abcdef".toCharArray(); char[] out = new char[bytes.length * 2]; for (int i = 0; i < bytes.length; i++) { - int v = bytes[i] & 0xFF; - out[i * 2] = HEX[v >>> 4]; - out[i * 2 + 1] = HEX[v & 0x0F]; + int vv = bytes[i] & 0xFF; + out[i * 2] = HEX[vv >>> 4]; + out[i * 2 + 1] = HEX[vv & 0x0F]; } return new String(out); } diff --git a/shine-server-blockchain/src/main/java/blockchain/BchCryptoVerifier.java b/shine-server-blockchain/src/main/java/blockchain/BchCryptoVerifier.java index e6f5a3c..ca657e8 100644 --- a/shine-server-blockchain/src/main/java/blockchain/BchCryptoVerifier.java +++ b/shine-server-blockchain/src/main/java/blockchain/BchCryptoVerifier.java @@ -6,9 +6,11 @@ import java.security.MessageDigest; import java.util.Objects; /** - * Новый верификатор по ТЗ: + * Верификатор SHiNE (Frame v0): + * + * preimage = первые blockSize байт блока (ВКЛЮЧАЯ frameCode=0x0000), + * = всё до TAIL (sigMarker+signature). * - * preimage = все байты блока без signature64 * hash32 = SHA-256(preimage) * verify = Ed25519.verify(hash32, signature64, pubKey32) */ diff --git a/src/test/java/test/it/blockchain/AddBlockSender.java b/src/test/java/test/it/blockchain/AddBlockSender.java index 5d0935f..f70ffb0 100644 --- a/src/test/java/test/it/blockchain/AddBlockSender.java +++ b/src/test/java/test/it/blockchain/AddBlockSender.java @@ -15,8 +15,9 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; /** - * AddBlockSender — под новый формат BchBlockEntry: - * - block хранит только preimage + signature + * AddBlockSender — под новый формат BchBlockEntry (Frame v0): + * - blockBytes = preimage + sigMarker(2) + signature64 + * - preimage начинается с frameCode(2) = 0x0000 * - hash32 вычисляется как sha256(preimage) * - signature = Ed25519.sign(hash32) * @@ -74,7 +75,9 @@ public final class AddBlockSender { byte[] bodyBytes = body.toBytes(); + // ВАЖНО: preimage должен быть БАЙТ-В-БАЙТ таким же, как в BchBlockEntry byte[] preimage = buildPreimage(prevHash32, blockNumber, tsSec, type, subType, version, bodyBytes); + byte[] hash32 = blockchain.BchCryptoVerifier.sha256(preimage); byte[] signature64 = utils.crypto.Ed25519Util.sign(hash32, loginPrivKey); @@ -191,7 +194,7 @@ public final class AddBlockSender { throw new IllegalArgumentException("Unknown body class: " + body.getClass()); } - // ---------- preimage builder (строго по BchBlockEntry) ---------- + // ---------- preimage builder (строго по BchBlockEntry Frame v0) ---------- private static byte[] buildPreimage(byte[] prevHash32, int blockNumber, @@ -201,17 +204,36 @@ public final class AddBlockSender { short version, byte[] bodyBytes) { - int blockSize = BchBlockEntry.RAW_HEADER_SIZE + (bodyBytes == null ? 0 : bodyBytes.length); + if (prevHash32 == null || prevHash32.length != 32) { + throw new IllegalArgumentException("prevHash32 must be 32 bytes"); + } + + int bodyLen = (bodyBytes == null ? 0 : bodyBytes.length); + int blockSize = BchBlockEntry.PREIMAGE_HEADER_SIZE + bodyLen; java.nio.ByteBuffer bb = java.nio.ByteBuffer.allocate(blockSize).order(java.nio.ByteOrder.BIG_ENDIAN); + // [2] frameCode (v0) + bb.putShort((short) (BchBlockEntry.FRAME_CODE_V0 & 0xFFFF)); + + // [32] prevHash32 bb.put(prevHash32); + + // [4] blockSize (preimage size) bb.putInt(blockSize); + + // [4] blockNumber bb.putInt(blockNumber); + + // [8] timestamp bb.putLong(tsSec); + + // [2][2][2] type/subType/version bb.putShort(type); bb.putShort(subType); bb.putShort(version); + + // [N] bodyBytes if (bodyBytes != null) bb.put(bodyBytes); return bb.array();