Дорабатываю добавление блоков!
This commit is contained in:
AidarKC 2025-12-23 15:48:23 +03:00
parent d949895fec
commit 62e4338e88
6 changed files with 333 additions and 217 deletions

View File

@ -1,106 +1,106 @@
package blockchain; //package blockchain;
import blockchain.body.BodyRecord;
import blockchain.body.HeaderBody;
import blockchain.body.TextBody;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* ============================================================================
* BodyRecordParser универсальный парсер тел (body) блоков .bch
* ============================================================================
*.
* 🧩 Назначение:
* Преобразует пару (recordType, recordTypeVersion, bodyBytes)
* в конкретный объект, реализующий интерфейс {@link BodyRecord}.
*.
* 🔹 Особенность:
* Используется объединённый 4-байтовый код:
*.
* fullCode = (recordType << 16) | (recordTypeVersion & 0xFFFF)
*.
* Это позволяет различать версии одного типа блока,
* например: TextBody v1, TextBody v2 и т.д.
*.
* 🔹 Пример:
* BodyRecord body = BodyRecordParser.parse(block.recordType, block.recordTypeVersion, block.body);
*.
* ============================================================================
*/
public final class BodyRecordParser {
private static final Logger log = LoggerFactory.getLogger(BodyRecordParser.class);
private BodyRecordParser() {}
/**
* Распарсить тело блока по типу и версии записи.
*
* @param recordType код типа записи (0 = Header, 1 = Text, ...)
* @param recordTypeVersion версия формата записи
* @param body массив байт тела записи
* @return объект, реализующий BodyRecord
*/
public static BodyRecord parse(short recordType, short recordTypeVersion, byte[] body) {
if (body == null)
throw new IllegalArgumentException("body == null");
// Объединяем тип и версию в 4-байтовый ключ
int fullCode = ((recordType & 0xFFFF) << 16) | (recordTypeVersion & 0xFFFF);
switch (fullCode) {
// ---------------------------------------------------------
// TYPE 0, VERSION 1 HeaderBody v1
// ---------------------------------------------------------
// Заголовок цепочки пользователя (первый блок).
// //
// Формат body (без общих 20 байт заголовка блока): //import blockchain.body.BodyRecord;
// [8] ASCII tag = "SHiNE001" //import blockchain.body.HeaderBody;
// [8] blockchainId (long, BE) //import blockchain.body.TextBody;
// [4] blockchainType (int, BE) //import org.slf4j.Logger;
// [4] blockchainNumber (int, BE) //import org.slf4j.LoggerFactory;
// [1] userLoginLength = N (unsigned byte)
// [N] userLogin (UTF-8)
// [2] versionUserBch (short, BE)
// [8] prevUserBchId (long, BE)
// [32] publicKey32
// //
// Назначение: ///**
// Создаёт новую пользовательскую цепочку (блок 0). // * ============================================================================
case (0x0000_0001): // * BodyRecordParser универсальный парсер тел (body) блоков .bch
return new HeaderBody(body); // * ============================================================================
// *.
// --------------------------------------------------------- // * 🧩 Назначение:
// TYPE 1, VERSION 1 TextBody v1 // * Преобразует пару (recordType, recordTypeVersion, bodyBytes)
// --------------------------------------------------------- // * в конкретный объект, реализующий интерфейс {@link BodyRecord}.
// Простое текстовое сообщение UTF-8. // *.
// * 🔹 Особенность:
// * Используется объединённый 4-байтовый код:
// *.
// * fullCode = (recordType << 16) | (recordTypeVersion & 0xFFFF)
// *.
// * Это позволяет различать версии одного типа блока,
// * например: TextBody v1, TextBody v2 и т.д.
// *.
// * 🔹 Пример:
// * BodyRecord body = BodyRecordParser.parse(block.recordType, block.recordTypeVersion, block.body);
// *.
// * ============================================================================
// */
//public final class BodyRecordParser {
// //
// Формат body (без общих 20 байт заголовка блока): // private static final Logger log = LoggerFactory.getLogger(BodyRecordParser.class);
// [N] message (UTF-8)
// //
// Назначение: // private BodyRecordParser() {}
// Текстовые и системные сообщения, описания, комментарии.
case (0x0001_0001):
return new TextBody(body);
// ---------------------------------------------------------
// РЕЗЕРВ будущие типы и версии
// ---------------------------------------------------------
// Пример: (0x0001_0002) TextBody v2 (например, JSON-структура)
// (0x0002_0001) FileBody v1
// //
// case (0x0001_0002): // /**
// return new TextBodyV2(body); // * Распарсить тело блока по типу и версии записи.
// *
// * @param recordType код типа записи (0 = Header, 1 = Text, ...)
// * @param recordTypeVersion версия формата записи
// * @param body массив байт тела записи
// * @return объект, реализующий BodyRecord
// */
// public static BodyRecord parse(short recordType, short recordTypeVersion, byte[] body) {
// if (body == null)
// throw new IllegalArgumentException("body == null");
// //
// case (0x0002_0001): // // Объединяем тип и версию в 4-байтовый ключ
// return new FileBody(body); // int fullCode = ((recordType & 0xFFFF) << 16) | (recordTypeVersion & 0xFFFF);
//
default: // switch (fullCode) {
throw new IllegalArgumentException(String.format( //
"Неизвестный тип блока: type=%d version=%d (fullCode=0x%08X)", // // ---------------------------------------------------------
recordType, recordTypeVersion, fullCode)); // // TYPE 0, VERSION 1 HeaderBody v1
} // // ---------------------------------------------------------
} // // Заголовок цепочки пользователя (первый блок).
} // //
// // Формат body (без общих 20 байт заголовка блока):
// // [8] ASCII tag = "SHiNE001"
// // [8] blockchainId (long, BE)
// // [4] blockchainType (int, BE)
// // [4] blockchainNumber (int, BE)
// // [1] userLoginLength = N (unsigned byte)
// // [N] userLogin (UTF-8)
// // [2] versionUserBch (short, BE)
// // [8] prevUserBchId (long, BE)
// // [32] publicKey32
// //
// // Назначение:
// // Создаёт новую пользовательскую цепочку (блок 0).
// case (0x0000_0001):
// return new HeaderBody(body);
//
// // ---------------------------------------------------------
// // TYPE 1, VERSION 1 TextBody v1
// // ---------------------------------------------------------
// // Простое текстовое сообщение UTF-8.
// //
// // Формат body (без общих 20 байт заголовка блока):
// // [N] message (UTF-8)
// //
// // Назначение:
// // Текстовые и системные сообщения, описания, комментарии.
// case (0x0001_0001):
// return new TextBody(body);
//
// // ---------------------------------------------------------
// // РЕЗЕРВ будущие типы и версии
// // ---------------------------------------------------------
// // Пример: (0x0001_0002) TextBody v2 (например, JSON-структура)
// // (0x0002_0001) FileBody v1
// //
// // case (0x0001_0002):
// // return new TextBodyV2(body);
// //
// // case (0x0002_0001):
// // return new FileBody(body);
//
// default:
// throw new IllegalArgumentException(String.format(
// "Неизвестный тип блока: type=%d version=%d (fullCode=0x%08X)",
// recordType, recordTypeVersion, fullCode));
// }
// }
//}

