SHA256
13 01 25
Переписал код кучи классов перешёл на новый надеюсь теперь подходящий формат блоков и тесты переделал. Но пока остались баги и тесты не проходят (в частности пользователи не создаются - ошибка в бд)
This commit is contained in:
+3
-3
@@ -24,7 +24,7 @@ import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_ListUserPa
|
||||
import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_UpsertUserParam_Request;
|
||||
|
||||
// !!! подставь реальные пакеты/имена, как у тебя в проекте:
|
||||
import server.logic.ws_protocol.JSON.handlers.subscriptions.Net_GetSubscribedChannels_Handler;
|
||||
//import server.logic.ws_protocol.JSON.handlers.subscriptions.Net_GetSubscribedChannels_Handler;
|
||||
import server.logic.ws_protocol.JSON.handlers.subscriptions.entyties.Net_GetSubscribedChannels_Request;
|
||||
|
||||
import java.util.Map;
|
||||
@@ -48,10 +48,10 @@ public final class JsonHandlerRegistry {
|
||||
// --- userParams ---
|
||||
Map.entry("UpsertUserParam", new Net_UpsertUserParam_Handler()),
|
||||
Map.entry("GetUserParam", new Net_GetUserParam_Handler()),
|
||||
Map.entry("ListUserParams", new Net_ListUserParams_Handler()),
|
||||
Map.entry("ListUserParams", new Net_ListUserParams_Handler())
|
||||
|
||||
// --- subscriptions ---
|
||||
Map.entry("ListSubscribedChannels", new Net_GetSubscribedChannels_Handler())
|
||||
// Map.entry("ListSubscribedChannels", new Net_GetSubscribedChannels_Handler())
|
||||
);
|
||||
|
||||
private static final Map<String, Class<? extends Net_Request>> REQUEST_TYPES = Map.ofEntries(
|
||||
|
||||
+179
-243
@@ -1,40 +1,52 @@
|
||||
// =======================
|
||||
// server/logic/ws_protocol/JSON/handlers/blockchain/Net_AddBlock_Handler.java (ИЗМЕНЁННЫЙ под ТЗ)
|
||||
// =======================
|
||||
package server.logic.ws_protocol.JSON.handlers.blockchain;
|
||||
|
||||
import blockchain.BchBlockEntry;
|
||||
import blockchain.BchCryptoVerifier;
|
||||
import blockchain.body.BodyHasLine;
|
||||
import blockchain.body.BodyHasTarget;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import server.logic.ws_protocol.JSON.ConnectionContext;
|
||||
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
||||
import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
|
||||
import server.logic.ws_protocol.JSON.handlers.blockchain.Net_AddBlock_Handler_utils.BlockchainLocks;
|
||||
import server.logic.ws_protocol.JSON.handlers.blockchain.Net_AddBlock_Handler_utils.BlockchainWriter;
|
||||
import server.logic.ws_protocol.JSON.handlers.blockchain.entyties.Net_AddBlock_Request;
|
||||
import server.logic.ws_protocol.JSON.handlers.blockchain.entyties.Net_AddBlock_Response;
|
||||
import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
|
||||
import server.logic.ws_protocol.WireCodes;
|
||||
import shine.db.dao.BlockchainStateDAO;
|
||||
import shine.db.dao.BlocksDAO;
|
||||
import shine.db.entities.BlockchainStateEntry;
|
||||
import shine.db.entities.BlockEntry;
|
||||
import utils.blockchain.BlockchainNameUtil;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
/**
|
||||
* Net_AddBlock_Handler — единый хэндлер добавления блока (JSON).
|
||||
*
|
||||
* Задачи:
|
||||
* 1) Лочим добавление блоков для конкретного blockchainName (защита от гонок в одном процессе).
|
||||
* 2) Декодируем блок из Base64 и парсим его структуру.
|
||||
* 3) Валидируем body (type/version + содержимое).
|
||||
* 4) Проверяем globalNumber и prevGlobalHash относительно server state.
|
||||
* 5) Проверяем линии:
|
||||
* - genesis: global=0, lineIndex=0, lineNumber=0
|
||||
* - остальные: lineIndex=1..7, lineNumber по счётчику линии
|
||||
* 6) Проверяем подпись/хэш (Ed25519 над hash32, hash32=sha256(preimage)).
|
||||
* preimage включает prevLineHash32 (берём из state по lineIndex).
|
||||
* 7) Пишем в БД+файл через BlockchainWriter (атомарность там).
|
||||
* Новый порядок валидации (ТЗ):
|
||||
* 1) Достаём из blockchain_state: last_block_number, last_block_hash
|
||||
* 2) Проверяем:
|
||||
* - incoming.blockNumber == last+1
|
||||
* - incoming.prevHash32 == last_hash (для genesis last_hash = 32 нулей)
|
||||
* 3) Считаем hash32 = SHA-256(preimage) (preimage = block_bytes без signature64)
|
||||
* 4) Проверяем подпись Ed25519.verify(hash32, signature64, pubKey)
|
||||
* 5) Если тип имеет линию:
|
||||
* - если prevLineNumber != -1:
|
||||
* достаём hash блока prevLineNumber из blocks
|
||||
* сравниваем с prevLineHash32 из body
|
||||
* 6) Сохраняем блок в blocks + обновляем blockchain_state
|
||||
*
|
||||
* Важно:
|
||||
* - Сетевой протокол AddBlock пока оставляем старые поля (globalNumber/prevGlobalHash),
|
||||
* но внутренняя логика использует НОВЫЙ формат блока.
|
||||
*/
|
||||
public final class Net_AddBlock_Handler implements JsonMessageHandler {
|
||||
|
||||
@@ -56,8 +68,8 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
|
||||
try {
|
||||
AddBlockResult r = addBlock(
|
||||
blockchainName,
|
||||
req.getGlobalNumber(),
|
||||
req.getPrevGlobalHash(),
|
||||
req.getGlobalNumber(), // старое поле, пока оставляем
|
||||
req.getPrevGlobalHash(), // старое поле, пока оставляем
|
||||
req.getBlockBytesB64()
|
||||
);
|
||||
|
||||
@@ -73,8 +85,8 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
|
||||
resp.setReasonCode(r.reasonCode);
|
||||
}
|
||||
|
||||
resp.setServerLastGlobalNumber(r.serverLastGlobalNumber);
|
||||
resp.setServerLastGlobalHash(r.serverLastGlobalHashHex);
|
||||
resp.setServerLastGlobalNumber(r.serverLastBlockNumber);
|
||||
resp.setServerLastGlobalHash(r.serverLastBlockHashHex);
|
||||
|
||||
return resp;
|
||||
|
||||
@@ -85,313 +97,237 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
|
||||
|
||||
private AddBlockResult addBlock(
|
||||
String blockchainName,
|
||||
int globalNumber,
|
||||
String prevGlobalHashHex,
|
||||
int globalNumberFromReq,
|
||||
String prevGlobalHashHexFromReq,
|
||||
String blockBytesB64
|
||||
) {
|
||||
if (blockchainName == null || blockchainName.isBlank()) {
|
||||
log.warn("AddBlock: пустой blockchainName (globalNumber={})", globalNumber);
|
||||
log.warn("AddBlock: пустой blockchainName (reqGlobalNumber={})", globalNumberFromReq);
|
||||
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "empty_blockchain_name", 0, "");
|
||||
}
|
||||
|
||||
String login = BlockchainNameUtil.loginFromBlockchainName(blockchainName);
|
||||
if (login == null || login.isBlank()) {
|
||||
log.warn("AddBlock: плохой blockchainName='{}' => login не получился (globalNumber={})",
|
||||
blockchainName, globalNumber);
|
||||
log.warn("AddBlock: плохой blockchainName='{}' => login не получился (reqGlobalNumber={})",
|
||||
blockchainName, globalNumberFromReq);
|
||||
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_blockchain_name", 0, "");
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// ✅ 1) state теперь ОБЯЗАТЕЛЕН (и ключ подписи берём из него)
|
||||
// -------------------------------------------------------------------
|
||||
// 1) state обязателен
|
||||
final BlockchainStateEntry st;
|
||||
try {
|
||||
st = stateDAO.getByBlockchainName(blockchainName);
|
||||
} catch (Exception e) {
|
||||
log.error("AddBlock: ошибка БД при чтении blockchain_state (login={}, blockchainName={}, globalNumber={})",
|
||||
login, blockchainName, globalNumber, e);
|
||||
log.error("AddBlock: ошибка БД при чтении blockchain_state (login={}, blockchainName={}, reqGlobalNumber={})",
|
||||
login, blockchainName, globalNumberFromReq, e);
|
||||
return new AddBlockResult(WireCodes.Status.INTERNAL_ERROR, "db_error", 0, "");
|
||||
}
|
||||
|
||||
if (st == null) {
|
||||
log.warn("AddBlock: blockchain_state_not_found (login={}, blockchainName={}, globalNumber={})",
|
||||
login, blockchainName, globalNumber);
|
||||
log.warn("AddBlock: blockchain_state_not_found (login={}, blockchainName={}, reqGlobalNumber={})",
|
||||
login, blockchainName, globalNumberFromReq);
|
||||
return new AddBlockResult(WireCodes.Status.NOT_FOUND, "blockchain_state_not_found", -1, "");
|
||||
}
|
||||
|
||||
final int serverLastNum = st.getLastGlobalNumber();
|
||||
final String serverLastHashHex = toHex(nnBytes(st.getLastGlobalHash()));
|
||||
final int serverLastNum = st.getLastBlockNumber();
|
||||
final byte[] serverLastHash32 = (serverLastNum < 0)
|
||||
? new byte[32]
|
||||
: require32OrThrow(st.getLastBlockHash(), "state.last_block_hash is null/invalid");
|
||||
|
||||
// ✅ для genesis ожидаем, что state уже в начальном состоянии (-1)
|
||||
if (globalNumber == 0 && serverLastNum != -1) {
|
||||
log.warn("AddBlock: genesis_but_state_not_initial (login={}, blockchainName={}, stateLastGlobalNumber={})",
|
||||
login, blockchainName, serverLastNum);
|
||||
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "genesis_but_state_not_initial", serverLastNum, serverLastHashHex);
|
||||
}
|
||||
final String serverLastHashHex = toHex(serverLastHash32);
|
||||
|
||||
// следующий global строго
|
||||
int expectedGlobal = serverLastNum + 1;
|
||||
if (globalNumber != expectedGlobal) {
|
||||
log.warn("AddBlock: bad_global_number (login={}, blockchainName={}, пришёл={}, ожидали={}, serverLastNum={}, serverLastHash={})",
|
||||
login, blockchainName, globalNumber, expectedGlobal, serverLastNum, serverLastHashHex);
|
||||
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_global_number", serverLastNum, serverLastHashHex);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// ✅ 2) Декодируем блок
|
||||
// -------------------------------------------------------------------
|
||||
// 2) decode block
|
||||
final byte[] blockBytes;
|
||||
try {
|
||||
blockBytes = decodeBase64(blockBytesB64);
|
||||
} catch (Exception e) {
|
||||
log.warn("AddBlock: некорректный base64 блока (login={}, blockchainName={}, globalNumber={})",
|
||||
login, blockchainName, globalNumber, e);
|
||||
log.warn("AddBlock: некорректный base64 блока (login={}, blockchainName={}, reqGlobalNumber={})",
|
||||
login, blockchainName, globalNumberFromReq, e);
|
||||
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_block_base64", serverLastNum, serverLastHashHex);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// ✅ 3) Ранняя проверка лимита
|
||||
// -------------------------------------------------------------------
|
||||
// 3) лимит (оставляем как было)
|
||||
try {
|
||||
long oldSize = st.getFileSizeBytes();
|
||||
long limit = st.getSizeLimit();
|
||||
long newSize = safeAdd(oldSize, blockBytes.length);
|
||||
|
||||
if (limit > 0 && newSize > limit) {
|
||||
log.warn("AddBlock: limit_exceeded (login={}, blockchainName={}, globalNumber={}, oldSize={}, addLen={}, newSize={}, limit={})",
|
||||
login, blockchainName, globalNumber, oldSize, blockBytes.length, newSize, limit);
|
||||
log.warn("AddBlock: limit_exceeded (login={}, blockchainName={}, oldSize={}, addLen={}, newSize={}, limit={})",
|
||||
login, blockchainName, oldSize, blockBytes.length, newSize, limit);
|
||||
return new AddBlockResult(413, "limit_exceeded", serverLastNum, serverLastHashHex);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("AddBlock: limit_check_failed (login={}, blockchainName={}, globalNumber={})",
|
||||
login, blockchainName, globalNumber, e);
|
||||
log.error("AddBlock: limit_check_failed (login={}, blockchainName={})", login, blockchainName, e);
|
||||
return new AddBlockResult(WireCodes.Status.INTERNAL_ERROR, "limit_check_failed", serverLastNum, serverLastHashHex);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// ✅ 4) Парсим блок
|
||||
// -------------------------------------------------------------------
|
||||
// 4) parse block
|
||||
final BchBlockEntry block;
|
||||
try {
|
||||
block = new BchBlockEntry(blockBytes);
|
||||
} catch (Exception e) {
|
||||
log.warn("AddBlock: не удалось распарсить BchBlockEntry (login={}, blockchainName={}, globalNumber={}, bytesLen={})",
|
||||
login, blockchainName, globalNumber, blockBytes.length, e);
|
||||
log.warn("AddBlock: не удалось распарсить BchBlockEntry (login={}, blockchainName={}, bytesLen={})",
|
||||
login, blockchainName, blockBytes.length, e);
|
||||
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_block_format", serverLastNum, serverLastHashHex);
|
||||
}
|
||||
|
||||
// body.check()
|
||||
try {
|
||||
block.body.check();
|
||||
} catch (Exception e) {
|
||||
log.warn("AddBlock: body.check() не прошёл (login={}, blockchainName={}, globalNumber={}, bodyType={}, bodyVersion={})",
|
||||
login, blockchainName, globalNumber, safeBodyType(block), safeBodyVersion(block), e);
|
||||
log.warn("AddBlock: body.check() не прошёл (login={}, blockchainName={}, blockNumber={}, type={}, ver={})",
|
||||
login, blockchainName, block.blockNumber, (block.type & 0xFFFF), (block.version & 0xFFFF), e);
|
||||
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_block_body", serverLastNum, serverLastHashHex);
|
||||
}
|
||||
|
||||
if (block.recordNumber != globalNumber) {
|
||||
log.warn("AddBlock: global_number_mismatch (login={}, blockchainName={}, заявлен={}, внутриБлока={})",
|
||||
login, blockchainName, globalNumber, block.recordNumber);
|
||||
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "global_number_mismatch", serverLastNum, serverLastHashHex);
|
||||
// 4.2) запрет дырок: blockNumber строго last+1
|
||||
int expectedBlockNumber = serverLastNum + 1;
|
||||
if (block.blockNumber != expectedBlockNumber) {
|
||||
log.warn("AddBlock: bad_block_number (login={}, blockchainName={}, пришёл={}, ожидали={}, serverLastNum={})",
|
||||
login, blockchainName, block.blockNumber, expectedBlockNumber, serverLastNum);
|
||||
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_block_number", serverLastNum, serverLastHashHex);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// ✅ 5) Ключ подписи берём из blockchain_state.blockchainKey (Base64(32))
|
||||
// -------------------------------------------------------------------
|
||||
final byte[] solanaKey32;
|
||||
try {
|
||||
solanaKey32 = st.getBlockchainKeyBytes();
|
||||
} catch (Exception e) {
|
||||
log.warn("AddBlock: bad_blockchain_key_in_state (login={}, blockchainName={}, globalNumber={})",
|
||||
login, blockchainName, globalNumber, e);
|
||||
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_blockchain_key_in_state", serverLastNum, serverLastHashHex);
|
||||
// (временная совместимость) req.globalNumber должен совпасть с block.blockNumber
|
||||
if (globalNumberFromReq != block.blockNumber) {
|
||||
log.warn("AddBlock: req_global_mismatch (login={}, blockchainName={}, reqGlobal={}, blockNumber={})",
|
||||
login, blockchainName, globalNumberFromReq, block.blockNumber);
|
||||
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "req_global_mismatch", serverLastNum, serverLastHashHex);
|
||||
}
|
||||
|
||||
if (solanaKey32 == null || solanaKey32.length != 32) {
|
||||
log.warn("AddBlock: bad_blockchain_key_len (login={}, blockchainName={}, globalNumber={}, keyLen={})",
|
||||
login, blockchainName, globalNumber, (solanaKey32 == null ? -1 : solanaKey32.length));
|
||||
// 4.3) проверка цепочки по prevHash32
|
||||
if (!Arrays.equals(block.prevHash32, serverLastHash32)) {
|
||||
log.warn("AddBlock: bad_prev_hash (login={}, blockchainName={}, blockNumber={}, clientPrev={}, serverPrev={})",
|
||||
login, blockchainName, block.blockNumber, toHex(block.prevHash32), serverLastHashHex);
|
||||
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_prev_hash", serverLastNum, serverLastHashHex);
|
||||
}
|
||||
|
||||
// 5) pubKey
|
||||
final byte[] pubKey32 = st.getBlockchainKeyBytes();
|
||||
if (pubKey32 == null || pubKey32.length != 32) {
|
||||
log.warn("AddBlock: bad_blockchain_key_len (login={}, blockchainName={}, blockNumber={}, keyLen={})",
|
||||
login, blockchainName, block.blockNumber, (pubKey32 == null ? -1 : pubKey32.length));
|
||||
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_blockchain_key_len", serverLastNum, serverLastHashHex);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// ✅ 6) prevGlobalHash сравниваем со state.lastGlobalHash (оба byte[32])
|
||||
// -------------------------------------------------------------------
|
||||
final byte[] prevGlobalHash32;
|
||||
// 6) подпись по hash32(preimage)
|
||||
boolean sigOk;
|
||||
try {
|
||||
prevGlobalHash32 = hexTo32(nn(prevGlobalHashHex)); // "" -> 32 нуля
|
||||
sigOk = BchCryptoVerifier.verifyBlock(block, pubKey32);
|
||||
} catch (Exception e) {
|
||||
log.warn("AddBlock: bad_prev_global_hash_format (login={}, blockchainName={}, globalNumber={}, prevGlobalHashHex='{}')",
|
||||
login, blockchainName, globalNumber, nn(prevGlobalHashHex), e);
|
||||
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_prev_global_hash_format", serverLastNum, serverLastHashHex);
|
||||
log.warn("AddBlock: signature_verify_failed (login={}, blockchainName={}, blockNumber={})",
|
||||
login, blockchainName, block.blockNumber, e);
|
||||
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_signature", serverLastNum, serverLastHashHex);
|
||||
}
|
||||
|
||||
final byte[] serverPrevGlobal32 = serverLastNum < 0 ? new byte[32] : nnBytes(st.getLastGlobalHash());
|
||||
if (!bytesEq(prevGlobalHash32, serverPrevGlobal32)) {
|
||||
log.warn("AddBlock: bad_prev_global_hash (login={}, blockchainName={}, globalNumber={}, clientPrev='{}', serverPrev='{}')",
|
||||
login, blockchainName, globalNumber, nn(prevGlobalHashHex), toHex(serverPrevGlobal32));
|
||||
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_prev_global_hash", serverLastNum, serverLastHashHex);
|
||||
if (!sigOk) {
|
||||
log.warn("AddBlock: bad_signature (login={}, blockchainName={}, blockNumber={})",
|
||||
login, blockchainName, block.blockNumber);
|
||||
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_signature", serverLastNum, serverLastHashHex);
|
||||
}
|
||||
|
||||
// ===========================
|
||||
// ЛИНИИ (строго)
|
||||
// ===========================
|
||||
int li = block.lineIndex;
|
||||
int ln = block.lineNumber;
|
||||
// 7) линейная проверка (только для типов с линией)
|
||||
Integer prevLineNumber = null;
|
||||
byte[] prevLineHash32 = null;
|
||||
Integer thisLineNumber = null;
|
||||
|
||||
if (globalNumber == 0) {
|
||||
if (li != 0 || ln != 0) {
|
||||
log.warn("AddBlock: bad_genesis_line_fields (login={}, blockchainName={}, lineIndex={}, lineNumber={})",
|
||||
login, blockchainName, li, ln);
|
||||
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_genesis_line_fields", serverLastNum, serverLastHashHex);
|
||||
}
|
||||
} else {
|
||||
if (li == 0) {
|
||||
log.warn("AddBlock: line0_only_genesis (login={}, blockchainName={}, globalNumber={}, lineIndex={})",
|
||||
login, blockchainName, globalNumber, li);
|
||||
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "line0_only_genesis", serverLastNum, serverLastHashHex);
|
||||
}
|
||||
if (li < 1 || li > 7) {
|
||||
log.warn("AddBlock: bad_line_index (login={}, blockchainName={}, globalNumber={}, lineIndex={})",
|
||||
login, blockchainName, globalNumber, li);
|
||||
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_line_index", serverLastNum, serverLastHashHex);
|
||||
}
|
||||
if (block.body instanceof BodyHasLine bl) {
|
||||
prevLineNumber = bl.prevLineNumber();
|
||||
prevLineHash32 = bl.prevLineHash32();
|
||||
thisLineNumber = bl.thisLineNumber();
|
||||
|
||||
int expectedLineNumber = st.getLastLineNumber(li) + 1;
|
||||
if (ln != expectedLineNumber) {
|
||||
log.warn("AddBlock: bad_line_number (login={}, blockchainName={}, globalNumber={}, lineIndex={}, пришёлLineNumber={}, ожидалиLineNumber={}, lastLineNumber={})",
|
||||
login, blockchainName, globalNumber, li, ln, expectedLineNumber, st.getLastLineNumber(li));
|
||||
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_line_number", serverLastNum, serverLastHashHex);
|
||||
if (prevLineNumber != null && prevLineNumber != -1) {
|
||||
try {
|
||||
byte[] dbPrevHash = blocksDAO.getHashByNumber(blockchainName, prevLineNumber);
|
||||
if (dbPrevHash == null) {
|
||||
log.warn("AddBlock: prev_line_block_not_found (login={}, blockchainName={}, blockNumber={}, prevLineNumber={})",
|
||||
login, blockchainName, block.blockNumber, prevLineNumber);
|
||||
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "prev_line_block_not_found", serverLastNum, serverLastHashHex);
|
||||
}
|
||||
if (!Arrays.equals(dbPrevHash, require32OrThrow(prevLineHash32, "prevLineHash32 invalid"))) {
|
||||
log.warn("AddBlock: bad_prev_line_hash (login={}, blockchainName={}, blockNumber={}, prevLineNumber={})",
|
||||
login, blockchainName, block.blockNumber, prevLineNumber);
|
||||
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_prev_line_hash", serverLastNum, serverLastHashHex);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("AddBlock: db_error_prev_line_check (login={}, blockchainName={}, blockNumber={})",
|
||||
login, blockchainName, block.blockNumber, e);
|
||||
return new AddBlockResult(WireCodes.Status.INTERNAL_ERROR, "db_error_prev_line_check", serverLastNum, serverLastHashHex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final byte[] prevLineHash32;
|
||||
// 8) сформировать запись и записать (DB + state + файл)
|
||||
try {
|
||||
prevLineHash32 = computePrevLineHash32(st, li);
|
||||
BlockEntry be = new BlockEntry();
|
||||
be.setLogin(login);
|
||||
be.setBchName(blockchainName);
|
||||
|
||||
be.setBlockNumber(block.blockNumber);
|
||||
be.setMsgType(block.type & 0xFFFF);
|
||||
be.setMsgSubType(block.subType & 0xFFFF);
|
||||
|
||||
be.setBlockBytes(block.toBytes());
|
||||
be.setBlockHash(block.getHash32());
|
||||
be.setBlockSignature(block.getSignature64());
|
||||
|
||||
// line columns (optional)
|
||||
be.setPrevLineNumber(prevLineNumber);
|
||||
be.setPrevLineHash(prevLineHash32);
|
||||
be.setThisLineNumber(thisLineNumber);
|
||||
|
||||
// target columns (optional)
|
||||
if (block.body instanceof BodyHasTarget t) {
|
||||
be.setToLogin(t.toLogin());
|
||||
be.setToBchName(t.toBchName());
|
||||
be.setToBlockNumber(t.toBlockGlobalNumber());
|
||||
be.setToBlockHash(t.toBlockHasheBytes());
|
||||
}
|
||||
|
||||
// edit helper (optional): если TEXT_EDIT — это "редактирование блока цели"
|
||||
if ((block.type & 0xFFFF) == 1 && (block.subType & 0xFFFF) == 10 && be.getToBlockNumber() != null) {
|
||||
be.setEditedByBlockNumber(be.getToBlockNumber());
|
||||
}
|
||||
|
||||
dbWriter.appendBlockAndState(blockchainName, block, st, be);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.warn("AddBlock: bad_prev_line_hash_in_state (login={}, blockchainName={}, globalNumber={}, lineIndex={})",
|
||||
login, blockchainName, globalNumber, li, e);
|
||||
return new AddBlockResult(WireCodes.Status.INTERNAL_ERROR, "bad_prev_line_hash_in_state", serverLastNum, serverLastHashHex);
|
||||
}
|
||||
|
||||
boolean ok = BchCryptoVerifier.verifyAll(
|
||||
login,
|
||||
prevGlobalHash32,
|
||||
prevLineHash32,
|
||||
block.getRawBytes(),
|
||||
block.getSignature64(),
|
||||
solanaKey32,
|
||||
block.getHash32()
|
||||
);
|
||||
|
||||
if (!ok) {
|
||||
log.warn("AddBlock: bad_signature_or_hash (login={}, blockchainName={}, globalNumber={}, lineIndex={}, lineNumber={})",
|
||||
login, blockchainName, globalNumber, li, ln);
|
||||
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_signature_or_hash", serverLastNum, serverLastHashHex);
|
||||
}
|
||||
|
||||
// write
|
||||
try {
|
||||
dbWriter.appendBlockAndState(
|
||||
login,
|
||||
blockchainName,
|
||||
prevGlobalHash32,
|
||||
prevLineHash32,
|
||||
block,
|
||||
st
|
||||
);
|
||||
} catch (Exception e) {
|
||||
log.error("AddBlock: внутренняя ошибка при записи блока (login={}, blockchainName={}, globalNumber={})",
|
||||
login, blockchainName, globalNumber, e);
|
||||
log.error("AddBlock: внутренняя ошибка при записи блока (login={}, blockchainName={}, blockNumber={})",
|
||||
login, blockchainName, block.blockNumber, e);
|
||||
return new AddBlockResult(WireCodes.Status.INTERNAL_ERROR, "internal_error", serverLastNum, serverLastHashHex);
|
||||
}
|
||||
|
||||
String newHashHex = toHex(block.getHash32());
|
||||
|
||||
log.info("✅ AddBlock ok: login={}, blockchainName={}, globalNumber={}, lineIndex={}, lineNumber={}, newHash={}",
|
||||
login, blockchainName, globalNumber, li, ln, newHashHex);
|
||||
log.info("✅ AddBlock ok: login={}, blockchainName={}, blockNumber={}, newHash={}",
|
||||
login, blockchainName, block.blockNumber, newHashHex);
|
||||
|
||||
return new AddBlockResult(WireCodes.Status.OK, null, globalNumber, newHashHex);
|
||||
return new AddBlockResult(WireCodes.Status.OK, null, block.blockNumber, newHashHex);
|
||||
}
|
||||
|
||||
/**
|
||||
* ✅ Правило:
|
||||
* - lineIndex=0: prevLineHash = 32 нулей
|
||||
* - lineIndex>0:
|
||||
* - если в этой линии ещё нет блоков (lastLineNumber==0) => prevLineHash = hash(genesis) (line0 hash)
|
||||
* - иначе => prevLineHash = lastLineHash(lineIndex)
|
||||
*/
|
||||
private static byte[] computePrevLineHash32(BlockchainStateEntry st, int lineIndex) {
|
||||
if (lineIndex == 0) {
|
||||
return new byte[32];
|
||||
}
|
||||
/* ===================================================================== */
|
||||
/* ====================== Helpers ====================================== */
|
||||
/* ===================================================================== */
|
||||
|
||||
int lastLn = st.getLastLineNumber(lineIndex);
|
||||
if (lastLn == 0) {
|
||||
byte[] genesis = nnBytes(st.getLastLineHash(0));
|
||||
if (genesis.length == 32) return genesis;
|
||||
|
||||
byte[] g = nnBytes(st.getLastGlobalHash());
|
||||
if (g.length == 32) return g;
|
||||
|
||||
return new byte[32];
|
||||
}
|
||||
|
||||
byte[] last = nnBytes(st.getLastLineHash(lineIndex));
|
||||
return last.length == 32 ? last : new byte[32];
|
||||
private static byte[] decodeBase64(String b64) {
|
||||
if (b64 == null) throw new IllegalArgumentException("blockBytesB64 == null");
|
||||
return Base64.getDecoder().decode(b64);
|
||||
}
|
||||
|
||||
private static final class AddBlockResult {
|
||||
final int httpStatus;
|
||||
final String reasonCode;
|
||||
final int serverLastGlobalNumber;
|
||||
final String serverLastGlobalHashHex;
|
||||
|
||||
AddBlockResult(int httpStatus, String reasonCode, int serverLastGlobalNumber, String serverLastGlobalHashHex) {
|
||||
this.httpStatus = httpStatus;
|
||||
this.reasonCode = reasonCode;
|
||||
this.serverLastGlobalNumber = serverLastGlobalNumber;
|
||||
this.serverLastGlobalHashHex = serverLastGlobalHashHex;
|
||||
}
|
||||
|
||||
boolean isOk() {
|
||||
return httpStatus == WireCodes.Status.OK;
|
||||
}
|
||||
private static long safeAdd(long a, long b) {
|
||||
long r = a + b;
|
||||
if (((a ^ r) & (b ^ r)) < 0) throw new ArithmeticException("long overflow");
|
||||
return r;
|
||||
}
|
||||
|
||||
private static String nn(String s) { return s == null ? "" : s; }
|
||||
|
||||
private static byte[] nnBytes(byte[] b) { return b == null ? new byte[0] : b; }
|
||||
|
||||
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 byte[] require32OrThrow(byte[] b, String msg) {
|
||||
if (b == null || b.length != 32) throw new IllegalArgumentException(msg);
|
||||
return b;
|
||||
}
|
||||
|
||||
private static String toHex(byte[] bytes) {
|
||||
if (bytes == null || bytes.length == 0) return "";
|
||||
if (bytes == null) return "null";
|
||||
char[] HEX = "0123456789abcdef".toCharArray();
|
||||
char[] out = new char[bytes.length * 2];
|
||||
for (int i = 0; i < bytes.length; i++) {
|
||||
@@ -402,19 +338,19 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
|
||||
return new String(out);
|
||||
}
|
||||
|
||||
private static String safeBodyType(BchBlockEntry b) {
|
||||
try { return String.valueOf(b.body.type()); } catch (Exception e) { return "unknown"; }
|
||||
}
|
||||
private static final class AddBlockResult {
|
||||
final int httpStatus;
|
||||
final String reasonCode;
|
||||
final int serverLastBlockNumber;
|
||||
final String serverLastBlockHashHex;
|
||||
|
||||
private static String safeBodyVersion(BchBlockEntry b) {
|
||||
try { return String.valueOf(b.body.version()); } catch (Exception e) { return "unknown"; }
|
||||
}
|
||||
|
||||
private static long safeAdd(long x, long y) {
|
||||
long r = x + y;
|
||||
if (((x ^ r) & (y ^ r)) < 0) {
|
||||
throw new IllegalArgumentException("overflow: " + x + " + " + y);
|
||||
AddBlockResult(int httpStatus, String reasonCode, int serverLastBlockNumber, String serverLastBlockHashHex) {
|
||||
this.httpStatus = httpStatus;
|
||||
this.reasonCode = reasonCode;
|
||||
this.serverLastBlockNumber = serverLastBlockNumber;
|
||||
this.serverLastBlockHashHex = serverLastBlockHashHex;
|
||||
}
|
||||
return r;
|
||||
|
||||
boolean isOk() { return httpStatus == WireCodes.Status.OK; }
|
||||
}
|
||||
}
|
||||
+32
-303
@@ -1,342 +1,71 @@
|
||||
// =======================
|
||||
// BlockchainWriter.java (НОВАЯ ВЕРСИЯ)
|
||||
// server/logic/ws_protocol/JSON/handlers/blockchain/Net_AddBlock_Handler_utils/BlockchainWriter.java
|
||||
// (НОВАЯ ВЕРСИЯ — чтобы AddBlock работал с новым blocks/state)
|
||||
// =======================
|
||||
package server.logic.ws_protocol.JSON.handlers.blockchain.Net_AddBlock_Handler_utils;
|
||||
|
||||
import blockchain.BchBlockEntry;
|
||||
import blockchain.body.BodyHasTarget;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import shine.db.SqliteDbController;
|
||||
import shine.db.dao.BlockchainStateDAO;
|
||||
import shine.db.dao.BlocksDAO;
|
||||
import shine.db.entities.BlockEntry;
|
||||
import shine.db.entities.BlockchainStateEntry;
|
||||
import utils.blockchain.BlockchainNameUtil;
|
||||
import shine.db.entities.BlockEntry;
|
||||
import utils.files.FileStoreUtil;
|
||||
import shine.log.BlockchainAdminNotifier;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Types;
|
||||
import java.util.Base64;
|
||||
|
||||
/**
|
||||
* BlockchainWriter — единая точка записи:
|
||||
* 1) создаём новый файл <name>.tmp_bch = oldFileBytes + newBlockBytes
|
||||
* 2) атомарно фиксируем в БД:
|
||||
* - blocks (строка блока)
|
||||
* - blockchain_state (включая новый fileSizeBytes)
|
||||
* 3) атомарно заменяем файл:
|
||||
* - удаляем/замещаем старый <name>.bch
|
||||
* - переименовываем <name>.tmp_bch -> <name>.bch
|
||||
* BlockchainWriter — запись блока в DB + обновление state + запись в файл.
|
||||
*
|
||||
* ВАЖНО:
|
||||
* - Это минимальный рабочий вариант под новый формат.
|
||||
* - Если у тебя уже есть "атомарность" сложнее (tmp_bch + commit/recovery) — можно усилить потом.
|
||||
*/
|
||||
public final class BlockchainWriter {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(BlockchainWriter.class);
|
||||
|
||||
private final SqliteDbController db;
|
||||
private final BlocksDAO blocksDAO;
|
||||
private final BlockchainStateDAO stateDAO;
|
||||
private final FileStoreUtil fs;
|
||||
private final FileStoreUtil fs = FileStoreUtil.getInstance();
|
||||
|
||||
public BlockchainWriter(BlocksDAO blocksDAO, BlockchainStateDAO stateDAO) {
|
||||
this.db = SqliteDbController.getInstance();
|
||||
this.blocksDAO = blocksDAO;
|
||||
this.stateDAO = stateDAO;
|
||||
this.fs = FileStoreUtil.getInstance();
|
||||
}
|
||||
|
||||
public void appendBlockAndState(
|
||||
String login,
|
||||
String blockchainName,
|
||||
byte[] prevGlobalHash32,
|
||||
byte[] prevLineHash32,
|
||||
BchBlockEntry block,
|
||||
BlockchainStateEntry stOrNull
|
||||
) throws SQLException {
|
||||
public void appendBlockAndState(String blockchainName,
|
||||
BchBlockEntry block,
|
||||
BlockchainStateEntry st,
|
||||
BlockEntry be) throws SQLException {
|
||||
|
||||
if (stOrNull == null) {
|
||||
throw new SQLException("blockchain_state not found for blockchainName=" + blockchainName + " (state обязателен)");
|
||||
}
|
||||
long nowMs = System.currentTimeMillis();
|
||||
|
||||
verifyMainFileSizeMatchesStateOrAlert(login, blockchainName, block, stOrNull);
|
||||
|
||||
// bytes FULL блока (raw+sig+hash)
|
||||
final byte[] newBlockFullBytes = block.toBytes();
|
||||
|
||||
final long oldFileSize = stOrNull.getFileSizeBytes();
|
||||
final long newFileSize = safeAdd(oldFileSize, newBlockFullBytes.length);
|
||||
|
||||
// tmp = old + new
|
||||
final byte[] tmpBytes;
|
||||
if (oldFileSize == 0) {
|
||||
tmpBytes = newBlockFullBytes;
|
||||
} else {
|
||||
byte[] oldBytes;
|
||||
try {
|
||||
oldBytes = fs.readBlockchain(blockchainName);
|
||||
} catch (Exception e) {
|
||||
log.error("Ошибка чтения старого файла блокчейна перед записью tmp (login={}, blockchainName={}, oldFileSize={}, blockNumber={})",
|
||||
login, blockchainName, oldFileSize, block.recordNumber, e);
|
||||
throw new SQLException("Cannot read old blockchain file for: " + blockchainName, e);
|
||||
}
|
||||
|
||||
if (oldBytes.length != (int) oldFileSize) {
|
||||
String msg =
|
||||
"Несовпадение размера файла блокчейна при чтении: " +
|
||||
"state ожидал oldFileSize=" + oldFileSize +
|
||||
", а реально прочитали oldBytes.length=" + oldBytes.length +
|
||||
" (login=" + login +
|
||||
", blockchainName=" + blockchainName +
|
||||
", blockNumber=" + block.recordNumber + ").";
|
||||
BlockchainAdminNotifier.critical(msg, null);
|
||||
throw new SQLException(msg);
|
||||
}
|
||||
|
||||
tmpBytes = concat(oldBytes, newBlockFullBytes);
|
||||
}
|
||||
|
||||
try {
|
||||
fs.writeBlockchainTmp(blockchainName, tmpBytes);
|
||||
} catch (Exception e) {
|
||||
log.error("Ошибка записи tmp файла блокчейна (login={}, blockchainName={}, tmpBytesLen={}, oldFileSize={}, newFileSize={}, blockNumber={})",
|
||||
login, blockchainName, tmpBytes.length, oldFileSize, newFileSize, block.recordNumber, e);
|
||||
throw new SQLException("Cannot write tmp blockchain file for: " + blockchainName, e);
|
||||
}
|
||||
|
||||
// атомарно БД
|
||||
try (Connection c = db.getConnection()) {
|
||||
|
||||
boolean oldAutoCommit = c.getAutoCommit();
|
||||
try (Connection c = shine.db.SqliteDbController.getInstance().getConnection()) {
|
||||
c.setAutoCommit(false);
|
||||
|
||||
boolean committed = false;
|
||||
|
||||
try {
|
||||
insertBlockRow(c, login, blockchainName, prevGlobalHash32, prevLineHash32, block);
|
||||
appendState(c, blockchainName, block, stOrNull, newFileSize);
|
||||
// 1) insert block
|
||||
blocksDAO.insert(c, be);
|
||||
|
||||
// 2) update state
|
||||
st.setLastBlockNumber(block.blockNumber);
|
||||
st.setLastBlockHash(block.getHash32());
|
||||
st.setFileSizeBytes(st.getFileSizeBytes() + block.toBytes().length);
|
||||
st.setUpdatedAtMs(nowMs);
|
||||
|
||||
stateDAO.upsert(c, st);
|
||||
|
||||
c.commit();
|
||||
committed = true;
|
||||
|
||||
} catch (Exception e) {
|
||||
try { c.rollback(); } catch (SQLException ignore) {}
|
||||
|
||||
log.error("Ошибка транзакции БД при добавлении блока (rollback выполнен) (login={}, blockchainName={}, blockNumber={}, oldFileSize={}, newFileSize={})",
|
||||
login, blockchainName, block.recordNumber, oldFileSize, newFileSize, e);
|
||||
|
||||
try { c.rollback(); } catch (Exception ignored) {}
|
||||
if (e instanceof SQLException se) throw se;
|
||||
throw new SQLException("appendBlockAndState failed (db tx)", e);
|
||||
|
||||
throw new SQLException("appendBlockAndState failed", e);
|
||||
} finally {
|
||||
try { c.setAutoCommit(oldAutoCommit); } catch (SQLException ignore) {}
|
||||
}
|
||||
|
||||
// после коммита БД — атомарно заменяем файл
|
||||
if (committed) {
|
||||
try {
|
||||
fs.atomicReplaceBlockchainFile(blockchainName);
|
||||
} catch (Exception moveError) {
|
||||
log.error("БД закоммичена, но атомарная замена файла блокчейна не удалась. tmp оставлен для recovery. (login={}, blockchainName={}, blockNumber={})",
|
||||
login, blockchainName, block.recordNumber, moveError);
|
||||
|
||||
throw new SQLException(
|
||||
"DB committed but file replace failed; tmp kept for recovery. blockchainName=" + blockchainName,
|
||||
moveError
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void verifyMainFileSizeMatchesStateOrAlert(
|
||||
String login,
|
||||
String blockchainName,
|
||||
BchBlockEntry block,
|
||||
BlockchainStateEntry stOrNull
|
||||
) throws SQLException {
|
||||
|
||||
if (stOrNull == null) return;
|
||||
|
||||
long expected = stOrNull.getFileSizeBytes();
|
||||
if (expected <= 0) return;
|
||||
|
||||
String mainFileName = fs.buildBlockchainFileName(blockchainName);
|
||||
|
||||
if (!fs.exists(mainFileName)) {
|
||||
String msg =
|
||||
"КРИТИЧЕСКАЯ ОШИБКА КОНСИСТЕНТНОСТИ: state ожидает основной файл, но его нет. " +
|
||||
"login=" + login +
|
||||
", blockchainName=" + blockchainName +
|
||||
", expectedSizeFromState=" + expected +
|
||||
", blockNumber=" + (block != null ? block.recordNumber : -1) + ".";
|
||||
BlockchainAdminNotifier.critical(msg, null);
|
||||
throw new SQLException(msg);
|
||||
}
|
||||
|
||||
long real;
|
||||
try {
|
||||
real = fs.size(mainFileName);
|
||||
} catch (Exception e) {
|
||||
String msg =
|
||||
"КРИТИЧЕСКАЯ ОШИБКА: не удалось получить размер основного файла блокчейна. " +
|
||||
"login=" + login +
|
||||
", blockchainName=" + blockchainName +
|
||||
", expectedSizeFromState=" + expected +
|
||||
", blockNumber=" + (block != null ? block.recordNumber : -1) + ".";
|
||||
BlockchainAdminNotifier.critical(msg, e);
|
||||
throw new SQLException(msg, e);
|
||||
}
|
||||
|
||||
if (real != expected) {
|
||||
String msg =
|
||||
"КРИТИЧЕСКАЯ ОШИБКА КОНСИСТЕНТНОСТИ: размер файла блокчейна НЕ СОВПАДАЕТ с state. " +
|
||||
"login=" + login +
|
||||
", blockchainName=" + blockchainName +
|
||||
", expectedSizeFromState=" + expected +
|
||||
", realMainFileSize=" + real +
|
||||
", blockNumber=" + (block != null ? block.recordNumber : -1) + ". " +
|
||||
"Похоже на внешнее вмешательство/порчу файла. Запись нового блока остановлена.";
|
||||
BlockchainAdminNotifier.critical(msg, null);
|
||||
throw new SQLException(msg);
|
||||
}
|
||||
}
|
||||
|
||||
private void appendState(
|
||||
Connection c,
|
||||
String blockchainName,
|
||||
BchBlockEntry block,
|
||||
BlockchainStateEntry stOrNull,
|
||||
long newFileSizeBytes
|
||||
) throws SQLException {
|
||||
|
||||
BlockchainStateEntry st = stOrNull;
|
||||
if (st == null) {
|
||||
throw new SQLException("blockchain_state not found for blockchainName=" + blockchainName);
|
||||
}
|
||||
|
||||
// глобальная цепочка
|
||||
st.setLastGlobalNumber(block.recordNumber);
|
||||
st.setLastGlobalHash(block.getHash32());
|
||||
|
||||
// линия
|
||||
int li = block.lineIndex;
|
||||
st.setLastLineNumber(li, block.lineNumber);
|
||||
st.setLastLineHash(li, block.getHash32());
|
||||
|
||||
// file size
|
||||
st.setFileSizeBytes(newFileSizeBytes);
|
||||
|
||||
// timestamp
|
||||
st.setUpdatedAtMs(System.currentTimeMillis());
|
||||
|
||||
stateDAO.upsert(c, st);
|
||||
}
|
||||
|
||||
/**
|
||||
* Вставка/апдейт строки блока в blocks (BLOB-вариант).
|
||||
*/
|
||||
private void insertBlockRow(
|
||||
Connection c,
|
||||
String login,
|
||||
String blockchainName,
|
||||
byte[] prevGlobalHash32,
|
||||
byte[] prevLineHash32,
|
||||
BchBlockEntry block
|
||||
) throws SQLException {
|
||||
|
||||
BlockEntry e = new BlockEntry();
|
||||
|
||||
e.setLogin(login);
|
||||
e.setBchName(blockchainName);
|
||||
|
||||
e.setBlockGlobalNumber(block.recordNumber);
|
||||
e.setBlockGlobalPreHashe(prevGlobalHash32);
|
||||
|
||||
e.setBlockLineIndex(block.lineIndex);
|
||||
e.setBlockLineNumber(block.lineNumber);
|
||||
e.setBlockLinePreHashe(prevLineHash32);
|
||||
|
||||
e.setMsgType(block.body.type());
|
||||
e.setMsgSubType(block.body.subType());
|
||||
|
||||
// ВАЖНО: здесь ты кладёшь FULL bytes (raw+sig+hash). Это ок, ты так задумал.
|
||||
e.setBlockByte(block.toBytes());
|
||||
|
||||
// to-поля
|
||||
e.setToLogin(null);
|
||||
e.setToBchName(null);
|
||||
e.setToBlockGlobalNumber(null);
|
||||
e.setToBlockHashe(null);
|
||||
|
||||
if (block.body instanceof BodyHasTarget tf) {
|
||||
e.setToLogin(tf.toLogin());
|
||||
e.setToBchName(tf.toBchName());
|
||||
e.setToBlockGlobalNumber(tf.toBlockGlobalNumber());
|
||||
e.setToBlockHashe(tf.toBlockHasheBytes());
|
||||
|
||||
// если to_login не пришёл, но есть to_bch_name — восстановим логин из имени цепочки
|
||||
if (e.getToLogin() == null && e.getToBchName() != null) {
|
||||
String toLogin = BlockchainNameUtil.loginFromBlockchainName(e.getToBchName());
|
||||
if (toLogin != null && !toLogin.isBlank()) {
|
||||
e.setToLogin(toLogin);
|
||||
}
|
||||
try { c.setAutoCommit(true); } catch (Exception ignored) {}
|
||||
}
|
||||
}
|
||||
|
||||
// новое: хэш и подпись самого блока
|
||||
e.setBlockHash(block.getHash32());
|
||||
e.setBlockSignature(block.getSignature64());
|
||||
|
||||
// новое: не трогаем (NULL); триггер пометит исходный блок
|
||||
e.setEditedByBlockGlobalNumber(null);
|
||||
|
||||
blocksDAO.upsert(c, e);
|
||||
}
|
||||
|
||||
// -------------------- utils --------------------
|
||||
|
||||
private static byte[] concat(byte[] a, byte[] b) {
|
||||
byte[] out = new byte[a.length + b.length];
|
||||
System.arraycopy(a, 0, out, 0, a.length);
|
||||
System.arraycopy(b, 0, out, a.length, b.length);
|
||||
return out;
|
||||
}
|
||||
|
||||
private static long safeAdd(long x, long y) {
|
||||
long r = x + y;
|
||||
if (((x ^ r) & (y ^ r)) < 0) {
|
||||
throw new IllegalArgumentException("fileSizeBytes overflow: " + x + " + " + y);
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
// Если у тебя где-то ещё остался String-хэш (legacy), используй это в месте парсинга JSON,
|
||||
// но НЕ в writer. Оставляю тут только на всякий случай для миграции:
|
||||
@SuppressWarnings("unused")
|
||||
private static byte[] decodeHashStringLenient(String s) {
|
||||
if (s == null) return null;
|
||||
String t = s.trim();
|
||||
if (t.isEmpty()) return null;
|
||||
|
||||
// hex 64
|
||||
if (t.length() == 64 && t.matches("^[0-9a-fA-F]+$")) {
|
||||
byte[] out = new byte[32];
|
||||
for (int i = 0; i < 32; i++) {
|
||||
int hi = Character.digit(t.charAt(i * 2), 16);
|
||||
int lo = Character.digit(t.charAt(i * 2 + 1), 16);
|
||||
out[i] = (byte) ((hi << 4) | lo);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// base64 (часто у тебя так)
|
||||
try {
|
||||
byte[] b = Base64.getDecoder().decode(t);
|
||||
return (b != null && b.length == 32) ? b : b;
|
||||
} catch (IllegalArgumentException ignore) {
|
||||
return null;
|
||||
}
|
||||
// 3) append to file (минимально: просто дописать)
|
||||
// Если у тебя уже есть логика tmp_bch+atomicReplace — можно заменить тут.
|
||||
String fileName = fs.buildBlockchainFileName(blockchainName);
|
||||
fs.addDataToFile(fileName, block.toBytes());
|
||||
}
|
||||
}
|
||||
+147
-147
@@ -1,147 +1,147 @@
|
||||
package server.logic.ws_protocol.JSON.handlers.subscriptions;
|
||||
|
||||
import blockchain.BchBlockEntry;
|
||||
import blockchain.body.TextBody;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import server.logic.ws_protocol.JSON.ConnectionContext;
|
||||
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
||||
import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
|
||||
import server.logic.ws_protocol.JSON.handlers.subscriptions.entyties.Net_GetSubscribedChannels_Request;
|
||||
import server.logic.ws_protocol.JSON.handlers.subscriptions.entyties.Net_GetSubscribedChannels_Response;
|
||||
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||
import server.logic.ws_protocol.WireCodes;
|
||||
import shine.db.SqliteDbController;
|
||||
import shine.db.dao.SubscriptionsDAO;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Handler: GetSubscribedChannels
|
||||
*
|
||||
* Логика:
|
||||
* - DAO возвращает last publication orig bytes (+ edit bytes если есть)
|
||||
* - Handler парсит FULL bytes блока:
|
||||
* timestamp берём из ОРИГИНАЛА (publication)
|
||||
* текст берём из EDIT (если есть) иначе из оригинала
|
||||
* - формируем превью первых 50 символов
|
||||
*/
|
||||
public class Net_GetSubscribedChannels_Handler implements JsonMessageHandler {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(Net_GetSubscribedChannels_Handler.class);
|
||||
|
||||
@Override
|
||||
public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) {
|
||||
Net_GetSubscribedChannels_Request req = (Net_GetSubscribedChannels_Request) baseRequest;
|
||||
|
||||
if (req.getLogin() == null || req.getLogin().isBlank()) {
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.BAD_REQUEST,
|
||||
"BAD_FIELDS",
|
||||
"Некорректное поле: login"
|
||||
);
|
||||
}
|
||||
|
||||
// Если хочешь жёстче:
|
||||
// if (!req.getLogin().matches("^[A-Za-z0-9_]+$")) ...
|
||||
|
||||
SubscriptionsDAO dao = SubscriptionsDAO.getInstance();
|
||||
SqliteDbController db = SqliteDbController.getInstance();
|
||||
|
||||
try (Connection c = db.getConnection()) {
|
||||
|
||||
List<SubscriptionsDAO.ChannelRow> rows = dao.getSubscribedChannels(c, req.getLogin());
|
||||
List<Net_GetSubscribedChannels_Response.ChannelInfo> out = new ArrayList<>(rows.size());
|
||||
|
||||
for (SubscriptionsDAO.ChannelRow r : rows) {
|
||||
Net_GetSubscribedChannels_Response.ChannelInfo dto =
|
||||
new Net_GetSubscribedChannels_Response.ChannelInfo();
|
||||
|
||||
dto.setChannelLogin(r.getChannelLogin());
|
||||
dto.setChannelBchName(r.getChannelBchName());
|
||||
dto.setPublicationsCount(r.getPublicationsCount());
|
||||
|
||||
byte[] pubBytes = r.getLastPublicationBlockBytes();
|
||||
byte[] editBytes = r.getLastEditBlockBytes();
|
||||
|
||||
if (pubBytes == null || pubBytes.length == 0) {
|
||||
dto.setLastPublicationTimestampSec(null);
|
||||
dto.setLastTextPreview(null);
|
||||
out.add(dto);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 1) timestamp берём из ОРИГИНАЛЬНОЙ публикации
|
||||
BchBlockEntry pubBlock = new BchBlockEntry(pubBytes);
|
||||
dto.setLastPublicationTimestampSec(pubBlock.timestamp);
|
||||
|
||||
// 2) текст — из EDIT (если есть) иначе из оригинала
|
||||
byte[] actualBytes = (editBytes != null && editBytes.length > 0) ? editBytes : pubBytes;
|
||||
BchBlockEntry actualBlock = new BchBlockEntry(actualBytes);
|
||||
|
||||
if (!(actualBlock.body instanceof TextBody)) {
|
||||
// Это уже нарушение данных: last publication должен быть текстовым блоком.
|
||||
throw new IllegalStateException("Last publication is not TextBody: type="
|
||||
+ (actualBlock.body == null ? "null" : (actualBlock.body.type() & 0xFFFF)));
|
||||
}
|
||||
|
||||
String msg = ((TextBody) actualBlock.body).message;
|
||||
dto.setLastTextPreview(firstNCharsSafe(msg, 50));
|
||||
|
||||
out.add(dto);
|
||||
}
|
||||
|
||||
Net_GetSubscribedChannels_Response resp = new Net_GetSubscribedChannels_Response();
|
||||
resp.setOp(req.getOp());
|
||||
resp.setRequestId(req.getRequestId());
|
||||
resp.setStatus(WireCodes.Status.OK);
|
||||
resp.setChannels(out);
|
||||
|
||||
return resp;
|
||||
|
||||
} catch (SQLException e) {
|
||||
log.error("❌ DB error GetSubscribedChannels", e);
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.SERVER_DATA_ERROR,
|
||||
"DB_ERROR",
|
||||
"Ошибка БД"
|
||||
);
|
||||
} catch (IllegalArgumentException e) {
|
||||
// сюда попадёт, например, если BchBlockEntry не смог распарсить block_byte
|
||||
log.error("❌ Bad block bytes in DB (cannot parse BchBlockEntry)", e);
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.SERVER_DATA_ERROR,
|
||||
"BAD_BLOCK_BYTES",
|
||||
"В БД обнаружен повреждённый блок"
|
||||
);
|
||||
} catch (Exception e) {
|
||||
log.error("❌ Internal error GetSubscribedChannels", e);
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.INTERNAL_ERROR,
|
||||
"INTERNAL_ERROR",
|
||||
"Внутренняя ошибка сервера"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Берём первые N "символов" безопасно для emoji/суррогатных пар:
|
||||
* режем по code points.
|
||||
*/
|
||||
private static String firstNCharsSafe(String s, int n) {
|
||||
if (s == null) return null;
|
||||
if (n <= 0) return "";
|
||||
int cp = s.codePointCount(0, s.length());
|
||||
if (cp <= n) return s;
|
||||
int end = s.offsetByCodePoints(0, n);
|
||||
return s.substring(0, end);
|
||||
}
|
||||
}
|
||||
//package server.logic.ws_protocol.JSON.handlers.subscriptions;
|
||||
//
|
||||
//import blockchain.BchBlockEntry;
|
||||
//import blockchain.body.TextBody;
|
||||
//import org.slf4j.Logger;
|
||||
//import org.slf4j.LoggerFactory;
|
||||
//import server.logic.ws_protocol.JSON.ConnectionContext;
|
||||
//import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||
//import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
||||
//import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
|
||||
//import server.logic.ws_protocol.JSON.handlers.subscriptions.entyties.Net_GetSubscribedChannels_Request;
|
||||
//import server.logic.ws_protocol.JSON.handlers.subscriptions.entyties.Net_GetSubscribedChannels_Response;
|
||||
//import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||
//import server.logic.ws_protocol.WireCodes;
|
||||
//import shine.db.SqliteDbController;
|
||||
//import shine.db.dao.SubscriptionsDAO;
|
||||
//
|
||||
//import java.sql.Connection;
|
||||
//import java.sql.SQLException;
|
||||
//import java.util.ArrayList;
|
||||
//import java.util.List;
|
||||
//
|
||||
///**
|
||||
// * Handler: GetSubscribedChannels
|
||||
// *
|
||||
// * Логика:
|
||||
// * - DAO возвращает last publication orig bytes (+ edit bytes если есть)
|
||||
// * - Handler парсит FULL bytes блока:
|
||||
// * timestamp берём из ОРИГИНАЛА (publication)
|
||||
// * текст берём из EDIT (если есть) иначе из оригинала
|
||||
// * - формируем превью первых 50 символов
|
||||
// */
|
||||
//public class Net_GetSubscribedChannels_Handler implements JsonMessageHandler {
|
||||
//
|
||||
// private static final Logger log = LoggerFactory.getLogger(Net_GetSubscribedChannels_Handler.class);
|
||||
//
|
||||
// @Override
|
||||
// public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) {
|
||||
// Net_GetSubscribedChannels_Request req = (Net_GetSubscribedChannels_Request) baseRequest;
|
||||
//
|
||||
// if (req.getLogin() == null || req.getLogin().isBlank()) {
|
||||
// return NetExceptionResponseFactory.error(
|
||||
// req,
|
||||
// WireCodes.Status.BAD_REQUEST,
|
||||
// "BAD_FIELDS",
|
||||
// "Некорректное поле: login"
|
||||
// );
|
||||
// }
|
||||
//
|
||||
// // Если хочешь жёстче:
|
||||
// // if (!req.getLogin().matches("^[A-Za-z0-9_]+$")) ...
|
||||
//
|
||||
// SubscriptionsDAO dao = SubscriptionsDAO.getInstance();
|
||||
// SqliteDbController db = SqliteDbController.getInstance();
|
||||
//
|
||||
// try (Connection c = db.getConnection()) {
|
||||
//
|
||||
// List<SubscriptionsDAO.ChannelRow> rows = dao.getSubscribedChannels(c, req.getLogin());
|
||||
// List<Net_GetSubscribedChannels_Response.ChannelInfo> out = new ArrayList<>(rows.size());
|
||||
//
|
||||
// for (SubscriptionsDAO.ChannelRow r : rows) {
|
||||
// Net_GetSubscribedChannels_Response.ChannelInfo dto =
|
||||
// new Net_GetSubscribedChannels_Response.ChannelInfo();
|
||||
//
|
||||
// dto.setChannelLogin(r.getChannelLogin());
|
||||
// dto.setChannelBchName(r.getChannelBchName());
|
||||
// dto.setPublicationsCount(r.getPublicationsCount());
|
||||
//
|
||||
// byte[] pubBytes = r.getLastPublicationBlockBytes();
|
||||
// byte[] editBytes = r.getLastEditBlockBytes();
|
||||
//
|
||||
// if (pubBytes == null || pubBytes.length == 0) {
|
||||
// dto.setLastPublicationTimestampSec(null);
|
||||
// dto.setLastTextPreview(null);
|
||||
// out.add(dto);
|
||||
// continue;
|
||||
// }
|
||||
//
|
||||
// // 1) timestamp берём из ОРИГИНАЛЬНОЙ публикации
|
||||
// BchBlockEntry pubBlock = new BchBlockEntry(pubBytes);
|
||||
// dto.setLastPublicationTimestampSec(pubBlock.timestamp);
|
||||
//
|
||||
// // 2) текст — из EDIT (если есть) иначе из оригинала
|
||||
// byte[] actualBytes = (editBytes != null && editBytes.length > 0) ? editBytes : pubBytes;
|
||||
// BchBlockEntry actualBlock = new BchBlockEntry(actualBytes);
|
||||
//
|
||||
// if (!(actualBlock.body instanceof TextBody)) {
|
||||
// // Это уже нарушение данных: last publication должен быть текстовым блоком.
|
||||
// throw new IllegalStateException("Last publication is not TextBody: type="
|
||||
// + (actualBlock.body == null ? "null" : (actualBlock.body.type() & 0xFFFF)));
|
||||
// }
|
||||
//
|
||||
// String msg = ((TextBody) actualBlock.body).message;
|
||||
// dto.setLastTextPreview(firstNCharsSafe(msg, 50));
|
||||
//
|
||||
// out.add(dto);
|
||||
// }
|
||||
//
|
||||
// Net_GetSubscribedChannels_Response resp = new Net_GetSubscribedChannels_Response();
|
||||
// resp.setOp(req.getOp());
|
||||
// resp.setRequestId(req.getRequestId());
|
||||
// resp.setStatus(WireCodes.Status.OK);
|
||||
// resp.setChannels(out);
|
||||
//
|
||||
// return resp;
|
||||
//
|
||||
// } catch (SQLException e) {
|
||||
// log.error("❌ DB error GetSubscribedChannels", e);
|
||||
// return NetExceptionResponseFactory.error(
|
||||
// req,
|
||||
// WireCodes.Status.SERVER_DATA_ERROR,
|
||||
// "DB_ERROR",
|
||||
// "Ошибка БД"
|
||||
// );
|
||||
// } catch (IllegalArgumentException e) {
|
||||
// // сюда попадёт, например, если BchBlockEntry не смог распарсить block_byte
|
||||
// log.error("❌ Bad block bytes in DB (cannot parse BchBlockEntry)", e);
|
||||
// return NetExceptionResponseFactory.error(
|
||||
// req,
|
||||
// WireCodes.Status.SERVER_DATA_ERROR,
|
||||
// "BAD_BLOCK_BYTES",
|
||||
// "В БД обнаружен повреждённый блок"
|
||||
// );
|
||||
// } catch (Exception e) {
|
||||
// log.error("❌ Internal error GetSubscribedChannels", e);
|
||||
// return NetExceptionResponseFactory.error(
|
||||
// req,
|
||||
// WireCodes.Status.INTERNAL_ERROR,
|
||||
// "INTERNAL_ERROR",
|
||||
// "Внутренняя ошибка сервера"
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * Берём первые N "символов" безопасно для emoji/суррогатных пар:
|
||||
// * режем по code points.
|
||||
// */
|
||||
// private static String firstNCharsSafe(String s, int n) {
|
||||
// if (s == null) return null;
|
||||
// if (n <= 0) return "";
|
||||
// int cp = s.codePointCount(0, s.length());
|
||||
// if (cp <= n) return s;
|
||||
// int end = s.offsetByCodePoints(0, n);
|
||||
// return s.substring(0, end);
|
||||
// }
|
||||
//}
|
||||
+2
-2
@@ -113,8 +113,8 @@ public class Net_AddUser_Handler implements JsonMessageHandler {
|
||||
st.setBlockchainName(req.getBlockchainName());
|
||||
st.setLogin(req.getLogin());
|
||||
st.setBlockchainKey(req.getBlockchainKey()); // Base64(32)
|
||||
st.setLastGlobalNumber(-1);
|
||||
st.setLastGlobalHash(new byte[32]);
|
||||
st.setLastBlockNumber(-1);
|
||||
st.setLastBlockHash(new byte[32]);
|
||||
st.setFileSizeBytes(0);
|
||||
st.setSizeLimit(limit);
|
||||
st.setUpdatedAtMs(System.currentTimeMillis());
|
||||
|
||||
Reference in New Issue
Block a user