Добавить ListBlockchainHeads для межсерверной сверки

This commit is contained in:
AidarKC 2026-06-25 17:52:04 +04:00
parent f0e1ab3af8
commit 1f8b20a7d1
8 changed files with 229 additions and 8 deletions

View File

@ -2,10 +2,11 @@
Этот файл описывает технические WebSocket-запросы, которые нужны для служебной работы клиента с сервером. Часть операций доступна без авторизации, часть требует успешной авторизованной сессии. Этот файл описывает технические WebSocket-запросы, которые нужны для служебной работы клиента с сервером. Часть операций доступна без авторизации, часть требует успешной авторизованной сессии.
Сейчас здесь шесть методов: Сейчас здесь семь методов:
- `Ping` — keep-alive запрос для поддержания живого WebSocket-соединения; - `Ping` — keep-alive запрос для поддержания живого WebSocket-соединения;
- `GetServerInfo` — запрос базовой публичной информации о сервере для выбора узла в децентрализованной сети; - `GetServerInfo` — запрос базовой публичной информации о сервере для выбора узла в децентрализованной сети;
- `ListBlockchainHeads` — краткая сводка по всем локальным блокчейнам сервера для межсерверной синхронизации;
- `GetCallIceConfig` — выдача STUN/TURN конфигурации для звонков; - `GetCallIceConfig` — выдача STUN/TURN конфигурации для звонков;
- `ClientErrorLog` — отправка клиентской ошибки в серверный лог; - `ClientErrorLog` — отправка клиентской ошибки в серверный лог;
- `ClientDebugLog` — отправка клиентского debug-события в серверный буфер; - `ClientDebugLog` — отправка клиентского debug-события в серверный буфер;
@ -15,6 +16,7 @@
- `Ping` нужен для регулярной проверки, что соединение всё ещё живо; - `Ping` нужен для регулярной проверки, что соединение всё ещё живо;
- `GetServerInfo` нужен до авторизации и до работы с данными, чтобы клиент понял, что сервер доступен, и показал пользователю краткую карточку этого узла. - `GetServerInfo` нужен до авторизации и до работы с данными, чтобы клиент понял, что сервер доступен, и показал пользователю краткую карточку этого узла.
- `ListBlockchainHeads` нужен для сервер-сервер сверки: партнёр получает список heads по всем цепочкам, сравнивает его со своим состоянием и затем добирает недостающие блоки по диапазону.
Ниже сначала описаны назначение методов, затем точные форматы запросов и ответов. Ниже сначала описаны назначение методов, затем точные форматы запросов и ответов.
@ -134,7 +136,68 @@
--- ---
## 3. `GetCallIceConfig` ## 3. `ListBlockchainHeads`
### Назначение
Запрос краткой сводки по всем локальным блокчейнам сервера.
Нужен для межсерверной синхронизации. Партнёр может:
- получить список всех блокчейнов;
- сравнить `lastBlockNumber` и `lastBlockHash` со своими значениями;
- понять, какие цепочки нужно догонять;
- затем отдельно запросить недостающие блоки по диапазону.
Этот запрос доступен без авторизации.
### Запрос
```json
{
"op": "ListBlockchainHeads",
"requestId": "heads-001",
"payload": {}
}
```
### Успешный ответ
```json
{
"op": "ListBlockchainHeads",
"requestId": "heads-001",
"status": 200,
"ok": true,
"payload": {
"blockchains": [
{
"blockchainName": "alice_main",
"lastBlockNumber": 124,
"lastBlockHash": "aabbccdd00112233445566778899aabbccddeeff00112233445566778899aabb",
"fileSizeBytes": 58720
}
]
}
}
```
### Поля ответа
- `blockchains` — массив текущих heads всех цепочек сервера.
- `blockchainName` — имя блокчейна.
- `lastBlockNumber` — последний номер блока в этой цепочке.
- `lastBlockHash` — последний хэш блока в HEX-формате `64` символа.
- `fileSizeBytes` — текущий размер файла блокчейна в байтах.
### Специфические коды ошибок `ListBlockchainHeads`
- У `ListBlockchainHeads` нет специальных прикладных ошибок при штатной работе.
- Если произойдёт непредвиденная проблема, сервер вернёт общую ошибку из раздела `00`, обычно `500 / INTERNAL_ERROR`.
---
## 4. `GetCallIceConfig`
Доступно только после успешной авторизации. Доступно только после успешной авторизации.
@ -184,7 +247,7 @@
--- ---
## 4. `ClientErrorLog` ## 5. `ClientErrorLog`
### Запрос ### Запрос
@ -231,7 +294,7 @@
--- ---
## 5. `ClientDebugLog` ## 6. `ClientDebugLog`
### Запрос ### Запрос
@ -269,7 +332,7 @@
--- ---
## 6. `CallDeliveryReport` ## 7. `CallDeliveryReport`
### Запрос ### Запрос

