Промежуточная версия
This commit is contained in:
AidarKC 2025-12-17 13:06:08 +03:00
parent ab44cc5282
commit eaf1affb27
30 changed files with 926 additions and 563 deletions

View File

@ -1,19 +1,38 @@
package blockchain.body; package blockchain.body;
/** /**
* Общий интерфейс для всех тел (body) блоков. * BodyRecord_new общий контракт для всех типов body (тела блока).
*. *
* Каждый тип тела реализует: * Идея:
* - check() проверку корректности данных * - На каждый тип body (Header, Text, File, ...) отдельный класс.
* - toBytes() опциональную сериализацию обратно в байты * - Десериализация из байтов делается КОНСТРУКТОРОМ:
* new XxxBody_new(byte[] bodyBytes)
* (конструктор обязан распарсить байты или кинуть IllegalArgumentException).
*
* - Валидация делается методом check().
* check() должен:
* - вернуть this, если всё корректно
* - кинуть IllegalArgumentException, если данные некорректны
*
* - Сериализация обратно в байты делается методом toBytes().
*
* - type() и version() это идентификаторы формата body.
* Они должны быть константами для класса (например TYPE=1, VERSION=1).
*/ */
public interface BodyRecord { public interface BodyRecord {
/** Проверить корректность содержимого. */ /** Код типа записи (совпадает с recordType в BchBlockEntry). */
short type();
/** Версия формата записи (совпадает с recordTypeVersion в BchBlockEntry). */
short version();
/** Проверить корректность содержимого и вернуть этот объект (или кинуть исключение). */
BodyRecord check(); BodyRecord check();
/** (опционально) Сериализация тела обратно в байты. */ /**
default byte[] toBytes() { * Сериализовать тело записи в байты (ровно то, что кладётся в block.body).
throw new UnsupportedOperationException("toBytes() не реализован"); * Важно: НЕ включает общий заголовок блока (recordNumber/timestamp/type/version).
} */
byte[] toBytes();
} }

View File

