/* Реальный адаптер, SQLite и транзакции; без копии алгоритма и без сетевых таймингов. */ #include "../src/chat/member_sync.c" #include "../src/chat/chat_member.h" #include "../src/member_files/protocol.h" #include void test_member_purge(struct UASYNC* ua); static unsigned apply_calls, cancelled_calls; static int nested_apply; static void cancelled_apply(const char* ns, uint64_t node, const uint8_t* x, uint64_t parent, const uint8_t* sig, void* arg) { (void)ns; (void)node; (void)x; (void)parent; (void)sig; (void)arg; cancelled_calls++; } static void removing_apply(const char* ns, uint64_t node, const uint8_t* x, uint64_t parent, const uint8_t* sig, void* arg) { struct UTUN_INSTANCE* inst = arg; apply_calls++; assert(!strcmp(ns, "1001")); if (!nested_apply) { nested_apply = 1; _fire_apply_cbk(inst, ns, node, x, parent, sig); nested_apply = 0; } member_sync_remove_apply_cbk(inst, removing_apply, inst); member_sync_remove_apply_cbk(inst, cancelled_apply, inst); } static void free_namespace(uint64_t node, const char* tags, const char* ns, void* arg) { (void)node; (void)tags; assert(!strcmp(ns, "1001")); char** owned = arg; u_free(*owned); *owned = NULL; } static void check_namespace(uint64_t node, const char* tags, const char* ns, void* arg) { (void)node; (void)tags; (void)arg; assert(!strcmp(ns, "1001")); } static void callback_lifetime(void) { size_t baseline = u_get_allocated_count(); struct UTUN_INSTANCE inst = {0}; struct chat_core_ctx cc = {.inst = &inst}; inst.chat_core = &cc; uint8_t key[32] = {1}, signature[64] = {1}; assert(!member_sync_add_apply_cbk(&inst, cancelled_apply, &inst)); assert(!member_sync_add_apply_cbk(&inst, removing_apply, &inst)); _fire_apply_cbk(&inst, "1001", 7, key, 0, signature); assert(apply_calls == 2 && !cancelled_calls && !cc.apply_cbks && !cc.apply_dispatch_depth); char* owned = u_strdup("1001"); assert(owned); assert(!member_sync_add_props_cbk(&inst, check_namespace, &owned)); assert(!member_sync_add_props_cbk(&inst, free_namespace, &owned)); _fire_props_changed(&inst, 7, NULL, owned); assert(!owned); member_sync_remove_props_cbk(&inst, check_namespace, &owned); member_sync_remove_props_cbk(&inst, free_namespace, &owned); assert(!cc.props_cbks && !cc.props_dispatch_depth && u_get_allocated_count() == baseline); puts("PASS: nested apply dispatch,self/other cancellation,and namespace owner freed during properties callback"); } static void setup(struct UTUN_INSTANCE* inst, struct UASYNC* ua) { memset(inst, 0, sizeof(*inst)); inst->ua = ua; inst->node_id = 1; assert(sqlite3_open(":memory:", &inst->topo_sqlite_db) == SQLITE_OK); assert(topo_node_sqlite_init(inst->topo_sqlite_db) == 0); uint8_t key[32] = {1}, signature[64] = {0}; assert(topo_node_sqlite_channel_put(inst->topo_sqlite_db, "1001", "test", 1, key, NULL, key, NULL, signature) == 0); assert(member_sync_init(inst) == 0); } static void put(struct UTUN_INSTANCE* inst, uint64_t id) { uint8_t key[32] = {1}; assert(member_sync_put(inst, "1001", id, key, key, NULL, 0, NULL, 0, "{}", NULL, NULL, 0, 0, NULL) >= 0); } static int snapshot_list_events, snapshot_updates, snapshot_bad; static uint64_t snapshot_first, snapshot_last; /* Событие проверяем синхронно, не сохраняем заимствованный буфер и не печатаем тысячи записей. */ static void snapshot_event(struct UTUN_INSTANCE* inst, int type, const uint8_t* data, int len) { (void)inst; if (type == CHAT_EVT_MEMBER_UPDATED) { snapshot_updates++; return; } if (type != CHAT_EVT_MEMBER_LIST) return; snapshot_list_events++; if (!data || len < 7 || data[0] != 4 || memcmp(data + 1, "1001", 4)) { snapshot_bad++; return; } uint16_t count; memcpy(&count, data + 5, sizeof(count)); if ((size_t)len != 7 + (size_t)count * CHAT_MEMBER_DISPLAY_SIZE) { snapshot_bad++; return; } snapshot_first = count ? mf_get64(data + 7) : 0; snapshot_last = count ? mf_get64(data + 7 + (size_t)(count - 1) * CHAT_MEMBER_DISPLAY_SIZE) : 0; } /* Настоящие SQLite-запрос/сериализатор/trampoline: count должен соответствовать числу wire-записей. */ static int test_snapshot_event_bounds(struct UASYNC* ua) { struct UTUN_INSTANCE inst; setup(&inst, ua); struct chat_core_ctx cc = { .inst = &inst, .db = inst.topo_sqlite_db, .initialized = 1 }; inst.chat_core = &cc; chat_event_set_handler(&inst, snapshot_event); const int sizes[] = {0, 1, UINT16_MAX, UINT16_MAX + 1}; int result = 0; for (unsigned test = 0; test < sizeof(sizes) / sizeof(sizes[0]); test++) { const int count = sizes[test]; inst.node_id = count; cc.my_node_id = count; assert(sqlite3_exec(cc.db, "DELETE FROM peers_1001", NULL, NULL, NULL) == SQLITE_OK); if (count) { char sql[512]; snprintf(sql, sizeof(sql), "WITH RECURSIVE n(id) AS (VALUES(1) UNION ALL SELECT id+1 FROM n WHERE id<%d) " "INSERT INTO peers_1001(node_id,x25519_pubkey,ed25519_pubkey,join_sig,join_ts,source) " "SELECT id,zeroblob(32),zeroblob(32),zeroblob(64),0,1 FROM n", count); assert(sqlite3_exec(cc.db, sql, NULL, NULL, NULL) == SQLITE_OK); } snapshot_list_events = snapshot_updates = snapshot_bad = 0; snapshot_first = snapshot_last = 0; const size_t before = u_get_allocated_count(); struct chat_member_list_req* request = u_calloc(1, sizeof(*request)); assert(request); request->inst = &inst; strcpy(request->ch_id, "1001"); chat_core_request_member_list_trampoline(request); if (snapshot_bad || snapshot_list_events != (count <= UINT16_MAX) || (count <= UINT16_MAX && count && (snapshot_first != (uint64_t)count || snapshot_last != (uint64_t)(count > 1 ? count - 1 : 1)))) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "member event boundary failed count=%d events=%d malformed=%d first=%llu last=%llu", count, snapshot_list_events, snapshot_bad, (unsigned long long)snapshot_first, (unsigned long long)snapshot_last); result = -1; } assert(u_get_allocated_count() == before); if (count <= 1) { request = u_calloc(1, sizeof(*request)); assert(request); request->inst = &inst; strcpy(request->ch_id, "1001"); chat_core_request_member_rtt_trampoline(request); assert(snapshot_updates == count && u_get_allocated_count() == before); } } if (!result) { uint8_t* wire = NULL; int count = -1; assert(chat_core_get_member_list(&inst, "1001", &wire, &count) == -1 && !wire && !count); u_free(wire); wire = NULL; assert(sqlite3_exec(cc.db, "DELETE FROM peers_1001;" "WITH n(id) AS (VALUES(1),(2),(-1),(-9223372036854775808)) " "INSERT INTO peers_1001(node_id,x25519_pubkey,ed25519_pubkey,join_sig,join_ts,source) " "SELECT id,zeroblob(32),zeroblob(32),zeroblob(64),0,1 FROM n", NULL, NULL, NULL) == SQLITE_OK); inst.node_id = cc.my_node_id = 2; assert(chat_core_get_member_list(&inst, "1001", &wire, &count) == 0 && count == 4); const uint64_t order[] = {2, 1, UINT64_C(1) << 63, UINT64_MAX}; for (int i = 0; i < 4; i++) { assert(mf_get64(wire + i * CHAT_MEMBER_DISPLAY_SIZE) == order[i]); assert(wire[i * CHAT_MEMBER_DISPLAY_SIZE + 8] == (i == 0)); } u_free(wire); wire = NULL; assert(chat_core_get_member_list(NULL, "1001", &wire, &count) < 0 && !wire && count == 0); struct chat_member_list_req* request = u_calloc(1, sizeof(*request)); assert(request); chat_core_request_member_list_trampoline(request); request = u_calloc(1, sizeof(*request)); assert(request); chat_core_request_member_rtt_trampoline(request); } inst.chat_core = NULL; member_sync_destroy(&inst); assert(sqlite3_close(inst.topo_sqlite_db) == SQLITE_OK); return result; } static void root(struct UTUN_INSTANCE* inst, uint8_t hash[32]) { assert(merkle_sync_read_hash(inst, MT_DATA_MEMBERS, "1001", 0, 0, hash) == 0); } 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); } /* Подтверждение корня доставляется независимо от версии профиля и переживает нулевой блок на проводе. */ static void test_invitation_certificate(struct UASYNC* ua) { struct UTUN_INSTANCE source, target; setup(&source, ua); setup(&target, ua); uint8_t x[32] = {9}, priv[32] = {7}, pub[32], owner_priv[32] = {8}, owner_pub[32], zero[64] = {0}; public_key(priv, pub); public_key(owner_priv, owner_pub); struct UTUN_INSTANCE* nodes[] = { &source, &target }; for (unsigned i = 0; i < 2; i++) { assert(topo_node_sqlite_channel_delete(nodes[i]->topo_sqlite_db, "1001") == 0); assert(topo_node_sqlite_channel_put(nodes[i]->topo_sqlite_db, "1001", "root", 1, x, NULL, owner_pub, NULL, zero) == 0); } uint64_t nid = sc_derive_node_id_from_pubkey(x); uint8_t join[64], tree[64], update[64], body[256]; int len = member_sync_build_join_msg(x, owner_pub, nid, x, 1, body, sizeof(body)); assert(len == 112); assert(sc_ed25519_sign(priv, body, len, join) == SC_OK && member_sync_sign_pubkey(owner_priv, x, tree) == 0); struct ms_member_rec rec = { .node_id = nid, .x25519 = x, .ed25519 = pub, .join_sig = join, .join_ts = 1, .update_sig = update, .update_ts = 2, .userinfo = "{\"name\":\"profile\"}", .signature = tree }; len = member_sync_build_update_msg(join, rec.update_ts, rec.userinfo, body, sizeof(body)); assert(len > 0); assert(sc_ed25519_sign(priv, body, len, update) == SC_OK); assert(member_sync_apply_record(&source, "1001", source.node_id, &rec) & MS_APPLY_CHANGED); rec.signature = zero; assert(member_sync_apply_record(&target, "1001", target.node_id, &rec) & MS_APPLY_CHANGED); assert(member_sync_verify_signature(target.topo_sqlite_db, "1001", &rec) == 0); rec.signature = tree; assert(member_sync_apply_record(&target, "1001", source.node_id + 1, &rec) & MS_APPLY_CHANGED); uint8_t expected[32], actual[32], stored[64]; uint64_t parent; root(&source, expected); root(&target, actual); assert(!memcmp(expected, actual, 32)); assert(topo_node_sqlite_member_get_sign(target.topo_sqlite_db, "1001", nid, &parent, stored) == 0); assert(parent == 0 && !memcmp(tree, stored, 64)); assert(member_sync_apply_record(&target, "1001", source.node_id + 1, &rec) == 0); tree[0] ^= 1; assert(member_sync_apply_record(&target, "1001", source.node_id + 1, &rec) < 0); root(&target, actual); assert(!memcmp(expected, actual, 32)); tree[0] ^= 1; /* Более свежий подписанный профиль с нулевой подписью приглашения сохраняет сертификат. */ rec.update_ts = 3; rec.userinfo = "{\"name\":\"new profile\"}"; len = member_sync_build_update_msg(join, rec.update_ts, rec.userinfo, body, sizeof(body)); assert(len > 0); assert(sc_ed25519_sign(priv, body, len, update) == SC_OK); assert(member_sync_apply_record(&source, "1001", source.node_id, &rec) & MS_APPLY_CHANGED); rec.signature = zero; assert(member_sync_apply_record(&target, "1001", source.node_id + 1, &rec) & MS_APPLY_CHANGED); assert(topo_node_sqlite_member_get_sign(target.topo_sqlite_db, "1001", nid, &parent, stored) == 0); assert(parent == 0 && !memcmp(tree, stored, 64)); root(&source, expected); root(&target, actual); assert(!memcmp(expected, actual, 32)); /* Старый профиль может донести подтверждение, сохранив более свежие userinfo/update_ts. */ assert(sqlite3_exec(target.topo_sqlite_db, "UPDATE peers_1001 SET signature=zeroblob(64)", NULL, NULL, NULL) == SQLITE_OK); assert(merkle_sync_recompute_path(&target, MT_DATA_MEMBERS, "1001", nid) == 1); rec.update_ts = 2; rec.userinfo = "{\"name\":\"old profile\"}"; rec.signature = tree; len = member_sync_build_update_msg(join, rec.update_ts, rec.userinfo, body, sizeof(body)); assert(len > 0); assert(sc_ed25519_sign(priv, body, len, update) == SC_OK); int result = member_sync_apply_record(&target, "1001", source.node_id + 1, &rec); assert((result & MS_APPLY_CHANGED) && (result & MS_APPLY_STALE)); root(&target, actual); assert(!memcmp(expected, actual, 32)); puts("PASS: root certificate validates with channel key; equal/stale profile versions do not block confirmation; empty wire signature preserves it"); for (unsigned i = 0; i < 2; i++) { member_sync_destroy(nodes[i]); assert(sqlite3_close(nodes[i]->topo_sqlite_db) == SQLITE_OK); } } static void test_owner_sizes(struct UASYNC* ua) { struct UTUN_INSTANCE source, target; setup(&source, ua); setup(&target, ua); uint8_t x[32] = {9}, priv[32] = {7}, pub[32], owner_priv[32] = {8}, owner_pub[32], zero[64] = {0}; public_key(priv, pub); public_key(owner_priv, owner_pub); /* setup создаёт пустой канал с тестовым ключом; новую идентичность создаём как новый канал. */ assert(topo_node_sqlite_channel_delete(source.topo_sqlite_db, "1001") == 0); assert(topo_node_sqlite_channel_delete(target.topo_sqlite_db, "1001") == 0); assert(topo_node_sqlite_channel_put(source.topo_sqlite_db, "1001", "test", 1, x, NULL, owner_pub, owner_priv, zero) == 0); assert(topo_node_sqlite_channel_put(target.topo_sqlite_db, "1001", "test", 1, x, NULL, owner_pub, NULL, zero) == 0); uint64_t nid = sc_derive_node_id_from_pubkey(x); uint8_t join[64], tree[64], owner[64], update[64], msg[MS_ADM_TAGS_MAX + 8]; int n = member_sync_build_join_msg(x, owner_pub, nid, x, 1, msg, sizeof(msg)); assert(n > 0); assert(sc_ed25519_sign(priv, msg, n, join) == SC_OK); assert(member_sync_sign_pubkey(owner_priv, x, tree) == 0); char tags[MS_ADM_TAGS_MAX + 2]; char userinfo[MS_USERINFO_MAX + 1]; struct ms_member_rec rec = { .node_id = nid, .x25519 = x, .ed25519 = pub, .join_sig = join, .join_ts = 1, .userinfo = userinfo, .update_sig = update, .adm_tags = tags, .adm_tags_sig = owner, .signature = tree }; const size_t sizes[] = {191, 192, 255, 256, 4096, MS_ADM_TAGS_MAX}; uint8_t page[MT_PAGE_SIZE], expected[32], actual[32]; for (size_t i = 0; i < sizeof(sizes) / sizeof(sizes[0]); i++) { const size_t profile_sizes[] = {191, 255, 256, 512, 1023, MS_USERINFO_MAX}; int head = snprintf(userinfo, sizeof(userinfo), "{\"name\":\"avatar\",\"pad\":\""); size_t end = profile_sizes[i] - 2; memset(userinfo + head, 'p', end - head); memcpy(userinfo + end, "\"}", 3); rec.update_ts = i + 1; n = member_sync_build_update_msg(join, rec.update_ts, userinfo, msg, sizeof(msg)); assert(n > 0); assert(sc_ed25519_sign(priv, msg, n, update) == SC_OK); size_t len = sizes[i]; int prefix = snprintf(tags, sizeof(tags), "{\"ver\":\"%zu\",\"pad\":\"", i + 1); const char* suffix = "\",\"storage\":\"yes\"}"; size_t tail = len - strlen(suffix); memset(tags + prefix, 'a', tail - (size_t)prefix); memcpy(tags + tail, suffix, strlen(suffix) + 1); n = member_sync_build_owner_msg(nid, tags, msg, sizeof(msg)); assert(n == (int)len + 8); assert(sc_ed25519_sign(owner_priv, msg, n, owner) == SC_OK); assert(member_sync_apply_record(&source, "1001", source.node_id, &rec) & MS_APPLY_CHANGED); assert(member_sync_verify_local_record(source.topo_sqlite_db, "1001", &rec) == 1); size_t bytes = sizeof(page); merkle_key_t next, after = {{0}}; int more; assert(_member_get_page(&source, "1001", merkle_sync_level_prefix(nid, 5), 0, &after, page, &bytes, &next, &more) == 0); assert(member_read64(next.bytes) == nid && !more && bytes < MT_PAGE_SIZE); struct member_wire w; size_t used; assert(member_decode(page + 2, bytes - 2, &w, &used) == 0 && used == bytes - 2 && !strcmp(w.rec.adm_tags, tags)); assert(!strcmp(w.rec.userinfo, userinfo)); assert(_member_apply_items(&target, "1001", 2, page, bytes) == 0); root(&source, expected); root(&target, actual); assert(!memcmp(expected, actual, 32)); sqlite3_stmt* st; assert(sqlite3_prepare_v2(target.topo_sqlite_db, "SELECT adm_tags,storage FROM peers_1001", -1, &st, NULL) == SQLITE_OK); assert(sqlite3_step(st) == SQLITE_ROW && sqlite3_column_bytes(st, 0) == (int)len); assert(!strcmp((const char*)sqlite3_column_text(st, 0), tags)); char storage[8]; assert(json_flat_get((const char*)sqlite3_column_text(st, 0), "storage", storage, sizeof(storage)) == 0); assert(!strcmp(storage, "yes")); sqlite3_finalize(st); size_t flip = tail - 1; tags[flip] = 'b'; assert(member_sync_apply_record(&target, "1001", 2, &rec) < 0); assert(member_sync_apply_record(&source, "1001", source.node_id, &rec) < 0); assert(member_sync_verify_local_record(target.topo_sqlite_db, "1001", &rec) == 0); root(&target, actual); assert(!memcmp(expected, actual, 32)); assert(sqlite3_exec(target.topo_sqlite_db, "SAVEPOINT hash_tail", NULL, NULL, NULL) == SQLITE_OK); assert(topo_node_sqlite_member_owner_put(target.topo_sqlite_db, "1001", nid, tags, owner, 0) == 0); assert(merkle_sync_recompute_path(&target, MT_DATA_MEMBERS, "1001", nid) == 1); root(&target, actual); assert(memcmp(expected, actual, 32)); assert(sqlite3_exec(target.topo_sqlite_db, "ROLLBACK TO hash_tail; RELEASE hash_tail", NULL, NULL, NULL) == SQLITE_OK); tags[flip] = 'a'; assert(_member_apply_items(&target, "1001", 2, page, bytes - 1) < 0); size_t length_offset = (size_t)(w.rec.adm_tags_sig - page) - len - 2; page[length_offset] = 0xff; page[length_offset + 1] = 0xff; assert(_member_apply_items(&target, "1001", 2, page, bytes) < 0); } puts("PASS: full owner signatures and wire roundtrip at 191/192/255/256/4096/8192 bytes; suffix tampering rejected"); struct chat_core_ctx cc = { .inst = &source, .db = source.topo_sqlite_db, .initialized = 1 }; source.chat_core = &cc; char listing[MT_PAGE_SIZE]; size_t listed; assert(chat_core_get_members_json(&source, "1001", listing, sizeof(listing), &listed) == 0 && listed > MS_ADM_TAGS_MAX); struct chat_member_tags* edit = chat_member_tags_load(&source, "1001", nid); assert(edit); const char* pad = chat_member_tags_get(edit, "pad"); assert(pad && strlen(pad) > 4096); assert(chat_member_tags_set(edit, "storage", "no") == 0); assert(chat_member_tags_commit(&source, edit) == 0); chat_member_tags_free(edit); edit = chat_member_tags_load(&source, "1001", nid); assert(edit); pad = chat_member_tags_get(edit, "pad"); assert(pad && strlen(pad) > 4096); assert(!strcmp(chat_member_tags_get(edit, "storage"), "no")); root(&source, expected); assert(sqlite3_exec(source.topo_sqlite_db, "CREATE TRIGGER reject_owner BEFORE UPDATE OF adm_tags ON peers_1001 " "BEGIN SELECT RAISE(ABORT,'injected owner failure'); END", NULL, NULL, NULL) == SQLITE_OK); assert(chat_member_tags_set(edit, "storage", "yes") == 0); assert(chat_member_tags_commit(&source, edit) < 0); root(&source, actual); assert(!memcmp(expected, actual, 32)); assert(sqlite3_exec(source.topo_sqlite_db, "DROP TRIGGER reject_owner", NULL, NULL, NULL) == SQLITE_OK); memset(tags, 'x', MS_ADM_TAGS_MAX + 1); tags[MS_ADM_TAGS_MAX + 1] = 0; assert(member_sync_build_owner_msg(nid, tags, msg, sizeof(msg)) < 0); assert(chat_member_tags_set(edit, "pad", tags) < 0); tags[MS_ADM_TAGS_MAX] = 0; assert(chat_member_tags_set(edit, "pad", tags) == 0 && chat_member_tags_commit(&source, edit) < 0); root(&source, actual); assert(!memcmp(expected, actual, 32)); assert(chat_member_tags_set(edit, "pad", "quoted\"\\value\n\r\t\b\f") == 0 && chat_member_tags_commit(&source, edit) == 0); chat_member_tags_free(edit); edit = chat_member_tags_load(&source, "1001", nid); assert(edit); assert(!strcmp(chat_member_tags_get(edit, "pad"), "quoted\"\\value\n\r\t\b\f")); chat_member_tags_free(edit); char small[8]; size_t output; assert(chat_core_get_members_json(&source, "1001", small, sizeof(small), &output) < 0 && output == 0); source.chat_core = NULL; puts("PASS: long JSON edit/reload; escaping; size and SQLite errors preserve data and tree"); 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); } int main(void) { debug_config_init(); debug_set_level(DEBUG_LEVEL_ERROR); callback_lifetime(); size_t baseline = u_get_allocated_count(); struct UASYNC* ua = uasync_create(); assert(ua); if (test_snapshot_event_bounds(ua) < 0) { uasync_destroy(ua, 0); return 1; } test_member_purge(ua); test_invitation_certificate(ua); test_owner_sizes(ua); struct UTUN_INSTANCE source, target; setup(&source, ua); setup(&target, ua); put(&source, 1); put(&source, 2); uint8_t raw[2049], *page = raw + 1; /* проверяем невыровненный буфер */ size_t len = 300; merkle_key_t next, after = {{0}}; int more; assert(_member_get_page(&source, "1001", 0, 0, &after, page, &len, &next, &more) == 0); assert(page[0] == 0 && page[1] == 1 && member_read64(next.bytes) == 1 && more == 1 && len <= 300); struct member_wire w; size_t used; assert(member_decode(page + 2, len - 2, &w, &used) == 0 && w.rec.node_id == 1 && used == len - 2); len = 300; assert(_member_get_page(&source, "1001", 0, 1, &next, page, &len, &next, &more) == 0 && member_read64(next.bytes) == 2 && !more); puts("PASS: bounded member pages, canonical integers and unaligned buffers"); len = 2048; assert(_member_get_page(&source, "1001", 0, 0, &after, page, &len, &next, &more) == 0 && page[1] == 2 && !more); assert(_member_apply_items(&target, "1001", 1, page, len - 1) < 0 && member_sync_count(&target, "1001") == 0); sqlite3* db = target.topo_sqlite_db; assert(sqlite3_exec(db, "CREATE TRIGGER fail_second BEFORE INSERT ON peers_1001 WHEN NEW.node_id=2 " "BEGIN SELECT RAISE(ABORT,'injected second record failure'); END", NULL, NULL, NULL) == SQLITE_OK); assert(_member_apply_items(&target, "1001", 1, page, len) < 0); uint8_t hash[32], empty[32] = {0}; root(&target, hash); assert(member_sync_count(&target, "1001") == 0 && !memcmp(hash, empty, 32) && sqlite3_get_autocommit(db)); assert(sqlite3_exec(db, "DROP TRIGGER fail_second", NULL, NULL, NULL) == SQLITE_OK); assert(_member_apply_items(&target, "1001", 1, page, len) == 0 && member_sync_count(&target, "1001") == 2); uint8_t expected[32]; root(&source, expected); root(&target, hash); assert(!memcmp(hash, expected, 32)); puts("PASS: malformed page rejected; second-record error rolls back data and tree; complete page converges"); assert(sqlite3_exec(db, "UPDATE peers_1001 SET x25519_pubkey=X'01' WHERE node_id=1", NULL, NULL, NULL) == SQLITE_OK); assert(merkle_sync_recompute_path(&target, MT_DATA_MEMBERS, "1001", 1) < 0); root(&target, hash); assert(!memcmp(hash, expected, 32)); len = 2048; assert(_member_get_page(&target, "1001", 0, 0, &after, page, &len, &next, &more) < 0); puts("PASS: corrupt BLOB cannot be hashed or serialized"); member_sync_destroy(&target); member_sync_destroy(&source); assert(sqlite3_close(target.topo_sqlite_db) == SQLITE_OK && sqlite3_close(source.topo_sqlite_db) == SQLITE_OK); uasync_poll(ua, 0); uasync_destroy(ua, 0); assert(u_get_allocated_count() == baseline); puts("ALL PASS: member adapter"); return 0; }