View File

@ -34,6 +34,7 @@
| `AddBlock` | `04_Add_Block_to_Blockchain_API.md` | добавление блока в блокчейн | | `AddBlock` | `04_Add_Block_to_Blockchain_API.md` | добавление блока в блокчейн |
| `Ping` | `05_Technical_Requests_API.md` | keep-alive | | `Ping` | `05_Technical_Requests_API.md` | keep-alive |
| `GetServerInfo` | `05_Technical_Requests_API.md` | публичная информация о сервере | | `GetServerInfo` | `05_Technical_Requests_API.md` | публичная информация о сервере |
| `ListBlockchainHeads` | `05_Technical_Requests_API.md` | список heads всех локальных блокчейнов |
| `GetCallIceConfig` | `05_Technical_Requests_API.md` | STUN/TURN конфигурация звонков | | `GetCallIceConfig` | `05_Technical_Requests_API.md` | STUN/TURN конфигурация звонков |
| `ClientErrorLog` | `05_Technical_Requests_API.md` | логирование клиентской ошибки | | `ClientErrorLog` | `05_Technical_Requests_API.md` | логирование клиентской ошибки |
| `ClientDebugLog` | `05_Technical_Requests_API.md` | клиентский debug-лог | | `ClientDebugLog` | `05_Technical_Requests_API.md` | клиентский debug-лог |

View File

