From c3ac2059f4162abc1bb4379966fd6934363850ec Mon Sep 17 00:00:00 2001 From: evgeny Date: Tue, 27 Oct 2026 01:55:03 +0100 Subject: [PATCH] Check complete Merkle cleanup before committing channel deletion --- src/routing_layer/topo_node_sqlite.c | 7 +++++++ tests/test_chat_attachment.c | 19 +++++++++++-------- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/src/routing_layer/topo_node_sqlite.c b/src/routing_layer/topo_node_sqlite.c index 976efdcd..15665bd4 100644 --- a/src/routing_layer/topo_node_sqlite.c +++ b/src/routing_layer/topo_node_sqlite.c @@ -646,6 +646,13 @@ int topo_node_sqlite_channel_delete(sqlite3* db, const char* channel_id) { if (rc != SQLITE_DONE) goto fail; rc = sqlite3_finalize(st); st = NULL; if (rc != SQLITE_OK) goto fail; + rc = sqlite3_prepare_v2(db, "SELECT 1 FROM merkle_tree_hash WHERE substr(namespace,instr(namespace,':')+1)=? LIMIT 1", + -1, &st, NULL); + if (rc == SQLITE_OK) rc = sqlite3_bind_text(st, 1, channel_id, -1, SQLITE_STATIC); + if (rc == SQLITE_OK) rc = sqlite3_step(st); + if (rc != SQLITE_DONE) goto fail; + rc = sqlite3_finalize(st); st = NULL; + if (rc != SQLITE_OK) goto fail; } if ((rc = sqlite3_exec(db, "RELEASE delete_topo_channel", NULL, NULL, NULL)) != SQLITE_OK) goto fail; DEBUG_INFO(DEBUG_CATEGORY_BGP, "channel deleted: channel=%s tables=peers/msg/feedback merkle=%d", channel_id, has_merkle); diff --git a/tests/test_chat_attachment.c b/tests/test_chat_attachment.c index 4f651256..eedf2482 100644 --- a/tests/test_chat_attachment.c +++ b/tests/test_chat_attachment.c @@ -531,10 +531,11 @@ static void channel_state(struct fixture* f, int deleted) { sqlite3_stmt* st = NULL; assert(sqlite3_prepare_v2(f->reader, "SELECT EXISTS(SELECT 1 FROM channels WHERE channel_id=?1)," "EXISTS(SELECT 1 FROM attachment_keys WHERE channel=?1),EXISTS(SELECT 1 FROM attachment_key_requests WHERE channel=?1)," - "EXISTS(SELECT 1 FROM attachment_key_outbox WHERE channel=?1),EXISTS(SELECT 1 FROM sqlite_master WHERE name='msg_'||?1)", + "EXISTS(SELECT 1 FROM attachment_key_outbox WHERE channel=?1),EXISTS(SELECT 1 FROM sqlite_master WHERE name='msg_'||?1)," + "EXISTS(SELECT 1 FROM merkle_tree_hash WHERE namespace='audit:'||?1)", -1, &st, NULL) == SQLITE_OK); assert(sqlite3_bind_text(st, 1, f->channel, -1, SQLITE_STATIC) == SQLITE_OK && sqlite3_step(st) == SQLITE_ROW); - for (int i = 0; i < 5; i++) { + for (int i = 0; i < 6; i++) { if (sqlite3_column_int(st, i) != !deleted) DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "delete test: reader column=%d exists=%d expected=%d", i, sqlite3_column_int(st, i), !deleted); assert(sqlite3_column_int(st, i) == !deleted); @@ -556,7 +557,9 @@ static void channel_seed(struct fixture* f) { snprintf(query, sizeof(query), "INSERT INTO attachment_key_requests(channel,message_id,peer,incoming,state,created_at)" " SELECT channel,message_id,99,1,2,123 FROM attachment_keys WHERE channel='%s';" " INSERT INTO attachment_key_outbox(channel,message_id,peer,command,retry_at)" - " SELECT channel,message_id,99,3,17 FROM attachment_keys WHERE channel='%s'", f->channel, f->channel); + " SELECT channel,message_id,99,3,17 FROM attachment_keys WHERE channel='%s';" + " INSERT INTO merkle_tree_hash(namespace,level,prefix64,hash) VALUES('audit:%s',0,0,zeroblob(32))", + f->channel, f->channel, f->channel); sql(f->inst->topo_sqlite_db, query); channel_state(f, 0); } @@ -567,11 +570,11 @@ static int veto_channel(void* arg) { } static void test_channel_delete_rollback(void) { - const char* tables[] = {"channels", "attachment_keys", "attachment_key_requests", "attachment_key_outbox"}; - for (unsigned table = 0; table < 4; table++) for (int ignored = 0; ignored < 2; ignored++) { + const char* tables[] = {"channels", "attachment_keys", "attachment_key_requests", "attachment_key_outbox", "merkle_tree_hash"}; + for (unsigned table = 0; table < 5; table++) for (int ignored = 0; ignored < 2; ignored++) { struct fixture f; setup(&f); channel_seed(&f); char path[512], trigger[384]; plaintext_path(&f, path); - snprintf(trigger, sizeof(trigger), "CREATE TEMP TRIGGER reject_channel BEFORE DELETE ON %s BEGIN SELECT RAISE(%s); END", - tables[table], ignored ? "IGNORE" : "ABORT,'channel'"); + snprintf(trigger, sizeof(trigger), "CREATE TEMP TRIGGER reject_channel BEFORE DELETE ON %s%s BEGIN SELECT RAISE(%s); END", + tables[table], table == 4 ? " WHEN OLD.namespace LIKE 'audit:%'" : "", ignored ? "IGNORE" : "ABORT,'channel'"); sql(f.inst->topo_sqlite_db, trigger); chat_core_delete_channel(f.inst, f.channel); assert(!f.deleted_events && rows(&f) == f.rows && ma_file_size(path) == sizeof(content)); committed(&f, 1); channel_state(&f, 0); sql(f.inst->topo_sqlite_db, "DROP TRIGGER reject_channel"); chat_core_delete_channel(f.inst, f.channel); @@ -601,7 +604,7 @@ static void test_channel_delete_rollback(void) { channel_state(&f, 0); decryption_state(&f, 1, 0, 0); assert(ma_file_size(path) == sizeof(content)); assert(sqlite3_close(f.reader) == SQLITE_OK); uasync_destroy(f.ua, 0); active = NULL; channel_transaction = 0; chat_core_delete_channel(NULL, "1"); - DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "PASS channel delete:12 SQL faults, cascade ABORT/IGNORE, commit veto and foreign transaction preserve key/files; reader in event sees all deleted"); + DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "PASS channel delete:14 SQL faults, cascade/Merkle ABORT/IGNORE, veto/foreign transaction/rollback fault; key/files preserved until committed event"); } int main(void) {