Files
utun2/tools/chatgui/doc/db_schema.md
T
evgeny 38c9f229bc stcp: ed25519 pubkey exchange in handshake, fix instance destroy segfault, fix chat_tcp_test
- 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
2026-08-07 00:48:27 +03:00

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.