View File

@ -1,5 +1,7 @@
package blockchain_new; package blockchain_new;
import utils.crypto.Ed25519Util;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.ByteOrder; import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
@ -63,11 +65,23 @@ public final class BchCryptoVerifier_new {
} }
} }
// TODO: сюда подключается твой Ed25519 util /**
* Проверка подписи Ed25519:
* verify(hash32, signature64, publicKey32)
*/
public static boolean verifySignature(byte[] hash32, public static boolean verifySignature(byte[] hash32,
byte[] signature64, byte[] signature64,
byte[] publicKey32) { byte[] publicKey32) {
// TODO: Ed25519.verify(hash32, signature64, publicKey32) Objects.requireNonNull(hash32, "hash32 == null");
return true; Objects.requireNonNull(signature64, "signature64 == null");
Objects.requireNonNull(publicKey32, "publicKey32 == null");
if (hash32.length != 32) throw new IllegalArgumentException("hash32 != 32");
if (signature64.length != 64) throw new IllegalArgumentException("signature64 != 64");
if (publicKey32.length != 32) throw new IllegalArgumentException("publicKey32 != 32");
// Подстрой под твой Ed25519Util:
// Идея ровно такая: verify(messageHash, signature, publicKey)
return Ed25519Util.verify(hash32, signature64, publicKey32);
} }
} }

