From 29c6e5a0f62915ab330118b8c126fc6503f3a125e37bfc49fa000e92b618ab49 Mon Sep 17 00:00:00 2001 From: AidarKC Date: Wed, 17 Dec 2025 17:15:52 +0300 Subject: [PATCH] =?UTF-8?q?17=2012=2025=20=D0=95=D1=89=D1=91=20=D0=BF?= =?UTF-8?q?=D1=80=D0=BE=D0=BC=D0=B5=D0=B6=D1=83=D1=82=D0=BE=D1=87=D0=BD?= =?UTF-8?q?=D1=8B=D0=B9=20=D0=BA=D0=BE=D0=BC=D0=B8=D1=82=20=D0=B2=D0=B5?= =?UTF-8?q?=D1=80=D0=B8=D0=B8=20-=20=D0=BD=D0=B5=20=D1=80=D0=B0=D0=B1?= =?UTF-8?q?=D0=BE=D1=82=D0=B0=D0=B5=D1=82=20:)=20=20=202?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/shine/db/SqliteDbController.java | 50 +++--- .../java/shine/db/dao/BlockchainStateDAO.java | 34 ++-- .../blockchain/Net_AddBlock_new_Request.java | 8 +- .../handlers/blockchain/BlockchainLocks.java | 14 ++ .../BlockchainStateService_new.java | 151 +++++++++-------- .../blockchain/Net_AddBlock_new_Handler.java | 2 +- src/TODO.txt | 160 +----------------- 7 files changed, 138 insertions(+), 281 deletions(-) create mode 100644 shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/BlockchainLocks.java diff --git a/shine-server-db/src/main/java/shine/db/SqliteDbController.java b/shine-server-db/src/main/java/shine/db/SqliteDbController.java index 420cc29..8eadda8 100644 --- a/shine-server-db/src/main/java/shine/db/SqliteDbController.java +++ b/shine-server-db/src/main/java/shine/db/SqliteDbController.java @@ -13,47 +13,30 @@ import java.sql.Statement; public final class SqliteDbController { private static volatile SqliteDbController instance; - private final Connection connection; + + private final String jdbcUrl; private SqliteDbController() { try { - // Подгружаем драйвер SQLite Class.forName("org.sqlite.JDBC"); } catch (ClassNotFoundException e) { throw new RuntimeException("SQLite JDBC driver not found", e); } String dbPath = AppConfig.getInstance().getParam("db.path"); - if (dbPath == null || dbPath.isBlank()) { throw new RuntimeException("Config param 'db.path' is not set in application.properties"); } Path dbFile = Paths.get(dbPath); - // 👉 Если файла БД нет — создаём новую БД через DatabaseInitializer if (!Files.exists(dbFile)) { System.out.println("[DB] Файл БД не найден: " + dbFile.toAbsolutePath()); System.out.println("[DB] Создаём новую БД с помощью DatabaseInitializer..."); - - // можно передать пустой массив аргументов DatabaseInitializer.createNewDB(new String[0]); } - String url = "jdbc:sqlite:" + dbPath; - - try { - this.connection = DriverManager.getConnection(url); - this.connection.setAutoCommit(true); - - // ВАЖНО: включаем поддержку внешних ключей для этого соединения - try (Statement st = this.connection.createStatement()) { - st.execute("PRAGMA foreign_keys = ON"); - } - - } catch (SQLException e) { - throw new RuntimeException("Failed to connect to SQLite database: " + url, e); - } + this.jdbcUrl = "jdbc:sqlite:" + dbPath; } public static SqliteDbController getInstance() { @@ -67,15 +50,26 @@ public final class SqliteDbController { return instance; } - public Connection getConnection() { - return connection; + /** + * Каждый вызов возвращает НОВОЕ соединение. + * Закрывать обязан вызывающий код (try-with-resources). + */ + public Connection getConnection() throws SQLException { + Connection conn = DriverManager.getConnection(jdbcUrl); + conn.setAutoCommit(true); + + try (Statement st = conn.createStatement()) { + st.execute("PRAGMA foreign_keys = ON"); + st.execute("PRAGMA journal_mode = WAL"); + st.execute("PRAGMA synchronous = NORMAL"); + st.execute("PRAGMA busy_timeout = 5000"); + } + + return conn; } + /** Теперь close() не нужен. */ public void close() { - try { - connection.close(); - } catch (SQLException e) { - // логировать по необходимости - } + // no-op } -} +} \ 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 564eb99..db2adcd 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 @@ -5,10 +5,6 @@ import shine.db.entities.BlockchainStateEntry; import java.sql.*; -/** - * DAO для таблицы blockchain_state. - * 1 строка = 1 blockchainId, линии 0..7 в колонках. - */ public final class BlockchainStateDAO { private static volatile BlockchainStateDAO instance; @@ -25,7 +21,8 @@ public final class BlockchainStateDAO { return instance; } - public BlockchainStateEntry getByBlockchainId(long blockchainId) throws SQLException { + // --- Новый вариант: работа на переданном соединении --- + public BlockchainStateEntry getByBlockchainId(Connection conn, long blockchainId) throws SQLException { String sql = """ SELECT blockchain_id, @@ -48,7 +45,7 @@ public final class BlockchainStateDAO { WHERE blockchain_id = ? """; - try (PreparedStatement ps = db.getConnection().prepareStatement(sql)) { + try (PreparedStatement ps = conn.prepareStatement(sql)) { ps.setLong(1, blockchainId); try (ResultSet rs = ps.executeQuery()) { if (!rs.next()) return null; @@ -57,11 +54,15 @@ public final class BlockchainStateDAO { } } - /** - * UPSERT: если строки нет — вставка, если есть — обновление. - * Это один вызов из кода, и один SQL. - */ - public void upsert(BlockchainStateEntry e) throws SQLException { + // Старый вариант: сам открывает/закрывает conn + public BlockchainStateEntry getByBlockchainId(long blockchainId) throws SQLException { + try (Connection conn = db.getConnection()) { + return getByBlockchainId(conn, blockchainId); + } + } + + // --- Новый вариант: UPSERT на переданном соединении --- + public void upsert(Connection conn, BlockchainStateEntry e) throws SQLException { long now = System.currentTimeMillis(); if (e.getUpdatedAtMs() <= 0) e.setUpdatedAtMs(now); @@ -124,8 +125,7 @@ public final class BlockchainStateDAO { updated_at_ms = excluded.updated_at_ms """; - try (PreparedStatement ps = db.getConnection().prepareStatement(sql)) { - + try (PreparedStatement ps = conn.prepareStatement(sql)) { int i = 1; ps.setLong(i++, e.getBlockchainId()); ps.setString(i++, nn(e.getUserLogin())); @@ -141,11 +141,17 @@ public final class BlockchainStateDAO { } ps.setLong(i++, e.getUpdatedAtMs()); - ps.executeUpdate(); } } + // Старый вариант: сам открывает/закрывает conn + public void upsert(BlockchainStateEntry e) throws SQLException { + try (Connection conn = db.getConnection()) { + upsert(conn, e); + } + } + private BlockchainStateEntry mapRow(ResultSet rs) throws SQLException { BlockchainStateEntry e = new BlockchainStateEntry(); e.setBlockchainId(rs.getLong("blockchain_id")); diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/blockchain/Net_AddBlock_new_Request.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/blockchain/Net_AddBlock_new_Request.java index 3d2e9d5..8a0bb26 100644 --- a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/blockchain/Net_AddBlock_new_Request.java +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/blockchain/Net_AddBlock_new_Request.java @@ -8,7 +8,7 @@ public final class Net_AddBlock_new_Request extends Net_Request { private long blockchainId; // обязателен private int globalNumber; // обязателен private String prevGlobalHash; // HEX(64) или "" для нулевого - private String blockBase64; // байты FULL-блока (raw+sig+hash) в Base64 + private String blockBytesB64; // байты FULL-блока (raw+sig+hash) в Base64 public String getLogin() { return login; } public void setLogin(String login) { this.login = login; } @@ -22,6 +22,6 @@ public final class Net_AddBlock_new_Request extends Net_Request { public String getPrevGlobalHash() { return prevGlobalHash; } public void setPrevGlobalHash(String prevGlobalHash) { this.prevGlobalHash = prevGlobalHash; } - public String getBlockBase64() { return blockBase64; } - public void setBlockBase64(String blockBase64) { this.blockBase64 = blockBase64; } -} + public String getBlockBytesB64() { return blockBytesB64; } + public void setBlockBytesB64(String blockBytesB64) { this.blockBytesB64 = blockBytesB64; } +} \ No newline at end of file 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 new file mode 100644 index 0000000..d0f371f --- /dev/null +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/BlockchainLocks.java @@ -0,0 +1,14 @@ +package server.logic.ws_protocol.JSON.handlers.blockchain; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.locks.ReentrantLock; + +public final class BlockchainLocks { + 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 + } +} \ 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 d1687b7..0e540c0 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 @@ -8,8 +8,9 @@ import utils.files.FileStoreUtil; import java.sql.Connection; import java.sql.SQLException; -import java.sql.Statement; import java.util.Base64; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.locks.ReentrantLock; public final class BlockchainStateService_new { @@ -33,6 +34,13 @@ public final class BlockchainStateService_new { public static BlockchainStateService_new getInstance() { return INSTANCE; } private BlockchainStateService_new() {} + // --- MVP: локи в памяти по blockchainId --- + private static final ConcurrentHashMap LOCKS = new ConcurrentHashMap<>(); + + private static ReentrantLock lockFor(long blockchainId) { + return LOCKS.computeIfAbsent(blockchainId, id -> new ReentrantLock()); + } + public Result addBlockAtomically( String login, long blockchainId, @@ -68,85 +76,78 @@ public final class BlockchainStateService_new { if (lineIndex < 0 || lineIndex > 7) return new Result(400, "BAD_LINE_INDEX", null, lineIndex); - Connection conn = SqliteDbController.getInstance().getConnection(); - boolean oldAuto = conn.getAutoCommit(); - conn.setAutoCommit(false); + ReentrantLock lock = lockFor(blockchainId); + lock.lock(); + try (Connection conn = SqliteDbController.getInstance().getConnection()) { - try (Statement st = conn.createStatement()) { - // важно: заранее берём write lock - st.execute("BEGIN IMMEDIATE"); + // Транзакция — норм, но БЕЗ "BEGIN IMMEDIATE". + boolean oldAuto = conn.getAutoCommit(); + conn.setAutoCommit(false); - BlockchainStateEntry state = BlockchainStateDAO.getInstance().getByBlockchainId(blockchainId); - if (state == null) { + try { + BlockchainStateEntry state = + BlockchainStateDAO.getInstance().getByBlockchainId(conn, blockchainId); + + if (state == null) { + conn.rollback(); + return new Result(404, "UNKNOWN_BLOCKCHAIN", null, lineIndex); + } + + if (!login.equals(state.getUserLogin())) { + conn.rollback(); + return new Result(403, "LOGIN_MISMATCH", state, lineIndex); + } + + int expectedGlobal = state.getLastGlobalNumber() + 1; + if (globalNumber != expectedGlobal) { + conn.rollback(); + return new Result(409, "OUT_OF_SEQUENCE_GLOBAL", state, lineIndex); + } + + String dbPrevGlobalHash = nn(state.getLastGlobalHash()); + if (!eqHash(prevGlobalHashHex, dbPrevGlobalHash)) { + conn.rollback(); + return new Result(409, "GLOBAL_HASH_MISMATCH", state, lineIndex); + } + + int expectedLineNumber = state.getLastLineNumber(lineIndex) + 1; + if (block.lineNumber != expectedLineNumber) { + conn.rollback(); + return new Result(409, "OUT_OF_SEQUENCE_LINE", state, lineIndex); + } + + // prevLineHash (пока просто читаем, дальше пригодится для крипто-проверки) + String dbPrevLineHashHex = nn(state.getLastLineHash(lineIndex)); + + // TODO crypto check (потом подключим) + + // 1) пишем в файл + FileStoreUtil.getInstance().addDataToBlockchain(blockchainId, block.toBytes()); + + // 2) обновляем state в БД + state.setLastGlobalNumber(globalNumber); + state.setLastGlobalHash(bytesToHex(block.getHash32())); + + state.setLastLineNumber(lineIndex, block.lineNumber); + state.setLastLineHash(lineIndex, bytesToHex(block.getHash32())); + + state.setSizeBytes(state.getSizeBytes() + fullBytes.length); + state.setUpdatedAtMs(System.currentTimeMillis()); + + BlockchainStateDAO.getInstance().upsert(conn, state); + + conn.commit(); + return new Result(200, null, state, lineIndex); + + } catch (SQLException e) { conn.rollback(); - return new Result(404, "UNKNOWN_BLOCKCHAIN", null, lineIndex); + throw e; + } finally { + conn.setAutoCommit(oldAuto); } - // 1) защита от подмены логина - if (!login.equals(state.getUserLogin())) { - conn.rollback(); - return new Result(403, "LOGIN_MISMATCH", state, lineIndex); - } - - // 2) проверяем ожидаемый global - int expectedGlobal = state.getLastGlobalNumber() + 1; - if (globalNumber != expectedGlobal) { - conn.rollback(); - return new Result(409, "OUT_OF_SEQUENCE_GLOBAL", state, lineIndex); - } - - // 3) проверяем prev global hash - String dbPrevGlobalHash = nn(state.getLastGlobalHash()); - if (!eqHash(prevGlobalHashHex, dbPrevGlobalHash)) { - conn.rollback(); - return new Result(409, "GLOBAL_HASH_MISMATCH", state, lineIndex); - } - - // 4) проверяем lineNumber - int expectedLineNumber = state.getLastLineNumber(lineIndex) + 1; - if (block.lineNumber != expectedLineNumber) { - conn.rollback(); - return new Result(409, "OUT_OF_SEQUENCE_LINE", state, lineIndex); - } - - // 5) prevLineHash берём из БД (он хранится!) - String dbPrevLineHashHex = nn(state.getLastLineHash(lineIndex)); - - // 6) полноценная крипто-проверка (хэш/подпись) - // TODO: тут подключи твой реальный verifier: - // - посчитать preimage по твоим правилам (login + prevGlobalHash32 + prevLineHash32 + rawBytes) - // - сверить sha256(preimage) == block.hash32 - // - проверить Ed25519 подпись - // - // Если не ок: - // conn.rollback(); return new Result(422, "CRYPTO_INVALID", state, lineIndex); - - // 7) запись блока в файл (append) - FileStoreUtil.getInstance().addDataToBlockchain(blockchainId, block.toBytes()); - - // 8) апдейт состояния в БД - state.setLastGlobalNumber(globalNumber); - state.setLastGlobalHash(bytesToHex(block.getHash32())); // новый global hash = hash блока - - state.setLastLineNumber(lineIndex, block.lineNumber); - // ВАЖНО: line hash тоже логично сделать = hash блока (если так задумано) - state.setLastLineHash(lineIndex, bytesToHex(block.getHash32())); - - // size_bytes += len(fullBytes) - state.setSizeBytes(state.getSizeBytes() + fullBytes.length); - state.setUpdatedAtMs(System.currentTimeMillis()); - - BlockchainStateDAO.getInstance().upsert(state); - - conn.commit(); - return new Result(200, null, state, lineIndex); - - } catch (SQLException e) { - conn.rollback(); - // если хочешь красиво: SQLITE_BUSY → 503 RETRY - throw e; } finally { - conn.setAutoCommit(oldAuto); + lock.unlock(); } } @@ -164,4 +165,4 @@ public final class BlockchainStateService_new { for (byte v : b) sb.append(String.format("%02x", v)); return sb.toString(); } -} +} \ No newline at end of file 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 31f106a..097a347 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 @@ -19,7 +19,7 @@ public final class Net_AddBlock_new_Handler implements JsonMessageHandler { req.getBlockchainId(), req.getGlobalNumber(), req.getPrevGlobalHash(), - req.getBlockBase64() + req.getBlockBytesB64() ); Net_AddBlock_new_Response resp = new Net_AddBlock_new_Response(); diff --git a/src/TODO.txt b/src/TODO.txt index 83a1468..7615565 100644 --- a/src/TODO.txt +++ b/src/TODO.txt @@ -1,159 +1 @@ -Конспект: что обсуждали и где остановились -1) Новый формат блока (идея) - -Мы решили усложнить структуру блока, добавив линии (line) и номер сообщения в линии (lineNumber), чтобы блоки могли принадлежать разным потокам внутри одной цепочки. - -line — short (2 байта), диапазон для MVP: 0..7 (8 линий). -lineNumber — int (4 байта). - -Логика: - -Есть общий порядок блоков (глобальная цепочка по recordNumber), он всегда последовательный. - -Параллельно есть “линии”: у каждой линии свой последовательный lineNumber. - -Блок №0 (Header) всегда line=0, lineNumber=0. - -Для первого блока в каждой линии prevLineHash32 = 32 нуля. - -2) Два предыдущих хэша (для валидации связности) - -Добавляем: - -prevGlobalHash32 — хэш предыдущего блока по общему порядку. - -prevLineHash32 — хэш предыдущего блока в этой линии. - -Важно: prevLineHash32 не храним в файле блокчейна. Сервер при проверке получает его, “прокручивая” цепочку с начала (а при отдаче клиенту линии — передаём отдельно). - -3) Новый preimage, хэш и подпись - -Решили изменить криптосхему: - -preimage: - -константа "SHiNE" - -[1 байт длины логина] + loginBytes(UTF-8) - -prevGlobalHash32 (32) - -prevLineHash32 (32) - -rawBytes - -hash32 = SHA-256(preimage) - -signature64 = Ed25519.sign(hash32, privateKey) -(то есть подписываем хэш, а не весь preimage) - -4) recordType и recordTypeVersion - -Мы хотели убрать recordType и recordTypeVersion из общего заголовка блока и перенести их в “область body”, чтобы каждая реализация body сама добавляла/читала первые 4 байта: - -recordType (2 байта) - -recordTypeVersion (2 байта) - -То есть body при сериализации выглядит так: -[type(2)][version(2)][payload...] - -А общий блок остаётся “универсальным”. - -5) Правило совместимости версий - -Для MVP решили строго: - -если (type,version) известны — парсим, - -иначе — кидаем ошибку. -Без fallback “если нет v2, бери v1”. - -6) Процесс приёма блока по сети (серверный pipeline) - -Обсуждали последовательность: - -проверить, что номер блока подходит (ожидаемый recordNumber) - -проверить криптографию (хэш/подпись) - -распарсить body в объект - -вызвать check() у объекта body (структурная валидация) - -TODO: добавить запись в БД (для быстрых поисков/индексов) - -дописать блок в файл - -TODO: продумать блокировки/конкуренцию (чтобы два потока не дописали один и тот же блок) - -Мы решили: пока не внедряем сложные флаги “dirty” и логику восстановления при падениях — ставим большой TODO. - -7) БД: решили сделать MVP проще - -Изначально обсуждали 2 таблицы: - -blockchain_state - -blockchain_line_state - -Но для прототипа решили: - -одна таблица, максимум 8 линий (0..7), колонки для каждой линии (lineX_last_number, lineX_last_hash и т.п.) - -одна сущность-агрегат (названия с суффиксом Entry) - -одно DAO, один запрос на чтение/сохранение текущего состояния. - -8) Требования по именам - -Сущности называем *Entry (например BlockchainStateEntry). - -Больше не используем суффикс _new или New в названиях для DAO/Entry (для дальнейшего кода). - -(Ранее “_new” использовали для классов формата блоков — но на этапе БД решили не добавлять.) - -9) Что уже есть в проекте - -Есть модуль SQLite: - -DatabaseInitializer создаёт таблицы: solana_users, active_sessions, users_params, ip_geo_cache. - -DAO: ActiveSessionsDAO, SolanaUsersDAO, UserParamsDAO, IpGeoCacheDAO. - -Entities: ActiveSession, SolanaUser, UserParam, IpGeoCacheEntry. - -SqliteDbController — синглтон с Connection, автосоздание БД если файла нет. - -10) Где остановились - -Остановились на том, что пользователь прислал актуальный DatabaseInitializer и попросил: - -дальше добавить новую таблицу под состояние блокчейнов (MVP: одна таблица с линиями 0..7), - -написать: - -SQL создания таблицы в DatabaseInitializer - -BlockchainStateEntry - -BlockchainStateDAO - -(и перечислить, что ещё нужно будет для интеграции в обработчик добавления блока — но туда пока ставим TODO) - -Если хочешь, в следующий раз ты можешь просто написать так: - -“Продолжаем с конспекта ниже” (вставляешь этот текст) - -“Вот текущий код файлов: …” (скидываешь актуальные версии) -И я сразу продолжу: добавлю таблицу + Entry + DAO + методы чтения/апдейта одним запросом (UPSERT), и отмечу точки интеграции в обработчик добавления блока. - -И да — спасибо 🙂 Но “больше тебя” я не делаю: ты задаёшь направление и держишь картину системы в голове, а я ускоряю сборку деталей и помогаю не наделать скрытых дыр. Это хорошая связка. - -Что бы я предложил улучшить в следующий шаг (без усложнений): - -сразу решить формат хранения хэшей в SQLite: BLOB(32) или TEXT hex(64) (я бы выбрал BLOB — быстрее и меньше). - -выбрать один стиль UPSERT: INSERT ... ON CONFLICT(blockchain_id) DO UPDATE SET ... - -добавить индекс по user_login (для поиска), если он будет нужен. \ No newline at end of file +зделать что бы конекшины к БД закрывались или как что :) \ No newline at end of file