diff --git a/shine-server-blockchain/src/main/java/blockchain/README.md b/shine-server-blockchain/src/main/java/blockchain/README.md index 365a091..64725ca 100644 --- a/shine-server-blockchain/src/main/java/blockchain/README.md +++ b/shine-server-blockchain/src/main/java/blockchain/README.md @@ -1,75 +1,205 @@ -# 📦 Модуль `blockchain` +формат добавления блоков и из чего реально состоит блок, плюс как именно считаются хэши и подписи. -Модуль отвечает за хранение, чтение, проверку и создание бинарных блоков формата `.bch` — базовых элементов цепочек в системе SHiNE Blockchain. -Каждый блок содержит заголовок, тело (body), подпись Ed25519 и хэш SHA-256. +1) Формат добавления блоков в файл .bch ---- +Файл — это просто конкатенация блоков один за другим, без разделителей: -## 🔹 `BchBlockEntry` -Главный класс блока. +BLOCK_0 | BLOCK_1 | BLOCK_2 | ... -**Публичные методы:** -- `BchBlockEntry(int num, long time, short type, short ver, byte[] body)` — создаёт новый блок без подписи. -- `BchBlockEntry(byte[] full)` — распаковывает блок из байтов (`RAW + SIG + HASH`). -- `addSignatureAndHash(byte[] sig, byte[] hash)` — добавляет подпись и хэш. -- `getBodyAsText()` — возвращает тело как строку. -- `getSignature64()`, `getHash32()`, `getRawBytesWithSignatureAndHash()` — доступ к данным блока. -- `toString()` — краткое описание блока. ---- +Чтобы читать файл, ты идёшь по нему последовательно: -## 🔹 `BchBlockValidator` // сделан на будущее, для сетевых запросов не используется -Проверяет, можно ли добавить блок в цепочку. +Читаешь первые 4 байта = recordSize -**Публичный метод:** -- `validate(BchBlockEntry block, BchInfoEntry chain, long chainId)` — сверяет номер блока, подпись и хэш. +Понимаешь, сколько всего байт занимает блок: ---- +fullBlockLen = recordSize + 64 + 32 -## 🔹 `BodyRecordParser` -Определяет, какой тип тела (`HeaderBody`, `TextBody` и т.п.) нужно создать. -**Публичный метод:** -- `parse(short type, short version, byte[] body)` — возвращает объект, реализующий `BodyRecord`. +Считываешь оставшиеся байты блока и парсишь: ---- +RAW часть длиной recordSize -## 🔹 `BodyRecord` (интерфейс) -Общее поведение для всех тел блоков. +затем signature64 -**Методы:** -- `check()` — проверить корректность данных. -- `toBytes()` — сериализация обратно в байты (по умолчанию не реализована). +затем hash32 ---- +Это удобно тем, что файл можно дописывать (APPEND) хоть по сети, хоть локально: блоки самодостаточные. -## 🔹 `HeaderBody` -Тело первого блока цепочки (тип 0). -Содержит логин, ID цепочки и публичный ключ пользователя. +2) Из чего состоит блок (формат блока) + 2.1. RAW (участвует в preimage и верификации; подпись/хэш к нему «пришиты») -**Публичные методы:** -- `HeaderBody(byte[] body)` — парсинг из байтов. -- `HeaderBody(long id, String login, …)` — создание нового заголовка. -- `check()` — валидация логина и ключа. -- `toBytes()` — сериализация в байты. -- `toString()` — краткое описание полей. +BigEndian, фиксированный заголовок + body: ---- +RAW: -## 🔹 `TextBody` -Простое текстовое сообщение (тип 1). +[4] recordSize (int) — размер RAW включая эти 4 байта, но без signature+hash -**Публичные методы:** -- `TextBody(byte[] body)` — парсинг из байтов. -- `TextBody(String msg)` — создание нового текстового блока. -- `check()` — проверка, что текст не пуст. -- `toBytes()` — вернуть текст как UTF-8-массив. -- `toString()` — короткое текстовое описание. +[4] recordNumber (int) — глобальный номер блока (цепочка) ---- +[8] timestamp (long) — unix seconds -💡 Вся логика создания, подписи и проверки блоков построена вокруг этих классов. -`BchBlockEntry` — контейнер блока, -`HeaderBody` / `TextBody` — содержимое, -`BodyRecordParser` — выбор нужного типа, -`BchBlockValidator` — контроль целостности. +[2] line (short) — линия (у вас пока по сути одна) + +[4] lineNumber (int) — номер в линии (у вас пока обычно равен global) + +[N] bodyBytes — тело, начинается с [2] type + [2] version, дальше payload + +RAW_HEADER_SIZE = 4 + 4 + 8 + 2 + 4 = 22 байта + +2.2. TAIL (не входит в recordSize) + +[64] signature64 — Ed25519 подпись + +[32] hash32 — SHA-256 хэш + +Итого полный блок: + +FULL = RAW(recordSize bytes) + signature64 + hash32 + +3) Формат body (тела блока) + +У тебя правило железное: каждый body начинается одинаково: + +[2] type + +[2] version + +дальше payload по типу/версии + +Парсер делает ключ: + +key = (type<<16) | version + +3.1 HeaderBody (type=0, ver=1) + +Полный bodyBytes: + +[2] type=0 + +[2] ver=1 + +[8] tag "SHiNE001" (ASCII) + +[1] loginLength (uint8) + +[N] login UTF-8 + +Это «генезис-смысл» для идентичности: в теле блока явно хранится логин. + +3.2 TextBody (type=1, ver=1) + +[2] type=1 + +[2] ver=1 + +[N] message UTF-8 + +4) Как считается хэш (hash32) + +Важно: hash32 в блоке — это не hash(rawBytes). + +Ты считаешь: + +4.1 Preimage +preimage = +"SHiNE" + +[1] loginLen + loginBytes(UTF-8) + +prevGlobalHash32 + +prevLineHash32 + +rawBytes + + +Где: + +"SHiNE" — доменная метка (защита от «подпиши этим же ключом что-то другое») + +loginLen + loginBytes — привязка блока к пользователю/цепочке + +prevGlobalHash32 — сцепление по глобальной цепи + +prevLineHash32 — сцепление по линии + +rawBytes — содержимое текущего блока без подписи/хэша + +4.2 Сам хэш +hash32 = SHA-256(preimage) + + +И именно этот hash32 должен лежать в конце блока. + +5) Как считается подпись (signature64) + +Подписывается не rawBytes, а hash32: + +signature64 = Ed25519.sign(hash32, privateKey) + + +Проверка: + +Пересобираем preimage + +Считаем hash32 = sha256(preimage) + +Сверяем hash32 с тем, что лежит в блоке + +Проверяем подпись: + +Ed25519.verify(hash32, signature64, publicKey32) + + +Это хорошая схема: подпись всегда фиксированного размера, а хэш — единая точка правды. + +6) Что является «цепочкой» и чем блоки сцеплены + +Сцепление идёт через prevGlobalHash32 и prevLineHash32, которые ты передаёшь в verifyAll(...). + +То есть логика добавления нового блока (в терминах данных) такая: + +берём последний блок + +достаём его hash32 + +это и будет prevGlobalHash32 для нового блока + +prevLineHash32 — по той линии, куда добавляем (у вас пока это то же самое, потому что одна линия/нет разветвления) + +У тебя даже отдельно отмечено: пока нет отдельных линий, lastLineHash == lastGlobalHash — значит сейчас обе цепочки фактически совпадают. + +7) Мини-схема “как добавить новый блок” (пошагово) + +Собрать BodyRecord → получить bodyBytes = body.toBytes() + +Собрать RAW: + +recordNumber = lastRecordNumber + 1 + +timestamp = nowSeconds + +line (скорее всего 0 или 1 — как вы договорились) + +lineNumber = lastLineNumber + 1 (сейчас равно global) + +recordSize = RAW_HEADER_SIZE + bodyBytes.length + +rawBytes = new BchBlockEntry(...).getRawBytes() (или собрать вручную) + +preimage = buildPreimage(login, prevGlobalHash32, prevLineHash32, rawBytes) + +hash32 = sha256(preimage) + +signature64 = Ed25519.sign(hash32, privateKey) + +Собрать финальный BchBlockEntry(..., signature64, hash32) + +Дописать toBytes() в файл .bch + +8) Важные нюансы (чтобы не словить «тихий баг») + +recordSize включает себя (первые 4 байта) — это ок, просто помнить. + +bodyLen = recordSize - RAW_HEADER_SIZE — значит RAW_HEADER_SIZE обязан быть всегда синхронен с реальным RAW. + +login в preimage ограничен 255 байт (у тебя [1] loginLen) — это правильно и предсказуемо. + +В HeaderBody tag фиксирован "SHiNE001" — фактически версия протокола body-header’а. \ No newline at end of file