2 changed files with 136 additions and 0 deletions
@ -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 <assert.h> |
||||
|
||||
#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; |
||||
} |
||||
Loading…
Reference in new issue