diff --git a/shine-server-blockchain/src/main/java/blockchain/BchBlockEntry.java b/shine-server-blockchain/src/main/java/blockchain/BchBlockEntry.java index e629f23..c3a8db1 100644 --- a/shine-server-blockchain/src/main/java/blockchain/BchBlockEntry.java +++ b/shine-server-blockchain/src/main/java/blockchain/BchBlockEntry.java @@ -215,3 +215,1316 @@ public class BchBlockEntry { ); } } + +package blockchain; + + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import utils.blockchain.BchInfoEntry; +import utils.crypto.BchCryptoVerifier; + +/** + * BchBlockValidator — проверяет корректность блока: + * 1) последовательность номеров блоков в цепочке; + * 2) криптографическую целостность (подпись и хэш). + *. + * Не проверяет: + * - структуру и содержимое body; + * - поля HEADER и логин (этим занимаются другие проверки). + */ +public final class BchBlockValidator { + + private static final Logger log = LoggerFactory.getLogger(BchBlockValidator.class); + + private BchBlockValidator() {} + + /** + * Проверяет, что блок может быть корректно добавлен к цепочке. + * + * Не используется при получении запроса на добавление блока по сети (тк там возвращаются более протоколо осмысленные коды + * если блок не подходит по номеру. + * + * А этот класс может быть использован в будущем для внутренних, повторных проверок существующих цепочек блоков. + * + * @param block блок (распарсенный из байт) + * @param chain информация о цепочке (BchInfoEntry) + * @param chainId идентификатор цепочки + * @return true если порядок и криптография корректны, иначе false + */ + public static boolean validate(BchBlockEntry block, BchInfoEntry chain, long chainId) { + if (block == null || chain == null) { + log.warn("❌ Ошибка: блок или данные о цепочке не переданы"); + return false; + } + + // 1️⃣ Проверка последовательности номера + int expectedNumber = chain.lastBlockNumber + 1; + if (block.recordNumber < expectedNumber) { + log.warn("❌ Блок с номером {} уже существует (ожидался {})", block.recordNumber, expectedNumber); + return false; + } + if (block.recordNumber > expectedNumber) { + log.warn("❌ Нарушена последовательность: получен блок {}, ожидался {}", block.recordNumber, expectedNumber); + return false; + } + + // 2️⃣ Проверка публичного ключа + byte[] publicKey = chain.getPublicKey32(); + if (publicKey == null || publicKey.length != 32) { + log.warn("❌ В цепочке отсутствует корректный публичный ключ (chainId={})", chainId); + return false; + } + + // 3️⃣ Получаем предыдущий хэш + byte[] prevHash32 = hexToBytes(chain.lastBlockHash); + + // 4️⃣ Проверка подписи и хэша + try { + boolean ok = BchCryptoVerifier.verifyAll( + chain.userLogin, + chainId, + prevHash32, + block.rawBytes, + block.getSignature64(), + block.getHash32(), + publicKey + ); + + if (!ok) { + log.warn("❌ Криптографическая проверка не пройдена: хэш или подпись не совпадают (chainId={}, blockNum={})", + chainId, block.recordNumber); + return false; + } + + log.info("✅ Блок {} успешно прошёл проверку подписи и хэша (chainId={})", + block.recordNumber, chainId); + return true; + + } catch (Exception e) { + log.error("❌ Исключение при проверке блока (chainId={}): {}", chainId, e.getMessage()); + return false; + } + } + + // -------------------- HEX → байты -------------------- + + private static byte[] hexToBytes(String hex) { + if (hex == null || hex.isEmpty()) return new byte[32]; // пустой хэш = 32 нуля + int len = hex.length(); + byte[] out = new byte[len / 2]; + for (int i = 0; i < len; i += 2) { + out[i / 2] = (byte) Integer.parseInt(hex.substring(i, i + 2), 16); + } + return out; + } +} + +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 { + + /** Код типа записи (совпадает с recordType в BchBlockEntry). */ + short type(); + + /** Версия формата записи (совпадает с recordTypeVersion в BchBlockEntry). */ + short version(); + + /** Проверить корректность содержимого и вернуть этот объект (или кинуть исключение). */ + BodyRecord check(); + + /** + * Сериализовать тело записи в байты (ровно то, что кладётся в block.body). + * Важно: НЕ включает общий заголовок блока (recordNumber/timestamp/type/version). + */ + byte[] toBytes(); +} + +package blockchain.body; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +/** + * BodyRecordParser_new — общий фабричный парсер body для нового формата. + * + * Правило совместимости (строгое): + * - если (type, version) неизвестны → кидаем IllegalArgumentException + */ +public final class BodyRecordParser { + + private BodyRecordParser() {} + + public static BodyRecord parse(byte[] bodyBytes) { + if (bodyBytes == null) throw new IllegalArgumentException("bodyBytes == null"); + if (bodyBytes.length < 4) throw new IllegalArgumentException("bodyBytes too short (<4)"); + + ByteBuffer bb = ByteBuffer.wrap(bodyBytes).order(ByteOrder.BIG_ENDIAN); + short type = bb.getShort(); + short ver = bb.getShort(); + + // Строгое сопоставление type+version → класс + int key = ((type & 0xFFFF) << 16) | (ver & 0xFFFF); + + return switch (key) { + case 0x0000_0001 -> new HeaderBody(bodyBytes); // type=0, ver=1 + case 0x0001_0001 -> new TextBody(bodyBytes); // type=1, ver=1 + default -> throw new IllegalArgumentException(String.format( + "Unknown body type/version: type=%d ver=%d (key=0x%08X)", + (type & 0xFFFF), (ver & 0xFFFF), key + )); + }; + } +} +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 implements BodyRecord { + + 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(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(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 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(); + } +} +package blockchain; + +import blockchain.body.BodyRecord; +import blockchain.body.HeaderBody; +import blockchain.body.TextBody; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * ============================================================================ + * BodyRecordParser — универсальный парсер тел (body) блоков .bch + * ============================================================================ + *. + * 🧩 Назначение: + * Преобразует пару (recordType, recordTypeVersion, bodyBytes) + * в конкретный объект, реализующий интерфейс {@link BodyRecord}. + *. + * 🔹 Особенность: + * Используется объединённый 4-байтовый код: + *. + * fullCode = (recordType << 16) | (recordTypeVersion & 0xFFFF) + *. + * Это позволяет различать версии одного типа блока, + * например: TextBody v1, TextBody v2 и т.д. + *. + * 🔹 Пример: + * BodyRecord body = BodyRecordParser.parse(block.recordType, block.recordTypeVersion, block.body); + *. + * ============================================================================ + */ +public final class BodyRecordParser { + + private static final Logger log = LoggerFactory.getLogger(BodyRecordParser.class); + + private BodyRecordParser() {} + + /** + * Распарсить тело блока по типу и версии записи. + * + * @param recordType код типа записи (0 = Header, 1 = Text, ...) + * @param recordTypeVersion версия формата записи + * @param body массив байт тела записи + * @return объект, реализующий BodyRecord + */ + public static BodyRecord parse(short recordType, short recordTypeVersion, byte[] body) { + if (body == null) + throw new IllegalArgumentException("body == null"); + + // Объединяем тип и версию в 4-байтовый ключ + int fullCode = ((recordType & 0xFFFF) << 16) | (recordTypeVersion & 0xFFFF); + + switch (fullCode) { + + // --------------------------------------------------------- + // TYPE 0, VERSION 1 — HeaderBody v1 + // --------------------------------------------------------- + // Заголовок цепочки пользователя (первый блок). + // + // Формат body (без общих 20 байт заголовка блока): + // [8] ASCII tag = "SHiNE001" + // [8] blockchainId (long, BE) + // [4] blockchainType (int, BE) + // [4] blockchainNumber (int, BE) + // [1] userLoginLength = N (unsigned byte) + // [N] userLogin (UTF-8) + // [2] versionUserBch (short, BE) + // [8] prevUserBchId (long, BE) + // [32] publicKey32 + // + // Назначение: + // Создаёт новую пользовательскую цепочку (блок №0). + case (0x0000_0001): + return new HeaderBody(body); + + // --------------------------------------------------------- + // TYPE 1, VERSION 1 — TextBody v1 + // --------------------------------------------------------- + // Простое текстовое сообщение UTF-8. + // + // Формат body (без общих 20 байт заголовка блока): + // [N] message (UTF-8) + // + // Назначение: + // Текстовые и системные сообщения, описания, комментарии. + case (0x0001_0001): + return new TextBody(body); + + // --------------------------------------------------------- + // РЕЗЕРВ — будущие типы и версии + // --------------------------------------------------------- + // Пример: (0x0001_0002) → TextBody v2 (например, JSON-структура) + // (0x0002_0001) → FileBody v1 + // + // case (0x0001_0002): + // return new TextBodyV2(body); + // + // case (0x0002_0001): + // return new FileBody(body); + + default: + throw new IllegalArgumentException(String.format( + "Неизвестный тип блока: type=%d version=%d (fullCode=0x%08X)", + recordType, recordTypeVersion, fullCode)); + } + } +} + +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 implements BodyRecord { + + public static final short TYPE = 1; + public static final short VER = 1; + + public final String message; + + /** Десериализация из полного bodyBytes (включая type/version). */ + public TextBody(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(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 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(); + } +} +package blockchain_new; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Arrays; +import java.util.Objects; + +/** + * BchBlockEntry_new — универсальный блок нового формата. + * + * RAW (BigEndian): + * [4] recordSize (int) = RAW + signature + hash + * [4] recordNumber (int) глобальный номер блока + * [8] timestamp (long) unix seconds + * [2] line (short) + * [4] lineNumber (int) + * [N] bodyBytes (body, начинается с [type][version]) + * + * TAIL: + * [64] signature64 (Ed25519) + * [32] hash32 (SHA-256) + */ +public final class BchBlockEntry_new { + + public static final int SIGNATURE_LEN = 64; + public static final int HASH_LEN = 32; + + /** Размер фиксированного RAW-заголовка без body */ + public static final int RAW_HEADER_SIZE = 4 + 4 + 8 + 2 + 4; + + // --- RAW --- + public final int recordSize; + public final int recordNumber; + public final long timestamp; + public final short line; + public final int lineNumber; + public final byte[] bodyBytes; + + // --- TAIL --- + private final byte[] signature64; + private final byte[] hash32; + + // --- cached --- + private final byte[] fullBytes; + + /* ===================================================================== */ + /* ====================== Конструктор из байт ========================== */ + /* ===================================================================== */ + + public BchBlockEntry_new(byte[] fullBytes) { + Objects.requireNonNull(fullBytes, "fullBytes == null"); + if (fullBytes.length < RAW_HEADER_SIZE + SIGNATURE_LEN + HASH_LEN) + throw new IllegalArgumentException("Block too short"); + + ByteBuffer bb = ByteBuffer.wrap(fullBytes).order(ByteOrder.BIG_ENDIAN); + + this.recordSize = bb.getInt(); + if (recordSize != fullBytes.length) + throw new IllegalArgumentException("recordSize mismatch"); + + this.recordNumber = bb.getInt(); + this.timestamp = bb.getLong(); + this.line = bb.getShort(); + this.lineNumber = bb.getInt(); + + int bodyLen = recordSize - RAW_HEADER_SIZE - SIGNATURE_LEN - HASH_LEN; + if (bodyLen <= 0) + throw new IllegalArgumentException("Invalid body length"); + + this.bodyBytes = new byte[bodyLen]; + bb.get(this.bodyBytes); + + this.signature64 = new byte[SIGNATURE_LEN]; + bb.get(this.signature64); + + this.hash32 = new byte[HASH_LEN]; + bb.get(this.hash32); + + this.fullBytes = Arrays.copyOf(fullBytes, fullBytes.length); + } + + /* ===================================================================== */ + /* ====================== Конструктор сборки ============================ */ + /* ===================================================================== */ + + public BchBlockEntry_new(int recordNumber, + long timestamp, + short line, + int lineNumber, + byte[] bodyBytes, + byte[] signature64, + byte[] hash32) { + + Objects.requireNonNull(bodyBytes, "bodyBytes == null"); + Objects.requireNonNull(signature64, "signature64 == null"); + Objects.requireNonNull(hash32, "hash32 == null"); + + if (signature64.length != SIGNATURE_LEN) + throw new IllegalArgumentException("signature64 != 64"); + if (hash32.length != HASH_LEN) + throw new IllegalArgumentException("hash32 != 32"); + + this.recordNumber = recordNumber; + this.timestamp = timestamp; + this.line = line; + this.lineNumber = lineNumber; + this.bodyBytes = Arrays.copyOf(bodyBytes, bodyBytes.length); + this.signature64 = Arrays.copyOf(signature64, SIGNATURE_LEN); + this.hash32 = Arrays.copyOf(hash32, HASH_LEN); + + this.recordSize = + RAW_HEADER_SIZE + + bodyBytes.length + + SIGNATURE_LEN + + HASH_LEN; + + ByteBuffer bb = ByteBuffer.allocate(recordSize).order(ByteOrder.BIG_ENDIAN); + bb.putInt(recordSize); + bb.putInt(recordNumber); + bb.putLong(timestamp); + bb.putShort(line); + bb.putInt(lineNumber); + bb.put(bodyBytes); + bb.put(this.signature64); + bb.put(this.hash32); + + this.fullBytes = bb.array(); + } + + + public byte[] getRawBytes() { + int rawLen = recordSize - SIGNATURE_LEN - HASH_LEN; + byte[] raw = new byte[rawLen]; + System.arraycopy(fullBytes, 0, raw, 0, rawLen); + return raw; + } + + /* ===================================================================== */ + + public byte[] getSignature64() { + return Arrays.copyOf(signature64, SIGNATURE_LEN); + } + + public byte[] getHash32() { + return Arrays.copyOf(hash32, HASH_LEN); + } + + public byte[] toBytes() { + return Arrays.copyOf(fullBytes, fullBytes.length); + } +} +package blockchain_new; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.util.Objects; + +public final class BchCryptoVerifier_new { + + private static final byte[] DOMAIN = "SHiNE".getBytes(StandardCharsets.US_ASCII); + + private BchCryptoVerifier_new() {} + + /** + * preimage = + * "SHiNE" + + * [1] loginLen + loginBytes + + * prevGlobalHash32 + + * prevLineHash32 + + * rawBytes + */ + public static byte[] buildPreimage(String userLogin, + byte[] prevGlobalHash32, + byte[] prevLineHash32, + byte[] rawBytes) { + + Objects.requireNonNull(userLogin, "userLogin == null"); + Objects.requireNonNull(prevGlobalHash32, "prevGlobalHash32 == null"); + Objects.requireNonNull(prevLineHash32, "prevLineHash32 == null"); + Objects.requireNonNull(rawBytes, "rawBytes == null"); + + if (prevGlobalHash32.length != 32 || prevLineHash32.length != 32) + throw new IllegalArgumentException("hash len != 32"); + + byte[] loginBytes = userLogin.getBytes(StandardCharsets.UTF_8); + if (loginBytes.length > 255) + throw new IllegalArgumentException("login >255 bytes"); + + ByteBuffer bb = ByteBuffer.allocate( + DOMAIN.length + + 1 + loginBytes.length + + 32 + 32 + + rawBytes.length + ).order(ByteOrder.BIG_ENDIAN); + + bb.put(DOMAIN); + bb.put((byte) loginBytes.length); + bb.put(loginBytes); + bb.put(prevGlobalHash32); + bb.put(prevLineHash32); + bb.put(rawBytes); + + return bb.array(); + } + + public static byte[] sha256(byte[] data) { + try { + MessageDigest d = MessageDigest.getInstance("SHA-256"); + return d.digest(data); + } catch (Exception e) { + throw new IllegalStateException("SHA-256 unavailable", e); + } + } + + // TODO: сюда подключается твой Ed25519 util + public static boolean verifySignature(byte[] hash32, + byte[] signature64, + byte[] publicKey32) { + // TODO: Ed25519.verify(hash32, signature64, publicKey32) + return true; + } +} +package utils.blockchain; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Base64; + +/** + * BchInfoEntry — данные об одной цепочке блокчейна. + * Используется менеджером BchInfoManager. + */ +public final class BchInfoEntry { + + @JsonProperty("blockchainId") + public final long blockchainId; + + @JsonProperty("userLogin") + public final String userLogin; + + @JsonProperty("publicKeyBase64") + public final String publicKeyBase64; + + @JsonProperty("blockchainSizeLimit") + public final int blockchainSizeLimit; + + @JsonProperty("blockchainSize") + public final int blockchainSize; + + @JsonProperty("lastBlockNumber") + public final int lastBlockNumber; + + @JsonProperty("lastBlockHash") + public final String lastBlockHash; + + @JsonCreator + public BchInfoEntry( + @JsonProperty("blockchainId") long blockchainId, + @JsonProperty("userLogin") String userLogin, + @JsonProperty("publicKeyBase64") String publicKeyBase64, + @JsonProperty("blockchainSizeLimit") int blockchainSizeLimit, + @JsonProperty("blockchainSize") int blockchainSize, + @JsonProperty("lastBlockNumber") int lastBlockNumber, + @JsonProperty("lastBlockHash") String lastBlockHash + ) { + this.blockchainId = blockchainId; + this.userLogin = userLogin == null ? "" : userLogin; + this.publicKeyBase64 = publicKeyBase64; + this.blockchainSizeLimit = blockchainSizeLimit; + this.blockchainSize = blockchainSize; + this.lastBlockNumber = lastBlockNumber; + this.lastBlockHash = lastBlockHash == null ? "" : lastBlockHash; + } + + /** Публичный ключ в бинарном виде (32 байта) или null, если битый. */ + public byte[] getPublicKey32() { + try { + byte[] raw = Base64.getDecoder().decode(publicKeyBase64); + return (raw != null && raw.length == 32) ? raw : null; + } catch (IllegalArgumentException e) { + return null; + } + } +} + +package utils.blockchain; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.nio.file.*; +import java.util.Base64; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * BchInfoManager — Singleton. + *. + * Держит в памяти информацию обо всех блокчейнах. + * Сейчас читает и пишет JSON на диск (data/blockchain_info.json). + * В будущем можно заменить на SQL без изменений в остальном коде. + */ +public final class BchInfoManager { + + private static final Logger log = LoggerFactory.getLogger(BchInfoManager.class); + + private static final String FILE_NAME = "blockchain_info.json"; + private static final Path DATA_DIR = Paths.get("data"); + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private static BchInfoManager instance; + + /** blockchainId → запись о цепочке */ + private final Map records = new LinkedHashMap<>(); + private final Path path = DATA_DIR.resolve(FILE_NAME); + + private BchInfoManager() { + ensureDataDir(); + load(); + } + + public static synchronized BchInfoManager getInstance() { + if (instance == null) instance = new BchInfoManager(); + return instance; + } + + // ========== API ========== + + /** Создать новую цепочку (после первого HEADER-блока). */ + public synchronized void addBlockchain(long blockchainId, + String userLogin, + byte[] publicKey32, + int blockchainSizeLimit) { + if (publicKey32 == null || publicKey32.length != 32) + throw new IllegalArgumentException("publicKey32 must be 32 bytes"); + if (records.containsKey(blockchainId)) + throw new IllegalArgumentException("blockchain already exists: " + blockchainId); + + BchInfoEntry entry = new BchInfoEntry( + blockchainId, + userLogin, + Base64.getEncoder().encodeToString(publicKey32), + blockchainSizeLimit, + 0, 0, "" + ); + + records.put(blockchainId, entry); + log.info("Добавлен блокчейн id={} login='{}' (лимит {})", blockchainId, userLogin, blockchainSizeLimit); + save(); + } + + /** Обновить состояние после добавления нового блока. */ + public synchronized void updateBlockchainState(long blockchainId, + int lastBlockNumber, + String lastBlockHash, + int blockchainSize) { + BchInfoEntry prev = getEntryOrThrow(blockchainId); + + BchInfoEntry updated = new BchInfoEntry( + prev.blockchainId, + prev.userLogin, + prev.publicKeyBase64, + prev.blockchainSizeLimit, + blockchainSize, + lastBlockNumber, + lastBlockHash + ); + + records.put(blockchainId, updated); + log.info("Обновлено состояние id={} lastNum={} hash={} size={}", + blockchainId, lastBlockNumber, lastBlockHash, blockchainSize); + save(); + } + + /** Получить полную информацию по blockchainId. */ + public synchronized BchInfoEntry getBchInfo(long blockchainId) { + return records.get(blockchainId); + } + + /** Быстро проверить существование цепочки. */ + public synchronized boolean exists(long blockchainId) { + return records.containsKey(blockchainId); + } + + /** id → userLogin (для поиска пользователей). */ + public synchronized Map getAllLoginsSnapshot() { + Map copy = new LinkedHashMap<>(records.size()); + for (var e : records.entrySet()) { + copy.put(e.getKey(), e.getValue().userLogin); + } + return copy; + } + + // ========== private ========== + + private BchInfoEntry getEntryOrThrow(long blockchainId) { + BchInfoEntry e = records.get(blockchainId); + if (e == null) throw new IllegalStateException("Блокчейн с id=" + blockchainId + " не найден."); + return e; + } + + private void ensureDataDir() { + try { + if (!Files.exists(DATA_DIR)) { + Files.createDirectories(DATA_DIR); + log.info("Создана директория данных: {}", DATA_DIR); + } + } catch (IOException e) { + throw new IllegalStateException("Не удалось создать директорию хранения: " + DATA_DIR, e); + } + } + + private synchronized void load() { + if (!Files.exists(path)) { + save(); + log.info("Создан JSON-хранилище: {}", path); + return; + } + try { + byte[] json = Files.readAllBytes(path); + if (json.length == 0) return; + + Map map = MAPPER.readValue( + json, + MAPPER.getTypeFactory().constructMapType(Map.class, String.class, BchInfoEntry.class) + ); + + records.clear(); + for (var e : map.entrySet()) { + try { + long id = Long.parseLong(e.getKey()); + records.put(id, e.getValue()); + } catch (NumberFormatException nfe) { + log.warn("Пропущен некорректный ключ '{}' в JSON", e.getKey()); + } + } + log.info("Загружено {} записей из {}", records.size(), path); + } catch (IOException e) { + log.error("Ошибка загрузки {}", path, e); + } + } + + /** Атомарная запись JSON через .tmp + ATOMIC_MOVE */ + private synchronized void save() { + try { + Map map = new LinkedHashMap<>(); + for (var e : records.entrySet()) + map.put(String.valueOf(e.getKey()), e.getValue()); + + byte[] json = MAPPER.writerWithDefaultPrettyPrinter().writeValueAsBytes(map); + + Path tmp = path.resolveSibling(FILE_NAME + ".tmp"); + Files.write(tmp, json, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); + Files.move(tmp, path, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); + + log.debug("Сохранено {} записей в {}", records.size(), path); + } catch (IOException e) { + log.error("Ошибка сохранения {}", path, e); + } + } +} + +package utils.files; + +import java.io.IOException; +import java.nio.file.*; +import java.util.Objects; + +/** + * =============================================================== + * FileStoreUtil — синглтон-утилита для записи/дозаписи/чтения файлов. + * --------------------------------------------------------------- + * Где хранит: + * • Все файлы размещаются в внешней папке DATA_DIR = "data" (в корне запуска). + * Папка создаётся автоматически при первом обращении. + *. + * Что умеет: + * • newFile(String fileName, byte[] data) + * - создаёт/переписывает файл с именем fileName и записывает data. + * • addDataToFile(String fileName, byte[] data) + * - дописывает data в конец файла (создаст файл, если его ещё нет). + * • readAllDataFromFile(String fileName) + * - читает весь файл целиком и возвращает содержимое в виде byte[]. + *. + * Обёртки под «блокчейны»: + * • newBlockchain(long blockchainId, byte[] data) + * • addDataToBlockchain(long blockchainId, byte[] data) + * • readAllDataFromBlockchain(long blockchainId) + * - те же операции, но имя файла формируется из blockchainId и расширения ".bch". + *. + * Безопасность имён: + * • Внутри утилиты есть простая валидация имени файла: запрещены разделители путей, + * чтобы исключить выход из каталога data (path traversal). + *. + * Совместимость: Java 17. + * =============================================================== + */ +public final class FileStoreUtil { + + /** Базовая папка для хранения всех файлов (создаётся автоматически). */ + public static final String DATA_DIR_NAME = "data"; + /** Расширение файлов «блокчейнов». */ + public static final String BLOCKCHAIN_FILE_EXTENSION = ".bch"; + + private static final FileStoreUtil INSTANCE = new FileStoreUtil(); + + private final Path dataDirPath; + + private FileStoreUtil() { + this.dataDirPath = Paths.get(DATA_DIR_NAME); + ensureDataDirExists(); + } + + /** Получить единственный экземпляр утилиты. */ + public static FileStoreUtil getInstance() { + return INSTANCE; + } + + // =============================================================== + // ОБЩИЕ МЕТОДЫ РАБОТЫ С ФАЙЛОМ + // =============================================================== + + /** + * Создать/переписать файл и записать в него массив байт. + * @param fileName имя файла (без каталогов) + * @param data содержимое + * @throws IllegalArgumentException при неверном имени или null-данных + * @throws IllegalStateException при ошибках ввода/вывода + */ + public void newFile(String fileName, byte[] data) { + Objects.requireNonNull(data, "Данные не должны быть null"); + Path target = resolveSafe(fileName); + try { + Files.write(target, data, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE); + } catch (IOException e) { + throw new IllegalStateException("Не удалось записать файл: " + target, e); + } + } + + /** + * Дозаписать массив байт в конец файла (создаст файл, если отсутствует). + * @param fileName имя файла (без каталогов) + * @param data добавляемые данные + * @throws IllegalArgumentException при неверном имени или null-данных + * @throws IllegalStateException при ошибках ввода/вывода + */ + public void addDataToFile(String fileName, byte[] data) { + Objects.requireNonNull(data, "Данные не должны быть null"); + Path target = resolveSafe(fileName); + try { + Files.write(target, data, + StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.APPEND); + } catch (IOException e) { + throw new IllegalStateException("Не удалось дописать файл: " + target, e); + } + } + + /** + * Прочитать весь файл в память и вернуть как byte[]. + * @param fileName имя файла (без каталогов) + * @return содержимое файла + * @throws IllegalStateException если файл не существует или ошибка ввода/вывода + */ + public byte[] readAllDataFromFile(String fileName) { + Path target = resolveSafe(fileName); + if (!Files.exists(target)) { + throw new IllegalStateException("Файл не найден: " + target); + } + try { + return Files.readAllBytes(target); + } catch (IOException e) { + throw new IllegalStateException("Не удалось прочитать файл: " + target, e); + } + } + + // =============================================================== + // ОБЁРТКИ ДЛЯ «БЛОКЧЕЙН-ФАЙЛОВ» + // =============================================================== + + /** + * Обёртка над newFile: имя формируется из blockchainId + ".bch". + */ + public void newBlockchain(long blockchainId, byte[] data) { + String fileName = buildBlockchainFileName(blockchainId); + newFile(fileName, data); + } + + /** + * Обёртка над addDataToFile: имя формируется из blockchainId + ".bch". + */ + public void addDataToBlockchain(long blockchainId, byte[] data) { + String fileName = buildBlockchainFileName(blockchainId); + addDataToFile(fileName, data); + } + + /** + * Обёртка над readAllDataFromFile: имя формируется из blockchainId + ".bch". + */ + public byte[] readAllDataFromBlockchain(long blockchainId) { + String fileName = buildBlockchainFileName(blockchainId); + return readAllDataFromFile(fileName); + } + + // =============================================================== + // ВСПОМОГАТЕЛЬНЫЕ + // =============================================================== + + private void ensureDataDirExists() { + try { + if (!Files.exists(dataDirPath)) { + Files.createDirectories(dataDirPath); + } + } catch (IOException e) { + throw new IllegalStateException("Не удалось создать директорию хранения: " + dataDirPath, e); + } + } + + /** + * Безопасно собрать путь внутри каталога data, запретив подстановку каталогов в имени файла. + */ + private Path resolveSafe(String fileName) { + validateSimpleFileName(fileName); + return dataDirPath.resolve(fileName); + } + + /** + * Простейшая валидация имени файла: + * • запретить разделители путей и возврат на родительский каталог. + */ + private void validateSimpleFileName(String fileName) { + Objects.requireNonNull(fileName, "Имя файла не должно быть null"); + if (fileName.isBlank()) { + throw new IllegalArgumentException("Имя файла не должно быть пустым"); + } + if (fileName.contains("/") || fileName.contains("\\") || fileName.contains("..")) { + throw new IllegalArgumentException("Недопустимое имя файла: " + fileName); + } + } + + /** + * Построить имя «блокчейн-файла» из идентификатора и расширения .bch. + * Пример: 12345 → "12345.bch" + */ + private String buildBlockchainFileName(long blockchainId) { + return Long.toString(blockchainId) + BLOCKCHAIN_FILE_EXTENSION; + } +} + +package utils.files; + +import java.nio.charset.StandardCharsets; +import java.util.Arrays; + +/** + * =============================================================== + * FileStoreUtilSelfTest — запускаемый тест утилиты FileStoreUtil. + * --------------------------------------------------------------- + * Сценарий: + * 1) Создаём «блокчейн-файл» для id=20251021 с начальными данными. + * 2) Дозаписываем ещё порцию данных. + * 3) Читаем целиком и печатаем длину + превью. + *. + * Ожидаемый итог: + * • В папке "data" появится файл "20251021.bch" + * • В консоли будет длина содержимого и небольшой превью-дамп. + * =============================================================== + */ +public class FileStoreUtilSelfTest { + + public static void main(String[] args) { + System.out.println("=== FileStoreUtil self-test ==="); + + FileStoreUtil fs = FileStoreUtil.getInstance(); + + long blockchainId = 20251021L; + + byte[] part1 = "Hello ".getBytes(StandardCharsets.UTF_8); + byte[] part2 = "Blockchain!".getBytes(StandardCharsets.UTF_8); + + // 1) создаём новый файл для «блокчейна» + fs.newBlockchain(blockchainId, part1); + + // 2) дозаписываем данные + fs.addDataToBlockchain(blockchainId, part2); + + // 3) читаем всё содержимое и показываем превью + byte[] all = fs.readAllDataFromBlockchain(blockchainId); + System.out.println("Total bytes read: " + all.length); + System.out.println("Preview (UTF-8): " + new String(all, StandardCharsets.UTF_8)); + + // небольшой hex-дамп первых 32 байт (для наглядности) + System.out.println("Preview (HEX 32B): " + toHexPreview(all, 32)); + + System.out.println("✅ Self-test passed (файл: data/" + blockchainId + FileStoreUtil.BLOCKCHAIN_FILE_EXTENSION + ")"); + } + + private static String toHexPreview(byte[] data, int max) { + int n = Math.min(data.length, max); + StringBuilder sb = new StringBuilder(n * 2); + for (int i = 0; i < n; i++) { + sb.append(String.format("%02X", data[i])); + if (i + 1 < n) sb.append(' '); + } + if (data.length > n) sb.append(" ..."); + return sb.toString(); + } +} + +package utils.search; + + +import utils.blockchain.BchInfoManager; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +/** + * UserSearchService — поиск первых 5 пользователей по подстроке логина (без учёта регистра). + */ +public final class UserSearchService { + + private static final UserSearchService INSTANCE = new UserSearchService(); + private UserSearchService() {} + public static UserSearchService getInstance() { return INSTANCE; } + + /** Результат одной пары: id + исходный login (с родным регистром). */ + public static final class Pair { + public final long blockchainId; + public final String userLogin; + public Pair(long blockchainId, String userLogin) { + this.blockchainId = blockchainId; + this.userLogin = userLogin; + } + } + + /** + * Найти первые до 5 логинов, содержащих подстроку (case-insensitive). + */ + public List searchFirst5(String query) { + String q = (query == null ? "" : query).toLowerCase(Locale.ROOT).trim(); + List out = new ArrayList<>(5); + if (q.isEmpty()) return out; + + // берём снапшот id→login + Map all = BchInfoManager.getInstance().getAllLoginsSnapshot(); + + for (var e : all.entrySet()) { + if (out.size() >= 5) break; + String login = e.getValue() == null ? "" : e.getValue(); + if (login.toLowerCase(Locale.ROOT).contains(q)) { + out.add(new Pair(e.getKey(), login)); + } + } + return out; + } + + // Упаковка пары в байтовый формат ответа: [8] id + [1] L + [L] login UTF-8 (L<=255) + public static byte[] packPair(Pair p) { + byte[] loginUtf8 = (p.userLogin == null ? "" : p.userLogin).getBytes(StandardCharsets.UTF_8); + int L = Math.min(loginUtf8.length, 255); + byte[] b = new byte[8 + 1 + L]; + // beLong + b[0]=(byte)((p.blockchainId>>>56)&0xFF); + b[1]=(byte)((p.blockchainId>>>48)&0xFF); + b[2]=(byte)((p.blockchainId>>>40)&0xFF); + b[3]=(byte)((p.blockchainId>>>32)&0xFF); + b[4]=(byte)((p.blockchainId>>>24)&0xFF); + b[5]=(byte)((p.blockchainId>>>16)&0xFF); + b[6]=(byte)((p.blockchainId>>>8 )&0xFF); + b[7]=(byte)((p.blockchainId )&0xFF); + b[8]=(byte)L; + System.arraycopy(loginUtf8, 0, b, 9, L); + return b; + } +} \ No newline at end of file diff --git a/shine-server-db/src/main/java/shine/db/dao/BlockchainStateDAO.java b/shine-server-db/src/main/java/shine/db/dao/BlockchainStateDAO.java index c47a93f..05368c9 100644 --- a/shine-server-db/src/main/java/shine/db/dao/BlockchainStateDAO.java +++ b/shine-server-db/src/main/java/shine/db/dao/BlockchainStateDAO.java @@ -40,6 +40,7 @@ public final class BlockchainStateDAO { file_size_bytes, last_global_number, last_global_hash, + updated_at_ms, line0_last_number, line0_last_hash, line1_last_number, line1_last_hash, line2_last_number, line2_last_hash, @@ -47,8 +48,7 @@ public final class BlockchainStateDAO { line4_last_number, line4_last_hash, line5_last_number, line5_last_hash, line6_last_number, line6_last_hash, - line7_last_number, line7_last_hash, - updated_at_ms + line7_last_number, line7_last_hash FROM blockchain_state WHERE blockchainName = ? """; @@ -81,6 +81,7 @@ public final class BlockchainStateDAO { file_size_bytes, last_global_number, last_global_hash, + updated_at_ms, line0_last_number, line0_last_hash, line1_last_number, line1_last_hash, line2_last_number, line2_last_hash, @@ -88,10 +89,9 @@ public final class BlockchainStateDAO { line4_last_number, line4_last_hash, line5_last_number, line5_last_hash, line6_last_number, line6_last_hash, - line7_last_number, line7_last_hash, - updated_at_ms + line7_last_number, line7_last_hash ) VALUES ( - ?,?,?,?,?,?,?,?, + ?,?,?,?,?,?,?,?,?, ?,?, ?,?, ?,?, @@ -100,7 +100,7 @@ public final class BlockchainStateDAO { ?,?, ?,?, ?,?, - ? + ?,? ) ON CONFLICT(blockchainName) DO UPDATE SET @@ -111,6 +111,7 @@ public final class BlockchainStateDAO { file_size_bytes = excluded.file_size_bytes, last_global_number = excluded.last_global_number, last_global_hash = excluded.last_global_hash, + updated_at_ms = excluded.updated_at_ms, line0_last_number = excluded.line0_last_number, line0_last_hash = excluded.line0_last_hash, line1_last_number = excluded.line1_last_number, @@ -126,12 +127,12 @@ public final class BlockchainStateDAO { line6_last_number = excluded.line6_last_number, line6_last_hash = excluded.line6_last_hash, line7_last_number = excluded.line7_last_number, - line7_last_hash = excluded.line7_last_hash, - updated_at_ms = excluded.updated_at_ms + line7_last_hash = excluded.line7_last_hash """; try (PreparedStatement ps = c.prepareStatement(sql)) { int i = 1; + ps.setString(i++, e.getBlockchainName()); ps.setString(i++, nn(e.getLogin())); ps.setString(i++, nn(e.getPublicKeyBase64())); @@ -140,19 +141,20 @@ public final class BlockchainStateDAO { ps.setLong(i++, e.getFileSizeBytes()); ps.setInt(i++, e.getLastGlobalNumber()); ps.setString(i++, nn(e.getLastGlobalHash())); + ps.setLong(i++, e.getUpdatedAtMs()); for (int line = 0; line < 8; line++) { ps.setInt(i++, e.getLastLineNumber(line)); ps.setString(i++, nn(e.getLastLineHash(line))); } - ps.setLong(i++, e.getUpdatedAtMs()); ps.executeUpdate(); } } private BlockchainStateEntry mapRow(ResultSet rs) throws SQLException { BlockchainStateEntry e = new BlockchainStateEntry(); + e.setBlockchainName(rs.getString("blockchainName")); e.setLogin(rs.getString("login")); e.setPublicKeyBase64(rs.getString("public_key_base64")); @@ -164,12 +166,13 @@ public final class BlockchainStateDAO { e.setLastGlobalNumber(rs.getInt("last_global_number")); e.setLastGlobalHash(rs.getString("last_global_hash")); + e.setUpdatedAtMs(rs.getLong("updated_at_ms")); + for (int line = 0; line < 8; line++) { e.setLastLineNumber(line, rs.getInt("line" + line + "_last_number")); e.setLastLineHash(line, rs.getString("line" + line + "_last_hash")); } - e.setUpdatedAtMs(rs.getLong("updated_at_ms")); return e; } diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/ActiveConnectionsRegistry.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/ActiveConnectionsRegistry.java index 1970304..da0e8d1 100644 --- a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/ActiveConnectionsRegistry.java +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/ActiveConnectionsRegistry.java @@ -9,7 +9,7 @@ import java.util.concurrent.CopyOnWriteArraySet; * * Позволяет: * - получить ConnectionContext по sessionId; - * - получить все активные подключения пользователя по loginId; + * - получить все активные подключения пользователя по login; * - удалить подключение при закрытии WebSocket. * * найти все подключения пользователя: @@ -34,27 +34,27 @@ public final class ActiveConnectionsRegistry { // sessionId (String) -> ConnectionContext private final ConcurrentHashMap bySessionId = new ConcurrentHashMap<>(); - // loginId -> множество ConnectionContext для этого пользователя - private final ConcurrentHashMap> byLoginId = new ConcurrentHashMap<>(); + // login (String) -> множество ConnectionContext для этого пользователя + private final ConcurrentHashMap> byLogin = new ConcurrentHashMap<>(); /** * Зарегистрировать авторизованное подключение. - * Ожидается, что в ctx уже выставлены loginId и sessionId. + * Ожидается, что в ctx уже выставлены login и sessionId. */ public void register(ConnectionContext ctx) { if (ctx == null) return; String sessionId = ctx.getSessionId(); - Long loginId = ctx.getLoginId(); + String login = ctx.getLogin(); - if (sessionId == null || loginId == null) { + if (sessionId == null || login == null || login.isBlank()) { return; } bySessionId.put(sessionId, ctx); - byLoginId - .computeIfAbsent(loginId, id -> new CopyOnWriteArraySet<>()) + byLogin + .computeIfAbsent(login, id -> new CopyOnWriteArraySet<>()) .add(ctx); } @@ -65,18 +65,18 @@ public final class ActiveConnectionsRegistry { if (ctx == null) return; String sessionId = ctx.getSessionId(); - Long loginId = ctx.getLoginId(); + String login = ctx.getLogin(); if (sessionId != null) { bySessionId.remove(sessionId); } - if (loginId != null) { - Set set = byLoginId.get(loginId); + if (login != null) { + Set set = byLogin.get(login); if (set != null) { set.remove(ctx); if (set.isEmpty()) { - byLoginId.remove(loginId); + byLogin.remove(login); } } } @@ -90,13 +90,13 @@ public final class ActiveConnectionsRegistry { ConnectionContext ctx = bySessionId.remove(sessionId); if (ctx != null) { - Long loginId = ctx.getLoginId(); - if (loginId != null) { - Set set = byLoginId.get(loginId); + String login = ctx.getLogin(); + if (login != null) { + Set set = byLogin.get(login); if (set != null) { set.remove(ctx); if (set.isEmpty()) { - byLoginId.remove(loginId); + byLogin.remove(login); } } } @@ -112,14 +112,15 @@ public final class ActiveConnectionsRegistry { } /** - * Получить все активные подключения пользователя по loginId. + * Получить все активные подключения пользователя по login. */ - public Set getByLoginId(long loginId) { - Set set = byLoginId.get(loginId); + public Set getByLogin(String login) { + if (login == null) return Set.of(); + Set set = byLogin.get(login); if (set == null) { return Set.of(); } // CopyOnWriteArraySet безопасно отдавать как есть return set; } -} +} \ No newline at end of file diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/ConnectionContext.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/ConnectionContext.java index deef4a4..535632e 100644 --- a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/ConnectionContext.java +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/ConnectionContext.java @@ -77,16 +77,12 @@ public class ConnectionContext { this.activeSessionEntry = activeSessionEntry; } - // --- Удобные геттеры для логина --- + // --- Удобный геттер для логина --- public String getLogin() { return solanaUserEntry != null ? solanaUserEntry.getLogin() : null; } - public Long getLoginId() { - return solanaUserEntry != null ? solanaUserEntry.getLoginId() : null; - } - // --- sessionId / sessionPwd --- public String getSessionId() { @@ -149,7 +145,6 @@ public class ConnectionContext { public String toString() { return "ConnectionContext{" + "login='" + getLogin() + '\'' + - ", loginId=" + getLoginId() + ", sessionId=" + sessionId + ", authenticationStatus=" + authenticationStatus + '}'; diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/blockchain/Net_AddBlock_Request.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/blockchain/Net_AddBlock_Request.java index a8a056e..36a4227 100644 --- a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/blockchain/Net_AddBlock_Request.java +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/blockchain/Net_AddBlock_Request.java @@ -4,17 +4,17 @@ import server.logic.ws_protocol.JSON.entyties.Net_Request; public final class Net_AddBlock_Request extends Net_Request { - private String login; // обязателен - private long blockchainId; // обязателен - private int globalNumber; // обязателен - private String prevGlobalHash; // HEX(64) или "" для нулевого - private String blockBytesB64; // байты FULL-блока (raw+sig+hash) в Base64 + private String login; // обязателен + private String blockchainName; // обязателен + private int globalNumber; // обязателен + private String prevGlobalHash; // HEX(64) или "" для нулевого + private String blockBytesB64; // байты FULL-блока (raw+sig+hash) в Base64 public String getLogin() { return login; } public void setLogin(String login) { this.login = login; } - public long getBlockchainId() { return blockchainId; } - public void setBlockchainId(long blockchainId) { this.blockchainId = blockchainId; } + public String getBlockchainName() { return blockchainName; } + public void setBlockchainName(String blockchainName) { this.blockchainName = blockchainName; } public int getGlobalNumber() { return globalNumber; } public void setGlobalNumber(int globalNumber) { this.globalNumber = globalNumber; } diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/tempToTest/Net_AddUser_Request.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/tempToTest/Net_AddUser_Request.java index ad0a462..8e0bd1e 100644 --- a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/tempToTest/Net_AddUser_Request.java +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/tempToTest/Net_AddUser_Request.java @@ -12,8 +12,7 @@ import server.logic.ws_protocol.JSON.entyties.Net_Request; * "requestId": "test-add-1", * "payload": { * "login": "anya", - * "loginId": 100211, - * "bchId": 4222, + * "blockchainName": "anya0001", * "loginKey": "base64-ed25519-public-key-login", * "deviceKey": "base64-ed25519-public-key-device", * "bchLimit": 1000000 @@ -25,57 +24,23 @@ import server.logic.ws_protocol.JSON.entyties.Net_Request; public class Net_AddUser_Request extends Net_Request { private String login; - private long loginId; - private long bchId; + private String blockchainName; private String loginKey; private String deviceKey; private Integer bchLimit; - public String getLogin() { - return login; - } + public String getLogin() { return login; } + public void setLogin(String login) { this.login = login; } - public void setLogin(String login) { - this.login = login; - } + public String getBlockchainName() { return blockchainName; } + public void setBlockchainName(String blockchainName) { this.blockchainName = blockchainName; } - public long getLoginId() { - return loginId; - } + public String getLoginKey() { return loginKey; } + public void setLoginKey(String loginKey) { this.loginKey = loginKey; } - public void setLoginId(long loginId) { - this.loginId = loginId; - } + public String getDeviceKey() { return deviceKey; } + public void setDeviceKey(String deviceKey) { this.deviceKey = deviceKey; } - public long getBchId() { - return bchId; - } - - public void setBchId(long bchId) { - this.bchId = bchId; - } - - public String getLoginKey() { - return loginKey; - } - - public void setLoginKey(String loginKey) { - this.loginKey = loginKey; - } - - public String getDeviceKey() { - return deviceKey; - } - - public void setDeviceKey(String deviceKey) { - this.deviceKey = deviceKey; - } - - public Integer getBchLimit() { - return bchLimit; - } - - public void setBchLimit(Integer bchLimit) { - this.bchLimit = bchLimit; - } -} + public Integer getBchLimit() { return bchLimit; } + public void setBchLimit(Integer bchLimit) { this.bchLimit = bchLimit; } +} \ No newline at end of file diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_CloseActiveSession_Handler.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_CloseActiveSession_Handler.java index 1b91849..442767b 100644 --- a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_CloseActiveSession_Handler.java +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_CloseActiveSession_Handler.java @@ -63,7 +63,7 @@ public class Net_CloseActiveSession_Handler implements JsonMessageHandler { } SolanaUserEntry user = ctx.getSolanaUser(); - long currentLoginId = user.getLoginId(); + String currentLogin = user.getLogin(); int authStatus = ctx.getAuthenticationStatus(); if (authStatus != ConnectionContext.AUTH_STATUS_USER @@ -180,7 +180,7 @@ public class Net_CloseActiveSession_Handler implements JsonMessageHandler { ); } - if (targetSession.getLoginId() != currentLoginId) { + if (currentLogin == null || !currentLogin.equals(targetSession.getLogin())) { return NetExceptionResponseFactory.error( req, WireCodes.Status.UNVERIFIED, @@ -236,10 +236,7 @@ public class Net_CloseActiveSession_Handler implements JsonMessageHandler { if (isCurrentSession && ctxToClose == currentCtx) { // Это текущее подключение: закрываем после отправки ответа. new Thread(() -> { - try { - Thread.sleep(50); // небольшая пауза, чтобы ответ ушёл - } catch (InterruptedException ignored) { - } + try { Thread.sleep(50); } catch (InterruptedException ignored) {} WsConnectionUtils.closeConnection( ctxToClose, 4000, diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_CreateAuthSession__Handler.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_CreateAuthSession__Handler.java index 3125f61..96435a1 100644 --- a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_CreateAuthSession__Handler.java +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_CreateAuthSession__Handler.java @@ -25,52 +25,14 @@ import java.sql.SQLException; import java.security.SecureRandom; import java.util.Base64; -/** - * Шаг 2 авторизации: проверка подписи и создание сессии. - * - * Клиент присылает в payload: - * - storagePwd (base64 от 32 байт) - * - timeMs (long, мс с 1970-01-01) - * - signatureB64 (подпись Ed25519 над строкой: - * "AUTHORIFICATED:" + timeMs + authNonce) - * - clientInfo (опционально, до 50 символов) - * - * authNonce клиент получил на шаге 1 (AuthChallenge) и сервер - * сохранил его в ctx.authNonce. - * - * При успехе: - * - создаётся запись ActiveSession в БД; - * - генерируется sessionId (base64 от 32 случайных байт); - * - генерируется sessionPwd (base64 от 32 случайных байт); - * - sessionCreatedAtMs и lastAuthirificatedAtMs = текущее время; - * - заполняются поля clientIp, clientInfoFromClient, clientInfoFromRequest, userLanguage; - * - возвращается sessionId и sessionPwd в ответе. - * - * При ошибке авторификации (битые данные, подпись, время и т.п.) — - * соединение закрывается через WsConnectionUtils. - */ public class Net_CreateAuthSession__Handler implements JsonMessageHandler { private static final Logger log = LoggerFactory.getLogger(Net_CreateAuthSession__Handler.class); private static final SecureRandom RANDOM = new SecureRandom(); - /** Допустимое расхождение времени клиента и сервера (мс). */ public static final long ALLOWED_SKEW_MS = 30_000L; - /** - * Общая проверка подписи Ed25519 над строкой: - * "AUTHORIFICATED:" + timeMs + authNonce. - * - * Используется и в CreateAuthSession, и в CloseActiveSession (для статуса AUTH_IN_PROGRESS). - * - * @param user пользователь (используется deviceKey) - * @param authNonce одноразовый nonce из шага 1 - * @param timeMs время на стороне клиента - * @param signatureB64 подпись в base64 - * @return true — подпись корректна; false — подпись не проходит верификацию - * @throws IllegalArgumentException при некорректном base64 ключа/подписи - */ public static boolean verifyAuthorificatedSignature( SolanaUserEntry user, String authNonce, @@ -92,7 +54,6 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler { public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) throws Exception { Net_CreateAuthSession_Request req = (Net_CreateAuthSession_Request) baseReq; - // --- базовые проверки контекста шага 1 --- if (ctx == null || ctx.getSolanaUser() == null || ctx.getAuthNonce() == null @@ -109,15 +70,15 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler { } SolanaUserEntry user = ctx.getSolanaUser(); - Long loginId = user.getLoginId(); - if (loginId == null) { + String login = user.getLogin(); + if (login == null || login.isBlank()) { Net_Response err = NetExceptionResponseFactory.error( req, WireCodes.Status.SERVER_DATA_ERROR, - "NO_LOGIN_ID", - "Для пользователя не задан loginId в БД" + "NO_LOGIN", + "Для пользователя не задан login в БД" ); - WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: no loginId"); + WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: no login"); return err; } @@ -160,13 +121,11 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler { return err; } - // Короткая строка clientInfo от клиента (до 50 символов) String clientInfoFromClient = req.getClientInfo(); if (clientInfoFromClient != null && clientInfoFromClient.length() > 50) { clientInfoFromClient = clientInfoFromClient.substring(0, 50); } - // --- выбираем публичный ключ pubkey1 --- String pubKeyB64 = user.getDeviceKey(); if (pubKeyB64 == null || pubKeyB64.isBlank()) { Net_Response err = NetExceptionResponseFactory.error( @@ -179,10 +138,8 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler { return err; } - // --- authNonce (challenge) мы сохранили в ctx.authNonce на шаге 1 --- String authNonce = ctx.getAuthNonce(); - // --- проверяем подпись через общий метод --- boolean sigOk; try { sigOk = verifyAuthorificatedSignature(user, authNonce, timeMs, signatureB64); @@ -230,10 +187,7 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler { } } } - - if (clientIp == null) { - clientIp = ""; - } + if (clientIp == null) clientIp = ""; // --- создаём запись ActiveSession и сохраняем в БД --- ActiveSessionsDAO dao = ActiveSessionsDAO.getInstance(); @@ -242,8 +196,8 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler { try { activeSessionEntry = new ActiveSessionEntry( sessionId, - loginId, - newSessionPwd, // настоящий секрет сессии + login, + newSessionPwd, storagePwd, now, now, @@ -258,7 +212,7 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler { dao.insert(activeSessionEntry); } catch (SQLException e) { - log.error("Ошибка БД при создании новой сессии для loginId={}", loginId, e); + log.error("Ошибка БД при создании новой сессии для login={}", login, e); Net_Response err = NetExceptionResponseFactory.error( req, WireCodes.Status.SERVER_DATA_ERROR, diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_ListSessions_Handler.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_ListSessions_Handler.java index 99d1ce0..8291f92 100644 --- a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_ListSessions_Handler.java +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_ListSessions_Handler.java @@ -51,7 +51,7 @@ public class Net_ListSessions_Handler implements JsonMessageHandler { } SolanaUserEntry user = ctx.getSolanaUser(); - long currentLoginId = user.getLoginId(); + String currentLogin = user.getLogin(); int authStatus = ctx.getAuthenticationStatus(); if (authStatus != ConnectionContext.AUTH_STATUS_USER @@ -130,9 +130,9 @@ public class Net_ListSessions_Handler implements JsonMessageHandler { // 3) Тянем все активные сессии пользователя из БД List sessions; try { - sessions = ActiveSessionsDAO.getInstance().getByLoginId(currentLoginId); + sessions = ActiveSessionsDAO.getInstance().getByLogin(currentLogin); } catch (SQLException e) { - log.error("Ошибка БД при получении списка сессий для loginId={}", currentLoginId, e); + log.error("Ошибка БД при получении списка сессий для login={}", currentLogin, e); return NetExceptionResponseFactory.error( req, WireCodes.Status.SERVER_DATA_ERROR, diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_RefreshSession_Handler.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_RefreshSession_Handler.java index 53a8cdb..c5da508 100644 --- a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_RefreshSession_Handler.java +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_RefreshSession_Handler.java @@ -95,14 +95,15 @@ public class Net_RefreshSession_Handler implements JsonMessageHandler { ); } - // --- вытаскиваем пользователя по loginId --- - SolanaUserEntry solanaUserEntry = null; - long loginId = session.getLoginId(); + // --- вытаскиваем пользователя по login из сессии --- + SolanaUserEntry solanaUserEntry; + String login = session.getLogin(); + try { SolanaUsersDAO usersDao = SolanaUsersDAO.getInstance(); - solanaUserEntry = usersDao.getByLoginId(loginId); + solanaUserEntry = usersDao.getByLogin(login); } catch (SQLException e) { - log.error("Ошибка БД при поиске пользователя по loginId={} из сессии", loginId, e); + log.error("Ошибка БД при поиске пользователя по login={} из сессии", login, e); return NetExceptionResponseFactory.error( req, WireCodes.Status.SERVER_DATA_ERROR, diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/BlockchainLocks.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/BlockchainLocks.java index d0f371f..1b599e6 100644 --- a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/BlockchainLocks.java +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/BlockchainLocks.java @@ -4,11 +4,11 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.locks.ReentrantLock; public final class BlockchainLocks { - private static final ConcurrentHashMap MAP = new ConcurrentHashMap<>(); + private static final ConcurrentHashMap MAP = new ConcurrentHashMap<>(); private BlockchainLocks() {} - public static ReentrantLock lockFor(long blockchainId) { - return MAP.computeIfAbsent(blockchainId, id -> new ReentrantLock(true)); // fair=true + public static ReentrantLock lockFor(String blockchainName) { + return MAP.computeIfAbsent(blockchainName, id -> new ReentrantLock(true)); // fair=true } } \ No newline at end of file diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/BlockchainStateService_new.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/BlockchainStateService_new.java index 1954142..28da112 100644 --- a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/BlockchainStateService_new.java +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/BlockchainStateService_new.java @@ -11,7 +11,6 @@ import shine.db.entities.SolanaUserEntry; import java.sql.Connection; import java.sql.SQLException; -import java.sql.Types; import java.util.Base64; /** @@ -28,9 +27,9 @@ public final class BlockchainStateService_new { /** Результат атомарного addBlock */ public static final class AddBlockResult { - public final int lineIndex; // 0..7 (пока ставим 0) - public final int httpStatus; // WireCodes.Status.* - public final String reasonCode; // null если ok + public final int lineIndex; // 0..7 (пока ставим 0) + public final int httpStatus; // WireCodes.Status.* + public final String reasonCode; // null если ok public final BlockchainStateEntry stateAfter; // состояние после (может быть null) public AddBlockResult(int lineIndex, int httpStatus, String reasonCode, BlockchainStateEntry stateAfter) { @@ -69,7 +68,7 @@ public final class BlockchainStateService_new { */ public AddBlockResult addBlockAtomically( String login, - long blockchainId, + String blockchainName, int globalNumber, String prevGlobalHash, String blockBytesB64 @@ -91,11 +90,21 @@ public final class BlockchainStateService_new { ); } + if (login == null || login.isBlank()) { + return new AddBlockResult(lineIndex, WireCodes.Status.BAD_REQUEST, "empty_login", null); + } + if (blockchainName == null || blockchainName.isBlank()) { + return new AddBlockResult(lineIndex, WireCodes.Status.BAD_REQUEST, "empty_blockchain_name", null); + } + if (blockBytes == null || blockBytes.length == 0) { + return new AddBlockResult(lineIndex, WireCodes.Status.BAD_REQUEST, "empty_block_bytes", null); + } + try (Connection c = db.getConnection()) { boolean oldAutoCommit = c.getAutoCommit(); c.setAutoCommit(false); try { - // 1) получаем loginId по login + // 1) получаем пользователя по login (если надо валидировать существование) SolanaUserEntry u = solanaUsersDAO.getByLogin(c, login); if (u == null) { c.rollback(); @@ -106,13 +115,12 @@ public final class BlockchainStateService_new { null ); } - long loginId = u.getLoginId(); // 2) вставляем блок в blocks - insertBlockRow(c, loginId, blockchainId, globalNumber, prevGlobalHash, blockBytes, lineIndex); + insertBlockRow(c, login, blockchainName, globalNumber, prevGlobalHash, blockBytes, lineIndex); - // 3) обновляем агрегатное состояние blockchain_state - BlockchainStateEntry st = stateDAO.getByBlockchainId(c, blockchainId); + // 3) обновляем агрегатное состояние blockchain_state (по blockchainName) + BlockchainStateEntry st = stateDAO.getByBlockchainName(c, blockchainName); if (st == null) { c.rollback(); return new AddBlockResult( @@ -124,7 +132,6 @@ public final class BlockchainStateService_new { } // MVP: обновляем “последний глобальный номер”. - // Хэш тут сейчас оставлен как заглушка — лучше поставить фактический хэш нового блока. st.setLastGlobalNumber(globalNumber); st.setLastGlobalHash(nn(prevGlobalHash)); // TODO: заменить на hash нового блока st.setUpdatedAtMs(System.currentTimeMillis()); @@ -158,8 +165,8 @@ public final class BlockchainStateService_new { private void insertBlockRow( Connection c, - long loginId, - long blockchainId, + String login, + String blockchainName, int globalNumber, String prevGlobalHash, byte[] blockBytes, @@ -167,8 +174,9 @@ public final class BlockchainStateService_new { ) throws SQLException { BlockEntry e = new BlockEntry(); - e.setLoginId(loginId); - e.setBlockchainId(blockchainId); + + e.setLogin(login); + e.setBchName(blockchainName); e.setBlockGlobalNumber(globalNumber); e.setBlockGlobalPreHashe(nn(prevGlobalHash)); @@ -182,10 +190,11 @@ public final class BlockchainStateService_new { e.setBlockByte(blockBytes); - e.setToLoginId(0); - e.setToBlockchainId(0); - e.setToBlockGlobalNumber(0); - e.setToBlockHashe(""); + // NEW: nullable ссылки (не забиваем фейковыми нулями) + e.setToLogin(null); + e.setToBchName(null); + e.setToBlockGlobalNumber(null); + e.setToBlockHashe(null); blocksDAO.upsert(c, e); } diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/Net_AddBlock_new_Handler.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/Net_AddBlock_new_Handler.java index af2f879..04ba90c 100644 --- a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/Net_AddBlock_new_Handler.java +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/Net_AddBlock_new_Handler.java @@ -16,7 +16,7 @@ public final class Net_AddBlock_new_Handler implements JsonMessageHandler { var r = BlockchainStateService_new.getInstance().addBlockAtomically( req.getLogin(), - req.getBlockchainId(), + req.getBlockchainName(), req.getGlobalNumber(), req.getPrevGlobalHash(), req.getBlockBytesB64() diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/tempToTest/Net_AddUser_Handler.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/tempToTest/Net_AddUser_Handler.java index 3751597..fe092da 100644 --- a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/tempToTest/Net_AddUser_Handler.java +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/tempToTest/Net_AddUser_Handler.java @@ -27,16 +27,15 @@ public class Net_AddUser_Handler implements JsonMessageHandler { Net_AddUser_Request req = (Net_AddUser_Request) baseRequest; if (req.getLogin() == null || req.getLogin().isBlank() + || req.getBlockchainName() == null || req.getBlockchainName().isBlank() || req.getLoginKey() == null || req.getLoginKey().isBlank() - || req.getDeviceKey() == null || req.getDeviceKey().isBlank() - || req.getLoginId() <= 0 - || req.getBchId() <= 0) { + || req.getDeviceKey() == null || req.getDeviceKey().isBlank()) { return NetExceptionResponseFactory.error( req, WireCodes.Status.BAD_REQUEST, "BAD_FIELDS", - "Некорректные поля: login/loginId/bchId/loginKey/deviceKey" + "Некорректные поля: login/blockchainName/loginKey/deviceKey" ); } @@ -47,9 +46,8 @@ public class Net_AddUser_Handler implements JsonMessageHandler { SolanaUsersDAO dao = SolanaUsersDAO.getInstance(); SolanaUserEntry user = new SolanaUserEntry( - req.getLoginId(), req.getLogin(), - req.getBchId(), + req.getBlockchainName(), req.getLoginKey(), req.getDeviceKey(), limit @@ -62,8 +60,8 @@ public class Net_AddUser_Handler implements JsonMessageHandler { resp.setRequestId(req.getRequestId()); resp.setStatus(WireCodes.Status.OK); - log.info("✅ AddUser ok: login={}, loginId={}, bchId={}, limit={}", - req.getLogin(), req.getLoginId(), req.getBchId(), limit); + log.info("✅ AddUser ok: login={}, blockchainName={}, limit={}", + req.getLogin(), req.getBlockchainName(), limit); return resp; diff --git a/src/main/java/Test/Test_AddBlock_new_NoAuth.java b/src/main/java/Test/Test_AddBlock_new_NoAuth.java index 5e6deb4..f717cdd 100644 --- a/src/main/java/Test/Test_AddBlock_new_NoAuth.java +++ b/src/main/java/Test/Test_AddBlock_new_NoAuth.java @@ -24,7 +24,8 @@ public class Test_AddBlock_new_NoAuth { private static final ObjectMapper JSON = new ObjectMapper(); private static final String TEST_LOGIN = "anya24"; - private static final long TEST_BCH_ID = 4222L; + // По твоему правилу: blockchainName = login + 4 цифры + private static final String TEST_BCH_NAME = TEST_LOGIN + "0001"; private static final byte[] LOGIN_PRIV_KEY; private static final byte[] LOGIN_PUB_KEY; @@ -66,7 +67,7 @@ public class Test_AddBlock_new_NoAuth { String json = buildAddBlockJson( "test-add-header", - TEST_BCH_ID, + TEST_BCH_NAME, 0, ZERO64, // prevGlobalHash для первого блока — нули base64(headerFull) @@ -124,7 +125,7 @@ public class Test_AddBlock_new_NoAuth { String json2 = buildAddBlockJson( "test-add-text", - TEST_BCH_ID, + TEST_BCH_NAME, 1, lastGlobalHashHex, // prevGlobalHash = хэш header'а из ответа сервера base64(textFull) @@ -181,7 +182,7 @@ public class Test_AddBlock_new_NoAuth { byte[] prevLineHash32) { HeaderBody body = new HeaderBody( - TEST_BCH_ID, + TEST_BCH_NAME, // было TEST_BCH_ID (long), теперь имя блокчейна (String) TEST_LOGIN, 0, 0, (short) 1, @@ -259,17 +260,17 @@ public class Test_AddBlock_new_NoAuth { // ================================================================================= private static String buildAddBlockJson(String requestId, - long blockchainId, - int globalNumber, - String prevGlobalHashHex, - String blockBytesB64) { + String blockchainName, + int globalNumber, + String prevGlobalHashHex, + String blockBytesB64) { return """ { "op": "AddBlock", "requestId": "%s", "payload": { "login": "%s", - "blockchainId": %d, + "blockchainName": "%s", "globalNumber": %d, "prevGlobalHash": "%s", "blockBytesB64": "%s" @@ -318,4 +319,4 @@ public class Test_AddBlock_new_NoAuth { } return out; } -} +} \ No newline at end of file diff --git a/src/main/java/Test/Test_AddUser_and_Authorification.java b/src/main/java/Test/Test_AddUser_and_Authorification.java index f885fe5..c88f7c2 100644 --- a/src/main/java/Test/Test_AddUser_and_Authorification.java +++ b/src/main/java/Test/Test_AddUser_and_Authorification.java @@ -57,8 +57,8 @@ public class Test_AddUser_and_Authorification { // Тестовые данные пользователя private static final String TEST_LOGIN = "anya24"; - private static final long TEST_LOGIN_ID = 1030120L; - private static final long TEST_BCH_ID = 4222L; + // По твоему правилу: blockchainName = login + 4 цифры + private static final String TEST_BCH_NAME = TEST_LOGIN + "0001"; private static final int TEST_BCH_LIMIT = 1_000_000; // Краткая строка clientInfo, которую клиент шлёт @@ -399,13 +399,6 @@ public class Test_AddUser_and_Authorification { // SCENARIO 3 / 5 / 7: ListSessions // ========================================================== - /** - * Общий сценарий: AuthChallenge → ListSessions в статусе AUTH_IN_PROGRESS. - * - * @param title заголовок для вывода - * @param expectSession1Present ожидать ли первую сессию в списке - * @param expectSession2Present ожидать ли вторую сессию в списке - */ private static void scenario3_ListSessions_AuthInProgress( String title, boolean expectSession1Present, @@ -476,8 +469,8 @@ public class Test_AddUser_and_Authorification { boolean ok = status == 200 - && (expectSession1Present == has1) - && (expectSession2Present == has2); + && (expectSession1Present == has1) + && (expectSession2Present == has2); printTestResult( "S-List/ListSessions (ожидаемые сессии)", @@ -767,8 +760,7 @@ public class Test_AddUser_and_Authorification { "requestId": "test-add-1", "payload": { "login": "%s", - "loginId": %d, - "bchId": %d, + "bchName": "%s", "loginKey": "%s", "deviceKey": "%s", "bchLimit": %d @@ -776,8 +768,7 @@ public class Test_AddUser_and_Authorification { } """.formatted( TEST_LOGIN, - TEST_LOGIN_ID, - TEST_BCH_ID, + TEST_BCH_NAME, LOGIN_PUBKEY_B64, // loginKey DEVICE_PUBKEY_B64, // deviceKey TEST_BCH_LIMIT