SHA256
28 12 25
Вроде как сделал работу с линиями :) но ещё не тестил
This commit is contained in:
+49
-64
@@ -1,6 +1,7 @@
|
||||
package server.logic.ws_protocol.JSON.handlers.blockchain;
|
||||
|
||||
import blockchain.BchBlockEntry;
|
||||
import blockchain.body.ReactionBody;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import shine.db.SqliteDbController;
|
||||
@@ -8,6 +9,7 @@ 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 utils.files.FileStoreUtil;
|
||||
import shine.log.BlockchainAdminNotifier;
|
||||
|
||||
@@ -61,47 +63,32 @@ public final class BlockchainWriter {
|
||||
String login,
|
||||
String blockchainName,
|
||||
String prevGlobalHashHex,
|
||||
String prevLineHashHex,
|
||||
BchBlockEntry block,
|
||||
BlockchainStateEntry stOrNull,
|
||||
String newHashHex
|
||||
) throws SQLException {
|
||||
|
||||
// =====================================================================
|
||||
// ШАГ 0. КРИТИЧЕСКАЯ ПРОВЕРКА КОНСИСТЕНТНОСТИ:
|
||||
// - если state есть и ожидает ненулевой размер,
|
||||
// то основной файл должен существовать и иметь точно этот размер.
|
||||
// - если не так — это почти наверняка внешнее вмешательство/порча,
|
||||
// и продолжать запись НЕЛЬЗЯ.
|
||||
// =====================================================================
|
||||
verifyMainFileSizeMatchesStateOrAlert(login, blockchainName, block, stOrNull);
|
||||
|
||||
// =====================================================================
|
||||
// ШАГ 1. Готовим bytes нового блока (включая signature+hash)
|
||||
// =====================================================================
|
||||
final byte[] newBlockFullBytes = block.toBytes(); // ✅ включает хвост signature+hash
|
||||
final byte[] newBlockFullBytes = block.toBytes();
|
||||
|
||||
// =====================================================================
|
||||
// ШАГ 2. Считаем новый fileSizeBytes
|
||||
// - если genesis (state == null): старый размер = 0
|
||||
// - иначе берём st.fileSizeBytes
|
||||
// =====================================================================
|
||||
final long oldFileSize = (stOrNull == null) ? 0L : stOrNull.getFileSizeBytes();
|
||||
final long newFileSize = safeAdd(oldFileSize, newBlockFullBytes.length);
|
||||
|
||||
// =====================================================================
|
||||
// ШАГ 3. Создаём новый tmp-файл:
|
||||
// tmp = (old file bytes) + (new block bytes)
|
||||
//
|
||||
// Важно:
|
||||
// - читаем старый файл ТОЛЬКО если state не null и size > 0
|
||||
// - если genesis: старого файла нет => tmp = newBlock
|
||||
// ШАГ 3. Создаём новый tmp-файл: tmp = (old file bytes) + (new block bytes)
|
||||
// =====================================================================
|
||||
final byte[] tmpBytes;
|
||||
if (stOrNull == null || oldFileSize == 0) {
|
||||
// genesis: tmp = только новый блок
|
||||
tmpBytes = newBlockFullBytes;
|
||||
} else {
|
||||
// не genesis: tmp = старый файл + новый блок
|
||||
byte[] oldBytes;
|
||||
try {
|
||||
oldBytes = fs.readBlockchain(blockchainName);
|
||||
@@ -111,7 +98,6 @@ public final class BlockchainWriter {
|
||||
throw new SQLException("Cannot read old blockchain file for: " + blockchainName, e);
|
||||
}
|
||||
|
||||
// (в идеале это всегда должно совпадать после verifyMainFileSizeMatchesStateOrAlert)
|
||||
if (oldBytes.length != (int) oldFileSize) {
|
||||
String msg =
|
||||
"Несовпадение размера файла блокчейна при чтении: " +
|
||||
@@ -127,8 +113,6 @@ public final class BlockchainWriter {
|
||||
tmpBytes = concat(oldBytes, newBlockFullBytes);
|
||||
}
|
||||
|
||||
// Пишем tmp на диск ДО транзакции БД:
|
||||
// - если сервер упадёт позже — tmp останется, но БД может не успеть обновиться (это ок для recovery)
|
||||
try {
|
||||
fs.writeBlockchainTmp(blockchainName, tmpBytes);
|
||||
} catch (Exception e) {
|
||||
@@ -138,9 +122,7 @@ public final class BlockchainWriter {
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// ШАГ 4. АТОМАРНО фиксируем БД:
|
||||
// - UPSERT blocks
|
||||
// - UPSERT blockchain_state (включая fileSizeBytes = newFileSize)
|
||||
// ШАГ 4. АТОМАРНО фиксируем БД
|
||||
// =====================================================================
|
||||
try (Connection c = db.getConnection()) {
|
||||
|
||||
@@ -150,21 +132,18 @@ public final class BlockchainWriter {
|
||||
boolean committed = false;
|
||||
|
||||
try {
|
||||
// 4.1) вставляем/апдейтим запись блока
|
||||
insertBlockRow(c, login, blockchainName, prevGlobalHashHex, block);
|
||||
insertBlockRow(c, login, blockchainName, prevGlobalHashHex, prevLineHashHex, block);
|
||||
|
||||
// 4.2) апдейтим состояние (включая fileSizeBytes)
|
||||
appendState(c, blockchainName, block.recordNumber, stOrNull, newHashHex, newFileSize);
|
||||
appendState(c, blockchainName, block, stOrNull, newHashHex, newFileSize);
|
||||
|
||||
// 4.3) commit
|
||||
c.commit();
|
||||
committed = true;
|
||||
|
||||
} catch (Exception e) {
|
||||
try { c.rollback(); } catch (SQLException ignore) {}
|
||||
|
||||
log.error("Ошибка транзакции БД при добавлении блока (rollback выполнен) (login={}, blockchainName={}, blockNumber={}, prevHash={}, newHash={}, oldFileSize={}, newFileSize={})",
|
||||
login, blockchainName, block.recordNumber, prevGlobalHashHex, newHashHex, oldFileSize, newFileSize, e);
|
||||
log.error("Ошибка транзакции БД при добавлении блока (rollback выполнен) (login={}, blockchainName={}, blockNumber={}, prevGlobalHash={}, prevLineHash={}, newHash={}, oldFileSize={}, newFileSize={})",
|
||||
login, blockchainName, block.recordNumber, prevGlobalHashHex, prevLineHashHex, newHashHex, oldFileSize, newFileSize, e);
|
||||
|
||||
if (e instanceof SQLException se) throw se;
|
||||
throw new SQLException("appendBlockAndState failed (db tx)", e);
|
||||
@@ -174,8 +153,7 @@ public final class BlockchainWriter {
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// ШАГ 5. После успешного коммита БД — атомарно заменяем файл:
|
||||
// <name>.tmp_bch -> <name>.bch
|
||||
// ШАГ 5. После успешного коммита БД — атомарно заменяем файл
|
||||
// =================================================================
|
||||
if (committed) {
|
||||
try {
|
||||
@@ -193,10 +171,6 @@ public final class BlockchainWriter {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверка: реальный размер <name>.bch должен совпадать с st.fileSizeBytes.
|
||||
* Если нет — это критическая внешняя порча/вмешательство, уведомляем админа и падаем.
|
||||
*/
|
||||
private void verifyMainFileSizeMatchesStateOrAlert(
|
||||
String login,
|
||||
String blockchainName,
|
||||
@@ -204,21 +178,13 @@ public final class BlockchainWriter {
|
||||
BlockchainStateEntry stOrNull
|
||||
) throws SQLException {
|
||||
|
||||
if (stOrNull == null) {
|
||||
// genesis — state ещё нет, проверять нечего
|
||||
return;
|
||||
}
|
||||
if (stOrNull == null) return;
|
||||
|
||||
long expected = stOrNull.getFileSizeBytes();
|
||||
if (expected <= 0) {
|
||||
// state есть, но ожидаемый размер 0 — это либо пустая цепочка, либо старый формат.
|
||||
// Здесь не трогаем (но можно усилить правила позже).
|
||||
return;
|
||||
}
|
||||
if (expected <= 0) return;
|
||||
|
||||
String mainFileName = fs.buildBlockchainFileName(blockchainName);
|
||||
|
||||
// Если файла нет — это уже очень подозрительно: state говорит “файл есть и размер > 0”
|
||||
if (!fs.exists(mainFileName)) {
|
||||
String msg =
|
||||
"КРИТИЧЕСКАЯ ОШИБКА КОНСИСТЕНТНОСТИ: state ожидает основной файл, но его нет. " +
|
||||
@@ -262,14 +228,16 @@ public final class BlockchainWriter {
|
||||
|
||||
/**
|
||||
* Обновление состояния blockchain_state (создаём если отсутствует).
|
||||
* Пока линии не используются: lineIndex=0 и lineHash = globalHash.
|
||||
*
|
||||
* + обновляем fileSizeBytes
|
||||
* ПРАВИЛО ЛИНИЙ (как ты описал):
|
||||
* - globalNumber=0 — genesis в lineIndex=0, lineNumber=0, и его hash — базовый для ВСЕХ линий.
|
||||
* - для lineIndex>0 первая запись имеет lineNumber=1, её prevLineHash = hash(genesis)
|
||||
* - lastLineNumber/lastLineHash ведём независимо по каждой линии.
|
||||
*/
|
||||
private void appendState(
|
||||
Connection c,
|
||||
String blockchainName,
|
||||
int globalNumber,
|
||||
BchBlockEntry block,
|
||||
BlockchainStateEntry stOrNull,
|
||||
String newHashHex,
|
||||
long newFileSizeBytes
|
||||
@@ -281,32 +249,38 @@ public final class BlockchainWriter {
|
||||
st.setBlockchainName(blockchainName);
|
||||
}
|
||||
|
||||
// Последний глобальный блок
|
||||
st.setLastGlobalNumber(globalNumber);
|
||||
// глобальная цепочка всегда растёт по recordNumber
|
||||
st.setLastGlobalNumber(block.recordNumber);
|
||||
st.setLastGlobalHash(newHashHex);
|
||||
|
||||
// Линии пока не используются
|
||||
st.setLastLineNumber(0, globalNumber);
|
||||
st.setLastLineHash(0, newHashHex);
|
||||
// обновляем конкретную линию блока
|
||||
int li = block.lineIndex;
|
||||
st.setLastLineNumber(li, block.lineNumber);
|
||||
st.setLastLineHash(li, newHashHex);
|
||||
|
||||
// ✅ ВАЖНО: сохраняем ожидаемый размер файла
|
||||
// file size
|
||||
st.setFileSizeBytes(newFileSizeBytes);
|
||||
|
||||
// Метка времени обновления
|
||||
// timestamp
|
||||
st.setUpdatedAtMs(System.currentTimeMillis());
|
||||
|
||||
// UPSERT
|
||||
stateDAO.upsert(c, st);
|
||||
}
|
||||
|
||||
/**
|
||||
* Вставка/апдейт строки блока в blocks.
|
||||
*
|
||||
* Важно:
|
||||
* - blockLinePreHashe = prevLineHashHex (а НЕ prevGlobalHashHex)
|
||||
* - msgType = body.type()
|
||||
* - Для ReactionBody заполняем toBchName/toBlockGlobalNumber/toBlockHashe (+ to_login если можем).
|
||||
*/
|
||||
private void insertBlockRow(
|
||||
Connection c,
|
||||
String login,
|
||||
String blockchainName,
|
||||
String prevGlobalHashHex,
|
||||
String prevLineHashHex,
|
||||
BchBlockEntry block
|
||||
) throws SQLException {
|
||||
|
||||
@@ -318,22 +292,33 @@ public final class BlockchainWriter {
|
||||
e.setBlockGlobalNumber(block.recordNumber);
|
||||
e.setBlockGlobalPreHashe(prevGlobalHashHex);
|
||||
|
||||
// линии пока не используем
|
||||
e.setBlockLineIndex(0);
|
||||
e.setBlockLineNumber(block.recordNumber);
|
||||
e.setBlockLinePreHashe(prevGlobalHashHex);
|
||||
e.setBlockLineIndex(block.lineIndex);
|
||||
e.setBlockLineNumber(block.lineNumber);
|
||||
e.setBlockLinePreHashe(prevLineHashHex);
|
||||
|
||||
// тип сообщения — по body.type()
|
||||
e.setMsgType(block.body.type());
|
||||
|
||||
// полный блок (RAW + signature + hash)
|
||||
e.setBlockByte(block.toBytes());
|
||||
|
||||
// defaults
|
||||
e.setToLogin(null);
|
||||
e.setToBchName(null);
|
||||
e.setToBlockGlobalNumber(null);
|
||||
e.setToBlockHashe(null);
|
||||
|
||||
// ReactionBody -> target fields
|
||||
if (block.body instanceof ReactionBody rb) {
|
||||
e.setToBchName(rb.toBlockchainName);
|
||||
e.setToBlockGlobalNumber(rb.toBlockGlobalNumber);
|
||||
e.setToBlockHashe(rb.toBlockHashHex());
|
||||
|
||||
// optional: try compute to_login from target chain name (для индекса idx_blocks_to_target)
|
||||
String toLogin = BlockchainNameUtil.loginFromBlockchainName(rb.toBlockchainName);
|
||||
if (toLogin != null && !toLogin.isBlank()) {
|
||||
e.setToLogin(toLogin);
|
||||
}
|
||||
}
|
||||
|
||||
blocksDAO.upsert(c, e);
|
||||
}
|
||||
|
||||
|
||||
+98
-64
@@ -27,22 +27,23 @@ import java.util.concurrent.locks.ReentrantLock;
|
||||
* Задачи:
|
||||
* 1) Лочим добавление блоков для конкретного blockchainName (защита от гонок в одном процессе).
|
||||
* 2) Декодируем блок из Base64 и парсим его структуру.
|
||||
* 3) Парсим body и валидируем (type/version + содержимое).
|
||||
* 3) Валидируем body (type/version + содержимое).
|
||||
* 4) Проверяем globalNumber и prevGlobalHash относительно server state.
|
||||
* 5) Проверяем подпись/хэш (Ed25519 над hash32, hash32=sha256(preimage)).
|
||||
* 6) Делаем запись в БД через BlockchainDbWriter (атомарность реализуется там).
|
||||
* 7) Возвращаем клиенту serverLastGlobalNumber/serverLastGlobalHash.
|
||||
* 5) Проверяем линии:
|
||||
* - genesis: global=0, lineIndex=0, lineNumber=0
|
||||
* - остальные: lineIndex=1..7, lineNumber по счётчику линии
|
||||
* 6) Проверяем подпись/хэш (Ed25519 над hash32, hash32=sha256(preimage)).
|
||||
* preimage включает prevLineHash32 (берём из state по lineIndex).
|
||||
* 7) Пишем в БД+файл через BlockchainWriter (атомарность там).
|
||||
*/
|
||||
public final class Net_AddBlock_Handler implements JsonMessageHandler {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(Net_AddBlock_Handler.class);
|
||||
|
||||
// DAO (перегрузки сами создают/закрывают Connection внутри)
|
||||
private final BlocksDAO blocksDAO = BlocksDAO.getInstance();
|
||||
private final BlockchainStateDAO stateDAO = BlockchainStateDAO.getInstance();
|
||||
private final SolanaUsersDAO solanaUsersDAO = SolanaUsersDAO.getInstance();
|
||||
|
||||
// Writer отвечает за транзакции/атомарность и консистентность БД
|
||||
private final BlockchainWriter dbWriter = new BlockchainWriter(blocksDAO, stateDAO);
|
||||
|
||||
@Override
|
||||
@@ -50,17 +51,17 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
|
||||
|
||||
Net_AddBlock_Request req = (Net_AddBlock_Request) baseReq;
|
||||
|
||||
// 0) Берём имя цепочки и лочим операции добавления для неё
|
||||
String blockchainName = req.getBlockchainName();
|
||||
ReentrantLock lock = BlockchainLocks.lockFor(blockchainName);
|
||||
lock.lock();
|
||||
try {
|
||||
AddBlockResult r = addBlock(blockchainName,
|
||||
AddBlockResult r = addBlock(
|
||||
blockchainName,
|
||||
req.getGlobalNumber(),
|
||||
req.getPrevGlobalHash(),
|
||||
req.getBlockBytesB64());
|
||||
req.getBlockBytesB64()
|
||||
);
|
||||
|
||||
// 7) Формируем стандартный Net_AddBlock_Response
|
||||
Net_AddBlock_Response resp = new Net_AddBlock_Response();
|
||||
resp.setOp(req.getOp());
|
||||
resp.setRequestId(req.getRequestId());
|
||||
@@ -73,7 +74,6 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
|
||||
resp.setReasonCode(r.reasonCode);
|
||||
}
|
||||
|
||||
// Возвращаем актуальное состояние сервера (даже при ошибках, где уместно)
|
||||
resp.setServerLastGlobalNumber(r.serverLastGlobalNumber);
|
||||
if (r.serverLastGlobalHash != null) {
|
||||
resp.setServerLastGlobalHash(r.serverLastGlobalHash);
|
||||
@@ -86,27 +86,17 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
|
||||
}
|
||||
}
|
||||
|
||||
/* ===================================================================== */
|
||||
/* ========================== Основная логика =========================== */
|
||||
/* ===================================================================== */
|
||||
|
||||
/**
|
||||
* Внутренняя логика добавления блока (без ручного управления Connection/tx).
|
||||
* Все атомарные записи — внутри BlockchainDbWriter.
|
||||
*/
|
||||
private AddBlockResult addBlock(
|
||||
String blockchainName,
|
||||
int globalNumber,
|
||||
String prevGlobalHashHex,
|
||||
String blockBytesB64
|
||||
) {
|
||||
// 1) Быстрая валидация входных параметров
|
||||
if (blockchainName == null || blockchainName.isBlank()) {
|
||||
log.warn("AddBlock: пустой blockchainName (globalNumber={})", globalNumber);
|
||||
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "empty_blockchain_name", 0, "");
|
||||
}
|
||||
|
||||
// 2) Из имени блокчейна вытаскиваем login (как ты и хотел — через util)
|
||||
String login = BlockchainNameUtil.loginFromBlockchainName(blockchainName);
|
||||
if (login == null || login.isBlank()) {
|
||||
log.warn("AddBlock: плохой blockchainName='{}' => login не получился (globalNumber={})",
|
||||
@@ -114,7 +104,6 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
|
||||
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_blockchain_name", 0, "");
|
||||
}
|
||||
|
||||
// 3) Декодируем блок из Base64
|
||||
final byte[] blockBytes;
|
||||
try {
|
||||
blockBytes = decodeBase64(blockBytesB64);
|
||||
@@ -124,17 +113,17 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
|
||||
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_block_base64", 0, "");
|
||||
}
|
||||
|
||||
// 4) Парсим блок (проверяется recordSize и минимальная длина)
|
||||
final BchBlockEntry block;
|
||||
try {
|
||||
block = new BchBlockEntry(blockBytes);
|
||||
} catch (Exception e) {
|
||||
// важно: BchBlockEntry теперь сам валит блок, если body в неправильной линии
|
||||
log.warn("AddBlock: не удалось распарсить BchBlockEntry (login={}, blockchainName={}, globalNumber={}, bytesLen={})",
|
||||
login, blockchainName, globalNumber, blockBytes.length, e);
|
||||
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_block_format", 0, "");
|
||||
}
|
||||
|
||||
// 5) Валидируем body (type/version + содержимое) — теперь body уже распарсен внутри BchBlockEntry
|
||||
// body.check()
|
||||
try {
|
||||
block.body.check();
|
||||
} catch (Exception e) {
|
||||
@@ -143,19 +132,18 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
|
||||
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_block_body", 0, "");
|
||||
}
|
||||
|
||||
// 6) Защита от рассинхрона: recordNumber внутри блока должен совпадать с заявленным globalNumber
|
||||
// recordNumber == globalNumber
|
||||
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", 0, "");
|
||||
}
|
||||
|
||||
// 7) Получаем пользователя и его loginKey (публичный ключ 32 байта)
|
||||
// user + pubkey
|
||||
SolanaUserEntry u;
|
||||
try {
|
||||
u = solanaUsersDAO.getByLogin(login); // перегрузка: сама открывает/закрывает соединение
|
||||
u = solanaUsersDAO.getByLogin(login);
|
||||
} catch (Exception e) {
|
||||
// ✅ ВОТ ТУТ ТВОЯ ОШИБКА РАНЬШЕ ТЕРЯЛАСЬ: теперь будет stacktrace в логе
|
||||
log.error("AddBlock: ошибка БД при чтении пользователя (login={}, blockchainName={}, globalNumber={})",
|
||||
login, blockchainName, globalNumber, e);
|
||||
return new AddBlockResult(WireCodes.Status.INTERNAL_ERROR, "db_error", 0, "");
|
||||
@@ -174,21 +162,21 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
|
||||
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_user_login_key", 0, "");
|
||||
}
|
||||
|
||||
// 8) Читаем текущее состояние блокчейна с сервера
|
||||
// state
|
||||
BlockchainStateEntry st;
|
||||
try {
|
||||
st = stateDAO.getByBlockchainName(blockchainName); // перегрузка: сама открывает/закрывает соединение
|
||||
st = stateDAO.getByBlockchainName(blockchainName);
|
||||
} catch (Exception e) {
|
||||
// ✅ ВОТ ТУТ ТВОЯ ОШИБКА РАНЬШЕ ТЕРЯЛАСЬ: теперь будет stacktrace в логе
|
||||
log.error("AddBlock: ошибка БД при чтении blockchain_state (login={}, blockchainName={}, globalNumber={})",
|
||||
login, blockchainName, globalNumber, 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) {
|
||||
// нет state => обязаны принимать genesis
|
||||
if (globalNumber != 0) {
|
||||
log.warn("AddBlock: blockchain_state_not_found, но globalNumber != 0 (login={}, blockchainName={}, globalNumber={})",
|
||||
login, blockchainName, globalNumber);
|
||||
@@ -201,15 +189,15 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
|
||||
serverLastHash = nn(st.getLastGlobalHash());
|
||||
}
|
||||
|
||||
// 10) Проверяем, что клиент присылает следующий блок ровно (last+1)
|
||||
int expected = serverLastNum + 1;
|
||||
if (globalNumber != expected) {
|
||||
// следующий global строго
|
||||
int expectedGlobal = serverLastNum + 1;
|
||||
if (globalNumber != expectedGlobal) {
|
||||
log.warn("AddBlock: bad_global_number (login={}, blockchainName={}, пришёл={}, ожидали={}, serverLastNum={}, serverLastHash={})",
|
||||
login, blockchainName, globalNumber, expected, serverLastNum, serverLastHash);
|
||||
login, blockchainName, globalNumber, expectedGlobal, serverLastNum, serverLastHash);
|
||||
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_global_number", serverLastNum, serverLastHash);
|
||||
}
|
||||
|
||||
// 11) Проверяем prevGlobalHash: клиент должен ссылаться на текущий serverLastHash
|
||||
// prevGlobalHash сравниваем со state.lastGlobalHash
|
||||
final byte[] prevGlobalHash32;
|
||||
final byte[] serverPrevGlobal32;
|
||||
try {
|
||||
@@ -227,60 +215,110 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
|
||||
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_prev_global_hash", serverLastNum, serverLastHash);
|
||||
}
|
||||
|
||||
// 12) Пока линии не используем — prevLineHash равен prevGlobalHash (как ты писал)
|
||||
byte[] prevLineHash32 = prevGlobalHash32;
|
||||
// ===========================
|
||||
// ЛИНИИ (строго)
|
||||
// ===========================
|
||||
|
||||
// 13) Криптопроверка: hash в блоке + подпись над hash
|
||||
int li = block.lineIndex;
|
||||
int ln = block.lineNumber;
|
||||
|
||||
if (globalNumber == 0) {
|
||||
// genesis
|
||||
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, serverLastHash);
|
||||
}
|
||||
} else {
|
||||
// MVP: запрещаем lineIndex=0 для не-genesis (чтобы техблоки не пролезли случайно)
|
||||
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, serverLastHash);
|
||||
}
|
||||
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, serverLastHash);
|
||||
}
|
||||
|
||||
if (st == null) {
|
||||
// теоретически сюда не должны попасть (global>0 при st==null уже отфутболили)
|
||||
return new AddBlockResult(WireCodes.Status.INTERNAL_ERROR, "internal_state_error", serverLastNum, serverLastHash);
|
||||
}
|
||||
|
||||
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, serverLastHash);
|
||||
}
|
||||
}
|
||||
|
||||
// prevLineHash берём из state по lineIndex:
|
||||
// - genesis: 32 нулей
|
||||
// - иначе: st.getLastLineHash(li) (для первой записи в линии это будет hash genesis)
|
||||
final byte[] prevLineHash32;
|
||||
final String prevLineHashHex;
|
||||
try {
|
||||
if (st == null) {
|
||||
prevLineHash32 = new byte[32];
|
||||
prevLineHashHex = "";
|
||||
} else {
|
||||
prevLineHashHex = nn(st.getLastLineHash(li));
|
||||
prevLineHash32 = hexTo32(prevLineHashHex);
|
||||
}
|
||||
} 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, serverLastHash);
|
||||
}
|
||||
|
||||
// crypto verify
|
||||
boolean ok = BchCryptoVerifier.verifyAll(
|
||||
login,
|
||||
prevGlobalHash32,
|
||||
prevLineHash32,
|
||||
block.getRawBytes(), // только RAW (без signature/hash)
|
||||
block.getSignature64(), // подпись Ed25519
|
||||
loginKey32, // public key пользователя
|
||||
block.getHash32() // ожидаемый hash32 из самого блока
|
||||
block.getRawBytes(),
|
||||
block.getSignature64(),
|
||||
loginKey32,
|
||||
block.getHash32()
|
||||
);
|
||||
|
||||
if (!ok) {
|
||||
log.warn("AddBlock: bad_signature_or_hash (login={}, blockchainName={}, globalNumber={})",
|
||||
login, blockchainName, globalNumber);
|
||||
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, serverLastHash);
|
||||
}
|
||||
|
||||
// 14) Новый hash блока (hex) — то, что будет записано как lastGlobalHash
|
||||
String newHashHex = toHex(block.getHash32());
|
||||
|
||||
// 15) Запись блока + обновление состояния (атомарность/транзакции — внутри dbWriter)
|
||||
// write
|
||||
try {
|
||||
dbWriter.appendBlockAndState(
|
||||
login,
|
||||
blockchainName,
|
||||
nn(prevGlobalHashHex),
|
||||
block, // ✅ передаём целиком объект блока
|
||||
prevLineHashHex,
|
||||
block,
|
||||
st,
|
||||
newHashHex
|
||||
);
|
||||
} catch (Exception e) {
|
||||
// ✅ ВОТ ЭТО САМОЕ ВАЖНОЕ: если упал writer/БД/файлы — теперь будет stacktrace в логах
|
||||
log.error("AddBlock: внутренняя ошибка при записи блока (login={}, blockchainName={}, globalNumber={}, newHash={})",
|
||||
login, blockchainName, globalNumber, newHashHex, e);
|
||||
return new AddBlockResult(WireCodes.Status.INTERNAL_ERROR, "internal_error", serverLastNum, serverLastHash);
|
||||
}
|
||||
|
||||
// 16) Успех
|
||||
log.info("✅ AddBlock ok: login={}, blockchainName={}, globalNumber={}, newHash={}",
|
||||
login, blockchainName, globalNumber, newHashHex);
|
||||
log.info("✅ AddBlock ok: login={}, blockchainName={}, globalNumber={}, lineIndex={}, lineNumber={}, newHash={}",
|
||||
login, blockchainName, globalNumber, li, ln, newHashHex);
|
||||
|
||||
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 httpStatus;
|
||||
final String reasonCode;
|
||||
final int serverLastGlobalNumber;
|
||||
final String serverLastGlobalHash;
|
||||
|
||||
@@ -296,10 +334,6 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
|
||||
}
|
||||
}
|
||||
|
||||
/* ===================================================================== */
|
||||
/* ============================== Utils ================================= */
|
||||
/* ===================================================================== */
|
||||
|
||||
private static String nn(String s) { return s == null ? "" : s; }
|
||||
|
||||
private static byte[] decodeBase64(String s) {
|
||||
|
||||
Reference in New Issue
Block a user