17 12 25
Ещё промежуточный комит верии - не работает :)
This commit is contained in:
parent
8188b91f86
commit
aa2caf1f10
@ -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();
|
||||||
|
|||||||
@ -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() {
|
||||||
|
|||||||
@ -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; }
|
|
||||||
}
|
|
||||||
@ -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; }
|
|
||||||
}
|
|
||||||
@ -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; }
|
||||||
|
}
|
||||||
@ -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; }
|
||||||
|
}
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,245 +1,167 @@
|
|||||||
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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
284
src/main/java/Test/Test_AddBlock_new_NoAuth.java
Normal file
284
src/main/java/Test/Test_AddBlock_new_NoAuth.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user