10 KiB
База данных децентрализованного чата (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 — наш собственный узел
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 — все известные узлы
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 — адреса узлов
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 — профили узлов
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 — метаданные каналов
CREATE TABLE IF NOT EXISTS channels (
channel_id TEXT PRIMARY KEY,
name TEXT NOT NULL,
owner_node_id INTEGER,
is_dm INTEGER DEFAULT 0,
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 создателя канала |
is_dm |
1 = личная переписка (direct message) |
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
(не-буквоцифры заменяются на _).
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 таблица участников
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 |
creator_sig |
Ed25519 подпись создателя канала: `sign(sk_creator, channel_id |
comment |
Комментарий/заметка об участнике |
ui_state — локальное состояние UI
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)
- Peer online → для каждого общего канала:
INIT_SYNC(my_count, last_chain_hash) - INIT_RESP → сравнение chain_hash на позиции
min(counts)-1. Если match → synced. Иначе sparse hashes (-1, -2, -4, -8, ...) для поиска точки расхождения. - SEND_DATA → обмен недостающими записями пакетами до 32 штук
- SYNC_DONE → финальная сверка count + last_chain_hash
- PUSH → реал-тайм доставка новых сообщений (→ ACK_PUSH)
- TTL → раз в час удаление своих неподтверждённых записей старше 24ч
Все операции с БД асинхронные: chat_sync (uasync) → gui_bridge_call() →
GUI-поток (SQL) → uasync_post() → callback в chat_sync.