diff --git a/.idea/gradle.xml b/.idea/gradle.xml
index 42b6756..6a8fc09 100644
--- a/.idea/gradle.xml
+++ b/.idea/gradle.xml
@@ -14,6 +14,7 @@
+
diff --git a/build.gradle b/build.gradle
index 62920f4..d301162 100644
--- a/build.gradle
+++ b/build.gradle
@@ -39,6 +39,7 @@ dependencies {
implementation project(':shine-server-config') // модуль настроек из application.properties
+ implementation project(":shine-server-log") // модуль логирования и уведомления админов
implementation project(':shine-server-crypto') // модуль сервера для работы с криптографией
implementation project(':shine-server-db') // модуль для работы с БД содержит и сущности из БД и саму работу с БД
diff --git a/settings.gradle b/settings.gradle
index 4795f39..e73bc6c 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1,5 +1,6 @@
rootProject.name = 'shine-server-server'
+include 'shine-server-log'
include 'shine-server-config'
include 'shine-server-geo'
include 'shine-server-crypto'
diff --git a/shine-server-blockchain/build.gradle b/shine-server-blockchain/build.gradle
index 8d43014..6f4eff7 100644
--- a/shine-server-blockchain/build.gradle
+++ b/shine-server-blockchain/build.gradle
@@ -25,6 +25,8 @@ dependencies {
implementation "org.slf4j:slf4j-api:2.0.16" // вызов логгера
testImplementation 'org.junit.jupiter:junit-jupiter:5.11.0'
+
+ implementation project(":shine-server-log") // модуль логирования и уведомления админов
implementation project(':shine-server-db') // модуль для работы с БД содержит и сущности из БД и саму работу с БД
}
diff --git a/shine-server-blockchain/src/main/java/utils/files/FileStoreUtilSelfTest.java b/shine-server-blockchain/src/main/java/utils/files/FileStoreUtilSelfTest.java
deleted file mode 100644
index 6dd0127..0000000
--- a/shine-server-blockchain/src/main/java/utils/files/FileStoreUtilSelfTest.java
+++ /dev/null
@@ -1,59 +0,0 @@
-//package utils.files;
-//
-//import java.nio.charset.StandardCharsets;
-//import java.util.Arrays;
-//
-///**
-// * ===============================================================
-// * FileStoreUtilSelfTest — запускаемый тест утилиты FileStoreUtil.
-// * ---------------------------------------------------------------
-// * Сценарий:
-// * 1) Создаём «блокчейн-файл» для id=20251021 с начальными данными.
-// * 2) Дозаписываем ещё порцию данных.
-// * 3) Читаем целиком и печатаем длину + превью.
-// *.
-// * Ожидаемый итог:
-// * • В папке "data" появится файл "20251021.bch"
-// * • В консоли будет длина содержимого и небольшой превью-дамп.
-// * ===============================================================
-// */
-//public class FileStoreUtilSelfTest {
-//
-// public static void main(String[] args) {
-// System.out.println("=== FileStoreUtil self-test ===");
-//
-// FileStoreUtil fs = FileStoreUtil.getInstance();
-//
-// long blockchainId = 20251021L;
-//
-// byte[] part1 = "Hello ".getBytes(StandardCharsets.UTF_8);
-// byte[] part2 = "Blockchain!".getBytes(StandardCharsets.UTF_8);
-//
-// // 1) создаём новый файл для «блокчейна»
-// fs.newBlockchain(blockchainId, part1);
-//
-// // 2) дозаписываем данные
-// fs.addDataToBlockchain(blockchainId, part2);
-//
-// // 3) читаем всё содержимое и показываем превью
-// byte[] all = fs.readAllDataFromBlockchain(blockchainId);
-// System.out.println("Total bytes read: " + all.length);
-// System.out.println("Preview (UTF-8): " + new String(all, StandardCharsets.UTF_8));
-//
-// // небольшой hex-дамп первых 32 байт (для наглядности)
-// System.out.println("Preview (HEX 32B): " + toHexPreview(all, 32));
-//
-// System.out.println("✅ Self-test passed (файл: data/" + blockchainId + FileStoreUtil.BLOCKCHAIN_FILE_EXTENSION + ")");
-// }
-//
-// private static String toHexPreview(byte[] data, int max) {
-// int n = Math.min(data.length, max);
-// StringBuilder sb = new StringBuilder(n * 2);
-// for (int i = 0; i < n; i++) {
-// sb.append(String.format("%02X", data[i]));
-// if (i + 1 < n) sb.append(' ');
-// }
-// if (data.length > n) sb.append(" ...");
-// return sb.toString();
-// }
-//}
diff --git a/shine-server-log/build.gradle b/shine-server-log/build.gradle
new file mode 100644
index 0000000..2e0c080
--- /dev/null
+++ b/shine-server-log/build.gradle
@@ -0,0 +1,27 @@
+plugins {
+ id 'java'
+}
+
+group = 'shine'
+version = '1.0.0'
+
+java {
+ toolchain {
+ languageVersion = JavaLanguageVersion.of(17)
+ }
+}
+
+repositories {
+ mavenCentral()
+}
+
+dependencies {
+
+ implementation "org.slf4j:slf4j-api:2.0.16" // вызов логгера
+
+}
+
+java {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+}
diff --git a/src/main/java/server/ws/BlockchainAdminNotifier.java b/shine-server-log/src/main/java/shine/log/BlockchainAdminNotifier.java
similarity index 79%
rename from src/main/java/server/ws/BlockchainAdminNotifier.java
rename to shine-server-log/src/main/java/shine/log/BlockchainAdminNotifier.java
index 4755cd2..9697f49 100644
--- a/src/main/java/server/ws/BlockchainAdminNotifier.java
+++ b/shine-server-log/src/main/java/shine/log/BlockchainAdminNotifier.java
@@ -1,4 +1,4 @@
-package server.ws;
+package shine.log;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -12,7 +12,7 @@ import org.slf4j.LoggerFactory;
* - пишет МАКСИМАЛЬНО ЗАМЕТНЫЙ лог
*
* TODO:
- * - отправка уведомления администратору:
+ * - реальная отправка уведомления администратору:
* * Telegram bot / email / SMS / webhook / Sentry / PagerDuty
* * желательно с hostname, временем, именем блокчейна, размерами и stacktrace
* ===============================================================
@@ -23,17 +23,21 @@ public final class BlockchainAdminNotifier {
private BlockchainAdminNotifier() {}
+ public static void critical(String message) {
+ critical(message, null);
+ }
+
public static void critical(String message, Throwable t) {
String bannerTop =
"\n" +
"=================================================================\n" +
- "==================== !!! CRITICAL ALERT !!! ===================\n" +
+ "==================== !!! КРИТИЧЕСКАЯ ОШИБКА !!! ===============\n" +
"=================================================================";
String bannerBottom =
"=================================================================\n" +
- "==================== !!! ACTION REQUIRED !!! ===================\n" +
+ "==================== !!! НУЖНО ВМЕШАТЕЛЬСТВО !!! ===============\n" +
"=================================================================\n";
if (t == null) {
@@ -51,6 +55,6 @@ public final class BlockchainAdminNotifier {
);
}
- // TODO: Реальная отправка уведомления администратору (telegram/email/webhook/sentry)
+ // TODO: Реальная отправка уведомления администратору (Telegram/email/webhook/Sentry)
}
}
\ No newline at end of file
diff --git a/shine-server-net-protocol/build.gradle b/shine-server-net-protocol/build.gradle
index bb8d2f9..6038c67 100644
--- a/shine-server-net-protocol/build.gradle
+++ b/shine-server-net-protocol/build.gradle
@@ -24,7 +24,9 @@ dependencies {
implementation "org.slf4j:slf4j-api:2.0.16" // вызов логгера
- implementation project(':shine-server-config') // модуль с настройками
+ implementation project(':shine-server-config') // модуль с настройками
+ implementation project(":shine-server-log") // модуль логирования и уведомления админов
+
implementation project(':shine-server-crypto') // модуль сервера для работы с криптографией
implementation project(':shine-server-blockchain') // модуль для работы с блокчейном
implementation project(':shine-server-db') // модуль для работы с БД содержит и сущности из БД и саму работу с БД
diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/BlockchainWriter.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/BlockchainWriter.java
index db4a853..39deca7 100644
--- a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/BlockchainWriter.java
+++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/BlockchainWriter.java
@@ -9,6 +9,7 @@ import shine.db.dao.BlocksDAO;
import shine.db.entities.BlockEntry;
import shine.db.entities.BlockchainStateEntry;
import utils.files.FileStoreUtil;
+import shine.log.BlockchainAdminNotifier;
import java.sql.Connection;
import java.sql.SQLException;
@@ -26,7 +27,12 @@ import java.sql.SQLException;
* Важно:
* - Шаг (2) — строго атомарный (SQL tx).
* - Шаг (3) — атомарный на уровне ФС, если поддерживается ATOMIC_MOVE.
- * - Если сервер упадёт между (2) и (3), останется tmp — твой recovery при старте починит.
+ *
+ * ДОПОЛНЕНИЕ (КРИТИЧНО):
+ * - Перед тем как дописывать блок, проверяем:
+ * реальный размер .bch == st.fileSizeBytes.
+ * Если не совпадает — считаем это критической внешней порчей файлов,
+ * шлём уведомление админу и НЕ продолжаем запись.
*/
public final class BlockchainWriter {
@@ -46,6 +52,7 @@ public final class BlockchainWriter {
/**
* Главный метод:
+ * - (0) проверяет соответствие размера файла и state (если это не genesis)
* - создаёт tmp-файл (старое+новое),
* - атомарно коммитит БД (block+state),
* - атомарно заменяет основной файл.
@@ -59,6 +66,15 @@ public final class BlockchainWriter {
String newHashHex
) throws SQLException {
+ // =====================================================================
+ // ШАГ 0. КРИТИЧЕСКАЯ ПРОВЕРКА КОНСИСТЕНТНОСТИ:
+ // - если state есть и ожидает ненулевой размер,
+ // то основной файл должен существовать и иметь точно этот размер.
+ // - если не так — это почти наверняка внешнее вмешательство/порча,
+ // и продолжать запись НЕЛЬЗЯ.
+ // =====================================================================
+ verifyMainFileSizeMatchesStateOrAlert(login, blockchainName, block, stOrNull);
+
// =====================================================================
// ШАГ 1. Готовим bytes нового блока (включая signature+hash)
// =====================================================================
@@ -90,19 +106,22 @@ public final class BlockchainWriter {
try {
oldBytes = fs.readBlockchain(blockchainName);
} catch (Exception e) {
- // ✅ Добавили подробный лог: это очень важная точка
log.error("Ошибка чтения старого файла блокчейна перед записью tmp (login={}, blockchainName={}, oldFileSize={}, blockNumber={})",
login, blockchainName, oldFileSize, block.recordNumber, e);
-
- // Здесь лучше падать: state говорит, что файл есть, а прочитать нельзя.
throw new SQLException("Cannot read old blockchain file for: " + blockchainName, e);
}
- // (на будущее) можно проверять согласованность: oldBytes.length == oldFileSize
- // но ты всё равно будешь делать recovery при старте — оставим как подсказку.
+ // (в идеале это всегда должно совпадать после verifyMainFileSizeMatchesStateOrAlert)
if (oldBytes.length != (int) oldFileSize) {
- log.warn("Несовпадение размера файла блокчейна: state говорит oldFileSize={}, а реально прочитали oldBytes.length={} (login={}, blockchainName={}, blockNumber={})",
- oldFileSize, oldBytes.length, login, blockchainName, block.recordNumber);
+ String msg =
+ "Несовпадение размера файла блокчейна при чтении: " +
+ "state ожидал oldFileSize=" + oldFileSize +
+ ", а реально прочитали oldBytes.length=" + oldBytes.length +
+ " (login=" + login +
+ ", blockchainName=" + blockchainName +
+ ", blockNumber=" + block.recordNumber + ").";
+ BlockchainAdminNotifier.critical(msg, null);
+ throw new SQLException(msg);
}
tmpBytes = concat(oldBytes, newBlockFullBytes);
@@ -113,7 +132,6 @@ public final class BlockchainWriter {
try {
fs.writeBlockchainTmp(blockchainName, tmpBytes);
} catch (Exception e) {
- // ✅ Добавили подробный лог: это тоже критично
log.error("Ошибка записи tmp файла блокчейна (login={}, blockchainName={}, tmpBytesLen={}, oldFileSize={}, newFileSize={}, blockNumber={})",
login, blockchainName, tmpBytes.length, oldFileSize, newFileSize, block.recordNumber, e);
throw new SQLException("Cannot write tmp blockchain file for: " + blockchainName, e);
@@ -145,7 +163,6 @@ public final class BlockchainWriter {
} catch (Exception e) {
try { c.rollback(); } catch (SQLException ignore) {}
- // ✅ ВАЖНО: логируем причину отката + контекст
log.error("Ошибка транзакции БД при добавлении блока (rollback выполнен) (login={}, blockchainName={}, blockNumber={}, prevHash={}, newHash={}, oldFileSize={}, newFileSize={})",
login, blockchainName, block.recordNumber, prevGlobalHashHex, newHashHex, oldFileSize, newFileSize, e);
@@ -159,23 +176,14 @@ public final class BlockchainWriter {
// =================================================================
// ШАГ 5. После успешного коммита БД — атомарно заменяем файл:
// .tmp_bch -> .bch
- //
- // Если тут упадём:
- // - БД уже обновлена
- // - tmp остаётся
- // - recovery при старте восстановит консистентность
// =================================================================
if (committed) {
try {
fs.atomicReplaceBlockchainFile(blockchainName);
} catch (Exception moveError) {
- // ✅ Очень важная ситуация: БД уже committed, а файл не заменился
log.error("БД закоммичена, но атомарная замена файла блокчейна не удалась. tmp оставлен для recovery. (login={}, blockchainName={}, blockNumber={}, newHash={}, tmpBytesLen={})",
login, blockchainName, block.recordNumber, newHashHex, tmpBytes.length, moveError);
- // Здесь ВАЖНО: мы уже не можем откатить БД.
- // Оставляем tmp и даём наверх ошибку — клиент увидит internal_error,
- // а ты при старте починишь файловую часть.
throw new SQLException(
"DB committed but file replace failed; tmp kept for recovery. blockchainName=" + blockchainName,
moveError
@@ -185,6 +193,73 @@ public final class BlockchainWriter {
}
}
+ /**
+ * Проверка: реальный размер .bch должен совпадать с st.fileSizeBytes.
+ * Если нет — это критическая внешняя порча/вмешательство, уведомляем админа и падаем.
+ */
+ private void verifyMainFileSizeMatchesStateOrAlert(
+ String login,
+ String blockchainName,
+ BchBlockEntry block,
+ BlockchainStateEntry stOrNull
+ ) throws SQLException {
+
+ if (stOrNull == null) {
+ // genesis — state ещё нет, проверять нечего
+ return;
+ }
+
+ long expected = stOrNull.getFileSizeBytes();
+ if (expected <= 0) {
+ // state есть, но ожидаемый размер 0 — это либо пустая цепочка, либо старый формат.
+ // Здесь не трогаем (но можно усилить правила позже).
+ return;
+ }
+
+ String mainFileName = fs.buildBlockchainFileName(blockchainName);
+
+ // Если файла нет — это уже очень подозрительно: state говорит “файл есть и размер > 0”
+ if (!fs.exists(mainFileName)) {
+ String msg =
+ "КРИТИЧЕСКАЯ ОШИБКА КОНСИСТЕНТНОСТИ: state ожидает основной файл, но его нет. " +
+ "login=" + login +
+ ", blockchainName=" + blockchainName +
+ ", expectedSizeFromState=" + expected +
+ ", blockNumber=" + (block != null ? block.recordNumber : -1) + ".";
+
+ BlockchainAdminNotifier.critical(msg, null);
+ throw new SQLException(msg);
+ }
+
+ long real;
+ try {
+ real = fs.size(mainFileName);
+ } catch (Exception e) {
+ String msg =
+ "КРИТИЧЕСКАЯ ОШИБКА: не удалось получить размер основного файла блокчейна. " +
+ "login=" + login +
+ ", blockchainName=" + blockchainName +
+ ", expectedSizeFromState=" + expected +
+ ", blockNumber=" + (block != null ? block.recordNumber : -1) + ".";
+ BlockchainAdminNotifier.critical(msg, e);
+ throw new SQLException(msg, e);
+ }
+
+ if (real != expected) {
+ String msg =
+ "КРИТИЧЕСКАЯ ОШИБКА КОНСИСТЕНТНОСТИ: размер файла блокчейна НЕ СОВПАДАЕТ с state. " +
+ "login=" + login +
+ ", blockchainName=" + blockchainName +
+ ", expectedSizeFromState=" + expected +
+ ", realMainFileSize=" + real +
+ ", blockNumber=" + (block != null ? block.recordNumber : -1) + ". " +
+ "Похоже на внешнее вмешательство/порчу файла. Запись нового блока остановлена.";
+
+ BlockchainAdminNotifier.critical(msg, null);
+ throw new SQLException(msg);
+ }
+ }
+
/**
* Обновление состояния blockchain_state (создаём если отсутствует).
* Пока линии не используются: lineIndex=0 и lineHash = globalHash.
@@ -274,7 +349,6 @@ public final class BlockchainWriter {
}
private static long safeAdd(long x, long y) {
- // защита от переполнения long (маловероятно, но пусть будет)
long r = x + y;
if (((x ^ r) & (y ^ r)) < 0) {
throw new IllegalArgumentException("fileSizeBytes overflow: " + x + " + " + y);
diff --git a/src/main/java/server/ws/BlockchainTmpRecoveryOnStartup.java b/src/main/java/server/ws/BlockchainTmpRecoveryOnStartup.java
index 3c2eae9..5a2bd92 100644
--- a/src/main/java/server/ws/BlockchainTmpRecoveryOnStartup.java
+++ b/src/main/java/server/ws/BlockchainTmpRecoveryOnStartup.java
@@ -5,6 +5,7 @@ import org.slf4j.LoggerFactory;
import shine.db.dao.BlockchainStateDAO;
import shine.db.entities.BlockchainStateEntry;
import utils.files.FileStoreUtil;
+import shine.log.BlockchainAdminNotifier;
import java.io.IOException;
import java.nio.file.*;