23 12 25
Дорабатываю добавление блоков!
This commit is contained in:
parent
d949895fec
commit
62e4338e88
@ -1,106 +1,106 @@
|
|||||||
package blockchain;
|
//package blockchain;
|
||||||
|
//
|
||||||
import blockchain.body.BodyRecord;
|
//import blockchain.body.BodyRecord;
|
||||||
import blockchain.body.HeaderBody;
|
//import blockchain.body.HeaderBody;
|
||||||
import blockchain.body.TextBody;
|
//import blockchain.body.TextBody;
|
||||||
import org.slf4j.Logger;
|
//import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
//import org.slf4j.LoggerFactory;
|
||||||
|
//
|
||||||
/**
|
///**
|
||||||
* ============================================================================
|
// * ============================================================================
|
||||||
* BodyRecordParser — универсальный парсер тел (body) блоков .bch
|
// * BodyRecordParser — универсальный парсер тел (body) блоков .bch
|
||||||
* ============================================================================
|
// * ============================================================================
|
||||||
*.
|
// *.
|
||||||
* 🧩 Назначение:
|
// * 🧩 Назначение:
|
||||||
* Преобразует пару (recordType, recordTypeVersion, bodyBytes)
|
// * Преобразует пару (recordType, recordTypeVersion, bodyBytes)
|
||||||
* в конкретный объект, реализующий интерфейс {@link BodyRecord}.
|
// * в конкретный объект, реализующий интерфейс {@link BodyRecord}.
|
||||||
*.
|
// *.
|
||||||
* 🔹 Особенность:
|
// * 🔹 Особенность:
|
||||||
* Используется объединённый 4-байтовый код:
|
// * Используется объединённый 4-байтовый код:
|
||||||
*.
|
// *.
|
||||||
* fullCode = (recordType << 16) | (recordTypeVersion & 0xFFFF)
|
// * fullCode = (recordType << 16) | (recordTypeVersion & 0xFFFF)
|
||||||
*.
|
// *.
|
||||||
* Это позволяет различать версии одного типа блока,
|
// * Это позволяет различать версии одного типа блока,
|
||||||
* например: TextBody v1, TextBody v2 и т.д.
|
// * например: TextBody v1, TextBody v2 и т.д.
|
||||||
*.
|
// *.
|
||||||
* 🔹 Пример:
|
// * 🔹 Пример:
|
||||||
* BodyRecord body = BodyRecordParser.parse(block.recordType, block.recordTypeVersion, block.body);
|
// * BodyRecord body = BodyRecordParser.parse(block.recordType, block.recordTypeVersion, block.body);
|
||||||
*.
|
// *.
|
||||||
* ============================================================================
|
// * ============================================================================
|
||||||
*/
|
// */
|
||||||
public final class BodyRecordParser {
|
//public final class BodyRecordParser {
|
||||||
|
//
|
||||||
private static final Logger log = LoggerFactory.getLogger(BodyRecordParser.class);
|
// private static final Logger log = LoggerFactory.getLogger(BodyRecordParser.class);
|
||||||
|
//
|
||||||
private BodyRecordParser() {}
|
// private BodyRecordParser() {}
|
||||||
|
//
|
||||||
/**
|
// /**
|
||||||
* Распарсить тело блока по типу и версии записи.
|
// * Распарсить тело блока по типу и версии записи.
|
||||||
*
|
// *
|
||||||
* @param recordType код типа записи (0 = Header, 1 = Text, ...)
|
// * @param recordType код типа записи (0 = Header, 1 = Text, ...)
|
||||||
* @param recordTypeVersion версия формата записи
|
// * @param recordTypeVersion версия формата записи
|
||||||
* @param body массив байт тела записи
|
// * @param body массив байт тела записи
|
||||||
* @return объект, реализующий BodyRecord
|
// * @return объект, реализующий BodyRecord
|
||||||
*/
|
// */
|
||||||
public static BodyRecord parse(short recordType, short recordTypeVersion, byte[] body) {
|
// public static BodyRecord parse(short recordType, short recordTypeVersion, byte[] body) {
|
||||||
if (body == null)
|
// if (body == null)
|
||||||
throw new IllegalArgumentException("body == null");
|
// throw new IllegalArgumentException("body == null");
|
||||||
|
//
|
||||||
// Объединяем тип и версию в 4-байтовый ключ
|
// // Объединяем тип и версию в 4-байтовый ключ
|
||||||
int fullCode = ((recordType & 0xFFFF) << 16) | (recordTypeVersion & 0xFFFF);
|
// int fullCode = ((recordType & 0xFFFF) << 16) | (recordTypeVersion & 0xFFFF);
|
||||||
|
//
|
||||||
switch (fullCode) {
|
// switch (fullCode) {
|
||||||
|
//
|
||||||
// ---------------------------------------------------------
|
// // ---------------------------------------------------------
|
||||||
// TYPE 0, VERSION 1 — HeaderBody v1
|
// // TYPE 0, VERSION 1 — HeaderBody v1
|
||||||
// ---------------------------------------------------------
|
// // ---------------------------------------------------------
|
||||||
// Заголовок цепочки пользователя (первый блок).
|
// // Заголовок цепочки пользователя (первый блок).
|
||||||
//
|
// //
|
||||||
// Формат body (без общих 20 байт заголовка блока):
|
// // Формат body (без общих 20 байт заголовка блока):
|
||||||
// [8] ASCII tag = "SHiNE001"
|
// // [8] ASCII tag = "SHiNE001"
|
||||||
// [8] blockchainId (long, BE)
|
// // [8] blockchainId (long, BE)
|
||||||
// [4] blockchainType (int, BE)
|
// // [4] blockchainType (int, BE)
|
||||||
// [4] blockchainNumber (int, BE)
|
// // [4] blockchainNumber (int, BE)
|
||||||
// [1] userLoginLength = N (unsigned byte)
|
// // [1] userLoginLength = N (unsigned byte)
|
||||||
// [N] userLogin (UTF-8)
|
// // [N] userLogin (UTF-8)
|
||||||
// [2] versionUserBch (short, BE)
|
// // [2] versionUserBch (short, BE)
|
||||||
// [8] prevUserBchId (long, BE)
|
// // [8] prevUserBchId (long, BE)
|
||||||
// [32] publicKey32
|
// // [32] publicKey32
|
||||||
//
|
// //
|
||||||
// Назначение:
|
// // Назначение:
|
||||||
// Создаёт новую пользовательскую цепочку (блок №0).
|
// // Создаёт новую пользовательскую цепочку (блок №0).
|
||||||
case (0x0000_0001):
|
// case (0x0000_0001):
|
||||||
return new HeaderBody(body);
|
// return new HeaderBody(body);
|
||||||
|
//
|
||||||
// ---------------------------------------------------------
|
// // ---------------------------------------------------------
|
||||||
// TYPE 1, VERSION 1 — TextBody v1
|
// // TYPE 1, VERSION 1 — TextBody v1
|
||||||
// ---------------------------------------------------------
|
// // ---------------------------------------------------------
|
||||||
// Простое текстовое сообщение UTF-8.
|
// // Простое текстовое сообщение UTF-8.
|
||||||
//
|
// //
|
||||||
// Формат body (без общих 20 байт заголовка блока):
|
// // Формат body (без общих 20 байт заголовка блока):
|
||||||
// [N] message (UTF-8)
|
// // [N] message (UTF-8)
|
||||||
//
|
// //
|
||||||
// Назначение:
|
// // Назначение:
|
||||||
// Текстовые и системные сообщения, описания, комментарии.
|
// // Текстовые и системные сообщения, описания, комментарии.
|
||||||
case (0x0001_0001):
|
// case (0x0001_0001):
|
||||||
return new TextBody(body);
|
// return new TextBody(body);
|
||||||
|
//
|
||||||
// ---------------------------------------------------------
|
// // ---------------------------------------------------------
|
||||||
// РЕЗЕРВ — будущие типы и версии
|
// // РЕЗЕРВ — будущие типы и версии
|
||||||
// ---------------------------------------------------------
|
// // ---------------------------------------------------------
|
||||||
// Пример: (0x0001_0002) → TextBody v2 (например, JSON-структура)
|
// // Пример: (0x0001_0002) → TextBody v2 (например, JSON-структура)
|
||||||
// (0x0002_0001) → FileBody v1
|
// // (0x0002_0001) → FileBody v1
|
||||||
//
|
// //
|
||||||
// case (0x0001_0002):
|
// // case (0x0001_0002):
|
||||||
// return new TextBodyV2(body);
|
// // return new TextBodyV2(body);
|
||||||
//
|
// //
|
||||||
// case (0x0002_0001):
|
// // case (0x0002_0001):
|
||||||
// return new FileBody(body);
|
// // return new FileBody(body);
|
||||||
|
//
|
||||||
default:
|
// default:
|
||||||
throw new IllegalArgumentException(String.format(
|
// throw new IllegalArgumentException(String.format(
|
||||||
"Неизвестный тип блока: type=%d version=%d (fullCode=0x%08X)",
|
// "Неизвестный тип блока: type=%d version=%d (fullCode=0x%08X)",
|
||||||
recordType, recordTypeVersion, fullCode));
|
// recordType, recordTypeVersion, fullCode));
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
//}
|
||||||
|
|||||||
@ -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;
|
||||||
@ -39,9 +41,9 @@ public final class BchCryptoVerifier_new {
|
|||||||
|
|
||||||
ByteBuffer bb = ByteBuffer.allocate(
|
ByteBuffer bb = ByteBuffer.allocate(
|
||||||
DOMAIN.length +
|
DOMAIN.length +
|
||||||
1 + loginBytes.length +
|
1 + loginBytes.length +
|
||||||
32 + 32 +
|
32 + 32 +
|
||||||
rawBytes.length
|
rawBytes.length
|
||||||
).order(ByteOrder.BIG_ENDIAN);
|
).order(ByteOrder.BIG_ENDIAN);
|
||||||
|
|
||||||
bb.put(DOMAIN);
|
bb.put(DOMAIN);
|
||||||
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -2,18 +2,18 @@ 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
|
||||||
|
|
||||||
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; }
|
|
||||||
}
|
|
||||||
@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -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;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user