В тот день Вроде дописал добавление но так и не заработало
This commit is contained in:
AidarKC 2025-12-19 11:06:25 +03:00
parent 6c4d8cd51b
commit 3cafd29ee5
2 changed files with 159 additions and 243 deletions

View File

@ -13,8 +13,7 @@ import java.sql.*;
* - методы без Connection сами открывают и закрывают соединение
*
* Важно:
* - PRIMARY KEY выбран: (loginId, blockchainId, blockGlobalNumber, blockLineIndex, blockLineNumber)
* Это означает: в рамках одной цепочки и одного globalNumber, каждая линия/номер уникальны.
* - PRIMARY KEY: (loginId, blockchainId, blockGlobalNumber, blockLineIndex, blockLineNumber)
*/
public final class BlocksDAO {
@ -55,27 +54,7 @@ public final class BlocksDAO {
""";
try (PreparedStatement ps = c.prepareStatement(sql)) {
int i = 1;
ps.setLong(i++, e.getLoginId());
ps.setLong(i++, e.getBlockchainId());
ps.setInt(i++, e.getBlockGlobalNumber());
ps.setString(i++, nn(e.getBlockGlobalPreHashe()));
ps.setInt(i++, e.getBlockLineIndex());
ps.setInt(i++, e.getBlockLineNumber());
ps.setString(i++, nn(e.getBlockLinePreHashe()));
ps.setInt(i++, e.getMsgType());
byte[] bytes = e.getBlockByte();
if (bytes != null) ps.setBytes(i++, bytes);
else ps.setNull(i++, Types.BLOB);
ps.setLong(i++, e.getToLoginId());
ps.setInt(i++, e.getToBlockchainId());
ps.setInt(i++, e.getToBlockGlobalNumber());
ps.setString(i++, nn(e.getToBlockHashe()));
bindAll(ps, e);
ps.executeUpdate();
}
}
@ -87,7 +66,7 @@ public final class BlocksDAO {
}
}
// -------------------- UPSERT (SAVE) --------------------
// -------------------- UPSERT --------------------
/**
* Сохранить (upsert) с внешним соединением. Соединение НЕ закрывает.
@ -123,27 +102,7 @@ public final class BlocksDAO {
""";
try (PreparedStatement ps = c.prepareStatement(sql)) {
int i = 1;
ps.setLong(i++, e.getLoginId());
ps.setLong(i++, e.getBlockchainId());
ps.setInt(i++, e.getBlockGlobalNumber());
ps.setString(i++, nn(e.getBlockGlobalPreHashe()));
ps.setInt(i++, e.getBlockLineIndex());
ps.setInt(i++, e.getBlockLineNumber());
ps.setString(i++, nn(e.getBlockLinePreHashe()));
ps.setInt(i++, e.getMsgType());
byte[] bytes = e.getBlockByte();
if (bytes != null) ps.setBytes(i++, bytes);
else ps.setNull(i++, Types.BLOB);
ps.setLong(i++, e.getToLoginId());
ps.setInt(i++, e.getToBlockchainId());
ps.setInt(i++, e.getToBlockGlobalNumber());
ps.setString(i++, nn(e.getToBlockHashe()));
bindAll(ps, e);
ps.executeUpdate();
}
}
@ -157,15 +116,13 @@ public final class BlocksDAO {
// -------------------- SELECT --------------------
/**
* Получить блок по PK с внешним соединением. Соединение НЕ закрывает.
*/
/** Получить блок по PK с внешним соединением. Соединение НЕ закрывает. */
public BlockEntry getByPk(Connection c,
long loginId,
long blockchainId,
int blockGlobalNumber,
int blockLineIndex,
int blockLineNumber) throws SQLException {
long loginId,
long blockchainId,
int blockGlobalNumber,
int blockLineIndex,
int blockLineNumber) throws SQLException {
String sql = """
SELECT
@ -207,10 +164,10 @@ public final class BlocksDAO {
/** Получить блок по PK без внешнего соединения. Сам открывает/закрывает. */
public BlockEntry getByPk(long loginId,
long blockchainId,
int blockGlobalNumber,
int blockLineIndex,
int blockLineNumber) throws SQLException {
long blockchainId,
int blockGlobalNumber,
int blockLineIndex,
int blockLineNumber) throws SQLException {
try (Connection c = db.getConnection()) {
return getByPk(c, loginId, blockchainId, blockGlobalNumber, blockLineIndex, blockLineNumber);
}
@ -275,7 +232,7 @@ public final class BlocksDAO {
}
}
// -------------------- DELETE (на всякий) --------------------
// -------------------- DELETE --------------------
/** Удалить по PK с внешним соединением. Соединение НЕ закрывает. */
public int deleteByPk(Connection c,
@ -316,7 +273,32 @@ public final class BlocksDAO {
}
}
// -------------------- MAPPER --------------------
// -------------------- INTERNAL --------------------
/** Единая привязка параметров под INSERT/UPSERT — чтобы не разъезжалось. */
private static void bindAll(PreparedStatement ps, BlockEntry e) throws SQLException {
int i = 1;
ps.setLong(i++, e.getLoginId());
ps.setLong(i++, e.getBlockchainId());
ps.setInt(i++, e.getBlockGlobalNumber());
ps.setString(i++, nn(e.getBlockGlobalPreHashe()));
ps.setInt(i++, e.getBlockLineIndex());
ps.setInt(i++, e.getBlockLineNumber());
ps.setString(i++, nn(e.getBlockLinePreHashe()));
ps.setInt(i++, e.getMsgType());
byte[] bytes = e.getBlockByte();
if (bytes != null) ps.setBytes(i++, bytes);
else ps.setNull(i++, Types.BLOB);
ps.setLong(i++, e.getToLoginId());
ps.setInt(i++, e.getToBlockchainId());
ps.setInt(i++, e.getToBlockGlobalNumber());
ps.setString(i++, nn(e.getToBlockHashe()));
}
private BlockEntry mapRow(ResultSet rs) throws SQLException {
BlockEntry e = new BlockEntry();

View File

@ -1,221 +1,155 @@
package server.logic.ws_protocol.JSON.handlers.blockchain;
import blockchain_new.BchBlockEntry_new;
import shine.db.SqliteDbController;
import shine.db.dao.BlockchainStateDAO;
import shine.db.dao.BlocksDAO;
import shine.db.dao.SolanaUsersDAO;
import shine.db.entities.BlockEntry;
import shine.db.entities.BlockchainStateEntry;
import shine.db.entities.SolanaUserEntry;
import utils.files.FileStoreUtil;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Base64;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.locks.ReentrantLock;
/**
* BlockchainStateService_new атомарное добавление блока:
* - (опционально) проверки
* - вставка строки блока в таблицу blocks
* - обновление агрегатного состояния blockchain_state
*
* Важно:
* - всё делается в одной транзакции
* - DAO-методы с Connection НЕ закрывают соединение
*/
public final class BlockchainStateService_new {
public static final class Result {
public final int httpStatus;
public final String reasonCode; // null если ok
public final BlockchainStateEntry stateAfter;
public final int lineIndex;
private static volatile BlockchainStateService_new instance;
public Result(int httpStatus, String reasonCode, BlockchainStateEntry stateAfter, int lineIndex) {
this.httpStatus = httpStatus;
this.reasonCode = reasonCode;
this.stateAfter = stateAfter;
this.lineIndex = lineIndex;
}
private final SqliteDbController db = SqliteDbController.getInstance();
private final BlocksDAO blocksDAO = BlocksDAO.getInstance();
private final BlockchainStateDAO stateDAO = BlockchainStateDAO.getInstance();
private final SolanaUsersDAO solanaUsersDAO = SolanaUsersDAO.getInstance();
public boolean isOk() { return reasonCode == null && httpStatus == 200; }
}
private static final BlockchainStateService_new INSTANCE = new BlockchainStateService_new();
public static BlockchainStateService_new getInstance() { return INSTANCE; }
private BlockchainStateService_new() {}
private static final String ZERO64 = "0".repeat(64);
// Локи по blockchainId (MVP, один сервер)
private final ConcurrentMap<Long, ReentrantLock> locks = new ConcurrentHashMap<>();
private ReentrantLock lockFor(long blockchainId) {
return locks.computeIfAbsent(blockchainId, k -> new ReentrantLock());
public static BlockchainStateService_new getInstance() {
if (instance == null) {
synchronized (BlockchainStateService_new.class) {
if (instance == null) instance = new BlockchainStateService_new();
}
}
return instance;
}
public Result addBlockAtomically(
/**
* Атомарно добавляет блок (в рамках одной транзакции).
*
* @param login логин (для поиска loginId)
* @param blockchainId id блокчейна
* @param globalNumber глобальный номер
* @param prevGlobalHash предыдущий глобальный хэш
* @param blockBytesB64 блок (в Base64) если у тебя уже byte[], сделай перегрузку
*/
public void addBlockAtomically(
String login,
long blockchainId,
int globalNumber,
String prevGlobalHashHex,
String blockBase64
String prevGlobalHash,
String blockBytesB64
) throws Exception {
// Тут я не трогаю твою бизнес-логику парсинга blockBytesB64.
// Просто предполагаю, что у тебя есть метод декодирования.
byte[] blockBytes = decodeBase64(blockBytesB64);
try (Connection c = db.getConnection()) {
boolean oldAutoCommit = c.getAutoCommit();
c.setAutoCommit(false);
try {
// 1) получаем loginId по login
SolanaUserEntry u = solanaUsersDAO.getByLogin(c, login);
if (u == null) {
throw new IllegalStateException("Не найден пользователь в solana_users по login=" + login);
}
long loginId = u.getLoginId();
// 2) вставляем блок в blocks
insertBlockRow(c, loginId, blockchainId, globalNumber, prevGlobalHash, blockBytes);
// 3) обновляем агрегатное состояние (если у тебя там отдельная логика подключи сюда)
// Ниже базовый пример, ты можешь заменить на свои расчёты lineHash/lineNumber и т.д.
BlockchainStateEntry st = stateDAO.getByBlockchainId(c, blockchainId);
if (st == null) {
throw new IllegalStateException("Не найден blockchain_state для blockchainId=" + blockchainId);
}
st.setLastGlobalNumber(globalNumber);
st.setLastGlobalHash(nn(prevGlobalHash)); // или новый hash, если ты его вычисляешь
st.setUpdatedAtMs(System.currentTimeMillis());
stateDAO.upsert(c, st);
c.commit();
} catch (Exception e) {
c.rollback();
throw e;
} finally {
c.setAutoCommit(oldAutoCommit);
}
}
}
/**
* Вставка/обновление строки блока в таблицу blocks.
*
* Раньше у тебя тут был SQL, который пытался использовать колонку user_login
* из-за этого и падало "table blocks has no column named user_login".
*
* Теперь всё делаем через BlocksDAO, где имена колонок гарантированно совпадают со схемой.
*/
private void insertBlockRow(
Connection c,
long loginId,
long blockchainId,
int globalNumber,
String prevGlobalHash,
byte[] blockBytes
) throws SQLException {
if (login == null || login.isBlank())
return new Result(400, "EMPTY_LOGIN", null, -1);
if (blockchainId <= 0)
return new Result(400, "BAD_BLOCKCHAIN_ID", null, -1);
if (globalNumber < 0)
return new Result(400, "BAD_GLOBAL_NUMBER", null, -1);
if (blockBase64 == null || blockBase64.isBlank())
return new Result(400, "EMPTY_BLOCK", null, -1);
BlockEntry e = new BlockEntry();
e.setLoginId(loginId);
e.setBlockchainId(blockchainId);
byte[] fullBytes;
try {
fullBytes = Base64.getDecoder().decode(blockBase64);
} catch (IllegalArgumentException e) {
return new Result(400, "BAD_BASE64_BLOCK", null, -1);
}
e.setBlockGlobalNumber(globalNumber);
e.setBlockGlobalPreHashe(nn(prevGlobalHash));
BchBlockEntry_new block;
try {
block = new BchBlockEntry_new(fullBytes);
} catch (Exception e) {
return new Result(400, "BAD_BLOCK_FORMAT", null, -1);
}
// Эти поля (линии/типы/маршрутизация) заполни так, как у тебя реально устроен блок.
// Я ставлю дефолты, чтобы код компилился и логика была ясна.
e.setBlockLineIndex(0);
e.setBlockLineNumber(0);
e.setBlockLinePreHashe("");
int lineIndex = block.line;
if (lineIndex < 0 || lineIndex > 7)
return new Result(400, "BAD_LINE_INDEX", null, lineIndex);
e.setMsgType(0);
boolean isHeaderBlock = (globalNumber == 0 && lineIndex == 0 && block.lineNumber == 0);
e.setBlockByte(blockBytes);
ReentrantLock lock = lockFor(blockchainId);
lock.lock();
try (Connection conn = SqliteDbController.getInstance().getConnection()) {
e.setToLoginId(0);
e.setToBlockchainId(0);
e.setToBlockGlobalNumber(0);
e.setToBlockHashe("");
BlockchainStateDAO stateDao = BlockchainStateDAO.getInstance();
SolanaUsersDAO usersDao = SolanaUsersDAO.getInstance();
BlockchainStateEntry state = stateDao.getByBlockchainId(conn, blockchainId);
if (state == null) {
// state отсутствует разрешаем ТОЛЬКО header-блок
if (!isHeaderBlock) {
return new Result(404, "UNKNOWN_BLOCKCHAIN", null, lineIndex);
}
SolanaUserEntry u = usersDao.getByLogin(conn, login);
if (u == null) {
return new Result(404, "UNKNOWN_USER", null, lineIndex);
}
if (u.getBchId() != blockchainId) {
return new Result(403, "BCHID_MISMATCH", null, lineIndex);
}
// prevGlobalHash для header должен быть нулевой
if (!eqHash(prevGlobalHashHex, ZERO64)) {
return new Result(409, "GLOBAL_HASH_MISMATCH", null, lineIndex);
}
// Создаём начальный state (ЕЩЁ НЕ ПИШЕМ В БД запишем после успешного append)
state = createInitialState(blockchainId, login, u.getLoginKey(), safeLimit(u.getBchLimit()));
} else {
if (!login.equals(state.getUserLogin())) {
return new Result(403, "LOGIN_MISMATCH", state, lineIndex);
}
}
// expected global
int expectedGlobal = state.getLastGlobalNumber() + 1;
if (globalNumber != expectedGlobal) {
return new Result(409, "OUT_OF_SEQUENCE_GLOBAL", state, lineIndex);
}
// prev global hash
String dbPrevGlobalHash = nn(state.getLastGlobalHash());
if (!eqHash(prevGlobalHashHex, dbPrevGlobalHash)) {
return new Result(409, "GLOBAL_HASH_MISMATCH", state, lineIndex);
}
// expected line number
int expectedLineNumber = state.getLastLineNumber(lineIndex) + 1;
if (block.lineNumber != expectedLineNumber) {
return new Result(409, "OUT_OF_SEQUENCE_LINE", state, lineIndex);
}
// TODO: крипто-проверка (потом подключим)
// 1) запись блока в файл (append-only)
FileStoreUtil.getInstance().addDataToBlockchain(blockchainId, block.toBytes());
// 2) апдейт state + запись state в БД
String newHashHex = bytesToHex(block.getHash32());
state.setLastGlobalNumber(globalNumber);
state.setLastGlobalHash(newHashHex);
state.setLastLineNumber(lineIndex, block.lineNumber);
state.setLastLineHash(lineIndex, newHashHex);
// Логический размер (как было)
state.setSizeBytes(state.getSizeBytes() + fullBytes.length);
// Новый: размер файла (ровно по твоему правилу)
state.setFileSizeBytes(state.getFileSizeBytes() + fullBytes.length);
state.setUpdatedAtMs(System.currentTimeMillis());
stateDao.upsert(conn, state);
return new Result(200, null, state, lineIndex);
} finally {
lock.unlock();
}
// upsert безопаснее, чем insert, если возможны повторы при ретраях
blocksDAO.upsert(c, e);
}
private static BlockchainStateEntry createInitialState(long blockchainId,
String login,
String loginKeyBase64,
int sizeLimit) {
BlockchainStateEntry s = new BlockchainStateEntry();
s.setBlockchainId(blockchainId);
s.setUserLogin(login);
s.setPublicKeyBase64(nn(loginKeyBase64));
// -------------------- utils --------------------
s.setSizeLimit(sizeLimit);
s.setSizeBytes(0);
s.setFileSizeBytes(0);
s.setLastGlobalNumber(-1);
s.setLastGlobalHash(ZERO64);
for (int i = 0; i < 8; i++) {
if (i == 0) {
// линия 0: заглавный блок имеет lineNumber=0 -> значит "последний" до него = -1
s.setLastLineNumber(i, -1);
} else {
// остальные линии: первый блок будет lineNumber=1 -> значит "последний" до него = 0
s.setLastLineNumber(i, 0);
}
s.setLastLineHash(i, ZERO64);
}
s.setUpdatedAtMs(System.currentTimeMillis());
return s;
private static String nn(String s) {
return s == null ? "" : s;
}
private static int safeLimit(Integer limit) {
if (limit == null || limit <= 0) return 1_000_000; // TEST ONLY fallback
return limit;
}
private static String nn(String s) { return s == null ? "" : s; }
private static boolean eqHash(String a, String b) {
String x = nn(a).trim();
String y = nn(b).trim();
return x.equalsIgnoreCase(y);
}
private static String bytesToHex(byte[] b) {
if (b == null) return "";
StringBuilder sb = new StringBuilder(b.length * 2);
for (byte v : b) sb.append(String.format("%02x", v));
return sb.toString();
private static byte[] decodeBase64(String s) {
if (s == null || s.isBlank()) return null;
return java.util.Base64.getDecoder().decode(s);
}
}