Document SQLite snapshots, projections and membership contracts

This commit is contained in:
evgeny committed 2026-10-24 14:13:26 +02:00
1 parent 19c3cc0434
commit 42a58881ca
2 files changed
+167 -46

No files matched your search

+117 -31
View File
@@ -1,38 +1,124 @@
# topo_node_sqlite — SQLite-персистентность топологии
# topo_node_sqlite — хранение топологии в SQLite
## 1. Назначение
Обеспечивает сохранение и восстановление топологии P2P-сети (ноды, адреса, каналы, участники каналов) в SQLite-базе. База используется как основной источник данных для chatgui и восстановления состояния между запусками utun.
## Назначение и владение
В отличие от LMDB-слоя (`topo_node_lmdb`), который хранит данные в бинарном формате BGP-сообщений, SQLite-слой работает с нормализованными таблицами и используется для клиентских целей — чтение данных chatgui, получение списка участников канала, проверка online-статуса.
Модуль работает с общей SQLite ядра: подписанные снимки узлов, производные адреса/состояния,
описания каналов и строки участников. Базу открывает и закрывает владелец ядра; все операции
этого модуля выполняются в его uasync-потоке. Отдельного LMDB-хранилища топологии здесь нет.
## 2. Как пользоваться
1. Получить готовый `sqlite3* db` (открывается извне, обычно в `chat_sync.c`)
2. Вызвать `topo_node_sqlite_init(db)` для создания таблиц
3. Сохранять/обновлять ноды через `topo_node_sqlite_node_put()`
4. Сохранять каналы через `topo_node_sqlite_channel_put()` (автоматически создаёт таблицу `peers_<channel_id>`)
5. Добавлять участников в канал через `topo_node_sqlite_member_put()`
6. Читать участников из `peers_<channel_id>`; постраничная сетевая сериализация принадлежит `member_sync`.
Проверку подписи выполняют только topo_node_sqlite_snapshot_put/load.
Канальные метаданные, ключи участников и их версии проверяют вызывающие chat_sync/member_sync.
SQL-функции записи блоков сами не проверяют подписи и не отбрасывают старые версии.
**Нюансы:**
- Имена таблиц `peers_<channel_id>` санитизируются: все символы кроме `[a-zA-Z0-9_]` заменяются на `_`
- Сетевой формат участников описан в `src/chat/member_sync_doc.md`; SQLite-слой не дублирует сериализатор.
- Ключи в BLOB-полях всегда 32 байта (x25519/ed25519), подписи — 64 байта
- Функции `online` и `get_ed25519_pubkey` — простые SELECT-запросы без JOIN
Возвращённый TOPO_NODE не зарегистрирован и имеет нулевой refcount.
Передайте владение через topo_node_registry_store либо вызовите topo_node_destroy(groups, node).
Массивы идентификаторов и topo_connect_peer освобождаются через u_free.
db, groups и входные строки/объекты остаются у вызывающего.
## 3. API
## Таблицы и версии
| Функция | Описание |
|---------|----------|
| `topo_node_sqlite_init(db)` | Создаёт таблицы `nodes`, `node_addresses`, `channels` (если не существуют) |
| `topo_node_sqlite_node_put(db, nq)` | Сохраняет ноду и её адреса (IPv4/IPv6) в транзакции; старые адреса удаляются |
| `topo_node_sqlite_channel_put(...)` | Сохраняет канал в `channels` + создаёт таблицу `peers_<ch_id>` |
| `topo_node_sqlite_member_put(...)` | Добавляет/обновляет участника в `peers_<ch_id>` (INSERT OR REPLACE) |
| `topo_node_sqlite_member_get_join(db, ch_id, node_id, ...)` | Читает join_sig и join_ts участника канала |
| `topo_node_sqlite_member_del(db, ch_id, node_id)` | Удаляет участника из `peers_<ch_id>` |
| `topo_node_sqlite_node_update_verified(...)` | Обновляет ключи и имя ноды по условию `update_ts < новое_значение` (защита от stale-данных) |
| `topo_node_sqlite_channel_get(...)` | Читает метаданные канала (имя, владелец, ключи, подпись) |
| `topo_node_sqlite_node_set_online(db, id, online)` | Устанавливает флаг `online` для ноды |
| `topo_node_sqlite_node_get_online(db, id)` | Возвращает 1 если нода online |
| `topo_node_sqlite_get_ed25519_pubkey(db, id, out)` | Извлекает ed25519-публичный ключ ноды (32 байта) |
| Таблица | Назначение |
| --- | --- |
| node_snapshots | node_id, максимальный timestamp, полный подписанный NODEINFO в BLOB record |
| nodes | Проекция имени и ключей, online/last_seen, update_ts, created_at; не замена подписанному снимку |
| node_addresses | IPv4/IPv6 endpoints, protocol, socket_id, address_kind, NAT-профиль, классификация, RTT и REALITY options |
| channels | Имя, владелец, публичные/необязательные приватные ключи, подпись канала и локальные поля |
| peers_&lt;id&gt; | Блок участника, блок владельца и локальные поля конкретного канала |
**Структуры:** внешние — `sqlite3*`, `struct TOPO_GROUP_NODE`, `struct TOPO_NODE`; собственных структур нет.
topo_node_sqlite_init создаёт общие таблицы/индексы, обрабатывает прежние форматы проекции адресов
и восстанавливает deleted из adm_tags. peers_<id> создаётся при channel_put;
member_local_init идемпотентно добавляет radio_muted.
В снимке timestamp — Unix microseconds и версия всей подписанной записи.
snapshot_put проверяет подпись и заменяет BLOB только при большем timestamp.
Старый или равный timestamp даёт успех без замены. При смене формата BLOB может инвалидироваться
без сброса сохранённого timestamp, чтобы новая локальная публикация оставалась новее.
node_update_verified записывает уже проверенные ключи и непустое имя независимо от update_ts.
В nodes.update_ts сохраняется максимум прежнего и входного значения; это не фильтр замены ключей.
created_at сохраняется, если уже не NULL. Новая строка получает online=1,
обновление существующей строки online не меняет.
## Снимок и bootstrap
topo_node_sqlite_node_load сначала загружает и проверяет снимок. Валидный снимок может содержать
пустой список адресов: это полноценное опубликованное состояние, а не повод вернуть старые адреса.
Если пригодного снимка нет, функция собирает bootstrap из nodes и node_addresses.
Для этого требуется хотя бы один адрес. Ключи/имя копируются из проекции, timestamp остаётся 0,
профили сокетов сбрасываются в UNKNOWN; сохранённая SQL-классификация не считается подписанным доказательством.
IPv4/IPv6 сохраняются отдельно, REALITY восстанавливается из options, порт 0 сохраняется как известный TCP IP.
Пулы берутся из groups; groups->instance->topo_groups должен указывать на тот же контейнер.
При ошибке SQL или памяти частичные списки, имя, REALITY и statement освобождаются; возвращается NULL.
## Адреса, классификация и RTT
addrs_put заменяет все адреса узла одним savepoint: DELETE, затем INSERT IPv4/IPv6.
Пустой список очищает проекцию. При отказе замена откатывается.
Профиль mapping/inbound/source берётся из SOCKMETA, REALITY — из записи соответствующего socket_id.
Функция не проверяет подпись входного узла. Замена адресов не сохраняет прежний RTT.
address_kind — INTERFACE/NAT/REAL, отдельно от вычисляемого addr_type.
Классификация учитывает IP, protocol, конфигурационный тип сокета и профиль доступности.
Адрес с port=0 хранится, но не является готовым endpoint для подключения.
nodeinfo_updated под savepoint обновляет node_type, storage, deleted во всех peers_*:
| node_type | Основание |
| --- | --- |
| 0 | Нет адресов или классификация неизвестна |
| 1 | NETIF, DIRECT или NAT_STRICT |
| 2 | NAT_EIM, NAT, CONTACT или EIM_UNVERIFIED |
| 4 | adm_tags.supernode=yes |
При выборе адреса DIRECT имеет приоритет, иначе берётся минимальный addr_type.
storage=yes применяется только при DIRECT; deleted выводится из adm_tags.deleted=yes.
При отказе одной канальной записи весь пересчёт узла откатывается, функция возвращает -1.
update_rtt обновляет node_addresses.rtt и peers_*.node_RTT в единицах 0.1 ms.
Обновление этих таблиц неатомарно; ошибки логируются. Online и connected меняются отдельными функциями.
## Каналы и участники
channel_put требует публичные ключи по 32 байта и подпись 64 байта.
При конфликте channel_id публичные ключи должны совпасть: меняются имя/владелец/подпись,
переданные privkey заменяют прежние, NULL сохраняет их. Криптографическую проверку делает вызывающий.
Суффикс таблиц строится из первых 63 символов channel_id: вне [A-Za-z0-9_] заменяются на '_'.
Штатные channel_id — десятичные uint64. Строка channels.channel_id хранит исходное значение.
member_placeholder_put вставляет source=1 с нулевой join_sig; существующую строку не меняет.
member_block_put заменяет ключи/join/update/userinfo/дерево приглашений и устанавливает source=0,
сохраняя owner-блок и локальные поля. NULL join_sig записывается как 64 нулевых байта.
member_owner_put меняет adm_tags/подпись/storage/deleted без вставки отсутствующей строки.
Placeholder не участвует в Merkle-хеше; сетевой формат и проверка версий принадлежат member_sync.
member_exists, member_in_channel, get_member_channels учитывают любое наличие строки,
в том числе source=1 и deleted=1. Это не проверка полной подписанной записи участника.
member_get_join требует ровно 64 байта подписи; channel_get проверяет размер запрошенных ключей/подписи.
member_del физически удаляет строку без tombstone и пересчёта Merkle.
channel_delete удаляет peers/msg/feedback, канал с приватными ключами и хеши datasets его namespace
(если таблица Merkle существует). Все действия, включая DROP TABLE, защищены savepoint:
ошибка возвращает -1 и откатывает удаление, сохраняя внешнюю транзакцию.
## Выбор пиров
Все выборки возвращают принадлежащий вызывающему массив. Пустой результат — NULL/0.
Ошибка чтения/памяти — -1 без публикации частичного массива.
| API | Выборка |
| --- | --- |
| get_connect_peers | Все неудалённые участники канала, включая self; connected, затем известный RTT/RTT/node_id |
| get_connected_peers | connected=1, deleted=0 в указанном канале |
| get_supernode_peers | node_type=4, deleted=0 в указанном канале |
| get_public_peers | Глобальные адреса addr_type 1/2 с записью nodes, кроме self и удалённых в указанном канале |
| get_local_peers | Аналогично public, но addr_type 0/3 |
Public/local не требуют членства в указанном канале и не фильтруют port=0.
Для get_connect_peers роль supernode берётся из adm_tags, client_type — из сохранённого
подписанного snapshot; отсутствие снимка даёт UINT8_MAX. Адреса выборку не ограничивают.
Сам запрос не повторяет криптографическую проверку BLOB; подпись проверяется при сохранении снимка.
Полный список функций, размеры буферов и возвраты — в topo_node_sqlite.h.