Дорабатываю добавление блоков.
Убрал лишние старые классы
This commit is contained in:
AidarKC 2025-12-24 17:11:29 +03:00
parent 4759521176
commit 4e14f300f9
5 changed files with 311 additions and 334 deletions

View File

@ -1,62 +1,62 @@
package utils.blockchain; //package utils.blockchain;
//
import com.fasterxml.jackson.annotation.JsonCreator; //import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty; //import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.Base64; //import java.util.Base64;
//
/** ///**
* BchInfoEntry данные об одной цепочке блокчейна. // * BchInfoEntry данные об одной цепочке блокчейна.
* Используется менеджером BchInfoManager. // * Используется менеджером BchInfoManager.
*/ // */
public final class BchInfoEntry { //public final class BchInfoEntry {
//
@JsonProperty("blockchainId") // @JsonProperty("blockchainId")
public final long blockchainId; // public final long blockchainId;
//
@JsonProperty("userLogin") // @JsonProperty("userLogin")
public final String userLogin; // public final String userLogin;
//
@JsonProperty("publicKeyBase64") // @JsonProperty("publicKeyBase64")
public final String publicKeyBase64; // public final String publicKeyBase64;
//
@JsonProperty("blockchainSizeLimit") // @JsonProperty("blockchainSizeLimit")
public final int blockchainSizeLimit; // public final int blockchainSizeLimit;
//
@JsonProperty("blockchainSize") // @JsonProperty("blockchainSize")
public final int blockchainSize; // public final int blockchainSize;
//
@JsonProperty("lastBlockNumber") // @JsonProperty("lastBlockNumber")
public final int lastBlockNumber; // public final int lastBlockNumber;
//
@JsonProperty("lastBlockHash") // @JsonProperty("lastBlockHash")
public final String lastBlockHash; // public final String lastBlockHash;
//
@JsonCreator // @JsonCreator
public BchInfoEntry( // public BchInfoEntry(
@JsonProperty("blockchainId") long blockchainId, // @JsonProperty("blockchainId") long blockchainId,
@JsonProperty("userLogin") String userLogin, // @JsonProperty("userLogin") String userLogin,
@JsonProperty("publicKeyBase64") String publicKeyBase64, // @JsonProperty("publicKeyBase64") String publicKeyBase64,
@JsonProperty("blockchainSizeLimit") int blockchainSizeLimit, // @JsonProperty("blockchainSizeLimit") int blockchainSizeLimit,
@JsonProperty("blockchainSize") int blockchainSize, // @JsonProperty("blockchainSize") int blockchainSize,
@JsonProperty("lastBlockNumber") int lastBlockNumber, // @JsonProperty("lastBlockNumber") int lastBlockNumber,
@JsonProperty("lastBlockHash") String lastBlockHash // @JsonProperty("lastBlockHash") String lastBlockHash
) { // ) {
this.blockchainId = blockchainId; // this.blockchainId = blockchainId;
this.userLogin = userLogin == null ? "" : userLogin; // this.userLogin = userLogin == null ? "" : userLogin;
this.publicKeyBase64 = publicKeyBase64; // this.publicKeyBase64 = publicKeyBase64;
this.blockchainSizeLimit = blockchainSizeLimit; // this.blockchainSizeLimit = blockchainSizeLimit;
this.blockchainSize = blockchainSize; // this.blockchainSize = blockchainSize;
this.lastBlockNumber = lastBlockNumber; // this.lastBlockNumber = lastBlockNumber;
this.lastBlockHash = lastBlockHash == null ? "" : lastBlockHash; // this.lastBlockHash = lastBlockHash == null ? "" : lastBlockHash;
} // }
//
/** Публичный ключ в бинарном виде (32 байта) или null, если битый. */ // /** Публичный ключ в бинарном виде (32 байта) или null, если битый. */
public byte[] getPublicKey32() { // public byte[] getPublicKey32() {
try { // try {
byte[] raw = Base64.getDecoder().decode(publicKeyBase64); // byte[] raw = Base64.getDecoder().decode(publicKeyBase64);
return (raw != null && raw.length == 32) ? raw : null; // return (raw != null && raw.length == 32) ? raw : null;
} catch (IllegalArgumentException e) { // } catch (IllegalArgumentException e) {
return null; // return null;
} // }
} // }
} //}

View File