View File

@ -1,5 +1,7 @@
package shine.db.entities; package shine.db.entities;
import java.util.Base64;
/** /**
* Локальная копия пользователя из Solana. * Локальная копия пользователя из Solana.
* *
@ -18,8 +20,7 @@ public class SolanaUserEntry {
private String deviceKey; // TEXT private String deviceKey; // TEXT
private Integer bchLimit; // INTEGER nullable private Integer bchLimit; // INTEGER nullable
public SolanaUserEntry() { public SolanaUserEntry() {}
}
public SolanaUserEntry(String login, public SolanaUserEntry(String login,
String bchName, String bchName,
@ -49,4 +50,36 @@ public class SolanaUserEntry {
public Integer getBchLimit() { return bchLimit; } public Integer getBchLimit() { return bchLimit; }
public void setBchLimit(Integer bchLimit) { this.bchLimit = bchLimit; } public void setBchLimit(Integer bchLimit) { this.bchLimit = bchLimit; }
/**
* Публичный ключ логина в байтах (32 байта) или null, если ключ битый/пустой.
*
* Поддержка форматов:
* - Base64 (предпочтительно)
* - HEX (ровно 64 hex-символа, без пробелов)
*/
public byte[] getLoginKeyByte() {
if (loginKey == null) return null;
String s = loginKey.trim();
if (s.isEmpty()) return null;
// 1) пробуем Base64
try {
byte[] b = Base64.getDecoder().decode(s);
if (b != null && b.length == 32) return b;
} catch (IllegalArgumentException ignore) {}
// 2) пробуем HEX (64 символа)
if (s.length() == 64 && s.matches("^[0-9a-fA-F]+$")) {
byte[] out = new byte[32];
for (int i = 0; i < 32; i++) {
int hi = Character.digit(s.charAt(i * 2), 16);
int lo = Character.digit(s.charAt(i * 2 + 1), 16);
out[i] = (byte) ((hi << 4) | lo);
}
return out;
}
return null;
}
} }

View File

