Промежуточная версия
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())
throw new IllegalArgumentException("Text message is blank");
} }
// ------------------------------------------------------------ /** Создание из строки. */
// Конструктор 2 из строки (для создания нового сообщения)
// ------------------------------------------------------------
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,22 +38,22 @@ 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,
long sessionCreatedAtMs, long sessionCreatedAtMs,
long lastAuthirificatedAtMs, long lastAuthirificatedAtMs,
String pushEndpoint, String pushEndpoint,
String pushP256dhKey, String pushP256dhKey,
String pushAuthKey, String pushAuthKey,
String clientIp, String clientIp,
String clientInfoFromClient, String clientInfoFromClient,
String clientInfoFromRequest, String clientInfoFromRequest,
String userLanguage) { String userLanguage) {
this.sessionId = sessionId; this.sessionId = sessionId;
this.loginId = loginId; this.loginId = loginId;
this.sessionPwd = sessionPwd; this.sessionPwd = sessionPwd;

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,15 +19,15 @@ 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,
String deviceKey, String deviceKey,
Integer bchLimit) { Integer bchLimit) {
this.loginId = loginId; this.loginId = loginId;
this.login = login; this.login = login;
this.bchId = bchId; this.bchId = bchId;

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,16 +10,16 @@ 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,
long timeMs, long timeMs,
short pubkeyNum, short pubkeyNum,
String signature) { String signature) {
this.loginId = loginId; this.loginId = loginId;
this.param = param; this.param = param;
this.bchChannelId = bchChannelId; this.bchChannelId = bchChannelId;

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;