Промежуточная версия и ТУДУ на чём остановился
This commit is contained in:
AidarKC
2025-12-16 17:56:36 +03:00
parent 19c4fd6cd1
commit ab44cc5282
30 changed files with 2511 additions and 3 deletions
@@ -0,0 +1,217 @@
package blockchain;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Objects;
/**
* ============================================================================
* BchBlockEntry — универсальная запись блокчейна SHiNE (.bch)
* ============================================================================
*.
* 🧩 Формат файла .bch:
* Каждый блок хранится последовательно, без промежутков.
* Один блок = «заголовок» (RAW) + подпись (64) + хэш (32).
*.
* FULL = RAW + signature(64) + hash(32)
*.
* ---------------------------------------------------------------------------
* 🔹 Структура RAW-части блока (без подписи и хэша)
* ---------------------------------------------------------------------------
* Размеры и порядок строго фиксированы (BigEndian).
*.
* Порядок байтов (сверху вниз, смещения от начала RAW):
*.
* ┌────────────────────────────┬────────┬───────────────────────────────┐
* │ Поле │ Размер │ Описание │
* ├────────────────────────────┼────────┼───────────────────────────────┤
* │ recordSize │ 4 байта│ = M + 20 — общий размер RAW │
* │ recordNumber │ 4 байта│ порядковый номер блока │
* │ timestamp │ 8 байт │ UNIX time (секунды) │
* │ recordType │ 2 байта│ тип тела (0=Header, 1=Text) │
* │ recordTypeVersion │ 2 байта│ версия структуры данного типа │
* │ body │ M байт │ бинарное тело записи │
* └────────────────────────────┴────────┴───────────────────────────────┘
*.
* ⇒ RAW_HEADER_SIZE = 4 + 4 + 8 + 2 + 2 = 20 байт.
* ⇒ recordSize = RAW_HEADER_SIZE + body.length
*.
* ---------------------------------------------------------------------------
* 🔹 Структура FULL-блока
* ---------------------------------------------------------------------------
*.
* ┌────────────────────────────┬─────────┬──────────────────────────────┐
* │ RAW │ M+20 │ тело блока без подписи │
* │ signature64 │ 64 │ подпись Ed25519(preimage) │
* │ hash32 │ 32 │ SHA-256(preimage) │
* └────────────────────────────┴─────────┴──────────────────────────────┘
*.
* ⇒ Общая длина FULL = recordSize + 96 байт.
*.
* ---------------------------------------------------------------------------
* 🔹 Канонический preimage для подписи/хэша
* ---------------------------------------------------------------------------
* preimage = userLogin(UTF-8, без длины) +
* blockchainId(8B, BE) +
* prevHash32(32B) +
* rawBytes (M+20B)
*.
* hash32 = SHA-256(preimage)
* signature64= Ed25519.sign(preimage, privateKey)
*.
* Проверка осуществляется через {@link utils.crypto.BchCryptoVerifier}.
*.
* ============================================================================
*/
public class BchBlockEntry {
// ---- Константы типов ----
public static final short TYPE_HEADER = 0;
public static final short TYPE_TEXT = 1;
// ---- Константы размеров ----
public static final int SIGNATURE_LEN = 64;
public static final int HASH_LEN = 32;
/** Размер «сырой» шапки без подписи/хэша. */
public static final int RAW_HEADER_SIZE = 20;
// ---- Поля RAW-заголовка ----
public final int recordSize; // [4] M + 20
public final int recordNumber; // [4] порядковый номер блока
public final long timestamp; // [8] UNIX time (секунды)
public final short recordType; // [2] тип тела (0=Header, 1=Text)
public final short recordTypeVersion; // [2] версия структуры данного типа
public final byte[] body; // [M] тело записи
// ---- Поля подписи и хэша ----
private byte[] signature64; // [64] подпись (Ed25519)
private byte[] hash32; // [32] хэш (SHA-256)
// ---- Кэшированные представления ----
public final byte[] rawBytes; // RAW без подписи/хэша
private byte[] rawBytesWithSignatureAndHash; // FULL (может быть null)
// ========================================================================
// КОНСТРУКТОР №1 — из полей (RAW only)
// ========================================================================
public BchBlockEntry(int recordNumber,
long timestamp,
short recordType,
short recordTypeVersion,
byte[] body) {
Objects.requireNonNull(body, "body == null");
this.recordNumber = recordNumber;
this.timestamp = timestamp;
this.recordType = recordType;
this.recordTypeVersion = recordTypeVersion;
this.body = Arrays.copyOf(body, body.length);
this.recordSize = body.length + RAW_HEADER_SIZE;
ByteBuffer buf = ByteBuffer
.allocate(RAW_HEADER_SIZE + body.length)
.order(ByteOrder.BIG_ENDIAN);
buf.putInt(recordSize);
buf.putInt(recordNumber);
buf.putLong(timestamp);
buf.putShort(recordType);
buf.putShort(recordTypeVersion);
buf.put(body);
this.rawBytes = buf.array();
}
// ========================================================================
// КОНСТРУКТОР №2 — из полного массива (RAW + SIG + HASH)
// ========================================================================
public BchBlockEntry(byte[] rawWithSigAndHash) {
Objects.requireNonNull(rawWithSigAndHash, "rawWithSigAndHash == null");
if (rawWithSigAndHash.length < RAW_HEADER_SIZE + SIGNATURE_LEN + HASH_LEN)
throw new IllegalArgumentException("Слишком мало данных для полного блока");
ByteBuffer probe = ByteBuffer.wrap(rawWithSigAndHash).order(ByteOrder.BIG_ENDIAN);
int rs = probe.getInt(); // recordSize
if (rs < RAW_HEADER_SIZE)
throw new IllegalArgumentException("Некорректный recordSize: " + rs);
if (rawWithSigAndHash.length < rs + SIGNATURE_LEN + HASH_LEN)
throw new IllegalArgumentException("Данные короче, чем raw+sig+hash");
this.rawBytes = Arrays.copyOfRange(rawWithSigAndHash, 0, rs);
ByteBuffer buf = ByteBuffer.wrap(this.rawBytes).order(ByteOrder.BIG_ENDIAN);
this.recordSize = buf.getInt();
this.recordNumber = buf.getInt();
this.timestamp = buf.getLong();
this.recordType = buf.getShort();
this.recordTypeVersion = buf.getShort();
int bodyLen = this.recordSize - RAW_HEADER_SIZE;
if (bodyLen < 0 || bodyLen != this.rawBytes.length - RAW_HEADER_SIZE)
throw new IllegalArgumentException("Неконсистентная длина тела блока");
this.body = new byte[bodyLen];
buf.get(this.body);
// подпись + хэш
ByteBuffer tail = ByteBuffer
.wrap(rawWithSigAndHash, rs, SIGNATURE_LEN + HASH_LEN)
.order(ByteOrder.BIG_ENDIAN);
this.signature64 = new byte[SIGNATURE_LEN];
tail.get(this.signature64);
this.hash32 = new byte[HASH_LEN];
tail.get(this.hash32);
this.rawBytesWithSignatureAndHash =
Arrays.copyOf(rawWithSigAndHash, rs + SIGNATURE_LEN + HASH_LEN);
}
// ========================================================================
// Добавить подпись и хэш
// ========================================================================
public BchBlockEntry addSignatureAndHash(byte[] signature64, byte[] hash32) {
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");
this.signature64 = Arrays.copyOf(signature64, SIGNATURE_LEN);
this.hash32 = Arrays.copyOf(hash32, HASH_LEN);
byte[] full = new byte[this.rawBytes.length + SIGNATURE_LEN + HASH_LEN];
System.arraycopy(this.rawBytes, 0, full, 0, this.rawBytes.length);
System.arraycopy(this.signature64, 0, full, this.rawBytes.length, SIGNATURE_LEN);
System.arraycopy(this.hash32, 0, full, this.rawBytes.length + SIGNATURE_LEN, HASH_LEN);
this.rawBytesWithSignatureAndHash = full;
return this;
}
// ========================================================================
// Геттеры
// ========================================================================
public String getBodyAsText() {
return new String(body, StandardCharsets.UTF_8);
}
public byte[] getSignature64() { return signature64 == null ? null : Arrays.copyOf(signature64, SIGNATURE_LEN); }
public byte[] getHash32() { return hash32 == null ? null : Arrays.copyOf(hash32, HASH_LEN); }
public byte[] getRawBytesWithSignatureAndHash() {
return rawBytesWithSignatureAndHash == null ? null : Arrays.copyOf(rawBytesWithSignatureAndHash, rawBytesWithSignatureAndHash.length);
}
// ========================================================================
// Отладка
// ========================================================================
@Override
public String toString() {
return String.format(
"BchBlock[num=%d, type=%d, ver=%d, time=%d, raw=%d, full=%s]",
recordNumber, recordType, recordTypeVersion, timestamp,
rawBytes.length,
rawBytesWithSignatureAndHash == null ? "null" : String.valueOf(rawBytesWithSignatureAndHash.length)
);
}
}
@@ -0,0 +1,103 @@
package blockchain;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import utils.blockchain.BchInfoEntry;
import utils.crypto.BchCryptoVerifier;
/**
* BchBlockValidator — проверяет корректность блока:
* 1) последовательность номеров блоков в цепочке;
* 2) криптографическую целостность (подпись и хэш).
*.
* Не проверяет:
* - структуру и содержимое body;
* - поля HEADER и логин (этим занимаются другие проверки).
*/
public final class BchBlockValidator {
private static final Logger log = LoggerFactory.getLogger(BchBlockValidator.class);
private BchBlockValidator() {}
/**
* Проверяет, что блок может быть корректно добавлен к цепочке.
*
* Не используется при получении запроса на добавление блока по сети (тк там возвращаются более протоколо осмысленные коды
* если блок не подходит по номеру.
*
* А этот класс может быть использован в будущем для внутренних, повторных проверок существующих цепочек блоков.
*
* @param block блок (распарсенный из байт)
* @param chain информация о цепочке (BchInfoEntry)
* @param chainId идентификатор цепочки
* @return true если порядок и криптография корректны, иначе false
*/
public static boolean validate(BchBlockEntry block, BchInfoEntry chain, long chainId) {
if (block == null || chain == null) {
log.warn("❌ Ошибка: блок или данные о цепочке не переданы");
return false;
}
// 1️⃣ Проверка последовательности номера
int expectedNumber = chain.lastBlockNumber + 1;
if (block.recordNumber < expectedNumber) {
log.warn("❌ Блок с номером {} уже существует (ожидался {})", block.recordNumber, expectedNumber);
return false;
}
if (block.recordNumber > expectedNumber) {
log.warn("❌ Нарушена последовательность: получен блок {}, ожидался {}", block.recordNumber, expectedNumber);
return false;
}
// 2️⃣ Проверка публичного ключа
byte[] publicKey = chain.getPublicKey32();
if (publicKey == null || publicKey.length != 32) {
log.warn("❌ В цепочке отсутствует корректный публичный ключ (chainId={})", chainId);
return false;
}
// 3️⃣ Получаем предыдущий хэш
byte[] prevHash32 = hexToBytes(chain.lastBlockHash);
// 4️⃣ Проверка подписи и хэша
try {
boolean ok = BchCryptoVerifier.verifyAll(
chain.userLogin,
chainId,
prevHash32,
block.rawBytes,
block.getSignature64(),
block.getHash32(),
publicKey
);
if (!ok) {
log.warn("❌ Криптографическая проверка не пройдена: хэш или подпись не совпадают (chainId={}, blockNum={})",
chainId, block.recordNumber);
return false;
}
log.info("✅ Блок {} успешно прошёл проверку подписи и хэша (chainId={})",
block.recordNumber, chainId);
return true;
} catch (Exception e) {
log.error("❌ Исключение при проверке блока (chainId={}): {}", chainId, e.getMessage());
return false;
}
}
// -------------------- HEX → байты --------------------
private static byte[] hexToBytes(String hex) {
if (hex == null || hex.isEmpty()) return new byte[32]; // пустой хэш = 32 нуля
int len = hex.length();
byte[] out = new byte[len / 2];
for (int i = 0; i < len; i += 2) {
out[i / 2] = (byte) Integer.parseInt(hex.substring(i, i + 2), 16);
}
return out;
}
}
@@ -0,0 +1,106 @@
package blockchain;
import blockchain.body.BodyRecord;
import blockchain.body.HeaderBody;
import blockchain.body.TextBody;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* ============================================================================
* BodyRecordParser — универсальный парсер тел (body) блоков .bch
* ============================================================================
*.
* 🧩 Назначение:
* Преобразует пару (recordType, recordTypeVersion, bodyBytes)
* в конкретный объект, реализующий интерфейс {@link BodyRecord}.
*.
* 🔹 Особенность:
* Используется объединённый 4-байтовый код:
*.
* fullCode = (recordType << 16) | (recordTypeVersion & 0xFFFF)
*.
* Это позволяет различать версии одного типа блока,
* например: TextBody v1, TextBody v2 и т.д.
*.
* 🔹 Пример:
* BodyRecord body = BodyRecordParser.parse(block.recordType, block.recordTypeVersion, block.body);
*.
* ============================================================================
*/
public final class BodyRecordParser {
private static final Logger log = LoggerFactory.getLogger(BodyRecordParser.class);
private BodyRecordParser() {}
/**
* Распарсить тело блока по типу и версии записи.
*
* @param recordType код типа записи (0 = Header, 1 = Text, ...)
* @param recordTypeVersion версия формата записи
* @param body массив байт тела записи
* @return объект, реализующий BodyRecord
*/
public static BodyRecord parse(short recordType, short recordTypeVersion, byte[] body) {
if (body == null)
throw new IllegalArgumentException("body == null");
// Объединяем тип и версию в 4-байтовый ключ
int fullCode = ((recordType & 0xFFFF) << 16) | (recordTypeVersion & 0xFFFF);
switch (fullCode) {
// ---------------------------------------------------------
// TYPE 0, VERSION 1 — HeaderBody v1
// ---------------------------------------------------------
// Заголовок цепочки пользователя (первый блок).
//
// Формат body (без общих 20 байт заголовка блока):
// [8] ASCII tag = "SHiNE001"
// [8] blockchainId (long, BE)
// [4] blockchainType (int, BE)
// [4] blockchainNumber (int, BE)
// [1] userLoginLength = N (unsigned byte)
// [N] userLogin (UTF-8)
// [2] versionUserBch (short, BE)
// [8] prevUserBchId (long, BE)
// [32] publicKey32
//
// Назначение:
// Создаёт новую пользовательскую цепочку (блок №0).
case (0x0000_0001):
return new HeaderBody(body);
// ---------------------------------------------------------
// TYPE 1, VERSION 1 — TextBody v1
// ---------------------------------------------------------
// Простое текстовое сообщение UTF-8.
//
// Формат body (без общих 20 байт заголовка блока):
// [N] message (UTF-8)
//
// Назначение:
// Текстовые и системные сообщения, описания, комментарии.
case (0x0001_0001):
return new TextBody(body);
// ---------------------------------------------------------
// РЕЗЕРВ — будущие типы и версии
// ---------------------------------------------------------
// Пример: (0x0001_0002) → TextBody v2 (например, JSON-структура)
// (0x0002_0001) → FileBody v1
//
// case (0x0001_0002):
// return new TextBodyV2(body);
//
// case (0x0002_0001):
// return new FileBody(body);
default:
throw new IllegalArgumentException(String.format(
"Неизвестный тип блока: type=%d version=%d (fullCode=0x%08X)",
recordType, recordTypeVersion, fullCode));
}
}
}
@@ -0,0 +1,75 @@
# 📦 Модуль `blockchain`
Модуль отвечает за хранение, чтение, проверку и создание бинарных блоков формата `.bch` — базовых элементов цепочек в системе SHiNE Blockchain.
Каждый блок содержит заголовок, тело (body), подпись Ed25519 и хэш SHA-256.
---
## 🔹 `BchBlockEntry`
Главный класс блока.
**Публичные методы:**
- `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` // сделан на будущее, для сетевых запросов не используется
Проверяет, можно ли добавить блок в цепочку.
**Публичный метод:**
- `validate(BchBlockEntry block, BchInfoEntry chain, long chainId)` — сверяет номер блока, подпись и хэш.
---
## 🔹 `BodyRecordParser`
Определяет, какой тип тела (`HeaderBody`, `TextBody` и т.п.) нужно создать.
**Публичный метод:**
- `parse(short type, short version, byte[] body)` — возвращает объект, реализующий `BodyRecord`.
---
## 🔹 `BodyRecord` (интерфейс)
Общее поведение для всех тел блоков.
**Методы:**
- `check()` — проверить корректность данных.
- `toBytes()` — сериализация обратно в байты (по умолчанию не реализована).
---
## 🔹 `HeaderBody`
Тело первого блока цепочки (тип 0).
Содержит логин, ID цепочки и публичный ключ пользователя.
**Публичные методы:**
- `HeaderBody(byte[] body)` — парсинг из байтов.
- `HeaderBody(long id, String login, …)` — создание нового заголовка.
- `check()` — валидация логина и ключа.
- `toBytes()` — сериализация в байты.
- `toString()` — краткое описание полей.
---
## 🔹 `TextBody`
Простое текстовое сообщение (тип 1).
**Публичные методы:**
- `TextBody(byte[] body)` — парсинг из байтов.
- `TextBody(String msg)` — создание нового текстового блока.
- `check()` — проверка, что текст не пуст.
- `toBytes()` — вернуть текст как UTF-8-массив.
- `toString()` — короткое текстовое описание.
---
💡 Вся логика создания, подписи и проверки блоков построена вокруг этих классов.
`BchBlockEntry` — контейнер блока,
`HeaderBody` / `TextBody` — содержимое,
`BodyRecordParser` — выбор нужного типа,
`BchBlockValidator` — контроль целостности.
@@ -0,0 +1,19 @@
package blockchain.body;
/**
* Общий интерфейс для всех тел (body) блоков.
*.
* Каждый тип тела реализует:
* - check() — проверку корректности данных
* - toBytes() — опциональную сериализацию обратно в байты
*/
public interface BodyRecord {
/** Проверить корректность содержимого. */
BodyRecord check();
/** (опционально) Сериализация тела обратно в байты. */
default byte[] toBytes() {
throw new UnsupportedOperationException("toBytes() не реализован");
}
}
@@ -0,0 +1,36 @@
package blockchain.body;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
/**
* BodyRecordParser_new — общий фабричный парсер body для нового формата.
*
* Правило совместимости (строгое):
* - если (type, version) неизвестны → кидаем IllegalArgumentException
*/
public final class BodyRecordParser_new {
private BodyRecordParser_new() {}
public static BodyRecord_new parse(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();
// Строгое сопоставление type+version → класс
int key = ((type & 0xFFFF) << 16) | (ver & 0xFFFF);
return switch (key) {
case 0x0000_0001 -> new HeaderBody_new(bodyBytes); // type=0, ver=1
case 0x0001_0001 -> new TextBody_new(bodyBytes); // type=1, ver=1
default -> throw new IllegalArgumentException(String.format(
"Unknown body type/version: type=%d ver=%d (key=0x%08X)",
(type & 0xFFFF), (ver & 0xFFFF), key
));
};
}
}
@@ -0,0 +1,38 @@
package blockchain.body;
/**
* BodyRecord_new — общий контракт для всех типов body (тела блока).
*
* Идея:
* - На каждый тип body (Header, Text, File, ...) — отдельный класс.
* - Десериализация из байтов делается КОНСТРУКТОРОМ:
* new XxxBody_new(byte[] bodyBytes)
* (конструктор обязан распарсить байты или кинуть IllegalArgumentException).
*
* - Валидация делается методом check().
* check() должен:
* - вернуть this, если всё корректно
* - кинуть IllegalArgumentException, если данные некорректны
*
* - Сериализация обратно в байты делается методом toBytes().
*
* - type() и version() — это идентификаторы формата body.
* Они должны быть константами для класса (например TYPE=1, VERSION=1).
*/
public interface BodyRecord_new {
/** Код типа записи (совпадает с recordType в BchBlockEntry). */
short type();
/** Версия формата записи (совпадает с recordTypeVersion в BchBlockEntry). */
short version();
/** Проверить корректность содержимого и вернуть этот объект (или кинуть исключение). */
BodyRecord_new check();
/**
* Сериализовать тело записи в байты (ровно то, что кладётся в block.body).
* Важно: НЕ включает общий заголовок блока (recordNumber/timestamp/type/version).
*/
byte[] toBytes();
}
@@ -0,0 +1,191 @@
package blockchain.body;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Objects;
/**
* ============================================================================
* HeaderBody — тело записи типа 0 (заглавие блокчейна)
* ============================================================================
*.
* 🧩 Назначение:
* Первый блок каждой пользовательской цепочки (.bch) — это "заголовок".
* Он хранит базовую информацию о владельце, версии и публичном ключе.
*.
* Этот блок всегда имеет:
* • recordType = 0
* • recordNumber = 0
* • recordTypeVersion = 1
*.
* ----------------------------------------------------------------------------
* 🔹 Формат body (без общих 20 байт заголовка блока BchBlock)
*.
* | Смещение | Размер | Поле | Формат | Описание |
* |-----------|--------|--------------------|---------|-----------|
* | 0x00 | 8 | tag | ASCII | Статическая сигнатура "SHiNE001" |
* | 0x08 | 8 | blockchainId | long BE | Уникальный идентификатор цепочки |
* | 0x10 | 1 | userLoginLength=N | uint8 | Длина логина пользователя |
* | 0x11 | N | userLogin | UTF-8 | Логин пользователя |
* | 0x11+N | 4 | blockchainType | int BE | Зарезервировано (всегда 0) |
* | 0x15+N | 4 | blockchainNumber | int BE | Зарезервировано (всегда 0) |
* | 0x19+N | 2 | versionUserBch | short BE| Версия формата (всегда 1) |
* | 0x1B+N | 8 | prevUserBchId | long BE | Зарезервировано (всегда 0) |
* | 0x23+N | 32 | publicKey32 | raw | Публичный ключ (Ed25519, 32 байта) |
*.
* ----------------------------------------------------------------------------
* 💡 Пример структуры в байтах:
*.
* 0000: 53 48 69 4E 45 30 30 31 "SHiNE001"
* 0008: 00 00 00 00 01 23 45 67 blockchainId
* 0010: 05 userLoginLength = 5
* 0011: 41 69 64 61 72 userLogin = "Aidar"
* 0016: 00 00 00 00 blockchainType = 0
* 001A: 00 00 00 00 blockchainNumber = 0
* 001E: 00 01 versionUserBch = 1
* 0020: 00 00 00 00 00 00 00 00 prevUserBchId = 0
* 0028: [32 байта публичного ключа]
*.
* ----------------------------------------------------------------------------
* 📘 Замечания:
* • Поля blockchainType, blockchainNumber, versionUserBch, prevUserBchId
* зарезервированы для будущего расширения формата.
* • На данный момент все они принимают фиксированные значения:
* blockchainType = 0
* blockchainNumber = 0
* versionUserBch = 1
* prevUserBchId = 0
*.
* ============================================================================
*/
public final class HeaderBody implements BodyRecord {
public static final short TYPE = 0;
public static final String TAG = "SHiNE001";
public static final int PUBKEY_LEN = 32;
public final String tag; // всегда "SHiNE001"
public final long blockchainId;
public final String userLogin; // UTF-8
public final int blockchainType; // пока 0
public final int blockchainNumber; // пока 0
public final short versionUserBch; // пока 1
public final long prevUserBchId; // пока 0
public final byte[] publicKey32; // 32 байта
// ------------------------------------------------------------
// Конструктор №1 — из массива байт (для парсинга существующего блока)
// ------------------------------------------------------------
public HeaderBody(byte[] body) {
Objects.requireNonNull(body, "body == null");
if (body.length < 8 + 8 + 1 + 2 + 4 + 4 + 8 + 32)
throw new IllegalArgumentException("HeaderBody слишком короткое");
ByteBuffer buf = ByteBuffer.wrap(body).order(ByteOrder.BIG_ENDIAN);
// [8] тег
byte[] tagBytes = new byte[8];
buf.get(tagBytes);
String tag = new String(tagBytes, StandardCharsets.US_ASCII);
if (!TAG.equals(tag))
throw new IllegalArgumentException("Неверный тег: " + tag);
this.tag = tag;
// [8] blockchainId
this.blockchainId = buf.getLong();
// [1] длина логина
int loginLen = Byte.toUnsignedInt(buf.get());
if (loginLen == 0 || buf.remaining() < loginLen + 4 + 4 + 2 + 8 + 32)
throw new IllegalArgumentException("Некорректная длина логина");
// [N] логин
byte[] loginBytes = new byte[loginLen];
buf.get(loginBytes);
this.userLogin = new String(loginBytes, StandardCharsets.UTF_8);
// Остальные поля
this.blockchainType = buf.getInt();
this.blockchainNumber = buf.getInt();
this.versionUserBch = buf.getShort();
this.prevUserBchId = buf.getLong();
this.publicKey32 = new byte[PUBKEY_LEN];
buf.get(this.publicKey32);
}
// ------------------------------------------------------------
// Конструктор №2 — из параметров (для создания нового заголовка)
// ------------------------------------------------------------
public HeaderBody(long blockchainId, String userLogin,
int blockchainType, int blockchainNumber,
short versionUserBch, long prevUserBchId,
byte[] publicKey32) {
Objects.requireNonNull(userLogin, "userLogin == null");
Objects.requireNonNull(publicKey32, "publicKey32 == null");
if (publicKey32.length != PUBKEY_LEN)
throw new IllegalArgumentException("Публичный ключ должен состоять из 32 байт");
this.tag = TAG;
this.blockchainId = blockchainId;
this.userLogin = userLogin;
this.blockchainType = blockchainType;
this.blockchainNumber = blockchainNumber;
this.versionUserBch = versionUserBch;
this.prevUserBchId = prevUserBchId;
this.publicKey32 = Arrays.copyOf(publicKey32, PUBKEY_LEN);
}
// ------------------------------------------------------------
// Проверка и сериализация
// ------------------------------------------------------------
@Override
public HeaderBody check() {
if (userLogin == null || userLogin.isBlank())
throw new IllegalArgumentException("Логин не может быть пустым");
if (!userLogin.matches("^[A-Za-z0-9_]+$"))
throw new IllegalArgumentException("Логин может содержать только латиницу, цифры и _");
if (publicKey32 == null || publicKey32.length != PUBKEY_LEN)
throw new IllegalArgumentException("Публичный ключ должен быть 32 байта");
return this;
}
@Override
public byte[] toBytes() {
byte[] loginUtf8 = userLogin.getBytes(StandardCharsets.UTF_8);
if (loginUtf8.length > 255)
throw new IllegalArgumentException("Логин слишком длинный (>255 байт)");
int cap = 8 + 8 + 1 + loginUtf8.length + 4 + 4 + 2 + 8 + 32;
ByteBuffer buf = ByteBuffer.allocate(cap).order(ByteOrder.BIG_ENDIAN);
buf.put(TAG.getBytes(StandardCharsets.US_ASCII)); // [8]
buf.putLong(blockchainId); // [8]
buf.put((byte) loginUtf8.length); // [1]
buf.put(loginUtf8); // [N]
buf.putInt(blockchainType); // [4]
buf.putInt(blockchainNumber); // [4]
buf.putShort(versionUserBch); // [2]
buf.putLong(prevUserBchId); // [8]
buf.put(publicKey32); // [32]
return buf.array();
}
@Override
public String toString() {
return "HeaderBody{" +
"id=" + blockchainId +
", login='" + userLogin + '\'' +
", type=" + blockchainType +
", num=" + blockchainNumber +
", ver=" + versionUserBch +
", prev=" + prevUserBchId +
", pubkey32=" + Arrays.toString(Arrays.copyOf(publicKey32, 4)) + "..." +
'}';
}
}
@@ -0,0 +1,155 @@
package blockchain.body;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Objects;
/**
* HeaderBody_new — type=0, version=1.
*
* Полный bodyBytes:
* [2] type=0
* [2] version=1
* [payload...]
*
* Payload (как у текущего HeaderBody):
* [8] tag ASCII "SHiNE001"
* [8] blockchainId (long BE)
* [1] loginLength=N (uint8)
* [N] userLogin UTF-8
* [4] blockchainType (int BE) (резерв)
* [4] blockchainNumber (int BE) (резерв)
* [2] versionUserBch (short BE) (резерв)
* [8] prevUserBchId (long BE) (резерв)
* [32] publicKey32 (raw)
*/
public final class HeaderBody_new implements BodyRecord_new {
public static final short TYPE = 0;
public static final short VER = 1;
public static final String TAG = "SHiNE001";
public static final int PUBKEY_LEN = 32;
public final String tag; // "SHiNE001"
public final long blockchainId;
public final String userLogin;
public final int blockchainType;
public final int blockchainNumber;
public final short versionUserBch;
public final long prevUserBchId;
public final byte[] publicKey32;
/**
* Десериализация из полного bodyBytes (ВКЛЮЧАЯ первые 4 байта type/version).
*/
public HeaderBody_new(byte[] bodyBytes) {
Objects.requireNonNull(bodyBytes, "bodyBytes == null");
if (bodyBytes.length < 4) throw new IllegalArgumentException("HeaderBody_new 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_new: type=" + type + " ver=" + ver);
// Теперь bb стоит на payload
if (bb.remaining() < 8 + 8 + 1 + 4 + 4 + 2 + 8 + 32)
throw new IllegalArgumentException("Header payload too short");
byte[] tagBytes = new byte[8];
bb.get(tagBytes);
String t = new String(tagBytes, StandardCharsets.US_ASCII);
if (!TAG.equals(t)) throw new IllegalArgumentException("Bad tag: " + t);
this.tag = t;
this.blockchainId = bb.getLong();
int loginLen = Byte.toUnsignedInt(bb.get());
if (loginLen <= 0 || bb.remaining() < loginLen + 4 + 4 + 2 + 8 + 32)
throw new IllegalArgumentException("Bad login length");
byte[] loginBytes = new byte[loginLen];
bb.get(loginBytes);
this.userLogin = new String(loginBytes, StandardCharsets.UTF_8);
this.blockchainType = bb.getInt();
this.blockchainNumber = bb.getInt();
this.versionUserBch = bb.getShort();
this.prevUserBchId = bb.getLong();
this.publicKey32 = new byte[PUBKEY_LEN];
bb.get(this.publicKey32);
}
/**
* Создание “вручную” (для генерации первого блока).
*/
public HeaderBody_new(long blockchainId,
String userLogin,
int blockchainType,
int blockchainNumber,
short versionUserBch,
long prevUserBchId,
byte[] publicKey32) {
Objects.requireNonNull(userLogin, "userLogin == null");
Objects.requireNonNull(publicKey32, "publicKey32 == null");
if (publicKey32.length != PUBKEY_LEN)
throw new IllegalArgumentException("publicKey32 must be 32 bytes");
this.tag = TAG;
this.blockchainId = blockchainId;
this.userLogin = userLogin;
this.blockchainType = blockchainType;
this.blockchainNumber = blockchainNumber;
this.versionUserBch = versionUserBch;
this.prevUserBchId = prevUserBchId;
this.publicKey32 = Arrays.copyOf(publicKey32, PUBKEY_LEN);
}
@Override public short type() { return TYPE; }
@Override public short version() { return VER; }
@Override
public HeaderBody_new check() {
if (userLogin == null || userLogin.isBlank())
throw new IllegalArgumentException("Login is blank");
if (!userLogin.matches("^[A-Za-z0-9_]+$"))
throw new IllegalArgumentException("Login must match ^[A-Za-z0-9_]+$");
if (publicKey32 == null || publicKey32.length != PUBKEY_LEN)
throw new IllegalArgumentException("publicKey32 must be 32 bytes");
return this;
}
@Override
public byte[] toBytes() {
byte[] loginUtf8 = userLogin.getBytes(StandardCharsets.UTF_8);
if (loginUtf8.length > 255)
throw new IllegalArgumentException("Login too long (>255 bytes)");
int payloadCap = 8 + 8 + 1 + loginUtf8.length + 4 + 4 + 2 + 8 + 32;
int cap = 4 + payloadCap;
ByteBuffer bb = ByteBuffer.allocate(cap).order(ByteOrder.BIG_ENDIAN);
// [type/version]
bb.putShort(TYPE);
bb.putShort(VER);
// payload
bb.put(TAG.getBytes(StandardCharsets.US_ASCII)); // [8]
bb.putLong(blockchainId); // [8]
bb.put((byte) loginUtf8.length); // [1]
bb.put(loginUtf8); // [N]
bb.putInt(blockchainType); // [4]
bb.putInt(blockchainNumber); // [4]
bb.putShort(versionUserBch); // [2]
bb.putLong(prevUserBchId); // [8]
bb.put(publicKey32); // [32]
return bb.array();
}
}
@@ -0,0 +1,77 @@
package blockchain.body;
import java.nio.ByteBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.CodingErrorAction;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
/**
* TextBody — тело записи типа 1 (простое текстовое сообщение).
*.
* Формат body:
* [N] message (UTF-8)
*.
* Тело полностью состоит из UTF-8-строки без каких-либо метаданных.
*/
public final class TextBody implements BodyRecord {
public static final short TYPE = 1;
public final String message;
// ------------------------------------------------------------
// Конструктор №1 — из массива байт (для парсинга)
// ------------------------------------------------------------
public TextBody(byte[] body) {
Objects.requireNonNull(body, "body == null");
if (body.length == 0)
throw new IllegalArgumentException("Тело текстового сообщения пустое");
// строгая проверка валидности UTF-8
var decoder = StandardCharsets.UTF_8
.newDecoder()
.onMalformedInput(CodingErrorAction.REPORT)
.onUnmappableCharacter(CodingErrorAction.REPORT);
try {
var chars = decoder.decode(ByteBuffer.wrap(body));
this.message = chars.toString();
} catch (CharacterCodingException e) {
throw new IllegalArgumentException("Тело не является корректным UTF-8", e);
}
}
// ------------------------------------------------------------
// Конструктор №2 — из строки (для создания нового сообщения)
// ------------------------------------------------------------
public TextBody(String message) {
Objects.requireNonNull(message, "message == null");
if (message.isBlank())
throw new IllegalArgumentException("Текст сообщения не может быть пустым");
this.message = message;
}
// ------------------------------------------------------------
// Проверка и сериализация
// ------------------------------------------------------------
@Override
public TextBody check() {
if (message == null || message.isBlank())
throw new IllegalArgumentException("Текст сообщения не может быть пустым");
return this;
}
@Override
public byte[] toBytes() {
return message.getBytes(StandardCharsets.UTF_8);
}
@Override
public String toString() {
return "TextBody{" +
"len=" + message.length() +
", msg='" + (message.length() > 60 ? message.substring(0, 57) + "..." : message) + '\'' +
'}';
}
}
@@ -0,0 +1,89 @@
package blockchain.body;
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.Objects;
/**
* TextBody_new — type=1, version=1.
*
* Полный bodyBytes:
* [2] type=1
* [2] version=1
* [payload...]
*
* Payload:
* UTF-8 bytes (N>0)
*/
public final class TextBody_new implements BodyRecord_new {
public static final short TYPE = 1;
public static final short VER = 1;
public final String message;
/** Десериализация из полного bodyBytes (включая type/version). */
public TextBody_new(byte[] bodyBytes) {
Objects.requireNonNull(bodyBytes, "bodyBytes == null");
if (bodyBytes.length < 5) // минимум: 4 байта type/ver + 1 байт текста
throw new IllegalArgumentException("TextBody_new 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_new: type=" + type + " ver=" + ver);
byte[] payload = new byte[bb.remaining()];
bb.get(payload);
// строгая проверка UTF-8
var decoder = StandardCharsets.UTF_8
.newDecoder()
.onMalformedInput(CodingErrorAction.REPORT)
.onUnmappableCharacter(CodingErrorAction.REPORT);
try {
this.message = decoder.decode(ByteBuffer.wrap(payload)).toString();
} catch (CharacterCodingException e) {
throw new IllegalArgumentException("Text payload is not valid UTF-8", e);
}
if (this.message.isBlank())
throw new IllegalArgumentException("Text message is blank");
}
/** Создание из строки. */
public TextBody_new(String message) {
Objects.requireNonNull(message, "message == null");
if (message.isBlank())
throw new IllegalArgumentException("message is blank");
this.message = message;
}
@Override public short type() { return TYPE; }
@Override public short version() { return VER; }
@Override
public TextBody_new check() {
if (message == null || message.isBlank())
throw new IllegalArgumentException("Text message is blank");
return this;
}
@Override
public byte[] toBytes() {
byte[] msg = message.getBytes(StandardCharsets.UTF_8);
if (msg.length == 0)
throw new IllegalArgumentException("Text payload is empty");
ByteBuffer bb = ByteBuffer.allocate(4 + msg.length).order(ByteOrder.BIG_ENDIAN);
bb.putShort(TYPE);
bb.putShort(VER);
bb.put(msg);
return bb.array();
}
}