Ещё промежуточный комит верии - не работает :)   3
This commit is contained in:
AidarKC 2025-12-17 17:21:10 +03:00
parent 29c6e5a0f6
commit 45a862b11f
3 changed files with 185 additions and 101 deletions

View File

@ -1,12 +1,12 @@
package server.logic.ws_protocol.JSON.handlers.blockchain; package server.logic.ws_protocol.JSON.handlers.blockchain;
import blockchain_new.BchBlockEntry_new; import blockchain_new.BchBlockEntry_new;
import shine.db.SqliteDbController;
import shine.db.dao.BlockchainStateDAO; import shine.db.dao.BlockchainStateDAO;
import shine.db.dao.SolanaUsersDAO;
import shine.db.entities.BlockchainStateEntry; import shine.db.entities.BlockchainStateEntry;
import shine.db.entities.SolanaUserEntry;
import utils.files.FileStoreUtil; import utils.files.FileStoreUtil;
import java.sql.Connection;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.Base64; import java.util.Base64;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
@ -34,14 +34,23 @@ 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 --- // ===== locks per blockchainId (MVP: один сервер) =====
private static final ConcurrentHashMap<Long, ReentrantLock> LOCKS = new ConcurrentHashMap<>(); private static final ConcurrentHashMap<Long, ReentrantLock> LOCKS = new ConcurrentHashMap<>();
private static ReentrantLock lockFor(long blockchainId) { private static ReentrantLock lockFor(long blockchainId) {
return LOCKS.computeIfAbsent(blockchainId, id -> new ReentrantLock()); return LOCKS.computeIfAbsent(blockchainId, id -> new ReentrantLock());
} }
public Result addBlockAtomically( // ===== constants =====
private static final String ZERO64 = "0".repeat(64);
// MVP: заглавный блок
// (пока без парсинга тела, просто по номеру)
private static boolean isHeaderBlock(int globalNumber, int lineNumber) {
return globalNumber == 0 && lineNumber == 0;
}
public Result addBlock(
String login, String login,
long blockchainId, long blockchainId,
int globalNumber, int globalNumber,
@ -78,79 +87,141 @@ public final class BlockchainStateService_new {
ReentrantLock lock = lockFor(blockchainId); ReentrantLock lock = lockFor(blockchainId);
lock.lock(); lock.lock();
try (Connection conn = SqliteDbController.getInstance().getConnection()) {
// Транзакция норм, но БЕЗ "BEGIN IMMEDIATE".
boolean oldAuto = conn.getAutoCommit();
conn.setAutoCommit(false);
try { try {
BlockchainStateEntry state = BlockchainStateEntry state = BlockchainStateDAO.getInstance().getByBlockchainId(blockchainId);
BlockchainStateDAO.getInstance().getByBlockchainId(conn, blockchainId);
// ===== GENESIS ветка: state ещё нет =====
if (state == null) { if (state == null) {
conn.rollback(); // разрешаем только заглавный блок
if (!isHeaderBlock(globalNumber, block.lineNumber)) {
return new Result(404, "UNKNOWN_BLOCKCHAIN", null, lineIndex); return new Result(404, "UNKNOWN_BLOCKCHAIN", null, lineIndex);
} }
// создаём первичное состояние (last_global=-1, hash=ZERO64, lines=0/ZERO64)
state = createInitialStateFromUser(login, blockchainId);
if (state == null) {
// нет такого юзера / не его bchId
return new Result(404, "UNKNOWN_BLOCKCHAIN", null, lineIndex);
}
// сохраняем стартовую строку
BlockchainStateDAO.getInstance().upsert(state);
}
// 1) защита от подмены логина
if (!login.equals(state.getUserLogin())) { if (!login.equals(state.getUserLogin())) {
conn.rollback();
return new Result(403, "LOGIN_MISMATCH", state, lineIndex); return new Result(403, "LOGIN_MISMATCH", state, lineIndex);
} }
// 2) expected global: last_global + 1 (у нас last_global стартует -1)
int expectedGlobal = state.getLastGlobalNumber() + 1; int expectedGlobal = state.getLastGlobalNumber() + 1;
if (globalNumber != expectedGlobal) { if (globalNumber != expectedGlobal) {
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();
return new Result(409, "GLOBAL_HASH_MISMATCH", state, lineIndex); return new Result(409, "GLOBAL_HASH_MISMATCH", state, lineIndex);
} }
// 4) lineNumber
// Нормально: первый обычный блок по линии должен быть lineNumber=1 при lastLine=0
// Исключение: заглавный блок имеет lineNumber=0
int expectedLineNumber = state.getLastLineNumber(lineIndex) + 1; int expectedLineNumber = state.getLastLineNumber(lineIndex) + 1;
boolean header = isHeaderBlock(globalNumber, block.lineNumber);
if (!header) {
if (block.lineNumber != expectedLineNumber) { if (block.lineNumber != expectedLineNumber) {
conn.rollback();
return new Result(409, "OUT_OF_SEQUENCE_LINE", state, lineIndex); return new Result(409, "OUT_OF_SEQUENCE_LINE", state, lineIndex);
} }
} else {
// заглавный блок допускаем только если текущий lastLineNumber == 0 и пришёл 0
if (state.getLastLineNumber(lineIndex) != 0 || block.lineNumber != 0) {
return new Result(409, "BAD_HEADER_LINE_NUMBER", state, lineIndex);
}
}
// prevLineHash (пока просто читаем, дальше пригодится для крипто-проверки) // 5) prevLineHash берём из БД (пока просто читаем)
String dbPrevLineHashHex = nn(state.getLastLineHash(lineIndex)); String dbPrevLineHashHex = nn(state.getLastLineHash(lineIndex));
// (можешь позже сравнивать с тем, что внутри блока, если там есть prevLineHash)
// TODO crypto check (потом подключим) // 6) крипто-проверка (позже)
// TODO:
// - восстановить preimage
// - sha256(preimage) == block.hash32
// - Ed25519 verify signature
// если не ок: return new Result(422, "CRYPTO_INVALID", state, lineIndex);
// 1) пишем в файл // 7) запись блока в файл
FileStoreUtil.getInstance().addDataToBlockchain(blockchainId, block.toBytes()); FileStoreUtil.getInstance().addDataToBlockchain(blockchainId, block.toBytes());
// 2) обновляем state в БД // 8) апдейт состояния
state.setLastGlobalNumber(globalNumber); state.setLastGlobalNumber(globalNumber);
state.setLastGlobalHash(bytesToHex(block.getHash32())); state.setLastGlobalHash(bytesToHex(block.getHash32()));
// line number:
// - для заглавного блока оставляем 0
// - для остальных двигаем как обычно
if (!header) {
state.setLastLineNumber(lineIndex, block.lineNumber); state.setLastLineNumber(lineIndex, block.lineNumber);
} else {
state.setLastLineNumber(lineIndex, 0);
}
// line hash обновляем в любом случае (так проще для цепочки)
state.setLastLineHash(lineIndex, bytesToHex(block.getHash32())); state.setLastLineHash(lineIndex, bytesToHex(block.getHash32()));
state.setSizeBytes(state.getSizeBytes() + fullBytes.length); state.setSizeBytes(state.getSizeBytes() + fullBytes.length);
state.setUpdatedAtMs(System.currentTimeMillis()); state.setUpdatedAtMs(System.currentTimeMillis());
BlockchainStateDAO.getInstance().upsert(conn, state); BlockchainStateDAO.getInstance().upsert(state);
conn.commit();
return new Result(200, null, state, lineIndex); return new Result(200, null, state, lineIndex);
} catch (SQLException e) { } catch (SQLException e) {
conn.rollback();
throw e; throw e;
} finally {
conn.setAutoCommit(oldAuto);
}
} finally { } finally {
lock.unlock(); lock.unlock();
} }
} }
/**
* Создаёт стартовое состояние по данным пользователя:
* - проверяем, что login существует и что bchId совпадает с blockchainId
* - public_key_base64 берём из loginKey
*/
private static BlockchainStateEntry createInitialStateFromUser(String login, long blockchainId) throws SQLException {
SolanaUserEntry u = SolanaUsersDAO.getInstance().getByLogin(login);
if (u == null) return null;
if (u.getBchId() != blockchainId) return null;
BlockchainStateEntry s = new BlockchainStateEntry();
s.setBlockchainId(blockchainId);
s.setUserLogin(login);
// публичный ключ для блокчейна = loginKey (как ты и хочешь)
s.setPublicKeyBase64(nn(u.getLoginKey()));
// лимит (пока тестовый / из пользователя)
int limit = (u.getBchLimit() != null) ? u.getBchLimit() : 1_000_000;
s.setSizeLimit(limit);
s.setSizeBytes(0);
// ВАЖНО: стартовые значения
s.setLastGlobalNumber(-1);
s.setLastGlobalHash(ZERO64);
for (int i = 0; i < 8; i++) {
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 String nn(String s) { return s == null ? "" : s; }
private static boolean eqHash(String a, String b) { private static boolean eqHash(String a, String b) {

View File

@ -14,7 +14,7 @@ public final class Net_AddBlock_new_Handler implements JsonMessageHandler {
public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) throws Exception { public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) throws Exception {
Net_AddBlock_new_Request req = (Net_AddBlock_new_Request) baseReq; Net_AddBlock_new_Request req = (Net_AddBlock_new_Request) baseReq;
var r = BlockchainStateService_new.getInstance().addBlockAtomically( var r = BlockchainStateService_new.getInstance().addBlock(
req.getLogin(), req.getLogin(),
req.getBlockchainId(), req.getBlockchainId(),
req.getGlobalNumber(), req.getGlobalNumber(),
@ -32,8 +32,6 @@ public final class Net_AddBlock_new_Handler implements JsonMessageHandler {
resp.setStatus(WireCodes.Status.OK); resp.setStatus(WireCodes.Status.OK);
resp.setReasonCode(null); resp.setReasonCode(null);
} else { } else {
// 409 / 422 / 403 / 404...
// у тебя WireCodes.Status это HTTP-подобное? тогда маппим:
resp.setStatus(r.httpStatus); resp.setStatus(r.httpStatus);
resp.setReasonCode(r.reasonCode); resp.setReasonCode(r.reasonCode);
} }

View File

@ -10,56 +10,45 @@ import server.logic.ws_protocol.JSON.entyties.tempToTest.Net_AddUser_Response;
import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler; import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory; import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
import server.logic.ws_protocol.WireCodes; import server.logic.ws_protocol.WireCodes;
import shine.db.dao.BlockchainStateDAO;
import shine.db.dao.SolanaUsersDAO; import shine.db.dao.SolanaUsersDAO;
import shine.db.entities.BlockchainStateEntry;
import shine.db.entities.SolanaUserEntry; import shine.db.entities.SolanaUserEntry;
import java.sql.SQLException; import java.sql.SQLException;
/**
* Временный хэндлер AddUser (тестовая регистрация локального пользователя).
*
* Ожидаемый запрос (все поля в payload):
* {
* "op": "AddUser",
* "requestId": "...",
* "payload": {
* "login": "anya",
* "loginId": 100211,
* "bchId": 4222,
* "loginKey": "base64-pubkey-login",
* "deviceKey": "base64-pubkey-device",
* "bchLimit": 1000000
* }
* }
*
* При успехе:
* - пользователь сохраняется в таблицу solana_users;
* - возвращается status=200 и пустой payload.
*/
public class Net_AddUser_Handler implements JsonMessageHandler { public class Net_AddUser_Handler implements JsonMessageHandler {
private static final Logger log = LoggerFactory.getLogger(Net_AddUser_Handler.class); private static final Logger log = LoggerFactory.getLogger(Net_AddUser_Handler.class);
// ====== TEST CONST (пока так) ======
private static final int TEST_BCH_LIMIT = 1_000_000;
private static final String ZERO64 = "0".repeat(64);
@Override @Override
public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) throws Exception { public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) throws Exception {
Net_AddUser_Request req = (Net_AddUser_Request) baseRequest; Net_AddUser_Request req = (Net_AddUser_Request) baseRequest;
// Одна общая проверка всех ключевых полей
if (req.getLogin() == null || req.getLogin().isBlank() if (req.getLogin() == null || req.getLogin().isBlank()
|| req.getLoginKey() == null || req.getLoginKey().isBlank() || req.getLoginKey() == null || req.getLoginKey().isBlank()
|| req.getDeviceKey() == null || req.getDeviceKey().isBlank() || req.getDeviceKey() == null || req.getDeviceKey().isBlank()) {
|| req.getBchLimit() == null) {
return NetExceptionResponseFactory.error( return NetExceptionResponseFactory.error(
req, req,
WireCodes.Status.BAD_REQUEST, WireCodes.Status.BAD_REQUEST,
"BAD_FIELDS", "BAD_FIELDS",
"Некорректные или пустые поля: login, loginKey, deviceKey, bchLimit" "Некорректные или пустые поля: login, loginKey, deviceKey"
); );
} }
// bchLimit: если клиент не прислал ставим тестовую константу
Integer limit = req.getBchLimit();
if (limit == null || limit <= 0) limit = TEST_BCH_LIMIT;
try { try {
SolanaUsersDAO dao = SolanaUsersDAO.getInstance(); SolanaUsersDAO users = SolanaUsersDAO.getInstance();
BlockchainStateDAO stateDao = BlockchainStateDAO.getInstance();
SolanaUserEntry user = new SolanaUserEntry( SolanaUserEntry user = new SolanaUserEntry(
req.getLoginId(), req.getLoginId(),
@ -67,21 +56,47 @@ public class Net_AddUser_Handler implements JsonMessageHandler {
req.getBchId(), req.getBchId(),
req.getLoginKey(), req.getLoginKey(),
req.getDeviceKey(), req.getDeviceKey(),
req.getBchLimit() limit
); );
dao.insert(user); users.insert(user);
// Создаём стартовую запись blockchain_state
BlockchainStateEntry s = new BlockchainStateEntry();
s.setBlockchainId(req.getBchId());
s.setUserLogin(req.getLogin());
// В блокчейн-стейте храним loginKey как основной pubkey
s.setPublicKeyBase64(req.getLoginKey());
s.setSizeLimit(limit);
s.setSizeBytes(0);
// ВАЖНО: твои стартовые значения
s.setLastGlobalNumber(-1);
s.setLastGlobalHash(ZERO64);
for (int i = 0; i < 8; i++) {
s.setLastLineNumber(i, 0);
s.setLastLineHash(i, ZERO64);
}
s.setUpdatedAtMs(System.currentTimeMillis());
stateDao.upsert(s);
Net_AddUser_Response resp = new Net_AddUser_Response(); Net_AddUser_Response resp = new Net_AddUser_Response();
resp.setOp(req.getOp()); resp.setOp(req.getOp());
resp.setRequestId(req.getRequestId()); resp.setRequestId(req.getRequestId());
resp.setStatus(WireCodes.Status.OK); resp.setStatus(WireCodes.Status.OK);
// payload станет {} через JsonInboundProcessor
log.info("✅ Пользователь добавлен: login={}, loginId={}", req.getLogin(), req.getLoginId()); log.info("✅ AddUser ok: login={}, loginId={}, bchId={}, limit={}",
req.getLogin(), req.getLoginId(), req.getBchId(), limit);
return resp; return resp;
} catch (SQLException e) { } catch (SQLException e) {
log.error("❌ Ошибка при вставке пользователя в БД", e); log.error("DB error in AddUser", e);
return NetExceptionResponseFactory.error( return NetExceptionResponseFactory.error(
req, req,
WireCodes.Status.SERVER_DATA_ERROR, WireCodes.Status.SERVER_DATA_ERROR,
@ -89,7 +104,7 @@ public class Net_AddUser_Handler implements JsonMessageHandler {
"Ошибка доступа к базе данных" "Ошибка доступа к базе данных"
); );
} catch (Exception e) { } catch (Exception e) {
log.error("Неожиданная ошибка в AddUser", e); log.error("Internal error in AddUser", e);
return NetExceptionResponseFactory.error( return NetExceptionResponseFactory.error(
req, req,
WireCodes.Status.INTERNAL_ERROR, WireCodes.Status.INTERNAL_ERROR,