From 06c77b1c1fea2a1ca609a2b907b33103d382dd461c060b03a8ed785b4834c7a0 Mon Sep 17 00:00:00 2001 From: AidarKC Date: Wed, 7 Jan 2026 19:58:50 +0300 Subject: [PATCH] =?UTF-8?q?07=2001=2025=20refactor:=20=D0=BF=D0=B5=D1=80?= =?UTF-8?q?=D0=B5=D0=B2=D0=B5=D0=BB=D0=B8=20=D1=85=D1=8D=D1=88=D0=B8=20?= =?UTF-8?q?=D0=BD=D0=B0=20BLOB=20=D0=B8=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D0=BB=D0=B8=20=D0=BF=D0=BE=D0=BB=D1=8F=20block=5Fhash=20?= =?UTF-8?q?/=20block=5Fsignature=20/=20edited=5Fby=5Fblock=5Fglobal=5Fnumb?= =?UTF-8?q?er?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit и главное добавили тип блока изменение сообщение и сслку на последнее изменение в табл блокс --- .../java/blockchain/body/BodyHasTarget.java | 4 +- .../java/blockchain/body/ConnectionBody.java | 7 +- .../java/blockchain/body/ReactionBody.java | 11 +- .../main/java/blockchain/body/TextBody.java | 19 +-- .../java/shine/db/DatabaseInitializer.java | 73 +++++--- .../java/shine/db/dao/BlockchainStateDAO.java | 23 ++- .../src/main/java/shine/db/dao/BlocksDAO.java | 75 ++++++--- .../main/java/shine/db/dao/UserCreateDAO.java | 6 +- .../java/shine/db/entities/BlockEntry.java | 44 +++-- .../db/entities/BlockchainStateEntry.java | 45 +++-- .../blockchain/Net_AddBlock_Handler.java | 122 ++++++-------- .../BlockchainWriter.java | 159 ++++++++---------- .../tempToTest/Net_AddUser_Handler.java | 2 +- 13 files changed, 306 insertions(+), 284 deletions(-) diff --git a/shine-server-blockchain/src/main/java/blockchain/body/BodyHasTarget.java b/shine-server-blockchain/src/main/java/blockchain/body/BodyHasTarget.java index 2f54481..8710038 100644 --- a/shine-server-blockchain/src/main/java/blockchain/body/BodyHasTarget.java +++ b/shine-server-blockchain/src/main/java/blockchain/body/BodyHasTarget.java @@ -27,6 +27,6 @@ public interface BodyHasTarget { /** globalNumber цели (nullable). */ Integer toBlockGlobalNumber(); - /** hash цели в HEX(64) (nullable). */ - String toBlockHashe(); + /** hash целевого блока (обычно 32 байта). Может быть null, если ссылки нет. */ + byte[] toBlockHasheBytes(); } \ No newline at end of file diff --git a/shine-server-blockchain/src/main/java/blockchain/body/ConnectionBody.java b/shine-server-blockchain/src/main/java/blockchain/body/ConnectionBody.java index 4e28ba1..971d799 100644 --- a/shine-server-blockchain/src/main/java/blockchain/body/ConnectionBody.java +++ b/shine-server-blockchain/src/main/java/blockchain/body/ConnectionBody.java @@ -49,9 +49,6 @@ import java.util.Objects; * * ВАЖНО: поля toBlockchainName/toBlockGlobalNumber/toBlockHash32 — это * "последний известный блок" того человека (снимок/якорь состояния). - * По сути можно было бы обойтись без них, но они полезны: - * - фиксируют, какой блок и какой хэш ты считаешь последним известным у друга/контакта; - * - помогают синхронизации/проверкам (например, если потом сравнивать, насколько данные устарели). * * ЛИНИЯ: * - строго lineIndex=3 (выделяем отдельную линию под связи). @@ -182,7 +179,7 @@ public final class ConnectionBody implements BodyRecord, BodyHasTarget { private static boolean isValidSubType(short st) { return st == SUB_FRIEND || st == SUB_CONTACT || st == SUB_FOLLOW - || st == SUB_UNFRIEND || st == SUB_UNCONTACT || st == SUB_UNFOLLOW; + || st == SUB_UNFRIEND || st == SUB_UNCONTACT || st == SUB_UNFOLLOW; } /** true если это событие установки связи (10/20/30). */ @@ -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; } } \ No newline at end of file diff --git a/shine-server-blockchain/src/main/java/blockchain/body/ReactionBody.java b/shine-server-blockchain/src/main/java/blockchain/body/ReactionBody.java index 450737a..c8c0439 100644 --- a/shine-server-blockchain/src/main/java/blockchain/body/ReactionBody.java +++ b/shine-server-blockchain/src/main/java/blockchain/body/ReactionBody.java @@ -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; } } \ No newline at end of file diff --git a/shine-server-blockchain/src/main/java/blockchain/body/TextBody.java b/shine-server-blockchain/src/main/java/blockchain/body/TextBody.java index c0e0aeb..d41786e 100644 --- a/shine-server-blockchain/src/main/java/blockchain/body/TextBody.java +++ b/shine-server-blockchain/src/main/java/blockchain/body/TextBody.java @@ -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; } } \ No newline at end of file diff --git a/shine-server-db/src/main/java/shine/db/DatabaseInitializer.java b/shine-server-db/src/main/java/shine/db/DatabaseInitializer.java index 2cdace0..9175eb1 100644 --- a/shine-server-db/src/main/java/shine/db/DatabaseInitializer.java +++ b/shine-server-db/src/main/java/shine/db/DatabaseInitializer.java @@ -151,41 +151,40 @@ 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, login TEXT NOT NULL, blockchain_key TEXT NOT NULL, - + size_limit INTEGER NOT NULL, 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; + """); } } } \ No newline at end of file diff --git a/shine-server-db/src/main/java/shine/db/dao/BlockchainStateDAO.java b/shine-server-db/src/main/java/shine/db/dao/BlockchainStateDAO.java index a8962ae..e3f7e27 100644 --- a/shine-server-db/src/main/java/shine/db/dao/BlockchainStateDAO.java +++ b/shine-server-db/src/main/java/shine/db/dao/BlockchainStateDAO.java @@ -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; } } \ No newline at end of file diff --git a/shine-server-db/src/main/java/shine/db/dao/BlocksDAO.java b/shine-server-db/src/main/java/shine/db/dao/BlocksDAO.java index 137f949..a44c975 100644 --- a/shine-server-db/src/main/java/shine/db/dao/BlocksDAO.java +++ b/shine-server-db/src/main/java/shine/db/dao/BlocksDAO.java @@ -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 = ? @@ -145,15 +151,18 @@ public final class BlocksDAO { String sql = """ UPDATE blocks SET - block_global_pre_hashe = ?, - block_line_pre_hashe = ?, - msg_type = ?, - msg_sub_type = ?, - block_byte = ?, - to_login = ?, - to_bch_name = ?, - to_block_global_number = ?, - to_block_hashe = ? + block_global_pre_hashe = ?, + block_line_pre_hashe = ?, + msg_type = ?, + msg_sub_type = ?, + block_byte = ?, + to_login = ?, + to_bch_name = ?, + to_block_global_number = ?, + 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; } } \ No newline at end of file diff --git a/shine-server-db/src/main/java/shine/db/dao/UserCreateDAO.java b/shine-server-db/src/main/java/shine/db/dao/UserCreateDAO.java index 68c6839..74ade2d 100644 --- a/shine-server-db/src/main/java/shine/db/dao/UserCreateDAO.java +++ b/shine-server-db/src/main/java/shine/db/dao/UserCreateDAO.java @@ -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); diff --git a/shine-server-db/src/main/java/shine/db/entities/BlockEntry.java b/shine-server-db/src/main/java/shine/db/entities/BlockEntry.java index 60b8b20..679b79a 100644 --- a/shine-server-db/src/main/java/shine/db/entities/BlockEntry.java +++ b/shine-server-db/src/main/java/shine/db/entities/BlockEntry.java @@ -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; } } \ No newline at end of file diff --git a/shine-server-db/src/main/java/shine/db/entities/BlockchainStateEntry.java b/shine-server-db/src/main/java/shine/db/entities/BlockchainStateEntry.java index bd777bd..629c561 100644 --- a/shine-server-db/src/main/java/shine/db/entities/BlockchainStateEntry.java +++ b/shine-server-db/src/main/java/shine/db/entities/BlockchainStateEntry.java @@ -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; } diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/Net_AddBlock_Handler.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/Net_AddBlock_Handler.java index 5e85888..805dcc9 100644 --- a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/Net_AddBlock_Handler.java +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/Net_AddBlock_Handler.java @@ -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++) { diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/Net_AddBlock_Handler_utils/BlockchainWriter.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/Net_AddBlock_Handler_utils/BlockchainWriter.java index 2385eff..12207d1 100644 --- a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/Net_AddBlock_Handler_utils/BlockchainWriter.java +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/Net_AddBlock_Handler_utils/BlockchainWriter.java @@ -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) атомарно заменяем файл: * - удаляем/замещаем старый .bch * - переименовываем .tmp_bch -> .bch - * - * Важно: - * - Шаг (2) — строго атомарный (SQL tx). - * - Шаг (3) — атомарный на уровне ФС, если поддерживается ATOMIC_MOVE. - * - * ДОПОЛНЕНИЕ (КРИТИЧНО): - * - Перед тем как дописывать блок, проверяем: - * реальный размер .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; + } + } } \ No newline at end of file diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/tempToTest/Net_AddUser_Handler.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/tempToTest/Net_AddUser_Handler.java index 8b84da0..0403088 100644 --- a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/tempToTest/Net_AddUser_Handler.java +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/tempToTest/Net_AddUser_Handler.java @@ -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());