24 12 25
Дорабатываю добавление блоков. Ура добавилось. Объеденил в один Хэндлер и сделал атомарную запись в БД.
This commit is contained in:
parent
834cf98ef9
commit
a309b6f3ef
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user