@ -1,178 +1,178 @@
package utils.blockchain; //package utils.blockchain;
//
import com.fasterxml.jackson.databind.ObjectMapper; //import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger; //import org.slf4j.Logger;
import org.slf4j.LoggerFactory; //import org.slf4j.LoggerFactory;
//
import java.io.IOException; //import java.io.IOException;
import java.nio.file.*; //import java.nio.file.*;
import java.util.Base64; //import java.util.Base64;
import java.util.LinkedHashMap; //import java.util.LinkedHashMap;
import java.util.Map; //import java.util.Map;
//
/** ///**
* BchInfoManager Singleton. // * BchInfoManager Singleton.
*. // *.
* Держит в памяти информацию обо всех блокчейнах. // * Держит в памяти информацию обо всех блокчейнах.
* Сейчас читает и пишет JSON на диск (data/blockchain_info.json). // * Сейчас читает и пишет JSON на диск (data/blockchain_info.json).
* В будущем можно заменить на SQL без изменений в остальном коде. // * В будущем можно заменить на SQL без изменений в остальном коде.
*/ // */
public final class BchInfoManager { //public final class BchInfoManager {
//
private static final Logger log = LoggerFactory.getLogger(BchInfoManager.class); // private static final Logger log = LoggerFactory.getLogger(BchInfoManager.class);
//
private static final String FILE_NAME = "blockchain_info.json"; // private static final String FILE_NAME = "blockchain_info.json";
private static final Path DATA_DIR = Paths.get("data"); // private static final Path DATA_DIR = Paths.get("data");
private static final ObjectMapper MAPPER = new ObjectMapper(); // private static final ObjectMapper MAPPER = new ObjectMapper();
//
private static BchInfoManager instance; // private static BchInfoManager instance;
//
/** blockchainId → запись о цепочке */ // /** blockchainId → запись о цепочке */
private final Map<Long, BchInfoEntry> records = new LinkedHashMap<>(); // private final Map<Long, BchInfoEntry> records = new LinkedHashMap<>();
private final Path path = DATA_DIR.resolve(FILE_NAME); // private final Path path = DATA_DIR.resolve(FILE_NAME);
//
private BchInfoManager() { // private BchInfoManager() {
ensureDataDir(); // ensureDataDir();
load(); // load();
} // }
//
public static synchronized BchInfoManager getInstance() { // public static synchronized BchInfoManager getInstance() {
if (instance == null) instance = new BchInfoManager(); // if (instance == null) instance = new BchInfoManager();
return instance; // return instance;
} // }
//
// ========== API ========== // // ========== API ==========
//
/** Создать новую цепочку (после первого HEADER-блока). */ // /** Создать новую цепочку (после первого HEADER-блока). */
public synchronized void addBlockchain(long blockchainId, // public synchronized void addBlockchain(long blockchainId,
String userLogin, // String userLogin,
byte[] publicKey32, // byte[] publicKey32,
int blockchainSizeLimit) { // int blockchainSizeLimit) {
if (publicKey32 == null || publicKey32.length != 32) // if (publicKey32 == null || publicKey32.length != 32)
throw new IllegalArgumentException("publicKey32 must be 32 bytes"); // throw new IllegalArgumentException("publicKey32 must be 32 bytes");
if (records.containsKey(blockchainId)) // if (records.containsKey(blockchainId))
throw new IllegalArgumentException("blockchain already exists: " + blockchainId); // throw new IllegalArgumentException("blockchain already exists: " + blockchainId);
//
BchInfoEntry entry = new BchInfoEntry( // BchInfoEntry entry = new BchInfoEntry(
blockchainId, // blockchainId,
userLogin, // userLogin,
Base64.getEncoder().encodeToString(publicKey32), // Base64.getEncoder().encodeToString(publicKey32),
blockchainSizeLimit, // blockchainSizeLimit,
0, 0, "" // 0, 0, ""
); // );
//
records.put(blockchainId, entry); // records.put(blockchainId, entry);
log.info("Добавлен блокчейн id={} login='{}' (лимит {})", blockchainId, userLogin, blockchainSizeLimit); // log.info("Добавлен блокчейн id={} login='{}' (лимит {})", blockchainId, userLogin, blockchainSizeLimit);
save(); // save();
} // }
//
/** Обновить состояние после добавления нового блока. */ // /** Обновить состояние после добавления нового блока. */
public synchronized void updateBlockchainState(long blockchainId, // public synchronized void updateBlockchainState(long blockchainId,
int lastBlockNumber, // int lastBlockNumber,
String lastBlockHash, // String lastBlockHash,
int blockchainSize) { // int blockchainSize) {
BchInfoEntry prev = getEntryOrThrow(blockchainId); // BchInfoEntry prev = getEntryOrThrow(blockchainId);
//
BchInfoEntry updated = new BchInfoEntry( // BchInfoEntry updated = new BchInfoEntry(
prev.blockchainId, // prev.blockchainId,
prev.userLogin, // prev.userLogin,
prev.publicKeyBase64, // prev.publicKeyBase64,
prev.blockchainSizeLimit, // prev.blockchainSizeLimit,
blockchainSize, // blockchainSize,
lastBlockNumber, // lastBlockNumber,
lastBlockHash // lastBlockHash
); // );
//
records.put(blockchainId, updated); // records.put(blockchainId, updated);
log.info("Обновлено состояние id={} lastNum={} hash={} size={}", // log.info("Обновлено состояние id={} lastNum={} hash={} size={}",
blockchainId, lastBlockNumber, lastBlockHash, blockchainSize); // blockchainId, lastBlockNumber, lastBlockHash, blockchainSize);
save(); // save();
} // }
//
/** Получить полную информацию по blockchainId. */ // /** Получить полную информацию по blockchainId. */
public synchronized BchInfoEntry getBchInfo(long blockchainId) { // public synchronized BchInfoEntry getBchInfo(long blockchainId) {
return records.get(blockchainId); // return records.get(blockchainId);
} // }
//
/** Быстро проверить существование цепочки. */ // /** Быстро проверить существование цепочки. */
public synchronized boolean exists(long blockchainId) { // public synchronized boolean exists(long blockchainId) {
return records.containsKey(blockchainId); // return records.containsKey(blockchainId);
} // }
//
/** id → userLogin (для поиска пользователей). */ // /** id → userLogin (для поиска пользователей). */
public synchronized Map<Long, String> getAllLoginsSnapshot() { // public synchronized Map<Long, String> getAllLoginsSnapshot() {
Map<Long, String> copy = new LinkedHashMap<>(records.size()); // Map<Long, String> copy = new LinkedHashMap<>(records.size());
for (var e : records.entrySet()) { // for (var e : records.entrySet()) {
copy.put(e.getKey(), e.getValue().userLogin); // copy.put(e.getKey(), e.getValue().userLogin);
} // }
return copy; // return copy;
} // }
//
// ========== private ========== // // ========== private ==========
//
private BchInfoEntry getEntryOrThrow(long blockchainId) { // private BchInfoEntry getEntryOrThrow(long blockchainId) {
BchInfoEntry e = records.get(blockchainId); // BchInfoEntry e = records.get(blockchainId);
if (e == null) throw new IllegalStateException("Блокчейн с id=" + blockchainId + " не найден."); // if (e == null) throw new IllegalStateException("Блокчейн с id=" + blockchainId + " не найден.");
return e; // return e;
} // }
//
private void ensureDataDir() { // private void ensureDataDir() {
try { // try {
if (!Files.exists(DATA_DIR)) { // if (!Files.exists(DATA_DIR)) {
Files.createDirectories(DATA_DIR); // Files.createDirectories(DATA_DIR);
log.info("Создана директория данных: {}", DATA_DIR); // log.info("Создана директория данных: {}", DATA_DIR);
} // }
} catch (IOException e) { // } catch (IOException e) {
throw new IllegalStateException("Не удалось создать директорию хранения: " + DATA_DIR, e); // throw new IllegalStateException("Не удалось создать директорию хранения: " + DATA_DIR, e);
} // }
} // }
//
private synchronized void load() { // private synchronized void load() {
if (!Files.exists(path)) { // if (!Files.exists(path)) {
save(); // save();
log.info("Создан JSON-хранилище: {}", path); // log.info("Создан JSON-хранилище: {}", path);
return; // return;
} // }
try { // try {
byte[] json = Files.readAllBytes(path); // byte[] json = Files.readAllBytes(path);
if (json.length == 0) return; // if (json.length == 0) return;
//
Map<String, BchInfoEntry> map = MAPPER.readValue( // Map<String, BchInfoEntry> map = MAPPER.readValue(
json, // json,
MAPPER.getTypeFactory().constructMapType(Map.class, String.class, BchInfoEntry.class) // MAPPER.getTypeFactory().constructMapType(Map.class, String.class, BchInfoEntry.class)
); // );
//
records.clear(); // records.clear();
for (var e : map.entrySet()) { // for (var e : map.entrySet()) {
try { // try {
long id = Long.parseLong(e.getKey()); // long id = Long.parseLong(e.getKey());
records.put(id, e.getValue()); // records.put(id, e.getValue());
} catch (NumberFormatException nfe) { // } catch (NumberFormatException nfe) {
log.warn("Пропущен некорректный ключ '{}' в JSON", e.getKey()); // log.warn("Пропущен некорректный ключ '{}' в JSON", e.getKey());
} // }
} // }
log.info("Загружено {} записей из {}", records.size(), path); // log.info("Загружено {} записей из {}", records.size(), path);
} catch (IOException e) { // } catch (IOException e) {
log.error("Ошибка загрузки {}", path, e); // log.error("Ошибка загрузки {}", path, e);
} // }
} // }
//
/** Атомарная запись JSON через .tmp + ATOMIC_MOVE */ // /** Атомарная запись JSON через .tmp + ATOMIC_MOVE */
private synchronized void save() { // private synchronized void save() {
try { // try {
Map<String, BchInfoEntry> map = new LinkedHashMap<>(); // Map<String, BchInfoEntry> map = new LinkedHashMap<>();
for (var e : records.entrySet()) // for (var e : records.entrySet())
map.put(String.valueOf(e.getKey()), e.getValue()); // map.put(String.valueOf(e.getKey()), e.getValue());
//
byte[] json = MAPPER.writerWithDefaultPrettyPrinter().writeValueAsBytes(map); // byte[] json = MAPPER.writerWithDefaultPrettyPrinter().writeValueAsBytes(map);
//
Path tmp = path.resolveSibling(FILE_NAME + ".tmp"); // Path tmp = path.resolveSibling(FILE_NAME + ".tmp");
Files.write(tmp, json, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); // Files.write(tmp, json, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
Files.move(tmp, path, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); // Files.move(tmp, path, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
//
log.debug("Сохранено {} записей в {}", records.size(), path); // log.debug("Сохранено {} записей в {}", records.size(), path);
} catch (IOException e) { // } catch (IOException e) {
log.error("Ошибка сохранения {}", path, e); // log.error("Ошибка сохранения {}", path, e);
} // }
} // }
} //}

