6.3 KiB
формат добавления блоков и из чего реально состоит блок, плюс как именно считаются хэши и подписи.
- Формат добавления блоков в файл .bch
Файл — это просто конкатенация блоков один за другим, без разделителей:
BLOCK_0 | BLOCK_1 | BLOCK_2 | ...
Чтобы читать файл, ты идёшь по нему последовательно:
Читаешь первые 4 байта = recordSize
Понимаешь, сколько всего байт занимает блок:
fullBlockLen = recordSize + 64 + 32
Считываешь оставшиеся байты блока и парсишь:
RAW часть длиной recordSize
затем signature64
затем hash32
Это удобно тем, что файл можно дописывать (APPEND) хоть по сети, хоть локально: блоки самодостаточные.
- Из чего состоит блок (формат блока) 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
- Формат 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
- Как считается хэш (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 должен лежать в конце блока.
- Как считается подпись (signature64)
Подписывается не rawBytes, а hash32:
signature64 = Ed25519.sign(hash32, privateKey)
Проверка:
Пересобираем preimage
Считаем hash32 = sha256(preimage)
Сверяем hash32 с тем, что лежит в блоке
Проверяем подпись:
Ed25519.verify(hash32, signature64, publicKey32)
Это хорошая схема: подпись всегда фиксированного размера, а хэш — единая точка правды.
- Что является «цепочкой» и чем блоки сцеплены
Сцепление идёт через prevGlobalHash32 и prevLineHash32, которые ты передаёшь в verifyAll(...).
То есть логика добавления нового блока (в терминах данных) такая:
берём последний блок
достаём его hash32
это и будет prevGlobalHash32 для нового блока
prevLineHash32 — по той линии, куда добавляем (у вас пока это то же самое, потому что одна линия/нет разветвления)
У тебя даже отдельно отмечено: пока нет отдельных линий, lastLineHash == lastGlobalHash — значит сейчас обе цепочки фактически совпадают.
- Мини-схема “как добавить новый блок” (пошагово)
Собрать 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
- Важные нюансы (чтобы не словить «тихий баг»)
recordSize включает себя (первые 4 байта) — это ок, просто помнить.
bodyLen = recordSize - RAW_HEADER_SIZE — значит RAW_HEADER_SIZE обязан быть всегда синхронен с реальным RAW.
login в preimage ограничен 255 байт (у тебя [1] loginLen) — это правильно и предсказуемо.
В HeaderBody tag фиксирован "SHiNE001" — фактически версия протокола body-header’а.