@ -2,6 +2,11 @@ package server.logic.ws_protocol.JSON.entyties.blockchain;
import server.logic.ws_protocol.JSON.entyties.Net_Response; import server.logic.ws_protocol.JSON.entyties.Net_Response;
/**
* Новый укороченный ответ:
* - reasonCode (null если ok)
* - serverLastGlobalNumber / serverLastGlobalHash
*/
public final class Net_AddBlock_Response extends Net_Response { public final class Net_AddBlock_Response extends Net_Response {
private String reasonCode; // null если ok private String reasonCode; // null если ok
@ -9,11 +14,6 @@ public final class Net_AddBlock_Response extends Net_Response {
private int serverLastGlobalNumber; private int serverLastGlobalNumber;
private String serverLastGlobalHash; private String serverLastGlobalHash;
private int serverLastLineNumber; // для линии блока
private String serverLastLineHash;
private int lineIndex; // какую линию сервер применил (из блока)
public String getReasonCode() { return reasonCode; } public String getReasonCode() { return reasonCode; }
public void setReasonCode(String reasonCode) { this.reasonCode = reasonCode; } public void setReasonCode(String reasonCode) { this.reasonCode = reasonCode; }
@ -22,13 +22,4 @@ public final class Net_AddBlock_Response extends Net_Response {
public String getServerLastGlobalHash() { return serverLastGlobalHash; } public String getServerLastGlobalHash() { return serverLastGlobalHash; }
public void setServerLastGlobalHash(String v) { this.serverLastGlobalHash = v; } public void setServerLastGlobalHash(String v) { this.serverLastGlobalHash = v; }
public int getServerLastLineNumber() { return serverLastLineNumber; }
public void setServerLastLineNumber(int v) { this.serverLastLineNumber = v; }
public String getServerLastLineHash() { return serverLastLineHash; }
public void setServerLastLineHash(String v) { this.serverLastLineHash = v; }
public int getLineIndex() { return lineIndex; }
public void setLineIndex(int lineIndex) { this.lineIndex = lineIndex; }
} }

View File

@ -1,5 +1,7 @@
package server.logic.ws_protocol.JSON.handlers.blockchain; package server.logic.ws_protocol.JSON.handlers.blockchain;
import blockchain_new.BchBlockEntry_new;
import blockchain_new.BchCryptoVerifier_new;
import server.logic.ws_protocol.WireCodes; import server.logic.ws_protocol.WireCodes;
import shine.db.SqliteDbController; import shine.db.SqliteDbController;
import shine.db.dao.BlockchainStateDAO; import shine.db.dao.BlockchainStateDAO;
@ -14,29 +16,31 @@ import java.sql.SQLException;
import java.util.Base64; import java.util.Base64;
/** /**
* BlockchainStateService_new атомарное добавление блока: * BlockchainStateService_new атомарное добавление блока (НОВЫЙ формат):
* - (опционально) проверки * - decode Base64 -> FULL block bytes
* - вставка строки блока в таблицу blocks * - parse block (recordSize must match)
* - обновление агрегатного состояния blockchain_state * - взять loginKey (publicKey32) пользователя
* - взять prevGlobalHash / prevLineHash из DB-состояния
* - собрать preimage -> sha256 -> verify signature
* - вставить blocks
* - обновить blockchain_state: lastGlobalNumber/lastGlobalHash (и позже line stuff)
* *
* Важно: * Ответ наружу: только reasonCode + serverLastGlobalNumber/serverLastGlobalHash
* - всё делается в одной транзакции
* - DAO-методы с Connection НЕ закрывают соединение
*/ */
public final class BlockchainStateService_new { public final class BlockchainStateService_new {
/** Результат атомарного addBlock */
public static final class AddBlockResult { public static final class AddBlockResult {
public final int lineIndex; // 0..7 (пока ставим 0) public final int httpStatus;
public final int httpStatus; // WireCodes.Status.*
public final String reasonCode; // null если ok public final String reasonCode; // null если ok
public final BlockchainStateEntry stateAfter; // состояние после (может быть null) public final Integer serverLastGlobalNumber; // может быть null при ошибке
public final String serverLastGlobalHash; // может быть null при ошибке
public AddBlockResult(int lineIndex, int httpStatus, String reasonCode, BlockchainStateEntry stateAfter) { public AddBlockResult(int httpStatus, String reasonCode,
this.lineIndex = lineIndex; Integer serverLastGlobalNumber, String serverLastGlobalHash) {
this.httpStatus = httpStatus; this.httpStatus = httpStatus;
this.reasonCode = reasonCode; this.reasonCode = reasonCode;
this.stateAfter = stateAfter; this.serverLastGlobalNumber = serverLastGlobalNumber;
this.serverLastGlobalHash = serverLastGlobalHash;
} }
public boolean isOk() { public boolean isOk() {
@ -62,104 +66,145 @@ public final class BlockchainStateService_new {
return instance; return instance;
} }
/**
* Атомарно добавляет блок (в рамках одной транзакции) и возвращает результат,
* чтобы хэндлер мог заполнить ответ клиенту.
*/
public AddBlockResult addBlockAtomically( public AddBlockResult addBlockAtomically(
String login, String login,
String blockchainName, String blockchainName,
int globalNumber, int globalNumber,
String prevGlobalHash, String prevGlobalHashFromClient,
String blockBytesB64 String blockBytesB64
) { ) {
byte[] fullBytes;
// Пока не парсим lineIndex из блока ставим 0, чтобы протокол работал.
// Позже сделаем реальный разбор (и это же место будет правильным для вычисления хэшей).
final int lineIndex = 0;
byte[] blockBytes;
try { try {
blockBytes = decodeBase64(blockBytesB64); fullBytes = decodeBase64(blockBytesB64);
} catch (Exception e) { } catch (Exception e) {
return new AddBlockResult( return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_block_base64", null, null);
lineIndex,
WireCodes.Status.BAD_REQUEST,
"bad_block_base64",
null
);
} }
if (login == null || login.isBlank()) { if (login == null || login.isBlank())
return new AddBlockResult(lineIndex, WireCodes.Status.BAD_REQUEST, "empty_login", null); return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "empty_login", null, null);
if (blockchainName == null || blockchainName.isBlank())
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "empty_blockchain_name", null, null);
if (fullBytes == null || fullBytes.length == 0)
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "empty_block_bytes", null, null);
// Разбор блока (проверит recordSize == fullBytes.length)
final BchBlockEntry_new block;
try {
block = new BchBlockEntry_new(fullBytes);
} catch (Exception e) {
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_block_format", null, null);
} }
if (blockchainName == null || blockchainName.isBlank()) {
return new AddBlockResult(lineIndex, WireCodes.Status.BAD_REQUEST, "empty_blockchain_name", null); // Минимальные sanity-checks запроса vs блока
} if (block.recordNumber != globalNumber) {
if (blockBytes == null || blockBytes.length == 0) { return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "global_number_mismatch", null, null);
return new AddBlockResult(lineIndex, WireCodes.Status.BAD_REQUEST, "empty_block_bytes", null);
} }
try (Connection c = db.getConnection()) { try (Connection c = db.getConnection()) {
boolean oldAutoCommit = c.getAutoCommit(); boolean oldAutoCommit = c.getAutoCommit();
c.setAutoCommit(false); c.setAutoCommit(false);
try { try {
// 1) получаем пользователя по login (если надо валидировать существование) // 1) user by login (loginKey нужен для подписи)
SolanaUserEntry u = solanaUsersDAO.getByLogin(c, login); SolanaUserEntry u = solanaUsersDAO.getByLogin(c, login);
if (u == null) { if (u == null) {
c.rollback(); c.rollback();
return new AddBlockResult( return new AddBlockResult(WireCodes.Status.NOT_FOUND, "user_not_found", null, null);
lineIndex,
WireCodes.Status.NOT_FOUND,
"user_not_found",
null
);
} }
// 2) вставляем блок в blocks byte[] loginKey32 = u.getLoginKeyByte();
insertBlockRow(c, login, blockchainName, globalNumber, prevGlobalHash, blockBytes, lineIndex); if (loginKey32 == null || loginKey32.length != 32) {
c.rollback();
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_login_key", null, null);
}
// 3) обновляем агрегатное состояние blockchain_state (по blockchainName) // 2) состояние цепочки по blockchainName
BlockchainStateEntry st = stateDAO.getByBlockchainName(c, blockchainName); BlockchainStateEntry st = stateDAO.getByBlockchainName(c, blockchainName);
if (st == null) { if (st == null) {
c.rollback(); c.rollback();
return new AddBlockResult( return new AddBlockResult(WireCodes.Status.NOT_FOUND, "blockchain_state_not_found", null, null);
lineIndex,
WireCodes.Status.NOT_FOUND,
"blockchain_state_not_found",
null
);
} }
// MVP: обновляем последний глобальный номер. // 3) проверка последовательности globalNumber (по DB, а не по клиенту)
int expected = st.getLastGlobalNumber() + 1;
if (globalNumber != expected) {
c.rollback();
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_global_sequence",
st.getLastGlobalNumber(), st.getLastGlobalHash());
}
// 4) prev hashes берём с сервера
byte[] prevGlobalHash32 = hexToBytes32(st.getLastGlobalHash());
short line = block.line;
int lineIndex = normalizeLineIndex(line);
byte[] prevLineHash32 = hexToBytes32(st.getLastLineHash(lineIndex));
// (опционально) можно сверить, что клиент прислал то же ожидание:
if (prevGlobalHashFromClient != null && !prevGlobalHashFromClient.isBlank()) {
String a = nn(prevGlobalHashFromClient).trim();
String b = nn(st.getLastGlobalHash()).trim();
if (!a.equalsIgnoreCase(b)) {
c.rollback();
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "prev_global_hash_mismatch",
st.getLastGlobalNumber(), st.getLastGlobalHash());
}
}
// 5) verify signature
byte[] rawBytes = block.getRawBytes();
byte[] preimage = BchCryptoVerifier_new.buildPreimage(
login,
prevGlobalHash32,
prevLineHash32,
rawBytes
);
byte[] computedHash32 = BchCryptoVerifier_new.sha256(preimage);
// hash, присланный в блоке
byte[] blockHash32 = block.getHash32();
if (!equals32(computedHash32, blockHash32)) {
c.rollback();
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_block_hash",
st.getLastGlobalNumber(), st.getLastGlobalHash());
}
boolean sigOk = BchCryptoVerifier_new.verifySignature(
computedHash32,
block.getSignature64(),
loginKey32
);
if (!sigOk) {
c.rollback();
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_block_signature",
st.getLastGlobalNumber(), st.getLastGlobalHash());
}
// 6) вставляем блок в blocks (пока line stuff MVP)
insertBlockRow(c, login, blockchainName, globalNumber, st.getLastGlobalHash(), fullBytes, lineIndex, block.lineNumber);
// 7) обновляем агрегатное состояние
st.setLastGlobalNumber(globalNumber); st.setLastGlobalNumber(globalNumber);
st.setLastGlobalHash(nn(prevGlobalHash)); // TODO: заменить на hash нового блока st.setLastGlobalHash(toHexLower(computedHash32));
st.setUpdatedAtMs(System.currentTimeMillis()); st.setUpdatedAtMs(System.currentTimeMillis());
// (линии пока не трогаем позже внесём логику lineNumber/lineHash) // линии (пока минимально)
st.setLastLineNumber(lineIndex, block.lineNumber);
st.setLastLineHash(lineIndex, toHexLower(computedHash32)); // пока можно тем же, позже разделим
stateDAO.upsert(c, st); stateDAO.upsert(c, st);
c.commit(); c.commit();
return new AddBlockResult(lineIndex, WireCodes.Status.OK, null, st); return new AddBlockResult(WireCodes.Status.OK, null, st.getLastGlobalNumber(), st.getLastGlobalHash());
} catch (Exception e) { } catch (Exception e) {
try { c.rollback(); } catch (SQLException ignore) {} try { c.rollback(); } catch (SQLException ignore) {}
return new AddBlockResult( return new AddBlockResult(WireCodes.Status.INTERNAL_ERROR, "internal_error", null, null);
lineIndex,
WireCodes.Status.INTERNAL_ERROR,
"internal_error",
null
);
} finally { } finally {
try { c.setAutoCommit(oldAutoCommit); } catch (SQLException ignore) {} try { c.setAutoCommit(oldAutoCommit); } catch (SQLException ignore) {}
} }
} catch (Exception e) { } catch (Exception e) {
return new AddBlockResult( return new AddBlockResult(WireCodes.Status.INTERNAL_ERROR, "db_error", null, null);
lineIndex,
WireCodes.Status.INTERNAL_ERROR,
"db_error",
null
);
} }
} }
@ -168,9 +213,10 @@ public final class BlockchainStateService_new {
String login, String login,
String blockchainName, String blockchainName,
int globalNumber, int globalNumber,
String prevGlobalHash, String prevGlobalHashServer,
byte[] blockBytes, byte[] blockBytes,
int lineIndex int lineIndex,
int lineNumber
) throws SQLException { ) throws SQLException {
BlockEntry e = new BlockEntry(); BlockEntry e = new BlockEntry();
@ -179,18 +225,16 @@ public final class BlockchainStateService_new {
e.setBchName(blockchainName); e.setBchName(blockchainName);
e.setBlockGlobalNumber(globalNumber); e.setBlockGlobalNumber(globalNumber);
e.setBlockGlobalPreHashe(nn(prevGlobalHash)); e.setBlockGlobalPreHashe(nn(prevGlobalHashServer));
// Заглушки под линии позже заменим на реальную логику из blockBytes.
e.setBlockLineIndex(lineIndex); e.setBlockLineIndex(lineIndex);
e.setBlockLineNumber(0); e.setBlockLineNumber(lineNumber);
e.setBlockLinePreHashe(""); e.setBlockLinePreHashe(nn("")); // можно потом хранить prevLineHash
e.setMsgType(0); e.setMsgType(0);
e.setBlockByte(blockBytes); e.setBlockByte(blockBytes);
// NEW: nullable ссылки (не забиваем фейковыми нулями) // nullable links
e.setToLogin(null); e.setToLogin(null);
e.setToBchName(null); e.setToBchName(null);
e.setToBlockGlobalNumber(null); e.setToBlockGlobalNumber(null);
@ -209,4 +253,40 @@ public final class BlockchainStateService_new {
if (s == null || s.isBlank()) return null; if (s == null || s.isBlank()) return null;
return Base64.getDecoder().decode(s); return Base64.getDecoder().decode(s);
} }
private static int normalizeLineIndex(short line) {
int v = line & 0xFFFF;
// пока поддержим 0..7 как линии
if (v < 0 || v > 7) return 0;
return v;
}
private static boolean equals32(byte[] a, byte[] b) {
if (a == null || b == null || a.length != 32 || b.length != 32) return false;
int x = 0;
for (int i = 0; i < 32; i++) x |= (a[i] ^ b[i]);
return x == 0;
}
private static byte[] hexToBytes32(String hex) {
if (hex == null) return new byte[32];
String s = hex.trim();
if (s.isEmpty()) return new byte[32];
if (s.length() != 64 || !s.matches("^[0-9a-fA-F]{64}$")) return new byte[32];
byte[] out = new byte[32];
for (int i = 0; i < 32; i++) {
int hi = Character.digit(s.charAt(i * 2), 16);
int lo = Character.digit(s.charAt(i * 2 + 1), 16);
out[i] = (byte) ((hi << 4) | lo);
}
return out;
}
private static String toHexLower(byte[] b32) {
if (b32 == null) return "";
StringBuilder sb = new StringBuilder(b32.length * 2);
for (byte b : b32) sb.append(String.format("%02x", b));
return sb.toString();
}
} }

