Дорабатываю добавление блоков. Ура добавилось.
Объеденил в один Хэндлер и сделал атомарную запись в БД.
This commit is contained in:
AidarKC 2025-12-24 16:42:26 +03:00
parent 834cf98ef9
commit a309b6f3ef
3 changed files with 340 additions and 252 deletions

View File

@ -1,5 +1,6 @@
package server.logic.ws_protocol.JSON.handlers.blockchain; package server.logic.ws_protocol.JSON.handlers.blockchain;
import shine.db.SqliteDbController;
import shine.db.dao.BlockchainStateDAO; import shine.db.dao.BlockchainStateDAO;
import shine.db.dao.BlocksDAO; import shine.db.dao.BlocksDAO;
import shine.db.entities.BlockEntry; import shine.db.entities.BlockEntry;
@ -8,16 +9,76 @@ import shine.db.entities.BlockchainStateEntry;
import java.sql.Connection; import java.sql.Connection;
import java.sql.SQLException; import java.sql.SQLException;
/**
* BlockchainDbWriter единая точка записи блока + состояния в БД.
*
* Важно:
* - Здесь обеспечивается атомарность записи: либо вставился блок и обновилось состояние, либо не вставилось ничего.
* - Соединение открывается/закрывается внутри (удобно для хэндлера).
* - При необходимости можно вызвать appendBlockAndState(Connection, ...) и управлять транзакцией снаружи.
*/
public final class BlockchainDbWriter { public final class BlockchainDbWriter {
private final SqliteDbController db;
private final BlocksDAO blocksDAO; private final BlocksDAO blocksDAO;
private final BlockchainStateDAO stateDAO; private final BlockchainStateDAO stateDAO;
public BlockchainDbWriter(BlocksDAO blocksDAO, BlockchainStateDAO stateDAO) { public BlockchainDbWriter(BlocksDAO blocksDAO, BlockchainStateDAO stateDAO) {
this.db = SqliteDbController.getInstance();
this.blocksDAO = blocksDAO; this.blocksDAO = blocksDAO;
this.stateDAO = stateDAO; this.stateDAO = stateDAO;
} }
/**
* Публичный метод: сам открывает соединение, делает транзакцию и закрывает соединение.
*
* @return true если всё записалось успешно, иначе кидает SQLException (или IllegalStateException выше по коду).
*/
public void appendBlockAndState(
String login,
String blockchainName,
int globalNumber,
String prevGlobalHashHex,
byte[] blockBytes,
BlockchainStateEntry stOrNull,
String newHashHex
) throws SQLException {
// 1) Открываем соединение (try-with-resources гарантирует закрытие)
try (Connection c = db.getConnection()) {
// 2) Включаем ручное управление транзакцией
boolean oldAutoCommit = c.getAutoCommit();
c.setAutoCommit(false);
try {
// 3) Внутри одной транзакции:
// - вставляем строку блока
// - обновляем/создаём blockchain_state
appendBlockAndState(c, login, blockchainName, globalNumber, prevGlobalHashHex, blockBytes, stOrNull, newHashHex);
// 4) Фиксируем транзакцию
c.commit();
} catch (Exception e) {
// 5) Если что-то упало откатываем транзакцию, чтобы не было "полузаписей"
try { c.rollback(); } catch (SQLException ignore) {}
// Пробрасываем как SQLException (чтобы вызывающий код мог отдать internal_error и т.п.)
if (e instanceof SQLException se) throw se;
throw new SQLException("appendBlockAndState failed", e);
} finally {
// 6) Возвращаем autoCommit как было
try { c.setAutoCommit(oldAutoCommit); } catch (SQLException ignore) {}
}
}
}
/**
* Внутренний/расширенный метод: запись в рамках УЖЕ открытого соединения.
* Удобно если снаружи хотят объединить несколько действий в одну транзакцию.
*/
public void appendBlockAndState( public void appendBlockAndState(
Connection c, Connection c,
String login, String login,
@ -29,24 +90,34 @@ public final class BlockchainDbWriter {
String newHashHex String newHashHex
) throws SQLException { ) throws SQLException {
// A) Вставляем блок (строка в таблицу blocks)
insertBlockRow(c, login, blockchainName, globalNumber, prevGlobalHashHex, blockBytes); insertBlockRow(c, login, blockchainName, globalNumber, prevGlobalHashHex, blockBytes);
// B) Обновляем состояние blockchain_state (создаём если отсутствует)
BlockchainStateEntry st = stOrNull; BlockchainStateEntry st = stOrNull;
if (st == null) { if (st == null) {
st = new BlockchainStateEntry(); st = new BlockchainStateEntry();
st.setBlockchainName(blockchainName); st.setBlockchainName(blockchainName);
} }
// Последний глобальный блок
st.setLastGlobalNumber(globalNumber); st.setLastGlobalNumber(globalNumber);
st.setLastGlobalHash(newHashHex); st.setLastGlobalHash(newHashHex);
// Пока линии не используются: lineIndex=0 и lineHash = globalHash
st.setLastLineNumber(0, globalNumber); st.setLastLineNumber(0, globalNumber);
st.setLastLineHash(0, newHashHex); st.setLastLineHash(0, newHashHex);
// Метка времени обновления
st.setUpdatedAtMs(System.currentTimeMillis()); st.setUpdatedAtMs(System.currentTimeMillis());
// UPSERT состояния
stateDAO.upsert(c, st); stateDAO.upsert(c, st);
} }
/**
* Вставка/апдейт строки блока в blocks.
*/
private void insertBlockRow( private void insertBlockRow(
Connection c, Connection c,
String login, String login,
@ -58,24 +129,32 @@ public final class BlockchainDbWriter {
BlockEntry e = new BlockEntry(); BlockEntry e = new BlockEntry();
// Кому принадлежит блок (логин владельца цепочки)
e.setLogin(login); e.setLogin(login);
e.setBchName(blockchainName); e.setBchName(blockchainName);
// Глобальная нумерация
e.setBlockGlobalNumber(globalNumber); e.setBlockGlobalNumber(globalNumber);
e.setBlockGlobalPreHashe(prevGlobalHashHex); e.setBlockGlobalPreHashe(prevGlobalHashHex);
// Линии пока не используются: lineIndex=0, lineNumber=globalNumber
e.setBlockLineIndex(0); e.setBlockLineIndex(0);
e.setBlockLineNumber(globalNumber); e.setBlockLineNumber(globalNumber);
e.setBlockLinePreHashe(prevGlobalHashHex); e.setBlockLinePreHashe(prevGlobalHashHex);
// msgType у тебя пока 0 (при желании позже можно ставить по Body/type)
e.setMsgType(0); e.setMsgType(0);
// Сырые байты полного блока
e.setBlockByte(blockBytes); e.setBlockByte(blockBytes);
// Поля "кому" (для сообщений/трансферов) пока пустые
e.setToLogin(null); e.setToLogin(null);
e.setToBchName(null); e.setToBchName(null);
e.setToBlockGlobalNumber(null); e.setToBlockGlobalNumber(null);
e.setToBlockHashe(null); e.setToBlockHashe(null);
// UPSERT блока
blocksDAO.upsert(c, e); blocksDAO.upsert(c, e);
} }
} }

View File

@ -1,243 +0,0 @@
package server.logic.ws_protocol.JSON.handlers.blockchain;
import blockchain.BchBlockEntry;
import blockchain.BchCryptoVerifier;
import blockchain.body.BodyRecordParser;
import server.logic.ws_protocol.WireCodes;
import shine.db.SqliteDbController;
import shine.db.dao.BlockchainStateDAO;
import shine.db.dao.BlocksDAO;
import shine.db.dao.SolanaUsersDAO;
import shine.db.entities.BlockchainStateEntry;
import shine.db.entities.SolanaUserEntry;
import utils.blockchain.BlockchainNameUtil;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Base64;
/**
* BlockchainStateService_new атомарное добавление блока (НОВЫЙ формат):
* - decode Base64 -> FULL block bytes
* - parse block (recordSize must match)
* - взять loginKey (publicKey32) пользователя
* - взять prevGlobalHash / prevLineHash из DB-состояния
* - собрать preimage -> sha256 -> verify signature
* - вставить blocks
* - обновить blockchain_state: lastGlobalNumber/lastGlobalHash (и позже line stuff)
*
* Ответ наружу: только reasonCode + serverLastGlobalNumber/serverLastGlobalHash
*/
public final class BlockchainStateService {
/** Результат атомарного addBlock */
public static final class AddBlockResult {
public final int httpStatus; // WireCodes.Status.*
public final String reasonCode; // null если ok
public final int serverLastGlobalNumber;
public final String serverLastGlobalHash;
public AddBlockResult(int httpStatus, String reasonCode, int serverLastGlobalNumber, String serverLastGlobalHash) {
this.httpStatus = httpStatus;
this.reasonCode = reasonCode;
this.serverLastGlobalNumber = serverLastGlobalNumber;
this.serverLastGlobalHash = serverLastGlobalHash;
}
public boolean isOk() {
return httpStatus == WireCodes.Status.OK;
}
}
private static volatile BlockchainStateService instance;
private final SqliteDbController db = SqliteDbController.getInstance();
private final BlocksDAO blocksDAO = BlocksDAO.getInstance();
private final BlockchainStateDAO stateDAO = BlockchainStateDAO.getInstance();
private final SolanaUsersDAO solanaUsersDAO = SolanaUsersDAO.getInstance();
private final BlockchainDbWriter dbWriter = new BlockchainDbWriter(blocksDAO, stateDAO);
private BlockchainStateService() {}
public static BlockchainStateService getInstance() {
if (instance == null) {
synchronized (BlockchainStateService.class) {
if (instance == null) instance = new BlockchainStateService();
}
}
return instance;
}
public AddBlockResult addBlockAtomically(
String blockchainName,
int globalNumber,
String prevGlobalHashHex,
String blockBytesB64
) {
if (blockchainName == null || blockchainName.isBlank())
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "empty_blockchain_name", 0, "");
String login = BlockchainNameUtil.loginFromBlockchainName(blockchainName);
if (login == null || login.isBlank())
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_blockchain_name", 0, "");
byte[] blockBytes;
try {
blockBytes = decodeBase64(blockBytesB64);
} catch (Exception e) {
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_block_base64", 0, "");
}
final BchBlockEntry block;
try {
block = new BchBlockEntry(blockBytes);
} catch (Exception e) {
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_block_format", 0, "");
}
try {
BodyRecordParser.parse(block.bodyBytes).check();
} catch (Exception e) {
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_block_body", 0, "");
}
if (block.recordNumber != globalNumber) {
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "global_number_mismatch", 0, "");
}
try (Connection c = db.getConnection()) {
boolean oldAutoCommit = c.getAutoCommit();
c.setAutoCommit(false);
try {
SolanaUserEntry u = solanaUsersDAO.getByLogin(c, login);
if (u == null) {
c.rollback();
return new AddBlockResult(WireCodes.Status.NOT_FOUND, "user_not_found", 0, "");
}
byte[] loginKey32 = u.getLoginKeyByte();
if (loginKey32 == null || loginKey32.length != 32) {
c.rollback();
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_user_login_key", 0, "");
}
BlockchainStateEntry st = stateDAO.getByBlockchainName(c, blockchainName);
int serverLastNum;
String serverLastHash;
if (st == null) {
if (globalNumber != 0) {
c.rollback();
return new AddBlockResult(WireCodes.Status.NOT_FOUND, "blockchain_state_not_found", 0, "");
}
serverLastNum = -1;
serverLastHash = "";
} else {
serverLastNum = st.getLastGlobalNumber();
serverLastHash = nn(st.getLastGlobalHash());
}
int expected = serverLastNum + 1;
if (globalNumber != expected) {
c.rollback();
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_global_number", serverLastNum, serverLastHash);
}
byte[] prevGlobalHash32 = hexTo32(nn(prevGlobalHashHex));
byte[] serverPrevGlobal32 = (st == null) ? new byte[32] : hexTo32(nn(st.getLastGlobalHash()));
if (!bytesEq(prevGlobalHash32, serverPrevGlobal32)) {
c.rollback();
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_prev_global_hash", serverLastNum, serverLastHash);
}
byte[] prevLineHash32 = prevGlobalHash32;
boolean ok = BchCryptoVerifier.verifyAll(
login,
prevGlobalHash32,
prevLineHash32,
block.getRawBytes(),
block.getSignature64(),
loginKey32,
block.getHash32()
);
if (!ok) {
c.rollback();
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_signature_or_hash", serverLastNum, serverLastHash);
}
String newHashHex = toHex(block.getHash32());
dbWriter.appendBlockAndState(
c,
login,
blockchainName,
globalNumber,
nn(prevGlobalHashHex),
blockBytes,
st,
newHashHex
);
c.commit();
return new AddBlockResult(WireCodes.Status.OK, null, globalNumber, newHashHex);
} catch (Exception e) {
try { c.rollback(); } catch (SQLException ignore) {}
return new AddBlockResult(WireCodes.Status.INTERNAL_ERROR, "internal_error", 0, "");
} finally {
try { c.setAutoCommit(oldAutoCommit); } catch (SQLException ignore) {}
}
} catch (Exception e) {
return new AddBlockResult(WireCodes.Status.INTERNAL_ERROR, "db_error", 0, "");
}
}
// -------------------- utils --------------------
private static String nn(String s) { return s == null ? "" : s; }
private static byte[] decodeBase64(String s) {
if (s == null || s.isBlank()) throw new IllegalArgumentException("empty base64");
return Base64.getDecoder().decode(s);
}
/** hex(64) -> 32 bytes; пустой -> 32 нуля */
private static byte[] hexTo32(String hex) {
if (hex == null || hex.isBlank()) return new byte[32];
String h = hex.trim();
if (h.length() != 64) throw new IllegalArgumentException("hex hash must be 64 chars");
byte[] out = new byte[32];
for (int i = 0; i < 32; i++) {
int hi = Character.digit(h.charAt(i * 2), 16);
int lo = Character.digit(h.charAt(i * 2 + 1), 16);
if (hi < 0 || lo < 0) throw new IllegalArgumentException("bad hex");
out[i] = (byte)((hi << 4) | lo);
}
return out;
}
private static boolean bytesEq(byte[] a, byte[] b) {
if (a == b) return true;
if (a == null || b == null) return false;
if (a.length != b.length) return false;
int x = 0;
for (int i = 0; i < a.length; i++) x |= (a[i] ^ b[i]);
return x == 0;
}
private static String toHex(byte[] bytes) {
char[] HEX = "0123456789abcdef".toCharArray();
char[] out = new char[bytes.length * 2];
for (int i = 0; i < bytes.length; i++) {
int v = bytes[i] & 0xFF;
out[i * 2] = HEX[v >>> 4];
out[i * 2 + 1] = HEX[v & 0x0F];
}
return new String(out);
}
}

