#include "topo_node_sqlite.h" #include "topo_node.h" #include "topo_group.h" #include "etcp_connections.h" #include "../config_parser.h" #include "../lib/debug_config.h" #include "../lib/mem.h" #include "../lib/memory_pool.h" #include "../lib/json_flat.h" #include #include #include #define PEERS_JOIN_SIG_SIZE 64 #define PEERS_JOIN_TS_SIZE 8 static void sanitize_ch_id(const char* ch_id, char* out, size_t out_sz) { size_t i = 0; while (*ch_id && i < out_sz - 1) { char c = *ch_id++; if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_') out[i++] = c; else out[i++] = '_'; } out[i] = '\0'; } static void peers_table_name(const char* ch_id, char* buf, size_t sz) { char san[64]; sanitize_ch_id(ch_id, san, sizeof(san)); snprintf(buf, sz, "peers_%s", san); } static int adm_tags_deleted(const char* tags) { if (!tags || !tags[0]) return 0; char buf[16]; return json_flat_get(tags, "deleted", buf, sizeof(buf)) == 0 && strcmp(buf, "yes") == 0; } /* ── Классификация адресов (локально, по IP) ── */ static int addr_type_from_ip(int family, const uint8_t* addr) { if (!addr) return ADDR_TYPE_NETIF; if (family == 4) { uint8_t b0 = addr[0], b1 = addr[1]; if (b0 == 0 || b0 == 10 || b0 == 127) return ADDR_TYPE_NETIF; if (b0 == 169 && b1 == 254) return ADDR_TYPE_NETIF; if (b0 >= 224 && b0 <= 239) return ADDR_TYPE_NETIF; if (b0 == 172 && b1 >= 16 && b1 <= 31) return ADDR_TYPE_NETIF; if (b0 == 100 && b1 >= 64 && b1 <= 127) return ADDR_TYPE_NETIF; /* 100.64.0.0/10 (CGNAT) */ if (b0 == 192 && b1 == 168) return ADDR_TYPE_NETIF; { uint32_t ip; memcpy(&ip, addr, 4); if (ip == 0 || ip == 0xffffffff) return ADDR_TYPE_NETIF; } return ADDR_TYPE_DIRECT; } else { if (addr[0] == 0xfe && (addr[1] & 0xc0) == 0x80) return ADDR_TYPE_NETIF; if (addr[0] == 0xfc || addr[0] == 0xfd) return ADDR_TYPE_NETIF; { static const uint8_t z[16]; if (!memcmp(addr, z, 16)) return ADDR_TYPE_NETIF; } { static const uint8_t lb[16]={0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}; if (!memcmp(addr,lb,16)) return ADDR_TYPE_NETIF; } return ADDR_TYPE_DIRECT; } } /* Итоговая классификация: конфиг + автоопределение + IP (всё локально). */ static int addr_type_classify(int kind, int config_type, int global, const struct address_profile *profile) { if (config_type == CFG_SERVER_TYPE_PRIVATE || config_type == CFG_SERVER_TYPE_LOCAL) return ADDR_TYPE_NETIF; if (!profile) return ADDR_TYPE_NAT; if (profile->mapping == ADDRESS_MAP_EIM) return ADDR_TYPE_NAT_EIM; if (profile->mapping == ADDRESS_MAP_DEPENDENT) return ADDR_TYPE_NAT_STRICT; /* Публичный адрес интерфейса известен без определения входящих и не становится NAT из-за UNKNOWN. */ if (global && kind == TOPO_ADDR_INTERFACE && (profile->mapping == ADDRESS_MAP_UNKNOWN || (profile->mapping == ADDRESS_MAP_UNCHANGED && profile->inbound == ADDRESS_IN_UNKNOWN))) return ADDR_TYPE_NETIF; if (!global) return config_type == CFG_SERVER_TYPE_NAT ? ADDR_TYPE_NAT : ADDR_TYPE_NETIF; if (profile->mapping == ADDRESS_MAP_UNCHANGED && profile->inbound == ADDRESS_IN_OPEN) return ADDR_TYPE_DIRECT; if (profile->mapping == ADDRESS_MAP_UNCHANGED && profile->inbound == ADDRESS_IN_CONTACT) return ADDR_TYPE_CONTACT; return ADDR_TYPE_NAT; } static const struct address_profile *smeta4_lookup(struct TOPO_SOCKMETA4 *head, uint8_t sid, uint8_t *config) { *config = CFG_SERVER_TYPE_UNKNOWN; for (const struct TOPO_SOCKMETA4 *meta = head; meta; meta = meta->next) if (meta->id == sid) { *config = meta->config_type; return &meta->address_profile; } return NULL; } static const struct address_profile *smeta6_lookup(struct TOPO_SOCKMETA6 *head, uint8_t sid, uint8_t *config) { *config = CFG_SERVER_TYPE_UNKNOWN; for (const struct TOPO_SOCKMETA6 *meta = head; meta; meta = meta->next) if (meta->id == sid) { *config = meta->config_type; return &meta->address_profile; } return NULL; } /* reality-параметры сокета → фиксированный blob для node_addresses.options. Возвращает 1 и заполняет opts, если у сокета есть reality; иначе 0. */ static int reality_opts_build(struct TOPO_NODE* ni, uint8_t sid, struct TOPO_ADDR_REALITY_OPTS* opts) { if (!ni || !opts) return 0; for (const struct TOPO_REALITY_SOCK* r = ni->reality_socks; r; r = r->next) { if (r->socket_id != sid) continue; memcpy(opts->short_id, r->short_id, 8); memcpy(opts->server_pubkey, r->server_pubkey, 32); memcpy(opts->version, r->version, 3); memcpy(opts->server_name, r->server_name, TOPO_REALITY_SNAME_MAX); return 1; } return 0; } /* ── Персистенция адресов узла (источник истины — BGP/topo_group) ── */ static int address_row_put(sqlite3_stmt *statement, struct TOPO_NODE *node, uint64_t node_id, int family, const uint8_t *address, uint16_t port, uint8_t kind, uint8_t socket_id, uint8_t protocol, uint8_t config, const struct address_profile *profile) { static const struct address_profile unknown = {0}; if (!profile) profile = &unknown; int summary = addr_type_classify(kind, config, addr_type_from_ip(family, address) == ADDR_TYPE_DIRECT, profile); struct TOPO_ADDR_REALITY_OPTS reality; int has_reality = (protocol & TOPO_PROTO_REALITY) && reality_opts_build(node, socket_id, &reality); sqlite3_bind_int64(statement, 1, (sqlite3_int64)node_id); sqlite3_bind_int(statement, 2, family); sqlite3_bind_int(statement, 3, protocol); sqlite3_bind_blob(statement, 4, address, family == 4 ? 4 : 16, SQLITE_STATIC); sqlite3_bind_int(statement, 5, port); sqlite3_bind_int(statement, 6, summary); sqlite3_bind_int(statement, 7, config); sqlite3_bind_int(statement, 8, profile->mapping); sqlite3_bind_int(statement, 9, socket_id); if (has_reality) sqlite3_bind_blob(statement, 10, &reality, sizeof(reality), SQLITE_STATIC); else sqlite3_bind_null(statement, 10); sqlite3_bind_int(statement, 11, kind); sqlite3_bind_int(statement, 12, profile->inbound); sqlite3_bind_int(statement, 13, profile->source); int result = sqlite3_step(statement); sqlite3_reset(statement); if (result != SQLITE_DONE) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "address projection insert failed node=%016llx family=%d socket=%u kind=%u" " mapping=%s inbound=%s: %s", (unsigned long long)node_id, family, socket_id, kind, address_mapping_name(profile->mapping), address_inbound_name(profile->inbound), sqlite3_errmsg(sqlite3_db_handle(statement))); return -1; } return 0; } int topo_node_sqlite_addrs_put(sqlite3 *db, uint64_t node_id, struct TOPO_NODE *node) { if (!db || !node) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "address projection: invalid arguments"); return -1; } if (sqlite3_exec(db, "SAVEPOINT node_address_projection", NULL, NULL, NULL) != SQLITE_OK) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "address projection savepoint failed: %s", sqlite3_errmsg(db)); return -1; } sqlite3_stmt *statement = NULL; if (sqlite3_prepare_v2(db, "DELETE FROM node_addresses WHERE node_id=?", -1, &statement, NULL) != SQLITE_OK) goto fail; sqlite3_bind_int64(statement, 1, (sqlite3_int64)node_id); if (sqlite3_step(statement) != SQLITE_DONE) goto fail; sqlite3_finalize(statement); statement = NULL; if (sqlite3_prepare_v2(db, "INSERT INTO node_addresses(node_id,family,protocol,address,port,addr_type,config_type," "nat_mapping,socket_id,options,address_kind,nat_inbound,nat_source)" " VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?)", -1, &statement, NULL) != SQLITE_OK) goto fail; int written = 0; for (const struct TOPO_ADDR4 *address = node->v4_addrs; address; address = address->next) { uint8_t config; const struct address_profile *profile = smeta4_lookup(node->v4_sock_meta, address->socket_id, &config); if (address_row_put(statement, node, node_id, 4, address->addr, address->port, address->type, address->socket_id, address->protocol, config, profile)) goto fail; written++; } for (const struct TOPO_ADDR6 *address = node->v6_addrs; address; address = address->next) { uint8_t config; const struct address_profile *profile = smeta6_lookup(node->v6_sock_meta, address->socket_id, &config); if (address_row_put(statement, node, node_id, 6, address->addr, address->port, address->type, address->socket_id, address->protocol, config, profile)) goto fail; written++; } sqlite3_finalize(statement); statement = NULL; if (sqlite3_exec(db, "RELEASE node_address_projection", NULL, NULL, NULL) != SQLITE_OK) goto fail; DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "address projection stored node=%016llx endpoints=%d", (unsigned long long)node_id, written); return 0; fail: DEBUG_ERROR(DEBUG_CATEGORY_BGP, "address projection failed node=%016llx: %s", (unsigned long long)node_id, sqlite3_errmsg(db)); sqlite3_finalize(statement); if (sqlite3_exec(db, "ROLLBACK TO node_address_projection; RELEASE node_address_projection", NULL, NULL, NULL) != SQLITE_OK) DEBUG_ERROR(DEBUG_CATEGORY_BGP, "address projection rollback failed: %s", sqlite3_errmsg(db)); return -1; } int topo_node_sqlite_member_exists(sqlite3* db, uint64_t node_id) { if (!db) return 0; sqlite3_stmt* s = NULL; if (sqlite3_prepare_v2(db, "SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'peers\\_%' ESCAPE '\\'", -1, &s, NULL) != SQLITE_OK) return 0; char sql[256]; int found = 0; while (!found && sqlite3_step(s) == SQLITE_ROW) { const char* tbl = (const char*)sqlite3_column_text(s, 0); if (!tbl) continue; snprintf(sql, sizeof(sql), "SELECT 1 FROM \"%s\" WHERE node_id=?", tbl); sqlite3_stmt* m = NULL; if (sqlite3_prepare_v2(db, sql, -1, &m, NULL) == SQLITE_OK) { sqlite3_bind_int64(m, 1, (sqlite3_int64)node_id); if (sqlite3_step(m) == SQLITE_ROW) found = 1; sqlite3_finalize(m); } } sqlite3_finalize(s); return found; } int topo_node_sqlite_get_member_channels(sqlite3* db, uint64_t node_id, uint64_t** out_ch_ids, int* out_count) { if (!db || !out_ch_ids || !out_count) return -1; *out_ch_ids = NULL; *out_count = 0; sqlite3_stmt* s = NULL; if (sqlite3_prepare_v2(db, "SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'peers\\_%' ESCAPE '\\'", -1, &s, NULL) != SQLITE_OK) return -1; uint64_t* ids = NULL; int count = 0, cap = 0; char sql[256]; while (sqlite3_step(s) == SQLITE_ROW) { const char* tbl = (const char*)sqlite3_column_text(s, 0); if (!tbl || strncmp(tbl, "peers_", 6) != 0) continue; snprintf(sql, sizeof(sql), "SELECT 1 FROM \"%s\" WHERE node_id=?", tbl); sqlite3_stmt* m = NULL; if (sqlite3_prepare_v2(db, sql, -1, &m, NULL) == SQLITE_OK) { sqlite3_bind_int64(m, 1, (sqlite3_int64)node_id); int is_member = (sqlite3_step(m) == SQLITE_ROW); sqlite3_finalize(m); if (is_member) { uint64_t ch = strtoull(tbl + 6, NULL, 10); if (ch != 0) { if (count == cap) { cap = cap ? cap * 2 : 8; uint64_t* tmp = u_realloc(ids, (size_t)cap * sizeof(uint64_t)); if (!tmp) { u_free(ids); sqlite3_finalize(s); return -1; } ids = tmp; } ids[count++] = ch; } } } } sqlite3_finalize(s); *out_ch_ids = ids; *out_count = count; return 0; } int topo_node_sqlite_init(sqlite3* db) { if (!db) return -1; /* Проекция адресов производная. Старый формат пересоздаём, не интерпретируя NAT_TYPE_* как новый профиль. */ sqlite3_stmt *schema = NULL; int legacy_profile = 0; if (sqlite3_prepare_v2(db, "SELECT 1 FROM sqlite_master WHERE type='table' AND name='node_addresses'", -1, &schema, NULL) != SQLITE_OK) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "address schema inspection failed: %s", sqlite3_errmsg(db)); return -1; } int exists = sqlite3_step(schema) == SQLITE_ROW; sqlite3_finalize(schema); schema = NULL; if (exists) { int compatible = sqlite3_prepare_v2(db, "SELECT nat_mapping,nat_inbound,nat_source,address_kind FROM node_addresses LIMIT 0", -1, &schema, NULL); sqlite3_finalize(schema); if (compatible == SQLITE_ERROR) { if (sqlite3_exec(db, "DROP TABLE node_addresses", NULL, NULL, NULL) != SQLITE_OK) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "old address projection reset failed: %s", sqlite3_errmsg(db)); return -1; } DEBUG_INFO(DEBUG_CATEGORY_BGP, "old address projection reset; signed NODEINFO will rebuild endpoint profiles"); } else if (compatible != SQLITE_OK) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "address schema read failed: %s", sqlite3_errmsg(db)); return -1; } else { if (sqlite3_prepare_v2(db, "SELECT count(*) FROM pragma_table_info('node_addresses')" " WHERE name IN ('nat_evidence','nat_checked')", -1, &schema, NULL) != SQLITE_OK || sqlite3_step(schema) != SQLITE_ROW) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "address profile schema inspection failed: %s", sqlite3_errmsg(db)); sqlite3_finalize(schema); return -1; } int fields = sqlite3_column_int(schema, 0); sqlite3_finalize(schema); schema = NULL; if (fields == 1) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "incomplete legacy NAT profile schema"); return -1; } legacy_profile = fields == 2; } } const char* sql = "CREATE TABLE IF NOT EXISTS node_snapshots (" "node_id INTEGER PRIMARY KEY, timestamp INTEGER NOT NULL, record BLOB NOT NULL);" "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," " online INTEGER DEFAULT 0," " client_type INTEGER NOT NULL DEFAULT 0," " update_ts INTEGER DEFAULT 0," " created_at INTEGER DEFAULT (unixepoch())" ");" "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," " addr_type INTEGER DEFAULT 0," " config_type INTEGER DEFAULT 0," " nat_mapping INTEGER NOT NULL DEFAULT 0," " nat_inbound INTEGER NOT NULL DEFAULT 0," " nat_source INTEGER NOT NULL DEFAULT 0," " address_kind INTEGER NOT NULL DEFAULT 0," " socket_id INTEGER NOT NULL DEFAULT 0," " options BLOB," // reality-параметры (TOPO_ADDR_REALITY_OPTS) " created_at INTEGER DEFAULT (unixepoch())" ");" "CREATE INDEX IF NOT EXISTS idx_na_node ON node_addresses(node_id);" "CREATE UNIQUE INDEX IF NOT EXISTS idx_na_unique ON node_addresses(node_id,family,socket_id,protocol,address,port,address_kind);" "CREATE INDEX IF NOT EXISTS idx_na_addr_lookup ON node_addresses(family, address, port, socket_id);" "CREATE TABLE IF NOT EXISTS channels (" " channel_id TEXT PRIMARY KEY," // ID канала " name TEXT NOT NULL," // Название канала " owner_node_id INTEGER," // node id создателя (надо выпилить) " x25519_pubkey BLOB NOT NULL," // ключ для шифрования канала " x25519_privkey BLOB," // если админ - то ключ канала " ed25519_pubkey BLOB NOT NULL," // ключ для проверки подписи " ed25519_privkey BLOB," // ключ для создания подписи " signature BLOB NOT NULL," // подпись при создании канал приватным ключом канала channel_id(NUL-terminated) || name(NUL-terminated) || owner_node_id(8 байт, LE uint64) || x25519_pubkey(32 байта) || ed25519_pubkey(32 байта) " avatar_version INTEGER NOT NULL DEFAULT 0," " avatar_data BLOB," " avatar_hash BLOB," " avatar_signature BLOB," " last_read_msg_id INTEGER," // для скроллинга чата " last_pos_msg_id INTEGER," // для скроллинга чата " last_msg_at INTEGER DEFAULT 0," // время последнего сообщения (unix timestamp) " created_at INTEGER DEFAULT (unixepoch())" // когда создан ");"; char* err = NULL; int rc = sqlite3_exec(db, sql, NULL, NULL, &err); if (rc != SQLITE_OK) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "topo_node_sqlite_init: %s", err); sqlite3_free(err); return -1; } /* Старые подписи непригодны; адреса и максимальные версии остаются для bootstrap и новой публикации. */ if (legacy_profile) { const char *migration = "SAVEPOINT compact_nat_profile;" "ALTER TABLE node_addresses DROP COLUMN nat_evidence;" "ALTER TABLE node_addresses DROP COLUMN nat_checked;" "UPDATE node_addresses SET nat_mapping=0,nat_inbound=0,nat_source=0," "addr_type=CASE WHEN addr_type=0 OR config_type IN (3,4) THEN 0 ELSE 4 END;" "UPDATE node_snapshots SET record=X'';" "RELEASE compact_nat_profile;"; if (sqlite3_exec(db, migration, NULL, NULL, NULL) != SQLITE_OK) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "compact NAT profile migration failed: %s", sqlite3_errmsg(db)); if (sqlite3_exec(db, "ROLLBACK TO compact_nat_profile; RELEASE compact_nat_profile", NULL, NULL, NULL) != SQLITE_OK) DEBUG_ERROR(DEBUG_CATEGORY_BGP, "compact NAT profile migration rollback failed: %s", sqlite3_errmsg(db)); return -1; } DEBUG_INFO(DEBUG_CATEGORY_BGP, "NAT profile compacted to mapping/inbound/source; bootstrap addresses and node versions preserved"); } /* migration: добавить колонку options в уже существующую node_addresses (idempotent) */ { sqlite3_stmt* cs = NULL; int has_options = 0; if (sqlite3_prepare_v2(db, "PRAGMA table_info(node_addresses)", -1, &cs, NULL) == SQLITE_OK) { while (sqlite3_step(cs) == SQLITE_ROW) { const char* cn = (const char*)sqlite3_column_text(cs, 1); if (cn && strcmp(cn, "options") == 0) { has_options = 1; break; } } sqlite3_finalize(cs); } if (!has_options) { char* aerr = NULL; int arc = sqlite3_exec(db, "ALTER TABLE node_addresses ADD COLUMN options BLOB", NULL, NULL, &aerr); if (arc != SQLITE_OK) { DEBUG_WARN(DEBUG_CATEGORY_BGP, "topo_node_sqlite_init: ALTER TABLE ADD options failed: %s", aerr ? aerr : "?"); if (aerr) sqlite3_free(aerr); } else { DEBUG_INFO(DEBUG_CATEGORY_BGP, "topo_node_sqlite_init: migrated node_addresses (added options column)"); } } } /* backfill: вывести deleted из adm_tags (idempotent, самовосстановление) */ { sqlite3_stmt* s = NULL; if (sqlite3_prepare_v2(db, "SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'peers\\_%' ESCAPE '\\'", -1, &s, NULL) == SQLITE_OK) { while (sqlite3_step(s) == SQLITE_ROW) { const char* tbl = (const char*)sqlite3_column_text(s, 0); if (!tbl) continue; sqlite3_stmt* sb = NULL; char bsql[256]; snprintf(bsql, sizeof(bsql), "SELECT node_id, adm_tags, deleted FROM \"%s\"", tbl); if (sqlite3_prepare_v2(db, bsql, -1, &sb, NULL) == SQLITE_OK) { sqlite3_stmt* ub = NULL; char usql[256]; snprintf(usql, sizeof(usql), "UPDATE \"%s\" SET deleted=? WHERE node_id=?", tbl); int up_ok = sqlite3_prepare_v2(db, usql, -1, &ub, NULL) == SQLITE_OK; int fixed = 0; while (sqlite3_step(sb) == SQLITE_ROW) { uint64_t nid = (uint64_t)sqlite3_column_int64(sb, 0); const char* tags = (const char*)sqlite3_column_text(sb, 1); int cur = sqlite3_column_int(sb, 2); int d = adm_tags_deleted(tags); if (d != cur) { if (up_ok) { sqlite3_bind_int(ub, 1, d); sqlite3_bind_int64(ub, 2, (sqlite3_int64)nid); sqlite3_step(ub); sqlite3_reset(ub); } fixed++; } } if (ub) sqlite3_finalize(ub); sqlite3_finalize(sb); if (fixed) DEBUG_INFO(DEBUG_CATEGORY_BGP, "backfill deleted: tbl=%s fixed=%d", tbl, fixed); } } sqlite3_finalize(s); } } DEBUG_INFO(DEBUG_CATEGORY_BGP, "topo_node_sqlite tables initialized"); return 0; } int topo_node_sqlite_member_local_init(sqlite3* db, const char* channel_id) { if (!db || !channel_id || !channel_id[0]) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "radio: invalid local member schema arguments"); return -1; } char peers_tbl[80]; peers_table_name(channel_id, peers_tbl, sizeof(peers_tbl)); char sql[160]; snprintf(sql, sizeof(sql), "PRAGMA table_info(\"%s\")", peers_tbl); sqlite3_stmt* st = NULL; if (sqlite3_prepare_v2(db, sql, -1, &st, NULL) != SQLITE_OK) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "radio: inspect member schema ch=%s: %s", channel_id, sqlite3_errmsg(db)); return -1; } int has_radio_muted = 0, rc; while ((rc = sqlite3_step(st)) == SQLITE_ROW) { const char* column = (const char*)sqlite3_column_text(st, 1); if (column && !strcmp(column, "radio_muted")) has_radio_muted = 1; } sqlite3_finalize(st); if (rc != SQLITE_DONE) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "radio: read member schema ch=%s: %s", channel_id, sqlite3_errmsg(db)); return -1; } if (!has_radio_muted) { snprintf(sql, sizeof(sql), "ALTER TABLE \"%s\" ADD COLUMN radio_muted INTEGER NOT NULL DEFAULT 0", peers_tbl); if (sqlite3_exec(db, sql, NULL, NULL, NULL) != SQLITE_OK) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "radio: extend member schema ch=%s: %s", channel_id, sqlite3_errmsg(db)); return -1; } DEBUG_INFO(DEBUG_CATEGORY_RADIO, "radio: added local member mute setting ch=%s", channel_id); } return 0; } 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) { if (!db || !channel_id || !name || !x25519_pub || !ed25519_pub || !signature) return -1; char peers_tbl[80]; peers_table_name(channel_id, peers_tbl, sizeof(peers_tbl)); const char* ddl_tmpl = "CREATE TABLE IF NOT EXISTS \"%s\" (" " id INTEGER PRIMARY KEY AUTOINCREMENT," /* суррогатный ключ */ " node_id INTEGER NOT NULL UNIQUE," /* уникальный ID узла (64-bit) */ " x25519_pubkey BLOB NOT NULL," /* X25519 публичный ключ (32 байта) */ " ed25519_pubkey BLOB NOT NULL," /* Ed25519 публичный ключ (32 байта) — верификация join_sig/update_sig */ " join_sig BLOB NOT NULL," /* Ed25519 подпись присоединения (PEERS_JOIN_SIG_SIZE=64). signed: ch_x25519(32)||ch_ed25519(32)||node_id(8 LE)||x25519(32)||join_ts(8 LE) */ " join_ts INTEGER NOT NULL," /* NTP timestamp подписи join_sig — защита от replay */ " update_sig BLOB," /* Ed25519 подпись обновления userinfo (PEERS_UPDATE_SIG_SIZE=64). signed: join_sig(64)||update_ts(8 LE)||userinfo(NUL-terminated) */ " update_ts INTEGER DEFAULT 0," /* NTP timestamp подписи update_sig */ " userinfo TEXT NOT NULL DEFAULT ''," /* JSON с информацией о пользователе: {"name":""} */ " creator_sig BLOB," /* Ed25519 подпись создателя (PEERS_JOIN_SIG_SIZE=64), пока заглушка */ " signed_by INTEGER," /* node_id инвайтера (родитель в дереве приглашений); 0 = корень, подписан ключом группы */ " signature BLOB," /* Ed25519 подпись x25519_pubkey||"group_join" ключом родителя (или канальным ed25519 для корня) */ " adm_tags TEXT," /* JSON-теги администратора: {"key":"val",...,"ver":"N"}. подпись: sc_ed25519_sign(ch_ed_priv, json||node_id_8LE) */ " adm_tags_sig BLOB," /* Ed25519 подпись над adm_tags (64 байта) канальным ключом */ " node_type INTEGER NOT NULL DEFAULT 0," /* вычисляемый тип узла (topo_node_sqlite_nodeinfo_updated): 0=неизвестно 1=прямой(addr_type NETIF/DIRECT/NAT_STRICT) 2=EIM NAT 3=(не исп.) 4=суперузел(adm_tags.supernode=yes) */ " node_RTT INTEGER," /* RTT до узла в мс, часть индекса (node_type, node_RTT) */ " connected INTEGER NOT NULL DEFAULT 0," /* 1 = было активное подключение к узлу */ " storage INTEGER NOT NULL DEFAULT 0," /* 1 = приоритетный хранитель аттачей */ " deleted INTEGER NOT NULL DEFAULT 0," /* 1 = аккаунт удалён владельцем (не отображаем, не подключаемся) */ " local_nick TEXT," /* локальный ник (GUI only, не синхронизируется) */ " radio_muted INTEGER NOT NULL DEFAULT 0," /* локальное отключение голоса участника */ " source INTEGER NOT NULL DEFAULT 0" /* 0=полная merkle-запись 1=упрощённый плейсхолдер topo_group (не участвует в merkle-хеше, дополняется полной записью) */ ")"; int ddl_sz = snprintf(NULL, 0, ddl_tmpl, peers_tbl); char* ddl = u_malloc(ddl_sz + 1); if (ddl) { snprintf(ddl, ddl_sz + 1, ddl_tmpl, peers_tbl); char* err = NULL; sqlite3_exec(db, ddl, NULL, NULL, &err); if (err) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "topo_node_sqlite_channel_put peers: %s", err); sqlite3_free(err); } u_free(ddl); } if (topo_node_sqlite_member_local_init(db, channel_id) < 0) return -1; int idx_sz = snprintf(NULL, 0, "CREATE INDEX IF NOT EXISTS \"idx_%s_type_rtt\" ON \"%s\" (node_type, node_RTT)", peers_tbl, peers_tbl); char* idx_sql = u_malloc(idx_sz + 1); if (idx_sql) { snprintf(idx_sql, idx_sz + 1, "CREATE INDEX IF NOT EXISTS \"idx_%s_type_rtt\" ON \"%s\" (node_type, node_RTT)", peers_tbl, peers_tbl); sqlite3_exec(db, idx_sql, NULL, NULL, NULL); u_free(idx_sql); } int sidx_sz = snprintf(NULL, 0, "CREATE INDEX IF NOT EXISTS \"idx_%s_storage\" ON \"%s\" (storage)", peers_tbl, peers_tbl); char* sidx_sql = u_malloc(sidx_sz + 1); if (sidx_sql) { snprintf(sidx_sql, sidx_sz + 1, "CREATE INDEX IF NOT EXISTS \"idx_%s_storage\" ON \"%s\" (storage)", peers_tbl, peers_tbl); sqlite3_exec(db, sidx_sql, NULL, NULL, NULL); u_free(sidx_sql); } sqlite3_stmt* stmt = NULL; if (sqlite3_prepare_v2(db, "INSERT INTO channels(channel_id, name, owner_node_id," " x25519_pubkey, x25519_privkey, ed25519_pubkey, ed25519_privkey, signature)" " VALUES(?,?,?,?,?,?,?,?) ON CONFLICT(channel_id) DO UPDATE SET" " name=excluded.name,owner_node_id=excluded.owner_node_id,signature=excluded.signature," " x25519_privkey=COALESCE(excluded.x25519_privkey,channels.x25519_privkey)," " ed25519_privkey=COALESCE(excluded.ed25519_privkey,channels.ed25519_privkey)" " WHERE channels.x25519_pubkey=excluded.x25519_pubkey AND channels.ed25519_pubkey=excluded.ed25519_pubkey", -1, &stmt, NULL) != SQLITE_OK) return -1; sqlite3_bind_text(stmt, 1, channel_id, -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 2, name, -1, SQLITE_STATIC); sqlite3_bind_int64(stmt, 3, (sqlite3_int64)owner_node_id); sqlite3_bind_blob(stmt, 4, x25519_pub, 32, SQLITE_STATIC); if (x25519_priv) sqlite3_bind_blob(stmt, 5, x25519_priv, 32, SQLITE_STATIC); else sqlite3_bind_null(stmt, 5); sqlite3_bind_blob(stmt, 6, ed25519_pub, 32, SQLITE_STATIC); if (ed25519_priv) sqlite3_bind_blob(stmt, 7, ed25519_priv, 32, SQLITE_STATIC); else sqlite3_bind_null(stmt, 7); sqlite3_bind_blob(stmt, 8, signature, 64, SQLITE_STATIC); int rc = sqlite3_step(stmt); sqlite3_finalize(stmt); if (rc == SQLITE_DONE && !sqlite3_changes(db)) { DEBUG_WARN(DEBUG_CATEGORY_CHAT, "channel: rejected identity change channel=%s", channel_id); return -1; } return rc == SQLITE_DONE ? 0 : -1; } int topo_node_sqlite_channel_delete(sqlite3* db, const char* channel_id) { if (!db || !channel_id || !channel_id[0]) return -1; char peers_tbl[80]; peers_table_name(channel_id, peers_tbl, sizeof(peers_tbl)); char msg_tbl[80]; sanitize_ch_id(channel_id, msg_tbl, sizeof(msg_tbl)); /* msg_ */ char msg_full[80]; snprintf(msg_full, sizeof(msg_full), "msg_%s", msg_tbl); /* 1. drop per-channel tables (индексы дропятся вместе с таблицей) */ char sql[128]; snprintf(sql, sizeof(sql), "DROP TABLE IF EXISTS \"%s\"", peers_tbl); sqlite3_exec(db, sql, NULL, NULL, NULL); snprintf(sql, sizeof(sql), "DROP TABLE IF EXISTS \"%s\"", msg_full); sqlite3_exec(db, sql, NULL, NULL, NULL); snprintf(sql, sizeof(sql), "DROP TABLE IF EXISTS \"feedback_%s\"", msg_tbl); if (sqlite3_exec(db, sql, NULL, NULL, NULL) != SQLITE_OK) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "feedback: delete channel=%s: %s", channel_id, sqlite3_errmsg(db)); return -1; } /* 2. delete channels row (вместе с канальными privkey) */ sqlite3_stmt* st = NULL; if (sqlite3_prepare_v2(db, "DELETE FROM channels WHERE channel_id=?", -1, &st, NULL) == SQLITE_OK) { sqlite3_bind_text(st, 1, channel_id, -1, SQLITE_STATIC); sqlite3_step(st); sqlite3_finalize(st); } /* 3. delete merkle tree hashes for this namespace */ sqlite3_stmt* mh = NULL; if (sqlite3_prepare_v2(db, "DELETE FROM merkle_tree_hash WHERE substr(namespace,instr(namespace,':')+1)=?", -1, &mh, NULL) == SQLITE_OK) { sqlite3_bind_text(mh, 1, channel_id, -1, SQLITE_STATIC); sqlite3_step(mh); sqlite3_finalize(mh); } DEBUG_INFO(DEBUG_CATEGORY_BGP, "topo_node_sqlite_channel_delete: ch=%s peers=%s msg=%s", channel_id, peers_tbl, msg_full); return 0; } /* Упрощённый плейсхолдер мембера от topo_group: только node_id + pubkeys, source=1. * Не участвует в merkle-хеше. Если запись уже есть (полная merkle) — не трогает её. * Позже полная merkle-запись дополняет/перезаписывает блоки и сбрасывает source=0. */ 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) { if (!db || !channel_id || !x25519_pubkey || !ed25519_pubkey) return -1; char peers_tbl[80]; peers_table_name(channel_id, peers_tbl, sizeof(peers_tbl)); char sql[512]; snprintf(sql, sizeof(sql), "INSERT INTO \"%s\"(node_id, x25519_pubkey, ed25519_pubkey, join_sig, join_ts, source)" " VALUES(?,?,?,?,0,1)" " ON CONFLICT(node_id) DO NOTHING", peers_tbl); sqlite3_stmt* stmt = NULL; if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1; sqlite3_bind_int64(stmt, 1, (sqlite3_int64)node_id); sqlite3_bind_blob(stmt, 2, x25519_pubkey, 32, SQLITE_STATIC); sqlite3_bind_blob(stmt, 3, ed25519_pubkey, 32, SQLITE_STATIC); static const unsigned char zsig[PEERS_JOIN_SIG_SIZE] = {0}; sqlite3_bind_blob(stmt, 4, zsig, PEERS_JOIN_SIG_SIZE, SQLITE_STATIC); int rc = sqlite3_step(stmt); sqlite3_finalize(stmt); return rc == SQLITE_DONE ? 0 : -1; } /* Обновляет только блок мембера (keys/join/update_sig/update_ts/userinfo), * НЕ трогая adm_tags/adm_tags_sig/storage (блок владельца) и локальные поля. */ 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, const uint8_t* x25519_pubkey, const uint8_t* ed25519_pubkey, const char* userinfo, const uint8_t* creator_sig, uint64_t signed_by, const uint8_t* signature) { if (!db || !channel_id || !x25519_pubkey || !ed25519_pubkey) return -1; char peers_tbl[80]; peers_table_name(channel_id, peers_tbl, sizeof(peers_tbl)); char sql[768]; snprintf(sql, sizeof(sql), "INSERT INTO \"%s\"(node_id, x25519_pubkey, ed25519_pubkey," " join_sig, join_ts, update_sig, update_ts, userinfo, creator_sig, signed_by, signature)" " VALUES(?,?,?,?,?,?,?,?,?,?,?)" " ON CONFLICT(node_id) DO UPDATE SET" " x25519_pubkey=excluded.x25519_pubkey," " ed25519_pubkey=excluded.ed25519_pubkey," " join_sig=excluded.join_sig," " join_ts=excluded.join_ts," " update_sig=excluded.update_sig," " update_ts=excluded.update_ts," " userinfo=excluded.userinfo," " creator_sig=excluded.creator_sig," " signed_by=excluded.signed_by," " signature=excluded.signature," " source=0", peers_tbl); sqlite3_stmt* stmt = NULL; if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1; sqlite3_bind_int64(stmt, 1, (sqlite3_int64)node_id); sqlite3_bind_blob(stmt, 2, x25519_pubkey, 32, SQLITE_STATIC); sqlite3_bind_blob(stmt, 3, ed25519_pubkey, 32, SQLITE_STATIC); if (join_sig) sqlite3_bind_blob(stmt, 4, join_sig, PEERS_JOIN_SIG_SIZE, SQLITE_STATIC); else { static const unsigned char zsig[PEERS_JOIN_SIG_SIZE] = {0}; sqlite3_bind_blob(stmt, 4, zsig, PEERS_JOIN_SIG_SIZE, SQLITE_STATIC); } sqlite3_bind_int64(stmt, 5, (sqlite3_int64)join_ts); if (update_sig) sqlite3_bind_blob(stmt, 6, update_sig, PEERS_UPDATE_SIG_SIZE, SQLITE_STATIC); else sqlite3_bind_null(stmt, 6); sqlite3_bind_int64(stmt, 7, (sqlite3_int64)update_ts); sqlite3_bind_text(stmt, 8, userinfo ? userinfo : "", -1, SQLITE_STATIC); if (creator_sig) sqlite3_bind_blob(stmt, 9, creator_sig, PEERS_JOIN_SIG_SIZE, SQLITE_STATIC); else sqlite3_bind_null(stmt, 9); sqlite3_bind_int64(stmt, 10, (sqlite3_int64)signed_by); if (signature) sqlite3_bind_blob(stmt, 11, signature, 64, SQLITE_STATIC); else sqlite3_bind_null(stmt, 11); int rc = sqlite3_step(stmt); sqlite3_finalize(stmt); return rc == SQLITE_DONE ? 0 : -1; } /* Обновляет только блок владельца (adm_tags/adm_tags_sig/storage), * НЕ трогая блок мембера. */ 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) { if (!db || !channel_id) return -1; char peers_tbl[80]; peers_table_name(channel_id, peers_tbl, sizeof(peers_tbl)); char sql[256]; snprintf(sql, sizeof(sql), "UPDATE \"%s\" SET adm_tags=?, adm_tags_sig=?, storage=?, deleted=? WHERE node_id=?", peers_tbl); sqlite3_stmt* u = NULL; if (sqlite3_prepare_v2(db, sql, -1, &u, NULL) != SQLITE_OK) return -1; if (adm_tags && adm_tags[0]) sqlite3_bind_text(u, 1, adm_tags, -1, SQLITE_STATIC); else sqlite3_bind_null(u, 1); if (adm_tags_sig) sqlite3_bind_blob(u, 2, adm_tags_sig, 64, SQLITE_STATIC); else sqlite3_bind_null(u, 2); sqlite3_bind_int(u, 3, storage); sqlite3_bind_int(u, 4, adm_tags_deleted(adm_tags)); sqlite3_bind_int64(u, 5, (sqlite3_int64)node_id); int rc = sqlite3_step(u); sqlite3_finalize(u); if (rc != SQLITE_DONE) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "member_owner_put: ch=%s node=%016llx rc=%d: %s", channel_id, (unsigned long long)node_id, rc, sqlite3_errmsg(db)); return -1; } return 0; } 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) { if (!db || !x25519 || !ed25519) return -1; sqlite3_stmt* stmt = NULL; if (sqlite3_prepare_v2(db, "INSERT INTO nodes(node_id, name, x25519_pubkey, ed25519_pubkey, created_at, update_ts, online)" " VALUES(?,?,?,?,?,?,1)" " ON CONFLICT(node_id) DO UPDATE SET" " name=CASE WHEN excluded.name IS NOT NULL AND excluded.name != ''" " THEN excluded.name ELSE nodes.name END," " x25519_pubkey=excluded.x25519_pubkey," " ed25519_pubkey=excluded.ed25519_pubkey," " created_at=COALESCE(nodes.created_at, excluded.created_at)," " update_ts=CASE WHEN nodes.update_ts IS NULL OR excluded.update_ts > nodes.update_ts" " THEN excluded.update_ts ELSE nodes.update_ts END", -1, &stmt, NULL) != SQLITE_OK) return -1; sqlite3_bind_int64(stmt, 1, (sqlite3_int64)node_id); sqlite3_bind_text(stmt, 2, name && name[0] ? name : "", -1, SQLITE_STATIC); sqlite3_bind_blob(stmt, 3, x25519, 32, SQLITE_STATIC); sqlite3_bind_blob(stmt, 4, ed25519, 32, SQLITE_STATIC); sqlite3_bind_int64(stmt, 5, (sqlite3_int64)created_at); sqlite3_bind_int64(stmt, 6, (sqlite3_int64)update_ts); sqlite3_step(stmt); sqlite3_finalize(stmt); return 0; } 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) { if (!db || !channel_id || !join_sig_out || !join_ts_out) return -1; char peers_tbl[80]; peers_table_name(channel_id, peers_tbl, sizeof(peers_tbl)); char sql[256]; snprintf(sql, sizeof(sql), "SELECT join_sig, join_ts FROM \"%s\" WHERE node_id=?", peers_tbl); sqlite3_stmt* stmt = NULL; if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1; sqlite3_bind_int64(stmt, 1, (sqlite3_int64)node_id); int rc = -1; if (sqlite3_step(stmt) == SQLITE_ROW) { const uint8_t* sig = (const uint8_t*)sqlite3_column_blob(stmt, 0); if (sig) { memcpy(join_sig_out, sig, PEERS_JOIN_SIG_SIZE); *join_ts_out = (uint64_t)sqlite3_column_int64(stmt, 1); rc = 0; } } sqlite3_finalize(stmt); return rc; } 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]) { if (!db || !channel_id || !signed_by_out || !signature_out) return -1; char peers_tbl[80]; peers_table_name(channel_id, peers_tbl, sizeof(peers_tbl)); char sql[256]; snprintf(sql, sizeof(sql), "SELECT signed_by, signature FROM \"%s\" WHERE node_id=?", peers_tbl); sqlite3_stmt* stmt = NULL; if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1; sqlite3_bind_int64(stmt, 1, (sqlite3_int64)node_id); int rc = -1; if (sqlite3_step(stmt) == SQLITE_ROW) { *signed_by_out = (uint64_t)sqlite3_column_int64(stmt, 0); const uint8_t* sig = (const uint8_t*)sqlite3_column_blob(stmt, 1); if (sig && sqlite3_column_bytes(stmt, 1) >= 64) { memcpy(signature_out, sig, 64); rc = 0; } } sqlite3_finalize(stmt); return rc; } int topo_node_sqlite_member_in_channel(sqlite3* db, const char* channel_id, uint64_t node_id) { if (!db || !channel_id || node_id == 0) return 0; char peers_tbl[80]; peers_table_name(channel_id, peers_tbl, sizeof(peers_tbl)); char sql[256]; snprintf(sql, sizeof(sql), "SELECT 1 FROM \"%s\" WHERE node_id=?", peers_tbl); sqlite3_stmt* stmt = NULL; if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return 0; sqlite3_bind_int64(stmt, 1, (sqlite3_int64)node_id); int found = (sqlite3_step(stmt) == SQLITE_ROW); sqlite3_finalize(stmt); return found; } int topo_node_sqlite_member_del(sqlite3* db, const char* channel_id, uint64_t node_id) { if (!db || !channel_id) return -1; char peers_tbl[80]; peers_table_name(channel_id, peers_tbl, sizeof(peers_tbl)); char sql[256]; snprintf(sql, sizeof(sql), "DELETE FROM \"%s\" WHERE node_id=?", peers_tbl); sqlite3_stmt* stmt = NULL; if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1; sqlite3_bind_int64(stmt, 1, (sqlite3_int64)node_id); sqlite3_step(stmt); sqlite3_finalize(stmt); return 0; } 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) { if (!db || !channel_id) return -1; sqlite3_stmt* stmt = NULL; if (sqlite3_prepare_v2(db, "SELECT name, owner_node_id, x25519_pubkey, ed25519_pubkey, signature" " FROM channels WHERE channel_id=?", -1, &stmt, NULL) != SQLITE_OK) return -1; sqlite3_bind_text(stmt, 1, channel_id, -1, SQLITE_STATIC); if (sqlite3_step(stmt) != SQLITE_ROW) { sqlite3_finalize(stmt); return -1; } if (name_out && name_sz > 0) { const unsigned char* txt = sqlite3_column_text(stmt, 0); if (txt) snprintf(name_out, name_sz, "%s", txt); else name_out[0] = '\0'; } if (owner_node_id) *owner_node_id = (uint64_t)sqlite3_column_int64(stmt, 1); if (x25519_pub) memcpy(x25519_pub, sqlite3_column_blob(stmt, 2), 32); if (ed25519_pub) memcpy(ed25519_pub, sqlite3_column_blob(stmt, 3), 32); if (signature) memcpy(signature, sqlite3_column_blob(stmt, 4), 64); sqlite3_finalize(stmt); return 0; } int topo_node_sqlite_channel_get_priv(sqlite3* db, const char* channel_id, uint8_t* ed25519_priv_out) { if (!db || !channel_id || !ed25519_priv_out) return -1; sqlite3_stmt* stmt = NULL; if (sqlite3_prepare_v2(db, "SELECT ed25519_privkey FROM channels WHERE channel_id=?", -1, &stmt, NULL) != SQLITE_OK) return -1; sqlite3_bind_text(stmt, 1, channel_id, -1, SQLITE_STATIC); if (sqlite3_step(stmt) != SQLITE_ROW) { sqlite3_finalize(stmt); return -1; } const uint8_t* blob = (const uint8_t*)sqlite3_column_blob(stmt, 0); if (!blob || sqlite3_column_bytes(stmt, 0) < 32) { sqlite3_finalize(stmt); return -1; } memcpy(ed25519_priv_out, blob, 32); sqlite3_finalize(stmt); return 0; } int topo_node_sqlite_channel_get_privs(sqlite3* db, const char* channel_id, uint8_t* x25519_priv_out, uint8_t* ed25519_priv_out) { if (!db || !channel_id || !x25519_priv_out || !ed25519_priv_out) return -1; sqlite3_stmt* stmt = NULL; if (sqlite3_prepare_v2(db, "SELECT x25519_privkey, ed25519_privkey FROM channels WHERE channel_id=?", -1, &stmt, NULL) != SQLITE_OK) return -1; sqlite3_bind_text(stmt, 1, channel_id, -1, SQLITE_STATIC); if (sqlite3_step(stmt) != SQLITE_ROW) { sqlite3_finalize(stmt); return -1; } const uint8_t* x = (const uint8_t*)sqlite3_column_blob(stmt, 0); const uint8_t* e = (const uint8_t*)sqlite3_column_blob(stmt, 1); if (!x || sqlite3_column_bytes(stmt, 0) < 32 || !e || sqlite3_column_bytes(stmt, 1) < 32) { sqlite3_finalize(stmt); return -1; } memcpy(x25519_priv_out, x, 32); memcpy(ed25519_priv_out, e, 32); sqlite3_finalize(stmt); return 0; } int topo_node_sqlite_channel_set_privs(sqlite3* db, const char* channel_id, const uint8_t* x25519_priv, const uint8_t* ed25519_priv) { if (!db || !channel_id || !x25519_priv || !ed25519_priv) return -1; sqlite3_stmt* stmt = NULL; if (sqlite3_prepare_v2(db, "UPDATE channels SET x25519_privkey=?, ed25519_privkey=? WHERE channel_id=?", -1, &stmt, NULL) != SQLITE_OK) return -1; sqlite3_bind_blob(stmt, 1, x25519_priv, 32, SQLITE_STATIC); sqlite3_bind_blob(stmt, 2, ed25519_priv, 32, SQLITE_STATIC); sqlite3_bind_text(stmt, 3, channel_id, -1, SQLITE_STATIC); int rc = sqlite3_step(stmt) == SQLITE_DONE ? 0 : -1; sqlite3_finalize(stmt); return rc; } int topo_node_sqlite_node_set_online(sqlite3* db, uint64_t node_id, int online) { if (!db) return -1; sqlite3_stmt* stmt = NULL; if (sqlite3_prepare_v2(db, "UPDATE nodes SET online=? WHERE node_id=?", -1, &stmt, NULL) != SQLITE_OK) return -1; sqlite3_bind_int(stmt, 1, online ? 1 : 0); sqlite3_bind_int64(stmt, 2, (sqlite3_int64)node_id); sqlite3_step(stmt); sqlite3_finalize(stmt); return 0; } int topo_node_sqlite_node_get_online(sqlite3* db, uint64_t node_id) { if (!db) return 0; sqlite3_stmt* stmt = NULL; if (sqlite3_prepare_v2(db, "SELECT online FROM nodes WHERE node_id=?", -1, &stmt, NULL) != SQLITE_OK) return 0; sqlite3_bind_int64(stmt, 1, (sqlite3_int64)node_id); int online = 0; if (sqlite3_step(stmt) == SQLITE_ROW) online = sqlite3_column_int(stmt, 0); sqlite3_finalize(stmt); return online; } int topo_node_sqlite_get_ed25519_pubkey(sqlite3* db, uint64_t node_id, uint8_t pubkey_out[32]) { if (!db || !pubkey_out) return -1; sqlite3_stmt* stmt = NULL; if (sqlite3_prepare_v2(db, "SELECT ed25519_pubkey FROM nodes WHERE node_id=?", -1, &stmt, NULL) != SQLITE_OK) return -1; sqlite3_bind_int64(stmt, 1, (sqlite3_int64)node_id); int rc = -1; if (sqlite3_step(stmt) == SQLITE_ROW) { const void* b = sqlite3_column_blob(stmt, 0); int bytes = sqlite3_column_bytes(stmt, 0); if (b && bytes >= 32) { uint64_t chk; memcpy(&chk, b, 8); if (chk != 0) { memcpy(pubkey_out, b, 32); rc = 0; } } } sqlite3_finalize(stmt); return rc; } int topo_node_sqlite_nodeinfo_updated(sqlite3* db, uint64_t node_id) { if (!db) return -1; int best_at = -1; { sqlite3_stmt* s = NULL; if (sqlite3_prepare_v2(db, "SELECT addr_type FROM node_addresses WHERE node_id=?", -1, &s, NULL) == SQLITE_OK) { sqlite3_bind_int64(s, 1, (sqlite3_int64)node_id); while (sqlite3_step(s) == SQLITE_ROW) { int at = sqlite3_column_int(s, 0); if (at == ADDR_TYPE_DIRECT) { best_at = ADDR_TYPE_DIRECT; break; } if (best_at < 0 || at < best_at) best_at = at; } sqlite3_finalize(s); } } static const int at2type[] = { [ADDR_TYPE_NETIF]=1, [ADDR_TYPE_DIRECT]=1, [ADDR_TYPE_NAT_EIM]=2, [ADDR_TYPE_NAT_STRICT]=1, [ADDR_TYPE_NAT]=2, [ADDR_TYPE_CONTACT]=2 }; int base_type = best_at < 0 || (unsigned)best_at >= sizeof(at2type) / sizeof(at2type[0]) ? 0 : at2type[best_at]; if (best_at >= 0 && (unsigned)best_at >= sizeof(at2type) / sizeof(at2type[0])) DEBUG_WARN(DEBUG_CATEGORY_BGP, "invalid projected address type=%d node=%016llx", best_at, (unsigned long long)node_id); sqlite3_stmt* s = NULL; if (sqlite3_prepare_v2(db, "SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'peers\\_%' ESCAPE '\\'", -1, &s, NULL) != SQLITE_OK) return -1; char sql[256]; while (sqlite3_step(s) == SQLITE_ROW) { const char* tbl = (const char*)sqlite3_column_text(s, 0); int node_type = base_type; int is_storage = 0; int is_deleted = 0; snprintf(sql, sizeof(sql), "SELECT adm_tags FROM \"%s\" WHERE node_id=?", tbl); sqlite3_stmt* a = NULL; if (sqlite3_prepare_v2(db, sql, -1, &a, NULL) == SQLITE_OK) { sqlite3_bind_int64(a, 1, (sqlite3_int64)node_id); if (sqlite3_step(a) == SQLITE_ROW) { const char* tags = (const char*)sqlite3_column_text(a, 0); if (tags) { char buf[64]; if (json_flat_get(tags, "supernode", buf, sizeof(buf)) == 0 && strcmp(buf, "yes") == 0) node_type = 4; if (best_at == ADDR_TYPE_DIRECT && base_type == 1) { if (json_flat_get(tags, "storage", buf, sizeof(buf)) == 0 && strcmp(buf, "yes") == 0) is_storage = 1; } is_deleted = adm_tags_deleted(tags); } } sqlite3_finalize(a); } snprintf(sql, sizeof(sql), "UPDATE \"%s\" SET node_type=?, storage=?, deleted=? WHERE node_id=?", tbl); sqlite3_stmt* u = NULL; if (sqlite3_prepare_v2(db, sql, -1, &u, NULL) == SQLITE_OK) { sqlite3_bind_int(u, 1, node_type); sqlite3_bind_int(u, 2, is_storage); sqlite3_bind_int(u, 3, is_deleted); sqlite3_bind_int64(u, 4, (sqlite3_int64)node_id); sqlite3_step(u); sqlite3_finalize(u); } } sqlite3_finalize(s); DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "nodeinfo_updated: node=%016llx best_at=%d base_type=%d", (unsigned long long)node_id, best_at, base_type); return 0; } void topo_node_sqlite_update_rtt(sqlite3* db, uint64_t node_id, uint16_t rtt) { if (!db) return; DEBUG_TRACE(DEBUG_CATEGORY_BGP, "RTT sample: node=%016llx rtt_ms=%.1f; updating address and membership tables", (unsigned long long)node_id, rtt / 10.0); { sqlite3_stmt* s = NULL; if (sqlite3_prepare_v2(db, "UPDATE node_addresses SET rtt=? WHERE node_id=?", -1, &s, NULL) == SQLITE_OK) { sqlite3_bind_int(s, 1, (int)rtt); sqlite3_bind_int64(s, 2, (sqlite3_int64)node_id); if (sqlite3_step(s) != SQLITE_DONE) DEBUG_ERROR(DEBUG_CATEGORY_BGP, "RTT persistence failed: table=node_addresses node=%016llx: %s", (unsigned long long)node_id, sqlite3_errmsg(db)); sqlite3_finalize(s); } else DEBUG_ERROR(DEBUG_CATEGORY_BGP, "RTT query preparation failed: table=node_addresses node=%016llx: %s", (unsigned long long)node_id, sqlite3_errmsg(db)); } { sqlite3_stmt* s = NULL; if (sqlite3_prepare_v2(db, "SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'peers\\_%' ESCAPE '\\'", -1, &s, NULL) != SQLITE_OK) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "RTT cannot enumerate membership tables: node=%016llx: %s", (unsigned long long)node_id, sqlite3_errmsg(db)); return; } char sql[256]; int rc; while ((rc = sqlite3_step(s)) == SQLITE_ROW) { const char* tbl = (const char*)sqlite3_column_text(s, 0); snprintf(sql, sizeof(sql), "UPDATE \"%s\" SET node_RTT=? WHERE node_id=?", tbl); sqlite3_stmt* u = NULL; if (sqlite3_prepare_v2(db, sql, -1, &u, NULL) == SQLITE_OK) { sqlite3_bind_int(u, 1, (int)rtt); sqlite3_bind_int64(u, 2, (sqlite3_int64)node_id); if (sqlite3_step(u) != SQLITE_DONE) DEBUG_ERROR(DEBUG_CATEGORY_BGP, "RTT persistence failed: table=%s node=%016llx: %s", tbl, (unsigned long long)node_id, sqlite3_errmsg(db)); sqlite3_finalize(u); } else DEBUG_ERROR(DEBUG_CATEGORY_BGP, "RTT query preparation failed: table=%s node=%016llx: %s", tbl, (unsigned long long)node_id, sqlite3_errmsg(db)); } if (rc != SQLITE_DONE) DEBUG_ERROR(DEBUG_CATEGORY_BGP, "RTT table enumeration failed: node=%016llx: %s", (unsigned long long)node_id, sqlite3_errmsg(db)); sqlite3_finalize(s); } } uint64_t topo_node_sqlite_snapshot_timestamp(sqlite3* db, uint64_t node_id) { sqlite3_stmt* stmt = NULL; uint64_t timestamp = 0; if (sqlite3_prepare_v2(db, "SELECT timestamp FROM node_snapshots WHERE node_id=?", -1, &stmt, NULL) != SQLITE_OK) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "snapshot timestamp: %s", sqlite3_errmsg(db)); return 0; } sqlite3_bind_int64(stmt, 1, (sqlite3_int64)node_id); int rc = sqlite3_step(stmt); if (rc == SQLITE_ROW) timestamp = (uint64_t)sqlite3_column_int64(stmt, 0); else if (rc != SQLITE_DONE) DEBUG_ERROR(DEBUG_CATEGORY_BGP, "snapshot timestamp read: %s", sqlite3_errmsg(db)); sqlite3_finalize(stmt); return timestamp; } int topo_node_sqlite_snapshot_put(sqlite3* db, struct TOPO_NODE* ni) { if (!db || topo_node_verify(ni) < 0) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "invalid node snapshot"); return -1; } uint8_t record[TOPO_NODE_WIRE_MAX_SIZE]; struct TOPO_GROUP_NODE nq = {0}; int len = topo_node_serialize(ni, &nq, 0, 0, record, sizeof(record), 0); if (len < 0) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "snapshot serialization failed"); return -1; } sqlite3_stmt* stmt = NULL; const char* sql = "INSERT INTO node_snapshots(node_id,timestamp,record) VALUES(?,?,?) " "ON CONFLICT(node_id) DO UPDATE SET timestamp=excluded.timestamp,record=excluded.record " "WHERE excluded.timestamp > node_snapshots.timestamp"; int rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL); if (rc == SQLITE_OK) { sqlite3_bind_int64(stmt, 1, (sqlite3_int64)ni->node_id); sqlite3_bind_int64(stmt, 2, (sqlite3_int64)ni->timestamp); sqlite3_bind_blob(stmt, 3, record, len, SQLITE_STATIC); rc = sqlite3_step(stmt); } if (rc != SQLITE_DONE) DEBUG_ERROR(DEBUG_CATEGORY_BGP, "snapshot write: %s", sqlite3_errmsg(db)); sqlite3_finalize(stmt); return rc == SQLITE_DONE ? 0 : -1; } struct TOPO_NODE* topo_node_sqlite_snapshot_load(sqlite3* db, struct TOPO_GROUPS* groups, uint64_t node_id) { sqlite3_stmt* stmt = NULL; struct TOPO_NODE* ni = NULL; if (sqlite3_prepare_v2(db, "SELECT record FROM node_snapshots WHERE node_id=?", -1, &stmt, NULL) != SQLITE_OK) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "snapshot load: %s", sqlite3_errmsg(db)); return NULL; } sqlite3_bind_int64(stmt, 1, (sqlite3_int64)node_id); int rc = sqlite3_step(stmt); if (rc == SQLITE_ROW) { struct TOPO_GROUP group = { .instance = groups->instance }; const uint8_t* data = sqlite3_column_blob(stmt, 0); int len = sqlite3_column_bytes(stmt, 0); if (!len) { DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "snapshot invalidated by format change node=%016llx", (unsigned long long)node_id); } else if (topo_node_deserialize(&group, data, (size_t)len, &ni, NULL, NULL, NULL, NULL) < 0 || !ni || ni->node_id != node_id || topo_node_verify(ni) < 0) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "invalid persisted snapshot node=%016llx", (unsigned long long)node_id); topo_node_destroy(groups, ni); ni = NULL; } } else if (rc != SQLITE_DONE) DEBUG_ERROR(DEBUG_CATEGORY_BGP, "snapshot read: %s", sqlite3_errmsg(db)); sqlite3_finalize(stmt); return ni; } struct TOPO_NODE* topo_node_sqlite_node_load(sqlite3* db, struct TOPO_GROUPS* groups, uint64_t node_id) { if (!db || !groups) return NULL; struct TOPO_NODE* snapshot = topo_node_sqlite_snapshot_load(db, groups, node_id); if (snapshot) return snapshot; uint8_t pubkey[32] = {0}, ed_pubkey[32] = {0}; char* name = NULL; uint8_t client_type = 0; int found = 0; { sqlite3_stmt* s = NULL; if (sqlite3_prepare_v2(db, "SELECT name,x25519_pubkey,ed25519_pubkey,client_type FROM nodes WHERE node_id=?", -1, &s, NULL) == SQLITE_OK) { sqlite3_bind_int64(s, 1, (sqlite3_int64)node_id); if (sqlite3_step(s) == SQLITE_ROW) { const char* n = (const char*)sqlite3_column_text(s, 0); if (n) name = u_strdup(n); const void* pk = sqlite3_column_blob(s, 1); if (pk && sqlite3_column_bytes(s, 1) >= 32) memcpy(pubkey, pk, 32); const void* ep = sqlite3_column_blob(s, 2); if (ep && sqlite3_column_bytes(s, 2) >= 32) memcpy(ed_pubkey, ep, 32); client_type = (uint8_t)sqlite3_column_int(s, 3); found = 1; } sqlite3_finalize(s); } } if (!found) { u_free(name); return NULL; } struct TOPO_ADDR4* v4_head = NULL; struct TOPO_ADDR4* v4_tail = NULL; struct TOPO_SOCKMETA4* m4_head = NULL; struct TOPO_SOCKMETA4* m4_tail = NULL; struct TOPO_REALITY_SOCK* r_head = NULL; struct TOPO_REALITY_SOCK* r_tail = NULL; { sqlite3_stmt* s = NULL; if (sqlite3_prepare_v2(db, "SELECT address,port,addr_type,socket_id,protocol,config_type,nat_mapping,options,address_kind FROM node_addresses" " WHERE node_id=? AND family=4 ORDER BY addr_type", -1, &s, NULL) == SQLITE_OK) { sqlite3_bind_int64(s, 1, (sqlite3_int64)node_id); while (sqlite3_step(s) == SQLITE_ROW) { const void* addr = sqlite3_column_blob(s, 0); if (!addr || sqlite3_column_bytes(s, 0) < 4) continue; int port = sqlite3_column_int(s, 1); int sock_id_raw = sqlite3_column_int(s, 3); uint8_t sock_id = (uint8_t)sock_id_raw; uint8_t type = (uint8_t)sqlite3_column_int(s, 8); struct TOPO_ADDR4* a4 = memory_pool_alloc(groups->v4_addr_pool); if (!a4) continue; memset(a4, 0, sizeof(*a4)); int proto = sqlite3_column_int(s, 4); memcpy(a4->addr, addr, 4); a4->port = (uint16_t)port; a4->type = type; a4->socket_id = sock_id; a4->protocol = (uint8_t)proto; if (!v4_head) v4_head = v4_tail = a4; else { v4_tail->next = a4; v4_tail = a4; } struct TOPO_SOCKMETA4* ex = m4_head; while (ex && ex->id != sock_id) ex = ex->next; if (!ex) { struct TOPO_SOCKMETA4* m4 = memory_pool_alloc(groups->v4_sock_meta_pool); if (m4) { m4->id = sock_id; m4->config_type = (uint8_t)sqlite3_column_int(s, 5); /* Без подписанного snapshot профиль из SQL-проекции не считается доказательством. */ memset(&m4->address_profile, 0, sizeof(m4->address_profile)); m4->next = NULL; if (!m4_head) m4_head = m4_tail = m4; else { m4_tail->next = m4; m4_tail = m4; } } } if (sqlite3_column_type(s, 7) == SQLITE_BLOB && sqlite3_column_bytes(s, 7) == TOPO_ADDR_REALITY_OPTS_SIZE && (proto & TOPO_PROTO_REALITY)) { const struct TOPO_ADDR_REALITY_OPTS* o = (const struct TOPO_ADDR_REALITY_OPTS*)sqlite3_column_blob(s, 7); struct TOPO_REALITY_SOCK* r = u_calloc(1, sizeof(*r)); if (r) { r->socket_id = sock_id; memcpy(r->short_id, o->short_id, 8); memcpy(r->server_pubkey, o->server_pubkey, 32); memcpy(r->version, o->version, 3); memcpy(r->server_name, o->server_name, TOPO_REALITY_SNAME_MAX); r->server_name[TOPO_REALITY_SNAME_MAX - 1] = 0; if (!r_head) r_head = r_tail = r; else { r_tail->next = r; r_tail = r; } } } } sqlite3_finalize(s); } } /* IPv6 addresses */ struct TOPO_ADDR6* v6_head = NULL; struct TOPO_ADDR6* v6_tail = NULL; struct TOPO_SOCKMETA6* m6_head = NULL; struct TOPO_SOCKMETA6* m6_tail = NULL; { sqlite3_stmt* s = NULL; if (sqlite3_prepare_v2(db, "SELECT address,port,addr_type,socket_id,protocol,config_type,nat_mapping,options,address_kind FROM node_addresses" " WHERE node_id=? AND family=6 ORDER BY addr_type", -1, &s, NULL) == SQLITE_OK) { sqlite3_bind_int64(s, 1, (sqlite3_int64)node_id); while (sqlite3_step(s) == SQLITE_ROW) { const void* addr = sqlite3_column_blob(s, 0); if (!addr || sqlite3_column_bytes(s, 0) < 16) continue; int port = sqlite3_column_int(s, 1); int sock_id_raw = sqlite3_column_int(s, 3); uint8_t sock_id = (uint8_t)sock_id_raw; uint8_t type = (uint8_t)sqlite3_column_int(s, 8); struct TOPO_ADDR6* a6 = memory_pool_alloc(groups->v6_addr_pool); if (!a6) continue; memset(a6, 0, sizeof(*a6)); int proto6 = sqlite3_column_int(s, 4); memcpy(a6->addr, addr, 16); a6->port = (uint16_t)port; a6->type = type; a6->socket_id = sock_id; a6->protocol = (uint8_t)proto6; if (!v6_head) v6_head = v6_tail = a6; else { v6_tail->next = a6; v6_tail = a6; } struct TOPO_SOCKMETA6* ex = m6_head; while (ex && ex->id != sock_id) ex = ex->next; if (!ex) { struct TOPO_SOCKMETA6* m6 = memory_pool_alloc(groups->v6_sock_meta_pool); if (m6) { m6->id = sock_id; m6->config_type = (uint8_t)sqlite3_column_int(s, 5); memset(&m6->address_profile, 0, sizeof(m6->address_profile)); m6->next = NULL; if (!m6_head) m6_head = m6_tail = m6; else { m6_tail->next = m6; m6_tail = m6; } } } if (sqlite3_column_type(s, 7) == SQLITE_BLOB && sqlite3_column_bytes(s, 7) == TOPO_ADDR_REALITY_OPTS_SIZE && (proto6 & TOPO_PROTO_REALITY)) { const struct TOPO_ADDR_REALITY_OPTS* o = (const struct TOPO_ADDR_REALITY_OPTS*)sqlite3_column_blob(s, 7); struct TOPO_REALITY_SOCK* r = u_calloc(1, sizeof(*r)); if (r) { r->socket_id = sock_id; memcpy(r->short_id, o->short_id, 8); memcpy(r->server_pubkey, o->server_pubkey, 32); memcpy(r->version, o->version, 3); memcpy(r->server_name, o->server_name, TOPO_REALITY_SNAME_MAX); r->server_name[TOPO_REALITY_SNAME_MAX - 1] = 0; if (!r_head) r_head = r_tail = r; else { r_tail->next = r; r_tail = r; } } } } sqlite3_finalize(s); } } if (!v4_head && !v6_head) { u_free(name); DEBUG_WARN(DEBUG_CATEGORY_BGP, "node_load: node 0x%016llx has no addresses (may be stub/seed — will retry when addresses arrive)", (unsigned long long)node_id); return NULL; } struct TOPO_NODE* ni = u_calloc(1, sizeof(struct TOPO_NODE)); if (!ni) { u_free(name); return NULL; } ni->node_id = node_id; ni->ver = 1; ni->group_ref_count = 0; ni->client_type = client_type; memcpy(ni->public_key, pubkey, 32); memcpy(ni->ed25519_public_key, ed_pubkey, 32); ni->node_name = name; ni->v4_addrs = v4_head; ni->v6_addrs = v6_head; ni->v4_sock_meta = m4_head; ni->v6_sock_meta = m6_head; ni->reality_socks = r_head; DEBUG_INFO(DEBUG_CATEGORY_BGP, "node_load: node 0x%016llx loaded from DB name=%s v4=%d v6=%d", (unsigned long long)node_id, name ? name : "", topo_list_count((struct _topo_head*)v4_head), topo_list_count((struct _topo_head*)v6_head)); return ni; } int topo_node_sqlite_set_connected(sqlite3* db, const char* channel_id, uint64_t node_id, int connected) { if (!db || !channel_id) return -1; char peers_tbl[80]; peers_table_name(channel_id, peers_tbl, sizeof(peers_tbl)); char sql[128]; snprintf(sql, sizeof(sql), "UPDATE \"%s\" SET connected=? WHERE node_id=?", peers_tbl); sqlite3_stmt* st = NULL; if (sqlite3_prepare_v2(db, sql, -1, &st, NULL) != SQLITE_OK) return -1; sqlite3_bind_int(st, 1, connected ? 1 : 0); sqlite3_bind_int64(st, 2, (sqlite3_int64)node_id); int rc = sqlite3_step(st); sqlite3_finalize(st); return (rc == SQLITE_DONE) ? 0 : -1; } /* Читаем тип из уже проверенного snapshot, а не из устаревшей проекции nodes.client_type. */ int topo_node_sqlite_get_connect_peers(sqlite3* db, const char* channel_id, struct topo_connect_peer** out, size_t* out_count) { if (!db || !channel_id || !out || !out_count) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "invalid group candidate query arguments"); return -1; } *out = NULL; *out_count = 0; char table[80]; peers_table_name(channel_id, table, sizeof(table)); char sql[512]; snprintf(sql, sizeof(sql), "SELECT p.node_id,p.adm_tags,s.timestamp,s.record FROM \"%s\" p" " LEFT JOIN node_snapshots s ON s.node_id=p.node_id WHERE p.deleted=0" " ORDER BY p.connected DESC,p.node_RTT IS NULL,p.node_RTT,p.node_id", table); sqlite3_stmt* stmt = NULL; int rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL); if (rc != SQLITE_OK) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "group candidate query channel=%s: %s", channel_id, sqlite3_errmsg(db)); return -1; } struct topo_connect_peer* peers = NULL; size_t count = 0, capacity = 0; while ((rc = sqlite3_step(stmt)) == SQLITE_ROW) { if (count == capacity) { size_t next = capacity ? capacity * 2 : 16; struct topo_connect_peer* list = u_realloc(peers, next * sizeof(*list)); if (!list) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "group candidate query allocation failed channel=%s", channel_id); break; } peers = list; capacity = next; } struct topo_connect_peer* peer = &peers[count++]; *peer = (struct topo_connect_peer){ .node_id = (uint64_t)sqlite3_column_int64(stmt, 0), .client_type = UINT8_MAX }; const char* tags = (const char*)sqlite3_column_text(stmt, 1); char value[16]; peer->supernode = tags && json_flat_get(tags, "supernode", value, sizeof(value)) == 0 && strcmp(value, "yes") == 0; const void* record = sqlite3_column_blob(stmt, 3); if (record) { struct TOPOMSG_NODE header; if (sqlite3_column_bytes(stmt, 3) < (int)sizeof(header)) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "short candidate snapshot channel=%s node=%016llx", channel_id, (unsigned long long)peer->node_id); break; } memcpy(&header, record, sizeof(header)); if (header.node_id != peer->node_id || header.timestamp != (uint64_t)sqlite3_column_int64(stmt, 2) || header.client_type > CLIENT_TYPE_MOBILE) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "invalid candidate snapshot channel=%s node=%016llx type=%u", channel_id, (unsigned long long)peer->node_id, header.client_type); break; } peer->client_type = header.client_type; } } if (rc != SQLITE_DONE) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "group candidate read failed channel=%s rc=%d: %s", channel_id, rc, sqlite3_errmsg(db)); sqlite3_finalize(stmt); u_free(peers); return -1; } sqlite3_finalize(stmt); *out = peers; *out_count = count; return 0; } int topo_node_sqlite_get_connected_peers(sqlite3* db, const char* channel_id, uint64_t** out_ids, int* out_count) { if (!db || !channel_id || !out_ids || !out_count) return -1; *out_ids = NULL; *out_count = 0; char peers_tbl[80]; peers_table_name(channel_id, peers_tbl, sizeof(peers_tbl)); char sql[192]; snprintf(sql, sizeof(sql), "SELECT node_id FROM \"%s\" WHERE connected=1 AND deleted=0", peers_tbl); sqlite3_stmt* st = NULL; if (sqlite3_prepare_v2(db, sql, -1, &st, NULL) != SQLITE_OK) return -1; int cnt = 0; while (sqlite3_step(st) == SQLITE_ROW) cnt++; if (cnt == 0) { sqlite3_finalize(st); return 0; } uint64_t* ids = u_malloc((size_t)cnt * sizeof(uint64_t)); if (!ids) { sqlite3_finalize(st); return -1; } sqlite3_reset(st); int i = 0; while (sqlite3_step(st) == SQLITE_ROW) ids[i++] = (uint64_t)sqlite3_column_int64(st, 0); sqlite3_finalize(st); *out_ids = ids; *out_count = cnt; return 0; } 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) { if (!db || !channel_id || !out_ids || !out_count) return -1; *out_ids = NULL; *out_count = 0; char peers_tbl[80]; peers_table_name(channel_id, peers_tbl, sizeof(peers_tbl)); char sql[384]; snprintf(sql, sizeof(sql), "SELECT DISTINCT na.node_id FROM node_addresses na" " WHERE na.node_id!=?1 AND na.family IN (4,6) AND na.addr_type IN (1,2)" " AND EXISTS (SELECT 1 FROM nodes n WHERE n.node_id=na.node_id)" " AND NOT EXISTS (SELECT 1 FROM \"%s\" p WHERE p.node_id=na.node_id AND p.deleted=1)", peers_tbl); sqlite3_stmt* st = NULL; if (sqlite3_prepare_v2(db, sql, -1, &st, NULL) != SQLITE_OK) return -1; sqlite3_bind_int64(st, 1, (sqlite3_int64)self_node_id); int cnt = 0; while (sqlite3_step(st) == SQLITE_ROW) cnt++; if (cnt == 0) { sqlite3_finalize(st); return 0; } uint64_t* ids = u_malloc((size_t)cnt * sizeof(uint64_t)); if (!ids) { sqlite3_finalize(st); return -1; } sqlite3_reset(st); int i = 0; while (sqlite3_step(st) == SQLITE_ROW) ids[i++] = (uint64_t)sqlite3_column_int64(st, 0); sqlite3_finalize(st); *out_ids = ids; *out_count = cnt; return 0; } int topo_node_sqlite_get_supernode_peers(sqlite3* db, const char* channel_id, uint64_t** out_ids, int* out_count) { if (!db || !channel_id || !out_ids || !out_count) return -1; *out_ids = NULL; *out_count = 0; char peers_tbl[80]; peers_table_name(channel_id, peers_tbl, sizeof(peers_tbl)); char sql[256]; snprintf(sql, sizeof(sql), "SELECT p.node_id FROM \"%s\" p WHERE p.node_type=4 AND p.deleted=0", peers_tbl); sqlite3_stmt* st = NULL; if (sqlite3_prepare_v2(db, sql, -1, &st, NULL) != SQLITE_OK) return -1; int cnt = 0; while (sqlite3_step(st) == SQLITE_ROW) cnt++; if (cnt == 0) { sqlite3_finalize(st); return 0; } uint64_t* ids = u_malloc((size_t)cnt * sizeof(uint64_t)); if (!ids) { sqlite3_finalize(st); return -1; } sqlite3_reset(st); int i = 0; while (sqlite3_step(st) == SQLITE_ROW) ids[i++] = (uint64_t)sqlite3_column_int64(st, 0); sqlite3_finalize(st); *out_ids = ids; *out_count = cnt; return 0; } 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) { if (!db || !channel_id || !out_ids || !out_count) return -1; *out_ids = NULL; *out_count = 0; char peers_tbl[80]; peers_table_name(channel_id, peers_tbl, sizeof(peers_tbl)); char sql[384]; snprintf(sql, sizeof(sql), "SELECT DISTINCT na.node_id FROM node_addresses na" " WHERE na.node_id!=?1 AND na.family IN (4,6) AND na.addr_type IN (0,3)" " AND EXISTS (SELECT 1 FROM nodes n WHERE n.node_id=na.node_id)" " AND NOT EXISTS (SELECT 1 FROM \"%s\" p WHERE p.node_id=na.node_id AND p.deleted=1)", peers_tbl); sqlite3_stmt* st = NULL; if (sqlite3_prepare_v2(db, sql, -1, &st, NULL) != SQLITE_OK) return -1; sqlite3_bind_int64(st, 1, (sqlite3_int64)self_node_id); int cnt = 0; while (sqlite3_step(st) == SQLITE_ROW) cnt++; if (cnt == 0) { sqlite3_finalize(st); return 0; } uint64_t* ids = u_malloc((size_t)cnt * sizeof(uint64_t)); if (!ids) { sqlite3_finalize(st); return -1; } sqlite3_reset(st); int i = 0; while (sqlite3_step(st) == SQLITE_ROW) ids[i++] = (uint64_t)sqlite3_column_int64(st, 0); sqlite3_finalize(st); *out_ids = ids; *out_count = cnt; return 0; } int topo_node_sqlite_get_node_type(sqlite3* db, const char* channel_id, uint64_t node_id) { if (!db || !channel_id || !node_id) return -1; char peers_tbl[80]; peers_table_name(channel_id, peers_tbl, sizeof(peers_tbl)); char sql[256]; snprintf(sql, sizeof(sql), "SELECT node_type FROM \"%s\" WHERE node_id=?", peers_tbl); sqlite3_stmt* st = NULL; if (sqlite3_prepare_v2(db, sql, -1, &st, NULL) != SQLITE_OK) return -1; sqlite3_bind_int64(st, 1, (sqlite3_int64)node_id); int node_type = -1; if (sqlite3_step(st) == SQLITE_ROW) node_type = sqlite3_column_int(st, 0); sqlite3_finalize(st); return node_type; }