- u_async.c: process immediate_queue BEFORE epoll_wait in uasync_poll - stcp_link.c: defensive NULL etcp_conn/etcp_link in stcp_link_close - utun_instance.c: move stcp_server_list_destroy_all to Phase J (before ETCP), add deferred drain in both Phase J and L - stcp.h/c: STCP_HS_ENC_CLIENT 6→38, STCP_HS_ENC_SERVER 7→39, exchange ed25519_pubkey in both handshake directions - etcp_connections.c: copy peer_ed25519 from STCP to ETCP_CONN in etcp_link_enter_ready_tcp; guard link->etcp in tcp_link_close_cb - chat_sync.c: fix lk->conn NULL dereference for TCP links - test_stcp.c: update stcp_server_create/stcp_client_connect callsites - tools/chat_tcp_test: change transport udp→tcp
243 lines
10 KiB
Markdown
243 lines
10 KiB
Markdown
# База данных децентрализованного чата (chatgui)
|
|
|
|
Чат децентрализованный — нет центрального сервера. Каждый узел хранит локальную SQLite БД
|
|
(`chats.db`, WAL mode). Синхронизация сообщений между узлами — через протокол `chat_sync`
|
|
поверх `etcp_router` (svc_id 0x30). Данные о других узлах поступают из NODEINFO
|
|
через conn_mgr.
|
|
|
|
Вся работа с БД — в GUI-потоке (класс `DbManager`). Протокол синхронизации работает
|
|
в uasync-потоке (`chat_sync.c`) и общается с БД асинхронно через `gui_bridge`.
|
|
|
|
## Обзор таблиц
|
|
|
|
| Таблица | Назначение |
|
|
|---------|-----------|
|
|
| `local_identity` | Наш собственный узел (id, ключи) |
|
|
| `nodes` | Все известные узлы (node_id, ключи, last_seen) |
|
|
| `node_addresses` | Адреса узлов (family, protocol, address, port, rtt, is_nat) |
|
|
| `accounts` | Профили узлов (display_name, avatar) |
|
|
| `channels` | Метаданные каналов/сетей (ключи, подпись, позиции скролла) |
|
|
| `msg_<ch>` | Per-channel таблица сообщений |
|
|
| `peers_<ch>` | Per-channel таблица участников (подписи входа, создателя, comment) |
|
|
| `ui_state` | Локальное состояние UI (key-value) |
|
|
|
|
---
|
|
|
|
## Таблицы
|
|
|
|
### `local_identity` — наш собственный узел
|
|
|
|
```sql
|
|
CREATE TABLE IF NOT EXISTS local_identity (
|
|
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
node_id INTEGER NOT NULL UNIQUE,
|
|
name TEXT NOT NULL,
|
|
x25519_pubkey BLOB NOT NULL,
|
|
x25519_privkey BLOB,
|
|
ed25519_pubkey BLOB,
|
|
created_at INTEGER DEFAULT (unixepoch()),
|
|
updated_at INTEGER DEFAULT (unixepoch())
|
|
);
|
|
```
|
|
|
|
### `nodes` — все известные узлы
|
|
|
|
```sql
|
|
CREATE TABLE IF NOT EXISTS nodes (
|
|
node_id INTEGER PRIMARY KEY,
|
|
name TEXT,
|
|
x25519_pubkey BLOB NOT NULL,
|
|
ed25519_pubkey BLOB,
|
|
last_seen_at INTEGER,
|
|
created_at INTEGER DEFAULT (unixepoch())
|
|
);
|
|
```
|
|
|
|
| Поле | Описание |
|
|
|------|---------|
|
|
| `node_id` | ID узла в сети uTun |
|
|
| `name` | Никнейм |
|
|
| `x25519_pubkey` | Публичный ключ для key exchange (32 байта) |
|
|
| `ed25519_pubkey` | Публичный ключ для подписей (32 байта), может быть NULL |
|
|
| `last_seen_at` | Unix-время последней активности |
|
|
|
|
Статус online определяется через conn_mgr, а не полем в БД.
|
|
|
|
### `node_addresses` — адреса узлов
|
|
|
|
```sql
|
|
CREATE TABLE IF NOT EXISTS node_addresses (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
node_id INTEGER NOT NULL REFERENCES nodes(node_id) ON DELETE CASCADE,
|
|
family INTEGER NOT NULL CHECK(family IN (4, 6)),
|
|
protocol INTEGER NOT NULL DEFAULT 1,
|
|
address BLOB NOT NULL,
|
|
port INTEGER NOT NULL CHECK(port > 0 AND port <= 65535),
|
|
rtt INTEGER,
|
|
is_nat INTEGER DEFAULT 0,
|
|
created_at INTEGER DEFAULT (unixepoch())
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_na_node ON node_addresses(node_id);
|
|
```
|
|
|
|
| Поле | Описание |
|
|
|------|---------|
|
|
| `family` | 4=IPv4, 6=IPv6 |
|
|
| `protocol` | Битовая маска: 1=UDP (NODE_PROTO_UDP), 2=TCP (NODE_PROTO_TCP), 3=UDP+TCP |
|
|
| `address` | BLOB: 4 байта для IPv4, 16 байт для IPv6 |
|
|
| `port` | Порт |
|
|
| `rtt` | RTT в миллисекундах, NULL если не измерен |
|
|
| `is_nat` | 1 = узел за NAT |
|
|
|
|
### `accounts` — профили узлов
|
|
|
|
```sql
|
|
CREATE TABLE IF NOT EXISTS accounts (
|
|
node_id INTEGER PRIMARY KEY REFERENCES nodes(node_id),
|
|
display_name TEXT NOT NULL,
|
|
avatar_color TEXT DEFAULT '#4A90E2',
|
|
avatar_letter TEXT NOT NULL,
|
|
is_contact INTEGER DEFAULT 1,
|
|
created_at INTEGER DEFAULT (unixepoch())
|
|
);
|
|
```
|
|
|
|
### `channels` — метаданные каналов
|
|
|
|
```sql
|
|
CREATE TABLE IF NOT EXISTS channels (
|
|
channel_id TEXT PRIMARY KEY,
|
|
name TEXT NOT NULL,
|
|
owner_node_id INTEGER,
|
|
x25519_pubkey BLOB NOT NULL,
|
|
x25519_privkey BLOB,
|
|
ed25519_pubkey BLOB NOT NULL,
|
|
ed25519_privkey BLOB,
|
|
signature BLOB NOT NULL,
|
|
last_read_msg_id INTEGER,
|
|
last_pos_msg_id INTEGER,
|
|
created_at INTEGER DEFAULT (unixepoch())
|
|
);
|
|
```
|
|
|
|
| Поле | Описание |
|
|
|------|---------|
|
|
| `channel_id` | Уникальный ID канала (напр. `"ch:general"`) |
|
|
| `name` | Отображаемое имя (напр. `"#general"`) |
|
|
| `owner_node_id` | ID создателя канала |
|
|
| `x25519_pubkey` | Публичный ключ канала (32 байта) |
|
|
| `x25519_privkey` | Приватный ключ канала, NULL если локальный узел не админ |
|
|
| `ed25519_pubkey` | Публичный ключ подписи канала (32 байта) |
|
|
| `ed25519_privkey` | Приватный ключ подписи, NULL если локальный узел не админ |
|
|
| `signature` | Ed25519 подпись всех полей записи приватным ключом канала |
|
|
| `last_read_msg_id` | ID последнего прочитанного сообщения (локальное) |
|
|
| `last_pos_msg_id` | ID сообщения на позиции скролла (для восстановления при открытии) |
|
|
|
|
### `msg_<ch>` — per-channel таблица сообщений
|
|
|
|
Для каждого канала создаётся отдельная таблица. Имя: `msg_` + sanitized `channel_id`
|
|
(не-буквоцифры заменяются на `_`).
|
|
|
|
```sql
|
|
CREATE TABLE IF NOT EXISTS "msg_<ch>" (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
node_id INTEGER NOT NULL,
|
|
content_type TEXT NOT NULL,
|
|
data BLOB NOT NULL,
|
|
timestamp INTEGER NOT NULL,
|
|
datahash INTEGER NOT NULL,
|
|
chain_hash BLOB NOT NULL,
|
|
signature BLOB,
|
|
is_outgoing INTEGER DEFAULT 0,
|
|
is_read INTEGER DEFAULT 0,
|
|
sync_flags INTEGER DEFAULT 0,
|
|
UNIQUE(timestamp, datahash)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS "idx_msg_<ch>_ts" ON "msg_<ch>"(timestamp, datahash);
|
|
```
|
|
|
|
| Поле | Описание |
|
|
|------|---------|
|
|
| `node_id` | Автор сообщения |
|
|
| `content_type` | MIME-тип: `"text/plain"`, `"image/png"`, ... |
|
|
| `data` | Содержимое сообщения (BLOB) |
|
|
| `timestamp` | Время отправки в микросекундах (первая часть ключа sync) |
|
|
| `datahash` | Первые 8 байт SHA256(data), вторая часть ключа sync |
|
|
| `chain_hash` | SHA256 цепочки (32 байта) — как в blockchain, связывает записи по порядку |
|
|
| `signature` | Ed25519 подпись автора (64 байта), пока заглушка (64 нуля) |
|
|
| `is_outgoing` | 1 = отправлено нами |
|
|
| `is_read` | 1 = прочитано |
|
|
| `sync_flags` | Битовая маска: 0x01 = WAS_SENT (запись подтверждена пирами) |
|
|
|
|
**Дедупликация:** уникальный индекс `(timestamp, datahash)` предотвращает повторную вставку
|
|
одного и того же сообщения при получении через разные пути.
|
|
|
|
**Chain hash:** `chain_hash = SHA256(prev_chain_hash || timestamp || datahash)`.
|
|
Обеспечивает проверку целостности порядка сообщений при синхронизации.
|
|
|
|
### `peers_<ch>` — per-channel таблица участников
|
|
|
|
```sql
|
|
CREATE TABLE IF NOT EXISTS "peers_<ch>" (
|
|
node_id INTEGER NOT NULL,
|
|
join_sig BLOB NOT NULL,
|
|
creator_sig BLOB,
|
|
comment TEXT,
|
|
joined_at INTEGER DEFAULT (unixepoch()),
|
|
PRIMARY KEY (node_id)
|
|
);
|
|
```
|
|
|
|
| Поле | Описание |
|
|
|------|---------|
|
|
| `join_sig` | Ed25519 подпись присоединяющегося: `sign(sk_user, channel_id || node_id)` |
|
|
| `creator_sig` | Ed25519 подпись создателя канала: `sign(sk_creator, channel_id || node_id)`. NULL = гость (ограниченные права) |
|
|
| `comment` | Комментарий/заметка об участнике |
|
|
|
|
### `ui_state` — локальное состояние UI
|
|
|
|
```sql
|
|
CREATE TABLE IF NOT EXISTS ui_state (
|
|
key TEXT PRIMARY KEY,
|
|
value TEXT
|
|
);
|
|
```
|
|
|
|
Key-value хранилище для произвольных настроек UI (позиции окон, выбранный канал, ...).
|
|
|
|
---
|
|
|
|
## Формат binary-записей sync (gui_bridge / chat_sync)
|
|
|
|
Записи передаются между потоками и по сети в компактном бинарном формате:
|
|
|
|
### Insert record (для GUI_OP_INSERT_RECORD / PUSH wire)
|
|
```
|
|
[timestamp:8 LE][datahash:8 LE][node_id:8 LE][ct_len:1][content_type:ct_len][data_len:4 LE][data:data_len][chain_hash:32]
|
|
```
|
|
|
|
### NodeInfo (GUI_OP_LOAD_NODEINFO)
|
|
```
|
|
[x25519_pubkey:32][ed25519_pubkey:32][addr_count:1]([family:1][proto:1][addr_len:1][addr:4|16][port:2 LE][rtt:2 LE])...
|
|
```
|
|
|
|
### SEND_DATA cursor record (GUI_OP_CURSOR_NEXT)
|
|
```
|
|
[ts:8 LE][dh:8 LE][node_id:8 LE][ct_len:1][ct:var][data_len:4 LE][data:var]
|
|
```
|
|
|
|
---
|
|
|
|
## Порядок синхронизации (chat_sync)
|
|
|
|
1. **Peer online** → для каждого общего канала: `INIT_SYNC(my_count, last_chain_hash)`
|
|
2. **INIT_RESP** → сравнение chain_hash на позиции `min(counts)-1`. Если match → synced.
|
|
Иначе sparse hashes (-1, -2, -4, -8, ...) для поиска точки расхождения.
|
|
3. **SEND_DATA** → обмен недостающими записями пакетами до 32 штук
|
|
4. **SYNC_DONE** → финальная сверка count + last_chain_hash
|
|
5. **PUSH** → реал-тайм доставка новых сообщений (→ ACK_PUSH)
|
|
6. **TTL** → раз в час удаление своих неподтверждённых записей старше 24ч
|
|
|
|
Все операции с БД асинхронные: `chat_sync (uasync)` → `gui_bridge_call()` →
|
|
GUI-поток (SQL) → `uasync_post()` → callback в chat_sync.
|