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

+50 -15
View File
@@ -1,6 +1,7 @@
/* topo_node_sqlite — хранение подписанных снимков узлов, адресов, каналов и блоков мемберов в общей SQLite.
* Модель/проверка подписей/registry — topo_node и member_sync; этот модуль реализует SQL-операции в uasync-потоке.
* snapshot_put проверяет подпись и сохраняет только более свежий timestamp; node_load предпочитает snapshot.
* Только snapshot_put/load проверяют подпись; остальные записи требуют проверенных вызывающим данных.
* snapshot_put сохраняет более свежий timestamp; node_load предпочитает snapshot.
* db/groups заимствованы. Возвращённый TOPO_NODE без registry ref: передать registry либо уничтожить через topo_node_destroy.
* Массивы out_ids/out_ch_ids/out_peers принадлежат вызывающему и освобождаются u_free. */
#ifndef TOPO_NODE_SQLITE_H
@@ -19,41 +20,55 @@
/* flags byte in wire format */
#define PEERS_FLAG_HAS_JOIN 0x01
/* Создать/обновить nodes, node_addresses, node_snapshots, channels; 0/-1. db открывает и закрывает вызывающий. */
int topo_node_sqlite_init(sqlite3* db);
/* Проверить и сохранить полный подписанный NODEINFO без групповых полей; старый/равный timestamp даёт 0 без замены. */
int topo_node_sqlite_snapshot_put(sqlite3* db, struct TOPO_NODE* ni);
/* Максимальный сохранённый timestamp (Unix microseconds), в том числе после инвалидирования record; 0 — нет/ошибка. */
uint64_t topo_node_sqlite_snapshot_timestamp(sqlite3* db, uint64_t node_id);
/* Незарегистрированный проверенный снимок, включая снимок без адресов; NULL — нет/невалиден/ошибка.
* groups и groups->instance->topo_groups должны обозначать один контейнер с пулами. */
struct TOPO_NODE* topo_node_sqlite_snapshot_load(sqlite3* db, struct TOPO_GROUPS* groups, uint64_t node_id);
/* Персистентная запись адресов узла в node_addresses (источник истины — BGP/topo_group).
* DELETE + INSERT из ni->v4_addrs/v6_addrs; mapping/inbound/source из подписанного SOCKMETA. */
* DELETE + INSERT из ni->v4_addrs/v6_addrs под savepoint, включая пустой набор; 0/-1, отказ откатывает замену.
* mapping/inbound/source берутся из SOCKMETA; проверку ni выполняет вызывающий. RTT при замене не сохраняется. */
int topo_node_sqlite_addrs_put(sqlite3* db, uint64_t node_id, struct TOPO_NODE* ni);
/* Есть ли у узла запись мембера хотя бы в одной peers_<ch> (реальный мембер с ключами). */
/* Есть ли строка хотя бы в одной peers_*: включая source=1 и deleted=1; 1/0, при ошибке также 0. */
int topo_node_sqlite_member_exists(sqlite3* db, uint64_t node_id);
/* Список channel_id (uint64) всех peers_<ch>, где узел является членом. 0=ок. */
/* Числовые ненулевые channel_id таблиц peers_*, содержащих строку узла; включает placeholders/deleted.
* 0/-1; пустой результат NULL/0, при ошибке частичный массив не выдаётся. */
int topo_node_sqlite_get_member_channels(sqlite3* db, uint64_t node_id,
uint64_t** out_ch_ids, int* out_count);
/* Создать peers_<id> и сохранить канал; 0/-1. Ключи — по 32, signature — 64 байта, privkey необязательны.
* При обновлении публичные ключи обязаны совпасть; имя/владелец/подпись заменяются, NULL privkey сохраняет прежний.
* Подпись/пары ключей не проверяет. Табличный суффикс: первые 63 символа, вне [A-Za-z0-9_] заменены на '_'. */
int topo_node_sqlite_channel_put(sqlite3* db, const char* channel_id,
const char* name, uint64_t owner_node_id,
const uint8_t* x25519_pub, const uint8_t* x25519_priv,
const uint8_t* ed25519_pub, const uint8_t* ed25519_priv,
const uint8_t* signature);
/* Подготовить локальные поля участника при создании и загрузке канала. */
/* Добавить radio_muted=0 в существующую peers_<id>, если поля ещё нет; 0/-1, идемпотентно. */
int topo_node_sqlite_member_local_init(sqlite3* db, const char* channel_id);
/* Полное локальное удаление канала: DROP peers_<id>/msg_<id>, DELETE channels,
* Удаляются feedback_<channel_id> и merkle_tree_hash всех datasets этого канала. 0=ок. */
* Удаляются feedback_<id> и merkle_tree_hash всех datasets канала (если таблица есть).
* 0/-1; savepoint откатывает в том числе DROP TABLE, внешняя транзакция сохраняется. */
int topo_node_sqlite_channel_delete(sqlite3* db, const char* channel_id);
/* Упрощённый плейсхолдер мембера от topo_group (source=1, не участвует в merkle-хеше). */
/* Вставить source=1 с нулевой join_sig; существующую строку не менять. 0/-1; ключи по 32 байта.
* Плейсхолдер не участвует в Merkle-хеше; канал/peers_<id> должны уже существовать. */
int topo_node_sqlite_member_placeholder_put(sqlite3* db, const char* channel_id, uint64_t node_id,
const uint8_t* x25519_pubkey, const uint8_t* ed25519_pubkey);
/* Обновляет только блок мембера (keys/join/update_sig/update_ts/userinfo), не трогая блок владельца.
* signed_by/signature — дерево приглашений: кто подписал pubkey узла (0 = корень, ключом группы). */
* signed_by/signature — дерево приглашений: кто подписал pubkey узла (0 = корень, ключом группы).
* 0/-1; подписи по 64, ключи по 32 байта. NULL join_sig даёт нулевую подпись, userinfo=NULL даёт ''.
* Сбрасывает source=0; свежесть/подписи проверяет вызывающий, owner/local-поля сохраняются. */
int topo_node_sqlite_member_block_put(sqlite3* db, const char* channel_id, uint64_t node_id,
const uint8_t* join_sig, uint64_t join_ts,
const uint8_t* update_sig, uint64_t update_ts,
@@ -64,25 +79,33 @@ int topo_node_sqlite_member_block_put(sqlite3* db, const char* channel_id, uint6
int topo_node_sqlite_member_get_sign(sqlite3* db, const char* channel_id, uint64_t node_id,
uint64_t* signed_by_out, uint8_t signature_out[64]);
/* Есть ли узел в таблице мемберов канала peers_<ch_id> (source 0 или 1). 1=да, 0=нет. */
/* Есть ли строка в peers_<id>, включая placeholders/deleted. 1/0; ошибка также даёт 0. */
int topo_node_sqlite_member_in_channel(sqlite3* db, const char* channel_id, uint64_t node_id);
/* Обновляет только блок владельца (adm_tags/adm_tags_sig/storage). */
/* Обновить owner-блок и deleted из adm_tags; 0/-1, отсутствующая строка даёт 0 без вставки.
* adm_tags/adm_tags_sig могут быть NULL; ненулевая подпись — 64 байта, проверяется вызывающим. */
int topo_node_sqlite_member_owner_put(sqlite3* db, const char* channel_id, uint64_t node_id,
const char* adm_tags, const uint8_t* adm_tags_sig, int storage);
/* Прочитать ровно 64 байта join_sig и join_ts; 0/-1, выходы обязательны и при неуспехе не меняются. */
int topo_node_sqlite_member_get_join(sqlite3* db, const char* channel_id, uint64_t node_id,
uint8_t* join_sig_out, uint64_t* join_ts_out);
/* Физически удалить строку участника; 0/-1. Отсутствующая строка даёт 0; tombstone/Merkle не создаёт. */
int topo_node_sqlite_member_del(sqlite3* db, const char* channel_id, uint64_t node_id);
/* Записать проверенные вызывающим ключи/имя в nodes; 0/-1. Пустое имя сохраняет прежнее при UPDATE.
* Ключи заменяются независимо от update_ts; update_ts хранит максимум, created_at сохраняется, если не NULL.
* Новая строка online=1, при UPDATE online сохраняется. Подпись и свежесть здесь не проверяются. */
int topo_node_sqlite_node_update_verified(sqlite3* db, uint64_t node_id,
const char* name, const uint8_t* x25519, const uint8_t* ed25519, uint64_t update_ts, time_t created_at);
/* Метаданные канала; 0/-1. Все выходы необязательны; name_sz включает NUL, ключи по 32, подпись 64 байта. */
int topo_node_sqlite_channel_get(sqlite3* db, const char* channel_id,
char* name_out, int name_sz, uint64_t* owner_node_id,
uint8_t* x25519_pub, uint8_t* ed25519_pub, uint8_t* signature);
/* Прочитать первые 32 байта Ed25519 privkey; 0/-1, отсутствующий/короткий ключ даёт -1. */
int topo_node_sqlite_channel_get_priv(sqlite3* db, const char* channel_id,
uint8_t* ed25519_priv_out);
@@ -90,32 +113,40 @@ int topo_node_sqlite_channel_get_priv(sqlite3* db, const char* channel_id,
int topo_node_sqlite_channel_get_privs(sqlite3* db, const char* channel_id,
uint8_t* x25519_priv_out, uint8_t* ed25519_priv_out);
/* Записать оба канальных privkey (импорт прав админа). */
/* Записать оба канальных privkey по 32 байта (импорт прав админа); 0/-1, без проверки пары/подписи.
* Отсутствующая строка даёт 0 без вставки; наличие канала проверяет вызывающий. */
int topo_node_sqlite_channel_set_privs(sqlite3* db, const char* channel_id,
const uint8_t* x25519_priv, const uint8_t* ed25519_priv);
/* Установить online=0/1 существующей строки; 0/-1. Отсутствующая строка даёт 0 без вставки. */
int topo_node_sqlite_node_set_online(sqlite3* db, uint64_t node_id, int online);
/* online из nodes; 0, если записи нет или запрос не выполнен. */
int topo_node_sqlite_node_get_online(sqlite3* db, uint64_t node_id);
/* Скопировать ровно 32 байта Ed25519 из nodes: 0 — найден, -1 — отсутствует/другая длина/ошибка SQLite.
* Значение байтов не определяет наличие ключа; криптографическую пригодность проверяет операция verify. */
int topo_node_sqlite_get_ed25519_pubkey(sqlite3* db, uint64_t node_id, uint8_t pubkey_out[32]);
/** Пересчитывает node_type во всех peers_* таблицах на основе addr_type из node_addresses */
/* Атомарно пересчитать node_type/storage/deleted во всех peers_* по адресам и adm_tags; 0/-1.
* node_type: 0 нет адресов, 1 прямой/локальный/strict, 2 NAT/contact, 4 supernode=yes.
* storage=yes действует только при наличии ADDR_TYPE_DIRECT, deleted берётся из adm_tags.deleted=yes. */
int topo_node_sqlite_nodeinfo_updated(sqlite3* db, uint64_t node_id);
/** Обновляет RTT узла: node_addresses.rtt и все peers_*.node_RTT */
/* Обновить node_addresses.rtt и все peers_*.node_RTT, единицы 0.1 ms; ошибки логируются, обновление неатомарно. */
void topo_node_sqlite_update_rtt(sqlite3* db, uint64_t node_id, uint16_t rtt);
/**
* @brief Загружает TOPO_NODE из SQLite (ref_count=0).
* @param groups нужен для выделения адресов из memory_pool
* @return TOPO_NODE* или NULL если узел не найден/нет адресов
* @return TOPO_NODE* или NULL, если нет пригодной записи/адресов bootstrap либо при ошибке
*
* Сначала загружает и проверяет подписанный snapshot; при его отсутствии собирает сведения nodes/node_addresses.
* Вызывающий передаёт владение через topo_node_registry_acquire() либо вызывает topo_node_destroy().
* Snapshot может быть без адресов. Bootstrap требует адресов и получает timestamp=0, профиль UNKNOWN.
* При отказе SQL/памяти частичная запись освобождается. groups->instance->topo_groups должен совпадать с groups.
* Вызывающий передаёт владение через topo_node_registry_store() либо вызывает topo_node_destroy().
*/
struct TOPO_NODE* topo_node_sqlite_node_load(sqlite3* db, struct TOPO_GROUPS* groups, uint64_t node_id);
/* Записать connected=0/1 существующего участника; 0/-1, без вставки отсутствующей строки. */
int topo_node_sqlite_set_connected(sqlite3* db, const char* channel_id, uint64_t node_id, int connected);
/* Кандидат CHAT-подключения: роль канальная, тип устройства из подписанного snapshot.
@@ -129,6 +160,10 @@ struct topo_connect_peer {
* 0/-1; массив принадлежит вызывающему и освобождается u_free(). */
int topo_node_sqlite_get_connect_peers(sqlite3* db, const char* channel_id,
struct topo_connect_peer** out, size_t* out_count);
/* Нижеследующие выборки: 0/-1, пусто/ошибка — NULL/0, частичный массив не выдаётся; освобождение u_free.
* connected/supernode ограничены участниками канала, self не исключают, deleted=1 исключают.
* public/local — глобальные адреса с записью nodes, исключены self и deleted=1 в указанном канале;
* наличие членства в канале не требуется. public: addr_type 1/2, local: 0/3; фильтра port!=0 нет. */
int topo_node_sqlite_get_connected_peers(sqlite3* db, const char* channel_id, uint64_t** out_ids, int* out_count);
int topo_node_sqlite_get_public_peers(sqlite3* db, const char* channel_id, uint64_t** out_ids, int* out_count, uint64_t self_node_id);
int topo_node_sqlite_get_local_peers(sqlite3* db, const char* channel_id, uint64_t** out_ids, int* out_count, uint64_t self_node_id);
+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.