/* Реальные SQLite, подписи и Merkle-адаптер: версии, отмены, конфликты и rollback. */ #include "../src/chat/chat_feedback.c" #include static const uint8_t owner_private[32] = {9}; static int events; /* Событие должно видеть опубликованную транзакцию. */ 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) { 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(":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); } int main(void) { debug_config_init(); debug_set_level(DEBUG_LEVEL_WARN); size_t baseline = u_get_allocated_count(); struct UASYNC* ua = uasync_create(); assert(ua); struct UTUN_INSTANCE source, target; setup(&source, ua); setup(&target, ua); 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); } uint8_t message[32], other_message[32], signature[64] = {7}; assert(chat_message_id(1001, 123, signature, message) == 0); assert(chat_message_id(1002, 123, signature, other_message) == 0 && memcmp(message, other_message, 32)); assert(chat_message_id(1001, 123, signature, other_message) == 0 && !memcmp(message, other_message, 32)); signature[63]++; assert(chat_message_id(1001, 123, signature, other_message) == 0 && memcmp(message, other_message, 32)); puts("PASS: message identity is deterministic and binds channel, timestamp and signature"); uint64_t ver; struct chat_feedback_record first, second, cancelled, actual; uint8_t a[] = {1, 0}, b[] = {2, 0, 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); 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"); 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; }