diff --git a/src/stream/stream_member.c b/src/stream/stream_member.c index 7520146f..0f36f785 100644 --- a/src/stream/stream_member.c +++ b/src/stream/stream_member.c @@ -221,13 +221,30 @@ int stream_member_online(struct UTUN_INSTANCE *inst, uint64_t group_id, uint64_t return node && topo_node_best_path(node) != NULL; } -/* Роль относится к каналу, не к устройству или глобальному media_delivery. */ +/* Роль читается по ID из блока владельца канала, без загрузки остальных участников и снимков узлов. */ int stream_member_supernode(struct UTUN_INSTANCE *inst, uint64_t group_id, uint64_t member_id) { struct TOPO_GROUP *group = inst && inst->topo_groups ? topo_groups_find(inst->topo_groups, group_id) : NULL; if (!group || group->stopping || group->group_type != TOPO_GROUP_TYPE_CHAT) return 0; - struct topo_connect_peer *peers = NULL; size_t count = 0; - if (topo_node_sqlite_get_connect_peers(inst->topo_sqlite_db, group->channel_id, &peers, &count) < 0) return -1; - int supernode = 0; - for (size_t i = 0; i < count; i++) if (peers[i].node_id == member_id) { supernode = peers[i].supernode; break; } - u_free(peers); return supernode; + if (!inst->topo_sqlite_db || !group->channel_id[0]) { + DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "member role missing DB/channel group=%llx", (unsigned long long)group_id); return -1; + } + for (const char *p = group->channel_id; *p; p++) if (*p < '0' || *p > '9') { + DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "member role invalid channel namespace group=%llx", (unsigned long long)group_id); return -1; + } + char query[192]; + snprintf(query, sizeof(query), "SELECT adm_tags FROM \"peers_%s\" WHERE node_id=? AND deleted=0", group->channel_id); + sqlite3 *db = inst->topo_sqlite_db; sqlite3_stmt *statement = NULL; + int result = -1, rc = sqlite3_prepare_v2(db, query, -1, &statement, NULL); + if (rc == SQLITE_OK) rc = sqlite3_bind_int64(statement, 1, (sqlite3_int64)member_id); + if (rc == SQLITE_OK) rc = sqlite3_step(statement); + if (rc == SQLITE_ROW) { + const char *tags = (const char *)sqlite3_column_text(statement, 0); char value[16]; + result = tags && json_flat_get(tags, "supernode", value, sizeof(value)) == 0 && !strcmp(value, "yes"); + } else if (rc == SQLITE_DONE) result = 0; + else DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "member role query failed group=%llx member=%llx rc=%d: %s", + (unsigned long long)group_id, (unsigned long long)member_id, rc, sqlite3_errmsg(db)); + sqlite3_finalize(statement); + DEBUG_TRACE(DEBUG_CATEGORY_STREAM, "member role group=%llx member=%llx supernode=%d", + (unsigned long long)group_id, (unsigned long long)member_id, result); + return result; } diff --git a/tests/test_stream_service.c b/tests/test_stream_service.c index 59945798..8763bd5c 100644 --- a/tests/test_stream_service.c +++ b/tests/test_stream_service.c @@ -1046,6 +1046,30 @@ static void relay_role(unsigned member, int enabled, unsigned version) { } } +/* Роль немедленно следует за подписанным блоком канала; deleted и чужой snapshot проверяются отдельно. */ +static void test_supernode_lookup(void) { + uint64_t member = nodes[1]->node_id; + CHECK(stream_member_supernode(nodes[0], GROUP2, member) == 0); + relay_role(1, 1, 90); + CHECK(stream_member_supernode(nodes[0], GROUP1, member) == 0); + CHECK(stream_member_supernode(nodes[0], GROUP2, UINT64_MAX) == 0); + sqlite3 *db = nodes[0]->topo_sqlite_db; sqlite3_stmt *statement = NULL; + CHECK(sqlite3_exec(db, "SAVEPOINT role_lookup", NULL, NULL, NULL) == SQLITE_OK); + CHECK(sqlite3_prepare_v2(db, "UPDATE peers_710002 SET deleted=1 WHERE node_id=?", -1, &statement, NULL) == SQLITE_OK); + sqlite3_bind_int64(statement, 1, (sqlite3_int64)member); + CHECK(sqlite3_step(statement) == SQLITE_DONE && sqlite3_changes(db) == 1); sqlite3_finalize(statement); + CHECK(stream_member_supernode(nodes[0], GROUP2, member) == 0); + CHECK(sqlite3_exec(db, "ROLLBACK TO role_lookup", NULL, NULL, NULL) == SQLITE_OK); + CHECK(stream_member_supernode(nodes[0], GROUP2, member) == 1); + CHECK(sqlite3_prepare_v2(db, "UPDATE node_snapshots SET record=x'00' WHERE node_id=?", -1, &statement, NULL) == SQLITE_OK); + sqlite3_bind_int64(statement, 1, (sqlite3_int64)nodes[2]->node_id); + CHECK(sqlite3_step(statement) == SQLITE_DONE && sqlite3_changes(db) == 1); sqlite3_finalize(statement); + CHECK(stream_member_supernode(nodes[0], GROUP2, member) == 1); + CHECK(sqlite3_exec(db, "ROLLBACK TO role_lookup; RELEASE role_lookup", NULL, NULL, NULL) == SQLITE_OK); + relay_role(1, 0, 91); + DEBUG_INFO(DEBUG_CATEGORY_STREAM, "[PASS] supernode lookup: signed grant/revoke, channel isolation, absent/deleted member, unrelated snapshot"); +} + /* Прямой stream-вход источника блокируем только у зрителей; Merkle/BGP и получение суперузла остаются настоящими. */ static void relay_test_raw(struct ETCP_CONN *conn, struct ll_entry *entry) { unsigned index = 0; while (index < 4 && conn->instance != nodes[index]) index++; CHECK(index < 4); @@ -1586,6 +1610,7 @@ int main(int argc, char **argv) { CHECK(identity && topo_node_sqlite_snapshot_put(nodes[destination]->topo_sqlite_db, identity) >= 0); } } + test_supernode_lookup(); struct TOPO_PEER_REQUEST *connections[3] = {0}; for (unsigned i = 1; i < 4; i++) CHECK(topo_group_peer_open(topo_groups_find(nodes[0]->topo_groups, GROUP1), nodes[i]->node_id, &connections[i - 1]) == 0);