Ещё промежуточный комит верии - не работает :)
This commit is contained in:
AidarKC 2025-12-17 15:57:05 +03:00
parent 8188b91f86
commit aa2caf1f10
10 changed files with 521 additions and 528 deletions

View File

@ -37,6 +37,23 @@ public final class HashSHA256Util {
.getLong(); .getLong();
} }
/**
* loginId = last 8 bytes of sha256(login UTF-8), big-endian.
* (берём 8 байт справа и читаем как unsigned long в BE)
*/
public static long loginIdFromLogin(String login) {
if (login == null || login.isBlank())
throw new IllegalArgumentException("login is blank");
byte[] h = sha256(login.getBytes(StandardCharsets.UTF_8));
long v = 0;
for (int i = 24; i < 32; i++) {
v = (v << 8) | (h[i] & 0xFFL);
}
return v;
}
/** Инкрементальный SHA-256 (если нужно будет кормить по кускам). */ /** Инкрементальный SHA-256 (если нужно будет кормить по кускам). */
public static final class Sha256 { public static final class Sha256 {
private final SHA256Digest d = new SHA256Digest(); private final SHA256Digest d = new SHA256Digest();

View File

@ -6,6 +6,8 @@ import server.logic.ws_protocol.JSON.entyties.Auth.Net_CreateAuthSession_Request
import server.logic.ws_protocol.JSON.entyties.Auth.Net_RefreshSession_Request; import server.logic.ws_protocol.JSON.entyties.Auth.Net_RefreshSession_Request;
import server.logic.ws_protocol.JSON.entyties.Auth.Net_CloseActiveSession_Request; import server.logic.ws_protocol.JSON.entyties.Auth.Net_CloseActiveSession_Request;
import server.logic.ws_protocol.JSON.entyties.Auth.Net_ListSessions_Request; import server.logic.ws_protocol.JSON.entyties.Auth.Net_ListSessions_Request;
import server.logic.ws_protocol.JSON.entyties.blockchain.Net_AddBlock_new_Request;
import server.logic.ws_protocol.JSON.entyties.blockchain.Net_AddBlock_new_Response;
import server.logic.ws_protocol.JSON.entyties.tempToTest.Net_AddUser_Request; import server.logic.ws_protocol.JSON.entyties.tempToTest.Net_AddUser_Request;
import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler; import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
import server.logic.ws_protocol.JSON.handlers.auth.Net_AuthChallenge_Handler; import server.logic.ws_protocol.JSON.handlers.auth.Net_AuthChallenge_Handler;
@ -13,6 +15,7 @@ import server.logic.ws_protocol.JSON.handlers.auth.Net_CreateAuthSession__Handle
import server.logic.ws_protocol.JSON.handlers.auth.Net_RefreshSession_Handler; import server.logic.ws_protocol.JSON.handlers.auth.Net_RefreshSession_Handler;
import server.logic.ws_protocol.JSON.handlers.auth.Net_CloseActiveSession_Handler; import server.logic.ws_protocol.JSON.handlers.auth.Net_CloseActiveSession_Handler;
import server.logic.ws_protocol.JSON.handlers.auth.Net_ListSessions_Handler; import server.logic.ws_protocol.JSON.handlers.auth.Net_ListSessions_Handler;
import server.logic.ws_protocol.JSON.handlers.blockchain.Net_AddBlock_new_Handler;
import server.logic.ws_protocol.JSON.handlers.tempToTest.Net_AddUser_Handler; import server.logic.ws_protocol.JSON.handlers.tempToTest.Net_AddUser_Handler;
import java.util.Map; import java.util.Map;
@ -34,7 +37,8 @@ public final class JsonHandlerRegistry {
"AuthChallenge", new Net_AuthChallenge_Handler(), "AuthChallenge", new Net_AuthChallenge_Handler(),
"CreateAuthSession", new Net_CreateAuthSession__Handler(), "CreateAuthSession", new Net_CreateAuthSession__Handler(),
"CloseActiveSession", new Net_CloseActiveSession_Handler(), "CloseActiveSession", new Net_CloseActiveSession_Handler(),
"ListSessions", new Net_ListSessions_Handler() "ListSessions", new Net_ListSessions_Handler(),
"AddBlock", new Net_AddBlock_new_Handler()
// сюда потом добавишь другие операции // сюда потом добавишь другие операции
); );
@ -44,7 +48,8 @@ public final class JsonHandlerRegistry {
"AuthChallenge", Net_AuthChallenge_Request.class, "AuthChallenge", Net_AuthChallenge_Request.class,
"CreateAuthSession", Net_CreateAuthSession_Request.class, "CreateAuthSession", Net_CreateAuthSession_Request.class,
"CloseActiveSession", Net_CloseActiveSession_Request.class, "CloseActiveSession", Net_CloseActiveSession_Request.class,
"ListSessions", Net_ListSessions_Request.class "ListSessions", Net_ListSessions_Request.class,
"AddBlock", Net_AddBlock_new_Request.class
); );
private JsonHandlerRegistry() { private JsonHandlerRegistry() {

View File

@ -1,50 +0,0 @@
package server.logic.ws_protocol.JSON.entyties.Blockchain;
import server.logic.ws_protocol.JSON.entyties.Net_Request;
/**
* AddBlock_new request.
*
* payload:
* - userLogin
* - blockchainId
* - globalBlockNumber
* - prevGlobalHashHex (может быть "" для нулевого)
* - line (0..7)
* - lineBlockNumber
* - blockBase64 (FULL bytes блока)
*/
public class Net_AddBlock_new_Request extends Net_Request {
private String userLogin;
private long blockchainId;
private int globalBlockNumber;
private String prevGlobalHashHex;
private short line;
private int lineBlockNumber;
private String blockBase64;
public String getUserLogin() { return userLogin; }
public void setUserLogin(String userLogin) { this.userLogin = userLogin; }
public long getBlockchainId() { return blockchainId; }
public void setBlockchainId(long blockchainId) { this.blockchainId = blockchainId; }
public int getGlobalBlockNumber() { return globalBlockNumber; }
public void setGlobalBlockNumber(int globalBlockNumber) { this.globalBlockNumber = globalBlockNumber; }
public String getPrevGlobalHashHex() { return prevGlobalHashHex; }
public void setPrevGlobalHashHex(String prevGlobalHashHex) { this.prevGlobalHashHex = prevGlobalHashHex; }
public short getLine() { return line; }
public void setLine(short line) { this.line = line; }
public int getLineBlockNumber() { return lineBlockNumber; }
public void setLineBlockNumber(int lineBlockNumber) { this.lineBlockNumber = lineBlockNumber; }
public String getBlockBase64() { return blockBase64; }
public void setBlockBase64(String blockBase64) { this.blockBase64 = blockBase64; }
}

View File

@ -1,45 +0,0 @@
package server.logic.ws_protocol.JSON.entyties.Blockchain;
import server.logic.ws_protocol.JSON.entyties.Net_Response;
/**
* AddBlock_new response.
*
* payload:
* - accepted (true/false)
* - newGlobalNumber
* - newGlobalHashHex
* - newLineNumber
* - newLineHashHex
* - sizeBytes
*/
public class Net_AddBlock_new_Response extends Net_Response {
private boolean accepted;
private int newGlobalNumber;
private String newGlobalHashHex;
private int newLineNumber;
private String newLineHashHex;
private int sizeBytes;
public boolean isAccepted() { return accepted; }
public void setAccepted(boolean accepted) { this.accepted = accepted; }
public int getNewGlobalNumber() { return newGlobalNumber; }
public void setNewGlobalNumber(int newGlobalNumber) { this.newGlobalNumber = newGlobalNumber; }
public String getNewGlobalHashHex() { return newGlobalHashHex; }
public void setNewGlobalHashHex(String newGlobalHashHex) { this.newGlobalHashHex = newGlobalHashHex; }
public int getNewLineNumber() { return newLineNumber; }
public void setNewLineNumber(int newLineNumber) { this.newLineNumber = newLineNumber; }
public String getNewLineHashHex() { return newLineHashHex; }
public void setNewLineHashHex(String newLineHashHex) { this.newLineHashHex = newLineHashHex; }
public int getSizeBytes() { return sizeBytes; }
public void setSizeBytes(int sizeBytes) { this.sizeBytes = sizeBytes; }
}

View File

@ -0,0 +1,27 @@
package server.logic.ws_protocol.JSON.entyties.blockchain;
import server.logic.ws_protocol.JSON.entyties.Net_Request;
public final class Net_AddBlock_new_Request extends Net_Request {
private String login; // обязателен
private long blockchainId; // обязателен
private int globalNumber; // обязателен
private String prevGlobalHash; // HEX(64) или "" для нулевого
private String blockBase64; // байты FULL-блока (raw+sig+hash) в Base64
public String getLogin() { return login; }
public void setLogin(String login) { this.login = login; }
public long getBlockchainId() { return blockchainId; }
public void setBlockchainId(long blockchainId) { this.blockchainId = blockchainId; }
public int getGlobalNumber() { return globalNumber; }
public void setGlobalNumber(int globalNumber) { this.globalNumber = globalNumber; }
public String getPrevGlobalHash() { return prevGlobalHash; }
public void setPrevGlobalHash(String prevGlobalHash) { this.prevGlobalHash = prevGlobalHash; }
public String getBlockBase64() { return blockBase64; }
public void setBlockBase64(String blockBase64) { this.blockBase64 = blockBase64; }
}

View File

@ -0,0 +1,34 @@
package server.logic.ws_protocol.JSON.entyties.blockchain;
import server.logic.ws_protocol.JSON.entyties.Net_Response;
public final class Net_AddBlock_new_Response extends Net_Response {
private String reasonCode; // null если ok
private int serverLastGlobalNumber;
private String serverLastGlobalHash;
private int serverLastLineNumber; // для линии блока
private String serverLastLineHash;
private int lineIndex; // какую линию сервер применил (из блока)
public String getReasonCode() { return reasonCode; }
public void setReasonCode(String reasonCode) { this.reasonCode = reasonCode; }
public int getServerLastGlobalNumber() { return serverLastGlobalNumber; }
public void setServerLastGlobalNumber(int v) { this.serverLastGlobalNumber = v; }
public String getServerLastGlobalHash() { return serverLastGlobalHash; }
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,63 +0,0 @@
package server.logic.ws_protocol.JSON.handlers.blockchain;
import server.logic.ws_protocol.JSON.ConnectionContext;
import server.logic.ws_protocol.JSON.entyties.Net_Request;
import server.logic.ws_protocol.JSON.entyties.Net_Response;
import server.logic.ws_protocol.JSON.entyties.Blockchain.Net_AddBlock_new_Request;
import server.logic.ws_protocol.JSON.entyties.Blockchain.Net_AddBlock_new_Response;
import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
import server.logic.ws_protocol.WireCodes;
import java.util.Base64;
public class AddBlock_new_Handler implements JsonMessageHandler {
@Override
public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) throws Exception {
Net_AddBlock_new_Request req = (Net_AddBlock_new_Request) baseReq;
// 1) простая валидация запроса
if (req.getLogin() == null || req.getLogin().isBlank())
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "EMPTY_LOGIN", "Пустой login");
if (req.getBlockchainId() <= 0)
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "BAD_CHAIN_ID", "Некорректный blockchainId");
if (req.getGlobalBlockNumber() < 0)
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "BAD_NUMBER", "Некорректный globalBlockNumber");
if (req.getBlockBase64() == null || req.getBlockBase64().isBlank())
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "EMPTY_BLOCK", "Пустой blockBase64");
byte[] blockBytes;
try {
blockBytes = Base64.getDecoder().decode(req.getBlockBase64());
} catch (Exception e) {
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "BAD_BASE64", "blockBase64 не декодируется");
}
// 2) основная логика в сервис
var r = BlockchainStateService_new.getInstance().addBlock(
req.getLogin(),
req.getBlockchainId(),
req.getGlobalBlockNumber(),
req.getPrevGlobalHashHex(),
blockBytes
);
// 3) собрать ответ
Net_AddBlock_new_Response resp = new Net_AddBlock_new_Response();
resp.setOp(req.getOp());
resp.setRequestId(req.getRequestId());
resp.setStatus(r.status);
resp.setLastGlobalNumber(r.lastGlobalNumber);
resp.setLastGlobalHashHex(r.lastGlobalHashHex);
resp.setExpectedGlobalNumber(r.expectedGlobalNumber);
resp.setExpectedPrevGlobalHashHex(r.expectedPrevGlobalPrevHashHex);
return resp;
}
}

