28 12 25
Вроде как сделал работу с линиями :) но ещё не тестил
This commit is contained in:
parent
b26e09904a
commit
c523816cdf
@ -79,6 +79,15 @@ public final class BchBlockEntry {
|
||||
// ✅ Сразу парсим BodyRecord (и если неизвестный type/version — тут же упадём)
|
||||
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];
|
||||
bb.get(this.signature64);
|
||||
|
||||
@ -118,6 +127,15 @@ public final class BchBlockEntry {
|
||||
// ✅ И при сборке — тоже сразу парсим body (чтобы объект был цельным)
|
||||
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.hash32 = Arrays.copyOf(hash32, HASH_LEN);
|
||||
|
||||
@ -140,14 +158,12 @@ public final class BchBlockEntry {
|
||||
}
|
||||
|
||||
public byte[] getRawBytes() {
|
||||
int rawLen = recordSize; // теперь это ровно RAW, без signature+hash
|
||||
int rawLen = recordSize; // ровно RAW, без signature+hash
|
||||
byte[] raw = new byte[rawLen];
|
||||
System.arraycopy(fullBytes, 0, raw, 0, rawLen);
|
||||
return raw;
|
||||
}
|
||||
|
||||
/* ===================================================================== */
|
||||
|
||||
public byte[] getSignature64() {
|
||||
return Arrays.copyOf(signature64, SIGNATURE_LEN);
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@ package blockchain.body;
|
||||
* BodyRecord_new — общий контракт для всех типов body (тела блока).
|
||||
*
|
||||
* Идея:
|
||||
* - На каждый тип body (Header, Text, File, ...) — отдельный класс.
|
||||
* - На каждый тип body (Header, Text, Reaction, ...) — отдельный класс.
|
||||
* - Десериализация из байтов делается КОНСТРУКТОРОМ:
|
||||
* new XxxBody_new(byte[] bodyBytes)
|
||||
* (конструктор обязан распарсить байты или кинуть IllegalArgumentException).
|
||||
@ -18,21 +18,28 @@ package blockchain.body;
|
||||
*
|
||||
* - type() и version() — это идентификаторы формата body.
|
||||
* Они должны быть константами для класса (например TYPE=1, VERSION=1).
|
||||
*
|
||||
* ДОПОЛНЕНИЕ (ЛИНИИ):
|
||||
* - Каждый тип body знает, в какой lineIndex он ДОЛЖЕН находиться.
|
||||
* Это проверяется в валидаторе блока (уровень B).
|
||||
*/
|
||||
public interface BodyRecord {
|
||||
|
||||
/** Код типа записи (совпадает с recordType в BchBlockEntry). */
|
||||
/** Код типа записи (совпадает с type в bodyBytes). */
|
||||
short type();
|
||||
|
||||
/** Версия формата записи (совпадает с recordTypeVersion в BchBlockEntry). */
|
||||
/** Версия формата записи (совпадает с version в bodyBytes). */
|
||||
short version();
|
||||
|
||||
/** Ожидаемый индекс линии для этого body. */
|
||||
short expectedLineIndex();
|
||||
|
||||
/** Проверить корректность содержимого и вернуть этот объект (или кинуть исключение). */
|
||||
BodyRecord check();
|
||||
|
||||
/**
|
||||
* Сериализовать тело записи в байты (ровно то, что кладётся в block.body).
|
||||
* Важно: НЕ включает общий заголовок блока (recordNumber/timestamp/type/version).
|
||||
* Важно: включает type/version.
|
||||
*/
|
||||
byte[] toBytes();
|
||||
}
|
||||
}
|
||||
@ -18,8 +18,9 @@ public final class BodyRecordParser {
|
||||
int key = ((type & 0xFFFF) << 16) | (ver & 0xFFFF);
|
||||
|
||||
return switch (key) {
|
||||
case 0x0000_0001 -> new HeaderBody(bodyBytes); // type=0, ver=1
|
||||
case 0x0001_0001 -> new TextBody(bodyBytes); // type=1, ver=1
|
||||
case 0x0000_0001 -> new HeaderBody(bodyBytes); // type=0, 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(
|
||||
"Unknown body type/version: type=%d ver=%d (key=0x%08X)",
|
||||
(type & 0xFFFF), (ver & 0xFFFF), key
|
||||
|
||||
@ -14,6 +14,9 @@ import java.util.Objects;
|
||||
* [8] tag ASCII "SHiNE001"
|
||||
* [1] loginLength=N (uint8)
|
||||
* [N] login UTF-8
|
||||
*
|
||||
* ЛИНИЯ:
|
||||
* - строго lineIndex=0 (genesis)
|
||||
*/
|
||||
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 version() { return VER; }
|
||||
|
||||
@Override
|
||||
public short expectedLineIndex() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HeaderBody check() {
|
||||
if (login == null || login.isBlank())
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -7,6 +7,17 @@ import java.nio.charset.CodingErrorAction;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
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 static final short TYPE = 1;
|
||||
@ -53,6 +64,11 @@ public final class TextBody implements BodyRecord {
|
||||
@Override public short type() { return TYPE; }
|
||||
@Override public short version() { return VER; }
|
||||
|
||||
@Override
|
||||
public short expectedLineIndex() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TextBody check() {
|
||||
if (message == null || message.isBlank())
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -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) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user