View File

@ -3,6 +3,7 @@ package utils.blockchain;
public final class BlockchainNameUtil { public final class BlockchainNameUtil {
/** Сколько символов отрезаем с конца blockchainName, чтобы получить login. */ /** Сколько символов отрезаем с конца blockchainName, чтобы получить login. */
/** Теперь новое правило везде использовать только 3 символа номера блокчена в конце его имени */
public static final int BLOCKCHAIN_NAME_LOGIN_SUFFIX_LEN = 3; public static final int BLOCKCHAIN_NAME_LOGIN_SUFFIX_LEN = 3;
private BlockchainNameUtil() {} private BlockchainNameUtil() {}

View File

@ -1,24 +0,0 @@
# TODO
1. Проверка перед созданием нового блокчейна
Сейчас:
- Метод `BlockchainIdInfo.addBlockchain(...)` просто добавляет новую цепочку (blockchainId + userLogin + publicKey32) в локальное хранилище.
- Любой первый блок (HEADER, recordNumber = 0) с валидной подписью автоматически создаёт новую запись.
Что нужно сделать в будущем:
- Перед созданием новой цепочки проверять, что этот пользователь реально зарегистрирован в системе и имеет право открыть блокчейн.
- Проверять, что `userLogin` и `publicKey32` совпадают с тем, что у нас уже привязано к этому пользователю.
- Если пользователь не найден или ключ не совпадает — отказ, цепочку не создавать.
Идея: `handleAddBlock(...)` должен вызывать будущий Auth/Users сервис до `addBlockchain(...)`.
2. Перенос хранения из файлов в базу SQL
Сейчас:
- Блоки пишутся в файл `data/<blockchainId>.bch` через `FileStoreUtil`.
- Метаданные по цепочке (логин, публичный ключ, последний номер блока, последний hash, размер и т.д.) хранятся в `data/blockchain_id_info.json` через `BlockchainIdInfo`.
Что нужно сделать:
- Убрать файловое хранение и перейти на SQL.

View File

@ -1,70 +1,70 @@
package utils.search; //package utils.search;
//
//
import utils.blockchain.BchInfoManager; //import utils.blockchain.BchInfoManager;
//
import java.nio.charset.StandardCharsets; //import java.nio.charset.StandardCharsets;
import java.util.ArrayList; //import java.util.ArrayList;
import java.util.List; //import java.util.List;
import java.util.Locale; //import java.util.Locale;
import java.util.Map; //import java.util.Map;
//
/** ///**
* UserSearchService поиск первых 5 пользователей по подстроке логина (без учёта регистра). // * UserSearchService поиск первых 5 пользователей по подстроке логина (без учёта регистра).
*/ // */
public final class UserSearchService { //public final class UserSearchService {
//
private static final UserSearchService INSTANCE = new UserSearchService(); // private static final UserSearchService INSTANCE = new UserSearchService();
private UserSearchService() {} // private UserSearchService() {}
public static UserSearchService getInstance() { return INSTANCE; } // public static UserSearchService getInstance() { return INSTANCE; }
//
/** Результат одной пары: id + исходный login (с родным регистром). */ // /** Результат одной пары: id + исходный login (с родным регистром). */
public static final class Pair { // public static final class Pair {
public final long blockchainId; // public final long blockchainId;
public final String userLogin; // public final String userLogin;
public Pair(long blockchainId, String userLogin) { // public Pair(long blockchainId, String userLogin) {
this.blockchainId = blockchainId; // this.blockchainId = blockchainId;
this.userLogin = userLogin; // this.userLogin = userLogin;
} // }
} // }
//
/** // /**
* Найти первые до 5 логинов, содержащих подстроку (case-insensitive). // * Найти первые до 5 логинов, содержащих подстроку (case-insensitive).
*/ // */
public List<Pair> searchFirst5(String query) { // public List<Pair> searchFirst5(String query) {
String q = (query == null ? "" : query).toLowerCase(Locale.ROOT).trim(); // String q = (query == null ? "" : query).toLowerCase(Locale.ROOT).trim();
List<Pair> out = new ArrayList<>(5); // List<Pair> out = new ArrayList<>(5);
if (q.isEmpty()) return out; // if (q.isEmpty()) return out;
//
// берём снапшот idlogin // // берём снапшот idlogin
Map<Long, String> all = BchInfoManager.getInstance().getAllLoginsSnapshot(); // Map<Long, String> all = BchInfoManager.getInstance().getAllLoginsSnapshot();
//
for (var e : all.entrySet()) { // for (var e : all.entrySet()) {
if (out.size() >= 5) break; // if (out.size() >= 5) break;
String login = e.getValue() == null ? "" : e.getValue(); // String login = e.getValue() == null ? "" : e.getValue();
if (login.toLowerCase(Locale.ROOT).contains(q)) { // if (login.toLowerCase(Locale.ROOT).contains(q)) {
out.add(new Pair(e.getKey(), login)); // out.add(new Pair(e.getKey(), login));
} // }
} // }
return out; // return out;
} // }
//
// Упаковка пары в байтовый формат ответа: [8] id + [1] L + [L] login UTF-8 (L<=255) // // Упаковка пары в байтовый формат ответа: [8] id + [1] L + [L] login UTF-8 (L<=255)
public static byte[] packPair(Pair p) { // public static byte[] packPair(Pair p) {
byte[] loginUtf8 = (p.userLogin == null ? "" : p.userLogin).getBytes(StandardCharsets.UTF_8); // byte[] loginUtf8 = (p.userLogin == null ? "" : p.userLogin).getBytes(StandardCharsets.UTF_8);
int L = Math.min(loginUtf8.length, 255); // int L = Math.min(loginUtf8.length, 255);
byte[] b = new byte[8 + 1 + L]; // byte[] b = new byte[8 + 1 + L];
// beLong // // beLong
b[0]=(byte)((p.blockchainId>>>56)&0xFF); // b[0]=(byte)((p.blockchainId>>>56)&0xFF);
b[1]=(byte)((p.blockchainId>>>48)&0xFF); // b[1]=(byte)((p.blockchainId>>>48)&0xFF);
b[2]=(byte)((p.blockchainId>>>40)&0xFF); // b[2]=(byte)((p.blockchainId>>>40)&0xFF);
b[3]=(byte)((p.blockchainId>>>32)&0xFF); // b[3]=(byte)((p.blockchainId>>>32)&0xFF);
b[4]=(byte)((p.blockchainId>>>24)&0xFF); // b[4]=(byte)((p.blockchainId>>>24)&0xFF);
b[5]=(byte)((p.blockchainId>>>16)&0xFF); // b[5]=(byte)((p.blockchainId>>>16)&0xFF);
b[6]=(byte)((p.blockchainId>>>8 )&0xFF); // b[6]=(byte)((p.blockchainId>>>8 )&0xFF);
b[7]=(byte)((p.blockchainId )&0xFF); // b[7]=(byte)((p.blockchainId )&0xFF);
b[8]=(byte)L; // b[8]=(byte)L;
System.arraycopy(loginUtf8, 0, b, 9, L); // System.arraycopy(loginUtf8, 0, b, 9, L);
return b; // return b;
} // }
} //}