SHA256
30 12 25
Ну типо переделал Всё под короткую таблицу солана юзерс, но теперь не надо поправить баги
This commit is contained in:
@@ -81,10 +81,7 @@ public class DatabaseInitializer {
|
||||
st.executeUpdate("""
|
||||
CREATE TABLE IF NOT EXISTS solana_users (
|
||||
login TEXT NOT NULL PRIMARY KEY,
|
||||
bchName TEXT NOT NULL,
|
||||
loginKey TEXT,
|
||||
deviceKey TEXT,
|
||||
bchLimit INTEGER
|
||||
deviceKey TEXT NOT NULL
|
||||
);
|
||||
""");
|
||||
|
||||
@@ -157,7 +154,7 @@ public class DatabaseInitializer {
|
||||
CREATE TABLE IF NOT EXISTS blockchain_state (
|
||||
blockchainName TEXT NOT NULL PRIMARY KEY,
|
||||
login TEXT NOT NULL,
|
||||
public_key_base64 TEXT NOT NULL,
|
||||
blockchainKey TEXT NOT NULL,
|
||||
|
||||
size_limit INTEGER NOT NULL,
|
||||
file_size_bytes INTEGER NOT NULL,
|
||||
@@ -181,7 +178,9 @@ public class DatabaseInitializer {
|
||||
line6_last_number INTEGER NOT NULL,
|
||||
line6_last_hash TEXT NOT NULL,
|
||||
line7_last_number INTEGER NOT NULL,
|
||||
line7_last_hash TEXT NOT NULL
|
||||
line7_last_hash TEXT NOT NULL,
|
||||
|
||||
FOREIGN KEY (login) REFERENCES solana_users(login)
|
||||
);
|
||||
""");
|
||||
|
||||
@@ -216,7 +215,8 @@ public class DatabaseInitializer {
|
||||
toBlockGlobalNumber INTEGER,
|
||||
toBlockHashe TEXT,
|
||||
|
||||
FOREIGN KEY (login) REFERENCES solana_users(login)
|
||||
FOREIGN KEY (login) REFERENCES solana_users(login),
|
||||
FOREIGN KEY (bchName) REFERENCES blockchain_state(blockchainName)
|
||||
);
|
||||
""");
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ public final class BlockchainStateDAO {
|
||||
SELECT
|
||||
blockchainName,
|
||||
login,
|
||||
public_key_base64,
|
||||
blockchainKey,
|
||||
size_limit,
|
||||
file_size_bytes,
|
||||
last_global_number,
|
||||
@@ -81,7 +81,7 @@ public final class BlockchainStateDAO {
|
||||
INSERT INTO blockchain_state (
|
||||
blockchainName,
|
||||
login,
|
||||
public_key_base64,
|
||||
blockchainKey,
|
||||
size_limit,
|
||||
file_size_bytes,
|
||||
last_global_number,
|
||||
@@ -109,7 +109,7 @@ public final class BlockchainStateDAO {
|
||||
ON CONFLICT(blockchainName)
|
||||
DO UPDATE SET
|
||||
login = excluded.login,
|
||||
public_key_base64 = excluded.public_key_base64,
|
||||
blockchainKey = excluded.blockchainKey,
|
||||
size_limit = excluded.size_limit,
|
||||
file_size_bytes = excluded.file_size_bytes,
|
||||
last_global_number = excluded.last_global_number,
|
||||
@@ -138,7 +138,7 @@ public final class BlockchainStateDAO {
|
||||
|
||||
ps.setString(i++, e.getBlockchainName());
|
||||
ps.setString(i++, nn(e.getLogin()));
|
||||
ps.setString(i++, nn(e.getPublicKeyBase64()));
|
||||
ps.setString(i++, nn(e.getBlockchainKey()));
|
||||
|
||||
ps.setLong(i++, e.getSizeLimit());
|
||||
ps.setLong(i++, e.getFileSizeBytes());
|
||||
@@ -156,12 +156,55 @@ public final class BlockchainStateDAO {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Атомарно увеличить file_size_bytes на deltaBytes, но только если НЕ превысим size_limit.
|
||||
*
|
||||
* Возвращает:
|
||||
* - true если обновили (лимит не превышен)
|
||||
* - false если лимит превышается или blockchainName не найден
|
||||
*
|
||||
* ВАЖНО: это именно тот механизм, который надо дергать при добавлении блока.
|
||||
*/
|
||||
public boolean tryIncreaseFileSizeWithinLimit(Connection c, String blockchainName, long deltaBytes, long nowMs) throws SQLException {
|
||||
String sql = """
|
||||
UPDATE blockchain_state
|
||||
SET
|
||||
file_size_bytes = file_size_bytes + ?,
|
||||
updated_at_ms = ?
|
||||
WHERE
|
||||
blockchainName = ?
|
||||
AND (file_size_bytes + ?) <= size_limit
|
||||
""";
|
||||
|
||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||
ps.setLong(1, deltaBytes);
|
||||
ps.setLong(2, nowMs);
|
||||
ps.setString(3, blockchainName);
|
||||
ps.setLong(4, deltaBytes);
|
||||
int updated = ps.executeUpdate();
|
||||
return updated > 0;
|
||||
}
|
||||
}
|
||||
|
||||
/** Удобная проверка для HEADER: запись должна быть и last_global_number должен быть -1. */
|
||||
public BlockchainStateEntry requireExistingAtGenesis(Connection c, String blockchainName) throws SQLException {
|
||||
BlockchainStateEntry st = getByBlockchainName(c, blockchainName);
|
||||
if (st == null) {
|
||||
throw new IllegalStateException("Blockchain state not found for blockchainName=" + blockchainName);
|
||||
}
|
||||
if (st.getLastGlobalNumber() != -1) {
|
||||
throw new IllegalStateException("Blockchain state is not at genesis (-1). blockchainName=" + blockchainName +
|
||||
" last_global_number=" + st.getLastGlobalNumber());
|
||||
}
|
||||
return st;
|
||||
}
|
||||
|
||||
private BlockchainStateEntry mapRow(ResultSet rs) throws SQLException {
|
||||
BlockchainStateEntry e = new BlockchainStateEntry();
|
||||
|
||||
e.setBlockchainName(rs.getString("blockchainName"));
|
||||
e.setLogin(rs.getString("login"));
|
||||
e.setPublicKeyBase64(rs.getString("public_key_base64"));
|
||||
e.setBlockchainKey(rs.getString("blockchainKey"));
|
||||
|
||||
// size_limit теперь long
|
||||
e.setSizeLimit(rs.getLong("size_limit"));
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
package shine.db.dao;
|
||||
|
||||
import shine.db.SqliteDbController;
|
||||
import shine.db.entities.BlockchainStateEntry;
|
||||
|
||||
import java.sql.*;
|
||||
|
||||
/**
|
||||
* SolanaBlockchainsDAO — таблица блокчейнов пользователя.
|
||||
*
|
||||
* Сейчас физически это blockchain_state, потому что:
|
||||
* - у одного login может быть несколько blockchainName
|
||||
* - у каждого blockchainName свой blockchainKey и size_limit
|
||||
*
|
||||
* Правило:
|
||||
* - методы с Connection НЕ закрывают соединение
|
||||
* - методы без Connection сами открывают и закрывают соединение
|
||||
*/
|
||||
public final class SolanaBlockchainsDAO {
|
||||
|
||||
private static volatile SolanaBlockchainsDAO instance;
|
||||
private final SqliteDbController db = SqliteDbController.getInstance();
|
||||
private final BlockchainStateDAO stateDao = BlockchainStateDAO.getInstance();
|
||||
|
||||
private SolanaBlockchainsDAO() {}
|
||||
|
||||
public static SolanaBlockchainsDAO getInstance() {
|
||||
if (instance == null) {
|
||||
synchronized (SolanaBlockchainsDAO.class) {
|
||||
if (instance == null) instance = new SolanaBlockchainsDAO();
|
||||
}
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
public BlockchainStateEntry getByBlockchainName(String blockchainName) throws SQLException {
|
||||
return stateDao.getByBlockchainName(blockchainName);
|
||||
}
|
||||
|
||||
public BlockchainStateEntry getByBlockchainName(Connection c, String blockchainName) throws SQLException {
|
||||
return stateDao.getByBlockchainName(c, blockchainName);
|
||||
}
|
||||
|
||||
/** Для HEADER: проверка, что blockchain_state существует и last_global_number=-1. */
|
||||
public BlockchainStateEntry requireExistingAtGenesis(Connection c, String blockchainName) throws SQLException {
|
||||
return stateDao.requireExistingAtGenesis(c, blockchainName);
|
||||
}
|
||||
|
||||
/** Для добавления блока: атомарная проверка лимита + увеличение размера файла. */
|
||||
public boolean tryIncreaseFileSizeWithinLimit(Connection c, String blockchainName, long deltaBytes, long nowMs) throws SQLException {
|
||||
return stateDao.tryIncreaseFileSizeWithinLimit(c, blockchainName, deltaBytes, nowMs);
|
||||
}
|
||||
}
|
||||
@@ -42,19 +42,13 @@ public final class SolanaUsersDAO {
|
||||
/** Вставка с внешним соединением. Соединение НЕ закрывает. */
|
||||
public void insert(Connection c, SolanaUserEntry user) throws SQLException {
|
||||
String sql = """
|
||||
INSERT INTO solana_users (login, bchName, loginKey, deviceKey, bchLimit)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
INSERT INTO solana_users (login, deviceKey)
|
||||
VALUES (?, ?)
|
||||
""";
|
||||
|
||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||
ps.setString(1, user.getLogin());
|
||||
ps.setString(2, user.getBchName());
|
||||
ps.setString(3, user.getLoginKey());
|
||||
ps.setString(4, user.getDeviceKey());
|
||||
|
||||
if (user.getBchLimit() != null) ps.setInt(5, user.getBchLimit());
|
||||
else ps.setNull(5, Types.INTEGER);
|
||||
|
||||
ps.setString(2, user.getDeviceKey());
|
||||
ps.executeUpdate();
|
||||
}
|
||||
}
|
||||
@@ -97,7 +91,7 @@ public final class SolanaUsersDAO {
|
||||
/** Получить по login (case-insensitive) с внешним соединением. Соединение НЕ закрывает. */
|
||||
public SolanaUserEntry getByLogin(Connection c, String login) throws SQLException {
|
||||
String sql = """
|
||||
SELECT login, bchName, loginKey, deviceKey, bchLimit
|
||||
SELECT login, deviceKey
|
||||
FROM solana_users
|
||||
WHERE LOWER(login) = LOWER(?)
|
||||
""";
|
||||
@@ -121,7 +115,7 @@ public final class SolanaUsersDAO {
|
||||
/** Поиск по префиксу с внешним соединением. Соединение НЕ закрывает. */
|
||||
public List<SolanaUserEntry> searchByLoginPrefix(Connection c, String prefix) throws SQLException {
|
||||
String sql = """
|
||||
SELECT login, bchName, loginKey, deviceKey, bchLimit
|
||||
SELECT login, deviceKey
|
||||
FROM solana_users
|
||||
WHERE LOWER(login) LIKE ?
|
||||
ORDER BY login
|
||||
@@ -152,10 +146,7 @@ public final class SolanaUsersDAO {
|
||||
private SolanaUserEntry mapRow(ResultSet rs) throws SQLException {
|
||||
return new SolanaUserEntry(
|
||||
rs.getString("login"),
|
||||
rs.getString("bchName"),
|
||||
rs.getString("loginKey"),
|
||||
rs.getString("deviceKey"),
|
||||
rs.getObject("bchLimit") != null ? rs.getInt("bchLimit") : null
|
||||
rs.getString("deviceKey")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package shine.db.dao;
|
||||
|
||||
import shine.db.SqliteDbController;
|
||||
import shine.db.entities.BlockchainStateEntry;
|
||||
import shine.db.entities.SolanaUserEntry;
|
||||
|
||||
import java.sql.*;
|
||||
|
||||
/**
|
||||
* UserCreateDAO — атомарное добавление пользователя:
|
||||
* - solana_users (login, deviceKey)
|
||||
* - blockchain_state (blockchainName, login, blockchainKey, size_limit, ... last_global_number=-1 ...)
|
||||
*
|
||||
* ВАЖНО:
|
||||
* - только INSERT
|
||||
* - если login или blockchainName заняты — возвращаем false (пользователь уже есть/занято)
|
||||
*/
|
||||
public final class UserCreateDAO {
|
||||
|
||||
private static volatile UserCreateDAO instance;
|
||||
private final SqliteDbController db = SqliteDbController.getInstance();
|
||||
private final SolanaUsersDAO usersDao = SolanaUsersDAO.getInstance();
|
||||
private final BlockchainStateDAO stateDao = BlockchainStateDAO.getInstance();
|
||||
|
||||
private UserCreateDAO() {}
|
||||
|
||||
public static UserCreateDAO getInstance() {
|
||||
if (instance == null) {
|
||||
synchronized (UserCreateDAO.class) {
|
||||
if (instance == null) instance = new UserCreateDAO();
|
||||
}
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true если добавили; false если занято (login уже есть или blockchainName уже существует).
|
||||
*/
|
||||
public boolean insertUserWithBlockchain(
|
||||
String login,
|
||||
String deviceKey,
|
||||
String blockchainName,
|
||||
String blockchainKey,
|
||||
long sizeLimit,
|
||||
long nowMs
|
||||
) throws SQLException {
|
||||
|
||||
try (Connection c = db.getConnection()) {
|
||||
boolean oldAuto = c.getAutoCommit();
|
||||
c.setAutoCommit(false);
|
||||
|
||||
// BEGIN IMMEDIATE — чтобы сразу взять write-lock и не ловить гонки
|
||||
try (Statement st = c.createStatement()) {
|
||||
st.execute("BEGIN IMMEDIATE");
|
||||
}
|
||||
|
||||
try {
|
||||
// 1) user
|
||||
SolanaUserEntry u = new SolanaUserEntry(login, deviceKey);
|
||||
usersDao.insert(c, u); // если login занят -> constraint
|
||||
|
||||
// 2) blockchain_state
|
||||
BlockchainStateEntry st = new BlockchainStateEntry();
|
||||
st.setBlockchainName(blockchainName);
|
||||
st.setLogin(login);
|
||||
st.setBlockchainKey(blockchainKey);
|
||||
st.setSizeLimit(sizeLimit);
|
||||
st.setFileSizeBytes(0L);
|
||||
|
||||
// старт: глобальных блоков ещё нет
|
||||
st.setLastGlobalNumber(-1);
|
||||
st.setLastGlobalHash("");
|
||||
|
||||
for (int line = 0; line < 8; line++) {
|
||||
st.setLastLineNumber(line, 0);
|
||||
st.setLastLineHash(line, "");
|
||||
}
|
||||
|
||||
st.setUpdatedAtMs(nowMs);
|
||||
|
||||
stateDao.upsert(c, st); // если blockchainName занят -> constraint (PK)
|
||||
|
||||
c.commit();
|
||||
return true;
|
||||
|
||||
} catch (SQLException e) {
|
||||
c.rollback();
|
||||
|
||||
// SQLITE_CONSTRAINT -> "уже существует"
|
||||
// Мы не делаем UPDATE, только insert.
|
||||
String msg = e.getMessage() == null ? "" : e.getMessage().toLowerCase();
|
||||
if (msg.contains("constraint")) {
|
||||
return false;
|
||||
}
|
||||
throw e;
|
||||
|
||||
} finally {
|
||||
c.setAutoCommit(oldAuto);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package shine.db.entities;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
|
||||
/**
|
||||
* Агрегатная сущность текущего состояния блокчейна.
|
||||
@@ -11,7 +12,9 @@ public final class BlockchainStateEntry {
|
||||
private String blockchainName;
|
||||
|
||||
private String login;
|
||||
private String publicKeyBase64;
|
||||
|
||||
/** Ключ блокчейна (pubkey), которым подписываются блоки. Base64(32 bytes). */
|
||||
private String blockchainKey;
|
||||
|
||||
/** Лимит (теперь long). */
|
||||
private long sizeLimit;
|
||||
@@ -36,7 +39,7 @@ public final class BlockchainStateEntry {
|
||||
|
||||
public BlockchainStateEntry(String blockchainName,
|
||||
String login,
|
||||
String publicKeyBase64,
|
||||
String blockchainKey,
|
||||
long sizeLimit,
|
||||
long fileSizeBytes,
|
||||
int lastGlobalNumber,
|
||||
@@ -46,7 +49,7 @@ public final class BlockchainStateEntry {
|
||||
long updatedAtMs) {
|
||||
this.blockchainName = blockchainName;
|
||||
this.login = login;
|
||||
this.publicKeyBase64 = publicKeyBase64;
|
||||
this.blockchainKey = blockchainKey;
|
||||
this.sizeLimit = sizeLimit;
|
||||
this.fileSizeBytes = fileSizeBytes;
|
||||
this.lastGlobalNumber = lastGlobalNumber;
|
||||
@@ -72,8 +75,21 @@ public final class BlockchainStateEntry {
|
||||
public String getLogin() { return login; }
|
||||
public void setLogin(String login) { this.login = login; }
|
||||
|
||||
public String getPublicKeyBase64() { return publicKeyBase64; }
|
||||
public void setPublicKeyBase64(String publicKeyBase64) { this.publicKeyBase64 = publicKeyBase64; }
|
||||
public String getBlockchainKey() { return blockchainKey; }
|
||||
public void setBlockchainKey(String blockchainKey) { this.blockchainKey = blockchainKey; }
|
||||
|
||||
/** blockchainKey в байтах (32) или null, если битый. */
|
||||
public byte[] getBlockchainKeyBytes() {
|
||||
if (blockchainKey == null) return null;
|
||||
String s = blockchainKey.trim();
|
||||
if (s.isEmpty()) return null;
|
||||
try {
|
||||
byte[] b = Base64.getDecoder().decode(s);
|
||||
return (b != null && b.length == 32) ? b : null;
|
||||
} catch (IllegalArgumentException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public long getSizeLimit() { return sizeLimit; }
|
||||
public void setSizeLimit(long sizeLimit) { this.sizeLimit = sizeLimit; }
|
||||
|
||||
@@ -15,52 +15,32 @@ import java.util.Base64;
|
||||
public class SolanaUserEntry {
|
||||
|
||||
private String login; // TEXT PK
|
||||
private String bchName; // TEXT NOT NULL
|
||||
private String loginKey; // TEXT
|
||||
private String deviceKey; // TEXT
|
||||
private Integer bchLimit; // INTEGER nullable
|
||||
private String deviceKey; // TEXT NOT NULL (Base64(32 bytes))
|
||||
|
||||
public SolanaUserEntry() {}
|
||||
|
||||
public SolanaUserEntry(String login,
|
||||
String bchName,
|
||||
String loginKey,
|
||||
String deviceKey,
|
||||
Integer bchLimit) {
|
||||
public SolanaUserEntry(String login, String deviceKey) {
|
||||
this.login = login;
|
||||
this.bchName = bchName;
|
||||
this.loginKey = loginKey;
|
||||
this.deviceKey = deviceKey;
|
||||
this.bchLimit = bchLimit;
|
||||
}
|
||||
|
||||
public String getLogin() { return login; }
|
||||
public void setLogin(String login) { this.login = login; }
|
||||
|
||||
public String getBchName() { return bchName; }
|
||||
public void setBchName(String bchName) { this.bchName = bchName; }
|
||||
|
||||
/** Публичный ключ логина (основной ключ пользователя). */
|
||||
public String getLoginKey() { return loginKey; }
|
||||
public void setLoginKey(String loginKey) { this.loginKey = loginKey; }
|
||||
|
||||
/** Публичный ключ устройства (device key). */
|
||||
public String getDeviceKey() { return deviceKey; }
|
||||
public void setDeviceKey(String deviceKey) { this.deviceKey = deviceKey; }
|
||||
|
||||
public Integer getBchLimit() { return bchLimit; }
|
||||
public void setBchLimit(Integer bchLimit) { this.bchLimit = bchLimit; }
|
||||
|
||||
/**
|
||||
* Публичный ключ логина в байтах (32 байта) или null, если ключ битый/пустой.
|
||||
* Device key в байтах (32 байта) или null, если ключ битый/пустой.
|
||||
*
|
||||
* Поддержка форматов:
|
||||
* - Base64 (предпочтительно)
|
||||
* - HEX (ровно 64 hex-символа, без пробелов)
|
||||
*/
|
||||
public byte[] getLoginKeyByte() {
|
||||
if (loginKey == null) return null;
|
||||
String s = loginKey.trim();
|
||||
public byte[] getDeviceKeyByte() {
|
||||
if (deviceKey == null) return null;
|
||||
String s = deviceKey.trim();
|
||||
if (s.isEmpty()) return null;
|
||||
|
||||
// 1) пробуем Base64
|
||||
|
||||
Reference in New Issue
Block a user