From 46f4ce70911db215ce1c1bcbdd67a117f9e492dd Mon Sep 17 00:00:00 2001 From: evgeny Date: Tue, 6 Oct 2026 13:48:14 +0200 Subject: [PATCH] Test real group avatar block delivery, permissions and recovery --- tests/Makefile.am | 4 ++ tests/test_group_avatar.c | 132 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 136 insertions(+) create mode 100644 tests/test_group_avatar.c diff --git a/tests/Makefile.am b/tests/Makefile.am index db069f5b..82462da7 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -74,6 +74,7 @@ check_PROGRAMS = \ test_services \ test_member_files \ test_member_files_network \ + test_group_avatar \ test_daemon_worker \ test_group_exchange \ test_group_recovery \ @@ -726,6 +727,9 @@ test_member_files_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS) test_member_files_network_SOURCES = test_member_files_network.c test_member_files_network_CFLAGS = $(test_services_CFLAGS) test_member_files_network_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS) +test_group_avatar_SOURCES = test_group_avatar.c +test_group_avatar_CFLAGS = $(test_services_CFLAGS) +test_group_avatar_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS) test_daemon_worker_SOURCES = test_daemon_worker.c test_daemon_worker_CFLAGS = $(test_services_CFLAGS) -DUTUN_CLI_PATH='"$(abs_top_builddir)/src/utun"' test_daemon_worker_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS) diff --git a/tests/test_group_avatar.c b/tests/test_group_avatar.c new file mode 100644 index 00000000..be8e8436 --- /dev/null +++ b/tests/test_group_avatar.c @@ -0,0 +1,132 @@ +/* Настоящие ядра/UDP/router: подпись группы, четыре блока, backpressure и restart. */ +#include "../src/chat/group_avatar.c" +#include "../src/chat/member_sync.h" +#include "test_utils.h" +#include + +#define GROUP 6767676767ULL +#define CHANNEL "6767676767" +static struct UTUN_INSTANCE* nodes[2]; +static struct UASYNC* ua; +static unsigned received_blocks; +static uint64_t old_version; +static int capture_blocks; +static uint8_t group_private[32], group_public[32], group_x[32]; + +static void hex(const uint8_t* input, char* out) { for (unsigned i = 0; i < 32; i++) snprintf(out + i * 2, 3, "%02x", input[i]); } +static int hold(struct UTUN_INSTANCE* inst, uint64_t group, uint64_t peer, const uint8_t* data, size_t size) { + (void)inst; (void)group; (void)peer; (void)data; (void)size; return 0; +} +static void capture(int level, const char* category, const char* message) { + (void)level; (void)category; + if (!capture_blocks || !strstr(message, "group avatar: block received")) return; + received_blocks++; + struct group_avatar_record current; assert(!group_avatar_get(nodes[1], GROUP, ¤t)); + assert(current.version == old_version); // Даже последний блок ещё не опубликован до проверки всей записи. +} +static void put_member(unsigned destination, unsigned member) { + uint8_t join[64], tree[64], message[512]; + int n = member_sync_build_join_msg(group_x, group_public, nodes[member]->node_id, + nodes[member]->my_keys.public_key, 123, message, sizeof(message)); assert(n > 0); + assert(sc_ed25519_sign(nodes[member]->my_ed25519_privkey, message, n, join) == SC_OK); + assert(member_sync_sign_pubkey(group_private, nodes[member]->my_keys.public_key, tree) == 0); + assert(member_sync_put(nodes[destination], CHANNEL, nodes[member]->node_id, nodes[member]->my_keys.public_key, + nodes[member]->my_ed25519_pubkey, join, 123, NULL, 0, "{}", NULL, NULL, 0, 0, tree) >= 0); +} +static int wait_record(unsigned node, uint64_t version, const uint8_t hash[32]) { + uint64_t deadline = get_time_tb() + 150000; + while (get_time_tb() < deadline) { + struct group_avatar_record r; + if (!group_avatar_get(nodes[node], GROUP, &r) && r.version == version && !memcmp(r.hash, hash, 32)) return 1; + uasync_poll(ua, 2); + } + DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "test group avatar: sync timeout node=%u version=%llu", node, (unsigned long long)version); return 0; +} +static void set_key(unsigned node, const uint8_t* key) { + sqlite3_stmt* st; assert(sqlite3_prepare_v2(nodes[node]->topo_sqlite_db, + "UPDATE channels SET ed25519_privkey=? WHERE channel_id=?", -1, &st, NULL) == SQLITE_OK); + if (key) sqlite3_bind_blob(st, 1, key, 32, SQLITE_STATIC); else sqlite3_bind_null(st, 1); + sqlite3_bind_text(st, 2, CHANNEL, -1, SQLITE_STATIC); assert(sqlite3_step(st) == SQLITE_DONE); sqlite3_finalize(st); +} + +int main(void) { + debug_config_init(); debug_set_level(DEBUG_LEVEL_ERROR); debug_set_category_level(DEBUG_CATEGORY_CHAT, DEBUG_LEVEL_DEBUG); + utun_instance_set_tun_init_enabled(0); ua = uasync_create(); assert(ua); + struct SC_MYKEYS keys[2], channel; + assert(sc_generate_keypair(&channel) == SC_OK); memcpy(group_x, channel.public_key, 32); + uint8_t seed[64]; SHA512(channel.private_key, 32, seed); memcpy(group_private, seed, 32); + assert(sc_derive_ed25519_pubkey(channel.private_key, group_public) == 0); + for (unsigned i = 0; i < 2; i++) assert(sc_generate_keypair(&keys[i]) == SC_OK); + int port = 19000 + (int)(getpid() % 1000); + char directory[512] = "/tmp/utun-group-avatar-XXXXXX"; assert(!test_mkdtemp(directory)); + for (unsigned i = 0; i < 2; i++) { + char pub[65], priv[65], peer[65], config[2048]; hex(keys[i].public_key, pub); hex(keys[i].private_key, priv); hex(keys[1-i].public_key, peer); + snprintf(config, sizeof(config), "[global]\nmy_private_key=%s\nmy_public_key=%s\ndb_path=%s/node%u\n" + "[server:s1]\naddr=127.0.0.1:%d\ntype=public\n[allowed_keys]\nallow_all=1\n" + "[chatserver]\ngroup_autoconnect=0\nstorage_autoload=0\nmember_files_enabled=0\n" + "[client:peer]\npeer_public_key=%s\nlink=s1:127.0.0.1:%d\n", priv, pub, directory, i, port + i, peer, port + 1-i); + nodes[i] = utun_instance_create_from_str(ua, config); assert(nodes[i] && !utun_instance_init(nodes[i])); + } + uint8_t signature[64] = {0}; + struct TOPO_PEER_REQUEST* connections[2] = {0}; + for (unsigned i = 0; i < 2; i++) { + assert(!topo_node_sqlite_channel_put(nodes[i]->topo_sqlite_db, CHANNEL, "avatar", nodes[0]->node_id, + group_x, NULL, group_public, i ? NULL : group_private, signature)); + chat_core_ensure_channel_ready(nodes[i], CHANNEL); + put_member(i, 0); put_member(i, 1); + assert(!topo_group_peer_open(topo_groups_find(nodes[i]->topo_groups, GROUP), nodes[1-i]->node_id, &connections[i])); + } + uint64_t deadline = get_time_tb() + 150000; + while ((topo_group_peer_phase(connections[0]) != TOPO_PEER_READY || topo_group_peer_phase(connections[1]) != TOPO_PEER_READY) && + get_time_tb() < deadline) uasync_poll(ua, 2); + assert(topo_group_peer_phase(connections[0]) == TOPO_PEER_READY && topo_group_peer_phase(connections[1]) == TOPO_PEER_READY); + uint8_t image[GROUP_AVATAR_MAX + 1]; for (unsigned i = 0; i < sizeof(image); i++) image[i] = (uint8_t)(i * 73 + 19); + assert(group_avatar_set(nodes[0], GROUP, image, sizeof(image)) < 0); + assert(group_avatar_set(nodes[1], GROUP, image, 1) < 0); + assert(!group_avatar_set(nodes[0], GROUP, image, GROUP_AVATAR_MAX)); + struct group_avatar_record first; assert(!group_avatar_get(nodes[0], GROUP, &first)); + capture_blocks = 1; debug_set_log_hook(capture); + assert(wait_record(1, first.version, first.hash)); capture_blocks = 0; debug_set_log_hook(NULL); + assert(received_blocks == 4); + struct group_avatar_record copy; assert(!group_avatar_get(nodes[1], GROUP, ©)); assert(!memcmp(copy.data, image, GROUP_AVATAR_MAX)); + copy.signature[0] ^= 1; assert(group_avatar_apply(nodes[1], GROUP, ©) < 0); + copy = first; copy.data[0] ^= 1; assert(group_avatar_apply(nodes[1], GROUP, ©) < 0); + assert(group_avatar_apply(nodes[1], GROUP + 1, &first) < 0); + + // Повторное описание группы сохраняет ключ владельца, аватарку и UI-состояние. + assert(!topo_node_sqlite_channel_put(nodes[0]->topo_sqlite_db, CHANNEL, "avatar", nodes[0]->node_id, + group_x, NULL, group_public, NULL, signature)); + assert(!group_avatar_get(nodes[0], GROUP, ©) && copy.version == first.version); + uint8_t private_key[32]; assert(!topo_node_sqlite_channel_get_priv(nodes[0]->topo_sqlite_db, CHANNEL, private_key)); + + etcp_router_set_receive_ready(nodes[1], ETCP_RT_ID_GROUP_AVATAR, hold); + image[0] ^= 1; assert(!group_avatar_set(nodes[0], GROUP, image, 4097)); + struct group_avatar_record next; assert(!group_avatar_get(nodes[0], GROUP, &next)); + deadline = get_time_tb() + 20000; while (get_time_tb() < deadline) uasync_poll(ua, 2); + assert(!group_avatar_get(nodes[1], GROUP, ©) && copy.version == first.version); + etcp_router_conn_restart(nodes[0], GROUP, nodes[1]->node_id, ETCP_RT_ID_GROUP_AVATAR); + etcp_router_set_receive_ready(nodes[1], ETCP_RT_ID_GROUP_AVATAR, ga_receive_ready); + etcp_router_resume_receive(nodes[1], GROUP, nodes[0]->node_id, ETCP_RT_ID_GROUP_AVATAR); + assert(wait_record(1, next.version, next.hash)); assert(group_avatar_apply(nodes[1], GROUP, &first) == 0); + + // Держатель ключа, который не является создателем группы, тоже может обновить картинку. + set_key(1, group_private); assert(!group_avatar_set(nodes[1], GROUP, image, 1)); set_key(1, NULL); + assert(!group_avatar_get(nodes[1], GROUP, &next)); assert(wait_record(0, next.version, next.hash)); + copy = next; copy.data[0]++; + SHA256(copy.data, copy.size, copy.hash); uint8_t signed_bytes[GROUP_AVATAR_MAX + 64]; + assert(sc_ed25519_sign(group_private, signed_bytes, ga_message(GROUP, ©, signed_bytes), copy.signature) == SC_OK); + int accepted = ga_compare(copy.version, copy.hash, next.version, next.hash) > 0; + assert(group_avatar_apply(nodes[0], GROUP, ©) == accepted); + assert(!group_avatar_set(nodes[0], GROUP, NULL, 0)); assert(!group_avatar_get(nodes[0], GROUP, &next)); + assert(wait_record(1, next.version, next.hash)); assert(!group_avatar_get(nodes[1], GROUP, ©) && !copy.size); + assert(group_avatar_apply(nodes[1], GROUP, &first) == 0); + + topo_group_peer_close(connections[0]); topo_group_peer_close(connections[1]); + utun_instance_destroy(nodes[1]); utun_instance_destroy(nodes[0]); + char config[1024]; snprintf(config, sizeof(config), "[global]\ndb_path=%s/node0\n[chatserver]\nmember_files_enabled=0\n", directory); + nodes[0] = utun_instance_create_from_str(ua, config); assert(nodes[0]); + assert(!utun_core_start(nodes[0]) && !chat_service_start(nodes[0])); + assert(!group_avatar_get(nodes[0], GROUP, ©) && copy.version == next.version && !copy.size); + utun_instance_destroy(nodes[0]); uasync_destroy(ua, 1); + puts("group avatar: PASS limits, signatures, four 4KiB blocks, atomicity, restart, key holders and tombstones"); return 0; +}