View File

@ -1,5 +1,8 @@
package server.logic.ws_protocol.JSON.handlers.blockchain; package server.logic.ws_protocol.JSON.handlers.blockchain;
import blockchain.BchBlockEntry;
import blockchain.BchCryptoVerifier;
import blockchain.body.BodyRecordParser;
import server.logic.ws_protocol.JSON.ConnectionContext; import server.logic.ws_protocol.JSON.ConnectionContext;
import server.logic.ws_protocol.JSON.entyties.Net_Request; import server.logic.ws_protocol.JSON.entyties.Net_Request;
import server.logic.ws_protocol.JSON.entyties.Net_Response; import server.logic.ws_protocol.JSON.entyties.Net_Response;
@ -7,27 +10,54 @@ import server.logic.ws_protocol.JSON.entyties.blockchain.Net_AddBlock_Request;
import server.logic.ws_protocol.JSON.entyties.blockchain.Net_AddBlock_Response; import server.logic.ws_protocol.JSON.entyties.blockchain.Net_AddBlock_Response;
import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler; import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
import server.logic.ws_protocol.WireCodes; import server.logic.ws_protocol.WireCodes;
import shine.db.dao.BlockchainStateDAO;
import shine.db.dao.BlocksDAO;
import shine.db.dao.SolanaUsersDAO;
import shine.db.entities.BlockchainStateEntry;
import shine.db.entities.SolanaUserEntry;
import utils.blockchain.BlockchainNameUtil;
import java.util.Base64;
import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.ReentrantLock;
/**
* Net_AddBlock_Handler единый хэндлер добавления блока (JSON).
*
* Задачи:
* 1) Лочим добавление блоков для конкретного blockchainName (защита от гонок в одном процессе).
* 2) Декодируем блок из Base64 и парсим его структуру.
* 3) Парсим body и валидируем (type/version + содержимое).
* 4) Проверяем globalNumber и prevGlobalHash относительно server state.
* 5) Проверяем подпись/хэш (Ed25519 над hash32, hash32=sha256(preimage)).
* 6) Делаем запись в БД через BlockchainDbWriter (атомарность реализуется там).
* 7) Возвращаем клиенту serverLastGlobalNumber/serverLastGlobalHash.
*/
public final class Net_AddBlock_Handler implements JsonMessageHandler { public final class Net_AddBlock_Handler implements JsonMessageHandler {
// DAO (перегрузки сами создают/закрывают Connection внутри)
private final BlocksDAO blocksDAO = BlocksDAO.getInstance();
private final BlockchainStateDAO stateDAO = BlockchainStateDAO.getInstance();
private final SolanaUsersDAO solanaUsersDAO = SolanaUsersDAO.getInstance();
// Writer отвечает за транзакции/атомарность и консистентность БД
private final BlockchainDbWriter dbWriter = new BlockchainDbWriter(blocksDAO, stateDAO);
@Override @Override
public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) throws Exception { public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) {
Net_AddBlock_Request req = (Net_AddBlock_Request) baseReq; Net_AddBlock_Request req = (Net_AddBlock_Request) baseReq;
String bchName = req.getBlockchainName(); // 0) Берём имя цепочки и лочим операции добавления для неё
ReentrantLock lock = BlockchainLocks.lockFor(bchName); String blockchainName = req.getBlockchainName();
ReentrantLock lock = BlockchainLocks.lockFor(blockchainName);
lock.lock(); lock.lock();
try { try {
var r = BlockchainStateService.getInstance().addBlockAtomically( AddBlockResult r = addBlock(blockchainName,
req.getBlockchainName(),
req.getGlobalNumber(), req.getGlobalNumber(),
req.getPrevGlobalHash(), req.getPrevGlobalHash(),
req.getBlockBytesB64() req.getBlockBytesB64());
);
// 7) Формируем стандартный Net_AddBlock_Response
Net_AddBlock_Response resp = new Net_AddBlock_Response(); Net_AddBlock_Response resp = new Net_AddBlock_Response();
resp.setOp(req.getOp()); resp.setOp(req.getOp());
resp.setRequestId(req.getRequestId()); resp.setRequestId(req.getRequestId());
@ -40,6 +70,7 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
resp.setReasonCode(r.reasonCode); resp.setReasonCode(r.reasonCode);
} }
// Возвращаем актуальное состояние сервера (даже при ошибках, где уместно)
resp.setServerLastGlobalNumber(r.serverLastGlobalNumber); resp.setServerLastGlobalNumber(r.serverLastGlobalNumber);
if (r.serverLastGlobalHash != null) { if (r.serverLastGlobalHash != null) {
resp.setServerLastGlobalHash(r.serverLastGlobalHash); resp.setServerLastGlobalHash(r.serverLastGlobalHash);
@ -51,4 +82,225 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
lock.unlock(); lock.unlock();
} }
} }
}
/* ===================================================================== */
/* ========================== Основная логика =========================== */
/* ===================================================================== */
/**
* Внутренняя логика добавления блока (без ручного управления Connection/tx).
* Все атомарные записи внутри BlockchainDbWriter.
*/
private AddBlockResult addBlock(
String blockchainName,
int globalNumber,
String prevGlobalHashHex,
String blockBytesB64
) {
// 1) Быстрая валидация входных параметров
if (blockchainName == null || blockchainName.isBlank()) {
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "empty_blockchain_name", 0, "");
}
// 2) Из имени блокчейна вытаскиваем login (как ты и хотел через util)
String login = BlockchainNameUtil.loginFromBlockchainName(blockchainName);
if (login == null || login.isBlank()) {
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_blockchain_name", 0, "");
}
// 3) Декодируем блок из Base64
final byte[] blockBytes;
try {
blockBytes = decodeBase64(blockBytesB64);
} catch (Exception e) {
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_block_base64", 0, "");
}
// 4) Парсим блок (проверяется recordSize и минимальная длина)
final BchBlockEntry block;
try {
block = new BchBlockEntry(blockBytes);
} catch (Exception e) {
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_block_format", 0, "");
}
// 5) Парсим и валидируем body (type/version + содержимое)
try {
BodyRecordParser.parse(block.bodyBytes).check();
} catch (Exception e) {
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_block_body", 0, "");
}
// 6) Защита от рассинхрона: recordNumber внутри блока должен совпадать с заявленным globalNumber
if (block.recordNumber != globalNumber) {
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "global_number_mismatch", 0, "");
}
// 7) Получаем пользователя и его loginKey (публичный ключ 32 байта)
SolanaUserEntry u;
try {
u = solanaUsersDAO.getByLogin(login); // перегрузка: сама открывает/закрывает соединение
} catch (Exception e) {
return new AddBlockResult(WireCodes.Status.INTERNAL_ERROR, "db_error", 0, "");
}
if (u == null) {
return new AddBlockResult(WireCodes.Status.NOT_FOUND, "user_not_found", 0, "");
}
byte[] loginKey32 = u.getLoginKeyByte();
if (loginKey32 == null || loginKey32.length != 32) {
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_user_login_key", 0, "");
}
// 8) Читаем текущее состояние блокчейна с сервера
BlockchainStateEntry st;
try {
st = stateDAO.getByBlockchainName(blockchainName); // перегрузка: сама открывает/закрывает соединение
} catch (Exception e) {
return new AddBlockResult(WireCodes.Status.INTERNAL_ERROR, "db_error", 0, "");
}
// 9) Определяем serverLastNum/serverLastHash (если state ещё нет ожидаем genesis с globalNumber=0)
final int serverLastNum;
final String serverLastHash;
if (st == null) {
if (globalNumber != 0) {
return new AddBlockResult(WireCodes.Status.NOT_FOUND, "blockchain_state_not_found", 0, "");
}
serverLastNum = -1;
serverLastHash = "";
} else {
serverLastNum = st.getLastGlobalNumber();
serverLastHash = nn(st.getLastGlobalHash());
}
// 10) Проверяем, что клиент присылает следующий блок ровно (last+1)
int expected = serverLastNum + 1;
if (globalNumber != expected) {
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_global_number", serverLastNum, serverLastHash);
}
// 11) Проверяем prevGlobalHash: клиент должен ссылаться на текущий serverLastHash
final byte[] prevGlobalHash32;
final byte[] serverPrevGlobal32;
try {
prevGlobalHash32 = hexTo32(nn(prevGlobalHashHex));
serverPrevGlobal32 = (st == null) ? new byte[32] : hexTo32(nn(st.getLastGlobalHash()));
} catch (Exception e) {
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_prev_global_hash_format", serverLastNum, serverLastHash);
}
if (!bytesEq(prevGlobalHash32, serverPrevGlobal32)) {
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_prev_global_hash", serverLastNum, serverLastHash);
}
// 12) Пока линии не используем prevLineHash равен prevGlobalHash (как ты писал)
byte[] prevLineHash32 = prevGlobalHash32;
// 13) Криптопроверка: hash в блоке + подпись над hash
boolean ok = BchCryptoVerifier.verifyAll(
login,
prevGlobalHash32,
prevLineHash32,
block.getRawBytes(), // только RAW (без signature/hash)
block.getSignature64(), // подпись Ed25519
loginKey32, // public key пользователя
block.getHash32() // ожидаемый hash32 из самого блока
);
if (!ok) {
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_signature_or_hash", serverLastNum, serverLastHash);
}
// 14) Новый hash блока (hex) то, что будет записано как lastGlobalHash
String newHashHex = toHex(block.getHash32());
// 15) Запись блока + обновление состояния (атомарность/транзакции внутри dbWriter)
try {
dbWriter.appendBlockAndState(
login,
blockchainName,
globalNumber,
nn(prevGlobalHashHex),
blockBytes,
st,
newHashHex
);
} catch (Exception e) {
return new AddBlockResult(WireCodes.Status.INTERNAL_ERROR, "internal_error", serverLastNum, serverLastHash);
}
// 16) Успех
return new AddBlockResult(WireCodes.Status.OK, null, globalNumber, newHashHex);
}
/* ===================================================================== */
/* ============================= Result ================================= */
/* ===================================================================== */
/** Результат обработки addBlock */
private static final class AddBlockResult {
final int httpStatus; // WireCodes.Status.*
final String reasonCode; // null если ok
final int serverLastGlobalNumber;
final String serverLastGlobalHash;
AddBlockResult(int httpStatus, String reasonCode, int serverLastGlobalNumber, String serverLastGlobalHash) {
this.httpStatus = httpStatus;
this.reasonCode = reasonCode;
this.serverLastGlobalNumber = serverLastGlobalNumber;
this.serverLastGlobalHash = serverLastGlobalHash;
}
boolean isOk() {
return httpStatus == WireCodes.Status.OK;
}
}
/* ===================================================================== */
/* ============================== Utils ================================= */
/* ===================================================================== */
private static String nn(String s) { return s == null ? "" : s; }
private static byte[] decodeBase64(String s) {
if (s == null || s.isBlank()) throw new IllegalArgumentException("empty base64");
return Base64.getDecoder().decode(s);
}
/** hex(64) -> 32 bytes; пустой -> 32 нуля */
private static byte[] hexTo32(String hex) {
if (hex == null || hex.isBlank()) return new byte[32];
String h = hex.trim();
if (h.length() != 64) throw new IllegalArgumentException("hex hash must be 64 chars");
byte[] out = new byte[32];
for (int i = 0; i < 32; i++) {
int hi = Character.digit(h.charAt(i * 2), 16);
int lo = Character.digit(h.charAt(i * 2 + 1), 16);
if (hi < 0 || lo < 0) throw new IllegalArgumentException("bad hex");
out[i] = (byte)((hi << 4) | lo);
}
return out;
}
private static boolean bytesEq(byte[] a, byte[] b) {
if (a == b) return true;
if (a == null || b == null) return false;
if (a.length != b.length) return false;
int x = 0;
for (int i = 0; i < a.length; i++) x |= (a[i] ^ b[i]);
return x == 0;
}
private static String toHex(byte[] bytes) {
char[] HEX = "0123456789abcdef".toCharArray();
char[] out = new char[bytes.length * 2];
for (int i = 0; i < bytes.length; i++) {
int v = bytes[i] & 0xFF;
out[i * 2] = HEX[v >>> 4];
out[i * 2 + 1] = HEX[v & 0x0F];
}
return new String(out);
}
}