SHiNE-server/shine-server-blockchain/src/main/java/blockchain/README.md
AidarKC 33635886e0 23 12 25
Дорабатываю добавление блоков! Вроде всё.  (но ещё не проверял и тестов нету)
2025-12-24 11:05:25 +03:00

6.3 KiB
Raw Blame History

формат добавления блоков и из чего реально состоит блок, плюс как именно считаются хэши и подписи.

  1. Формат добавления блоков в файл .bch

Файл — это просто конкатенация блоков один за другим, без разделителей:

BLOCK_0 | BLOCK_1 | BLOCK_2 | ...

Чтобы читать файл, ты идёшь по нему последовательно:

Читаешь первые 4 байта = recordSize

Понимаешь, сколько всего байт занимает блок:

fullBlockLen = recordSize + 64 + 32

Считываешь оставшиеся байты блока и парсишь:

RAW часть длиной recordSize

затем signature64

затем hash32

Это удобно тем, что файл можно дописывать (APPEND) хоть по сети, хоть локально: блоки самодостаточные.

  1. Из чего состоит блок (формат блока) 2.1. RAW (участвует в preimage и верификации; подпись/хэш к нему «пришиты»)

BigEndian, фиксированный заголовок + body:

RAW:

[4] recordSize (int) — размер RAW включая эти 4 байта, но без signature+hash

[4] recordNumber (int) — глобальный номер блока (цепочка)

[8] timestamp (long) — unix seconds

[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

  1. Формат 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

  1. Как считается хэш (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 должен лежать в конце блока.

  1. Как считается подпись (signature64)

Подписывается не rawBytes, а hash32:

signature64 = Ed25519.sign(hash32, privateKey)

Проверка:

Пересобираем preimage

Считаем hash32 = sha256(preimage)

Сверяем hash32 с тем, что лежит в блоке

Проверяем подпись:

Ed25519.verify(hash32, signature64, publicKey32)

Это хорошая схема: подпись всегда фиксированного размера, а хэш — единая точка правды.

  1. Что является «цепочкой» и чем блоки сцеплены

Сцепление идёт через prevGlobalHash32 и prevLineHash32, которые ты передаёшь в verifyAll(...).

То есть логика добавления нового блока (в терминах данных) такая:

берём последний блок

достаём его hash32

это и будет prevGlobalHash32 для нового блока

prevLineHash32 — по той линии, куда добавляем (у вас пока это то же самое, потому что одна линия/нет разветвления)

У тебя даже отдельно отмечено: пока нет отдельных линий, lastLineHash == lastGlobalHash — значит сейчас обе цепочки фактически совпадают.

  1. Мини-схема “как добавить новый блок” (пошагово)

Собрать 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

  1. Важные нюансы (чтобы не словить «тихий баг»)

recordSize включает себя (первые 4 байта) — это ок, просто помнить.

bodyLen = recordSize - RAW_HEADER_SIZE — значит RAW_HEADER_SIZE обязан быть всегда синхронен с реальным RAW.

login в preimage ограничен 255 байт (у тебя [1] loginLen) — это правильно и предсказуемо.

В HeaderBody tag фиксирован "SHiNE001" — фактически версия протокола body-headerа.