Ещё промежуточный комит верии - не работает :)   2
This commit is contained in:
AidarKC 2025-12-17 17:15:52 +03:00
parent aa2caf1f10
commit 29c6e5a0f6
7 changed files with 138 additions and 281 deletions

View File

@ -13,47 +13,30 @@ import java.sql.Statement;
public final class SqliteDbController { public final class SqliteDbController {
private static volatile SqliteDbController instance; private static volatile SqliteDbController instance;
private final Connection connection;
private final String jdbcUrl;
private SqliteDbController() { private SqliteDbController() {
try { try {
// Подгружаем драйвер SQLite
Class.forName("org.sqlite.JDBC"); Class.forName("org.sqlite.JDBC");
} catch (ClassNotFoundException e) { } catch (ClassNotFoundException e) {
throw new RuntimeException("SQLite JDBC driver not found", e); throw new RuntimeException("SQLite JDBC driver not found", e);
} }
String dbPath = AppConfig.getInstance().getParam("db.path"); String dbPath = AppConfig.getInstance().getParam("db.path");
if (dbPath == null || dbPath.isBlank()) { if (dbPath == null || dbPath.isBlank()) {
throw new RuntimeException("Config param 'db.path' is not set in application.properties"); throw new RuntimeException("Config param 'db.path' is not set in application.properties");
} }
Path dbFile = Paths.get(dbPath); Path dbFile = Paths.get(dbPath);
// 👉 Если файла БД нет создаём новую БД через DatabaseInitializer
if (!Files.exists(dbFile)) { if (!Files.exists(dbFile)) {
System.out.println("[DB] Файл БД не найден: " + dbFile.toAbsolutePath()); System.out.println("[DB] Файл БД не найден: " + dbFile.toAbsolutePath());
System.out.println("[DB] Создаём новую БД с помощью DatabaseInitializer..."); System.out.println("[DB] Создаём новую БД с помощью DatabaseInitializer...");
// можно передать пустой массив аргументов
DatabaseInitializer.createNewDB(new String[0]); DatabaseInitializer.createNewDB(new String[0]);
} }
String url = "jdbc:sqlite:" + dbPath; this.jdbcUrl = "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);
}
} }
public static SqliteDbController getInstance() { public static SqliteDbController getInstance() {
@ -67,15 +50,26 @@ public final class SqliteDbController {
return instance; 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() { public void close() {
try { // no-op
connection.close();
} catch (SQLException e) {
// логировать по необходимости
}
} }
} }

View File

@ -5,10 +5,6 @@ import shine.db.entities.BlockchainStateEntry;
import java.sql.*; import java.sql.*;
/**
* DAO для таблицы blockchain_state.
* 1 строка = 1 blockchainId, линии 0..7 в колонках.
*/
public final class BlockchainStateDAO { public final class BlockchainStateDAO {
private static volatile BlockchainStateDAO instance; private static volatile BlockchainStateDAO instance;
@ -25,7 +21,8 @@ public final class BlockchainStateDAO {
return instance; return instance;
} }
public BlockchainStateEntry getByBlockchainId(long blockchainId) throws SQLException { // --- Новый вариант: работа на переданном соединении ---
public BlockchainStateEntry getByBlockchainId(Connection conn, long blockchainId) throws SQLException {
String sql = """ String sql = """
SELECT SELECT
blockchain_id, blockchain_id,
@ -48,7 +45,7 @@ public final class BlockchainStateDAO {
WHERE blockchain_id = ? WHERE blockchain_id = ?
"""; """;
try (PreparedStatement ps = db.getConnection().prepareStatement(sql)) { try (PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setLong(1, blockchainId); ps.setLong(1, blockchainId);
try (ResultSet rs = ps.executeQuery()) { try (ResultSet rs = ps.executeQuery()) {
if (!rs.next()) return null; if (!rs.next()) return null;
@ -57,11 +54,15 @@ public final class BlockchainStateDAO {
} }
} }
/** // Старый вариант: сам открывает/закрывает conn
* UPSERT: если строки нет вставка, если есть обновление. public BlockchainStateEntry getByBlockchainId(long blockchainId) throws SQLException {
* Это один вызов из кода, и один SQL. try (Connection conn = db.getConnection()) {
*/ return getByBlockchainId(conn, blockchainId);
public void upsert(BlockchainStateEntry e) throws SQLException { }
}
// --- Новый вариант: UPSERT на переданном соединении ---
public void upsert(Connection conn, BlockchainStateEntry e) throws SQLException {
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();
if (e.getUpdatedAtMs() <= 0) e.setUpdatedAtMs(now); if (e.getUpdatedAtMs() <= 0) e.setUpdatedAtMs(now);
@ -124,8 +125,7 @@ public final class BlockchainStateDAO {
updated_at_ms = excluded.updated_at_ms updated_at_ms = excluded.updated_at_ms
"""; """;
try (PreparedStatement ps = db.getConnection().prepareStatement(sql)) { try (PreparedStatement ps = conn.prepareStatement(sql)) {
int i = 1; int i = 1;
ps.setLong(i++, e.getBlockchainId()); ps.setLong(i++, e.getBlockchainId());
ps.setString(i++, nn(e.getUserLogin())); ps.setString(i++, nn(e.getUserLogin()));
@ -141,11 +141,17 @@ public final class BlockchainStateDAO {
} }
ps.setLong(i++, e.getUpdatedAtMs()); ps.setLong(i++, e.getUpdatedAtMs());
ps.executeUpdate(); 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 { private BlockchainStateEntry mapRow(ResultSet rs) throws SQLException {
BlockchainStateEntry e = new BlockchainStateEntry(); BlockchainStateEntry e = new BlockchainStateEntry();
e.setBlockchainId(rs.getLong("blockchain_id")); e.setBlockchainId(rs.getLong("blockchain_id"));

View File

@ -8,7 +8,7 @@ public final class Net_AddBlock_new_Request extends Net_Request {
private long blockchainId; // обязателен private long blockchainId; // обязателен
private int globalNumber; // обязателен private int globalNumber; // обязателен
private String prevGlobalHash; // HEX(64) или "" для нулевого 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 String getLogin() { return login; }
public void setLogin(String login) { this.login = 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 String getPrevGlobalHash() { return prevGlobalHash; }
public void setPrevGlobalHash(String prevGlobalHash) { this.prevGlobalHash = prevGlobalHash; } public void setPrevGlobalHash(String prevGlobalHash) { this.prevGlobalHash = prevGlobalHash; }
public String getBlockBase64() { return blockBase64; } public String getBlockBytesB64() { return blockBytesB64; }
public void setBlockBase64(String blockBase64) { this.blockBase64 = blockBase64; } public void setBlockBytesB64(String blockBytesB64) { this.blockBytesB64 = blockBytesB64; }
} }

View File

@ -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<Long, ReentrantLock> MAP = new ConcurrentHashMap<>();
private BlockchainLocks() {}
public static ReentrantLock lockFor(long blockchainId) {
return MAP.computeIfAbsent(blockchainId, id -> new ReentrantLock(true)); // fair=true
}
}

View File

@ -8,8 +8,9 @@ import utils.files.FileStoreUtil;
import java.sql.Connection; import java.sql.Connection;
import java.sql.SQLException; import java.sql.SQLException;
import java.sql.Statement;
import java.util.Base64; import java.util.Base64;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantLock;
public final class BlockchainStateService_new { public final class BlockchainStateService_new {
@ -33,6 +34,13 @@ public final class BlockchainStateService_new {
public static BlockchainStateService_new getInstance() { return INSTANCE; } public static BlockchainStateService_new getInstance() { return INSTANCE; }
private BlockchainStateService_new() {} private BlockchainStateService_new() {}
// --- MVP: локи в памяти по blockchainId ---
private static final ConcurrentHashMap<Long, ReentrantLock> LOCKS = new ConcurrentHashMap<>();
private static ReentrantLock lockFor(long blockchainId) {
return LOCKS.computeIfAbsent(blockchainId, id -> new ReentrantLock());
}
public Result addBlockAtomically( public Result addBlockAtomically(
String login, String login,
long blockchainId, long blockchainId,
@ -68,85 +76,78 @@ public final class BlockchainStateService_new {
if (lineIndex < 0 || lineIndex > 7) if (lineIndex < 0 || lineIndex > 7)
return new Result(400, "BAD_LINE_INDEX", null, lineIndex); return new Result(400, "BAD_LINE_INDEX", null, lineIndex);
Connection conn = SqliteDbController.getInstance().getConnection(); ReentrantLock lock = lockFor(blockchainId);
boolean oldAuto = conn.getAutoCommit(); lock.lock();
conn.setAutoCommit(false); try (Connection conn = SqliteDbController.getInstance().getConnection()) {
try (Statement st = conn.createStatement()) { // Транзакция норм, но БЕЗ "BEGIN IMMEDIATE".
// важно: заранее берём write lock boolean oldAuto = conn.getAutoCommit();
st.execute("BEGIN IMMEDIATE"); conn.setAutoCommit(false);
BlockchainStateEntry state = BlockchainStateDAO.getInstance().getByBlockchainId(blockchainId); try {
if (state == null) { 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(); 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 { } finally {
conn.setAutoCommit(oldAuto); lock.unlock();
} }
} }

View File

@ -19,7 +19,7 @@ public final class Net_AddBlock_new_Handler implements JsonMessageHandler {
req.getBlockchainId(), req.getBlockchainId(),
req.getGlobalNumber(), req.getGlobalNumber(),
req.getPrevGlobalHash(), req.getPrevGlobalHash(),
req.getBlockBase64() req.getBlockBytesB64()
); );
Net_AddBlock_new_Response resp = new Net_AddBlock_new_Response(); Net_AddBlock_new_Response resp = new Net_AddBlock_new_Response();

View File

@ -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 (для поиска), если он будет нужен.