Вроде как сделал работу с линиями :) но ещё не тестил
This commit is contained in:
AidarKC 2025-12-28 20:11:31 +03:00
parent b26e09904a
commit c523816cdf
8 changed files with 344 additions and 138 deletions

View File

@ -79,6 +79,15 @@ public final class BchBlockEntry {
// Сразу парсим BodyRecord (и если неизвестный type/version тут же упадём) // Сразу парсим BodyRecord (и если неизвестный type/version тут же упадём)
this.body = BodyRecordParser.parse(this.bodyBytes); this.body = BodyRecordParser.parse(this.bodyBytes);
// УРОВЕНЬ B: проверка ожидаемой линии по типу body
short expectedLine = this.body.expectedLineIndex();
if (this.lineIndex != expectedLine) {
throw new IllegalArgumentException(
"Body is in wrong lineIndex: expected=" + expectedLine + " actual=" + this.lineIndex +
" (type=" + this.body.type() + " ver=" + this.body.version() + ")"
);
}
this.signature64 = new byte[SIGNATURE_LEN]; this.signature64 = new byte[SIGNATURE_LEN];
bb.get(this.signature64); bb.get(this.signature64);
@ -118,6 +127,15 @@ public final class BchBlockEntry {
// И при сборке тоже сразу парсим body (чтобы объект был цельным) // И при сборке тоже сразу парсим body (чтобы объект был цельным)
this.body = BodyRecordParser.parse(this.bodyBytes); this.body = BodyRecordParser.parse(this.bodyBytes);
// УРОВЕНЬ B: проверка ожидаемой линии по типу body
short expectedLine = this.body.expectedLineIndex();
if (this.lineIndex != expectedLine) {
throw new IllegalArgumentException(
"Body is in wrong lineIndex: expected=" + expectedLine + " actual=" + this.lineIndex +
" (type=" + this.body.type() + " ver=" + this.body.version() + ")"
);
}
this.signature64 = Arrays.copyOf(signature64, SIGNATURE_LEN); this.signature64 = Arrays.copyOf(signature64, SIGNATURE_LEN);
this.hash32 = Arrays.copyOf(hash32, HASH_LEN); this.hash32 = Arrays.copyOf(hash32, HASH_LEN);
@ -140,14 +158,12 @@ public final class BchBlockEntry {
} }
public byte[] getRawBytes() { public byte[] getRawBytes() {
int rawLen = recordSize; // теперь это ровно RAW, без signature+hash int rawLen = recordSize; // ровно RAW, без signature+hash
byte[] raw = new byte[rawLen]; byte[] raw = new byte[rawLen];
System.arraycopy(fullBytes, 0, raw, 0, rawLen); System.arraycopy(fullBytes, 0, raw, 0, rawLen);
return raw; return raw;
} }
/* ===================================================================== */
public byte[] getSignature64() { public byte[] getSignature64() {
return Arrays.copyOf(signature64, SIGNATURE_LEN); return Arrays.copyOf(signature64, SIGNATURE_LEN);
} }

View File

@ -4,7 +4,7 @@ package blockchain.body;
* BodyRecord_new общий контракт для всех типов body (тела блока). * BodyRecord_new общий контракт для всех типов body (тела блока).
* *
* Идея: * Идея:
* - На каждый тип body (Header, Text, File, ...) отдельный класс. * - На каждый тип body (Header, Text, Reaction, ...) отдельный класс.
* - Десериализация из байтов делается КОНСТРУКТОРОМ: * - Десериализация из байтов делается КОНСТРУКТОРОМ:
* new XxxBody_new(byte[] bodyBytes) * new XxxBody_new(byte[] bodyBytes)
* (конструктор обязан распарсить байты или кинуть IllegalArgumentException). * (конструктор обязан распарсить байты или кинуть IllegalArgumentException).
@ -18,21 +18,28 @@ package blockchain.body;
* *
* - type() и version() это идентификаторы формата body. * - type() и version() это идентификаторы формата body.
* Они должны быть константами для класса (например TYPE=1, VERSION=1). * Они должны быть константами для класса (например TYPE=1, VERSION=1).
*
* ДОПОЛНЕНИЕ (ЛИНИИ):
* - Каждый тип body знает, в какой lineIndex он ДОЛЖЕН находиться.
* Это проверяется в валидаторе блока (уровень B).
*/ */
public interface BodyRecord { public interface BodyRecord {
/** Код типа записи (совпадает с recordType в BchBlockEntry). */ /** Код типа записи (совпадает с type в bodyBytes). */
short type(); short type();
/** Версия формата записи (совпадает с recordTypeVersion в BchBlockEntry). */ /** Версия формата записи (совпадает с version в bodyBytes). */
short version(); short version();
/** Ожидаемый индекс линии для этого body. */
short expectedLineIndex();
/** Проверить корректность содержимого и вернуть этот объект (или кинуть исключение). */ /** Проверить корректность содержимого и вернуть этот объект (или кинуть исключение). */
BodyRecord check(); BodyRecord check();
/** /**
* Сериализовать тело записи в байты (ровно то, что кладётся в block.body). * Сериализовать тело записи в байты (ровно то, что кладётся в block.body).
* Важно: НЕ включает общий заголовок блока (recordNumber/timestamp/type/version). * Важно: включает type/version.
*/ */
byte[] toBytes(); byte[] toBytes();
} }

View File

@ -20,6 +20,7 @@ public final class BodyRecordParser {
return switch (key) { return switch (key) {
case 0x0000_0001 -> new HeaderBody(bodyBytes); // type=0, ver=1 case 0x0000_0001 -> new HeaderBody(bodyBytes); // type=0, ver=1
case 0x0001_0001 -> new TextBody(bodyBytes); // type=1, ver=1 case 0x0001_0001 -> new TextBody(bodyBytes); // type=1, ver=1
case 0x0002_0001 -> new ReactionBody(bodyBytes); // type=2, ver=1
default -> throw new IllegalArgumentException(String.format( default -> throw new IllegalArgumentException(String.format(
"Unknown body type/version: type=%d ver=%d (key=0x%08X)", "Unknown body type/version: type=%d ver=%d (key=0x%08X)",
(type & 0xFFFF), (ver & 0xFFFF), key (type & 0xFFFF), (ver & 0xFFFF), key

View File

@ -14,6 +14,9 @@ import java.util.Objects;
* [8] tag ASCII "SHiNE001" * [8] tag ASCII "SHiNE001"
* [1] loginLength=N (uint8) * [1] loginLength=N (uint8)
* [N] login UTF-8 * [N] login UTF-8
*
* ЛИНИЯ:
* - строго lineIndex=0 (genesis)
*/ */
public final class HeaderBody implements BodyRecord { public final class HeaderBody implements BodyRecord {
@ -64,6 +67,11 @@ public final class HeaderBody implements BodyRecord {
@Override public short type() { return TYPE; } @Override public short type() { return TYPE; }
@Override public short version() { return VER; } @Override public short version() { return VER; }
@Override
public short expectedLineIndex() {
return 0;
}
@Override @Override
public HeaderBody check() { public HeaderBody check() {
if (login == null || login.isBlank()) if (login == null || login.isBlank())

View File

@ -0,0 +1,139 @@
package blockchain.body;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Objects;
/**
* ReactionBody type=2, version=1.
*
* Сериализация bodyBytes:
* [2] type=2
* [2] ver=1
* [4] reactionCode (int32)
* [1] toBlockchainNameLen (uint8)
* [N] toBlockchainName UTF-8
* [4] toBlockGlobalNumber (int32)
* [32] toBlockHash (raw 32 bytes)
*
* ЛИНИЯ:
* - строго lineIndex=2
*
* ВАЖНО:
* - Здесь мы НЕ проверяем, существует ли цель реакции (MVP правило).
*/
public final class ReactionBody implements BodyRecord {
public static final short TYPE = 2;
public static final short VER = 1;
public final int reactionCode;
public final String toBlockchainName;
public final int toBlockGlobalNumber;
public final byte[] toBlockHash32;
/** Десериализация из полного bodyBytes (включая type/version). */
public ReactionBody(byte[] bodyBytes) {
Objects.requireNonNull(bodyBytes, "bodyBytes == null");
if (bodyBytes.length < 4 + 4 + 1 + 1 + 4 + 32) {
throw new IllegalArgumentException("ReactionBody too short");
}
ByteBuffer bb = ByteBuffer.wrap(bodyBytes).order(ByteOrder.BIG_ENDIAN);
short type = bb.getShort();
short ver = bb.getShort();
if (type != TYPE || ver != VER)
throw new IllegalArgumentException("Not ReactionBody: type=" + type + " ver=" + ver);
this.reactionCode = bb.getInt();
int nameLen = Byte.toUnsignedInt(bb.get());
if (nameLen <= 0) throw new IllegalArgumentException("toBlockchainNameLen is 0");
if (bb.remaining() < nameLen + 4 + 32) throw new IllegalArgumentException("ReactionBody payload too short");
byte[] nameBytes = new byte[nameLen];
bb.get(nameBytes);
this.toBlockchainName = new String(nameBytes, StandardCharsets.UTF_8);
this.toBlockGlobalNumber = bb.getInt();
this.toBlockHash32 = new byte[32];
bb.get(this.toBlockHash32);
}
public ReactionBody(int reactionCode, String toBlockchainName, int toBlockGlobalNumber, byte[] toBlockHash32) {
Objects.requireNonNull(toBlockchainName, "toBlockchainName == null");
Objects.requireNonNull(toBlockHash32, "toBlockHash32 == null");
if (toBlockchainName.isBlank()) throw new IllegalArgumentException("toBlockchainName is blank");
if (toBlockHash32.length != 32) throw new IllegalArgumentException("toBlockHash32 != 32");
this.reactionCode = reactionCode;
this.toBlockchainName = toBlockchainName;
this.toBlockGlobalNumber = toBlockGlobalNumber;
this.toBlockHash32 = Arrays.copyOf(toBlockHash32, 32);
}
@Override public short type() { return TYPE; }
@Override public short version() { return VER; }
@Override
public short expectedLineIndex() {
return 2;
}
@Override
public ReactionBody check() {
if (toBlockchainName == null || toBlockchainName.isBlank())
throw new IllegalArgumentException("toBlockchainName is blank");
byte[] nameBytes = toBlockchainName.getBytes(StandardCharsets.UTF_8);
if (nameBytes.length == 0 || nameBytes.length > 255)
throw new IllegalArgumentException("toBlockchainName utf8 len must be 1..255");
if (toBlockGlobalNumber < 0)
throw new IllegalArgumentException("toBlockGlobalNumber < 0");
if (toBlockHash32 == null || toBlockHash32.length != 32)
throw new IllegalArgumentException("toBlockHash32 invalid");
return this;
}
@Override
public byte[] toBytes() {
byte[] nameBytes = toBlockchainName.getBytes(StandardCharsets.UTF_8);
if (nameBytes.length == 0 || nameBytes.length > 255)
throw new IllegalArgumentException("toBlockchainName utf8 len must be 1..255");
int cap = 4 + 4 + 1 + nameBytes.length + 4 + 32;
ByteBuffer bb = ByteBuffer.allocate(cap).order(ByteOrder.BIG_ENDIAN);
bb.putShort(TYPE);
bb.putShort(VER);
bb.putInt(reactionCode);
bb.put((byte) nameBytes.length);
bb.put(nameBytes);
bb.putInt(toBlockGlobalNumber);
bb.put(toBlockHash32);
return bb.array();
}
/** Для записи в БД (toBlockHashe TEXT) удобно хранить hex. */
public String toBlockHashHex() {
return toHex(toBlockHash32);
}
private static String toHex(byte[] bytes) {
char[] HEX = "0123456789abcdef".toCharArray();
char[] out = new char[bytes.length * 2];
for (int i = 0; i < bytes.length; i++) {
int v = bytes[i] & 0xFF;
out[i * 2] = HEX[v >>> 4];
out[i * 2 + 1] = HEX[v & 0x0F];
}
return new String(out);
}
}

View File

@ -7,6 +7,17 @@ import java.nio.charset.CodingErrorAction;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.Objects; import java.util.Objects;
/**
* TextBody type=1, ver=1.
*
* bodyBytes:
* [2] type=1
* [2] ver=1
* [N] utf8 message
*
* ЛИНИЯ:
* - строго lineIndex=1
*/
public final class TextBody implements BodyRecord { public final class TextBody implements BodyRecord {
public static final short TYPE = 1; public static final short TYPE = 1;
@ -53,6 +64,11 @@ public final class TextBody implements BodyRecord {
@Override public short type() { return TYPE; } @Override public short type() { return TYPE; }
@Override public short version() { return VER; } @Override public short version() { return VER; }
@Override
public short expectedLineIndex() {
return 1;
}
@Override @Override
public TextBody check() { public TextBody check() {
if (message == null || message.isBlank()) if (message == null || message.isBlank())

View File

@ -1,6 +1,7 @@
package server.logic.ws_protocol.JSON.handlers.blockchain; package server.logic.ws_protocol.JSON.handlers.blockchain;
import blockchain.BchBlockEntry; import blockchain.BchBlockEntry;
import blockchain.body.ReactionBody;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import shine.db.SqliteDbController; import shine.db.SqliteDbController;
@ -8,6 +9,7 @@ import shine.db.dao.BlockchainStateDAO;
import shine.db.dao.BlocksDAO; import shine.db.dao.BlocksDAO;
import shine.db.entities.BlockEntry; import shine.db.entities.BlockEntry;
import shine.db.entities.BlockchainStateEntry; import shine.db.entities.BlockchainStateEntry;
import utils.blockchain.BlockchainNameUtil;
import utils.files.FileStoreUtil; import utils.files.FileStoreUtil;
import shine.log.BlockchainAdminNotifier; import shine.log.BlockchainAdminNotifier;
@ -61,47 +63,32 @@ public final class BlockchainWriter {
String login, String login,
String blockchainName, String blockchainName,
String prevGlobalHashHex, String prevGlobalHashHex,
String prevLineHashHex,
BchBlockEntry block, BchBlockEntry block,
BlockchainStateEntry stOrNull, BlockchainStateEntry stOrNull,
String newHashHex String newHashHex
) throws SQLException { ) throws SQLException {
// =====================================================================
// ШАГ 0. КРИТИЧЕСКАЯ ПРОВЕРКА КОНСИСТЕНТНОСТИ:
// - если state есть и ожидает ненулевой размер,
// то основной файл должен существовать и иметь точно этот размер.
// - если не так это почти наверняка внешнее вмешательство/порча,
// и продолжать запись НЕЛЬЗЯ.
// =====================================================================
verifyMainFileSizeMatchesStateOrAlert(login, blockchainName, block, stOrNull); verifyMainFileSizeMatchesStateOrAlert(login, blockchainName, block, stOrNull);
// ===================================================================== // =====================================================================
// ШАГ 1. Готовим bytes нового блока (включая signature+hash) // ШАГ 1. Готовим bytes нового блока (включая signature+hash)
// ===================================================================== // =====================================================================
final byte[] newBlockFullBytes = block.toBytes(); // включает хвост signature+hash final byte[] newBlockFullBytes = block.toBytes();
// ===================================================================== // =====================================================================
// ШАГ 2. Считаем новый fileSizeBytes // ШАГ 2. Считаем новый fileSizeBytes
// - если genesis (state == null): старый размер = 0
// - иначе берём st.fileSizeBytes
// ===================================================================== // =====================================================================
final long oldFileSize = (stOrNull == null) ? 0L : stOrNull.getFileSizeBytes(); final long oldFileSize = (stOrNull == null) ? 0L : stOrNull.getFileSizeBytes();
final long newFileSize = safeAdd(oldFileSize, newBlockFullBytes.length); final long newFileSize = safeAdd(oldFileSize, newBlockFullBytes.length);
// ===================================================================== // =====================================================================
// ШАГ 3. Создаём новый tmp-файл: // ШАГ 3. Создаём новый tmp-файл: tmp = (old file bytes) + (new block bytes)
// tmp = (old file bytes) + (new block bytes)
//
// Важно:
// - читаем старый файл ТОЛЬКО если state не null и size > 0
// - если genesis: старого файла нет => tmp = newBlock
// ===================================================================== // =====================================================================
final byte[] tmpBytes; final byte[] tmpBytes;
if (stOrNull == null || oldFileSize == 0) { if (stOrNull == null || oldFileSize == 0) {
// genesis: tmp = только новый блок
tmpBytes = newBlockFullBytes; tmpBytes = newBlockFullBytes;
} else { } else {
// не genesis: tmp = старый файл + новый блок
byte[] oldBytes; byte[] oldBytes;
try { try {
oldBytes = fs.readBlockchain(blockchainName); oldBytes = fs.readBlockchain(blockchainName);
@ -111,7 +98,6 @@ public final class BlockchainWriter {
throw new SQLException("Cannot read old blockchain file for: " + blockchainName, e); throw new SQLException("Cannot read old blockchain file for: " + blockchainName, e);
} }
// (в идеале это всегда должно совпадать после verifyMainFileSizeMatchesStateOrAlert)
if (oldBytes.length != (int) oldFileSize) { if (oldBytes.length != (int) oldFileSize) {
String msg = String msg =
"Несовпадение размера файла блокчейна при чтении: " + "Несовпадение размера файла блокчейна при чтении: " +
@ -127,8 +113,6 @@ public final class BlockchainWriter {
tmpBytes = concat(oldBytes, newBlockFullBytes); tmpBytes = concat(oldBytes, newBlockFullBytes);
} }
// Пишем tmp на диск ДО транзакции БД:
// - если сервер упадёт позже tmp останется, но БД может не успеть обновиться (это ок для recovery)
try { try {
fs.writeBlockchainTmp(blockchainName, tmpBytes); fs.writeBlockchainTmp(blockchainName, tmpBytes);
} catch (Exception e) { } catch (Exception e) {
@ -138,9 +122,7 @@ public final class BlockchainWriter {
} }
// ===================================================================== // =====================================================================
// ШАГ 4. АТОМАРНО фиксируем БД: // ШАГ 4. АТОМАРНО фиксируем БД
// - UPSERT blocks
// - UPSERT blockchain_state (включая fileSizeBytes = newFileSize)
// ===================================================================== // =====================================================================
try (Connection c = db.getConnection()) { try (Connection c = db.getConnection()) {
@ -150,21 +132,18 @@ public final class BlockchainWriter {
boolean committed = false; boolean committed = false;
try { try {
// 4.1) вставляем/апдейтим запись блока insertBlockRow(c, login, blockchainName, prevGlobalHashHex, prevLineHashHex, block);
insertBlockRow(c, login, blockchainName, prevGlobalHashHex, block);
// 4.2) апдейтим состояние (включая fileSizeBytes) appendState(c, blockchainName, block, stOrNull, newHashHex, newFileSize);
appendState(c, blockchainName, block.recordNumber, stOrNull, newHashHex, newFileSize);
// 4.3) commit
c.commit(); c.commit();
committed = true; committed = true;
} catch (Exception e) { } catch (Exception e) {
try { c.rollback(); } catch (SQLException ignore) {} try { c.rollback(); } catch (SQLException ignore) {}
log.error("Ошибка транзакции БД при добавлении блока (rollback выполнен) (login={}, blockchainName={}, blockNumber={}, prevHash={}, newHash={}, oldFileSize={}, newFileSize={})", log.error("Ошибка транзакции БД при добавлении блока (rollback выполнен) (login={}, blockchainName={}, blockNumber={}, prevGlobalHash={}, prevLineHash={}, newHash={}, oldFileSize={}, newFileSize={})",
login, blockchainName, block.recordNumber, prevGlobalHashHex, newHashHex, oldFileSize, newFileSize, e); login, blockchainName, block.recordNumber, prevGlobalHashHex, prevLineHashHex, newHashHex, oldFileSize, newFileSize, e);
if (e instanceof SQLException se) throw se; if (e instanceof SQLException se) throw se;
throw new SQLException("appendBlockAndState failed (db tx)", e); throw new SQLException("appendBlockAndState failed (db tx)", e);
@ -174,8 +153,7 @@ public final class BlockchainWriter {
} }
// ================================================================= // =================================================================
// ШАГ 5. После успешного коммита БД атомарно заменяем файл: // ШАГ 5. После успешного коммита БД атомарно заменяем файл
// <name>.tmp_bch -> <name>.bch
// ================================================================= // =================================================================
if (committed) { if (committed) {
try { try {
@ -193,10 +171,6 @@ public final class BlockchainWriter {
} }
} }
/**
* Проверка: реальный размер <name>.bch должен совпадать с st.fileSizeBytes.
* Если нет это критическая внешняя порча/вмешательство, уведомляем админа и падаем.
*/
private void verifyMainFileSizeMatchesStateOrAlert( private void verifyMainFileSizeMatchesStateOrAlert(
String login, String login,
String blockchainName, String blockchainName,
@ -204,21 +178,13 @@ public final class BlockchainWriter {
BlockchainStateEntry stOrNull BlockchainStateEntry stOrNull
) throws SQLException { ) throws SQLException {
if (stOrNull == null) { if (stOrNull == null) return;
// genesis state ещё нет, проверять нечего
return;
}
long expected = stOrNull.getFileSizeBytes(); long expected = stOrNull.getFileSizeBytes();
if (expected <= 0) { if (expected <= 0) return;
// state есть, но ожидаемый размер 0 это либо пустая цепочка, либо старый формат.
// Здесь не трогаем (но можно усилить правила позже).
return;
}
String mainFileName = fs.buildBlockchainFileName(blockchainName); String mainFileName = fs.buildBlockchainFileName(blockchainName);
// Если файла нет это уже очень подозрительно: state говорит файл есть и размер > 0
if (!fs.exists(mainFileName)) { if (!fs.exists(mainFileName)) {
String msg = String msg =
"КРИТИЧЕСКАЯ ОШИБКА КОНСИСТЕНТНОСТИ: state ожидает основной файл, но его нет. " + "КРИТИЧЕСКАЯ ОШИБКА КОНСИСТЕНТНОСТИ: state ожидает основной файл, но его нет. " +
@ -262,14 +228,16 @@ public final class BlockchainWriter {
/** /**
* Обновление состояния blockchain_state (создаём если отсутствует). * Обновление состояния 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( private void appendState(
Connection c, Connection c,
String blockchainName, String blockchainName,
int globalNumber, BchBlockEntry block,
BlockchainStateEntry stOrNull, BlockchainStateEntry stOrNull,
String newHashHex, String newHashHex,
long newFileSizeBytes long newFileSizeBytes
@ -281,32 +249,38 @@ public final class BlockchainWriter {
st.setBlockchainName(blockchainName); st.setBlockchainName(blockchainName);
} }
// Последний глобальный блок // глобальная цепочка всегда растёт по recordNumber
st.setLastGlobalNumber(globalNumber); st.setLastGlobalNumber(block.recordNumber);
st.setLastGlobalHash(newHashHex); st.setLastGlobalHash(newHashHex);
// Линии пока не используются // обновляем конкретную линию блока
st.setLastLineNumber(0, globalNumber); int li = block.lineIndex;
st.setLastLineHash(0, newHashHex); st.setLastLineNumber(li, block.lineNumber);
st.setLastLineHash(li, newHashHex);
// ВАЖНО: сохраняем ожидаемый размер файла // file size
st.setFileSizeBytes(newFileSizeBytes); st.setFileSizeBytes(newFileSizeBytes);
// Метка времени обновления // timestamp
st.setUpdatedAtMs(System.currentTimeMillis()); st.setUpdatedAtMs(System.currentTimeMillis());
// UPSERT
stateDAO.upsert(c, st); stateDAO.upsert(c, st);
} }
/** /**
* Вставка/апдейт строки блока в blocks. * Вставка/апдейт строки блока в blocks.
*
* Важно:
* - blockLinePreHashe = prevLineHashHex (а НЕ prevGlobalHashHex)
* - msgType = body.type()
* - Для ReactionBody заполняем toBchName/toBlockGlobalNumber/toBlockHashe (+ to_login если можем).
*/ */
private void insertBlockRow( private void insertBlockRow(
Connection c, Connection c,
String login, String login,
String blockchainName, String blockchainName,
String prevGlobalHashHex, String prevGlobalHashHex,
String prevLineHashHex,
BchBlockEntry block BchBlockEntry block
) throws SQLException { ) throws SQLException {
@ -318,22 +292,33 @@ public final class BlockchainWriter {
e.setBlockGlobalNumber(block.recordNumber); e.setBlockGlobalNumber(block.recordNumber);
e.setBlockGlobalPreHashe(prevGlobalHashHex); e.setBlockGlobalPreHashe(prevGlobalHashHex);
// линии пока не используем e.setBlockLineIndex(block.lineIndex);
e.setBlockLineIndex(0); e.setBlockLineNumber(block.lineNumber);
e.setBlockLineNumber(block.recordNumber); e.setBlockLinePreHashe(prevLineHashHex);
e.setBlockLinePreHashe(prevGlobalHashHex);
// тип сообщения по body.type()
e.setMsgType(block.body.type()); e.setMsgType(block.body.type());
// полный блок (RAW + signature + hash)
e.setBlockByte(block.toBytes()); e.setBlockByte(block.toBytes());
// defaults
e.setToLogin(null); e.setToLogin(null);
e.setToBchName(null); e.setToBchName(null);
e.setToBlockGlobalNumber(null); e.setToBlockGlobalNumber(null);
e.setToBlockHashe(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); blocksDAO.upsert(c, e);
} }

View File

@ -27,22 +27,23 @@ import java.util.concurrent.locks.ReentrantLock;
* Задачи: * Задачи:
* 1) Лочим добавление блоков для конкретного blockchainName (защита от гонок в одном процессе). * 1) Лочим добавление блоков для конкретного blockchainName (защита от гонок в одном процессе).
* 2) Декодируем блок из Base64 и парсим его структуру. * 2) Декодируем блок из Base64 и парсим его структуру.
* 3) Парсим body и валидируем (type/version + содержимое). * 3) Валидируем body (type/version + содержимое).
* 4) Проверяем globalNumber и prevGlobalHash относительно server state. * 4) Проверяем globalNumber и prevGlobalHash относительно server state.
* 5) Проверяем подпись/хэш (Ed25519 над hash32, hash32=sha256(preimage)). * 5) Проверяем линии:
* 6) Делаем запись в БД через BlockchainDbWriter (атомарность реализуется там). * - genesis: global=0, lineIndex=0, lineNumber=0
* 7) Возвращаем клиенту serverLastGlobalNumber/serverLastGlobalHash. * - остальные: lineIndex=1..7, lineNumber по счётчику линии
* 6) Проверяем подпись/хэш (Ed25519 над hash32, hash32=sha256(preimage)).
* preimage включает prevLineHash32 (берём из state по lineIndex).
* 7) Пишем в БД+файл через BlockchainWriter (атомарность там).
*/ */
public final class Net_AddBlock_Handler implements JsonMessageHandler { public final class Net_AddBlock_Handler implements JsonMessageHandler {
private static final Logger log = LoggerFactory.getLogger(Net_AddBlock_Handler.class); private static final Logger log = LoggerFactory.getLogger(Net_AddBlock_Handler.class);
// DAO (перегрузки сами создают/закрывают Connection внутри)
private final BlocksDAO blocksDAO = BlocksDAO.getInstance(); private final BlocksDAO blocksDAO = BlocksDAO.getInstance();
private final BlockchainStateDAO stateDAO = BlockchainStateDAO.getInstance(); private final BlockchainStateDAO stateDAO = BlockchainStateDAO.getInstance();
private final SolanaUsersDAO solanaUsersDAO = SolanaUsersDAO.getInstance(); private final SolanaUsersDAO solanaUsersDAO = SolanaUsersDAO.getInstance();
// Writer отвечает за транзакции/атомарность и консистентность БД
private final BlockchainWriter dbWriter = new BlockchainWriter(blocksDAO, stateDAO); private final BlockchainWriter dbWriter = new BlockchainWriter(blocksDAO, stateDAO);
@Override @Override
@ -50,17 +51,17 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
Net_AddBlock_Request req = (Net_AddBlock_Request) baseReq; Net_AddBlock_Request req = (Net_AddBlock_Request) baseReq;
// 0) Берём имя цепочки и лочим операции добавления для неё
String blockchainName = req.getBlockchainName(); String blockchainName = req.getBlockchainName();
ReentrantLock lock = BlockchainLocks.lockFor(blockchainName); ReentrantLock lock = BlockchainLocks.lockFor(blockchainName);
lock.lock(); lock.lock();
try { try {
AddBlockResult r = addBlock(blockchainName, AddBlockResult r = addBlock(
blockchainName,
req.getGlobalNumber(), req.getGlobalNumber(),
req.getPrevGlobalHash(), req.getPrevGlobalHash(),
req.getBlockBytesB64()); req.getBlockBytesB64()
);
// 7) Формируем стандартный Net_AddBlock_Response
Net_AddBlock_Response resp = new Net_AddBlock_Response(); Net_AddBlock_Response resp = new Net_AddBlock_Response();
resp.setOp(req.getOp()); resp.setOp(req.getOp());
resp.setRequestId(req.getRequestId()); resp.setRequestId(req.getRequestId());
@ -73,7 +74,6 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
resp.setReasonCode(r.reasonCode); resp.setReasonCode(r.reasonCode);
} }
// Возвращаем актуальное состояние сервера (даже при ошибках, где уместно)
resp.setServerLastGlobalNumber(r.serverLastGlobalNumber); resp.setServerLastGlobalNumber(r.serverLastGlobalNumber);
if (r.serverLastGlobalHash != null) { if (r.serverLastGlobalHash != null) {
resp.setServerLastGlobalHash(r.serverLastGlobalHash); resp.setServerLastGlobalHash(r.serverLastGlobalHash);
@ -86,27 +86,17 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
} }
} }
/* ===================================================================== */
/* ========================== Основная логика =========================== */
/* ===================================================================== */
/**
* Внутренняя логика добавления блока (без ручного управления Connection/tx).
* Все атомарные записи внутри BlockchainDbWriter.
*/
private AddBlockResult addBlock( private AddBlockResult addBlock(
String blockchainName, String blockchainName,
int globalNumber, int globalNumber,
String prevGlobalHashHex, String prevGlobalHashHex,
String blockBytesB64 String blockBytesB64
) { ) {
// 1) Быстрая валидация входных параметров
if (blockchainName == null || blockchainName.isBlank()) { if (blockchainName == null || blockchainName.isBlank()) {
log.warn("AddBlock: пустой blockchainName (globalNumber={})", globalNumber); log.warn("AddBlock: пустой blockchainName (globalNumber={})", globalNumber);
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "empty_blockchain_name", 0, ""); return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "empty_blockchain_name", 0, "");
} }
// 2) Из имени блокчейна вытаскиваем login (как ты и хотел через util)
String login = BlockchainNameUtil.loginFromBlockchainName(blockchainName); String login = BlockchainNameUtil.loginFromBlockchainName(blockchainName);
if (login == null || login.isBlank()) { if (login == null || login.isBlank()) {
log.warn("AddBlock: плохой blockchainName='{}' => login не получился (globalNumber={})", 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, ""); return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_blockchain_name", 0, "");
} }
// 3) Декодируем блок из Base64
final byte[] blockBytes; final byte[] blockBytes;
try { try {
blockBytes = decodeBase64(blockBytesB64); 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, ""); return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_block_base64", 0, "");
} }
// 4) Парсим блок (проверяется recordSize и минимальная длина)
final BchBlockEntry block; final BchBlockEntry block;
try { try {
block = new BchBlockEntry(blockBytes); block = new BchBlockEntry(blockBytes);
} catch (Exception e) { } catch (Exception e) {
// важно: BchBlockEntry теперь сам валит блок, если body в неправильной линии
log.warn("AddBlock: не удалось распарсить BchBlockEntry (login={}, blockchainName={}, globalNumber={}, bytesLen={})", log.warn("AddBlock: не удалось распарсить BchBlockEntry (login={}, blockchainName={}, globalNumber={}, bytesLen={})",
login, blockchainName, globalNumber, blockBytes.length, e); login, blockchainName, globalNumber, blockBytes.length, e);
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_block_format", 0, ""); return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_block_format", 0, "");
} }
// 5) Валидируем body (type/version + содержимое) теперь body уже распарсен внутри BchBlockEntry // body.check()
try { try {
block.body.check(); block.body.check();
} catch (Exception e) { } 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, ""); return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_block_body", 0, "");
} }
// 6) Защита от рассинхрона: recordNumber внутри блока должен совпадать с заявленным globalNumber // recordNumber == globalNumber
if (block.recordNumber != globalNumber) { if (block.recordNumber != globalNumber) {
log.warn("AddBlock: global_number_mismatch (login={}, blockchainName={}, заявлен={}, внутриБлока={})", log.warn("AddBlock: global_number_mismatch (login={}, blockchainName={}, заявлен={}, внутриБлока={})",
login, blockchainName, globalNumber, block.recordNumber); login, blockchainName, globalNumber, block.recordNumber);
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "global_number_mismatch", 0, ""); return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "global_number_mismatch", 0, "");
} }
// 7) Получаем пользователя и его loginKey (публичный ключ 32 байта) // user + pubkey
SolanaUserEntry u; SolanaUserEntry u;
try { try {
u = solanaUsersDAO.getByLogin(login); // перегрузка: сама открывает/закрывает соединение u = solanaUsersDAO.getByLogin(login);
} catch (Exception e) { } catch (Exception e) {
// ВОТ ТУТ ТВОЯ ОШИБКА РАНЬШЕ ТЕРЯЛАСЬ: теперь будет stacktrace в логе
log.error("AddBlock: ошибка БД при чтении пользователя (login={}, blockchainName={}, globalNumber={})", log.error("AddBlock: ошибка БД при чтении пользователя (login={}, blockchainName={}, globalNumber={})",
login, blockchainName, globalNumber, e); login, blockchainName, globalNumber, e);
return new AddBlockResult(WireCodes.Status.INTERNAL_ERROR, "db_error", 0, ""); 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, ""); return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_user_login_key", 0, "");
} }
// 8) Читаем текущее состояние блокчейна с сервера // state
BlockchainStateEntry st; BlockchainStateEntry st;
try { try {
st = stateDAO.getByBlockchainName(blockchainName); // перегрузка: сама открывает/закрывает соединение st = stateDAO.getByBlockchainName(blockchainName);
} catch (Exception e) { } catch (Exception e) {
// ВОТ ТУТ ТВОЯ ОШИБКА РАНЬШЕ ТЕРЯЛАСЬ: теперь будет stacktrace в логе
log.error("AddBlock: ошибка БД при чтении blockchain_state (login={}, blockchainName={}, globalNumber={})", log.error("AddBlock: ошибка БД при чтении blockchain_state (login={}, blockchainName={}, globalNumber={})",
login, blockchainName, globalNumber, e); login, blockchainName, globalNumber, e);
return new AddBlockResult(WireCodes.Status.INTERNAL_ERROR, "db_error", 0, ""); return new AddBlockResult(WireCodes.Status.INTERNAL_ERROR, "db_error", 0, "");
} }
// 9) Определяем serverLastNum/serverLastHash (если state ещё нет ожидаем genesis с globalNumber=0)
final int serverLastNum; final int serverLastNum;
final String serverLastHash; final String serverLastHash;
if (st == null) { if (st == null) {
// нет state => обязаны принимать genesis
if (globalNumber != 0) { if (globalNumber != 0) {
log.warn("AddBlock: blockchain_state_not_found, но globalNumber != 0 (login={}, blockchainName={}, globalNumber={})", log.warn("AddBlock: blockchain_state_not_found, но globalNumber != 0 (login={}, blockchainName={}, globalNumber={})",
login, blockchainName, globalNumber); login, blockchainName, globalNumber);
@ -201,15 +189,15 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
serverLastHash = nn(st.getLastGlobalHash()); serverLastHash = nn(st.getLastGlobalHash());
} }
// 10) Проверяем, что клиент присылает следующий блок ровно (last+1) // следующий global строго
int expected = serverLastNum + 1; int expectedGlobal = serverLastNum + 1;
if (globalNumber != expected) { if (globalNumber != expectedGlobal) {
log.warn("AddBlock: bad_global_number (login={}, blockchainName={}, пришёл={}, ожидали={}, serverLastNum={}, serverLastHash={})", 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); return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_global_number", serverLastNum, serverLastHash);
} }
// 11) Проверяем prevGlobalHash: клиент должен ссылаться на текущий serverLastHash // prevGlobalHash сравниваем со state.lastGlobalHash
final byte[] prevGlobalHash32; final byte[] prevGlobalHash32;
final byte[] serverPrevGlobal32; final byte[] serverPrevGlobal32;
try { 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); 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( boolean ok = BchCryptoVerifier.verifyAll(
login, login,
prevGlobalHash32, prevGlobalHash32,
prevLineHash32, prevLineHash32,
block.getRawBytes(), // только RAW (без signature/hash) block.getRawBytes(),
block.getSignature64(), // подпись Ed25519 block.getSignature64(),
loginKey32, // public key пользователя loginKey32,
block.getHash32() // ожидаемый hash32 из самого блока block.getHash32()
); );
if (!ok) { if (!ok) {
log.warn("AddBlock: bad_signature_or_hash (login={}, blockchainName={}, globalNumber={})", log.warn("AddBlock: bad_signature_or_hash (login={}, blockchainName={}, globalNumber={}, lineIndex={}, lineNumber={})",
login, blockchainName, globalNumber); 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, serverLastHash);
} }
// 14) Новый hash блока (hex) то, что будет записано как lastGlobalHash
String newHashHex = toHex(block.getHash32()); String newHashHex = toHex(block.getHash32());
// 15) Запись блока + обновление состояния (атомарность/транзакции внутри dbWriter) // write
try { try {
dbWriter.appendBlockAndState( dbWriter.appendBlockAndState(
login, login,
blockchainName, blockchainName,
nn(prevGlobalHashHex), nn(prevGlobalHashHex),
block, // передаём целиком объект блока prevLineHashHex,
block,
st, st,
newHashHex newHashHex
); );
} catch (Exception e) { } catch (Exception e) {
// ВОТ ЭТО САМОЕ ВАЖНОЕ: если упал writer/БД/файлы теперь будет stacktrace в логах
log.error("AddBlock: внутренняя ошибка при записи блока (login={}, blockchainName={}, globalNumber={}, newHash={})", log.error("AddBlock: внутренняя ошибка при записи блока (login={}, blockchainName={}, globalNumber={}, newHash={})",
login, blockchainName, globalNumber, newHashHex, e); login, blockchainName, globalNumber, newHashHex, e);
return new AddBlockResult(WireCodes.Status.INTERNAL_ERROR, "internal_error", serverLastNum, serverLastHash); return new AddBlockResult(WireCodes.Status.INTERNAL_ERROR, "internal_error", serverLastNum, serverLastHash);
} }
// 16) Успех log.info("✅ AddBlock ok: login={}, blockchainName={}, globalNumber={}, lineIndex={}, lineNumber={}, newHash={}",
log.info("✅ AddBlock ok: login={}, blockchainName={}, globalNumber={}, newHash={}", login, blockchainName, globalNumber, li, ln, newHashHex);
login, blockchainName, globalNumber, newHashHex);
return new AddBlockResult(WireCodes.Status.OK, null, globalNumber, newHashHex); return new AddBlockResult(WireCodes.Status.OK, null, globalNumber, newHashHex);
} }
/* ===================================================================== */
/* ============================= Result ================================= */
/* ===================================================================== */
/** Результат обработки addBlock */
private static final class AddBlockResult { private static final class AddBlockResult {
final int httpStatus; // WireCodes.Status.* final int httpStatus;
final String reasonCode; // null если ok final String reasonCode;
final int serverLastGlobalNumber; final int serverLastGlobalNumber;
final String serverLastGlobalHash; 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 String nn(String s) { return s == null ? "" : s; }
private static byte[] decodeBase64(String s) { private static byte[] decodeBase64(String s) {