From 557c98aeae8116a4fb78b2ff3e57c8bd19fa92a0 Mon Sep 17 00:00:00 2001 From: evgeny Date: Mon, 26 Oct 2026 15:45:05 +0100 Subject: [PATCH] Cover history preservation, corruption cleanup and exact delivery ACKs --- tests/Makefile.am | 8 ++ tests/test_db_sync_integrity.c | 178 +++++++++++++++++++++++++++++++++ 2 files changed, 186 insertions(+) create mode 100644 tests/test_db_sync_integrity.c diff --git a/tests/Makefile.am b/tests/Makefile.am index d4b7391a..514a8497 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -113,6 +113,7 @@ check_PROGRAMS += \ test_mem \ test_config_updater \ test_stun_nat \ + test_db_sync_integrity \ test_reality_fallback \ test_etcp_padding \ test_broadcast_cache \ @@ -842,6 +843,13 @@ test_db_sync_schema_SOURCES = test_db_sync_schema.c test_db_sync_schema_CFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/lib test_db_sync_schema_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS) +if OS_LINUX +test_db_sync_integrity_SOURCES = test_db_sync_integrity.c +test_db_sync_integrity_CFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/lib +test_db_sync_integrity_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS) +test_db_sync_integrity_LDFLAGS = -Wl,--wrap=EVP_DigestVerifyInit -Wl,--wrap=sqlite3_exec -Wl,--wrap=uasync_set_timeout +endif + test_merkle_sync_SOURCES = test_merkle_sync.c $(test_clock_sources) test_chat_feedback_SOURCES = test_chat_feedback.c test_chat_feedback_CFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/src/chat -I$(top_srcdir)/lib diff --git a/tests/test_db_sync_integrity.c b/tests/test_db_sync_integrity.c new file mode 100644 index 00000000..27d27556 --- /dev/null +++ b/tests/test_db_sync_integrity.c @@ -0,0 +1,178 @@ +/* Собственная SQLite и настоящие подписи: сохранность истории, очистка порчи, + * транзакционные отказы, восстановление цепочки и точная идентичность ACK. */ +#include "../src/chat/db_sync.c" +#include + +static int fail_verify, fail_commit, fail_timer; +static unsigned fault_hits; +extern int __real_EVP_DigestVerifyInit(EVP_MD_CTX*, EVP_PKEY_CTX**, const EVP_MD*, ENGINE*, EVP_PKEY*); +int __wrap_EVP_DigestVerifyInit(EVP_MD_CTX* ctx, EVP_PKEY_CTX** pctx, const EVP_MD* md, ENGINE* engine, EVP_PKEY* key) { + if (fail_verify && !--fail_verify) { fault_hits++; return 0; } + return __real_EVP_DigestVerifyInit(ctx, pctx, md, engine, key); +} + +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** err) { + if (fail_commit && !strcmp(sql, "COMMIT")) { fail_commit = 0; fault_hits++; return SQLITE_BUSY; } + return __real_sqlite3_exec(db, sql, cb, arg, err); +} + +extern void* __real_uasync_set_timeout(struct UASYNC*, uint64_t, void*, void (*)(void*), const char*); +void* __wrap_uasync_set_timeout(struct UASYNC* ua, uint64_t delay, void* arg, void (*cb)(void*), const char* name) { + if (fail_timer && !strcmp(name, "db_recalc")) { fail_timer = 0; fault_hits++; return NULL; } + return __real_uasync_set_timeout(ua, delay, arg, cb, name); +} + +struct fixture { + struct UTUN_INSTANCE inst; + struct DB_SYNC db; + struct DB_SYNC_INSTANCE si, other; + uint8_t seed[32]; +}; + +static void sql(struct fixture* f, const char* text) { + assert(sqlite3_exec(f->db.db, text, NULL, NULL, NULL) == SQLITE_OK); +} + +static void init(struct fixture* f) { + memset(f, 0, sizeof(*f)); + f->inst.node_id = 1; f->inst.ua = uasync_create(); assert(f->inst.ua); + f->inst.connections = queue_new(f->inst.ua, 8, 0, 8, "history test connections"); assert(f->inst.connections); + memset(f->seed, 9, sizeof(f->seed)); + EVP_PKEY* key = EVP_PKEY_new_raw_private_key(EVP_PKEY_ED25519, NULL, f->seed, sizeof(f->seed)); assert(key); + size_t len = 32; + assert(EVP_PKEY_get_raw_public_key(key, f->inst.my_ed25519_pubkey, &len) == 1 && len == 32); EVP_PKEY_free(key); + assert(sqlite3_open(":memory:", &f->db.db) == SQLITE_OK); + f->db.inst = &f->inst; f->inst.db_sync = &f->db; + f->si.db_sync = &f->db; f->si.enabled = 1; f->si.group_id = 10; f->si.next_id = 1; f->si.sync_gated = 1; + strcpy(f->si.table_name, "history"); assert(si_table_create(&f->si, "history", 0) == 0); + f->other = f->si; f->other.group_id = 11; strcpy(f->other.table_name, "other_history"); + assert(si_table_create(&f->other, "other_history", 0) == 0); +} + +static void destroy(struct fixture* f) { + if (f->si.recalc.timer) uasync_cancel_timeout(f->inst.ua, f->si.recalc.timer); + if (f->other.recalc.timer) uasync_cancel_timeout(f->inst.ua, f->other.recalc.timer); + u_free(f->si.peers); u_free(f->other.peers); + queue_free(f->inst.connections); + assert(sqlite3_get_autocommit(f->db.db) && sqlite3_close(f->db.db) == SQLITE_OK); + uasync_destroy(f->inst.ua, 0); +} + +static void sign(struct fixture* f, uint64_t ts, const char* text, uint8_t sig[64]) { + uint8_t body[DB_SYNC_MESSAGE_MAX + 8]; size_t len = strlen(text); assert(len <= DB_SYNC_MESSAGE_MAX); + memcpy(body, &ts, 8); memcpy(body + 8, text, len); + assert(sc_ed25519_sign(f->seed, body, len + 8, sig) == SC_OK); +} + +static int insert(struct fixture* f, uint64_t ts, const char* text, const uint8_t sig[64]) { + return db_sync_insert_signed(&f->si, text, strlen(text), sig, 64, ts, NULL); +} + +static void intact(struct fixture* f, const char* text) { + sqlite3_stmt* stmt = NULL; + assert(sqlite3_prepare_v2(f->db.db, "SELECT data FROM history", -1, &stmt, NULL) == SQLITE_OK); + assert(sqlite3_step(stmt) == SQLITE_ROW); + assert(sqlite3_column_bytes(stmt, 0) == (int)strlen(text)); + assert(!memcmp(sqlite3_column_blob(stmt, 0), text, strlen(text))); + assert(sqlite3_step(stmt) == SQLITE_DONE && sqlite3_finalize(stmt) == SQLITE_OK); + assert(sqlite3_get_autocommit(f->db.db)); +} + +static void test_preservation(void) { + struct fixture f; init(&f); uint8_t sig[64]; sign(&f, 100, "original", sig); + assert(insert(&f, 100, "original", sig) == 0 && insert(&f, 100, "original", sig) == 1); + fail_verify = 1; assert(insert(&f, 100, "original", sig) == -1); intact(&f, "original"); + fail_verify = 2; assert(insert(&f, 100, "original", sig) == -1); intact(&f, "original"); + assert(insert(&f, 100, "modified", sig) == -2); intact(&f, "original"); + sql(&f, "UPDATE history SET node_id=99"); + assert(insert(&f, 100, "original", sig) == -1); intact(&f, "original"); + assert(si_send_data_batch(&f.si, 2, 0, 1, 0, DB_WANT_FROM_NONE) == -1); intact(&f, "original"); + sql(&f, "UPDATE history SET node_id=1"); + fail_verify = 1; + assert(si_send_data_batch(&f.si, 2, 0, 1, 0, DB_WANT_FROM_NONE) == -1); intact(&f, "original"); + assert(db_sync_chain_verify(&f.si) == 0); + destroy(&f); + DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "PASS history operational failure, missing key and invalid duplicate preserve stored bytes"); +} + +static void test_deletion(void) { + struct fixture f; init(&f); uint8_t sig[64]; sign(&f, 100, "original", sig); + assert(insert(&f, 100, "original", sig) == 0); + assert(db_sync_insert_signed(&f.other, "original", 8, sig, 64, 100, NULL) == 0); + struct SI_PEER* peer = si_peer_add(&f.si, 2); assert(peer); peer->sync_state = 2; peer->synced_pos = 20; + sql(&f, "UPDATE history SET data='modified'"); + sql(&f, "CREATE TRIGGER deny_delete BEFORE DELETE ON history BEGIN SELECT RAISE(FAIL,'delete denied'); END"); + assert(insert(&f, 100, "modified", sig) == -1); intact(&f, "modified"); + assert(peer->sync_state == 2 && peer->synced_pos == 20); + sql(&f, "DROP TRIGGER deny_delete"); + fail_commit = 1; assert(insert(&f, 100, "modified", sig) == -1); intact(&f, "modified"); + assert(peer->sync_state == 2 && peer->synced_pos == 20); + assert(insert(&f, 100, "modified", sig) == -2 && db_sync_count(&f.si) == 0); + assert(!peer->sync_state && !peer->synced_pos && f.si.recalc.pending); + assert(db_sync_chain_verify(&f.si) == 0 && db_sync_count(&f.other) == 1); + /* Корректная входящая копия заменяет независимо проверенную порчу. */ + assert(insert(&f, 100, "original", sig) == 0); sql(&f, "UPDATE history SET data='modified'"); + assert(insert(&f, 100, "original", sig) == 0); intact(&f, "original"); + sql(&f, "UPDATE history SET data=zeroblob(4001)"); + assert(si_send_data_batch(&f.si, 2, 0, 1, 0, DB_WANT_FROM_NONE) == -1 && db_sync_count(&f.si) == 0); + const unsigned sizes[] = {0, 1, 63, 65, 255}; + for (unsigned i = 0; i < sizeof(sizes) / sizeof(*sizes); i++) { + assert(insert(&f, 100, "original", sig) == 0); + char query[96]; snprintf(query, sizeof(query), "UPDATE history SET author_signature=zeroblob(%u)", sizes[i]); sql(&f, query); + assert(si_send_data_batch(&f.si, 2, 0, 1, 0, DB_WANT_FROM_NONE) == -1 && db_sync_count(&f.si) == 0); + assert(db_sync_chain_verify(&f.si) == 0); + } + assert(insert(&f, 100, "original", sig) == 0); + sql(&f, "UPDATE history SET author_signature=zeroblob(1)"); + assert(db_sync_chain_verify(&f.si) == 0 && db_sync_count(&f.si) == 0); /* pending recalc also cleans malformed signature */ + destroy(&f); + DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "PASS history stored corruption deleted, SQL failures rolled back, peers reset, other channel retained"); +} + +static void test_recalc(void) { + struct fixture f; init(&f); uint8_t sig[64]; + for (uint64_t ts = 1; ts <= 55; ts++) { sign(&f, ts, "record", sig); assert(insert(&f, ts, "record", sig) == 0); } + uint64_t next = f.si.next_id, hash = 0; + fail_commit = 1; + assert(db_sync_last_chain_hash8(&f.si, &hash) == -1 && f.si.recalc.pending && f.si.recalc.pos == 0); + assert(sqlite3_get_autocommit(f.db.db)); + sql(&f, "CREATE TRIGGER deny_hash BEFORE UPDATE OF chain_hash ON history BEGIN SELECT RAISE(FAIL,'hash denied'); END"); + assert(db_sync_last_chain_hash8(&f.si, &hash) == -1 && f.si.recalc.pending && f.si.recalc.pos == 0); + sql(&f, "DROP TRIGGER deny_hash"); + assert(db_sync_last_chain_hash8(&f.si, &hash) == 0 && hash && !f.si.recalc.pending && db_sync_chain_verify(&f.si) == 0); + sign(&f, 56, "record", sig); fail_commit = 1; + assert(insert(&f, 56, "record", sig) == -1 && db_sync_count(&f.si) == 55 && f.si.next_id == next); + fail_timer = 1; assert(insert(&f, 56, "record", sig) == 0 && f.si.recalc.pending && !f.si.recalc.timer); + assert(db_sync_chain_verify(&f.si) == 0 && !f.si.recalc.pending); + destroy(&f); + DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "PASS history recalc retries commit/update/timer failures across multiple batches"); +} + +static void test_delivery(void) { + struct fixture f; init(&f); uint8_t a[64], b[64], id_a[32], id_b[32]; + sign(&f, 100, "first", a); sign(&f, 100, "second", b); + assert(insert(&f, 100, "first", a) == 0 && insert(&f, 100, "second", b) == 0); + assert(chat_message_id(10, 100, a, id_a) == 0 && chat_message_id(10, 100, b, id_b) == 0); + db_handle_ack_push(&f.si, 2, id_a, 32); db_handle_ack_push(&f.si, 2, id_a, 32); + sqlite3_stmt* stmt = NULL; + assert(sqlite3_prepare_v2(f.db.db, "SELECT data,delivered_peers,flags FROM history ORDER BY data", -1, &stmt, NULL) == SQLITE_OK); + assert(sqlite3_step(stmt) == SQLITE_ROW && sqlite3_column_int(stmt, 1) == 1 && sqlite3_column_int(stmt, 2) == 1); + assert(sqlite3_step(stmt) == SQLITE_ROW && sqlite3_column_int(stmt, 1) == 0 && sqlite3_column_int(stmt, 2) == 0); + assert(sqlite3_step(stmt) == SQLITE_DONE && sqlite3_finalize(stmt) == SQLITE_OK); + sql(&f, "CREATE TRIGGER deny_ack BEFORE UPDATE ON history BEGIN SELECT RAISE(FAIL,'ACK denied'); END"); + assert(si_delivery_update(&f.si, id_b, 3) == -1); + sql(&f, "DROP TRIGGER deny_ack"); + assert(si_delivery_update(&f.si, id_b, 3) == 0); + destroy(&f); + DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "PASS history ACK identifies one same-author same-timestamp message and deduplicates confirmations"); +} + +int main(void) { + debug_config_init(); debug_set_level(DEBUG_LEVEL_WARN); debug_set_category_level(DEBUG_CATEGORY_CHAT_SYNC, DEBUG_LEVEL_DEBUG); + size_t baseline = u_get_allocated_count(); + test_preservation(); test_deletion(); test_recalc(); test_delivery(); + DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "PASS history integrity; injected failures=%u; tracked allocations restored", fault_hits); + assert(fault_hits == 7 && u_get_allocated_count() == baseline); + return 0; +}