View File

@ -25,7 +25,6 @@ public final class Net_AddBlock_new_Handler implements JsonMessageHandler {
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());
resp.setLineIndex(r.lineIndex);
if (r.isOk()) { if (r.isOk()) {
resp.setStatus(WireCodes.Status.OK); resp.setStatus(WireCodes.Status.OK);
@ -35,13 +34,12 @@ public final class Net_AddBlock_new_Handler implements JsonMessageHandler {
resp.setReasonCode(r.reasonCode); resp.setReasonCode(r.reasonCode);
} }
if (r.stateAfter != null) { // Даже при ошибке (например bad_global_sequence) можно вернуть что сервер считает последним
resp.setServerLastGlobalNumber(r.stateAfter.getLastGlobalNumber()); if (r.serverLastGlobalNumber != null) {
resp.setServerLastGlobalHash(r.stateAfter.getLastGlobalHash()); resp.setServerLastGlobalNumber(r.serverLastGlobalNumber);
}
int line = (r.lineIndex >= 0 && r.lineIndex <= 7) ? r.lineIndex : 0; if (r.serverLastGlobalHash != null) {
resp.setServerLastLineNumber(r.stateAfter.getLastLineNumber(line)); resp.setServerLastGlobalHash(r.serverLastGlobalHash);
resp.setServerLastLineHash(r.stateAfter.getLastLineHash(line));
} }
return resp; return resp;