@ -4,6 +4,8 @@ import shine.db.SqliteDbController;
import shine.db.entities.BlockchainStateEntry; import shine.db.entities.BlockchainStateEntry;
import java.sql.*; import java.sql.*;
import java.util.ArrayList;
import java.util.List;
public final class BlockchainStateDAO { public final class BlockchainStateDAO {
@ -53,6 +55,39 @@ public final class BlockchainStateDAO {
} }
} }
/** Получить все blockchain_state записи. */
public List<BlockchainStateEntry> listAll() throws SQLException {
try (Connection c = db.getConnection()) {
return listAll(c);
}
}
/** Получить все blockchain_state записи с внешним соединением. Соединение НЕ закрывает. */
public List<BlockchainStateEntry> listAll(Connection c) throws SQLException {
String sql = """
SELECT
blockchain_name,
login,
blockchain_key,
size_limit,
file_size_bytes,
last_block_number,
last_block_hash,
updated_at_ms
FROM blockchain_state
ORDER BY blockchain_name COLLATE NOCASE
""";
List<BlockchainStateEntry> result = new ArrayList<>();
try (PreparedStatement ps = c.prepareStatement(sql);
ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
result.add(mapRow(rs));
}
}
return result;
}
/** UPSERT без внешнего соединения. Сам открывает/закрывает. */ /** UPSERT без внешнего соединения. Сам открывает/закрывает. */
public void upsert(BlockchainStateEntry e) throws SQLException { public void upsert(BlockchainStateEntry e) throws SQLException {
try (Connection c = db.getConnection()) { try (Connection c = db.getConnection()) {

View File

@ -106,6 +106,7 @@ import server.logic.ws_protocol.JSON.handlers.system.Net_GetServerInfo_Handler;
import server.logic.ws_protocol.JSON.handlers.system.Net_GetCallIceConfig_Handler; import server.logic.ws_protocol.JSON.handlers.system.Net_GetCallIceConfig_Handler;
import server.logic.ws_protocol.JSON.handlers.system.Net_ClientErrorLog_Handler; import server.logic.ws_protocol.JSON.handlers.system.Net_ClientErrorLog_Handler;
import server.logic.ws_protocol.JSON.handlers.system.Net_ClientDebugLog_Handler; import server.logic.ws_protocol.JSON.handlers.system.Net_ClientDebugLog_Handler;
import server.logic.ws_protocol.JSON.handlers.system.Net_ListBlockchainHeads_Handler;
import server.logic.ws_protocol.JSON.handlers.system.Net_CallDeliveryReport_Handler; import server.logic.ws_protocol.JSON.handlers.system.Net_CallDeliveryReport_Handler;
import server.logic.ws_protocol.JSON.handlers.system.Net_Ping_Handler; import server.logic.ws_protocol.JSON.handlers.system.Net_Ping_Handler;
import server.logic.ws_protocol.JSON.handlers.system.entyties.Net_CallDeliveryReport_Request; import server.logic.ws_protocol.JSON.handlers.system.entyties.Net_CallDeliveryReport_Request;
@ -113,6 +114,7 @@ import server.logic.ws_protocol.JSON.handlers.system.entyties.Net_ClientErrorLog
import server.logic.ws_protocol.JSON.handlers.system.entyties.Net_ClientDebugLog_Request; import server.logic.ws_protocol.JSON.handlers.system.entyties.Net_ClientDebugLog_Request;
import server.logic.ws_protocol.JSON.handlers.system.entyties.Net_GetCallIceConfig_Request; import server.logic.ws_protocol.JSON.handlers.system.entyties.Net_GetCallIceConfig_Request;
import server.logic.ws_protocol.JSON.handlers.system.entyties.Net_GetServerInfo_Request; import server.logic.ws_protocol.JSON.handlers.system.entyties.Net_GetServerInfo_Request;
import server.logic.ws_protocol.JSON.handlers.system.entyties.Net_ListBlockchainHeads_Request;
import server.logic.ws_protocol.JSON.handlers.system.entyties.Net_Ping_Request; import server.logic.ws_protocol.JSON.handlers.system.entyties.Net_Ping_Request;
import java.util.Map; import java.util.Map;
@ -193,6 +195,7 @@ public final class JsonHandlerRegistry {
// --- system --- // --- system ---
Map.entry("Ping", new Net_Ping_Handler()), Map.entry("Ping", new Net_Ping_Handler()),
Map.entry("GetServerInfo", new Net_GetServerInfo_Handler()), Map.entry("GetServerInfo", new Net_GetServerInfo_Handler()),
Map.entry("ListBlockchainHeads", new Net_ListBlockchainHeads_Handler()),
Map.entry("GetCallIceConfig", new Net_GetCallIceConfig_Handler()), Map.entry("GetCallIceConfig", new Net_GetCallIceConfig_Handler()),
Map.entry("ClientErrorLog", new Net_ClientErrorLog_Handler()), Map.entry("ClientErrorLog", new Net_ClientErrorLog_Handler()),
Map.entry("ClientDebugLog", new Net_ClientDebugLog_Handler()), Map.entry("ClientDebugLog", new Net_ClientDebugLog_Handler()),
@ -268,6 +271,7 @@ public final class JsonHandlerRegistry {
// --- system --- // --- system ---
Map.entry("Ping", Net_Ping_Request.class), Map.entry("Ping", Net_Ping_Request.class),
Map.entry("GetServerInfo", Net_GetServerInfo_Request.class), Map.entry("GetServerInfo", Net_GetServerInfo_Request.class),
Map.entry("ListBlockchainHeads", Net_ListBlockchainHeads_Request.class),
Map.entry("GetCallIceConfig", Net_GetCallIceConfig_Request.class), Map.entry("GetCallIceConfig", Net_GetCallIceConfig_Request.class),
Map.entry("ClientErrorLog", Net_ClientErrorLog_Request.class), Map.entry("ClientErrorLog", Net_ClientErrorLog_Request.class),
Map.entry("ClientDebugLog", Net_ClientDebugLog_Request.class), Map.entry("ClientDebugLog", Net_ClientDebugLog_Request.class),

View File

@ -0,0 +1,76 @@
package server.logic.ws_protocol.JSON.handlers.system;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import server.logic.ws_protocol.JSON.ConnectionContext;
import server.logic.ws_protocol.JSON.entyties.Net_Request;
import server.logic.ws_protocol.JSON.entyties.Net_Response;
import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
import server.logic.ws_protocol.JSON.handlers.system.entyties.Net_ListBlockchainHeads_Request;
import server.logic.ws_protocol.JSON.handlers.system.entyties.Net_ListBlockchainHeads_Response;
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
import server.logic.ws_protocol.WireCodes;
import shine.db.dao.BlockchainStateDAO;
import shine.db.entities.BlockchainStateEntry;
import java.util.ArrayList;
import java.util.List;
/**
* ListBlockchainHeads получить краткую сводку по всем blockchain_state.
* Используется для межсерверной сверки heads перед догоняющей синхронизацией.
*/
public class Net_ListBlockchainHeads_Handler implements JsonMessageHandler {
private static final Logger log = LoggerFactory.getLogger(Net_ListBlockchainHeads_Handler.class);
@Override
public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) {
Net_ListBlockchainHeads_Request req = (Net_ListBlockchainHeads_Request) baseRequest;
try {
List<BlockchainStateEntry> states = BlockchainStateDAO.getInstance().listAll();
Net_ListBlockchainHeads_Response resp = new Net_ListBlockchainHeads_Response();
resp.setOp(req.getOp());
resp.setRequestId(req.getRequestId());
resp.setStatus(WireCodes.Status.OK);
List<Net_ListBlockchainHeads_Response.Item> items = new ArrayList<>(states.size());
for (BlockchainStateEntry state : states) {
Net_ListBlockchainHeads_Response.Item item = new Net_ListBlockchainHeads_Response.Item();
item.setBlockchainName(state.getBlockchainName());
item.setLastBlockNumber(state.getLastBlockNumber());
item.setLastBlockHash(toHex32(state.getLastBlockHash()));
item.setFileSizeBytes(state.getFileSizeBytes());
items.add(item);
}
resp.setBlockchains(items);
return resp;
} catch (Exception e) {
log.error("❌ Internal error ListBlockchainHeads", e);
return NetExceptionResponseFactory.error(
req,
WireCodes.Status.INTERNAL_ERROR,
"INTERNAL_ERROR",
NetExceptionResponseFactory.detailedMessage("Внутренняя ошибка сервера при ListBlockchainHeads", e)
);
}
}
private static String toHex32(byte[] bytes32) {
byte[] b = bytes32;
if (b == null || b.length != 32) {
b = new byte[32];
}
final char[] hex = "0123456789abcdef".toCharArray();
char[] out = new char[64];
for (int i = 0; i < 32; i++) {
int v = b[i] & 0xFF;
out[i * 2] = hex[v >>> 4];
out[i * 2 + 1] = hex[v & 0x0F];
}
return new String(out);
}
}

View File

@ -0,0 +1,9 @@
package server.logic.ws_protocol.JSON.handlers.system.entyties;
import server.logic.ws_protocol.JSON.entyties.Net_Request;
/**
* Пустой запрос для получения списка heads всех blockchain_state.
*/
public class Net_ListBlockchainHeads_Request extends Net_Request {
}

View File

@ -0,0 +1,33 @@
package server.logic.ws_protocol.JSON.handlers.system.entyties;
import server.logic.ws_protocol.JSON.entyties.Net_Response;
import java.util.ArrayList;
import java.util.List;
public class Net_ListBlockchainHeads_Response extends Net_Response {
public static final class Item {
private String blockchainName;
private int lastBlockNumber;
private String lastBlockHash;
private long fileSizeBytes;
public String getBlockchainName() { return blockchainName; }
public void setBlockchainName(String blockchainName) { this.blockchainName = blockchainName; }
public int getLastBlockNumber() { return lastBlockNumber; }
public void setLastBlockNumber(int lastBlockNumber) { this.lastBlockNumber = lastBlockNumber; }
public String getLastBlockHash() { return lastBlockHash; }
public void setLastBlockHash(String lastBlockHash) { this.lastBlockHash = lastBlockHash; }
public long getFileSizeBytes() { return fileSizeBytes; }
public void setFileSizeBytes(long fileSizeBytes) { this.fileSizeBytes = fileSizeBytes; }
}
private List<Item> blockchains = new ArrayList<>();
public List<Item> getBlockchains() { return blockchains; }
public void setBlockchains(List<Item> blockchains) { this.blockchains = blockchains; }
}

View File

@ -1,2 +1,2 @@
client.version=1.2.269 client.version=1.2.270
server.version=1.2.249 server.version=1.2.250