17 12 25
Ещё промежуточный комит верии - не работает :) 2
This commit is contained in:
parent
aa2caf1f10
commit
29c6e5a0f6
@ -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");
|
||||||
}
|
}
|
||||||
|
|
||||||
public void close() {
|
return conn;
|
||||||
try {
|
|
||||||
connection.close();
|
|
||||||
} catch (SQLException e) {
|
|
||||||
// логировать по необходимости
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Теперь close() не нужен. */
|
||||||
|
public void close() {
|
||||||
|
// no-op
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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"));
|
||||||
|
|||||||
@ -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; }
|
||||||
}
|
}
|
||||||
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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,86 +76,79 @@ 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);
|
||||||
|
lock.lock();
|
||||||
|
try (Connection conn = SqliteDbController.getInstance().getConnection()) {
|
||||||
|
|
||||||
|
// Транзакция — норм, но БЕЗ "BEGIN IMMEDIATE".
|
||||||
boolean oldAuto = conn.getAutoCommit();
|
boolean oldAuto = conn.getAutoCommit();
|
||||||
conn.setAutoCommit(false);
|
conn.setAutoCommit(false);
|
||||||
|
|
||||||
try (Statement st = conn.createStatement()) {
|
try {
|
||||||
// важно: заранее берём write lock
|
BlockchainStateEntry state =
|
||||||
st.execute("BEGIN IMMEDIATE");
|
BlockchainStateDAO.getInstance().getByBlockchainId(conn, blockchainId);
|
||||||
|
|
||||||
BlockchainStateEntry state = BlockchainStateDAO.getInstance().getByBlockchainId(blockchainId);
|
|
||||||
if (state == null) {
|
if (state == null) {
|
||||||
conn.rollback();
|
conn.rollback();
|
||||||
return new Result(404, "UNKNOWN_BLOCKCHAIN", null, lineIndex);
|
return new Result(404, "UNKNOWN_BLOCKCHAIN", null, lineIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1) защита от подмены логина
|
|
||||||
if (!login.equals(state.getUserLogin())) {
|
if (!login.equals(state.getUserLogin())) {
|
||||||
conn.rollback();
|
conn.rollback();
|
||||||
return new Result(403, "LOGIN_MISMATCH", state, lineIndex);
|
return new Result(403, "LOGIN_MISMATCH", state, lineIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2) проверяем ожидаемый global
|
|
||||||
int expectedGlobal = state.getLastGlobalNumber() + 1;
|
int expectedGlobal = state.getLastGlobalNumber() + 1;
|
||||||
if (globalNumber != expectedGlobal) {
|
if (globalNumber != expectedGlobal) {
|
||||||
conn.rollback();
|
conn.rollback();
|
||||||
return new Result(409, "OUT_OF_SEQUENCE_GLOBAL", state, lineIndex);
|
return new Result(409, "OUT_OF_SEQUENCE_GLOBAL", state, lineIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3) проверяем prev global hash
|
|
||||||
String dbPrevGlobalHash = nn(state.getLastGlobalHash());
|
String dbPrevGlobalHash = nn(state.getLastGlobalHash());
|
||||||
if (!eqHash(prevGlobalHashHex, dbPrevGlobalHash)) {
|
if (!eqHash(prevGlobalHashHex, dbPrevGlobalHash)) {
|
||||||
conn.rollback();
|
conn.rollback();
|
||||||
return new Result(409, "GLOBAL_HASH_MISMATCH", state, lineIndex);
|
return new Result(409, "GLOBAL_HASH_MISMATCH", state, lineIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4) проверяем lineNumber
|
|
||||||
int expectedLineNumber = state.getLastLineNumber(lineIndex) + 1;
|
int expectedLineNumber = state.getLastLineNumber(lineIndex) + 1;
|
||||||
if (block.lineNumber != expectedLineNumber) {
|
if (block.lineNumber != expectedLineNumber) {
|
||||||
conn.rollback();
|
conn.rollback();
|
||||||
return new Result(409, "OUT_OF_SEQUENCE_LINE", state, lineIndex);
|
return new Result(409, "OUT_OF_SEQUENCE_LINE", state, lineIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5) prevLineHash берём из БД (он хранится!)
|
// prevLineHash (пока просто читаем, дальше пригодится для крипто-проверки)
|
||||||
String dbPrevLineHashHex = nn(state.getLastLineHash(lineIndex));
|
String dbPrevLineHashHex = nn(state.getLastLineHash(lineIndex));
|
||||||
|
|
||||||
// 6) полноценная крипто-проверка (хэш/подпись)
|
// TODO crypto check (потом подключим)
|
||||||
// TODO: тут подключи твой реальный verifier:
|
|
||||||
// - посчитать preimage по твоим правилам (login + prevGlobalHash32 + prevLineHash32 + rawBytes)
|
|
||||||
// - сверить sha256(preimage) == block.hash32
|
|
||||||
// - проверить Ed25519 подпись
|
|
||||||
//
|
|
||||||
// Если не ок:
|
|
||||||
// conn.rollback(); return new Result(422, "CRYPTO_INVALID", state, lineIndex);
|
|
||||||
|
|
||||||
// 7) запись блока в файл (append)
|
// 1) пишем в файл
|
||||||
FileStoreUtil.getInstance().addDataToBlockchain(blockchainId, block.toBytes());
|
FileStoreUtil.getInstance().addDataToBlockchain(blockchainId, block.toBytes());
|
||||||
|
|
||||||
// 8) апдейт состояния в БД
|
// 2) обновляем state в БД
|
||||||
state.setLastGlobalNumber(globalNumber);
|
state.setLastGlobalNumber(globalNumber);
|
||||||
state.setLastGlobalHash(bytesToHex(block.getHash32())); // новый global hash = hash блока
|
state.setLastGlobalHash(bytesToHex(block.getHash32()));
|
||||||
|
|
||||||
state.setLastLineNumber(lineIndex, block.lineNumber);
|
state.setLastLineNumber(lineIndex, block.lineNumber);
|
||||||
// ВАЖНО: line hash тоже логично сделать = hash блока (если так задумано)
|
|
||||||
state.setLastLineHash(lineIndex, bytesToHex(block.getHash32()));
|
state.setLastLineHash(lineIndex, bytesToHex(block.getHash32()));
|
||||||
|
|
||||||
// size_bytes += len(fullBytes)
|
|
||||||
state.setSizeBytes(state.getSizeBytes() + fullBytes.length);
|
state.setSizeBytes(state.getSizeBytes() + fullBytes.length);
|
||||||
state.setUpdatedAtMs(System.currentTimeMillis());
|
state.setUpdatedAtMs(System.currentTimeMillis());
|
||||||
|
|
||||||
BlockchainStateDAO.getInstance().upsert(state);
|
BlockchainStateDAO.getInstance().upsert(conn, state);
|
||||||
|
|
||||||
conn.commit();
|
conn.commit();
|
||||||
return new Result(200, null, state, lineIndex);
|
return new Result(200, null, state, lineIndex);
|
||||||
|
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
conn.rollback();
|
conn.rollback();
|
||||||
// если хочешь красиво: SQLITE_BUSY → 503 RETRY
|
|
||||||
throw e;
|
throw e;
|
||||||
} finally {
|
} finally {
|
||||||
conn.setAutoCommit(oldAuto);
|
conn.setAutoCommit(oldAuto);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
lock.unlock();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String nn(String s) { return s == null ? "" : s; }
|
private static String nn(String s) { return s == null ? "" : s; }
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
160
src/TODO.txt
160
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 (для поиска), если он будет нужен.
|
|
||||||
Loading…
Reference in New Issue
Block a user