07 01 25
refactor: перевели хэши на BLOB и добавили поля block_hash / block_signature / edited_by_block_global_number и главное добавили тип блока изменение сообщение и сслку на последнее изменение в табл блокс
This commit is contained in:
parent
8bcaa192c5
commit
06c77b1c1f
@ -27,6 +27,6 @@ public interface BodyHasTarget {
|
||||
/** globalNumber цели (nullable). */
|
||||
Integer toBlockGlobalNumber();
|
||||
|
||||
/** hash цели в HEX(64) (nullable). */
|
||||
String toBlockHashe();
|
||||
/** hash целевого блока (обычно 32 байта). Может быть null, если ссылки нет. */
|
||||
byte[] toBlockHasheBytes();
|
||||
}
|
||||
@ -49,9 +49,6 @@ import java.util.Objects;
|
||||
*
|
||||
* ВАЖНО: поля toBlockchainName/toBlockGlobalNumber/toBlockHash32 — это
|
||||
* "последний известный блок" того человека (снимок/якорь состояния).
|
||||
* По сути можно было бы обойтись без них, но они полезны:
|
||||
* - фиксируют, какой блок и какой хэш ты считаешь последним известным у друга/контакта;
|
||||
* - помогают синхронизации/проверкам (например, если потом сравнивать, насколько данные устарели).
|
||||
*
|
||||
* ЛИНИЯ:
|
||||
* - строго lineIndex=3 (выделяем отдельную линию под связи).
|
||||
@ -348,5 +345,5 @@ public final class ConnectionBody implements BodyRecord, BodyHasTarget {
|
||||
|
||||
@Override public Integer toBlockGlobalNumber() { return toBlockGlobalNumber; }
|
||||
|
||||
@Override public String toBlockHashe() { return toBlockHashHex(); }
|
||||
@Override public byte[] toBlockHasheBytes() { return toBlockHash32; }
|
||||
}
|
||||
@ -15,9 +15,8 @@ import java.util.Objects;
|
||||
* [2] type=2
|
||||
* [2] ver=1
|
||||
*
|
||||
* [2] subType (uint16) — подтип реакции (раньше это был reactionCode int32)
|
||||
* [2] subType (uint16) — подтип реакции
|
||||
* 1 = LIKE (лайк)
|
||||
* (в будущем: 2=DISLIKE, 3=LAUGH, 4=WOW ... если захочешь)
|
||||
*
|
||||
* [1] toBlockchainNameLen (uint8)
|
||||
* [N] toBlockchainName UTF-8
|
||||
@ -26,10 +25,6 @@ import java.util.Objects;
|
||||
*
|
||||
* ЛИНИЯ:
|
||||
* - строго lineIndex=2
|
||||
*
|
||||
* ВАЖНО (MVP):
|
||||
* - Здесь мы НЕ проверяем, существует ли цель реакции.
|
||||
* - Мы проверяем только корректность формата и целостность полей.
|
||||
*/
|
||||
public final class ReactionBody implements BodyRecord, BodyHasTarget {
|
||||
|
||||
@ -191,7 +186,7 @@ public final class ReactionBody implements BodyRecord, BodyHasTarget {
|
||||
}
|
||||
|
||||
/* ===================================================================== */
|
||||
/* ====================== BodyToFields контракт ========================= */
|
||||
/* ====================== BodyHasTarget контракт ========================= */
|
||||
/* ===================================================================== */
|
||||
|
||||
/** В самом формате ReactionBody login цели не хранится => null. */
|
||||
@ -201,5 +196,5 @@ public final class ReactionBody implements BodyRecord, BodyHasTarget {
|
||||
|
||||
@Override public Integer toBlockGlobalNumber() { return toBlockGlobalNumber; }
|
||||
|
||||
@Override public String toBlockHashe() { return toBlockHashHex(); }
|
||||
@Override public byte[] toBlockHasheBytes() { return toBlockHash32; }
|
||||
}
|
||||
@ -33,13 +33,6 @@ import java.util.Objects;
|
||||
*
|
||||
* ЛИНИЯ:
|
||||
* - строго lineIndex=1
|
||||
*
|
||||
* Правила строгого парсинга (чтобы формат не “плыл”):
|
||||
* - subType обязан быть 1/2/3
|
||||
* - textLen обязан быть >0 и <=65535
|
||||
* - text обязан быть валидным UTF-8 и не blank
|
||||
* - для subType=NEW запрещены поля ссылки и запрещены любые “лишние байты” в хвосте
|
||||
* - для subType=REPLY/REPOST хвост обязан быть ровно по формату и без мусора в конце
|
||||
*/
|
||||
public final class TextBody implements BodyRecord, BodyHasTarget {
|
||||
|
||||
@ -160,10 +153,6 @@ public final class TextBody implements BodyRecord, BodyHasTarget {
|
||||
/* ====================== Конструкторы “для тестов” ====================== */
|
||||
/* ===================================================================== */
|
||||
|
||||
/**
|
||||
* Удобный конструктор для тестов/сборки простого сообщения:
|
||||
* new TextBody(text) == new TextBody(SUB_NEW, text)
|
||||
*/
|
||||
public TextBody(String message) {
|
||||
this(SUB_NEW, message);
|
||||
}
|
||||
@ -244,7 +233,6 @@ public final class TextBody implements BodyRecord, BodyHasTarget {
|
||||
if (toBlockHash32 == null || toBlockHash32.length != 32)
|
||||
throw new IllegalArgumentException("toBlockHash32 invalid");
|
||||
} else {
|
||||
// SUB_NEW
|
||||
if (toBlockchainName != null) throw new IllegalArgumentException("toBlockchainName must be null for SUB_NEW");
|
||||
if (toBlockHash32 != null) throw new IllegalArgumentException("toBlockHash32 must be null for SUB_NEW");
|
||||
}
|
||||
@ -279,7 +267,6 @@ public final class TextBody implements BodyRecord, BodyHasTarget {
|
||||
cap += 1 + nameBytes.length + 4 + 32;
|
||||
|
||||
} else {
|
||||
// SUB_NEW — ссылка запрещена
|
||||
if (toBlockchainName != null || toBlockHash32 != null) {
|
||||
throw new IllegalArgumentException("SUB_NEW must not contain reply/repost fields");
|
||||
}
|
||||
@ -363,7 +350,7 @@ public final class TextBody implements BodyRecord, BodyHasTarget {
|
||||
}
|
||||
|
||||
/* ===================================================================== */
|
||||
/* ====================== BodyToFields контракт ========================= */
|
||||
/* ====================== BodyHasTarget контракт ========================= */
|
||||
/* ===================================================================== */
|
||||
|
||||
/** В формате TextBody login цели не хранится => null. */
|
||||
@ -380,7 +367,7 @@ public final class TextBody implements BodyRecord, BodyHasTarget {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toBlockHashe() {
|
||||
return (subType == SUB_REPLY || subType == SUB_REPOST) ? toBlockHashHex() : null;
|
||||
public byte[] toBlockHasheBytes() {
|
||||
return (subType == SUB_REPLY || subType == SUB_REPOST) ? toBlockHash32 : null;
|
||||
}
|
||||
}
|
||||
@ -151,7 +151,7 @@ public class DatabaseInitializer {
|
||||
ON ip_geo_cache (updated_at_ms);
|
||||
""");
|
||||
|
||||
// 5. blockchain_state
|
||||
// 5. blockchain_state (хэши -> BLOB NULLABLE)
|
||||
st.executeUpdate("""
|
||||
CREATE TABLE IF NOT EXISTS blockchain_state (
|
||||
blockchain_name TEXT NOT NULL PRIMARY KEY,
|
||||
@ -162,30 +162,29 @@ public class DatabaseInitializer {
|
||||
file_size_bytes INTEGER NOT NULL,
|
||||
|
||||
last_global_number INTEGER NOT NULL,
|
||||
last_global_hash TEXT NOT NULL,
|
||||
last_global_hash BLOB,
|
||||
updated_at_ms INTEGER NOT NULL,
|
||||
|
||||
line0_last_number INTEGER NOT NULL,
|
||||
line0_last_hash TEXT NOT NULL,
|
||||
line0_last_hash BLOB,
|
||||
line1_last_number INTEGER NOT NULL,
|
||||
line1_last_hash TEXT NOT NULL,
|
||||
line1_last_hash BLOB,
|
||||
line2_last_number INTEGER NOT NULL,
|
||||
line2_last_hash TEXT NOT NULL,
|
||||
line2_last_hash BLOB,
|
||||
line3_last_number INTEGER NOT NULL,
|
||||
line3_last_hash TEXT NOT NULL,
|
||||
line3_last_hash BLOB,
|
||||
line4_last_number INTEGER NOT NULL,
|
||||
line4_last_hash TEXT NOT NULL,
|
||||
line4_last_hash BLOB,
|
||||
line5_last_number INTEGER NOT NULL,
|
||||
line5_last_hash TEXT NOT NULL,
|
||||
line5_last_hash BLOB,
|
||||
line6_last_number INTEGER NOT NULL,
|
||||
line6_last_hash TEXT NOT NULL,
|
||||
line6_last_hash BLOB,
|
||||
line7_last_number INTEGER NOT NULL,
|
||||
line7_last_hash TEXT NOT NULL,
|
||||
line7_last_hash BLOB,
|
||||
|
||||
FOREIGN KEY (login) REFERENCES solana_users(login)
|
||||
);
|
||||
""");
|
||||
|
||||
st.executeUpdate("""
|
||||
CREATE INDEX IF NOT EXISTS idx_blockchain_state_login
|
||||
ON blockchain_state (login);
|
||||
@ -196,27 +195,35 @@ public class DatabaseInitializer {
|
||||
ON blockchain_state (updated_at_ms);
|
||||
""");
|
||||
|
||||
// 6. blocks
|
||||
// 6. blocks (хэши/подпись -> BLOB, + edited_by_block_global_number)
|
||||
st.executeUpdate("""
|
||||
CREATE TABLE IF NOT EXISTS blocks (
|
||||
login TEXT NOT NULL,
|
||||
bch_name TEXT NOT NULL,
|
||||
block_global_number INTEGER NOT NULL,
|
||||
block_global_pre_hashe TEXT NOT NULL,
|
||||
block_global_pre_hashe BLOB NOT NULL,
|
||||
|
||||
block_line_index INTEGER NOT NULL,
|
||||
block_line_number INTEGER NOT NULL,
|
||||
block_line_pre_hashe TEXT NOT NULL,
|
||||
block_line_pre_hashe BLOB NOT NULL,
|
||||
|
||||
msg_type INTEGER NOT NULL,
|
||||
msg_sub_type INTEGER NOT NULL,
|
||||
|
||||
block_byte BLOB,
|
||||
|
||||
-- Ссылка на целевой блок (для reply/like/edit и т.д.)
|
||||
to_login TEXT,
|
||||
to_bch_name TEXT,
|
||||
to_block_global_number INTEGER,
|
||||
to_block_hashe TEXT,
|
||||
to_block_hashe BLOB,
|
||||
|
||||
-- Собственные данные блока (по просьбе)
|
||||
block_hash BLOB NOT NULL,
|
||||
block_signature BLOB NOT NULL,
|
||||
|
||||
-- Последний edit, который изменил этот блок (NULL если не редактировали)
|
||||
edited_by_block_global_number INTEGER,
|
||||
|
||||
FOREIGN KEY (login) REFERENCES solana_users(login),
|
||||
FOREIGN KEY (bch_name) REFERENCES blockchain_state(blockchain_name)
|
||||
@ -233,7 +240,7 @@ public class DatabaseInitializer {
|
||||
ON blocks (to_login, to_bch_name, to_block_global_number);
|
||||
""");
|
||||
|
||||
// 7) connections_state
|
||||
// 7) connections_state (to_block_hashe -> BLOB)
|
||||
st.executeUpdate("""
|
||||
CREATE TABLE IF NOT EXISTS connections_state (
|
||||
login TEXT NOT NULL,
|
||||
@ -241,7 +248,7 @@ public class DatabaseInitializer {
|
||||
to_login TEXT NOT NULL,
|
||||
to_bch_name TEXT NOT NULL,
|
||||
to_block_global_number INTEGER,
|
||||
to_block_hashe TEXT,
|
||||
to_block_hashe BLOB,
|
||||
|
||||
FOREIGN KEY (login) REFERENCES solana_users(login),
|
||||
|
||||
@ -264,7 +271,7 @@ public class DatabaseInitializer {
|
||||
ON connections_state (login, to_login);
|
||||
""");
|
||||
|
||||
// 8) Trigger: connection state
|
||||
// 8) Trigger: connection state (логика та же)
|
||||
st.executeUpdate("""
|
||||
CREATE TRIGGER IF NOT EXISTS trg_blocks_connection_state_ai
|
||||
AFTER INSERT ON blocks
|
||||
@ -304,13 +311,13 @@ public class DatabaseInitializer {
|
||||
END;
|
||||
""");
|
||||
|
||||
// 9) message_stats
|
||||
// 9) message_stats (to_block_hash -> BLOB)
|
||||
st.executeUpdate("""
|
||||
CREATE TABLE IF NOT EXISTS message_stats (
|
||||
to_login TEXT NOT NULL,
|
||||
to_bch_name TEXT NOT NULL,
|
||||
to_block_global_number INTEGER NOT NULL,
|
||||
to_block_hash TEXT NOT NULL,
|
||||
to_block_hash BLOB NOT NULL,
|
||||
|
||||
likes_count INTEGER NOT NULL DEFAULT 0,
|
||||
replies_count INTEGER NOT NULL DEFAULT 0,
|
||||
@ -334,7 +341,7 @@ public class DatabaseInitializer {
|
||||
ON message_stats (to_login);
|
||||
""");
|
||||
|
||||
// 10) Trigger: LIKE
|
||||
// 10) Trigger: LIKE (to_block_hashe -> to_block_hash BLOB)
|
||||
st.executeUpdate("""
|
||||
CREATE TRIGGER IF NOT EXISTS trg_blocks_message_stats_like_ai
|
||||
AFTER INSERT ON blocks
|
||||
@ -365,7 +372,7 @@ public class DatabaseInitializer {
|
||||
END;
|
||||
""");
|
||||
|
||||
// 11) Trigger: REPLY
|
||||
// 11) Trigger: REPLY (to_block_hashe -> to_block_hash BLOB)
|
||||
st.executeUpdate("""
|
||||
CREATE TRIGGER IF NOT EXISTS trg_blocks_message_stats_reply_ai
|
||||
AFTER INSERT ON blocks
|
||||
@ -395,6 +402,20 @@ public class DatabaseInitializer {
|
||||
replies_count = message_stats.replies_count + 1;
|
||||
END;
|
||||
""");
|
||||
|
||||
// 12) Trigger: EDIT — пометить исходный блок
|
||||
st.executeUpdate("""
|
||||
CREATE TRIGGER IF NOT EXISTS trg_blocks_edit_apply_ai
|
||||
AFTER INSERT ON blocks
|
||||
WHEN NEW.msg_type = 1 AND NEW.msg_sub_type = 10
|
||||
BEGIN
|
||||
UPDATE blocks
|
||||
SET edited_by_block_global_number = NEW.block_global_number
|
||||
WHERE login = NEW.login
|
||||
AND bch_name = NEW.bch_name
|
||||
AND block_global_number = NEW.to_block_global_number;
|
||||
END;
|
||||
""");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,3 +1,6 @@
|
||||
// =======================
|
||||
// BlockchainStateDAO.java (НОВАЯ ВЕРСИЯ)
|
||||
// =======================
|
||||
package shine.db.dao;
|
||||
|
||||
import shine.db.SqliteDbController;
|
||||
@ -71,11 +74,8 @@ public final class BlockchainStateDAO {
|
||||
/** UPSERT с внешним соединением. Соединение НЕ закрывает. */
|
||||
public void upsert(Connection c, BlockchainStateEntry e) throws SQLException {
|
||||
|
||||
// ВАЖНО:
|
||||
// Колонок должно быть ровно 24:
|
||||
// 8 основных + (8 линий * 2 поля) = 8 + 16 = 24
|
||||
//
|
||||
// size_bytes УДАЛЁН ИЗ ПРОЕКТА, здесь его быть не должно.
|
||||
// Колонок ровно 24:
|
||||
// 8 основных + (8 линий * 2 поля) = 24
|
||||
|
||||
String sql = """
|
||||
INSERT INTO blockchain_state (
|
||||
@ -144,12 +144,12 @@ public final class BlockchainStateDAO {
|
||||
ps.setLong(i++, e.getFileSizeBytes());
|
||||
|
||||
ps.setInt(i++, e.getLastGlobalNumber());
|
||||
ps.setString(i++, nn(e.getLastGlobalHash()));
|
||||
setBytesNullable(ps, i++, e.getLastGlobalHash());
|
||||
ps.setLong(i++, e.getUpdatedAtMs());
|
||||
|
||||
for (int line = 0; line < 8; line++) {
|
||||
ps.setInt(i++, e.getLastLineNumber(line));
|
||||
ps.setString(i++, nn(e.getLastLineHash(line)));
|
||||
setBytesNullable(ps, i++, e.getLastLineHash(line));
|
||||
}
|
||||
|
||||
ps.executeUpdate();
|
||||
@ -204,17 +204,22 @@ public final class BlockchainStateDAO {
|
||||
e.setFileSizeBytes(rs.getLong("file_size_bytes"));
|
||||
|
||||
e.setLastGlobalNumber(rs.getInt("last_global_number"));
|
||||
e.setLastGlobalHash(rs.getString("last_global_hash"));
|
||||
e.setLastGlobalHash(rs.getBytes("last_global_hash")); // может быть null
|
||||
|
||||
e.setUpdatedAtMs(rs.getLong("updated_at_ms"));
|
||||
|
||||
for (int line = 0; line < 8; line++) {
|
||||
e.setLastLineNumber(line, rs.getInt("line" + line + "_last_number"));
|
||||
e.setLastLineHash(line, rs.getString("line" + line + "_last_hash"));
|
||||
e.setLastLineHash(line, rs.getBytes("line" + line + "_last_hash")); // может быть null
|
||||
}
|
||||
|
||||
return e;
|
||||
}
|
||||
|
||||
private static void setBytesNullable(PreparedStatement ps, int index, byte[] b) throws SQLException {
|
||||
if (b != null) ps.setBytes(index, b);
|
||||
else ps.setNull(index, Types.BLOB);
|
||||
}
|
||||
|
||||
private static String nn(String s) { return s == null ? "" : s; }
|
||||
}
|
||||
@ -50,8 +50,11 @@ public final class BlocksDAO {
|
||||
to_login,
|
||||
to_bch_name,
|
||||
to_block_global_number,
|
||||
to_block_hashe
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
to_block_hashe,
|
||||
block_hash,
|
||||
block_signature,
|
||||
edited_by_block_global_number
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""";
|
||||
|
||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||
@ -104,7 +107,10 @@ public final class BlocksDAO {
|
||||
to_login,
|
||||
to_bch_name,
|
||||
to_block_global_number,
|
||||
to_block_hashe
|
||||
to_block_hashe,
|
||||
block_hash,
|
||||
block_signature,
|
||||
edited_by_block_global_number
|
||||
FROM blocks
|
||||
WHERE
|
||||
login = ?
|
||||
@ -153,7 +159,10 @@ public final class BlocksDAO {
|
||||
to_login = ?,
|
||||
to_bch_name = ?,
|
||||
to_block_global_number = ?,
|
||||
to_block_hashe = ?
|
||||
to_block_hashe = ?,
|
||||
block_hash = ?,
|
||||
block_signature = ?,
|
||||
edited_by_block_global_number = ?
|
||||
WHERE
|
||||
login = ?
|
||||
AND bch_name = ?
|
||||
@ -165,8 +174,8 @@ public final class BlocksDAO {
|
||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||
int i = 1;
|
||||
|
||||
ps.setString(i++, nn(e.getBlockGlobalPreHashe()));
|
||||
ps.setString(i++, nn(e.getBlockLinePreHashe()));
|
||||
ps.setBytes(i++, bb(e.getBlockGlobalPreHashe()));
|
||||
ps.setBytes(i++, bb(e.getBlockLinePreHashe()));
|
||||
ps.setInt(i++, e.getMsgType());
|
||||
ps.setInt(i++, e.getMsgSubType());
|
||||
|
||||
@ -183,8 +192,14 @@ public final class BlocksDAO {
|
||||
if (e.getToBlockGlobalNumber() != null) ps.setInt(i++, e.getToBlockGlobalNumber());
|
||||
else ps.setNull(i++, Types.INTEGER);
|
||||
|
||||
if (e.getToBlockHashe() != null) ps.setString(i++, e.getToBlockHashe());
|
||||
else ps.setNull(i++, Types.VARCHAR);
|
||||
if (e.getToBlockHashe() != null) ps.setBytes(i++, e.getToBlockHashe());
|
||||
else ps.setNull(i++, Types.BLOB);
|
||||
|
||||
ps.setBytes(i++, bb(e.getBlockHash()));
|
||||
ps.setBytes(i++, bb(e.getBlockSignature()));
|
||||
|
||||
if (e.getEditedByBlockGlobalNumber() != null) ps.setInt(i++, e.getEditedByBlockGlobalNumber());
|
||||
else ps.setNull(i++, Types.INTEGER);
|
||||
|
||||
ps.setString(i++, e.getLogin());
|
||||
ps.setString(i++, e.getBchName());
|
||||
@ -249,11 +264,11 @@ public final class BlocksDAO {
|
||||
ps.setString(i++, e.getLogin());
|
||||
ps.setString(i++, e.getBchName());
|
||||
ps.setInt(i++, e.getBlockGlobalNumber());
|
||||
ps.setString(i++, nn(e.getBlockGlobalPreHashe()));
|
||||
ps.setBytes(i++, bb(e.getBlockGlobalPreHashe()));
|
||||
|
||||
ps.setInt(i++, e.getBlockLineIndex());
|
||||
ps.setInt(i++, e.getBlockLineNumber());
|
||||
ps.setString(i++, nn(e.getBlockLinePreHashe()));
|
||||
ps.setBytes(i++, bb(e.getBlockLinePreHashe()));
|
||||
|
||||
ps.setInt(i++, e.getMsgType());
|
||||
ps.setInt(i++, e.getMsgSubType());
|
||||
@ -271,8 +286,14 @@ public final class BlocksDAO {
|
||||
if (e.getToBlockGlobalNumber() != null) ps.setInt(i++, e.getToBlockGlobalNumber());
|
||||
else ps.setNull(i++, Types.INTEGER);
|
||||
|
||||
if (e.getToBlockHashe() != null) ps.setString(i++, e.getToBlockHashe());
|
||||
else ps.setNull(i++, Types.VARCHAR);
|
||||
if (e.getToBlockHashe() != null) ps.setBytes(i++, e.getToBlockHashe());
|
||||
else ps.setNull(i++, Types.BLOB);
|
||||
|
||||
ps.setBytes(i++, bb(e.getBlockHash()));
|
||||
ps.setBytes(i++, bb(e.getBlockSignature()));
|
||||
|
||||
if (e.getEditedByBlockGlobalNumber() != null) ps.setInt(i++, e.getEditedByBlockGlobalNumber());
|
||||
else ps.setNull(i++, Types.INTEGER);
|
||||
}
|
||||
|
||||
private BlockEntry mapRow(ResultSet rs) throws SQLException {
|
||||
@ -281,11 +302,11 @@ public final class BlocksDAO {
|
||||
e.setLogin(rs.getString("login"));
|
||||
e.setBchName(rs.getString("bch_name"));
|
||||
e.setBlockGlobalNumber(rs.getInt("block_global_number"));
|
||||
e.setBlockGlobalPreHashe(rs.getString("block_global_pre_hashe"));
|
||||
e.setBlockGlobalPreHashe(rs.getBytes("block_global_pre_hashe"));
|
||||
|
||||
e.setBlockLineIndex(rs.getInt("block_line_index"));
|
||||
e.setBlockLineNumber(rs.getInt("block_line_number"));
|
||||
e.setBlockLinePreHashe(rs.getString("block_line_pre_hashe"));
|
||||
e.setBlockLinePreHashe(rs.getBytes("block_line_pre_hashe"));
|
||||
|
||||
e.setMsgType(rs.getInt("msg_type"));
|
||||
e.setMsgSubType(rs.getInt("msg_sub_type"));
|
||||
@ -301,12 +322,18 @@ public final class BlocksDAO {
|
||||
Integer toBlockGlobalNumber = (Integer) rs.getObject("to_block_global_number");
|
||||
e.setToBlockGlobalNumber(toBlockGlobalNumber);
|
||||
|
||||
String toBlockHashe = rs.getString("to_block_hashe");
|
||||
byte[] toBlockHashe = rs.getBytes("to_block_hashe");
|
||||
if (rs.wasNull()) toBlockHashe = null;
|
||||
e.setToBlockHashe(toBlockHashe);
|
||||
|
||||
e.setBlockHash(rs.getBytes("block_hash"));
|
||||
e.setBlockSignature(rs.getBytes("block_signature"));
|
||||
|
||||
Integer editedBy = (Integer) rs.getObject("edited_by_block_global_number");
|
||||
e.setEditedByBlockGlobalNumber(editedBy);
|
||||
|
||||
return e;
|
||||
}
|
||||
|
||||
private static String nn(String s) { return s == null ? "" : s; }
|
||||
private static byte[] bb(byte[] b) { return b == null ? new byte[0] : b; }
|
||||
}
|
||||
@ -12,7 +12,7 @@ import java.sql.*;
|
||||
* - blockchain_state (blockchain_name, login, blockchain_key, size_limit, ... last_global_number=-1 ...)
|
||||
*
|
||||
* ВАЖНО:
|
||||
* - только INSERT
|
||||
* - только INSERT/UPSERT
|
||||
* - если login или blockchainName заняты — возвращаем false (пользователь уже есть/занято)
|
||||
*/
|
||||
public final class UserCreateDAO {
|
||||
@ -69,11 +69,11 @@ public final class UserCreateDAO {
|
||||
|
||||
// старт: глобальных блоков ещё нет
|
||||
st.setLastGlobalNumber(-1);
|
||||
st.setLastGlobalHash("");
|
||||
st.setLastGlobalHash(null);
|
||||
|
||||
for (int line = 0; line < 8; line++) {
|
||||
st.setLastLineNumber(line, 0);
|
||||
st.setLastLineHash(line, "");
|
||||
st.setLastLineHash(line, null);
|
||||
}
|
||||
|
||||
st.setUpdatedAtMs(nowMs);
|
||||
|
||||
@ -9,11 +9,11 @@ public class BlockEntry {
|
||||
private String bchName;
|
||||
|
||||
private int blockGlobalNumber;
|
||||
private String blockGlobalPreHashe;
|
||||
private byte[] blockGlobalPreHashe;
|
||||
|
||||
private int blockLineIndex;
|
||||
private int blockLineNumber;
|
||||
private String blockLinePreHashe;
|
||||
private byte[] blockLinePreHashe;
|
||||
|
||||
private int msgType;
|
||||
private int msgSubType;
|
||||
@ -23,24 +23,32 @@ public class BlockEntry {
|
||||
private String toLogin;
|
||||
private String toBchName;
|
||||
private Integer toBlockGlobalNumber;
|
||||
private String toBlockHashe;
|
||||
private byte[] toBlockHashe;
|
||||
|
||||
// новое
|
||||
private byte[] blockHash;
|
||||
private byte[] blockSignature;
|
||||
private Integer editedByBlockGlobalNumber;
|
||||
|
||||
public BlockEntry() {}
|
||||
|
||||
public BlockEntry(String login,
|
||||
String bchName,
|
||||
int blockGlobalNumber,
|
||||
String blockGlobalPreHashe,
|
||||
byte[] blockGlobalPreHashe,
|
||||
int blockLineIndex,
|
||||
int blockLineNumber,
|
||||
String blockLinePreHashe,
|
||||
byte[] blockLinePreHashe,
|
||||
int msgType,
|
||||
int msgSubType,
|
||||
byte[] blockByte,
|
||||
String toLogin,
|
||||
String toBchName,
|
||||
Integer toBlockGlobalNumber,
|
||||
String toBlockHashe) {
|
||||
byte[] toBlockHashe,
|
||||
byte[] blockHash,
|
||||
byte[] blockSignature,
|
||||
Integer editedByBlockGlobalNumber) {
|
||||
this.login = login;
|
||||
this.bchName = bchName;
|
||||
this.blockGlobalNumber = blockGlobalNumber;
|
||||
@ -55,6 +63,9 @@ public class BlockEntry {
|
||||
this.toBchName = toBchName;
|
||||
this.toBlockGlobalNumber = toBlockGlobalNumber;
|
||||
this.toBlockHashe = toBlockHashe;
|
||||
this.blockHash = blockHash;
|
||||
this.blockSignature = blockSignature;
|
||||
this.editedByBlockGlobalNumber = editedByBlockGlobalNumber;
|
||||
}
|
||||
|
||||
public String getLogin() { return login; }
|
||||
@ -66,8 +77,8 @@ public class BlockEntry {
|
||||
public int getBlockGlobalNumber() { return blockGlobalNumber; }
|
||||
public void setBlockGlobalNumber(int blockGlobalNumber) { this.blockGlobalNumber = blockGlobalNumber; }
|
||||
|
||||
public String getBlockGlobalPreHashe() { return blockGlobalPreHashe; }
|
||||
public void setBlockGlobalPreHashe(String blockGlobalPreHashe) { this.blockGlobalPreHashe = blockGlobalPreHashe; }
|
||||
public byte[] getBlockGlobalPreHashe() { return blockGlobalPreHashe; }
|
||||
public void setBlockGlobalPreHashe(byte[] blockGlobalPreHashe) { this.blockGlobalPreHashe = blockGlobalPreHashe; }
|
||||
|
||||
public int getBlockLineIndex() { return blockLineIndex; }
|
||||
public void setBlockLineIndex(int blockLineIndex) { this.blockLineIndex = blockLineIndex; }
|
||||
@ -75,8 +86,8 @@ public class BlockEntry {
|
||||
public int getBlockLineNumber() { return blockLineNumber; }
|
||||
public void setBlockLineNumber(int blockLineNumber) { this.blockLineNumber = blockLineNumber; }
|
||||
|
||||
public String getBlockLinePreHashe() { return blockLinePreHashe; }
|
||||
public void setBlockLinePreHashe(String blockLinePreHashe) { this.blockLinePreHashe = blockLinePreHashe; }
|
||||
public byte[] getBlockLinePreHashe() { return blockLinePreHashe; }
|
||||
public void setBlockLinePreHashe(byte[] blockLinePreHashe) { this.blockLinePreHashe = blockLinePreHashe; }
|
||||
|
||||
public int getMsgType() { return msgType; }
|
||||
public void setMsgType(int msgType) { this.msgType = msgType; }
|
||||
@ -96,6 +107,15 @@ public class BlockEntry {
|
||||
public Integer getToBlockGlobalNumber() { return toBlockGlobalNumber; }
|
||||
public void setToBlockGlobalNumber(Integer toBlockGlobalNumber) { this.toBlockGlobalNumber = toBlockGlobalNumber; }
|
||||
|
||||
public String getToBlockHashe() { return toBlockHashe; }
|
||||
public void setToBlockHashe(String toBlockHashe) { this.toBlockHashe = toBlockHashe; }
|
||||
public byte[] getToBlockHashe() { return toBlockHashe; }
|
||||
public void setToBlockHashe(byte[] toBlockHashe) { this.toBlockHashe = toBlockHashe; }
|
||||
|
||||
public byte[] getBlockHash() { return blockHash; }
|
||||
public void setBlockHash(byte[] blockHash) { this.blockHash = blockHash; }
|
||||
|
||||
public byte[] getBlockSignature() { return blockSignature; }
|
||||
public void setBlockSignature(byte[] blockSignature) { this.blockSignature = blockSignature; }
|
||||
|
||||
public Integer getEditedByBlockGlobalNumber() { return editedByBlockGlobalNumber; }
|
||||
public void setEditedByBlockGlobalNumber(Integer editedByBlockGlobalNumber) { this.editedByBlockGlobalNumber = editedByBlockGlobalNumber; }
|
||||
}
|
||||
@ -1,3 +1,6 @@
|
||||
// =======================
|
||||
// BlockchainStateEntry.java (НОВАЯ ВЕРСИЯ)
|
||||
// =======================
|
||||
package shine.db.entities;
|
||||
|
||||
import java.util.Arrays;
|
||||
@ -6,6 +9,11 @@ import java.util.Base64;
|
||||
/**
|
||||
* Агрегатная сущность текущего состояния блокчейна.
|
||||
* 1 строка = 1 blockchain_name, плюс состояние линий 0..7.
|
||||
*
|
||||
* ВАЖНО:
|
||||
* - hash-поля теперь храним как byte[] и допускаем NULL:
|
||||
* * NULL = "ещё не было ни одного блока" (genesis и т.п.)
|
||||
* * не подменяем на new byte[0], чтобы не терять смысл
|
||||
*/
|
||||
public final class BlockchainStateEntry {
|
||||
|
||||
@ -18,16 +26,15 @@ public final class BlockchainStateEntry {
|
||||
private long fileSizeBytes;
|
||||
|
||||
private int lastGlobalNumber;
|
||||
private String lastGlobalHash;
|
||||
private byte[] lastGlobalHash; // nullable
|
||||
|
||||
private final int[] lastLineNumbers = new int[8];
|
||||
private final String[] lastLineHashes = new String[8];
|
||||
private final byte[][] lastLineHashes = new byte[8][]; // nullable elements
|
||||
|
||||
private long updatedAtMs;
|
||||
|
||||
public BlockchainStateEntry() {
|
||||
for (int i = 0; i < 8; i++) lastLineHashes[i] = "";
|
||||
this.lastGlobalHash = "";
|
||||
// hashes остаются null по умолчанию (genesis)
|
||||
}
|
||||
|
||||
public BlockchainStateEntry(String blockchainName,
|
||||
@ -36,9 +43,9 @@ public final class BlockchainStateEntry {
|
||||
long sizeLimit,
|
||||
long fileSizeBytes,
|
||||
int lastGlobalNumber,
|
||||
String lastGlobalHash,
|
||||
byte[] lastGlobalHash,
|
||||
int[] lastLineNumbers,
|
||||
String[] lastLineHashes,
|
||||
byte[][] lastLineHashes,
|
||||
long updatedAtMs) {
|
||||
this.blockchainName = blockchainName;
|
||||
this.login = login;
|
||||
@ -46,17 +53,16 @@ public final class BlockchainStateEntry {
|
||||
this.sizeLimit = sizeLimit;
|
||||
this.fileSizeBytes = fileSizeBytes;
|
||||
this.lastGlobalNumber = lastGlobalNumber;
|
||||
this.lastGlobalHash = lastGlobalHash == null ? "" : lastGlobalHash;
|
||||
this.lastGlobalHash = lastGlobalHash;
|
||||
|
||||
if (lastLineNumbers != null) {
|
||||
if (lastLineNumbers.length != 8) throw new IllegalArgumentException("lastLineNumbers must be len=8");
|
||||
System.arraycopy(lastLineNumbers, 0, this.lastLineNumbers, 0, 8);
|
||||
}
|
||||
|
||||
if (lastLineHashes != null) {
|
||||
if (lastLineHashes.length != 8) throw new IllegalArgumentException("lastLineHashes must be len=8");
|
||||
for (int i = 0; i < 8; i++) this.lastLineHashes[i] = lastLineHashes[i] == null ? "" : lastLineHashes[i];
|
||||
} else {
|
||||
for (int i = 0; i < 8; i++) this.lastLineHashes[i] = "";
|
||||
System.arraycopy(lastLineHashes, 0, this.lastLineHashes, 0, 8);
|
||||
}
|
||||
|
||||
this.updatedAtMs = updatedAtMs;
|
||||
@ -92,8 +98,8 @@ public final class BlockchainStateEntry {
|
||||
public int getLastGlobalNumber() { return lastGlobalNumber; }
|
||||
public void setLastGlobalNumber(int lastGlobalNumber) { this.lastGlobalNumber = lastGlobalNumber; }
|
||||
|
||||
public String getLastGlobalHash() { return lastGlobalHash; }
|
||||
public void setLastGlobalHash(String lastGlobalHash) { this.lastGlobalHash = lastGlobalHash == null ? "" : lastGlobalHash; }
|
||||
public byte[] getLastGlobalHash() { return lastGlobalHash; }
|
||||
public void setLastGlobalHash(byte[] lastGlobalHash) { this.lastGlobalHash = lastGlobalHash; }
|
||||
|
||||
public int getLastLineNumber(int line) {
|
||||
checkLine(line);
|
||||
@ -104,17 +110,22 @@ public final class BlockchainStateEntry {
|
||||
lastLineNumbers[line] = value;
|
||||
}
|
||||
|
||||
public String getLastLineHash(int line) {
|
||||
public byte[] getLastLineHash(int line) {
|
||||
checkLine(line);
|
||||
return lastLineHashes[line];
|
||||
}
|
||||
public void setLastLineHash(int line, String value) {
|
||||
public void setLastLineHash(int line, byte[] value) {
|
||||
checkLine(line);
|
||||
lastLineHashes[line] = value == null ? "" : value;
|
||||
lastLineHashes[line] = value;
|
||||
}
|
||||
|
||||
public int[] getLastLineNumbersCopy() { return Arrays.copyOf(lastLineNumbers, 8); }
|
||||
public String[] getLastLineHashesCopy() { return Arrays.copyOf(lastLineHashes, 8); }
|
||||
public int[] getLastLineNumbersCopy() {
|
||||
return Arrays.copyOf(lastLineNumbers, 8);
|
||||
}
|
||||
|
||||
public byte[][] getLastLineHashesCopy() {
|
||||
return Arrays.copyOf(lastLineHashes, 8);
|
||||
}
|
||||
|
||||
public long getUpdatedAtMs() { return updatedAtMs; }
|
||||
public void setUpdatedAtMs(long updatedAtMs) { this.updatedAtMs = updatedAtMs; }
|
||||
|
||||
@ -74,9 +74,7 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
|
||||
}
|
||||
|
||||
resp.setServerLastGlobalNumber(r.serverLastGlobalNumber);
|
||||
if (r.serverLastGlobalHash != null) {
|
||||
resp.setServerLastGlobalHash(r.serverLastGlobalHash);
|
||||
}
|
||||
resp.setServerLastGlobalHash(r.serverLastGlobalHashHex);
|
||||
|
||||
return resp;
|
||||
|
||||
@ -116,32 +114,31 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
|
||||
}
|
||||
|
||||
if (st == null) {
|
||||
// теперь даже для genesis это ошибка: state должен быть создан заранее (с lastGlobalNumber=-1)
|
||||
log.warn("AddBlock: blockchain_state_not_found (login={}, blockchainName={}, globalNumber={})",
|
||||
login, blockchainName, globalNumber);
|
||||
return new AddBlockResult(WireCodes.Status.NOT_FOUND, "blockchain_state_not_found", -1, "");
|
||||
}
|
||||
|
||||
final int serverLastNum = st.getLastGlobalNumber();
|
||||
final String serverLastHash = nn(st.getLastGlobalHash());
|
||||
final String serverLastHashHex = toHex(nnBytes(st.getLastGlobalHash()));
|
||||
|
||||
// ✅ для genesis ожидаем, что state уже в начальном состоянии (-1)
|
||||
if (globalNumber == 0 && serverLastNum != -1) {
|
||||
log.warn("AddBlock: genesis_but_state_not_initial (login={}, blockchainName={}, stateLastGlobalNumber={})",
|
||||
login, blockchainName, serverLastNum);
|
||||
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "genesis_but_state_not_initial", serverLastNum, serverLastHash);
|
||||
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "genesis_but_state_not_initial", serverLastNum, serverLastHashHex);
|
||||
}
|
||||
|
||||
// следующий global строго
|
||||
int expectedGlobal = serverLastNum + 1;
|
||||
if (globalNumber != expectedGlobal) {
|
||||
log.warn("AddBlock: bad_global_number (login={}, blockchainName={}, пришёл={}, ожидали={}, serverLastNum={}, serverLastHash={})",
|
||||
login, blockchainName, globalNumber, expectedGlobal, serverLastNum, serverLastHash);
|
||||
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_global_number", serverLastNum, serverLastHash);
|
||||
login, blockchainName, globalNumber, expectedGlobal, serverLastNum, serverLastHashHex);
|
||||
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_global_number", serverLastNum, serverLastHashHex);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// ✅ 2) Декодируем блок (раньше парсинга body)
|
||||
// ✅ 2) Декодируем блок
|
||||
// -------------------------------------------------------------------
|
||||
final byte[] blockBytes;
|
||||
try {
|
||||
@ -149,26 +146,26 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
|
||||
} catch (Exception e) {
|
||||
log.warn("AddBlock: некорректный base64 блока (login={}, blockchainName={}, globalNumber={})",
|
||||
login, blockchainName, globalNumber, e);
|
||||
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_block_base64", serverLastNum, serverLastHash);
|
||||
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_block_base64", serverLastNum, serverLastHashHex);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// ✅ 3) Ранняя проверка лимита ДО любых записей (как ты попросил)
|
||||
// ✅ 3) Ранняя проверка лимита
|
||||
// -------------------------------------------------------------------
|
||||
try {
|
||||
long oldSize = st.getFileSizeBytes();
|
||||
long limit = st.getSizeLimit(); // предполагается, что поле уже есть (size_limit)
|
||||
long limit = st.getSizeLimit();
|
||||
long newSize = safeAdd(oldSize, blockBytes.length);
|
||||
|
||||
if (limit > 0 && newSize > limit) {
|
||||
log.warn("AddBlock: limit_exceeded (login={}, blockchainName={}, globalNumber={}, oldSize={}, addLen={}, newSize={}, limit={})",
|
||||
login, blockchainName, globalNumber, oldSize, blockBytes.length, newSize, limit);
|
||||
return new AddBlockResult(413, "limit_exceeded", serverLastNum, serverLastHash);
|
||||
return new AddBlockResult(413, "limit_exceeded", serverLastNum, serverLastHashHex);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("AddBlock: limit_check_failed (login={}, blockchainName={}, globalNumber={})",
|
||||
login, blockchainName, globalNumber, e);
|
||||
return new AddBlockResult(WireCodes.Status.INTERNAL_ERROR, "limit_check_failed", serverLastNum, serverLastHash);
|
||||
return new AddBlockResult(WireCodes.Status.INTERNAL_ERROR, "limit_check_failed", serverLastNum, serverLastHashHex);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
@ -178,26 +175,23 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
|
||||
try {
|
||||
block = new BchBlockEntry(blockBytes);
|
||||
} catch (Exception e) {
|
||||
// важно: BchBlockEntry теперь сам валит блок, если body в неправильной линии
|
||||
log.warn("AddBlock: не удалось распарсить BchBlockEntry (login={}, blockchainName={}, globalNumber={}, bytesLen={})",
|
||||
login, blockchainName, globalNumber, blockBytes.length, e);
|
||||
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_block_format", serverLastNum, serverLastHash);
|
||||
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_block_format", serverLastNum, serverLastHashHex);
|
||||
}
|
||||
|
||||
// body.check()
|
||||
try {
|
||||
block.body.check();
|
||||
} catch (Exception e) {
|
||||
log.warn("AddBlock: body.check() не прошёл (login={}, blockchainName={}, globalNumber={}, bodyType={}, bodyVersion={})",
|
||||
login, blockchainName, globalNumber, safeBodyType(block), safeBodyVersion(block), e);
|
||||
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_block_body", serverLastNum, serverLastHash);
|
||||
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_block_body", serverLastNum, serverLastHashHex);
|
||||
}
|
||||
|
||||
// recordNumber == globalNumber
|
||||
if (block.recordNumber != globalNumber) {
|
||||
log.warn("AddBlock: global_number_mismatch (login={}, blockchainName={}, заявлен={}, внутриБлока={})",
|
||||
login, blockchainName, globalNumber, block.recordNumber);
|
||||
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "global_number_mismatch", serverLastNum, serverLastHash);
|
||||
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "global_number_mismatch", serverLastNum, serverLastHashHex);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
@ -205,90 +199,79 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
|
||||
// -------------------------------------------------------------------
|
||||
final byte[] loginKey32;
|
||||
try {
|
||||
// предполагается, что st.getBlockchainKey() возвращает base64-строку, а getBlockchainKeyByte() -> 32 bytes
|
||||
loginKey32 = st.getBlockchainKeyBytes();
|
||||
} catch (Exception e) {
|
||||
log.warn("AddBlock: bad_blockchain_key_in_state (login={}, blockchainName={}, globalNumber={})",
|
||||
login, blockchainName, globalNumber, e);
|
||||
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_blockchain_key_in_state", serverLastNum, serverLastHash);
|
||||
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_blockchain_key_in_state", serverLastNum, serverLastHashHex);
|
||||
}
|
||||
|
||||
if (loginKey32 == null || loginKey32.length != 32) {
|
||||
log.warn("AddBlock: bad_blockchain_key_len (login={}, blockchainName={}, globalNumber={}, keyLen={})",
|
||||
login, blockchainName, globalNumber, (loginKey32 == null ? -1 : loginKey32.length));
|
||||
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_blockchain_key_len", serverLastNum, serverLastHash);
|
||||
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_blockchain_key_len", serverLastNum, serverLastHashHex);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// ✅ 6) prevGlobalHash сравниваем со state.lastGlobalHash
|
||||
// ✅ 6) prevGlobalHash сравниваем со state.lastGlobalHash (оба byte[32])
|
||||
// -------------------------------------------------------------------
|
||||
final byte[] prevGlobalHash32;
|
||||
final byte[] serverPrevGlobal32;
|
||||
try {
|
||||
prevGlobalHash32 = hexTo32(nn(prevGlobalHashHex));
|
||||
serverPrevGlobal32 = hexTo32(nn(st.getLastGlobalHash())); // если пусто -> 32 нуля
|
||||
prevGlobalHash32 = hexTo32(nn(prevGlobalHashHex)); // "" -> 32 нуля
|
||||
} catch (Exception e) {
|
||||
log.warn("AddBlock: bad_prev_global_hash_format (login={}, blockchainName={}, globalNumber={}, prevGlobalHashHex='{}')",
|
||||
login, blockchainName, globalNumber, nn(prevGlobalHashHex), e);
|
||||
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_prev_global_hash_format", serverLastNum, serverLastHash);
|
||||
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_prev_global_hash_format", serverLastNum, serverLastHashHex);
|
||||
}
|
||||
|
||||
final byte[] serverPrevGlobal32 = serverLastNum < 0 ? new byte[32] : nnBytes(st.getLastGlobalHash());
|
||||
if (!bytesEq(prevGlobalHash32, serverPrevGlobal32)) {
|
||||
log.warn("AddBlock: bad_prev_global_hash (login={}, blockchainName={}, globalNumber={}, clientPrev='{}', serverPrev='{}')",
|
||||
login, blockchainName, globalNumber, nn(prevGlobalHashHex), nn(st.getLastGlobalHash()));
|
||||
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_prev_global_hash", serverLastNum, serverLastHash);
|
||||
login, blockchainName, globalNumber, nn(prevGlobalHashHex), toHex(serverPrevGlobal32));
|
||||
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_prev_global_hash", serverLastNum, serverLastHashHex);
|
||||
}
|
||||
|
||||
// ===========================
|
||||
// ЛИНИИ (строго)
|
||||
// ===========================
|
||||
|
||||
int li = block.lineIndex;
|
||||
int ln = block.lineNumber;
|
||||
|
||||
if (globalNumber == 0) {
|
||||
// genesis
|
||||
if (li != 0 || ln != 0) {
|
||||
log.warn("AddBlock: bad_genesis_line_fields (login={}, blockchainName={}, lineIndex={}, lineNumber={})",
|
||||
login, blockchainName, li, ln);
|
||||
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_genesis_line_fields", serverLastNum, serverLastHash);
|
||||
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_genesis_line_fields", serverLastNum, serverLastHashHex);
|
||||
}
|
||||
} else {
|
||||
// MVP: запрещаем lineIndex=0 для не-genesis (чтобы техблоки не пролезли случайно)
|
||||
if (li == 0) {
|
||||
log.warn("AddBlock: line0_only_genesis (login={}, blockchainName={}, globalNumber={}, lineIndex={})",
|
||||
login, blockchainName, globalNumber, li);
|
||||
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "line0_only_genesis", serverLastNum, serverLastHash);
|
||||
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "line0_only_genesis", serverLastNum, serverLastHashHex);
|
||||
}
|
||||
if (li < 1 || li > 7) {
|
||||
log.warn("AddBlock: bad_line_index (login={}, blockchainName={}, globalNumber={}, lineIndex={})",
|
||||
login, blockchainName, globalNumber, li);
|
||||
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_line_index", serverLastNum, serverLastHash);
|
||||
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_line_index", serverLastNum, serverLastHashHex);
|
||||
}
|
||||
|
||||
int expectedLineNumber = st.getLastLineNumber(li) + 1;
|
||||
if (ln != expectedLineNumber) {
|
||||
log.warn("AddBlock: bad_line_number (login={}, blockchainName={}, globalNumber={}, lineIndex={}, пришёлLineNumber={}, ожидалиLineNumber={}, lastLineNumber={})",
|
||||
login, blockchainName, globalNumber, li, ln, expectedLineNumber, st.getLastLineNumber(li));
|
||||
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_line_number", serverLastNum, serverLastHash);
|
||||
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_line_number", serverLastNum, serverLastHashHex);
|
||||
}
|
||||
}
|
||||
|
||||
// prevLineHash берём из state по lineIndex:
|
||||
// - genesis: 32 нулей
|
||||
// - иначе: st.getLastLineHash(li) (для первой записи в линии это будет hash genesis)
|
||||
final byte[] prevLineHash32;
|
||||
final String prevLineHashHex;
|
||||
try {
|
||||
prevLineHashHex = computePrevLineHashHex(st, li);
|
||||
prevLineHash32 = hexTo32(prevLineHashHex);
|
||||
prevLineHash32 = computePrevLineHash32(st, li);
|
||||
} catch (Exception e) {
|
||||
log.warn("AddBlock: bad_prev_line_hash_in_state (login={}, blockchainName={}, globalNumber={}, lineIndex={})",
|
||||
login, blockchainName, globalNumber, li, e);
|
||||
return new AddBlockResult(WireCodes.Status.INTERNAL_ERROR, "bad_prev_line_hash_in_state", serverLastNum, serverLastHash);
|
||||
return new AddBlockResult(WireCodes.Status.INTERNAL_ERROR, "bad_prev_line_hash_in_state", serverLastNum, serverLastHashHex);
|
||||
}
|
||||
|
||||
// crypto verify
|
||||
boolean ok = BchCryptoVerifier.verifyAll(
|
||||
login,
|
||||
prevGlobalHash32,
|
||||
@ -302,28 +285,27 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
|
||||
if (!ok) {
|
||||
log.warn("AddBlock: bad_signature_or_hash (login={}, blockchainName={}, globalNumber={}, lineIndex={}, lineNumber={})",
|
||||
login, blockchainName, globalNumber, li, ln);
|
||||
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_signature_or_hash", serverLastNum, serverLastHash);
|
||||
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_signature_or_hash", serverLastNum, serverLastHashHex);
|
||||
}
|
||||
|
||||
String newHashHex = toHex(block.getHash32());
|
||||
|
||||
// write
|
||||
try {
|
||||
dbWriter.appendBlockAndState(
|
||||
login,
|
||||
blockchainName,
|
||||
nn(prevGlobalHashHex),
|
||||
prevLineHashHex,
|
||||
prevGlobalHash32,
|
||||
prevLineHash32,
|
||||
block,
|
||||
st,
|
||||
newHashHex
|
||||
st
|
||||
);
|
||||
} catch (Exception e) {
|
||||
log.error("AddBlock: внутренняя ошибка при записи блока (login={}, blockchainName={}, globalNumber={}, newHash={})",
|
||||
login, blockchainName, globalNumber, newHashHex, e);
|
||||
return new AddBlockResult(WireCodes.Status.INTERNAL_ERROR, "internal_error", serverLastNum, serverLastHash);
|
||||
log.error("AddBlock: внутренняя ошибка при записи блока (login={}, blockchainName={}, globalNumber={})",
|
||||
login, blockchainName, globalNumber, e);
|
||||
return new AddBlockResult(WireCodes.Status.INTERNAL_ERROR, "internal_error", serverLastNum, serverLastHashHex);
|
||||
}
|
||||
|
||||
String newHashHex = toHex(block.getHash32());
|
||||
|
||||
log.info("✅ AddBlock ok: login={}, blockchainName={}, globalNumber={}, lineIndex={}, lineNumber={}, newHash={}",
|
||||
login, blockchainName, globalNumber, li, ln, newHashHex);
|
||||
|
||||
@ -332,43 +314,42 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
|
||||
|
||||
/**
|
||||
* ✅ Правило:
|
||||
* - lineIndex=0 (genesis линия): prevLineHash = 32 нулей (пустая строка => hexTo32 даст 32 нуля)
|
||||
* - lineIndex=0: prevLineHash = 32 нулей
|
||||
* - lineIndex>0:
|
||||
* - если в этой линии ещё нет блоков (lastLineNumber==0) => prevLineHash = hash(genesis) (line0 hash)
|
||||
* - иначе => prevLineHash = lastLineHash(lineIndex)
|
||||
*/
|
||||
private static String computePrevLineHashHex(BlockchainStateEntry st, int lineIndex) {
|
||||
private static byte[] computePrevLineHash32(BlockchainStateEntry st, int lineIndex) {
|
||||
if (lineIndex == 0) {
|
||||
return ""; // -> 32 нуля
|
||||
return new byte[32];
|
||||
}
|
||||
|
||||
int lastLn = st.getLastLineNumber(lineIndex);
|
||||
if (lastLn == 0) {
|
||||
// первая запись линии -> от genesis
|
||||
String genesis = nn(st.getLastLineHash(0));
|
||||
if (!genesis.isBlank()) return genesis;
|
||||
byte[] genesis = nnBytes(st.getLastLineHash(0));
|
||||
if (genesis.length == 32) return genesis;
|
||||
|
||||
// fallback: если line0 почему-то не заполнена, но genesis глобально есть
|
||||
String g = nn(st.getLastGlobalHash());
|
||||
if (!g.isBlank()) return g;
|
||||
byte[] g = nnBytes(st.getLastGlobalHash());
|
||||
if (g.length == 32) return g;
|
||||
|
||||
return "";
|
||||
return new byte[32];
|
||||
}
|
||||
|
||||
return nn(st.getLastLineHash(lineIndex));
|
||||
byte[] last = nnBytes(st.getLastLineHash(lineIndex));
|
||||
return last.length == 32 ? last : new byte[32];
|
||||
}
|
||||
|
||||
private static final class AddBlockResult {
|
||||
final int httpStatus;
|
||||
final String reasonCode;
|
||||
final int serverLastGlobalNumber;
|
||||
final String serverLastGlobalHash;
|
||||
final String serverLastGlobalHashHex;
|
||||
|
||||
AddBlockResult(int httpStatus, String reasonCode, int serverLastGlobalNumber, String serverLastGlobalHash) {
|
||||
AddBlockResult(int httpStatus, String reasonCode, int serverLastGlobalNumber, String serverLastGlobalHashHex) {
|
||||
this.httpStatus = httpStatus;
|
||||
this.reasonCode = reasonCode;
|
||||
this.serverLastGlobalNumber = serverLastGlobalNumber;
|
||||
this.serverLastGlobalHash = serverLastGlobalHash;
|
||||
this.serverLastGlobalHashHex = serverLastGlobalHashHex;
|
||||
}
|
||||
|
||||
boolean isOk() {
|
||||
@ -378,6 +359,8 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
|
||||
|
||||
private static String nn(String s) { return s == null ? "" : s; }
|
||||
|
||||
private static byte[] nnBytes(byte[] b) { return b == null ? new byte[0] : b; }
|
||||
|
||||
private static byte[] decodeBase64(String s) {
|
||||
if (s == null || s.isBlank()) throw new IllegalArgumentException("empty base64");
|
||||
return Base64.getDecoder().decode(s);
|
||||
@ -408,6 +391,7 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
|
||||
}
|
||||
|
||||
private static String toHex(byte[] bytes) {
|
||||
if (bytes == null || bytes.length == 0) return "";
|
||||
char[] HEX = "0123456789abcdef".toCharArray();
|
||||
char[] out = new char[bytes.length * 2];
|
||||
for (int i = 0; i < bytes.length; i++) {
|
||||
|
||||
@ -1,3 +1,6 @@
|
||||
// =======================
|
||||
// BlockchainWriter.java (НОВАЯ ВЕРСИЯ)
|
||||
// =======================
|
||||
package server.logic.ws_protocol.JSON.handlers.blockchain.Net_AddBlock_Handler_utils;
|
||||
|
||||
import blockchain.BchBlockEntry;
|
||||
@ -15,6 +18,8 @@ import shine.log.BlockchainAdminNotifier;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Types;
|
||||
import java.util.Base64;
|
||||
|
||||
/**
|
||||
* BlockchainWriter — единая точка записи:
|
||||
@ -25,24 +30,11 @@ import java.sql.SQLException;
|
||||
* 3) атомарно заменяем файл:
|
||||
* - удаляем/замещаем старый <name>.bch
|
||||
* - переименовываем <name>.tmp_bch -> <name>.bch
|
||||
*
|
||||
* Важно:
|
||||
* - Шаг (2) — строго атомарный (SQL tx).
|
||||
* - Шаг (3) — атомарный на уровне ФС, если поддерживается ATOMIC_MOVE.
|
||||
*
|
||||
* ДОПОЛНЕНИЕ (КРИТИЧНО):
|
||||
* - Перед тем как дописывать блок, проверяем:
|
||||
* реальный размер <name>.bch == st.fileSizeBytes.
|
||||
* Если не совпадает — считаем это критической внешней порчей файлов,
|
||||
* шлём уведомление админу и НЕ продолжаем запись.
|
||||
*/
|
||||
public final class BlockchainWriter {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(BlockchainWriter.class);
|
||||
|
||||
private static final String ZERO_HASH_64 =
|
||||
"0000000000000000000000000000000000000000000000000000000000000000";
|
||||
|
||||
private final SqliteDbController db;
|
||||
private final BlocksDAO blocksDAO;
|
||||
private final BlockchainStateDAO stateDAO;
|
||||
@ -55,44 +47,28 @@ public final class BlockchainWriter {
|
||||
this.fs = FileStoreUtil.getInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
* Главный метод:
|
||||
* - (0) проверяет соответствие размера файла и state (если это не genesis)
|
||||
* - создаёт tmp-файл (старое+новое),
|
||||
* - атомарно коммитит БД (block+state),
|
||||
* - атомарно заменяет основной файл.
|
||||
*/
|
||||
public void appendBlockAndState(
|
||||
String login,
|
||||
String blockchainName,
|
||||
String prevGlobalHashHex,
|
||||
String prevLineHashHex,
|
||||
byte[] prevGlobalHash32,
|
||||
byte[] prevLineHash32,
|
||||
BchBlockEntry block,
|
||||
BlockchainStateEntry stOrNull,
|
||||
String newHashHex
|
||||
BlockchainStateEntry stOrNull
|
||||
) throws SQLException {
|
||||
|
||||
// ✅ ВАЖНО: state теперь ОБЯЗАТЕЛЕН, genesis НЕ создаёт запись, а обновляет существующую
|
||||
if (stOrNull == null) {
|
||||
throw new SQLException("blockchain_state not found for blockchainName=" + blockchainName + " (state обязателен)");
|
||||
}
|
||||
|
||||
verifyMainFileSizeMatchesStateOrAlert(login, blockchainName, block, stOrNull);
|
||||
|
||||
// =====================================================================
|
||||
// ШАГ 1. Готовим bytes нового блока (включая signature+hash)
|
||||
// =====================================================================
|
||||
// bytes FULL блока (raw+sig+hash)
|
||||
final byte[] newBlockFullBytes = block.toBytes();
|
||||
|
||||
// =====================================================================
|
||||
// ШАГ 2. Считаем новый fileSizeBytes
|
||||
// =====================================================================
|
||||
final long oldFileSize = stOrNull.getFileSizeBytes();
|
||||
final long newFileSize = safeAdd(oldFileSize, newBlockFullBytes.length);
|
||||
|
||||
// =====================================================================
|
||||
// ШАГ 3. Создаём новый tmp-файл: tmp = (old file bytes) + (new block bytes)
|
||||
// =====================================================================
|
||||
// tmp = old + new
|
||||
final byte[] tmpBytes;
|
||||
if (oldFileSize == 0) {
|
||||
tmpBytes = newBlockFullBytes;
|
||||
@ -129,9 +105,7 @@ public final class BlockchainWriter {
|
||||
throw new SQLException("Cannot write tmp blockchain file for: " + blockchainName, e);
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// ШАГ 4. АТОМАРНО фиксируем БД
|
||||
// =====================================================================
|
||||
// атомарно БД
|
||||
try (Connection c = db.getConnection()) {
|
||||
|
||||
boolean oldAutoCommit = c.getAutoCommit();
|
||||
@ -140,9 +114,8 @@ public final class BlockchainWriter {
|
||||
boolean committed = false;
|
||||
|
||||
try {
|
||||
insertBlockRow(c, login, blockchainName, prevGlobalHashHex, prevLineHashHex, block);
|
||||
|
||||
appendState(c, blockchainName, block, stOrNull, newHashHex, newFileSize);
|
||||
insertBlockRow(c, login, blockchainName, prevGlobalHash32, prevLineHash32, block);
|
||||
appendState(c, blockchainName, block, stOrNull, newFileSize);
|
||||
|
||||
c.commit();
|
||||
committed = true;
|
||||
@ -150,8 +123,8 @@ public final class BlockchainWriter {
|
||||
} catch (Exception e) {
|
||||
try { c.rollback(); } catch (SQLException ignore) {}
|
||||
|
||||
log.error("Ошибка транзакции БД при добавлении блока (rollback выполнен) (login={}, blockchainName={}, blockNumber={}, prevGlobalHash={}, prevLineHash={}, newHash={}, oldFileSize={}, newFileSize={})",
|
||||
login, blockchainName, block.recordNumber, prevGlobalHashHex, prevLineHashHex, newHashHex, oldFileSize, newFileSize, e);
|
||||
log.error("Ошибка транзакции БД при добавлении блока (rollback выполнен) (login={}, blockchainName={}, blockNumber={}, oldFileSize={}, newFileSize={})",
|
||||
login, blockchainName, block.recordNumber, oldFileSize, newFileSize, e);
|
||||
|
||||
if (e instanceof SQLException se) throw se;
|
||||
throw new SQLException("appendBlockAndState failed (db tx)", e);
|
||||
@ -160,15 +133,13 @@ public final class BlockchainWriter {
|
||||
try { c.setAutoCommit(oldAutoCommit); } catch (SQLException ignore) {}
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// ШАГ 5. После успешного коммита БД — атомарно заменяем файл
|
||||
// =================================================================
|
||||
// после коммита БД — атомарно заменяем файл
|
||||
if (committed) {
|
||||
try {
|
||||
fs.atomicReplaceBlockchainFile(blockchainName);
|
||||
} catch (Exception moveError) {
|
||||
log.error("БД закоммичена, но атомарная замена файла блокчейна не удалась. tmp оставлен для recovery. (login={}, blockchainName={}, blockNumber={}, newHash={}, tmpBytesLen={})",
|
||||
login, blockchainName, block.recordNumber, newHashHex, tmpBytes.length, moveError);
|
||||
log.error("БД закоммичена, но атомарная замена файла блокчейна не удалась. tmp оставлен для recovery. (login={}, blockchainName={}, blockNumber={})",
|
||||
login, blockchainName, block.recordNumber, moveError);
|
||||
|
||||
throw new SQLException(
|
||||
"DB committed but file replace failed; tmp kept for recovery. blockchainName=" + blockchainName,
|
||||
@ -200,7 +171,6 @@ public final class BlockchainWriter {
|
||||
", blockchainName=" + blockchainName +
|
||||
", expectedSizeFromState=" + expected +
|
||||
", blockNumber=" + (block != null ? block.recordNumber : -1) + ".";
|
||||
|
||||
BlockchainAdminNotifier.critical(msg, null);
|
||||
throw new SQLException(msg);
|
||||
}
|
||||
@ -228,43 +198,32 @@ public final class BlockchainWriter {
|
||||
", realMainFileSize=" + real +
|
||||
", blockNumber=" + (block != null ? block.recordNumber : -1) + ". " +
|
||||
"Похоже на внешнее вмешательство/порчу файла. Запись нового блока остановлена.";
|
||||
|
||||
BlockchainAdminNotifier.critical(msg, null);
|
||||
throw new SQLException(msg);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Обновление состояния blockchain_state (создаём если отсутствует).
|
||||
*
|
||||
* ПРАВИЛО ЛИНИЙ (как ты описал):
|
||||
* - globalNumber=0 — genesis в lineIndex=0, lineNumber=0, и его hash — базовый для ВСЕХ линий.
|
||||
* - для lineIndex>0 первая запись имеет lineNumber=1, её prevLineHash = hash(genesis)
|
||||
* - lastLineNumber/lastLineHash ведём независимо по каждой линии.
|
||||
*/
|
||||
private void appendState(
|
||||
Connection c,
|
||||
String blockchainName,
|
||||
BchBlockEntry block,
|
||||
BlockchainStateEntry stOrNull,
|
||||
String newHashHex,
|
||||
long newFileSizeBytes
|
||||
) throws SQLException {
|
||||
|
||||
// ✅ state обязателен
|
||||
BlockchainStateEntry st = stOrNull;
|
||||
if (st == null) {
|
||||
throw new SQLException("blockchain_state not found for blockchainName=" + blockchainName);
|
||||
}
|
||||
|
||||
// глобальная цепочка всегда растёт по recordNumber
|
||||
// глобальная цепочка
|
||||
st.setLastGlobalNumber(block.recordNumber);
|
||||
st.setLastGlobalHash(newHashHex);
|
||||
st.setLastGlobalHash(block.getHash32());
|
||||
|
||||
// обновляем конкретную линию блока
|
||||
// линия
|
||||
int li = block.lineIndex;
|
||||
st.setLastLineNumber(li, block.lineNumber);
|
||||
st.setLastLineHash(li, newHashHex);
|
||||
st.setLastLineHash(li, block.getHash32());
|
||||
|
||||
// file size
|
||||
st.setFileSizeBytes(newFileSizeBytes);
|
||||
@ -276,24 +235,14 @@ public final class BlockchainWriter {
|
||||
}
|
||||
|
||||
/**
|
||||
* Вставка/апдейт строки блока в blocks.
|
||||
*
|
||||
* Важно:
|
||||
* - blockLinePreHashe = prevLineHashHex (а НЕ prevGlobalHashHex)
|
||||
* - msgType = body.type()
|
||||
* - msgSubType = body.subType()
|
||||
* - to* поля берём через BodyToFields (если body его поддерживает)
|
||||
*
|
||||
* Про toLogin:
|
||||
* - если body сам даёт toLogin — пишем его
|
||||
* - иначе, если есть toBchName — пробуем вычислить login из имени блокчейна (про запас)
|
||||
* Вставка/апдейт строки блока в blocks (BLOB-вариант).
|
||||
*/
|
||||
private void insertBlockRow(
|
||||
Connection c,
|
||||
String login,
|
||||
String blockchainName,
|
||||
String prevGlobalHashHex,
|
||||
String prevLineHashHex,
|
||||
byte[] prevGlobalHash32,
|
||||
byte[] prevLineHash32,
|
||||
BchBlockEntry block
|
||||
) throws SQLException {
|
||||
|
||||
@ -303,38 +252,31 @@ public final class BlockchainWriter {
|
||||
e.setBchName(blockchainName);
|
||||
|
||||
e.setBlockGlobalNumber(block.recordNumber);
|
||||
e.setBlockGlobalPreHashe(prevGlobalHashHex);
|
||||
e.setBlockGlobalPreHashe(prevGlobalHash32);
|
||||
|
||||
e.setBlockLineIndex(block.lineIndex);
|
||||
e.setBlockLineNumber(block.lineNumber);
|
||||
|
||||
// ✅ минимальная правка: для genesis сохраняем именно "64 нуля", а не пустую строку/NULL
|
||||
String linePre = prevLineHashHex;
|
||||
if (block.recordNumber == 0 && (linePre == null || linePre.isBlank())) {
|
||||
linePre = ZERO_HASH_64;
|
||||
}
|
||||
e.setBlockLinePreHashe(linePre);
|
||||
e.setBlockLinePreHashe(prevLineHash32);
|
||||
|
||||
e.setMsgType(block.body.type());
|
||||
e.setMsgSubType(block.body.subType());
|
||||
|
||||
// ВАЖНО: здесь ты кладёшь FULL bytes (raw+sig+hash). Это ок, ты так задумал.
|
||||
e.setBlockByte(block.toBytes());
|
||||
|
||||
// defaults
|
||||
// to-поля
|
||||
e.setToLogin(null);
|
||||
e.setToBchName(null);
|
||||
e.setToBlockGlobalNumber(null);
|
||||
e.setToBlockHashe(null);
|
||||
|
||||
// ✅ Универсально: если body поддерживает to-поля — пишем их
|
||||
if (block.body instanceof BodyHasTarget tf) {
|
||||
|
||||
e.setToLogin(tf.toLogin());
|
||||
e.setToBchName(tf.toBchName());
|
||||
e.setToBlockGlobalNumber(tf.toBlockGlobalNumber());
|
||||
e.setToBlockHashe(tf.toBlockHashe());
|
||||
e.setToBlockHashe(tf.toBlockHasheBytes());
|
||||
|
||||
// optional: try compute to_login from target chain name (для индекса idx_blocks_to_target)
|
||||
// если to_login не пришёл, но есть to_bch_name — восстановим логин из имени цепочки
|
||||
if (e.getToLogin() == null && e.getToBchName() != null) {
|
||||
String toLogin = BlockchainNameUtil.loginFromBlockchainName(e.getToBchName());
|
||||
if (toLogin != null && !toLogin.isBlank()) {
|
||||
@ -343,12 +285,17 @@ public final class BlockchainWriter {
|
||||
}
|
||||
}
|
||||
|
||||
// новое: хэш и подпись самого блока
|
||||
e.setBlockHash(block.getHash32());
|
||||
e.setBlockSignature(block.getSignature64());
|
||||
|
||||
// новое: не трогаем (NULL); триггер пометит исходный блок
|
||||
e.setEditedByBlockGlobalNumber(null);
|
||||
|
||||
blocksDAO.upsert(c, e);
|
||||
}
|
||||
|
||||
/* ===================================================================== */
|
||||
/* =============================== Utils ================================ */
|
||||
/* ===================================================================== */
|
||||
// -------------------- utils --------------------
|
||||
|
||||
private static byte[] concat(byte[] a, byte[] b) {
|
||||
byte[] out = new byte[a.length + b.length];
|
||||
@ -364,4 +311,32 @@ public final class BlockchainWriter {
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
// Если у тебя где-то ещё остался String-хэш (legacy), используй это в месте парсинга JSON,
|
||||
// но НЕ в writer. Оставляю тут только на всякий случай для миграции:
|
||||
@SuppressWarnings("unused")
|
||||
private static byte[] decodeHashStringLenient(String s) {
|
||||
if (s == null) return null;
|
||||
String t = s.trim();
|
||||
if (t.isEmpty()) return null;
|
||||
|
||||
// hex 64
|
||||
if (t.length() == 64 && t.matches("^[0-9a-fA-F]+$")) {
|
||||
byte[] out = new byte[32];
|
||||
for (int i = 0; i < 32; i++) {
|
||||
int hi = Character.digit(t.charAt(i * 2), 16);
|
||||
int lo = Character.digit(t.charAt(i * 2 + 1), 16);
|
||||
out[i] = (byte) ((hi << 4) | lo);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// base64 (часто у тебя так)
|
||||
try {
|
||||
byte[] b = Base64.getDecoder().decode(t);
|
||||
return (b != null && b.length == 32) ? b : b;
|
||||
} catch (IllegalArgumentException ignore) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -102,7 +102,7 @@ public class Net_AddUser_Handler implements JsonMessageHandler {
|
||||
st.setLogin(req.getLogin());
|
||||
st.setBlockchainKey(req.getLoginKey()); // Base64(32)
|
||||
st.setLastGlobalNumber(-1);
|
||||
st.setLastGlobalHash("");
|
||||
st.setLastGlobalHash(new byte[32]);
|
||||
st.setFileSizeBytes(0);
|
||||
st.setSizeLimit(limit);
|
||||
st.setUpdatedAtMs(System.currentTimeMillis());
|
||||
|
||||
Loading…
Reference in New Issue
Block a user