refactor: перевели хэши на BLOB и добавили поля block_hash / block_signature / edited_by_block_global_number

и главное добавили тип блока изменение сообщение и сслку на последнее изменение в табл блокс
This commit is contained in:
AidarKC
2026-01-07 19:58:50 +03:00
parent 8bcaa192c5
commit 06c77b1c1f
13 changed files with 306 additions and 284 deletions
@@ -74,9 +74,7 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
}
resp.setServerLastGlobalNumber(r.serverLastGlobalNumber);
if (r.serverLastGlobalHash != null) {
resp.setServerLastGlobalHash(r.serverLastGlobalHash);
}
resp.setServerLastGlobalHash(r.serverLastGlobalHashHex);
return resp;
@@ -116,32 +114,31 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
}
if (st == null) {
// теперь даже для genesis это ошибка: state должен быть создан заранее (с lastGlobalNumber=-1)
log.warn("AddBlock: blockchain_state_not_found (login={}, blockchainName={}, globalNumber={})",
login, blockchainName, globalNumber);
return new AddBlockResult(WireCodes.Status.NOT_FOUND, "blockchain_state_not_found", -1, "");
}
final int serverLastNum = st.getLastGlobalNumber();
final String serverLastHash = nn(st.getLastGlobalHash());
final String serverLastHashHex = toHex(nnBytes(st.getLastGlobalHash()));
// ✅ для 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, serverLastHash);
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "genesis_but_state_not_initial", serverLastNum, serverLastHashHex);
}
// следующий global строго
int expectedGlobal = serverLastNum + 1;
if (globalNumber != expectedGlobal) {
log.warn("AddBlock: bad_global_number (login={}, blockchainName={}, пришёл={}, ожидали={}, serverLastNum={}, serverLastHash={})",
login, blockchainName, globalNumber, expectedGlobal, serverLastNum, serverLastHash);
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_global_number", serverLastNum, serverLastHash);
login, blockchainName, globalNumber, expectedGlobal, serverLastNum, serverLastHashHex);
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_global_number", serverLastNum, serverLastHashHex);
}
// -------------------------------------------------------------------
// ✅ 2) Декодируем блок (раньше парсинга body)
// ✅ 2) Декодируем блок
// -------------------------------------------------------------------
final byte[] blockBytes;
try {
@@ -149,26 +146,26 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
} catch (Exception e) {
log.warn("AddBlock: некорректный base64 блока (login={}, blockchainName={}, globalNumber={})",
login, blockchainName, globalNumber, e);
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_block_base64", serverLastNum, serverLastHash);
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_block_base64", serverLastNum, serverLastHashHex);
}
// -------------------------------------------------------------------
// ✅ 3) Ранняя проверка лимита ДО любых записей (как ты попросил)
// ✅ 3) Ранняя проверка лимита
// -------------------------------------------------------------------
try {
long oldSize = st.getFileSizeBytes();
long limit = st.getSizeLimit(); // предполагается, что поле уже есть (size_limit)
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);
return new AddBlockResult(413, "limit_exceeded", serverLastNum, serverLastHash);
return new AddBlockResult(413, "limit_exceeded", serverLastNum, serverLastHashHex);
}
} catch (Exception e) {
log.error("AddBlock: limit_check_failed (login={}, blockchainName={}, globalNumber={})",
login, blockchainName, globalNumber, e);
return new AddBlockResult(WireCodes.Status.INTERNAL_ERROR, "limit_check_failed", serverLastNum, serverLastHash);
return new AddBlockResult(WireCodes.Status.INTERNAL_ERROR, "limit_check_failed", serverLastNum, serverLastHashHex);
}
// -------------------------------------------------------------------
@@ -178,26 +175,23 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
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", serverLastNum, serverLastHash);
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);
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_block_body", serverLastNum, serverLastHash);
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_block_body", serverLastNum, serverLastHashHex);
}
// 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", serverLastNum, serverLastHash);
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "global_number_mismatch", serverLastNum, serverLastHashHex);
}
// -------------------------------------------------------------------
@@ -205,90 +199,79 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
// -------------------------------------------------------------------
final byte[] loginKey32;
try {
// предполагается, что st.getBlockchainKey() возвращает base64-строку, а getBlockchainKeyByte() -> 32 bytes
loginKey32 = 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, serverLastHash);
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_blockchain_key_in_state", serverLastNum, serverLastHashHex);
}
if (loginKey32 == null || loginKey32.length != 32) {
log.warn("AddBlock: bad_blockchain_key_len (login={}, blockchainName={}, globalNumber={}, keyLen={})",
login, blockchainName, globalNumber, (loginKey32 == null ? -1 : loginKey32.length));
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_blockchain_key_len", serverLastNum, serverLastHash);
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_blockchain_key_len", serverLastNum, serverLastHashHex);
}
// -------------------------------------------------------------------
// ✅ 6) prevGlobalHash сравниваем со state.lastGlobalHash
// ✅ 6) prevGlobalHash сравниваем со state.lastGlobalHash (оба byte[32])
// -------------------------------------------------------------------
final byte[] prevGlobalHash32;
final byte[] serverPrevGlobal32;
try {
prevGlobalHash32 = hexTo32(nn(prevGlobalHashHex));
serverPrevGlobal32 = hexTo32(nn(st.getLastGlobalHash())); // если пусто -> 32 нуля
prevGlobalHash32 = hexTo32(nn(prevGlobalHashHex)); // "" -> 32 нуля
} 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, serverLastHash);
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_prev_global_hash_format", 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), nn(st.getLastGlobalHash()));
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_prev_global_hash", serverLastNum, serverLastHash);
login, blockchainName, globalNumber, nn(prevGlobalHashHex), toHex(serverPrevGlobal32));
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_prev_global_hash", serverLastNum, serverLastHashHex);
}
// ===========================
// ЛИНИИ (строго)
// ===========================
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);
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_genesis_line_fields", serverLastNum, serverLastHashHex);
}
} 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);
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, serverLastHash);
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_line_index", serverLastNum, serverLastHashHex);
}
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);
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_line_number", serverLastNum, serverLastHashHex);
}
}
// prevLineHash берём из state по lineIndex:
// - genesis: 32 нулей
// - иначе: st.getLastLineHash(li) (для первой записи в линии это будет hash genesis)
final byte[] prevLineHash32;
final String prevLineHashHex;
try {
prevLineHashHex = computePrevLineHashHex(st, li);
prevLineHash32 = hexTo32(prevLineHashHex);
prevLineHash32 = computePrevLineHash32(st, li);
} 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);
return new AddBlockResult(WireCodes.Status.INTERNAL_ERROR, "bad_prev_line_hash_in_state", serverLastNum, serverLastHashHex);
}
// crypto verify
boolean ok = BchCryptoVerifier.verifyAll(
login,
prevGlobalHash32,
@@ -302,28 +285,27 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
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, serverLastHash);
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_signature_or_hash", serverLastNum, serverLastHashHex);
}
String newHashHex = toHex(block.getHash32());
// write
try {
dbWriter.appendBlockAndState(
login,
blockchainName,
nn(prevGlobalHashHex),
prevLineHashHex,
prevGlobalHash32,
prevLineHash32,
block,
st,
newHashHex
st
);
} catch (Exception e) {
log.error("AddBlock: внутренняя ошибка при записи блока (login={}, blockchainName={}, globalNumber={}, newHash={})",
login, blockchainName, globalNumber, newHashHex, e);
return new AddBlockResult(WireCodes.Status.INTERNAL_ERROR, "internal_error", serverLastNum, serverLastHash);
log.error("AddBlock: внутренняя ошибка при записи блока (login={}, blockchainName={}, globalNumber={})",
login, blockchainName, globalNumber, 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);
@@ -332,43 +314,42 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
/**
* ✅ Правило:
* - lineIndex=0 (genesis линия): prevLineHash = 32 нулей (пустая строка => hexTo32 даст 32 нуля)
* - lineIndex=0: prevLineHash = 32 нулей
* - lineIndex>0:
* - если в этой линии ещё нет блоков (lastLineNumber==0) => prevLineHash = hash(genesis) (line0 hash)
* - иначе => prevLineHash = lastLineHash(lineIndex)
*/
private static String computePrevLineHashHex(BlockchainStateEntry st, int lineIndex) {
private static byte[] computePrevLineHash32(BlockchainStateEntry st, int lineIndex) {
if (lineIndex == 0) {
return ""; // -> 32 нуля
return new byte[32];
}
int lastLn = st.getLastLineNumber(lineIndex);
if (lastLn == 0) {
// первая запись линии -> от genesis
String genesis = nn(st.getLastLineHash(0));
if (!genesis.isBlank()) return genesis;
byte[] genesis = nnBytes(st.getLastLineHash(0));
if (genesis.length == 32) return genesis;
// fallback: если line0 почему-то не заполнена, но genesis глобально есть
String g = nn(st.getLastGlobalHash());
if (!g.isBlank()) return g;
byte[] g = nnBytes(st.getLastGlobalHash());
if (g.length == 32) return g;
return "";
return new byte[32];
}
return nn(st.getLastLineHash(lineIndex));
byte[] last = nnBytes(st.getLastLineHash(lineIndex));
return last.length == 32 ? last : new byte[32];
}
private static final class AddBlockResult {
final int httpStatus;
final String reasonCode;
final int serverLastGlobalNumber;
final String serverLastGlobalHash;
final String serverLastGlobalHashHex;
AddBlockResult(int httpStatus, String reasonCode, int serverLastGlobalNumber, String serverLastGlobalHash) {
AddBlockResult(int httpStatus, String reasonCode, int serverLastGlobalNumber, String serverLastGlobalHashHex) {
this.httpStatus = httpStatus;
this.reasonCode = reasonCode;
this.serverLastGlobalNumber = serverLastGlobalNumber;
this.serverLastGlobalHash = serverLastGlobalHash;
this.serverLastGlobalHashHex = serverLastGlobalHashHex;
}
boolean isOk() {
@@ -378,6 +359,8 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
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);
@@ -408,6 +391,7 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
}
private static String toHex(byte[] bytes) {
if (bytes == null || bytes.length == 0) return "";
char[] HEX = "0123456789abcdef".toCharArray();
char[] out = new char[bytes.length * 2];
for (int i = 0; i < bytes.length; i++) {
@@ -1,3 +1,6 @@
// =======================
// BlockchainWriter.java (НОВАЯ ВЕРСИЯ)
// =======================
package server.logic.ws_protocol.JSON.handlers.blockchain.Net_AddBlock_Handler_utils;
import blockchain.BchBlockEntry;
@@ -15,6 +18,8 @@ import shine.log.BlockchainAdminNotifier;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Types;
import java.util.Base64;
/**
* BlockchainWriter — единая точка записи:
@@ -25,24 +30,11 @@ import java.sql.SQLException;
* 3) атомарно заменяем файл:
* - удаляем/замещаем старый <name>.bch
* - переименовываем <name>.tmp_bch -> <name>.bch
*
* Важно:
* - Шаг (2) — строго атомарный (SQL tx).
* - Шаг (3) — атомарный на уровне ФС, если поддерживается ATOMIC_MOVE.
*
* ДОПОЛНЕНИЕ (КРИТИЧНО):
* - Перед тем как дописывать блок, проверяем:
* реальный размер <name>.bch == st.fileSizeBytes.
* Если не совпадает — считаем это критической внешней порчей файлов,
* шлём уведомление админу и НЕ продолжаем запись.
*/
public final class BlockchainWriter {
private static final Logger log = LoggerFactory.getLogger(BlockchainWriter.class);
private static final String ZERO_HASH_64 =
"0000000000000000000000000000000000000000000000000000000000000000";
private final SqliteDbController db;
private final BlocksDAO blocksDAO;
private final BlockchainStateDAO stateDAO;
@@ -55,44 +47,28 @@ public final class BlockchainWriter {
this.fs = FileStoreUtil.getInstance();
}
/**
* Главный метод:
* - (0) проверяет соответствие размера файла и state (если это не genesis)
* - создаёт tmp-файл (старое+новое),
* - атомарно коммитит БД (block+state),
* - атомарно заменяет основной файл.
*/
public void appendBlockAndState(
String login,
String blockchainName,
String prevGlobalHashHex,
String prevLineHashHex,
byte[] prevGlobalHash32,
byte[] prevLineHash32,
BchBlockEntry block,
BlockchainStateEntry stOrNull,
String newHashHex
BlockchainStateEntry stOrNull
) throws SQLException {
// ✅ ВАЖНО: state теперь ОБЯЗАТЕЛЕН, genesis НЕ создаёт запись, а обновляет существующую
if (stOrNull == null) {
throw new SQLException("blockchain_state not found for blockchainName=" + blockchainName + " (state обязателен)");
}
verifyMainFileSizeMatchesStateOrAlert(login, blockchainName, block, stOrNull);
// =====================================================================
// ШАГ 1. Готовим bytes нового блока (включая signature+hash)
// =====================================================================
// bytes FULL блока (raw+sig+hash)
final byte[] newBlockFullBytes = block.toBytes();
// =====================================================================
// ШАГ 2. Считаем новый fileSizeBytes
// =====================================================================
final long oldFileSize = stOrNull.getFileSizeBytes();
final long newFileSize = safeAdd(oldFileSize, newBlockFullBytes.length);
// =====================================================================
// ШАГ 3. Создаём новый tmp-файл: tmp = (old file bytes) + (new block bytes)
// =====================================================================
// tmp = old + new
final byte[] tmpBytes;
if (oldFileSize == 0) {
tmpBytes = newBlockFullBytes;
@@ -129,9 +105,7 @@ public final class BlockchainWriter {
throw new SQLException("Cannot write tmp blockchain file for: " + blockchainName, e);
}
// =====================================================================
// ШАГ 4. АТОМАРНО фиксируем БД
// =====================================================================
// атомарно БД
try (Connection c = db.getConnection()) {
boolean oldAutoCommit = c.getAutoCommit();
@@ -140,9 +114,8 @@ public final class BlockchainWriter {
boolean committed = false;
try {
insertBlockRow(c, login, blockchainName, prevGlobalHashHex, prevLineHashHex, block);
appendState(c, blockchainName, block, stOrNull, newHashHex, newFileSize);
insertBlockRow(c, login, blockchainName, prevGlobalHash32, prevLineHash32, block);
appendState(c, blockchainName, block, stOrNull, newFileSize);
c.commit();
committed = true;
@@ -150,8 +123,8 @@ public final class BlockchainWriter {
} catch (Exception e) {
try { c.rollback(); } catch (SQLException ignore) {}
log.error("Ошибка транзакции БД при добавлении блока (rollback выполнен) (login={}, blockchainName={}, blockNumber={}, prevGlobalHash={}, prevLineHash={}, newHash={}, oldFileSize={}, newFileSize={})",
login, blockchainName, block.recordNumber, prevGlobalHashHex, prevLineHashHex, newHashHex, oldFileSize, newFileSize, e);
log.error("Ошибка транзакции БД при добавлении блока (rollback выполнен) (login={}, blockchainName={}, blockNumber={}, oldFileSize={}, newFileSize={})",
login, blockchainName, block.recordNumber, oldFileSize, newFileSize, e);
if (e instanceof SQLException se) throw se;
throw new SQLException("appendBlockAndState failed (db tx)", e);
@@ -160,15 +133,13 @@ public final class BlockchainWriter {
try { c.setAutoCommit(oldAutoCommit); } catch (SQLException ignore) {}
}
// =================================================================
// ШАГ 5. После успешного коммита БД — атомарно заменяем файл
// =================================================================
// после коммита БД — атомарно заменяем файл
if (committed) {
try {
fs.atomicReplaceBlockchainFile(blockchainName);
} catch (Exception moveError) {
log.error("БД закоммичена, но атомарная замена файла блокчейна не удалась. tmp оставлен для recovery. (login={}, blockchainName={}, blockNumber={}, newHash={}, tmpBytesLen={})",
login, blockchainName, block.recordNumber, newHashHex, tmpBytes.length, 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,
@@ -200,7 +171,6 @@ public final class BlockchainWriter {
", blockchainName=" + blockchainName +
", expectedSizeFromState=" + expected +
", blockNumber=" + (block != null ? block.recordNumber : -1) + ".";
BlockchainAdminNotifier.critical(msg, null);
throw new SQLException(msg);
}
@@ -228,43 +198,32 @@ public final class BlockchainWriter {
", realMainFileSize=" + real +
", blockNumber=" + (block != null ? block.recordNumber : -1) + ". " +
"Похоже на внешнее вмешательство/порчу файла. Запись нового блока остановлена.";
BlockchainAdminNotifier.critical(msg, null);
throw new SQLException(msg);
}
}
/**
* Обновление состояния blockchain_state (создаём если отсутствует).
*
* ПРАВИЛО ЛИНИЙ (как ты описал):
* - globalNumber=0 — genesis в lineIndex=0, lineNumber=0, и его hash — базовый для ВСЕХ линий.
* - для lineIndex>0 первая запись имеет lineNumber=1, её prevLineHash = hash(genesis)
* - lastLineNumber/lastLineHash ведём независимо по каждой линии.
*/
private void appendState(
Connection c,
String blockchainName,
BchBlockEntry block,
BlockchainStateEntry stOrNull,
String newHashHex,
long newFileSizeBytes
) throws SQLException {
// ✅ state обязателен
BlockchainStateEntry st = stOrNull;
if (st == null) {
throw new SQLException("blockchain_state not found for blockchainName=" + blockchainName);
}
// глобальная цепочка всегда растёт по recordNumber
// глобальная цепочка
st.setLastGlobalNumber(block.recordNumber);
st.setLastGlobalHash(newHashHex);
st.setLastGlobalHash(block.getHash32());
// обновляем конкретную линию блока
// линия
int li = block.lineIndex;
st.setLastLineNumber(li, block.lineNumber);
st.setLastLineHash(li, newHashHex);
st.setLastLineHash(li, block.getHash32());
// file size
st.setFileSizeBytes(newFileSizeBytes);
@@ -276,24 +235,14 @@ public final class BlockchainWriter {
}
/**
* Вставка/апдейт строки блока в blocks.
*
* Важно:
* - blockLinePreHashe = prevLineHashHex (а НЕ prevGlobalHashHex)
* - msgType = body.type()
* - msgSubType = body.subType()
* - to* поля берём через BodyToFields (если body его поддерживает)
*
* Про toLogin:
* - если body сам даёт toLogin — пишем его
* - иначе, если есть toBchName — пробуем вычислить login из имени блокчейна (про запас)
* Вставка/апдейт строки блока в blocks (BLOB-вариант).
*/
private void insertBlockRow(
Connection c,
String login,
String blockchainName,
String prevGlobalHashHex,
String prevLineHashHex,
byte[] prevGlobalHash32,
byte[] prevLineHash32,
BchBlockEntry block
) throws SQLException {
@@ -303,38 +252,31 @@ public final class BlockchainWriter {
e.setBchName(blockchainName);
e.setBlockGlobalNumber(block.recordNumber);
e.setBlockGlobalPreHashe(prevGlobalHashHex);
e.setBlockGlobalPreHashe(prevGlobalHash32);
e.setBlockLineIndex(block.lineIndex);
e.setBlockLineNumber(block.lineNumber);
// ✅ минимальная правка: для genesis сохраняем именно "64 нуля", а не пустую строку/NULL
String linePre = prevLineHashHex;
if (block.recordNumber == 0 && (linePre == null || linePre.isBlank())) {
linePre = ZERO_HASH_64;
}
e.setBlockLinePreHashe(linePre);
e.setBlockLinePreHashe(prevLineHash32);
e.setMsgType(block.body.type());
e.setMsgSubType(block.body.subType());
// ВАЖНО: здесь ты кладёшь FULL bytes (raw+sig+hash). Это ок, ты так задумал.
e.setBlockByte(block.toBytes());
// defaults
// to-поля
e.setToLogin(null);
e.setToBchName(null);
e.setToBlockGlobalNumber(null);
e.setToBlockHashe(null);
// ✅ Универсально: если body поддерживает to-поля — пишем их
if (block.body instanceof BodyHasTarget tf) {
e.setToLogin(tf.toLogin());
e.setToBchName(tf.toBchName());
e.setToBlockGlobalNumber(tf.toBlockGlobalNumber());
e.setToBlockHashe(tf.toBlockHashe());
e.setToBlockHashe(tf.toBlockHasheBytes());
// optional: try compute to_login from target chain name (для индекса idx_blocks_to_target)
// если to_login не пришёл, но есть to_bch_name — восстановим логин из имени цепочки
if (e.getToLogin() == null && e.getToBchName() != null) {
String toLogin = BlockchainNameUtil.loginFromBlockchainName(e.getToBchName());
if (toLogin != null && !toLogin.isBlank()) {
@@ -343,12 +285,17 @@ public final class BlockchainWriter {
}
}
// новое: хэш и подпись самого блока
e.setBlockHash(block.getHash32());
e.setBlockSignature(block.getSignature64());
// новое: не трогаем (NULL); триггер пометит исходный блок
e.setEditedByBlockGlobalNumber(null);
blocksDAO.upsert(c, e);
}
/* ===================================================================== */
/* =============================== Utils ================================ */
/* ===================================================================== */
// -------------------- utils --------------------
private static byte[] concat(byte[] a, byte[] b) {
byte[] out = new byte[a.length + b.length];
@@ -364,4 +311,32 @@ public final class BlockchainWriter {
}
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;
}
}
}
@@ -102,7 +102,7 @@ public class Net_AddUser_Handler implements JsonMessageHandler {
st.setLogin(req.getLogin());
st.setBlockchainKey(req.getLoginKey()); // Base64(32)
st.setLastGlobalNumber(-1);
st.setLastGlobalHash("");
st.setLastGlobalHash(new byte[32]);
st.setFileSizeBytes(0);
st.setSizeLimit(limit);
st.setUpdatedAtMs(System.currentTimeMillis());