From 42a58881ca8f02254383cc338db5f5fb9cf855aa Mon Sep 17 00:00:00 2001 From: evgeny Date: Sat, 24 Oct 2026 14:13:26 +0200 Subject: [PATCH] Document SQLite snapshots, projections and membership contracts --- src/routing_layer/topo_node_sqlite.h | 65 +++++++--- src/routing_layer/topo_node_sqlite_doc.md | 148 +++++++++++++++++----- 2 files changed, 167 insertions(+), 46 deletions(-) diff --git a/src/routing_layer/topo_node_sqlite.h b/src/routing_layer/topo_node_sqlite.h index cc14875c..bf35595f 100644 --- a/src/routing_layer/topo_node_sqlite.h +++ b/src/routing_layer/topo_node_sqlite.h @@ -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_ (реальный мембер с ключами). */ +/* Есть ли строка хотя бы в одной peers_*: включая source=1 и deleted=1; 1/0, при ошибке также 0. */ int topo_node_sqlite_member_exists(sqlite3* db, uint64_t node_id); -/* Список channel_id (uint64) всех peers_, где узел является членом. 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_ и сохранить канал; 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_, если поля ещё нет; 0/-1, идемпотентно. */ int topo_node_sqlite_member_local_init(sqlite3* db, const char* channel_id); /* Полное локальное удаление канала: DROP peers_/msg_, DELETE channels, - * Удаляются feedback_ и merkle_tree_hash всех datasets этого канала. 0=ок. */ + * Удаляются feedback_ и 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_ должны уже существовать. */ 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_ (source 0 или 1). 1=да, 0=нет. */ +/* Есть ли строка в peers_, включая 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); diff --git a/src/routing_layer/topo_node_sqlite_doc.md b/src/routing_layer/topo_node_sqlite_doc.md index 4c3d5b7e..0e891b69 100644 --- a/src/routing_layer/topo_node_sqlite_doc.md +++ b/src/routing_layer/topo_node_sqlite_doc.md @@ -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_`) -5. Добавлять участников в канал через `topo_node_sqlite_member_put()` -6. Читать участников из `peers_`; постраничная сетевая сериализация принадлежит `member_sync`. +Проверку подписи выполняют только topo_node_sqlite_snapshot_put/load. +Канальные метаданные, ключи участников и их версии проверяют вызывающие chat_sync/member_sync. +SQL-функции записи блоков сами не проверяют подписи и не отбрасывают старые версии. -**Нюансы:** -- Имена таблиц `peers_` санитизируются: все символы кроме `[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_` | -| `topo_node_sqlite_member_put(...)` | Добавляет/обновляет участника в `peers_` (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_` | -| `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_<id> | Блок участника, блок владельца и локальные поля конкретного канала | -**Структуры:** внешние — `sqlite3*`, `struct TOPO_GROUP_NODE`, `struct TOPO_NODE`; собственных структур нет. +topo_node_sqlite_init создаёт общие таблицы/индексы, обрабатывает прежние форматы проекции адресов +и восстанавливает deleted из adm_tags. peers_ создаётся при 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.