/* Реальные SQLite, подписи и Merkle-адаптер: версии, отмены, конфликты и rollback. */ #include "../src/chat/chat_feedback.c" #include static const uint8_t owner_private[32] = {9}; static int events; enum sql_fault { F_NONE, F_BEGIN, F_PREPARE, F_BIND, F_STEP, F_CHANGES, F_FINALIZE, F_COMMIT }; enum sql_operation { F_GET, F_PUT }; static enum sql_fault injected; static enum sql_operation injected_operation; static int injected_bind, failed_rollback, last_put; static int fault_statement(const char* sql) { const char* prefix = injected_operation == F_GET ? "SELECT message_id,node_id,kind,ver,data,signature FROM feedback_1001 " : "INSERT INTO feedback_1001(record_key,bucket,message_id,node_id,kind,ver,data,signature)"; return sql && !strncmp(sql, prefix, strlen(prefix)); } static int take_fault(enum sql_fault point, const char* sql, int index) { if (injected != point || (sql && !fault_statement(sql)) || (point == F_BIND && index != injected_bind)) return 0; injected = F_NONE; DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "feedback test: injected stage=%d operation=%d bind=%d", point, injected_operation, index); return 1; } extern int __real_sqlite3_exec(sqlite3*, const char*, int (*)(void*, int, char**, char**), void*, char**); int __wrap_sqlite3_exec(sqlite3* db, const char* sql, int (*cb)(void*, int, char**, char**), void* arg, char** error) { if ((!strcmp(sql, "SAVEPOINT feedback_update") || strstr(sql, "BEGIN IMMEDIATE /* feedback")) && take_fault(F_BEGIN, NULL, 0)) return SQLITE_BUSY; if ((!strcmp(sql, "RELEASE feedback_update") || !strncmp(sql, "COMMIT /* feedback", 18)) && take_fault(F_COMMIT, NULL, 0)) return SQLITE_BUSY; if (failed_rollback && !strncmp(sql, "ROLLBACK", 8)) { failed_rollback = 0; DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "feedback test: injected rollback failure"); return SQLITE_IOERR; } return __real_sqlite3_exec(db, sql, cb, arg, error); } extern int __real_sqlite3_prepare_v2(sqlite3*, const char*, int, sqlite3_stmt**, const char**); int __wrap_sqlite3_prepare_v2(sqlite3* db, const char* sql, int length, sqlite3_stmt** st, const char** tail) { if (take_fault(F_PREPARE, sql, 0)) { *st = NULL; return SQLITE_NOMEM; } return __real_sqlite3_prepare_v2(db, sql, length, st, tail); } extern int __real_sqlite3_bind_blob(sqlite3_stmt*, int, const void*, int, void (*)(void*)); int __wrap_sqlite3_bind_blob(sqlite3_stmt* st, int index, const void* bytes, int length, void (*destroy)(void*)) { if (take_fault(F_BIND, sqlite3_sql(st), index)) return SQLITE_NOMEM; return __real_sqlite3_bind_blob(st, index, bytes, length, destroy); } extern int __real_sqlite3_bind_int64(sqlite3_stmt*, int, sqlite3_int64); int __wrap_sqlite3_bind_int64(sqlite3_stmt* st, int index, sqlite3_int64 value) { if (take_fault(F_BIND, sqlite3_sql(st), index)) return SQLITE_NOMEM; return __real_sqlite3_bind_int64(st, index, value); } extern int __real_sqlite3_bind_int(sqlite3_stmt*, int, int); int __wrap_sqlite3_bind_int(sqlite3_stmt* st, int index, int value) { if (take_fault(F_BIND, sqlite3_sql(st), index)) return SQLITE_NOMEM; return __real_sqlite3_bind_int(st, index, value); } extern int __real_sqlite3_step(sqlite3_stmt*); int __wrap_sqlite3_step(sqlite3_stmt* st) { const char* sql = sqlite3_sql(st); last_put = !strncmp(sql, "INSERT INTO feedback_1001(", 26); if (take_fault(F_STEP, sql, 0)) return SQLITE_IOERR; return __real_sqlite3_step(st); } extern int __real_sqlite3_changes(sqlite3*); int __wrap_sqlite3_changes(sqlite3* db) { if (last_put && take_fault(F_CHANGES, NULL, 0)) return 0; return __real_sqlite3_changes(db); } extern int __real_sqlite3_finalize(sqlite3_stmt*); int __wrap_sqlite3_finalize(sqlite3_stmt* st) { int fail = st && take_fault(F_FINALIZE, sqlite3_sql(st), 0); int rc = __real_sqlite3_finalize(st); return fail ? SQLITE_IOERR : rc; } /* Событие должно видеть опубликованную транзакцию. */ static void on_event(struct UTUN_INSTANCE* inst, int type, const uint8_t* data, int len) { (void)data; (void)len; if (type == CHAT_EVT_FEEDBACK_CHANGED) { assert(sqlite3_get_autocommit(inst->topo_sqlite_db)); events++; } } static void public_key(const uint8_t priv[32], uint8_t pub[32]) { EVP_PKEY* pk = EVP_PKEY_new_raw_private_key(EVP_PKEY_ED25519, NULL, priv, 32); assert(pk); size_t len = 32; assert(EVP_PKEY_get_raw_public_key(pk, pub, &len) == 1); EVP_PKEY_free(pk); } /* Экземпляры используют один uasync данного потока, но разные БД и идентичности. */ static void setup(struct UTUN_INSTANCE* inst, struct UASYNC* ua, const char* path) { memset(inst, 0, sizeof(*inst)); inst->ua = ua; assert(sc_generate_keypair(&inst->my_keys) == SC_OK); inst->node_id = sc_derive_node_id_from_pubkey(inst->my_keys.public_key); memcpy(inst->my_ed25519_privkey, inst->my_keys.private_key, 32); public_key(inst->my_ed25519_privkey, inst->my_ed25519_pubkey); assert(sqlite3_open(path ? path : ":memory:", &inst->topo_sqlite_db) == SQLITE_OK); assert(topo_node_sqlite_init(inst->topo_sqlite_db) == 0); uint8_t x[32] = {3}, ed[32], zero[64] = {0}; public_key(owner_private, ed); assert(topo_node_sqlite_channel_put(inst->topo_sqlite_db, "1001", "one", 1, x, NULL, ed, NULL, zero) == 0); assert(topo_node_sqlite_channel_put(inst->topo_sqlite_db, "1002", "two", 1, x, NULL, ed, NULL, zero) == 0); assert(member_sync_init(inst) == 0 && chat_feedback_init(inst) == 0); chat_event_set_handler(inst, on_event); } /* Настоящие подписи join и дерева приглашений связывают автора с каналом. */ static void add_member(struct UTUN_INSTANCE* dst, const struct UTUN_INSTANCE* author, const char* channel) { uint8_t x[32] = {3}, ed[32], join[64], tree[64], body[112]; public_key(owner_private, ed); assert(member_sync_build_join_msg(x, ed, author->node_id, author->my_keys.public_key, 1, body, sizeof(body)) == 112); assert(sc_ed25519_sign(author->my_ed25519_privkey, body, sizeof(body), join) == SC_OK); assert(member_sync_sign_pubkey(owner_private, author->my_keys.public_key, tree) == 0); assert(member_sync_put(dst, channel, author->node_id, author->my_keys.public_key, author->my_ed25519_pubkey, join, 1, NULL, 0, "", NULL, NULL, 0, 0, tree) >= 0); } static void root(struct UTUN_INSTANCE* inst, uint8_t out[32]) { assert(merkle_sync_read_hash(inst, MT_DATA_FEEDBACK, "1001", 0, 0, out) == 0); } static void sign_record(const struct UTUN_INSTANCE* author, uint64_t channel, struct chat_feedback_record* r) { uint8_t body[FB_WIRE_MAX + 32]; int bytes = fb_signed_bytes(channel, r, body); assert(bytes > 0); assert(sc_ed25519_sign(author->my_ed25519_privkey, body, bytes, r->signature) == SC_OK); } static void check_author_rejection(struct UTUN_INSTANCE* inst) { sqlite3_stmt* st = NULL; uint8_t key[32], tree[64]; assert(member_sync_sign_pubkey(owner_private, inst->my_keys.public_key, tree) == 0); assert(member_sync_verified_ed25519(inst, "1001", inst->node_id, key) == 0 && !memcmp(key, inst->my_ed25519_pubkey, 32)); assert(sqlite3_prepare_v2(inst->topo_sqlite_db, "UPDATE peers_1001 SET signature=? WHERE node_id=?", -1, &st, NULL) == SQLITE_OK); uint8_t zero[64] = {0}; sqlite3_bind_blob(st, 1, zero, 64, SQLITE_STATIC); sqlite3_bind_int64(st, 2, inst->node_id); assert(sqlite3_step(st) == SQLITE_DONE && member_sync_verified_ed25519(inst, "1001", inst->node_id, key) < 0); sqlite3_reset(st); sqlite3_bind_null(st, 1); assert(sqlite3_step(st) == SQLITE_DONE && member_sync_verified_ed25519(inst, "1001", inst->node_id, key) < 0); sqlite3_reset(st); tree[0] ^= 1; sqlite3_bind_blob(st, 1, tree, 64, SQLITE_STATIC); assert(sqlite3_step(st) == SQLITE_DONE && member_sync_verified_ed25519(inst, "1001", inst->node_id, key) < 0); sqlite3_reset(st); tree[0] ^= 1; assert(sqlite3_step(st) == SQLITE_DONE && member_sync_verified_ed25519(inst, "1001", inst->node_id, key) == 0); sqlite3_finalize(st); puts("PASS: root author requires a valid channel signature; zero, missing and forged certificates are rejected"); } /* Решение владельца проходит штатную проверку подписи и транзакцию MEMBERS. */ static int set_deleted(struct UTUN_INSTANCE* dst, const struct UTUN_INSTANCE* author, unsigned ver, int deleted) { char tags[96]; uint8_t body[128], sig[64]; snprintf(tags, sizeof(tags), "{\"ver\":\"%u\",\"deleted\":\"%s\"}", ver, deleted ? "yes" : "no"); int len = member_sync_build_owner_msg(author->node_id, tags, body, sizeof(body)); assert(len > 0); assert(sc_ed25519_sign(owner_private, body, len, sig) == SC_OK); return member_sync_put(dst, "1001", author->node_id, author->my_keys.public_key, author->my_ed25519_pubkey, NULL, 0, NULL, 0, NULL, tags, sig, 0, 0, NULL); } static void assert_stored(struct UTUN_INSTANCE* inst, const struct chat_feedback_record* expected) { struct chat_feedback_record actual; assert(chat_feedback_get(inst, "1001", expected->message_id, expected->node_id, expected->kind, &actual) == 1); assert(actual.ver == expected->ver && actual.data_len == expected->data_len); assert(!memcmp(actual.data, expected->data, actual.data_len) && !memcmp(actual.signature, expected->signature, 64)); } static int deny_node_keys(void* arg, int action, const char* table, const char* column, const char* db, const char* trigger) { (void)arg; (void)column; (void)db; (void)trigger; return action == SQLITE_READ && table && !strcmp(table, "nodes") ? SQLITE_DENY : SQLITE_OK; } static int veto_commit(void* arg) { int* remaining = arg; return (*remaining)-- > 0; } /* Исключение, purge и ожидание signer не уничтожают последние состояния и отмены. */ static void membership_regressions(struct UASYNC* ua) { struct UTUN_INSTANCE author, target, inviter; setup(&author, ua, NULL); setup(&target, ua, NULL); setup(&inviter, ua, NULL); for (unsigned ch = 0; ch < 2; ch++) { const char* channel = ch ? "1002" : "1001"; add_member(&author, &author, channel); add_member(&target, &author, channel); add_member(&target, &target, channel); } uint8_t message[32] = {11}, cancelled_message[32] = {12}, reaction[] = {1, 0}, answer[] = {1, 0, 0, 0}; struct chat_feedback_record original[3], newer, local; assert(chat_feedback_set(&author, "1001", message, CHAT_FEEDBACK_REACTION, reaction, sizeof(reaction), NULL) == 1); assert(chat_feedback_get(&author, "1001", message, author.node_id, CHAT_FEEDBACK_REACTION, &original[0]) == 1); assert(chat_feedback_set(&author, "1001", message, CHAT_FEEDBACK_ANSWER, answer, sizeof(answer), NULL) == 1); assert(chat_feedback_get(&author, "1001", message, author.node_id, CHAT_FEEDBACK_ANSWER, &original[1]) == 1); assert(chat_feedback_set(&author, "1001", cancelled_message, CHAT_FEEDBACK_REACTION, reaction, sizeof(reaction), NULL) == 1); assert(chat_feedback_set(&author, "1001", cancelled_message, CHAT_FEEDBACK_REACTION, NULL, 0, NULL) == 1); assert(chat_feedback_get(&author, "1001", cancelled_message, author.node_id, CHAT_FEEDBACK_REACTION, &original[2]) == 1); for (unsigned i = 0; i < 3; i++) assert(chat_feedback_apply(&target, "1001", &original[i]) == 1); assert(chat_feedback_set(&author, "1002", message, CHAT_FEEDBACK_REACTION, reaction, sizeof(reaction), NULL) == 1); struct chat_feedback_record other = original[0]; sign_record(&author, 1002, &other); assert(chat_feedback_apply(&target, "1002", &other) == 1); uint8_t before[32], after[32], empty[32] = {0}, other_root[32], key[32]; root(&target, before); assert(merkle_sync_read_hash(&target, MT_DATA_FEEDBACK, "1002", 0, 0, other_root) == 0); /* Подписанный профиль меняет MEMBERS, но не запускает feedback и его UI-события. */ uint8_t profile_join[64], profile_update[64], profile_body[256], profile_x[32] = {3}, profile_ed[32]; public_key(owner_private, profile_ed); int profile_len = member_sync_build_join_msg(profile_x, profile_ed, author.node_id, author.my_keys.public_key, 1, profile_body, sizeof(profile_body)); assert(profile_len == 112); assert(sc_ed25519_sign(author.my_ed25519_privkey, profile_body, profile_len, profile_join) == SC_OK); profile_len = member_sync_build_update_msg(profile_join, 2, "{\"name\":\"profile\"}", profile_body, sizeof(profile_body)); assert(profile_len > 0 && sc_ed25519_sign(author.my_ed25519_privkey, profile_body, profile_len, profile_update) == SC_OK); int profile_events = events; int profile_changed = member_sync_put(&target, "1001", author.node_id, author.my_keys.public_key, author.my_ed25519_pubkey, profile_join, 1, profile_update, 2, "{\"name\":\"profile\"}", NULL, NULL, 0, 0, NULL); assert((profile_changed & MS_APPLY_CHANGED) && !(profile_changed & MS_APPLY_FEEDBACK) && events == profile_events); root(&target, after); assert(!memcmp(before, after, 32)); int saved_events = events; assert(set_deleted(&target, &author, 1, 1) & MS_APPLY_CHANGED); root(&target, after); assert(!memcmp(after, empty, 32) && events > saved_events); assert(member_sync_author_key(&target, "1001", author.node_id, key) == MS_AUTHOR_INACTIVE); assert(member_sync_author_key(&target, "1001", target.node_id, key) == MS_AUTHOR_ACTIVE); assert(member_sync_author_key(&target, "1002", author.node_id, key) == MS_AUTHOR_ACTIVE); assert(merkle_sync_read_hash(&target, MT_DATA_FEEDBACK, "1002", 0, 0, after) == 0 && !memcmp(after, other_root, 32)); for (unsigned i = 0; i < 3; i++) assert_stored(&target, &original[i]); merkle_key_t cursor = {{0}}, record_key, next; int more; uint8_t page[2 + 2 * FB_WIRE_MAX]; size_t bytes = sizeof(page); assert(fb_key(1001, &original[0], &record_key) == 0); assert(fb_page(&target, "1001", merkle_sync_level_prefix(fb_read64(record_key.bytes), 5), 0, &cursor, page, &bytes, &next, &more) == 0 && bytes == 2 && !page[0] && !page[1] && !more); member_sync_destroy(&target); assert(member_sync_init(&target) == 0 && chat_feedback_init(&target) == 0); root(&target, after); assert(!memcmp(after, empty, 32)); for (unsigned i = 0; i < 3; i++) assert_stored(&target, &original[i]); assert(set_deleted(&author, &author, 1, 1) & MS_APPLY_CHANGED); assert(chat_feedback_set(&author, "1001", message, CHAT_FEEDBACK_REACTION, reaction, sizeof(reaction), NULL) < 0); assert(chat_feedback_set(&author, "1001", message, CHAT_FEEDBACK_REACTION, NULL, 0, NULL) < 0); assert(set_deleted(&author, &author, 2, 0) & MS_APPLY_CHANGED); newer = original[0]; newer.ver++; newer.data[1] = 1; sign_record(&author, 1001, &newer); saved_events = events; assert(chat_feedback_apply(&target, "1001", &newer) == 0 && events == saved_events); assert_stored(&target, &original[0]); assert(set_deleted(&target, &author, 2, 0) & MS_APPLY_CHANGED); root(&target, after); assert(!memcmp(before, after, 32)); assert(chat_feedback_apply(&target, "1001", &newer) == 1); original[0] = newer; puts("PASS: exclusion/restart retains reaction, answer and cancellation; local no-op is denied; other channel is intact"); /* Старая страница исключённого автора не блокирует корректное состояние другого. */ assert(set_deleted(&target, &author, 3, 1) & MS_APPLY_CHANGED); local = original[0]; local.node_id = target.node_id; local.ver = 1; sign_record(&target, 1001, &local); page[0] = 0; page[1] = 2; int n = fb_encode(&newer, page + 2), m = fb_encode(&local, page + 2 + n); assert(n > 0 && m > 0 && fb_apply_page(&target, "1001", target.node_id, page, 2 + n + m) == 0); assert_stored(&target, &local); assert_stored(&target, &original[0]); assert(set_deleted(&target, &author, 4, 0) & MS_APPLY_CHANGED); root(&target, before); /* Короткое повреждение сертификата удаляет только участника, а не его feedback. */ sqlite3_stmt* st = NULL; assert(sqlite3_prepare_v2(target.topo_sqlite_db, "UPDATE peers_1001 SET signature=x'01' WHERE node_id=?", -1, &st, NULL) == SQLITE_OK); assert(sqlite3_bind_int64(st, 1, (sqlite3_int64)author.node_id) == SQLITE_OK); assert(sqlite3_step(st) == SQLITE_DONE); assert(sqlite3_finalize(st) == SQLITE_OK); assert(member_sync_verify_and_purge(&target, "1001") == 1); assert(member_sync_author_key(&target, "1001", author.node_id, key) == MS_AUTHOR_INACTIVE); for (unsigned i = 0; i < 3; i++) assert_stored(&target, &original[i]); member_sync_destroy(&target); assert(member_sync_init(&target) == 0 && chat_feedback_init(&target) == 0); add_member(&target, &author, "1001"); root(&target, after); assert(!memcmp(before, after, 32)); struct chat_feedback_record old_cancel = original[2]; old_cancel.ver = 1; old_cancel.data_len = sizeof(reaction); memcpy(old_cancel.data, reaction, sizeof(reaction)); sign_record(&author, 1001, &old_cancel); assert(chat_feedback_apply(&target, "1001", &old_cancel) == 0); assert_stored(&target, &original[2]); puts("PASS: mixed page, physical purge and restored membership preserve the version barrier of cancellations"); /* Подтверждение автора зависит от ещё неизвестного общего ключа пригласившего. */ uint8_t channel_x[32] = {3}, channel_ed[32], body[112], join[64], invitation[64]; public_key(owner_private, channel_ed); assert(member_sync_build_join_msg(channel_x, channel_ed, author.node_id, author.my_keys.public_key, 2, body, sizeof(body)) == 112); assert(sc_ed25519_sign(author.my_ed25519_privkey, body, sizeof(body), join) == SC_OK); assert(member_sync_sign_pubkey(inviter.my_ed25519_privkey, author.my_keys.public_key, invitation) == 0); struct ms_member_rec update = { .node_id = author.node_id, .x25519 = author.my_keys.public_key, .ed25519 = author.my_ed25519_pubkey, .join_sig = join, .join_ts = 2, .userinfo = "", .signed_by = inviter.node_id, .signature = invitation }; assert(member_sync_apply_record(&target, "1001", author.node_id, &update) & MS_APPLY_CHANGED); assert(member_sync_author_key(&target, "1001", author.node_id, key) == MS_AUTHOR_PENDING); root(&target, after); assert(memcmp(before, after, 32)); member_sync_destroy(&target); assert(member_sync_init(&target) == 0 && chat_feedback_init(&target) == 0); root(&target, after); uint8_t pending_root[32]; memcpy(pending_root, after, 32); newer = original[0]; newer.ver++; sign_record(&author, 1001, &newer); assert(chat_feedback_apply(&target, "1001", &newer) == 0); assert_stored(&target, &original[0]); saved_events = events; add_member(&target, &inviter, "1002"); assert(member_sync_author_key(&target, "1001", author.node_id, key) == MS_AUTHOR_ACTIVE && events > saved_events); root(&target, after); assert(!memcmp(before, after, 32) && memcmp(pending_root, after, 32)); for (unsigned i = 0; i < 3; i++) assert_stored(&target, &original[i]); puts("PASS: pending invitation survives restart; signer delivered only through another channel reactivates unchanged feedback"); /* Отказ чтения не становится штатным пропуском автора. */ memset(key, 0xa5, sizeof(key)); assert(sqlite3_set_authorizer(target.topo_sqlite_db, deny_node_keys, NULL) == SQLITE_OK); assert(member_sync_author_key(&target, "1001", author.node_id, key) == MS_AUTHOR_ERROR); for (unsigned i = 0; i < sizeof(key); i++) assert(key[i] == 0xa5); assert(chat_feedback_apply(&target, "1001", &newer) < 0); assert(sqlite3_set_authorizer(target.topo_sqlite_db, NULL, NULL) == SQLITE_OK); root(&target, after); assert(!memcmp(before, after, 32)); uint8_t member_before[32], member_after[32]; assert(merkle_sync_read_hash(&target, MT_DATA_MEMBERS, "1001", 0, 0, member_before) == 0); assert(sqlite3_exec(target.topo_sqlite_db, "CREATE TRIGGER fail_feedback_delete BEFORE DELETE ON merkle_tree_hash" " WHEN OLD.namespace='1:1001' BEGIN SELECT RAISE(ABORT,'injected feedback index failure'); END", NULL, NULL, NULL) == SQLITE_OK); saved_events = events; assert(set_deleted(&target, &author, 1, 1) < 0 && events == saved_events); assert(sqlite3_get_autocommit(target.topo_sqlite_db)); assert(member_sync_author_key(&target, "1001", author.node_id, key) == MS_AUTHOR_ACTIVE); root(&target, after); assert(!memcmp(before, after, 32)); assert(merkle_sync_read_hash(&target, MT_DATA_MEMBERS, "1001", 0, 0, member_after) == 0 && !memcmp(member_before, member_after, 32)); assert(sqlite3_exec(target.topo_sqlite_db, "DROP TRIGGER fail_feedback_delete", NULL, NULL, NULL) == SQLITE_OK); assert(set_deleted(&target, &author, 1, 1) & MS_APPLY_CHANGED); root(&target, before); saved_events = events; int vetoes = 1; sqlite3_commit_hook(target.topo_sqlite_db, veto_commit, &vetoes); assert(set_deleted(&target, &author, 2, 0) < 0 && events == saved_events); sqlite3_commit_hook(target.topo_sqlite_db, NULL, NULL); assert(sqlite3_get_autocommit(target.topo_sqlite_db)); assert(member_sync_author_key(&target, "1001", author.node_id, key) == MS_AUTHOR_INACTIVE); root(&target, after); assert(!memcmp(before, after, 32)); assert(set_deleted(&target, &author, 2, 0) & MS_APPLY_CHANGED); root(&target, before); saved_events = events; assert(sqlite3_exec(target.topo_sqlite_db, "BEGIN IMMEDIATE", NULL, NULL, NULL) == SQLITE_OK); assert(set_deleted(&target, &author, 3, 1) & MS_APPLY_CHANGED); assert(events == saved_events); assert(sqlite3_exec(target.topo_sqlite_db, "ROLLBACK", NULL, NULL, NULL) == SQLITE_OK); root(&target, after); assert(!memcmp(before, after, 32) && events == saved_events); assert(sqlite3_exec(target.topo_sqlite_db, "BEGIN IMMEDIATE", NULL, NULL, NULL) == SQLITE_OK); int changes = set_deleted(&target, &author, 3, 1); assert(changes & MS_APPLY_CHANGED); assert(sqlite3_exec(target.topo_sqlite_db, "COMMIT", NULL, NULL, NULL) == SQLITE_OK && events == saved_events); member_sync_record_committed(&target, "1001", author.node_id, (unsigned)changes); assert(events > saved_events); for (unsigned i = 0; i < 3; i++) assert_stored(&target, &original[i]); puts("PASS: SQL lookup/index failures, commit veto and outer rollback publish no partial data, roots or events"); member_sync_destroy(&author); member_sync_destroy(&target); member_sync_destroy(&inviter); assert(sqlite3_close(author.topo_sqlite_db) == SQLITE_OK && sqlite3_close(target.topo_sqlite_db) == SQLITE_OK); assert(sqlite3_close(inviter.topo_sqlite_db) == SQLITE_OK); } /* Диагностика копии профиля: без изменения записей или доступа к приватным ключам. */ static int check_profile_authors(const char* path) { struct UTUN_INSTANCE inst = {0}; assert(sqlite3_open_v2(path, &inst.topo_sqlite_db, SQLITE_OPEN_READONLY, NULL) == SQLITE_OK); sqlite3_stmt *channels = NULL, *members = NULL; assert(sqlite3_prepare_v2(inst.topo_sqlite_db, "SELECT channel_id FROM channels", -1, &channels, NULL) == SQLITE_OK); int total = 0, rc; while ((rc = sqlite3_step(channels)) == SQLITE_ROW) { const char* channel = (const char*)sqlite3_column_text(channels, 0); char sql[256]; snprintf(sql, sizeof(sql), "SELECT node_id FROM peers_%s WHERE source=0", channel); assert(sqlite3_prepare_v2(inst.topo_sqlite_db, sql, -1, &members, NULL) == SQLITE_OK); int step; while ((step = sqlite3_step(members)) == SQLITE_ROW) { uint8_t key[32]; assert(member_sync_verified_ed25519(&inst, channel, sqlite3_column_int64(members, 0), key) == 0); total++; } assert(step == SQLITE_DONE); sqlite3_finalize(members); members = NULL; } assert(rc == SQLITE_DONE); sqlite3_finalize(channels); assert(sqlite3_close(inst.topo_sqlite_db) == SQLITE_OK); printf("PASS: all %d authors in the profile have valid node keys, joins and invitation certificates\n", total); return 0; } int main(int argc, char** argv) { debug_config_init(); debug_set_level(DEBUG_LEVEL_WARN); if (argc == 2) return check_profile_authors(argv[1]); size_t baseline = u_get_allocated_count(); struct UASYNC* ua = uasync_create(); assert(ua); struct UTUN_INSTANCE source, target; setup(&source, ua, NULL); setup(&target, ua, NULL); for (int ch = 0; ch < 2; ch++) { const char* channel = ch ? "1002" : "1001"; add_member(&source, &source, channel); add_member(&target, &source, channel); add_member(&source, &target, channel); add_member(&target, &target, channel); } check_author_rejection(&source); uint8_t message[32], other_message[32], nonce[16] = {7}; assert(chat_message_id(1001, 123, source.node_id, nonce, message) == 0); assert(chat_message_id(1002, 123, source.node_id, nonce, other_message) == 0 && memcmp(message, other_message, 32)); assert(chat_message_id(1001, 123, source.node_id, nonce, other_message) == 0 && !memcmp(message, other_message, 32)); nonce[15]++; assert(chat_message_id(1001, 123, source.node_id, nonce, other_message) == 0 && memcmp(message, other_message, 32)); puts("PASS: message identity is deterministic and binds channel, timestamp, author and nonce"); uint64_t ver; struct chat_feedback_record first, second, cancelled, actual; uint8_t a[] = {1, 0}, b[] = {1, 1}, answer_data[] = {1, 0, 0, 0}; assert(chat_feedback_set(&source, "1001", message, CHAT_FEEDBACK_REACTION, a, sizeof(a), &ver) == 1 && ver == 1); assert(chat_feedback_get(&source, "1001", message, source.node_id, CHAT_FEEDBACK_REACTION, &first) == 1); uint8_t invalid_multi[] = {2, 0, 1}; assert(chat_feedback_set(&source, "1001", message, CHAT_FEEDBACK_REACTION, invalid_multi, sizeof(invalid_multi), NULL) < 0); struct chat_feedback_record invalid = first; invalid.data_len = sizeof(invalid_multi); memcpy(invalid.data, invalid_multi, sizeof(invalid_multi)); uint8_t invalid_body[FB_WIRE_MAX + 32]; int invalid_bytes = fb_signed_bytes(1001, &first, invalid_body); assert(invalid_bytes > 4); invalid_body[invalid_bytes - 3] = sizeof(invalid_multi); invalid_body[invalid_bytes - 2] = 2; invalid_body[invalid_bytes] = 1; assert(sc_ed25519_sign(source.my_ed25519_privkey, invalid_body, invalid_bytes + 1, invalid.signature) == SC_OK); assert(chat_feedback_apply(&target, "1001", &invalid) < 0); int saved_events = events; assert(chat_feedback_set(&source, "1001", message, CHAT_FEEDBACK_REACTION, a, sizeof(a), &ver) == 0 && ver == 1); assert(events == saved_events); assert(chat_feedback_set(&source, "1001", message, CHAT_FEEDBACK_REACTION, b, sizeof(b), &ver) == 1 && ver == 2); assert(chat_feedback_get(&source, "1001", message, source.node_id, CHAT_FEEDBACK_REACTION, &second) == 1); assert(chat_feedback_set(&source, "1001", message, CHAT_FEEDBACK_REACTION, NULL, 0, &ver) == 1 && ver == 3); assert(chat_feedback_get(&source, "1001", message, source.node_id, CHAT_FEEDBACK_REACTION, &cancelled) == 1); assert(cancelled.ver == 3 && !cancelled.data_len); assert(chat_feedback_apply(&target, "1001", &cancelled) == 1); assert(chat_feedback_apply(&target, "1001", &first) == 0 && chat_feedback_apply(&target, "1001", &second) == 0); assert(chat_feedback_get(&target, "1001", message, source.node_id, CHAT_FEEDBACK_REACTION, &actual) == 1); assert(actual.ver == 3 && !actual.data_len); uint8_t expected[32], received[32]; root(&source, expected); root(&target, received); assert(!memcmp(expected, received, 32)); puts("PASS: ver increases; duplicates are inert; old states cannot resurrect a cancellation"); struct chat_feedback_record forged = cancelled; forged.ver++; root(&target, expected); saved_events = events; assert(chat_feedback_apply(&target, "1001", &forged) < 0 && events == saved_events); root(&target, received); assert(!memcmp(expected, received, 32)); assert(chat_feedback_apply(&target, "1002", &cancelled) < 0); forged = cancelled; forged.node_id = target.node_id; assert(chat_feedback_apply(&target, "1001", &forged) < 0); puts("PASS: signature binds version, channel and author; rejection preserves tree and events"); struct chat_feedback_record low = first, high = first; low.ver = high.ver = 4; low.data[1] = 3; high.data[1] = 4; sign_record(&source, 1001, &low); sign_record(&source, 1001, &high); assert(chat_feedback_apply(&source, "1001", &low) == 1 && chat_feedback_apply(&source, "1001", &high) == 1); assert(chat_feedback_apply(&target, "1001", &high) == 1 && chat_feedback_apply(&target, "1001", &low) == 0); root(&source, expected); root(&target, received); assert(!memcmp(expected, received, 32)); assert(chat_feedback_set(&source, "1001", message, CHAT_FEEDBACK_ANSWER, answer_data, sizeof(answer_data), &ver) == 1 && ver == 1); puts("PASS: equal-version conflicts converge in both orders; answer and reaction have independent versions"); struct chat_feedback_record newer = high, answer = first; newer.ver = 5; newer.data[1] = 5; sign_record(&source, 1001, &newer); answer.kind = CHAT_FEEDBACK_ANSWER; answer.ver = 2; answer.data_len = sizeof(answer_data); memcpy(answer.data, answer_data, sizeof(answer_data)); sign_record(&source, 1001, &answer); uint8_t page[2 + 2 * FB_WIRE_MAX]; page[0] = 0; page[1] = 2; int n = fb_encode(&newer, page + 2); assert(n > 0); int m = fb_encode(&answer, page + 2 + n); assert(m > 0); root(&target, expected); saved_events = events; page[2 + n + m - 1] ^= 1; assert(fb_apply_page(&target, "1001", source.node_id, page, 2 + n + m) < 0); root(&target, received); assert(!memcmp(expected, received, 32) && events == saved_events); page[2 + n + m - 1] ^= 1; assert(sqlite3_exec(target.topo_sqlite_db, "CREATE TRIGGER fail_answer BEFORE INSERT ON feedback_1001" " WHEN NEW.kind=2 BEGIN SELECT RAISE(ABORT,'injected feedback failure'); END", NULL, NULL, NULL) == SQLITE_OK); assert(fb_apply_page(&target, "1001", source.node_id, page, 2 + n + m) < 0); root(&target, received); assert(!memcmp(expected, received, 32) && events == saved_events); assert(sqlite3_exec(target.topo_sqlite_db, "DROP TRIGGER fail_answer", NULL, NULL, NULL) == SQLITE_OK); assert(fb_apply_page(&target, "1001", source.node_id, page, 2 + n + m - 1) < 0); assert(fb_apply_page(&target, "1001", source.node_id, page, 2 + n + m) == 0 && events == saved_events + 2); puts("PASS: malformed page, invalid signature and SQLite failure roll back the whole page"); merkle_key_t key, cursor = {{0}}, next; assert(fb_key(1001, &newer, &key) == 0); size_t bytes = sizeof(page); int more; assert(fb_page(&target, "1001", merkle_sync_level_prefix(fb_read64(key.bytes), 5), 0, &cursor, page, &bytes, &next, &more) == 0 && !more); assert(bytes >= 2 + FB_FIXED_SIZE && page[1] >= 1); struct chat_feedback_record decoded; assert(fb_decode(page + 2, bytes - 2, &decoded) > 0); assert(decoded.ver == 5 && decoded.data[1] == 5); assert(chat_feedback_set(&target, "1001", message, CHAT_FEEDBACK_REACTION, NULL, 0, &ver) == 1 && ver == 1); assert(chat_feedback_set(&target, "1001", message, CHAT_FEEDBACK_REACTION, a, sizeof(a), &ver) == 1 && ver == 2); assert(chat_feedback_set(&target, "1001", message, CHAT_FEEDBACK_REACTION, NULL, 0, &ver) == 1 && ver == 3); root(&target, expected); member_sync_destroy(&target); assert(member_sync_init(&target) == 0 && chat_feedback_init(&target) == 0); root(&target, received); assert(!memcmp(expected, received, 32)); assert(chat_feedback_set(&target, "1001", message, CHAT_FEEDBACK_REACTION, a, sizeof(a), &ver) == 1 && ver == 4); puts("PASS: wire roundtrip; persisted empty state and ver survive Merkle rebuild"); assert(topo_node_sqlite_channel_delete(target.topo_sqlite_db, "1001") == 0); sqlite3_stmt* st; assert(sqlite3_prepare_v2(target.topo_sqlite_db, "SELECT 1 FROM sqlite_master WHERE name='feedback_1001'", -1, &st, NULL) == SQLITE_OK); assert(sqlite3_step(st) == SQLITE_DONE); sqlite3_finalize(st); assert(merkle_sync_read_hash(&target, MT_DATA_FEEDBACK, "1001", 0, 0, received) == 0); uint8_t empty[32] = {0}; assert(!memcmp(received, empty, 32)); puts("PASS: deleting a channel removes its feedback table and derived root"); membership_regressions(ua); member_sync_destroy(&source); member_sync_destroy(&target); assert(sqlite3_close(source.topo_sqlite_db) == SQLITE_OK && sqlite3_close(target.topo_sqlite_db) == SQLITE_OK); uasync_poll(ua, 0); uasync_destroy(ua, 0); assert(u_get_allocated_count() == baseline); puts("ALL PASS: signed feedback regressions, no tracked allocations leaked"); return 0; }