@ -9,11 +9,11 @@ import java.nio.ByteOrder;
* Правило совместимости (строгое): * Правило совместимости (строгое):
* - если (type, version) неизвестны кидаем IllegalArgumentException * - если (type, version) неизвестны кидаем IllegalArgumentException
*/ */
public final class BodyRecordParser_new { public final class BodyRecordParser {
private BodyRecordParser_new() {} private BodyRecordParser() {}
public static BodyRecord_new parse(byte[] bodyBytes) { public static BodyRecord parse(byte[] bodyBytes) {
if (bodyBytes == null) throw new IllegalArgumentException("bodyBytes == null"); if (bodyBytes == null) throw new IllegalArgumentException("bodyBytes == null");
if (bodyBytes.length < 4) throw new IllegalArgumentException("bodyBytes too short (<4)"); if (bodyBytes.length < 4) throw new IllegalArgumentException("bodyBytes too short (<4)");
@ -25,8 +25,8 @@ public final class BodyRecordParser_new {
int key = ((type & 0xFFFF) << 16) | (ver & 0xFFFF); int key = ((type & 0xFFFF) << 16) | (ver & 0xFFFF);
return switch (key) { return switch (key) {
case 0x0000_0001 -> new HeaderBody_new(bodyBytes); // type=0, ver=1 case 0x0000_0001 -> new HeaderBody(bodyBytes); // type=0, ver=1
case 0x0001_0001 -> new TextBody_new(bodyBytes); // type=1, ver=1 case 0x0001_0001 -> new TextBody(bodyBytes); // type=1, ver=1
default -> throw new IllegalArgumentException(String.format( default -> throw new IllegalArgumentException(String.format(
"Unknown body type/version: type=%d ver=%d (key=0x%08X)", "Unknown body type/version: type=%d ver=%d (key=0x%08X)",
(type & 0xFFFF), (ver & 0xFFFF), key (type & 0xFFFF), (ver & 0xFFFF), key

View File

@ -1,38 +0,0 @@
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();
}

View File

@ -7,128 +7,98 @@ import java.util.Arrays;
import java.util.Objects; import java.util.Objects;
/** /**
* ============================================================================ * HeaderBody_new type=0, version=1.
* HeaderBody тело записи типа 0 (заглавие блокчейна) *
* ============================================================================ * Полный bodyBytes:
*. * [2] type=0
* 🧩 Назначение: * [2] version=1
* Первый блок каждой пользовательской цепочки (.bch) это "заголовок". * [payload...]
* Он хранит базовую информацию о владельце, версии и публичном ключе. *
*. * Payload (как у текущего HeaderBody):
* Этот блок всегда имеет: * [8] tag ASCII "SHiNE001"
* recordType = 0 * [8] blockchainId (long BE)
* recordNumber = 0 * [1] loginLength=N (uint8)
* recordTypeVersion = 1 * [N] userLogin UTF-8
*. * [4] blockchainType (int BE) (резерв)
* ---------------------------------------------------------------------------- * [4] blockchainNumber (int BE) (резерв)
* 🔹 Формат body (без общих 20 байт заголовка блока BchBlock) * [2] versionUserBch (short BE) (резерв)
*. * [8] prevUserBchId (long BE) (резерв)
* | Смещение | Размер | Поле | Формат | Описание | * [32] publicKey32 (raw)
* |-----------|--------|--------------------|---------|-----------|
* | 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 final class HeaderBody implements BodyRecord {
public static final short TYPE = 0; public static final short TYPE = 0;
public static final short VER = 1;
public static final String TAG = "SHiNE001"; public static final String TAG = "SHiNE001";
public static final int PUBKEY_LEN = 32; public static final int PUBKEY_LEN = 32;
public final String tag; // всегда "SHiNE001" public final String tag; // "SHiNE001"
public final long blockchainId; public final long blockchainId;
public final String userLogin; // UTF-8 public final String userLogin;
public final int blockchainType; // пока 0 public final int blockchainType;
public final int blockchainNumber; // пока 0 public final int blockchainNumber;
public final short versionUserBch; // пока 1 public final short versionUserBch;
public final long prevUserBchId; // пока 0 public final long prevUserBchId;
public final byte[] publicKey32; // 32 байта public final byte[] publicKey32;
// ------------------------------------------------------------ /**
// Конструктор 1 из массива байт (для парсинга существующего блока) * Десериализация из полного bodyBytes (ВКЛЮЧАЯ первые 4 байта type/version).
// ------------------------------------------------------------ */
public HeaderBody(byte[] body) { public HeaderBody(byte[] bodyBytes) {
Objects.requireNonNull(body, "body == null"); Objects.requireNonNull(bodyBytes, "bodyBytes == null");
if (body.length < 8 + 8 + 1 + 2 + 4 + 4 + 8 + 32) if (bodyBytes.length < 4) throw new IllegalArgumentException("HeaderBody_new too short");
throw new IllegalArgumentException("HeaderBody слишком короткое");
ByteBuffer buf = ByteBuffer.wrap(body).order(ByteOrder.BIG_ENDIAN); 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");
// [8] тег
byte[] tagBytes = new byte[8]; byte[] tagBytes = new byte[8];
buf.get(tagBytes); bb.get(tagBytes);
String tag = new String(tagBytes, StandardCharsets.US_ASCII); String t = new String(tagBytes, StandardCharsets.US_ASCII);
if (!TAG.equals(tag)) if (!TAG.equals(t)) throw new IllegalArgumentException("Bad tag: " + t);
throw new IllegalArgumentException("Неверный тег: " + tag); this.tag = t;
this.tag = tag;
// [8] blockchainId this.blockchainId = bb.getLong();
this.blockchainId = buf.getLong();
// [1] длина логина int loginLen = Byte.toUnsignedInt(bb.get());
int loginLen = Byte.toUnsignedInt(buf.get()); if (loginLen <= 0 || bb.remaining() < loginLen + 4 + 4 + 2 + 8 + 32)
if (loginLen == 0 || buf.remaining() < loginLen + 4 + 4 + 2 + 8 + 32) throw new IllegalArgumentException("Bad login length");
throw new IllegalArgumentException("Некорректная длина логина");
// [N] логин
byte[] loginBytes = new byte[loginLen]; byte[] loginBytes = new byte[loginLen];
buf.get(loginBytes); bb.get(loginBytes);
this.userLogin = new String(loginBytes, StandardCharsets.UTF_8); this.userLogin = new String(loginBytes, StandardCharsets.UTF_8);
// Остальные поля this.blockchainType = bb.getInt();
this.blockchainType = buf.getInt(); this.blockchainNumber = bb.getInt();
this.blockchainNumber = buf.getInt(); this.versionUserBch = bb.getShort();
this.versionUserBch = buf.getShort(); this.prevUserBchId = bb.getLong();
this.prevUserBchId = buf.getLong();
this.publicKey32 = new byte[PUBKEY_LEN]; this.publicKey32 = new byte[PUBKEY_LEN];
buf.get(this.publicKey32); bb.get(this.publicKey32);
} }
// ------------------------------------------------------------ /**
// Конструктор 2 из параметров (для создания нового заголовка) * Создание вручную (для генерации первого блока).
// ------------------------------------------------------------ */
public HeaderBody(long blockchainId, String userLogin, public HeaderBody(long blockchainId,
int blockchainType, int blockchainNumber, String userLogin,
short versionUserBch, long prevUserBchId, int blockchainType,
int blockchainNumber,
short versionUserBch,
long prevUserBchId,
byte[] publicKey32) { byte[] publicKey32) {
Objects.requireNonNull(userLogin, "userLogin == null"); Objects.requireNonNull(userLogin, "userLogin == null");
Objects.requireNonNull(publicKey32, "publicKey32 == null"); Objects.requireNonNull(publicKey32, "publicKey32 == null");
if (publicKey32.length != PUBKEY_LEN) if (publicKey32.length != PUBKEY_LEN)
throw new IllegalArgumentException("Публичный ключ должен состоять из 32 байт"); throw new IllegalArgumentException("publicKey32 must be 32 bytes");
this.tag = TAG; this.tag = TAG;
this.blockchainId = blockchainId; this.blockchainId = blockchainId;
@ -140,17 +110,17 @@ public final class HeaderBody implements BodyRecord {
this.publicKey32 = Arrays.copyOf(publicKey32, PUBKEY_LEN); this.publicKey32 = Arrays.copyOf(publicKey32, PUBKEY_LEN);
} }
// ------------------------------------------------------------ @Override public short type() { return TYPE; }
// Проверка и сериализация @Override public short version() { return VER; }
// ------------------------------------------------------------
@Override @Override
public HeaderBody check() { public HeaderBody check() {
if (userLogin == null || userLogin.isBlank()) if (userLogin == null || userLogin.isBlank())
throw new IllegalArgumentException("Логин не может быть пустым"); throw new IllegalArgumentException("Login is blank");
if (!userLogin.matches("^[A-Za-z0-9_]+$")) if (!userLogin.matches("^[A-Za-z0-9_]+$"))
throw new IllegalArgumentException("Логин может содержать только латиницу, цифры и _"); throw new IllegalArgumentException("Login must match ^[A-Za-z0-9_]+$");
if (publicKey32 == null || publicKey32.length != PUBKEY_LEN) if (publicKey32 == null || publicKey32.length != PUBKEY_LEN)
throw new IllegalArgumentException("Публичный ключ должен быть 32 байта"); throw new IllegalArgumentException("publicKey32 must be 32 bytes");
return this; return this;
} }
@ -158,34 +128,28 @@ public final class HeaderBody implements BodyRecord {
public byte[] toBytes() { public byte[] toBytes() {
byte[] loginUtf8 = userLogin.getBytes(StandardCharsets.UTF_8); byte[] loginUtf8 = userLogin.getBytes(StandardCharsets.UTF_8);
if (loginUtf8.length > 255) if (loginUtf8.length > 255)
throw new IllegalArgumentException("Логин слишком длинный (>255 байт)"); throw new IllegalArgumentException("Login too long (>255 bytes)");
int cap = 8 + 8 + 1 + loginUtf8.length + 4 + 4 + 2 + 8 + 32; int payloadCap = 8 + 8 + 1 + loginUtf8.length + 4 + 4 + 2 + 8 + 32;
ByteBuffer buf = ByteBuffer.allocate(cap).order(ByteOrder.BIG_ENDIAN); int cap = 4 + payloadCap;
buf.put(TAG.getBytes(StandardCharsets.US_ASCII)); // [8] ByteBuffer bb = ByteBuffer.allocate(cap).order(ByteOrder.BIG_ENDIAN);
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(); // [type/version]
} bb.putShort(TYPE);
bb.putShort(VER);
@Override // payload
public String toString() { bb.put(TAG.getBytes(StandardCharsets.US_ASCII)); // [8]
return "HeaderBody{" + bb.putLong(blockchainId); // [8]
"id=" + blockchainId + bb.put((byte) loginUtf8.length); // [1]
", login='" + userLogin + '\'' + bb.put(loginUtf8); // [N]
", type=" + blockchainType + bb.putInt(blockchainType); // [4]
", num=" + blockchainNumber + bb.putInt(blockchainNumber); // [4]
", ver=" + versionUserBch + bb.putShort(versionUserBch); // [2]
", prev=" + prevUserBchId + bb.putLong(prevUserBchId); // [8]
", pubkey32=" + Arrays.toString(Arrays.copyOf(publicKey32, 4)) + "..." + bb.put(publicKey32); // [32]
'}';
return bb.array();
} }
} }

View File

@ -1,155 +0,0 @@
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();
}
}

View File

@ -1,77 +1,89 @@
package blockchain.body; package blockchain.body;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.CharacterCodingException; import java.nio.charset.CharacterCodingException;
import java.nio.charset.CodingErrorAction; import java.nio.charset.CodingErrorAction;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.Objects; import java.util.Objects;
/** /**
* TextBody тело записи типа 1 (простое текстовое сообщение). * TextBody_new type=1, version=1.
*. *
* Формат body: * Полный bodyBytes:
* [N] message (UTF-8) * [2] type=1
*. * [2] version=1
* Тело полностью состоит из UTF-8-строки без каких-либо метаданных. * [payload...]
*
* Payload:
* UTF-8 bytes (N>0)
*/ */
public final class TextBody implements BodyRecord { public final class TextBody implements BodyRecord {
public static final short TYPE = 1; public static final short TYPE = 1;
public static final short VER = 1;
public final String message; public final String message;
// ------------------------------------------------------------ /** Десериализация из полного bodyBytes (включая type/version). */
// Конструктор 1 из массива байт (для парсинга) public TextBody(byte[] bodyBytes) {
// ------------------------------------------------------------ Objects.requireNonNull(bodyBytes, "bodyBytes == null");
public TextBody(byte[] body) { if (bodyBytes.length < 5) // минимум: 4 байта type/ver + 1 байт текста
Objects.requireNonNull(body, "body == null"); throw new IllegalArgumentException("TextBody_new too short");
if (body.length == 0)
throw new IllegalArgumentException("Тело текстового сообщения пустое");
// строгая проверка валидности UTF-8 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 var decoder = StandardCharsets.UTF_8
.newDecoder() .newDecoder()
.onMalformedInput(CodingErrorAction.REPORT) .onMalformedInput(CodingErrorAction.REPORT)
.onUnmappableCharacter(CodingErrorAction.REPORT); .onUnmappableCharacter(CodingErrorAction.REPORT);
try { try {
var chars = decoder.decode(ByteBuffer.wrap(body)); this.message = decoder.decode(ByteBuffer.wrap(payload)).toString();
this.message = chars.toString();
} catch (CharacterCodingException e) { } catch (CharacterCodingException e) {
throw new IllegalArgumentException("Тело не является корректным UTF-8", e); throw new IllegalArgumentException("Text payload is not valid UTF-8", e);
}
} }
// ------------------------------------------------------------ if (this.message.isBlank())
// Конструктор 2 из строки (для создания нового сообщения) throw new IllegalArgumentException("Text message is blank");
// ------------------------------------------------------------ }
/** Создание из строки. */
public TextBody(String message) { public TextBody(String message) {
Objects.requireNonNull(message, "message == null"); Objects.requireNonNull(message, "message == null");
if (message.isBlank()) if (message.isBlank())
throw new IllegalArgumentException("Текст сообщения не может быть пустым"); throw new IllegalArgumentException("message is blank");
this.message = message; this.message = message;
} }
// ------------------------------------------------------------ @Override public short type() { return TYPE; }
// Проверка и сериализация @Override public short version() { return VER; }
// ------------------------------------------------------------
@Override @Override
public TextBody check() { public TextBody check() {
if (message == null || message.isBlank()) if (message == null || message.isBlank())
throw new IllegalArgumentException("Текст сообщения не может быть пустым"); throw new IllegalArgumentException("Text message is blank");
return this; return this;
} }
@Override @Override
public byte[] toBytes() { public byte[] toBytes() {
return message.getBytes(StandardCharsets.UTF_8); byte[] msg = message.getBytes(StandardCharsets.UTF_8);
} if (msg.length == 0)
throw new IllegalArgumentException("Text payload is empty");
@Override ByteBuffer bb = ByteBuffer.allocate(4 + msg.length).order(ByteOrder.BIG_ENDIAN);
public String toString() { bb.putShort(TYPE);
return "TextBody{" + bb.putShort(VER);
"len=" + message.length() + bb.put(msg);
", msg='" + (message.length() > 60 ? message.substring(0, 57) + "..." : message) + '\'' + return bb.array();
'}';
} }
} }

View File

@ -1,89 +0,0 @@
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();
}
}

View File

@ -0,0 +1,91 @@
package utils.crypto;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Objects;
/**
* BchCryptoVerifier проверка хэша и подписи Ed25519 для .bch сущностей.
*.
* Канонический пре-имидж:
* [N] userLogin UTF-8 (без длины! строго как байты строки)
* [8] blockchainId (big-endian long)
* [32] prevHash32
* [*] rawBytes (без подписи и без хэша)
*.
* Проверяем:
* hash32 == SHA-256(preimage)
* signature64 валидна как Ed25519(preimage, publicKey32)
*/
public final class BchCryptoVerifier {
private static final Logger log = LoggerFactory.getLogger(BchCryptoVerifier.class);
private BchCryptoVerifier() {}
public static boolean verifyAll(String userLogin,
long blockchainId,
byte[] prevHash32,
byte[] rawBytes,
byte[] signature64,
byte[] hash32,
byte[] publicKey32) {
try {
Objects.requireNonNull(userLogin, "userLogin");
requireLen(prevHash32, 32, "prevHash32");
requireLen(signature64, 64, "signature64");
requireLen(hash32, 32, "hash32");
requireLen(publicKey32, 32, "publicKey32");
Objects.requireNonNull(rawBytes, "rawBytes");
byte[] preimage = buildPreimage(userLogin, blockchainId, prevHash32, rawBytes);
// 1) Проверка хэша (BC)
byte[] calcHash = HashSHA256Util.sha256(preimage);
boolean hashOk = Arrays.equals(calcHash, hash32);
// 2) Проверка подписи Ed25519
boolean sigOk = Ed25519Util.verify(preimage, signature64, publicKey32);
if (!hashOk) log.warn("Hash mismatch: hash32 != SHA-256(preimage)");
if (!sigOk) log.warn("Signature mismatch: Ed25519 verify failed");
return hashOk && sigOk;
} catch (IllegalArgumentException ex) {
log.error("verifyAll: bad arguments", ex);
return false;
}
}
/** Собрать канонический пре-имидж без длины логина. */
public static byte[] buildPreimage(String userLogin,
long blockchainId,
byte[] prevHash32,
byte[] rawBytes) {
Objects.requireNonNull(userLogin, "userLogin");
Objects.requireNonNull(prevHash32, "prevHash32");
Objects.requireNonNull(rawBytes, "rawBytes");
byte[] loginUtf8 = userLogin.getBytes(StandardCharsets.UTF_8);
requireLen(prevHash32, 32, "prevHash32");
int capacity = loginUtf8.length + 8 + 32 + rawBytes.length;
ByteBuffer buf = ByteBuffer.allocate(capacity).order(ByteOrder.BIG_ENDIAN);
buf.put(loginUtf8);
buf.putLong(blockchainId);
buf.put(prevHash32);
buf.put(rawBytes);
return buf.array();
}
private static void requireLen(byte[] arr, int len, String name) {
if (arr == null) throw new IllegalArgumentException(name + " is null");
if (arr.length != len) {
throw new IllegalArgumentException(name + " length != " + len + " (got " + arr.length + ")");
}
}
}

View File

@ -0,0 +1,50 @@
package utils.crypto;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
public final class CryptoSelfTest {
private CryptoSelfTest() {}
/**
* Простой запуск: убедиться, что всё собрано и работает.
* Выводит ключи в Base64, знак/проверка подписи OK/FAIL.
*/
public static void main(String[] args) {
System.out.println("=== Ed25519 self-check ===");
// 1) Генерация ключей
byte[] priv = Ed25519Util.generatePrivateKey();
byte[] pub = Ed25519Util.derivePublicKey(priv);
// 2) Конвертация в/из Base64 (чисто для демонстрации)
String privB64 = Ed25519Util.keyToBase64(priv);
String pubB64 = Ed25519Util.keyToBase64(pub);
System.out.println("Private (seed) Base64: " + privB64);
System.out.println("Public Base64 : " + pubB64);
byte[] priv2 = Ed25519Util.keyFromBase64(privB64);
byte[] pub2 = Ed25519Util.keyFromBase64(pubB64);
if (!Arrays.equals(priv, priv2) || !Arrays.equals(pub, pub2)) {
throw new IllegalStateException("Base64 ⇆ bytes дала несовпадение (не должно случаться).");
}
// 3) Подпись и проверка
byte[] data = "Привет, мир Ed25519!".getBytes(StandardCharsets.UTF_8);
byte[] sig = Ed25519Util.sign(data, priv);
boolean ok = Ed25519Util.verify(data, sig, pub);
System.out.println("Verify OK? " + ok);
// 4) Негативный тест: портим данные
byte[] bad = "Привет, мир Ed25519?".getBytes(StandardCharsets.UTF_8);
boolean shouldFail = Ed25519Util.verify(bad, sig, pub);
System.out.println("Verify on changed data (should be false): " + shouldFail);
if (!ok || shouldFail) {
throw new IllegalStateException("Self-test failed.");
}
System.out.println("Self-test passed ✅");
}
}

View File

@ -0,0 +1,173 @@
package utils.crypto;
import org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters;
import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters;
import org.bouncycastle.crypto.signers.Ed25519Signer;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.Base64;
import java.util.Objects;
/**
* ===============================================================
* Ed25519Util статическая утилита для работы с подписями Ed25519
* на базе Bouncy Castle (bcprov). Совместимо с Java 17.
* ---------------------------------------------------------------
* Возможности:
* generatePrivateKey() приватный ключ 32 байта (seed) из SecureRandom.
* generatePrivateKeyFromString(String) приватный ключ 32 байта из строки через SHA-256.
* derivePublicKey(byte[32]) публичный ключ 32 байта из приватного.
* sign(byte[], byte[32]) подпись 64 байта.
* verify(byte[], byte[64], byte[32]) проверка подписи (true/false).
* keyToBase64(byte[32]) / keyFromBase64(String) Base64 ключ (ровно 32 байта).
*.
* Форматы:
* Приватный ключ 32-байтный seed Ed25519.
* Публичный ключ 32-байтный public key.
* Подпись 64 байта.
*.
* Важно:
* Здесь используется «классический» Ed25519 (подпись сырых данных).
* Если нужен режим Ed25519ph (prehash), делай отдельный класс.
*.
* Зависимость (Gradle/Groovy):
* implementation 'org.bouncycastle:bcprov-jdk18on:1.78.1'
* ===============================================================
*/
public final class Ed25519Util {
/** Длина приватного ключа (seed) в байтах. */
public static final int PRIVATE_KEY_LEN = 32;
/** Длина публичного ключа в байтах. */
public static final int PUBLIC_KEY_LEN = 32;
/** Длина подписи в байтах. */
public static final int SIGNATURE_LEN = 64;
// Запрещаем инстанцирование: только статические методы
private Ed25519Util() {}
// ===== Надёжный генератор случайных чисел (ленивая инициализация) =====
private static final SecureRandom SECURE_RANDOM = createSecureRandom();
private static SecureRandom createSecureRandom() {
try {
return SecureRandom.getInstanceStrong();
} catch (Exception ignore) {
return new SecureRandom();
}
}
// =====================================================================
// API
// =====================================================================
/**
* Сгенерировать приватный ключ (seed) Ed25519: 32 случайных байта.
*/
public static byte[] generatePrivateKey() {
byte[] seed = new byte[PRIVATE_KEY_LEN];
SECURE_RANDOM.nextBytes(seed);
return seed;
}
/**
* Сгенерировать приватный ключ (seed, 32 байта) из произвольной строки:
* строка UTF-8 SHA-256 32 байта.
*
* @param anyString любая строка (не null)
* @return массив 32 байта (seed)
*/
public static byte[] generatePrivateKeyFromString(String anyString) {
Objects.requireNonNull(anyString, "Строка для генерации приватного ключа не должна быть null");
byte[] input = anyString.getBytes(StandardCharsets.UTF_8);
return HashSHA256Util.sha256(input); // ровно 32 байта
}
/**
* Получить публичный ключ (32 байта) из приватного (seed, 32 байта).
*/
public static byte[] derivePublicKey(byte[] privateKey32) {
requireLength(privateKey32, PRIVATE_KEY_LEN, "приватного ключа (seed)");
Ed25519PrivateKeyParameters priv = new Ed25519PrivateKeyParameters(privateKey32, 0);
Ed25519PublicKeyParameters pub = priv.generatePublicKey();
return pub.getEncoded(); // 32 байта
}
/**
* Подписать сырые данные (без предварительного хеширования) приватным ключом Ed25519.
*
* @param data данные для подписи (не null)
* @param privateKey32 приватный ключ (seed) 32 байта
* @return подпись длиной 64 байта
*/
public static byte[] sign(byte[] data, byte[] privateKey32) {
Objects.requireNonNull(data, "Данные для подписи не должны быть null");
requireLength(privateKey32, PRIVATE_KEY_LEN, "приватного ключа (seed)");
Ed25519PrivateKeyParameters priv = new Ed25519PrivateKeyParameters(privateKey32, 0);
Ed25519Signer signer = new Ed25519Signer();
signer.init(true, priv);
signer.update(data, 0, data.length);
byte[] signature = signer.generateSignature();
if (signature == null || signature.length != SIGNATURE_LEN) {
throw new IllegalStateException("Ожидалась подпись длиной 64 байта.");
}
return signature;
}
/**
* Проверить подпись Ed25519.
*
* @param data исходные данные
* @param signature64 подпись 64 байта
* @param publicKey32 публичный ключ 32 байта
* @return true, если подпись корректна для этих данных и ключа
*/
public static boolean verify(byte[] data, byte[] signature64, byte[] publicKey32) {
Objects.requireNonNull(data, "Данные для проверки подписи не должны быть null");
requireLength(signature64, SIGNATURE_LEN, "подписи Ed25519");
requireLength(publicKey32, PUBLIC_KEY_LEN, "публичного ключа");
Ed25519PublicKeyParameters pub = new Ed25519PublicKeyParameters(publicKey32, 0);
Ed25519Signer verifier = new Ed25519Signer();
verifier.init(false, pub);
verifier.update(data, 0, data.length);
return verifier.verifySignature(signature64);
}
/**
* Преобразовать 32-байтный ключ (приватный seed или публичный key) в Base64-строку.
*/
public static String keyToBase64(byte[] key32) {
requireLength(key32, 32, "ключа (ожидалось 32 байта)");
return Base64.getEncoder().encodeToString(key32);
}
/**
* Из Base64-строки получить 32-байтный ключ.
* @throws IllegalArgumentException если после декодирования длина 32
*/
public static byte[] keyFromBase64(String base64) {
Objects.requireNonNull(base64, "Base64-строка не должна быть null");
byte[] raw = Base64.getDecoder().decode(base64);
requireLength(raw, 32, "ключа после декодирования Base64 (ожидалось 32 байта)");
return raw;
}
// =====================================================================
// ВСПОМОГАТЕЛЬНЫЕ
// =====================================================================
private static void requireLength(byte[] data, int expectedLen, String what) {
if (data == null) {
throw new IllegalArgumentException("Массив " + what + " не должен быть null.");
}
if (data.length != expectedLen) {
throw new IllegalArgumentException(
"Некорректная длина " + what + ": " + data.length + " байт(а). Ожидалось: " + expectedLen + "."
);
}
}
}

View File

@ -0,0 +1,53 @@
package utils.crypto;
import org.bouncycastle.crypto.digests.SHA256Digest;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
public final class HashSHA256Util {
private HashSHA256Util() {}
/** Посчитать SHA-256 от всего массива. */
public static byte[] sha256(byte[] data) {
if (data == null) throw new IllegalArgumentException("data == null");
SHA256Digest d = new SHA256Digest();
d.update(data, 0, data.length);
byte[] out = new byte[32];
d.doFinal(out, 0);
return out;
}
/** Получить loginId из строки логина.
* Алгоритм:
* - login -> UTF-8 bytes
* - SHA-256
* - берём последние 8 байт (справа)
* - интерпретируем как signed long (BigEndian)
*/
public static long loginToLoginId(String login) {
if (login == null || login.isBlank())
throw new IllegalArgumentException("login is null or empty");
byte[] hash = sha256(login.getBytes(StandardCharsets.UTF_8));
// последние 8 байт SHA-256
return ByteBuffer.wrap(hash, 24, 8)
.order(ByteOrder.BIG_ENDIAN)
.getLong();
}
/** Инкрементальный SHA-256 (если нужно будет кормить по кускам). */
public static final class Sha256 {
private final SHA256Digest d = new SHA256Digest();
public Sha256 update(byte[] part) {
if (part != null) d.update(part, 0, part.length);
return this;
}
public byte[] doFinal() {
byte[] out = new byte[32];
d.doFinal(out, 0);
return out;
}
}
}

View File

@ -0,0 +1,30 @@
# utils.crypto
Пакет отвечает за криптографию — подписи и хэши блоков.
Используется при создании и проверке целостности `.bch`-блоков.
---
## Классы
### **Ed25519Util**
Работает с подписями Ed25519.
Методы:
- `generatePrivateKey()` — создать приватный ключ (32 байта)
- `generatePrivateKeyFromString(String)` — детерминированный ключ из строки
- `derivePublicKey(byte[32])` — получить публичный ключ (32 байта)
- `sign(byte[], byte[32])` — подписать данные (64-байтная подпись)
- `verify(byte[], byte[64], byte[32])` — проверить подпись
### **HashUtil**
Вычисляет SHA-256.
Методы:
- `sha256(byte[])``[32]` — вернуть хэш массива
- `Sha256` — вложенный класс для пошагового хэширования
### **BchCryptoVerifier**
Проверяет подпись и хэш блока перед записью в блокчейн.
Методы:
- `verifyAll(userLogin, blockchainId, prevHash32, rawBytes, signature64, hash32, publicKey32)`
`true/false` — корректна ли подпись и хэш
- `buildPreimage(...)` — собирает байты, которые подписываются:

View File

@ -1,7 +1,5 @@
package shine.db; package shine.db;
import shine.db.dao.ActiveSessionsDAO;
import shine.db.entities.ActiveSession;
import utils.config.AppConfig; import utils.config.AppConfig;
import java.nio.file.Files; import java.nio.file.Files;

View File

@ -1,7 +1,7 @@
package shine.db.dao; package shine.db.dao;
import shine.db.SqliteDbController; import shine.db.SqliteDbController;
import shine.db.entities.ActiveSession; import shine.db.entities.ActiveSessionEntry;
import java.sql.*; import java.sql.*;
import java.util.ArrayList; import java.util.ArrayList;
@ -53,7 +53,7 @@ public final class ActiveSessionsDAO {
/** /**
* Вставка новой сессии. * Вставка новой сессии.
*/ */
public void insert(ActiveSession session) throws SQLException { public void insert(ActiveSessionEntry session) throws SQLException {
String sql = """ String sql = """
INSERT INTO active_sessions ( INSERT INTO active_sessions (
sessionId, sessionId,
@ -94,7 +94,7 @@ public final class ActiveSessionsDAO {
/** /**
* Получить сессию по sessionId. * Получить сессию по sessionId.
*/ */
public ActiveSession getBySessionId(String sessionId) throws SQLException { public ActiveSessionEntry getBySessionId(String sessionId) throws SQLException {
String sql = """ String sql = """
SELECT SELECT
sessionId, sessionId,
@ -128,7 +128,7 @@ public final class ActiveSessionsDAO {
/** /**
* Получить список всех активных сессий пользователя по loginId. * Получить список всех активных сессий пользователя по loginId.
*/ */
public List<ActiveSession> getByLoginId(long loginId) throws SQLException { public List<ActiveSessionEntry> getByLoginId(long loginId) throws SQLException {
String sql = """ String sql = """
SELECT SELECT
sessionId, sessionId,
@ -148,7 +148,7 @@ public final class ActiveSessionsDAO {
WHERE loginId = ? WHERE loginId = ?
"""; """;
List<ActiveSession> result = new ArrayList<>(); List<ActiveSessionEntry> result = new ArrayList<>();
try (PreparedStatement ps = db.getConnection().prepareStatement(sql)) { try (PreparedStatement ps = db.getConnection().prepareStatement(sql)) {
ps.setLong(1, loginId); ps.setLong(1, loginId);
@ -235,7 +235,7 @@ public final class ActiveSessionsDAO {
/** /**
* Маппинг ResultSet ActiveSession (все 13 полей). * Маппинг ResultSet ActiveSession (все 13 полей).
*/ */
private ActiveSession mapRow(ResultSet rs) throws SQLException { private ActiveSessionEntry mapRow(ResultSet rs) throws SQLException {
String sessionId = rs.getString("sessionId"); String sessionId = rs.getString("sessionId");
long loginId = rs.getLong("loginId"); long loginId = rs.getLong("loginId");
String sessionPwd = rs.getString("sessionPwd"); String sessionPwd = rs.getString("sessionPwd");
@ -250,7 +250,7 @@ public final class ActiveSessionsDAO {
String clientInfoFromRequest = rs.getString("clientInfoFromRequest"); String clientInfoFromRequest = rs.getString("clientInfoFromRequest");
String userLanguage = rs.getString("userLanguage"); String userLanguage = rs.getString("userLanguage");
return new ActiveSession( return new ActiveSessionEntry(
sessionId, sessionId,
loginId, loginId,
sessionPwd, sessionPwd,

View File

@ -1,7 +1,7 @@
package shine.db.dao; package shine.db.dao;
import shine.db.SqliteDbController; import shine.db.SqliteDbController;
import shine.db.entities.SolanaUser; import shine.db.entities.SolanaUserEntry;
import java.sql.*; import java.sql.*;
import java.util.ArrayList; import java.util.ArrayList;
@ -36,7 +36,7 @@ public final class SolanaUsersDAO {
return instance; return instance;
} }
public void insert(SolanaUser user) throws SQLException { public void insert(SolanaUserEntry user) throws SQLException {
String sql = """ String sql = """
INSERT INTO solana_users (login, loginId, bchId, loginKey, deviceKey, bchLimit) INSERT INTO solana_users (login, loginId, bchId, loginKey, deviceKey, bchLimit)
VALUES (?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?)
@ -59,7 +59,7 @@ public final class SolanaUsersDAO {
} }
} }
public SolanaUser getByLoginId(long loginId) throws SQLException { public SolanaUserEntry getByLoginId(long loginId) throws SQLException {
String sql = """ String sql = """
SELECT login, loginId, bchId, loginKey, deviceKey, bchLimit SELECT login, loginId, bchId, loginKey, deviceKey, bchLimit
FROM solana_users FROM solana_users
@ -76,7 +76,7 @@ public final class SolanaUsersDAO {
} }
} }
public SolanaUser getByLogin(String login) throws SQLException { public SolanaUserEntry getByLogin(String login) throws SQLException {
String sql = """ String sql = """
SELECT login, loginId, bchId, loginKey, deviceKey, bchLimit SELECT login, loginId, bchId, loginKey, deviceKey, bchLimit
FROM solana_users FROM solana_users
@ -93,7 +93,7 @@ public final class SolanaUsersDAO {
} }
} }
public List<SolanaUser> searchByLoginPrefix(String prefix) throws SQLException { public List<SolanaUserEntry> searchByLoginPrefix(String prefix) throws SQLException {
String sql = """ String sql = """
SELECT login, loginId, bchId, loginKey, deviceKey, bchLimit SELECT login, loginId, bchId, loginKey, deviceKey, bchLimit
FROM solana_users FROM solana_users
@ -102,7 +102,7 @@ public final class SolanaUsersDAO {
LIMIT 5 LIMIT 5
"""; """;
List<SolanaUser> result = new ArrayList<>(); List<SolanaUserEntry> result = new ArrayList<>();
try (PreparedStatement ps = db.getConnection().prepareStatement(sql)) { try (PreparedStatement ps = db.getConnection().prepareStatement(sql)) {
ps.setString(1, prefix.toLowerCase() + "%"); ps.setString(1, prefix.toLowerCase() + "%");
@ -115,8 +115,8 @@ public final class SolanaUsersDAO {
return result; return result;
} }
private SolanaUser mapRow(ResultSet rs) throws SQLException { private SolanaUserEntry mapRow(ResultSet rs) throws SQLException {
return new SolanaUser( return new SolanaUserEntry(
rs.getLong("loginId"), rs.getLong("loginId"),
rs.getString("login"), rs.getString("login"),
rs.getLong("bchId"), rs.getLong("bchId"),

View File

@ -1,7 +1,7 @@
package shine.db.dao; package shine.db.dao;
import shine.db.SqliteDbController; import shine.db.SqliteDbController;
import shine.db.entities.UserParam; import shine.db.entities.UserParamEntry;
import java.sql.*; import java.sql.*;
import java.util.ArrayList; import java.util.ArrayList;
@ -33,7 +33,7 @@ public final class UserParamsDAO {
* Если запись существует -> обновляем поля. * Если запись существует -> обновляем поля.
* Если нет -> вставляем новую запись. * Если нет -> вставляем новую запись.
*/ */
public void upsert(UserParam param) throws SQLException { public void upsert(UserParamEntry param) throws SQLException {
String sql = """ String sql = """
INSERT INTO users_params ( INSERT INTO users_params (
loginId, loginId,
@ -68,7 +68,7 @@ public final class UserParamsDAO {
/** /**
* Получить параметр по loginId + param. * Получить параметр по loginId + param.
*/ */
public UserParam getByUserIdAndParam(long loginId, String paramName) throws SQLException { public UserParamEntry getByUserIdAndParam(long loginId, String paramName) throws SQLException {
String sql = """ String sql = """
SELECT SELECT
loginId, loginId,
@ -95,7 +95,7 @@ public final class UserParamsDAO {
/** /**
* Получить все параметры пользователя. * Получить все параметры пользователя.
*/ */
public List<UserParam> getByUserId(long loginId) throws SQLException { public List<UserParamEntry> getByUserId(long loginId) throws SQLException {
String sql = """ String sql = """
SELECT SELECT
loginId, loginId,
@ -110,7 +110,7 @@ public final class UserParamsDAO {
ORDER BY time_ms DESC ORDER BY time_ms DESC
"""; """;
List<UserParam> result = new ArrayList<>(); List<UserParamEntry> result = new ArrayList<>();
try (PreparedStatement ps = db.getConnection().prepareStatement(sql)) { try (PreparedStatement ps = db.getConnection().prepareStatement(sql)) {
ps.setLong(1, loginId); ps.setLong(1, loginId);
@ -122,8 +122,8 @@ public final class UserParamsDAO {
return result; return result;
} }
private UserParam mapRow(ResultSet rs) throws SQLException { private UserParamEntry mapRow(ResultSet rs) throws SQLException {
return new UserParam( return new UserParamEntry(
rs.getLong("loginId"), rs.getLong("loginId"),
rs.getString("param"), rs.getString("param"),
rs.getLong("bch_channel_id"), rs.getLong("bch_channel_id"),

View File

@ -20,7 +20,7 @@ package shine.db.entities;
* FOREIGN KEY (loginId) REFERENCES solana_users(loginId) * FOREIGN KEY (loginId) REFERENCES solana_users(loginId)
* ); * );
*/ */
public class ActiveSession { public class ActiveSessionEntry {
private String sessionId; // TEXT base64(32 bytes) private String sessionId; // TEXT base64(32 bytes)
private long loginId; // INTEGER private long loginId; // INTEGER
@ -38,10 +38,10 @@ public class ActiveSession {
private String clientInfoFromRequest; // строка, собранная на сервере private String clientInfoFromRequest; // строка, собранная на сервере
private String userLanguage; // prefer-language (например, "ru-RU") private String userLanguage; // prefer-language (например, "ru-RU")
public ActiveSession() { public ActiveSessionEntry() {
} }
public ActiveSession(String sessionId, public ActiveSessionEntry(String sessionId,
long loginId, long loginId,
String sessionPwd, String sessionPwd,
String storagePwd, String storagePwd,

View File

@ -10,7 +10,7 @@ package shine.db.entities;
* - deviceKey публичный ключ устройства (второй ключ); * - deviceKey публичный ключ устройства (второй ключ);
* - bchLimit лимит по количеству блоков / размеру цепочки (может быть null). * - bchLimit лимит по количеству блоков / размеру цепочки (может быть null).
*/ */
public class SolanaUser { public class SolanaUserEntry {
private long loginId; private long loginId;
private String login; private String login;
@ -19,10 +19,10 @@ public class SolanaUser {
private String deviceKey; // раньше pubkey1 private String deviceKey; // раньше pubkey1
private Integer bchLimit; // может быть null private Integer bchLimit; // может быть null
public SolanaUser() { public SolanaUserEntry() {
} }
public SolanaUser(long loginId, public SolanaUserEntry(long loginId,
String login, String login,
long bchId, long bchId,
String loginKey, String loginKey,

View File

@ -1,6 +1,6 @@
package shine.db.entities; package shine.db.entities;
public class UserParam { public class UserParamEntry {
private long loginId; private long loginId;
private String param; private String param;
@ -10,10 +10,10 @@ public class UserParam {
private short pubkeyNum; private short pubkeyNum;
private String signature; private String signature;
public UserParam() { public UserParamEntry() {
} }
public UserParam(long loginId, public UserParamEntry(long loginId,
String param, String param,
long bchChannelId, long bchChannelId,
String value, String value,

View File

@ -1,8 +1,8 @@
package server.logic.ws_protocol.JSON; package server.logic.ws_protocol.JSON;
import org.eclipse.jetty.websocket.api.Session; import org.eclipse.jetty.websocket.api.Session;
import shine.db.entities.SolanaUser; import shine.db.entities.SolanaUserEntry;
import shine.db.entities.ActiveSession; import shine.db.entities.ActiveSessionEntry;
/** /**
* ConnectionContext контекст состояния одного WebSocket-соединения. * ConnectionContext контекст состояния одного WebSocket-соединения.
@ -16,10 +16,10 @@ public class ConnectionContext {
public static final int AUTH_STATUS_USER = 2; // авторизованный пользователь public static final int AUTH_STATUS_USER = 2; // авторизованный пользователь
// Полный пользователь из БД (solana_users) // Полный пользователь из БД (solana_users)
private SolanaUser solanaUser; private SolanaUserEntry solanaUserEntry;
// Активная сессия из БД (active_sessions) // Активная сессия из БД (active_sessions)
private ActiveSession activeSession; private ActiveSessionEntry activeSessionEntry;
/** /**
* Идентификатор сессии base64-строка от 32 байт. * Идентификатор сессии base64-строка от 32 байт.
@ -61,30 +61,30 @@ public class ConnectionContext {
// --- SolanaUser / ActiveSession --- // --- SolanaUser / ActiveSession ---
public SolanaUser getSolanaUser() { public SolanaUserEntry getSolanaUser() {
return solanaUser; return solanaUserEntry;
} }
public void setSolanaUser(SolanaUser solanaUser) { public void setSolanaUser(SolanaUserEntry solanaUserEntry) {
this.solanaUser = solanaUser; this.solanaUserEntry = solanaUserEntry;
} }
public ActiveSession getActiveSession() { public ActiveSessionEntry getActiveSession() {
return activeSession; return activeSessionEntry;
} }
public void setActiveSession(ActiveSession activeSession) { public void setActiveSession(ActiveSessionEntry activeSessionEntry) {
this.activeSession = activeSession; this.activeSessionEntry = activeSessionEntry;
} }
// --- Удобные геттеры для логина --- // --- Удобные геттеры для логина ---
public String getLogin() { public String getLogin() {
return solanaUser != null ? solanaUser.getLogin() : null; return solanaUserEntry != null ? solanaUserEntry.getLogin() : null;
} }
public Long getLoginId() { public Long getLoginId() {
return solanaUser != null ? solanaUser.getLoginId() : null; return solanaUserEntry != null ? solanaUserEntry.getLoginId() : null;
} }
// --- sessionId / sessionPwd --- // --- sessionId / sessionPwd ---
@ -134,8 +134,8 @@ public class ConnectionContext {
} }
public void reset() { public void reset() {
solanaUser = null; solanaUserEntry = null;
activeSession = null; activeSessionEntry = null;
sessionId = null; sessionId = null;
sessionPwd = null; sessionPwd = null;

View File

@ -0,0 +1,38 @@
package server.logic.ws_protocol.JSON.entyties.Blockchain;
import server.logic.ws_protocol.JSON.entyties.Net_Request;
public class Net_AddBlock_new_Request extends Net_Request {
private long blockchainId;
private int globalNumber;
private String prevGlobalHash; // HEX(64) or ""
private int lineNumber; // 0..7
private int lineBlockNumber;
private String prevLineHash; // HEX(64) or ""
private String blockBase64; // base64url of raw .bch bytes
public long getBlockchainId() { return blockchainId; }
public void setBlockchainId(long blockchainId) { this.blockchainId = blockchainId; }
public int getGlobalNumber() { return globalNumber; }
public void setGlobalNumber(int globalNumber) { this.globalNumber = globalNumber; }
public String getPrevGlobalHash() { return prevGlobalHash; }
public void setPrevGlobalHash(String prevGlobalHash) { this.prevGlobalHash = prevGlobalHash; }
public int getLineNumber() { return lineNumber; }
public void setLineNumber(int lineNumber) { this.lineNumber = lineNumber; }
public int getLineBlockNumber() { return lineBlockNumber; }
public void setLineBlockNumber(int lineBlockNumber) { this.lineBlockNumber = lineBlockNumber; }
public String getPrevLineHash() { return prevLineHash; }
public void setPrevLineHash(String prevLineHash) { this.prevLineHash = prevLineHash; }
public String getBlockBase64() { return blockBase64; }
public void setBlockBase64(String blockBase64) { this.blockBase64 = blockBase64; }
}

View File

@ -0,0 +1,29 @@
package server.logic.ws_protocol.JSON.entyties.Blockchain;
import server.logic.ws_protocol.JSON.entyties.Net_Response;
public class Net_AddBlock_new_Response extends Net_Response {
private int serverLastGlobalNumber;
private String serverLastGlobalHash;
private int serverLastLineNumber;
private String serverLastLineHash;
private String reasonCode; // "OUT_OF_SEQUENCE", "HASH_MISMATCH", ...
public int getServerLastGlobalNumber() { return serverLastGlobalNumber; }
public void setServerLastGlobalNumber(int v) { this.serverLastGlobalNumber = v; }
public String getServerLastGlobalHash() { return serverLastGlobalHash; }
public void setServerLastGlobalHash(String v) { this.serverLastGlobalHash = v; }
public int getServerLastLineNumber() { return serverLastLineNumber; }
public void setServerLastLineNumber(int v) { this.serverLastLineNumber = v; }
public String getServerLastLineHash() { return serverLastLineHash; }
public void setServerLastLineHash(String v) { this.serverLastLineHash = v; }
public String getReasonCode() { return reasonCode; }
public void setReasonCode(String reasonCode) { this.reasonCode = reasonCode; }
}

View File

@ -8,7 +8,7 @@ import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory; import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
import server.logic.ws_protocol.WireCodes; import server.logic.ws_protocol.WireCodes;
import shine.db.dao.SolanaUsersDAO; import shine.db.dao.SolanaUsersDAO;
import shine.db.entities.SolanaUser; import shine.db.entities.SolanaUserEntry;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.util.Base64; import java.util.Base64;
@ -49,9 +49,9 @@ public class Net_AuthChallenge_Handler implements JsonMessageHandler {
} }
// 2) Ищем пользователя в локальной БД // 2) Ищем пользователя в локальной БД
SolanaUser solanaUser = SolanaUsersDAO.getInstance().getByLogin(login); SolanaUserEntry solanaUserEntry = SolanaUsersDAO.getInstance().getByLogin(login);
if (solanaUser == null) { if (solanaUserEntry == null) {
return NetExceptionResponseFactory.error( return NetExceptionResponseFactory.error(
req, req,
WireCodes.Status.UNVERIFIED, WireCodes.Status.UNVERIFIED,
@ -61,7 +61,7 @@ public class Net_AuthChallenge_Handler implements JsonMessageHandler {
} }
// 3) Заполняем контекст пользователем // 3) Заполняем контекст пользователем
ctx.setSolanaUser(solanaUser); ctx.setSolanaUser(solanaUserEntry);
// 3.1) Отмечаем, что по этому соединению начата авторификация // 3.1) Отмечаем, что по этому соединению начата авторификация
ctx.setAuthenticationStatus(ConnectionContext.AUTH_STATUS_AUTH_IN_PROGRESS); ctx.setAuthenticationStatus(ConnectionContext.AUTH_STATUS_AUTH_IN_PROGRESS);

View File

@ -13,8 +13,8 @@ import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
import server.logic.ws_protocol.WireCodes; import server.logic.ws_protocol.WireCodes;
import server.ws.WsConnectionUtils; import server.ws.WsConnectionUtils;
import shine.db.dao.ActiveSessionsDAO; import shine.db.dao.ActiveSessionsDAO;
import shine.db.entities.ActiveSession; import shine.db.entities.ActiveSessionEntry;
import shine.db.entities.SolanaUser; import shine.db.entities.SolanaUserEntry;
import java.sql.SQLException; import java.sql.SQLException;
@ -62,7 +62,7 @@ public class Net_CloseActiveSession_Handler implements JsonMessageHandler {
); );
} }
SolanaUser user = ctx.getSolanaUser(); SolanaUserEntry user = ctx.getSolanaUser();
long currentLoginId = user.getLoginId(); long currentLoginId = user.getLoginId();
int authStatus = ctx.getAuthenticationStatus(); int authStatus = ctx.getAuthenticationStatus();
@ -158,7 +158,7 @@ public class Net_CloseActiveSession_Handler implements JsonMessageHandler {
} }
ActiveSessionsDAO sessionsDao = ActiveSessionsDAO.getInstance(); ActiveSessionsDAO sessionsDao = ActiveSessionsDAO.getInstance();
ActiveSession targetSession; ActiveSessionEntry targetSession;
try { try {
targetSession = sessionsDao.getBySessionId(targetSessionId); targetSession = sessionsDao.getBySessionId(targetSessionId);
} catch (SQLException e) { } catch (SQLException e) {

View File

@ -14,8 +14,8 @@ import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
import server.logic.ws_protocol.WireCodes; import server.logic.ws_protocol.WireCodes;
import server.ws.WsConnectionUtils; import server.ws.WsConnectionUtils;
import shine.db.dao.ActiveSessionsDAO; import shine.db.dao.ActiveSessionsDAO;
import shine.db.entities.ActiveSession; import shine.db.entities.ActiveSessionEntry;
import shine.db.entities.SolanaUser; import shine.db.entities.SolanaUserEntry;
import shine.geo.ClientInfoService; import shine.geo.ClientInfoService;
import shine.geo.GeoLookupService; import shine.geo.GeoLookupService;
import utils.crypto.Ed25519Util; import utils.crypto.Ed25519Util;
@ -72,7 +72,7 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
* @throws IllegalArgumentException при некорректном base64 ключа/подписи * @throws IllegalArgumentException при некорректном base64 ключа/подписи
*/ */
public static boolean verifyAuthorificatedSignature( public static boolean verifyAuthorificatedSignature(
SolanaUser user, SolanaUserEntry user,
String authNonce, String authNonce,
long timeMs, long timeMs,
String signatureB64 String signatureB64
@ -108,7 +108,7 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
return err; return err;
} }
SolanaUser user = ctx.getSolanaUser(); SolanaUserEntry user = ctx.getSolanaUser();
Long loginId = user.getLoginId(); Long loginId = user.getLoginId();
if (loginId == null) { if (loginId == null) {
Net_Response err = NetExceptionResponseFactory.error( Net_Response err = NetExceptionResponseFactory.error(
@ -237,10 +237,10 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
// --- создаём запись ActiveSession и сохраняем в БД --- // --- создаём запись ActiveSession и сохраняем в БД ---
ActiveSessionsDAO dao = ActiveSessionsDAO.getInstance(); ActiveSessionsDAO dao = ActiveSessionsDAO.getInstance();
ActiveSession activeSession; ActiveSessionEntry activeSessionEntry;
try { try {
activeSession = new ActiveSession( activeSessionEntry = new ActiveSessionEntry(
sessionId, sessionId,
loginId, loginId,
newSessionPwd, // настоящий секрет сессии newSessionPwd, // настоящий секрет сессии
@ -256,7 +256,7 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
userLanguage userLanguage
); );
dao.insert(activeSession); dao.insert(activeSessionEntry);
} catch (SQLException e) { } catch (SQLException e) {
log.error("Ошибка БД при создании новой сессии для loginId={}", loginId, e); log.error("Ошибка БД при создании новой сессии для loginId={}", loginId, e);
Net_Response err = NetExceptionResponseFactory.error( Net_Response err = NetExceptionResponseFactory.error(
@ -270,7 +270,7 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
} }
// --- обновляем контекст --- // --- обновляем контекст ---
ctx.setActiveSession(activeSession); ctx.setActiveSession(activeSessionEntry);
ctx.setSessionId(sessionId); ctx.setSessionId(sessionId);
ctx.setSessionPwd(newSessionPwd); // теперь в контексте хранится секрет сессии ctx.setSessionPwd(newSessionPwd); // теперь в контексте хранится секрет сессии
ctx.setAuthNonce(null); // одноразовый nonce больше не нужен ctx.setAuthNonce(null); // одноразовый nonce больше не нужен

View File

@ -12,8 +12,8 @@ import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory; import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
import server.logic.ws_protocol.WireCodes; import server.logic.ws_protocol.WireCodes;
import shine.db.dao.ActiveSessionsDAO; import shine.db.dao.ActiveSessionsDAO;
import shine.db.entities.ActiveSession; import shine.db.entities.ActiveSessionEntry;
import shine.db.entities.SolanaUser; import shine.db.entities.SolanaUserEntry;
import shine.geo.GeoLookupService; import shine.geo.GeoLookupService;
import java.sql.SQLException; import java.sql.SQLException;
@ -50,7 +50,7 @@ public class Net_ListSessions_Handler implements JsonMessageHandler {
); );
} }
SolanaUser user = ctx.getSolanaUser(); SolanaUserEntry user = ctx.getSolanaUser();
long currentLoginId = user.getLoginId(); long currentLoginId = user.getLoginId();
int authStatus = ctx.getAuthenticationStatus(); int authStatus = ctx.getAuthenticationStatus();
@ -128,7 +128,7 @@ public class Net_ListSessions_Handler implements JsonMessageHandler {
} }
// 3) Тянем все активные сессии пользователя из БД // 3) Тянем все активные сессии пользователя из БД
List<ActiveSession> sessions; List<ActiveSessionEntry> sessions;
try { try {
sessions = ActiveSessionsDAO.getInstance().getByLoginId(currentLoginId); sessions = ActiveSessionsDAO.getInstance().getByLoginId(currentLoginId);
} catch (SQLException e) { } catch (SQLException e) {
@ -143,7 +143,7 @@ public class Net_ListSessions_Handler implements JsonMessageHandler {
// 4) Собираем DTO с геолокацией // 4) Собираем DTO с геолокацией
List<SessionInfo> resultList = new ArrayList<>(); List<SessionInfo> resultList = new ArrayList<>();
for (ActiveSession s : sessions) { for (ActiveSessionEntry s : sessions) {
SessionInfo info = new SessionInfo(); SessionInfo info = new SessionInfo();
info.setSessionId(s.getSessionId()); info.setSessionId(s.getSessionId());
info.setClientInfoFromClient(s.getClientInfoFromClient()); info.setClientInfoFromClient(s.getClientInfoFromClient());

View File

@ -13,8 +13,8 @@ import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
import server.logic.ws_protocol.WireCodes; import server.logic.ws_protocol.WireCodes;
import shine.db.dao.ActiveSessionsDAO; import shine.db.dao.ActiveSessionsDAO;
import shine.db.dao.SolanaUsersDAO; import shine.db.dao.SolanaUsersDAO;
import shine.db.entities.ActiveSession; import shine.db.entities.ActiveSessionEntry;
import shine.db.entities.SolanaUser; import shine.db.entities.SolanaUserEntry;
import shine.geo.ClientInfoService; import shine.geo.ClientInfoService;
import shine.geo.GeoLookupService; import shine.geo.GeoLookupService;
@ -63,7 +63,7 @@ public class Net_RefreshSession_Handler implements JsonMessageHandler {
} }
ActiveSessionsDAO sessionsDao = ActiveSessionsDAO.getInstance(); ActiveSessionsDAO sessionsDao = ActiveSessionsDAO.getInstance();
ActiveSession session; ActiveSessionEntry session;
try { try {
session = sessionsDao.getBySessionId(sessionId); session = sessionsDao.getBySessionId(sessionId);
} catch (SQLException e) { } catch (SQLException e) {
@ -96,11 +96,11 @@ public class Net_RefreshSession_Handler implements JsonMessageHandler {
} }
// --- вытаскиваем пользователя по loginId --- // --- вытаскиваем пользователя по loginId ---
SolanaUser solanaUser = null; SolanaUserEntry solanaUserEntry = null;
long loginId = session.getLoginId(); long loginId = session.getLoginId();
try { try {
SolanaUsersDAO usersDao = SolanaUsersDAO.getInstance(); SolanaUsersDAO usersDao = SolanaUsersDAO.getInstance();
solanaUser = usersDao.getByLoginId(loginId); solanaUserEntry = usersDao.getByLoginId(loginId);
} catch (SQLException e) { } catch (SQLException e) {
log.error("Ошибка БД при поиске пользователя по loginId={} из сессии", loginId, e); log.error("Ошибка БД при поиске пользователя по loginId={} из сессии", loginId, e);
return NetExceptionResponseFactory.error( return NetExceptionResponseFactory.error(
@ -111,7 +111,7 @@ public class Net_RefreshSession_Handler implements JsonMessageHandler {
); );
} }
if (solanaUser == null) { if (solanaUserEntry == null) {
return NetExceptionResponseFactory.error( return NetExceptionResponseFactory.error(
req, req,
WireCodes.Status.UNVERIFIED, WireCodes.Status.UNVERIFIED,
@ -171,7 +171,7 @@ public class Net_RefreshSession_Handler implements JsonMessageHandler {
// --- обновляем контекст соединения --- // --- обновляем контекст соединения ---
if (ctx != null) { if (ctx != null) {
ctx.setActiveSession(session); ctx.setActiveSession(session);
ctx.setSolanaUser(solanaUser); ctx.setSolanaUser(solanaUserEntry);
ctx.setSessionId(sessionId); ctx.setSessionId(sessionId);
ctx.setSessionPwd(sessionPwd); ctx.setSessionPwd(sessionPwd);
ctx.setAuthenticationStatus(ConnectionContext.AUTH_STATUS_USER); ctx.setAuthenticationStatus(ConnectionContext.AUTH_STATUS_USER);

View File

@ -0,0 +1,190 @@
package server.logic.ws_protocol.JSON.handlers.blockchain;
import blockchain.BchBlockEntry;
import blockchain.BodyRecordParser;
import blockchain.body.BodyRecord;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import server.logic.ws_protocol.JSON.ConnectionContext;
import server.logic.ws_protocol.JSON.entyties.Net_Request;
import server.logic.ws_protocol.JSON.entyties.Net_Response;
import server.logic.ws_protocol.JSON.entyties.Blockchain.Net_AddBlock_new_Request;
import server.logic.ws_protocol.JSON.entyties.Blockchain.Net_AddBlock_new_Response;
import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
import server.logic.ws_protocol.WireCodes;
import shine.db.dao.BlockchainStateDAO;
import shine.db.entities.BlockchainStateEntry;
import utils.crypto.BchCryptoVerifier;
import utils.files.FileStoreUtil;
import java.util.Base64;
public class Net_AddBlock_new_Handler implements JsonMessageHandler {
private static final Logger log = LoggerFactory.getLogger(Net_AddBlock_new_Handler.class);
@Override
public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) throws Exception {
Net_AddBlock_new_Request req = (Net_AddBlock_new_Request) baseReq;
// 0) базовые проверки
if (req.getBlockchainId() <= 0) {
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "BAD_BLOCKCHAIN_ID", "blockchainId <= 0");
}
if (req.getGlobalNumber() < 0) {
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "BAD_GLOBAL_NUMBER", "globalNumber < 0");
}
if (req.getLineNumber() < 0 || req.getLineNumber() > 7) {
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "BAD_LINE_NUMBER", "lineNumber must be 0..7");
}
if (req.getLineBlockNumber() < 0) {
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "BAD_LINE_BLOCK_NUMBER", "lineBlockNumber < 0");
}
if (req.getBlockBase64() == null || req.getBlockBase64().isBlank()) {
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "EMPTY_BLOCK", "blockBase64 is empty");
}
// 1) грузим состояние из БД
BlockchainStateDAO dao = BlockchainStateDAO.getInstance();
BlockchainStateEntry state = dao.getByBlockchainId(req.getBlockchainId());
if (state == null) {
// на MVP можно: запретить добавление, пока цепочка не создана отдельно
// либо разрешить только genesis/header как ты делал раньше
return NetExceptionResponseFactory.error(req, WireCodes.Status.CHAIN_NOT_FOUND, "CHAIN_NOT_FOUND", "chain not found in DB");
}
// 2) быстрые проверки на подходит ли блок
int expectedGlobal = state.getLastGlobalNumber() + 1;
int expectedLine = state.getLastLineNumber(req.getLineNumber()) + 1;
String dbPrevGlobalHash = nn(state.getLastGlobalHash());
String dbPrevLineHash = nn(state.getLastLineHash(req.getLineNumber()));
if (req.getGlobalNumber() != expectedGlobal) {
return outOfSeq(req, state, req.getLineNumber(), "OUT_OF_SEQUENCE_GLOBAL");
}
if (!eqHash(req.getPrevGlobalHash(), dbPrevGlobalHash)) {
return outOfSeq(req, state, req.getLineNumber(), "GLOBAL_HASH_MISMATCH");
}
if (req.getLineBlockNumber() != expectedLine) {
return outOfSeq(req, state, req.getLineNumber(), "OUT_OF_SEQUENCE_LINE");
}
if (!eqHash(req.getPrevLineHash(), dbPrevLineHash)) {
return outOfSeq(req, state, req.getLineNumber(), "LINE_HASH_MISMATCH");
}
// 3) декодируем блок
byte[] fullBlockBytes;
try {
fullBlockBytes = Base64.getUrlDecoder().decode(req.getBlockBase64());
} catch (IllegalArgumentException e) {
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "BAD_BASE64", "blockBase64 decode failed");
}
// 4) парсим .bch
BchBlockEntry block;
try {
block = new BchBlockEntry(fullBlockBytes);
} catch (Exception e) {
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "BAD_BLOCK_FORMAT", "cannot parse BchBlockEntry");
}
// 5) ПОЛНАЯ валидация: подпись/хэш/тело
// ниже я оставляю общий вызов verifyAll как у тебя раньше,
// но теперь prevHash берём из БД, а publicKey из state (или из solana_users).
byte[] prevHashGlobal32 = hexToBytes32(dbPrevGlobalHash);
boolean verified = BchCryptoVerifier.verifyAll(
state.getUserLogin(),
req.getBlockchainId(),
prevHashGlobal32,
block.rawBytes,
block.getSignature64(),
block.getHash32(),
Base64.getDecoder().decode(state.getPublicKeyBase64())
);
if (!verified) {
return NetExceptionResponseFactory.error(req, WireCodes.Status.UNVERIFIED, "UNVERIFIED", "signature/hash verification failed");
}
// Проверка тела блока
BodyRecord body = BodyRecordParser.parse(block.recordType, block.recordTypeVersion, block.body).check();
// 6) TODO: извлечь lineNumber/lineBlockNumber/prevLineHash из body (если они реально в теле есть)
// и сверить с req + DB. Сейчас оставляю как крючок.
// BlockLineMeta meta = BlockLineMetaExtractor.extract(body);
// if (meta.lineNumber != req.getLineNumber()) ...
// if (meta.lineBlockNumber != req.getLineBlockNumber()) ...
// if (!eqHash(meta.prevLineHashHex, dbPrevLineHash)) ...
// 7) запись в файл (фактическое хранение блоков)
FileStoreUtil.getInstance().addDataToBlockchain(req.getBlockchainId(), fullBlockBytes);
// 8) TODO: обновление состояния в БД (вместо BchInfoManager)
// - state.sizeBytes += fullBlockBytes.length
// - state.lastGlobalNumber = req.globalNumber
// - state.lastGlobalHash = bytesToHex(block.getHash32())
// - state.lineX_last_number/hash обновить по lineNumber
// - state.updatedAtMs = now
// dao.upsert(state);
// 9) ответ OK
Net_AddBlock_new_Response resp = new Net_AddBlock_new_Response();
resp.setOp(req.getOp());
resp.setRequestId(req.getRequestId());
resp.setStatus(WireCodes.Status.OK);
// можно вернуть новое состояние, но на MVP вернём хотя бы серверные lastы до апдейта/после апдейта
resp.setServerLastGlobalNumber(req.getGlobalNumber());
resp.setServerLastGlobalHash(bytesToHex(block.getHash32()));
resp.setServerLastLineNumber(req.getLineBlockNumber());
resp.setServerLastLineHash(resp.getServerLastGlobalHash());
resp.setReasonCode(null);
return resp;
}
private static Net_AddBlock_new_Response outOfSeq(Net_AddBlock_new_Request req, BlockchainStateEntry state, int line, String reason) {
Net_AddBlock_new_Response resp = new Net_AddBlock_new_Response();
resp.setOp(req.getOp());
resp.setRequestId(req.getRequestId());
resp.setStatus(WireCodes.Status.OUT_OF_SEQUENCE); // или свой статус
resp.setReasonCode(reason);
resp.setServerLastGlobalNumber(state.getLastGlobalNumber());
resp.setServerLastGlobalHash(nn(state.getLastGlobalHash()));
resp.setServerLastLineNumber(state.getLastLineNumber(line));
resp.setServerLastLineHash(nn(state.getLastLineHash(line)));
return resp;
}
private static boolean eqHash(String a, String b) {
return nn(a).equalsIgnoreCase(nn(b));
}
private static String nn(String s) { return s == null ? "" : s.trim(); }
private static byte[] hexToBytes32(String hex) {
hex = nn(hex);
if (hex.isEmpty()) return new byte[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);
if (out.length == 32) return out;
byte[] full = new byte[32];
int copy = Math.min(out.length, 32);
System.arraycopy(out, out.length - copy, full, 32 - copy, copy);
return full;
}
private static String bytesToHex(byte[] b) {
StringBuilder sb = new StringBuilder(b.length * 2);
for (byte x : b) sb.append(String.format("%02x", x));
return sb.toString();
}
}

View File

@ -11,7 +11,7 @@ import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory; import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
import server.logic.ws_protocol.WireCodes; import server.logic.ws_protocol.WireCodes;
import shine.db.dao.SolanaUsersDAO; import shine.db.dao.SolanaUsersDAO;
import shine.db.entities.SolanaUser; import shine.db.entities.SolanaUserEntry;
import java.sql.SQLException; import java.sql.SQLException;
@ -61,7 +61,7 @@ public class Net_AddUser_Handler implements JsonMessageHandler {
try { try {
SolanaUsersDAO dao = SolanaUsersDAO.getInstance(); SolanaUsersDAO dao = SolanaUsersDAO.getInstance();
SolanaUser user = new SolanaUser( SolanaUserEntry user = new SolanaUserEntry(
req.getLoginId(), req.getLoginId(),
req.getLogin(), req.getLogin(),
req.getBchId(), req.getBchId(),

View File

@ -5,8 +5,6 @@ import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.websocket.server.config.JettyWebSocketServletContainerInitializer; import org.eclipse.jetty.websocket.server.config.JettyWebSocketServletContainerInitializer;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import shine.db.dao.SolanaUsersDAO;
import shine.db.entities.SolanaUser;
import utils.config.AppConfig; import utils.config.AppConfig;
import java.time.Duration; import java.time.Duration;