View File

@ -1,243 +1,165 @@
package server.logic.blockchain_new; package server.logic.ws_protocol.JSON.handlers.blockchain;
import blockchain_new.BchBlockEntry_new; import blockchain_new.BchBlockEntry_new;
import blockchain_new.BchCryptoVerifier_new;
import shine.db.SqliteDbController; import shine.db.SqliteDbController;
import shine.db.dao.BlockchainStateDAO; import shine.db.dao.BlockchainStateDAO;
import shine.db.entities.BlockchainStateEntry; import shine.db.entities.BlockchainStateEntry;
import utils.files.FileStoreUtil; import utils.files.FileStoreUtil;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.sql.Connection; import java.sql.Connection;
import java.sql.SQLException; import java.sql.SQLException;
import java.sql.Statement; import java.sql.Statement;
import java.util.Base64; import java.util.Base64;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
public final class BlockchainStateService_new { public final class BlockchainStateService_new {
private static final BlockchainStateService_new INSTANCE = new BlockchainStateService_new(); public static final class Result {
public final int httpStatus;
public final String reasonCode; // null если ok
public final BlockchainStateEntry stateAfter;
public final int lineIndex;
public static BlockchainStateService_new getInstance() { return INSTANCE; } public Result(int httpStatus, String reasonCode, BlockchainStateEntry stateAfter, int lineIndex) {
this.httpStatus = httpStatus;
private final SqliteDbController db = SqliteDbController.getInstance(); this.reasonCode = reasonCode;
private final BlockchainStateDAO stateDao = BlockchainStateDAO.getInstance(); this.stateAfter = stateAfter;
private final FileStoreUtil fileStore = FileStoreUtil.getInstance(); this.lineIndex = lineIndex;
/** JVM-level locks per blockchainId */
private final ConcurrentHashMap<Long, Object> locks = new ConcurrentHashMap<>();
private BlockchainStateService_new() {}
public static final class ApplyResult {
public final int newGlobalNumber;
public final String newGlobalHashHex;
public final int newLineNumber;
public final String newLineHashHex;
public final int sizeBytes;
public ApplyResult(int newGlobalNumber, String newGlobalHashHex,
int newLineNumber, String newLineHashHex,
int sizeBytes) {
this.newGlobalNumber = newGlobalNumber;
this.newGlobalHashHex = newGlobalHashHex;
this.newLineNumber = newLineNumber;
this.newLineHashHex = newLineHashHex;
this.sizeBytes = sizeBytes;
} }
public boolean isOk() { return reasonCode == null && httpStatus == 200; }
} }
public ApplyResult applyAddBlock( private static final BlockchainStateService_new INSTANCE = new BlockchainStateService_new();
String userLogin, public static BlockchainStateService_new getInstance() { return INSTANCE; }
private BlockchainStateService_new() {}
public Result addBlockAtomically(
String login,
long blockchainId, long blockchainId,
int globalBlockNumber, int globalNumber,
String prevGlobalHashHexFromClient, String prevGlobalHashHex,
short lineIndex,
int lineBlockNumber,
String blockBase64 String blockBase64
) throws Exception { ) throws SQLException {
Objects.requireNonNull(userLogin, "userLogin == null"); if (login == null || login.isBlank())
Objects.requireNonNull(blockBase64, "blockBase64 == null"); return new Result(400, "EMPTY_LOGIN", null, -1);
if (blockchainId <= 0)
if (blockchainId <= 0) throw new IllegalArgumentException("blockchainId <= 0"); return new Result(400, "BAD_BLOCKCHAIN_ID", null, -1);
if (globalBlockNumber < 0) throw new IllegalArgumentException("globalBlockNumber < 0"); if (globalNumber < 0)
if (lineIndex < 0 || lineIndex > 7) throw new IllegalArgumentException("lineIndex must be 0..7"); return new Result(400, "BAD_GLOBAL_NUMBER", null, -1);
if (lineBlockNumber < 0) throw new IllegalArgumentException("lineBlockNumber < 0"); if (blockBase64 == null || blockBase64.isBlank())
return new Result(400, "EMPTY_BLOCK", null, -1);
byte[] fullBytes; byte[] fullBytes;
try { try {
fullBytes = Base64.getDecoder().decode(blockBase64); fullBytes = Base64.getDecoder().decode(blockBase64);
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
throw new IllegalArgumentException("blockBase64 is not valid Base64", e); return new Result(400, "BAD_BASE64_BLOCK", null, -1);
} }
BchBlockEntry_new block = new BchBlockEntry_new(fullBytes); BchBlockEntry_new block;
try {
block = new BchBlockEntry_new(fullBytes);
} catch (Exception e) {
return new Result(400, "BAD_BLOCK_FORMAT", null, -1);
}
// Быстрая проверка: что клиентские в шапке запроса совпадают с тем, что внутри блока. int lineIndex = block.line; // short -> int
if (block.recordNumber != globalBlockNumber) if (lineIndex < 0 || lineIndex > 7)
throw new IllegalArgumentException("Global number mismatch: req=" + globalBlockNumber + " block=" + block.recordNumber); return new Result(400, "BAD_LINE_INDEX", null, lineIndex);
if (block.line != lineIndex)
throw new IllegalArgumentException("Line mismatch: req=" + lineIndex + " block=" + block.line);
if (block.lineNumber != lineBlockNumber)
throw new IllegalArgumentException("LineBlockNumber mismatch: req=" + lineBlockNumber + " block=" + block.lineNumber);
Object lock = locks.computeIfAbsent(blockchainId, k -> new Object()); Connection conn = SqliteDbController.getInstance().getConnection();
boolean oldAuto = conn.getAutoCommit();
conn.setAutoCommit(false);
synchronized (lock) { try (Statement st = conn.createStatement()) {
Connection conn = db.getConnection(); // важно: заранее берём write lock
boolean prevAutoCommit = conn.getAutoCommit(); st.execute("BEGIN IMMEDIATE");
try { BlockchainStateEntry state = BlockchainStateDAO.getInstance().getByBlockchainId(blockchainId);
conn.setAutoCommit(false); if (state == null) {
conn.rollback();
// SQLite writer-lock return new Result(404, "UNKNOWN_BLOCKCHAIN", null, lineIndex);
try (Statement st = conn.createStatement()) {
st.execute("BEGIN IMMEDIATE");
}
BlockchainStateEntry state = stateDao.getByBlockchainId(blockchainId);
if (state == null)
throw new IllegalStateException("BLOCKCHAIN_NOT_FOUND: id=" + blockchainId);
// 1) логин должен совпадать с тем, что хранится в state (иначе легко подделывать)
if (!userLogin.equals(state.getUserLogin()))
throw new IllegalStateException("LOGIN_MISMATCH: requestLogin=" + userLogin + " dbLogin=" + state.getUserLogin());
// 2) глобальная последовательность
int expectedGlobal = state.getLastGlobalNumber() + 1;
if (globalBlockNumber != expectedGlobal)
throw new IllegalStateException("BAD_GLOBAL_NUMBER: expected=" + expectedGlobal + " got=" + globalBlockNumber);
String prevGlobalHashHexDb = nn(state.getLastGlobalHash());
String prevGlobalHashHexClient = nn(prevGlobalHashHexFromClient);
// 3) prev global hash должен совпасть с db
if (!eqHash(prevGlobalHashHexDb, prevGlobalHashHexClient))
throw new IllegalStateException("BAD_PREV_GLOBAL_HASH");
// 4) line последовательность
int expectedLine = state.getLastLineNumber(lineIndex) + 1;
if (lineBlockNumber != expectedLine)
throw new IllegalStateException("BAD_LINE_NUMBER: expected=" + expectedLine + " got=" + lineBlockNumber);
String prevLineHashHexDb = nn(state.getLastLineHash(lineIndex));
// 5) криптография: проверка хэша и подписи
byte[] publicKey32 = decodeBase64_32(state.getPublicKeyBase64());
if (publicKey32 == null)
throw new IllegalStateException("BAD_PUBLIC_KEY_BASE64 in db");
byte[] prevGlobalHash32 = hexTo32(prevGlobalHashHexDb);
byte[] prevLineHash32 = hexTo32(prevLineHashHexDb);
byte[] rawBytes = block.getRawBytes(); // нужно добавить метод в BchBlockEntry_new
byte[] preimage = BchCryptoVerifier_new.buildPreimage(
userLogin,
prevGlobalHash32,
prevLineHash32,
rawBytes
);
byte[] expectedHash32 = BchCryptoVerifier_new.sha256(preimage);
if (!constTimeEq32(expectedHash32, block.getHash32()))
throw new IllegalStateException("HASH_MISMATCH");
// Подпись тут подключишь свой Ed25519 util (сейчас у тебя в new-верификаторе TODO)
boolean sigOk = BchCryptoVerifier_new.verifySignature(
expectedHash32,
block.getSignature64(),
publicKey32
);
if (!sigOk)
throw new IllegalStateException("SIGNATURE_MISMATCH");
// 6) лимит / размер
int newSizeBytes = state.getSizeBytes() + block.recordSize;
if (newSizeBytes > state.getSizeLimit())
throw new IllegalStateException("SIZE_LIMIT_EXCEEDED");
// 7) Сначала дописываем файл (если упадёт транзакция откатится)
fileStore.addDataToBlockchain(blockchainId, block.toBytes());
// 8) Апдейт state в памяти
state.setSizeBytes(newSizeBytes);
state.setLastGlobalNumber(globalBlockNumber);
String newGlobalHashHex = toHex(expectedHash32);
state.setLastGlobalHash(newGlobalHashHex);
state.setLastLineNumber(lineIndex, lineBlockNumber);
String newLineHashHex = newGlobalHashHex; // если глобальный hash = hash блока (обычно да)
state.setLastLineHash(lineIndex, newLineHashHex);
state.setUpdatedAtMs(System.currentTimeMillis());
// 9) UPSERT в БД
stateDao.upsert(state);
// 10) commit
conn.commit();
return new ApplyResult(
globalBlockNumber,
newGlobalHashHex,
lineBlockNumber,
newLineHashHex,
newSizeBytes
);
} catch (Exception e) {
try { conn.rollback(); } catch (SQLException ignore) {}
throw e;
} finally {
try { conn.setAutoCommit(prevAutoCommit); } catch (SQLException ignore) {}
} }
// 1) защита от подмены логина
if (!login.equals(state.getUserLogin())) {
conn.rollback();
return new Result(403, "LOGIN_MISMATCH", state, lineIndex);
}
// 2) проверяем ожидаемый global
int expectedGlobal = state.getLastGlobalNumber() + 1;
if (globalNumber != expectedGlobal) {
conn.rollback();
return new Result(409, "OUT_OF_SEQUENCE_GLOBAL", state, lineIndex);
}
// 3) проверяем prev global hash
String dbPrevGlobalHash = nn(state.getLastGlobalHash());
if (!eqHash(prevGlobalHashHex, dbPrevGlobalHash)) {
conn.rollback();
return new Result(409, "GLOBAL_HASH_MISMATCH", state, lineIndex);
}
// 4) проверяем lineNumber
int expectedLineNumber = state.getLastLineNumber(lineIndex) + 1;
if (block.lineNumber != expectedLineNumber) {
conn.rollback();
return new Result(409, "OUT_OF_SEQUENCE_LINE", state, lineIndex);
}
// 5) prevLineHash берём из БД (он хранится!)
String dbPrevLineHashHex = nn(state.getLastLineHash(lineIndex));
// 6) полноценная крипто-проверка (хэш/подпись)
// TODO: тут подключи твой реальный verifier:
// - посчитать preimage по твоим правилам (login + prevGlobalHash32 + prevLineHash32 + rawBytes)
// - сверить sha256(preimage) == block.hash32
// - проверить Ed25519 подпись
//
// Если не ок:
// conn.rollback(); return new Result(422, "CRYPTO_INVALID", state, lineIndex);
// 7) запись блока в файл (append)
FileStoreUtil.getInstance().addDataToBlockchain(blockchainId, block.toBytes());
// 8) апдейт состояния в БД
state.setLastGlobalNumber(globalNumber);
state.setLastGlobalHash(bytesToHex(block.getHash32())); // новый global hash = hash блока
state.setLastLineNumber(lineIndex, block.lineNumber);
// ВАЖНО: line hash тоже логично сделать = hash блока (если так задумано)
state.setLastLineHash(lineIndex, bytesToHex(block.getHash32()));
// size_bytes += len(fullBytes)
state.setSizeBytes(state.getSizeBytes() + fullBytes.length);
state.setUpdatedAtMs(System.currentTimeMillis());
BlockchainStateDAO.getInstance().upsert(state);
conn.commit();
return new Result(200, null, state, lineIndex);
} catch (SQLException e) {
conn.rollback();
// если хочешь красиво: SQLITE_BUSY 503 RETRY
throw e;
} finally {
conn.setAutoCommit(oldAuto);
} }
} }
// ---------------- helpers ----------------
private static String nn(String s) { return s == null ? "" : s; } private static String nn(String s) { return s == null ? "" : s; }
/** сравнение хэшей: пустой == "0"*? — упростим: пустой = пустой. */
private static boolean eqHash(String a, String b) { private static boolean eqHash(String a, String b) {
return nn(a).equalsIgnoreCase(nn(b)); String x = nn(a).trim();
String y = nn(b).trim();
return x.equalsIgnoreCase(y);
} }
private static byte[] decodeBase64_32(String b64) { private static String bytesToHex(byte[] b) {
try { if (b == null) return "";
byte[] x = Base64.getDecoder().decode(b64);
return (x != null && x.length == 32) ? x : null;
} catch (Exception e) {
return null;
}
}
private static byte[] hexTo32(String hex) {
if (hex == null || hex.isBlank()) return new byte[32];
String h = hex.trim();
if (h.length() != 64) throw new IllegalArgumentException("hex must be 64 chars (or empty)");
byte[] out = new byte[32];
for (int i = 0; i < 32; i++) {
int hi = Character.digit(h.charAt(i * 2), 16);
int lo = Character.digit(h.charAt(i * 2 + 1), 16);
if (hi < 0 || lo < 0) throw new IllegalArgumentException("bad hex");
out[i] = (byte) ((hi << 4) | lo);
}
return out;
}
private static boolean constTimeEq32(byte[] a, byte[] b) {
if (a == null || b == null || a.length != 32 || b.length != 32) return false;
int r = 0;
for (int i = 0; i < 32; i++) r |= (a[i] ^ b[i]);
return r == 0;
}
private static String toHex(byte[] b) {
StringBuilder sb = new StringBuilder(b.length * 2); StringBuilder sb = new StringBuilder(b.length * 2);
for (byte v : b) sb.append(String.format("%02x", v)); for (byte v : b) sb.append(String.format("%02x", v));
return sb.toString(); return sb.toString();

View File

@ -1,190 +1,52 @@
package server.logic.ws_protocol.JSON.handlers.blockchain; package server.logic.ws_protocol.JSON.handlers.blockchain;
import blockchain.BchBlockEntry;
import blockchain.BodyRecordParser;
import blockchain.body.BodyRecord;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import server.logic.ws_protocol.JSON.ConnectionContext; import server.logic.ws_protocol.JSON.ConnectionContext;
import server.logic.ws_protocol.JSON.entyties.Net_Request; import server.logic.ws_protocol.JSON.entyties.Net_Request;
import server.logic.ws_protocol.JSON.entyties.Net_Response; import server.logic.ws_protocol.JSON.entyties.Net_Response;
import server.logic.ws_protocol.JSON.entyties.Blockchain.Net_AddBlock_new_Request; import server.logic.ws_protocol.JSON.entyties.blockchain.Net_AddBlock_new_Request;
import server.logic.ws_protocol.JSON.entyties.Blockchain.Net_AddBlock_new_Response; import server.logic.ws_protocol.JSON.entyties.blockchain.Net_AddBlock_new_Response;
import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler; import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
import server.logic.ws_protocol.WireCodes; import server.logic.ws_protocol.WireCodes;
import shine.db.dao.BlockchainStateDAO;
import shine.db.entities.BlockchainStateEntry;
import utils.crypto.BchCryptoVerifier;
import utils.files.FileStoreUtil;
import java.util.Base64; public final class Net_AddBlock_new_Handler implements JsonMessageHandler {
public class Net_AddBlock_new_Handler implements JsonMessageHandler {
private static final Logger log = LoggerFactory.getLogger(Net_AddBlock_new_Handler.class);
@Override @Override
public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) throws Exception { public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) throws Exception {
Net_AddBlock_new_Request req = (Net_AddBlock_new_Request) baseReq; Net_AddBlock_new_Request req = (Net_AddBlock_new_Request) baseReq;
// 0) базовые проверки var r = BlockchainStateService_new.getInstance().addBlockAtomically(
if (req.getBlockchainId() <= 0) { req.getLogin(),
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "BAD_BLOCKCHAIN_ID", "blockchainId <= 0");
}
if (req.getGlobalNumber() < 0) {
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "BAD_GLOBAL_NUMBER", "globalNumber < 0");
}
if (req.getLineNumber() < 0 || req.getLineNumber() > 7) {
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "BAD_LINE_NUMBER", "lineNumber must be 0..7");
}
if (req.getLineBlockNumber() < 0) {
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "BAD_LINE_BLOCK_NUMBER", "lineBlockNumber < 0");
}
if (req.getBlockBase64() == null || req.getBlockBase64().isBlank()) {
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "EMPTY_BLOCK", "blockBase64 is empty");
}
// 1) грузим состояние из БД
BlockchainStateDAO dao = BlockchainStateDAO.getInstance();
BlockchainStateEntry state = dao.getByBlockchainId(req.getBlockchainId());
if (state == null) {
// на MVP можно: запретить добавление, пока цепочка не создана отдельно
// либо разрешить только genesis/header как ты делал раньше
return NetExceptionResponseFactory.error(req, WireCodes.Status.CHAIN_NOT_FOUND, "CHAIN_NOT_FOUND", "chain not found in DB");
}
// 2) быстрые проверки на подходит ли блок
int expectedGlobal = state.getLastGlobalNumber() + 1;
int expectedLine = state.getLastLineNumber(req.getLineNumber()) + 1;
String dbPrevGlobalHash = nn(state.getLastGlobalHash());
String dbPrevLineHash = nn(state.getLastLineHash(req.getLineNumber()));
if (req.getGlobalNumber() != expectedGlobal) {
return outOfSeq(req, state, req.getLineNumber(), "OUT_OF_SEQUENCE_GLOBAL");
}
if (!eqHash(req.getPrevGlobalHash(), dbPrevGlobalHash)) {
return outOfSeq(req, state, req.getLineNumber(), "GLOBAL_HASH_MISMATCH");
}
if (req.getLineBlockNumber() != expectedLine) {
return outOfSeq(req, state, req.getLineNumber(), "OUT_OF_SEQUENCE_LINE");
}
if (!eqHash(req.getPrevLineHash(), dbPrevLineHash)) {
return outOfSeq(req, state, req.getLineNumber(), "LINE_HASH_MISMATCH");
}
// 3) декодируем блок
byte[] fullBlockBytes;
try {
fullBlockBytes = Base64.getUrlDecoder().decode(req.getBlockBase64());
} catch (IllegalArgumentException e) {
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "BAD_BASE64", "blockBase64 decode failed");
}
// 4) парсим .bch
BchBlockEntry block;
try {
block = new BchBlockEntry(fullBlockBytes);
} catch (Exception e) {
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "BAD_BLOCK_FORMAT", "cannot parse BchBlockEntry");
}
// 5) ПОЛНАЯ валидация: подпись/хэш/тело
// ниже я оставляю общий вызов verifyAll как у тебя раньше,
// но теперь prevHash берём из БД, а publicKey из state (или из solana_users).
byte[] prevHashGlobal32 = hexToBytes32(dbPrevGlobalHash);
boolean verified = BchCryptoVerifier.verifyAll(
state.getUserLogin(),
req.getBlockchainId(), req.getBlockchainId(),
prevHashGlobal32, req.getGlobalNumber(),
block.rawBytes, req.getPrevGlobalHash(),
block.getSignature64(), req.getBlockBase64()
block.getHash32(),
Base64.getDecoder().decode(state.getPublicKeyBase64())
); );
if (!verified) { Net_AddBlock_new_Response resp = new Net_AddBlock_new_Response();
return NetExceptionResponseFactory.error(req, WireCodes.Status.UNVERIFIED, "UNVERIFIED", "signature/hash verification failed"); resp.setOp(req.getOp());
resp.setRequestId(req.getRequestId());
resp.setLineIndex(r.lineIndex);
if (r.isOk()) {
resp.setStatus(WireCodes.Status.OK);
resp.setReasonCode(null);
} else {
// 409 / 422 / 403 / 404...
// у тебя WireCodes.Status это HTTP-подобное? тогда маппим:
resp.setStatus(r.httpStatus);
resp.setReasonCode(r.reasonCode);
} }
// Проверка тела блока if (r.stateAfter != null) {
BodyRecord body = BodyRecordParser.parse(block.recordType, block.recordTypeVersion, block.body).check(); resp.setServerLastGlobalNumber(r.stateAfter.getLastGlobalNumber());
resp.setServerLastGlobalHash(r.stateAfter.getLastGlobalHash());
// 6) TODO: извлечь lineNumber/lineBlockNumber/prevLineHash из body (если они реально в теле есть) int line = (r.lineIndex >= 0 && r.lineIndex <= 7) ? r.lineIndex : 0;
// и сверить с req + DB. Сейчас оставляю как крючок. resp.setServerLastLineNumber(r.stateAfter.getLastLineNumber(line));
// BlockLineMeta meta = BlockLineMetaExtractor.extract(body); resp.setServerLastLineHash(r.stateAfter.getLastLineHash(line));
// if (meta.lineNumber != req.getLineNumber()) ... }
// if (meta.lineBlockNumber != req.getLineBlockNumber()) ...
// if (!eqHash(meta.prevLineHashHex, dbPrevLineHash)) ...
// 7) запись в файл (фактическое хранение блоков)
FileStoreUtil.getInstance().addDataToBlockchain(req.getBlockchainId(), fullBlockBytes);
// 8) TODO: обновление состояния в БД (вместо BchInfoManager)
// - state.sizeBytes += fullBlockBytes.length
// - state.lastGlobalNumber = req.globalNumber
// - state.lastGlobalHash = bytesToHex(block.getHash32())
// - state.lineX_last_number/hash обновить по lineNumber
// - state.updatedAtMs = now
// dao.upsert(state);
// 9) ответ OK
Net_AddBlock_new_Response resp = new Net_AddBlock_new_Response();
resp.setOp(req.getOp());
resp.setRequestId(req.getRequestId());
resp.setStatus(WireCodes.Status.OK);
// можно вернуть новое состояние, но на MVP вернём хотя бы серверные lastы до апдейта/после апдейта
resp.setServerLastGlobalNumber(req.getGlobalNumber());
resp.setServerLastGlobalHash(bytesToHex(block.getHash32()));
resp.setServerLastLineNumber(req.getLineBlockNumber());
resp.setServerLastLineHash(resp.getServerLastGlobalHash());
resp.setReasonCode(null);
return resp; return resp;
} }
private static Net_AddBlock_new_Response outOfSeq(Net_AddBlock_new_Request req, BlockchainStateEntry state, int line, String reason) {
Net_AddBlock_new_Response resp = new Net_AddBlock_new_Response();
resp.setOp(req.getOp());
resp.setRequestId(req.getRequestId());
resp.setStatus(WireCodes.Status.OUT_OF_SEQUENCE); // или свой статус
resp.setReasonCode(reason);
resp.setServerLastGlobalNumber(state.getLastGlobalNumber());
resp.setServerLastGlobalHash(nn(state.getLastGlobalHash()));
resp.setServerLastLineNumber(state.getLastLineNumber(line));
resp.setServerLastLineHash(nn(state.getLastLineHash(line)));
return resp;
}
private static boolean eqHash(String a, String b) {
return nn(a).equalsIgnoreCase(nn(b));
}
private static String nn(String s) { return s == null ? "" : s.trim(); }
private static byte[] hexToBytes32(String hex) {
hex = nn(hex);
if (hex.isEmpty()) return new byte[32];
int len = hex.length();
byte[] out = new byte[len / 2];
for (int i = 0; i < len; i += 2) out[i / 2] = (byte) Integer.parseInt(hex.substring(i, i + 2), 16);
if (out.length == 32) return out;
byte[] full = new byte[32];
int copy = Math.min(out.length, 32);
System.arraycopy(out, out.length - copy, full, 32 - copy, copy);
return full;
}
private static String bytesToHex(byte[] b) {
StringBuilder sb = new StringBuilder(b.length * 2);
for (byte x : b) sb.append(String.format("%02x", x));
return sb.toString();
}
} }

