13 KiB
topo_node_sqlite — хранение топологии в SQLite
Назначение и владение
Модуль работает с общей SQLite ядра: подписанные снимки узлов, производные адреса/состояния, описания каналов и строки участников. Базу открывает и закрывает владелец ядра; все операции этого модуля выполняются в его uasync-потоке. Отдельного LMDB-хранилища топологии здесь нет.
Проверку подписи выполняют только topo_node_sqlite_snapshot_put/load. Канальные метаданные, ключи участников и их версии проверяют вызывающие chat_sync/member_sync. SQL-функции записи блоков сами не проверяют подписи и не отбрасывают старые версии.
Возвращённый TOPO_NODE не зарегистрирован и имеет нулевой refcount. Передайте владение через topo_node_registry_store либо вызовите topo_node_destroy(groups, node). Массивы идентификаторов и topo_connect_peer освобождаются через u_free. db, groups и входные строки/объекты остаются у вызывающего.
Таблицы и версии
| Таблица | Назначение |
|---|---|
| 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_<id> | Блок участника, блок владельца и локальные поля конкретного канала |
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 не меняет. Проверяются bind/step/changes=1/finalize;подавленная trigger запись возвращает ошибку. member_owner_put также требует ровно одну изменённую строку,проверяет все SQL-этапы и принимает NULL adm_tags/подпись. Эти операции используют транзакцию caller: caller откатывает ошибку до публикации runtime-состояния и уведомлений.
Снимок и 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 сохраняет их. Криптографическую проверку делает вызывающий. SAVEPOINT channel_put атомарно объединяет описание с peers-таблицей, локальной схемой и индексами. Проверяются выделение SQL, все bind/step/changes=1/finalize и RELEASE; при отказе частичная схема не фиксируется. Если ROLLBACK TO сам завершился ошибкой, RELEASE не выполняется: открытая транзакция требует отката/закрытия БД владельцем.
Суффикс таблиц строится из первых 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 нулевых байта. Все 11 bind, step, changes=1 и finalize проверяются; функция работает в транзакции вызывающего. member_owner_put меняет adm_tags/подпись/storage/deleted без вставки отсутствующей строки. Placeholder не участвует в Merkle-хеше; сетевой формат и проверка версий принадлежат member_sync.
member_exists, member_in_channel, get_member_channels учитывают любое наличие строки, в том числе source=1 и deleted=1. Это не проверка полной подписанной записи участника. member_allowed требует source=0 и deleted=0: его используют допуск CHAT-сессий и запуск связи после INVITE_INFO. Запись в БД должна быть проверена перед сохранением; SQL-запрос подписи повторно не проверяет. member_get_join требует ровно 64 байта подписи; channel_get проверяет размер запрошенных ключей/подписи. channel_get и get_ed25519_pubkey различают отсутствие строки/ключа (TOPO_SQLITE_LOOKUP_MISSING=-1) и отказ аргументов/SQL/повреждённый ненулевой размер (TOPO_SQLITE_LOOKUP_ERROR=-2). Успех — 0; key lookup не меняет выходной ключ при отказе. Проверяющий подпись caller может отложить проверку при отсутствии зависимости, но должен прервать её при SQL-ошибке. channel_get_priv/channel_get_privs принимают только BLOB ровно32 байта для каждого ключа. Они проверяют prepare/bind/step/finalize,не меняют выходы при отказе и очищают временные копии ключей.
member_del физически удаляет строку без tombstone и пересчёта Merkle. channel_delete удаляет peers/msg/feedback, канал с приватными ключами и хеши datasets его namespace (если таблица Merkle существует). Все действия, включая DROP TABLE, защищены savepoint: ошибка возвращает -1 и откатывает удаление, сохраняя внешнюю транзакцию.
Выбор пиров
Все выборки возвращают принадлежащий вызывающему массив. Пустой результат — NULL/0. Ошибка чтения/памяти — -1 без публикации частичного массива.
| API | Выборка |
|---|---|
| get_connect_peers | Полные неудалённые участники (source=0, deleted=0), включая 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.