В тот день Вроде дописал добавление но так и не заработало
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 сами открывают и закрывают соединение * - методы без Connection сами открывают и закрывают соединение
* *
* Важно: * Важно:
* - PRIMARY KEY выбран: (loginId, blockchainId, blockGlobalNumber, blockLineIndex, blockLineNumber) * - PRIMARY KEY: (loginId, blockchainId, blockGlobalNumber, blockLineIndex, blockLineNumber)
* Это означает: в рамках одной цепочки и одного globalNumber, каждая линия/номер уникальны.
*/ */
public final class BlocksDAO { public final class BlocksDAO {
@ -55,27 +54,7 @@ public final class BlocksDAO {
"""; """;
try (PreparedStatement ps = c.prepareStatement(sql)) { try (PreparedStatement ps = c.prepareStatement(sql)) {
int i = 1; bindAll(ps, e);
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()));
ps.executeUpdate(); ps.executeUpdate();
} }
} }
@ -87,7 +66,7 @@ public final class BlocksDAO {
} }
} }
// -------------------- UPSERT (SAVE) -------------------- // -------------------- UPSERT --------------------
/** /**
* Сохранить (upsert) с внешним соединением. Соединение НЕ закрывает. * Сохранить (upsert) с внешним соединением. Соединение НЕ закрывает.
@ -123,27 +102,7 @@ public final class BlocksDAO {
"""; """;
try (PreparedStatement ps = c.prepareStatement(sql)) { try (PreparedStatement ps = c.prepareStatement(sql)) {
int i = 1; bindAll(ps, e);
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()));
ps.executeUpdate(); ps.executeUpdate();
} }
} }
@ -157,9 +116,7 @@ public final class BlocksDAO {
// -------------------- SELECT -------------------- // -------------------- SELECT --------------------
/** /** Получить блок по PK с внешним соединением. Соединение НЕ закрывает. */
* Получить блок по PK с внешним соединением. Соединение НЕ закрывает.
*/
public BlockEntry getByPk(Connection c, public BlockEntry getByPk(Connection c,
long loginId, long loginId,
long blockchainId, long blockchainId,
@ -275,7 +232,7 @@ public final class BlocksDAO {
} }
} }
// -------------------- DELETE (на всякий) -------------------- // -------------------- DELETE --------------------
/** Удалить по PK с внешним соединением. Соединение НЕ закрывает. */ /** Удалить по PK с внешним соединением. Соединение НЕ закрывает. */
public int deleteByPk(Connection c, 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 { private BlockEntry mapRow(ResultSet rs) throws SQLException {
BlockEntry e = new BlockEntry(); BlockEntry e = new BlockEntry();

View File

@ -1,221 +1,155 @@
package server.logic.ws_protocol.JSON.handlers.blockchain; package server.logic.ws_protocol.JSON.handlers.blockchain;
import blockchain_new.BchBlockEntry_new;
import shine.db.SqliteDbController; import shine.db.SqliteDbController;
import shine.db.dao.BlockchainStateDAO; import shine.db.dao.BlockchainStateDAO;
import shine.db.dao.BlocksDAO;
import shine.db.dao.SolanaUsersDAO; import shine.db.dao.SolanaUsersDAO;
import shine.db.entities.BlockEntry;
import shine.db.entities.BlockchainStateEntry; import shine.db.entities.BlockchainStateEntry;
import shine.db.entities.SolanaUserEntry; import shine.db.entities.SolanaUserEntry;
import utils.files.FileStoreUtil;
import java.sql.Connection; import java.sql.Connection;
import java.sql.SQLException; 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 final class BlockchainStateService_new {
public static final class Result { private static volatile BlockchainStateService_new instance;
public final int httpStatus;
public final String reasonCode; // null если ok
public final BlockchainStateEntry stateAfter;
public final int lineIndex;
public Result(int httpStatus, String reasonCode, BlockchainStateEntry stateAfter, int lineIndex) { private final SqliteDbController db = SqliteDbController.getInstance();
this.httpStatus = httpStatus; private final BlocksDAO blocksDAO = BlocksDAO.getInstance();
this.reasonCode = reasonCode; private final BlockchainStateDAO stateDAO = BlockchainStateDAO.getInstance();
this.stateAfter = stateAfter; private final SolanaUsersDAO solanaUsersDAO = SolanaUsersDAO.getInstance();
this.lineIndex = lineIndex;
}
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 BlockchainStateService_new() {}
private static final String ZERO64 = "0".repeat(64); public static BlockchainStateService_new getInstance() {
if (instance == null) {
// Локи по blockchainId (MVP, один сервер) synchronized (BlockchainStateService_new.class) {
private final ConcurrentMap<Long, ReentrantLock> locks = new ConcurrentHashMap<>(); if (instance == null) instance = new BlockchainStateService_new();
private ReentrantLock lockFor(long blockchainId) { }
return locks.computeIfAbsent(blockchainId, k -> new ReentrantLock()); }
return instance;
} }
public Result addBlockAtomically( /**
* Атомарно добавляет блок (в рамках одной транзакции).
*
* @param login логин (для поиска loginId)
* @param blockchainId id блокчейна
* @param globalNumber глобальный номер
* @param prevGlobalHash предыдущий глобальный хэш
* @param blockBytesB64 блок (в Base64) если у тебя уже byte[], сделай перегрузку
*/
public void addBlockAtomically(
String login, String login,
long blockchainId, long blockchainId,
int globalNumber, int globalNumber,
String prevGlobalHashHex, String prevGlobalHash,
String blockBase64 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 { ) throws SQLException {
if (login == null || login.isBlank()) BlockEntry e = new BlockEntry();
return new Result(400, "EMPTY_LOGIN", null, -1); e.setLoginId(loginId);
if (blockchainId <= 0) e.setBlockchainId(blockchainId);
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);
byte[] fullBytes; e.setBlockGlobalNumber(globalNumber);
try { e.setBlockGlobalPreHashe(nn(prevGlobalHash));
fullBytes = Base64.getDecoder().decode(blockBase64);
} catch (IllegalArgumentException e) { // Эти поля (линии/типы/маршрутизация) заполни так, как у тебя реально устроен блок.
return new Result(400, "BAD_BASE64_BLOCK", null, -1); // Я ставлю дефолты, чтобы код компилился и логика была ясна.
e.setBlockLineIndex(0);
e.setBlockLineNumber(0);
e.setBlockLinePreHashe("");
e.setMsgType(0);
e.setBlockByte(blockBytes);
e.setToLoginId(0);
e.setToBlockchainId(0);
e.setToBlockGlobalNumber(0);
e.setToBlockHashe("");
// upsert безопаснее, чем insert, если возможны повторы при ретраях
blocksDAO.upsert(c, e);
} }
BchBlockEntry_new block; // -------------------- utils --------------------
try {
block = new BchBlockEntry_new(fullBytes); private static String nn(String s) {
} catch (Exception e) { return s == null ? "" : s;
return new Result(400, "BAD_BLOCK_FORMAT", null, -1);
} }
int lineIndex = block.line; private static byte[] decodeBase64(String s) {
if (lineIndex < 0 || lineIndex > 7) if (s == null || s.isBlank()) return null;
return new Result(400, "BAD_LINE_INDEX", null, lineIndex); return java.util.Base64.getDecoder().decode(s);
boolean isHeaderBlock = (globalNumber == 0 && lineIndex == 0 && block.lineNumber == 0);
ReentrantLock lock = lockFor(blockchainId);
lock.lock();
try (Connection conn = SqliteDbController.getInstance().getConnection()) {
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();
}
}
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));
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 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();
} }
} }