View File

@ -0,0 +1,284 @@
package Test;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import utils.crypto.Ed25519Util;
import blockchain.body.HeaderBody;
import blockchain.body.TextBody;
import blockchain_new.BchCryptoVerifier_new;
import blockchain_new.BchBlockEntry_new;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.WebSocket;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.CountDownLatch;
public class Test_AddBlock_new_NoAuth {
private static final String WS_URI = "ws://localhost:7070/ws";
private static final ObjectMapper JSON = new ObjectMapper();
// ======= ДАННЫЕ (взяты по аналогии с твоим тестом) =======
private static final String TEST_LOGIN = "anya24";
private static final long TEST_BCH_ID = 4222L;
private static final byte[] LOGIN_PRIV_KEY;
private static final byte[] LOGIN_PUB_KEY;
static {
LOGIN_PRIV_KEY = Ed25519Util.generatePrivateKeyFromString("test-ed25519-login-11" + TEST_LOGIN);
LOGIN_PUB_KEY = Ed25519Util.derivePublicKey(LOGIN_PRIV_KEY);
}
// Нулевой хэш (для первого блока)
private static final byte[] ZERO32 = new byte[32];
public static void main(String[] args) throws Exception {
CountDownLatch latch = new CountDownLatch(1);
HttpClient client = HttpClient.newHttpClient();
client.newWebSocketBuilder()
.buildAsync(URI.create(WS_URI), new WebSocket.Listener() {
private int step = 0;
// сервер просил в request: blockchainId + globalNumber + prevGlobalHash + bytes блока
// prevLineHash сервер может не просить но для подписи нам он нужен
private byte[] lastGlobalHash = ZERO32;
private byte[] lastLineHash = ZERO32;
@Override
public void onOpen(WebSocket ws) {
System.out.println("✅ WS connected: " + WS_URI);
ws.request(1);
// 1) Header block
byte[] headerFull = buildHeaderBlockFullBytes(
/*global*/0,
/*lineIndex*/(short)0,
/*lineBlock*/0,
lastGlobalHash,
lastLineHash
);
String json = buildAddBlockJson("test-add-header", TEST_BCH_ID, 0, bytesToHex(lastGlobalHash), base64(headerFull));
System.out.println("\n📤 SEND #1 (HEADER):\n" + json);
ws.sendText(json, true);
}
@Override
public CompletionStage<?> onText(WebSocket ws, CharSequence data, boolean last) {
String msg = data.toString();
System.out.println("\n📥 RECV:\n" + msg);
System.out.println("-----------------------------------------------------");
try {
int status = extractStatus(msg);
if (step == 0) {
if (status != 200) {
System.out.println("❌ HEADER rejected, status=" + status);
ws.sendClose(WebSocket.NORMAL_CLOSURE, "fail");
return CompletableFuture.completedFuture(null);
}
// Обновляем prev-хэши для следующего блока: берём хэш из нашего же блока (как ожидаемую цепочку)
byte[] headerFull = lastSentBlockFullFromResponseOrLocalFallback(true);
// Fallback: просто пересоберём ровно так же (надёжнее: хранить отправленные байты)
headerFull = buildHeaderBlockFullBytes(0, (short)0, 0, ZERO32, ZERO32);
BchBlockEntry_new hb = new BchBlockEntry_new(headerFull);
lastGlobalHash = hb.getHash32();
lastLineHash = hb.getHash32();
// 2) Text block
byte[] textFull = buildTextBlockFullBytes(
/*global*/1,
/*lineIndex*/(short)0,
/*lineBlock*/1,
lastGlobalHash,
lastLineHash,
"Hello from test client"
);
String json2 = buildAddBlockJson("test-add-text", TEST_BCH_ID, 1, bytesToHex(lastGlobalHash), base64(textFull));
System.out.println("\n📤 SEND #2 (TEXT):\n" + json2);
step = 1;
ws.sendText(json2, true);
} else if (step == 1) {
System.out.println("✅ Done. Closing.");
ws.sendClose(WebSocket.NORMAL_CLOSURE, "ok");
}
} catch (Exception e) {
e.printStackTrace(System.out);
ws.sendClose(WebSocket.NORMAL_CLOSURE, "exception");
}
ws.request(1);
return CompletableFuture.completedFuture(null);
}
@Override
public void onError(WebSocket ws, Throwable error) {
System.out.println("❌ WS error: " + error.getMessage());
error.printStackTrace(System.out);
latch.countDown();
}
@Override
public CompletionStage<?> onClose(WebSocket ws, int statusCode, String reason) {
System.out.println("🔚 WS closed. code=" + statusCode + " reason=" + reason);
latch.countDown();
return CompletableFuture.completedFuture(null);
}
}).join();
latch.await();
}
// =================================================================================
// BUILD BLOCKS
// =================================================================================
private static byte[] buildHeaderBlockFullBytes(int globalNumber,
short lineIndex,
int lineBlockNumber,
byte[] prevGlobalHash32,
byte[] prevLineHash32) {
// bodyBytes (включая type+version внутри)
HeaderBody body = new HeaderBody(
TEST_BCH_ID,
TEST_LOGIN,
0, 0,
(short) 1,
0L,
LOGIN_PUB_KEY
);
byte[] bodyBytes = body.toBytes();
return buildSignedBlockFullBytes(globalNumber, lineIndex, lineBlockNumber, bodyBytes, prevGlobalHash32, prevLineHash32);
}
private static byte[] buildTextBlockFullBytes(int globalNumber,
short lineIndex,
int lineBlockNumber,
byte[] prevGlobalHash32,
byte[] prevLineHash32,
String text) {
TextBody body = new TextBody(text);
byte[] bodyBytes = body.toBytes();
return buildSignedBlockFullBytes(globalNumber, lineIndex, lineBlockNumber, bodyBytes, prevGlobalHash32, prevLineHash32);
}
private static byte[] buildSignedBlockFullBytes(int globalNumber,
short lineIndex,
int lineBlockNumber,
byte[] bodyBytes,
byte[] prevGlobalHash32,
byte[] prevLineHash32) {
long ts = System.currentTimeMillis() / 1000L;
// Собираем rawBytes вручную в точности как BchBlockEntry_new RAW:
// [4]recordSize [4]recordNumber [8]ts [2]lineIndex [4]lineBlockNumber [body...]
int recordSize =
BchBlockEntry_new.RAW_HEADER_SIZE +
bodyBytes.length +
BchBlockEntry_new.SIGNATURE_LEN +
BchBlockEntry_new.HASH_LEN;
byte[] rawBytes = ByteBuffer.allocate(BchBlockEntry_new.RAW_HEADER_SIZE + bodyBytes.length)
.order(ByteOrder.BIG_ENDIAN)
.putInt(recordSize)
.putInt(globalNumber)
.putLong(ts)
.putShort(lineIndex)
.putInt(lineBlockNumber)
.put(bodyBytes)
.array();
byte[] preimage = BchCryptoVerifier_new.buildPreimage(
TEST_LOGIN,
prevGlobalHash32,
prevLineHash32,
rawBytes
);
byte[] hash32 = BchCryptoVerifier_new.sha256(preimage);
// ВАЖНО: если у тебя в протоколе подпись делается НЕ по hash32, а по preimage замени тут на preimage
byte[] signature64 = Ed25519Util.sign(hash32, LOGIN_PRIV_KEY);
// FULL block
return new BchBlockEntry_new(
globalNumber,
ts,
lineIndex,
lineBlockNumber,
bodyBytes,
signature64,
hash32
).toBytes();
}
// =================================================================================
// JSON BUILD
// =================================================================================
private static String buildAddBlockJson(String requestId,
long blockchainId,
int globalNumber,
String prevGlobalHashHex,
String blockBytesB64) {
// Если у тебя в Net_AddBlock_new_Request другие имена полей скажешь, подправлю.
return """
{
"op": "AddBlock",
"requestId": "%s",
"payload": {
"login": "%s",
"blockchainId": %d,
"globalNumber": %d,
"prevGlobalHash": "%s",
"blockBytesB64": "%s"
}
}
""".formatted(requestId, TEST_LOGIN, blockchainId, globalNumber, prevGlobalHashHex, blockBytesB64);
}
// =================================================================================
// HELPERS
// =================================================================================
private static int extractStatus(String json) {
try {
JsonNode root = JSON.readTree(json);
if (root.has("status")) return root.get("status").asInt();
} catch (Exception ignore) {}
return -1;
}
private static String base64(byte[] bytes) {
return Base64.getEncoder().encodeToString(bytes);
}
private static String bytesToHex(byte[] b) {
StringBuilder sb = new StringBuilder(b.length * 2);
for (byte x : b) sb.append(String.format("%02x", x));
return sb.toString();
}
// Заглушка: в этом тесте проще хранить отправленные байты локально.
private static byte[] lastSentBlockFullFromResponseOrLocalFallback(boolean header) {
return null;
}
}