From 5dab86e84047da5ff0ce3b8bd4d6f377d9596b01 Mon Sep 17 00:00:00 2001 From: evgeny Date: Mon, 26 Oct 2026 21:48:02 +0100 Subject: [PATCH] Initialize chat schema before storing and publishing local identity --- src/chat/chat_core.c | 64 +++++++++++--------- tools/chatgui/tests/test_channel_replies.cpp | 38 ++++++++++++ 2 files changed, 74 insertions(+), 28 deletions(-) diff --git a/src/chat/chat_core.c b/src/chat/chat_core.c index db04d6e7..2d01fd63 100644 --- a/src/chat/chat_core.c +++ b/src/chat/chat_core.c @@ -120,33 +120,8 @@ int chat_core_init(struct UTUN_INSTANCE* inst, const char* db_path) { sqlite3_int64 now_sec = (sqlite3_int64)ntp_time_get_seconds(inst); - /* записать себя в nodes + local_identity */ - { - const char* my_name = inst->name[0] ? inst->name : "Me"; - topo_node_sqlite_node_update_verified(cc->db, cc->my_node_id, - my_name, inst->my_keys.public_key, inst->my_ed25519_pubkey, - (uint64_t)now_sec, (time_t)now_sec); - sqlite3_stmt* st = NULL; - sqlite3_prepare_v2(cc->db, - "INSERT OR REPLACE INTO local_identity(id,node_id,name,x25519_pubkey,ed25519_pubkey,created_at,updated_at)" - " VALUES(1,?,?,?,?,?,?)", -1, &st, NULL); - if (st) { - sqlite3_bind_int64(st, 1, (sqlite3_int64)cc->my_node_id); - sqlite3_bind_text(st, 2, my_name, -1, SQLITE_STATIC); - sqlite3_bind_blob(st, 3, inst->my_keys.public_key, 32, SQLITE_STATIC); - sqlite3_bind_blob(st, 4, inst->my_ed25519_pubkey, 32, SQLITE_STATIC); - sqlite3_bind_int64(st, 5, now_sec); - sqlite3_bind_int64(st, 6, now_sec); - sqlite3_step(st); sqlite3_finalize(st); - } - } - - cc->initialized = 1; - - /* register adm_tags change callback → CHAT_EVT_MEMBER_UPDATED */ - chat_member_init(inst); - - db_exec(cc, + /* Схема нужна до первой записи идентичности, в том числе для новой пустой БД. */ + if (db_exec(cc, "CREATE TABLE IF NOT EXISTS local_identity (" " id INTEGER PRIMARY KEY CHECK (id = 1)," " node_id INTEGER NOT NULL UNIQUE," @@ -171,7 +146,37 @@ int chat_core_init(struct UTUN_INSTANCE* inst, const char* db_path) { " key TEXT PRIMARY KEY," " value TEXT" ");" - ); + ) != SQLITE_OK) goto fail; + + /* Идентичность публикуется в GUI только после успешной записи узла и локального профиля. */ + { + const char* my_name = inst->name[0] ? inst->name : "Me"; + if (topo_node_sqlite_node_update_verified(cc->db, cc->my_node_id, + my_name, inst->my_keys.public_key, inst->my_ed25519_pubkey, (uint64_t)now_sec, (time_t)now_sec) < 0) { + DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: local node registration failed", CC_ID); goto fail; + } + sqlite3_stmt* st = NULL; + int rc = sqlite3_prepare_v2(cc->db, + "INSERT OR REPLACE INTO local_identity(id,node_id,name,x25519_pubkey,ed25519_pubkey,created_at,updated_at)" + " VALUES(1,?,?,?,?,?,?)", -1, &st, NULL); + if (rc == SQLITE_OK) rc = sqlite3_bind_int64(st, 1, (sqlite3_int64)cc->my_node_id); + if (rc == SQLITE_OK) rc = sqlite3_bind_text(st, 2, my_name, -1, SQLITE_STATIC); + if (rc == SQLITE_OK) rc = sqlite3_bind_blob(st, 3, inst->my_keys.public_key, 32, SQLITE_STATIC); + if (rc == SQLITE_OK) rc = sqlite3_bind_blob(st, 4, inst->my_ed25519_pubkey, 32, SQLITE_STATIC); + if (rc == SQLITE_OK) rc = sqlite3_bind_int64(st, 5, now_sec); + if (rc == SQLITE_OK) rc = sqlite3_bind_int64(st, 6, now_sec); + if (rc == SQLITE_OK) rc = sqlite3_step(st); + if (rc != SQLITE_DONE) DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: local identity write rc=%d: %s", CC_ID, rc, sqlite3_errmsg(cc->db)); + int final = sqlite3_finalize(st); + if (final != SQLITE_OK) DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: local identity finalize rc=%d: %s", CC_ID, final, sqlite3_errmsg(cc->db)); + if (rc != SQLITE_DONE || final != SQLITE_OK) goto fail; + DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: local identity stored node=%016llx", CC_ID, (unsigned long long)cc->my_node_id); + } + + cc->initialized = 1; + /* register adm_tags change callback → CHAT_EVT_MEMBER_UPDATED */ + chat_member_init(inst); + /* store media_base for block_req handler */ { char mbase[MEDIA_BASE_MAX]; @@ -248,6 +253,9 @@ int chat_core_init(struct UTUN_INSTANCE* inst, const char* db_path) { chat_event_post(inst, CHAT_EVT_DB_READY, NULL, 0); return 0; +fail: + if (!cc->shared_db) sqlite3_close(cc->db); + inst->chat_core = NULL; u_free(cc); return -1; } void chat_core_destroy(struct UTUN_INSTANCE* inst) { diff --git a/tools/chatgui/tests/test_channel_replies.cpp b/tools/chatgui/tests/test_channel_replies.cpp index 79a191d1..c8e149bb 100644 --- a/tools/chatgui/tests/test_channel_replies.cpp +++ b/tools/chatgui/tests/test_channel_replies.cpp @@ -43,6 +43,7 @@ class ChannelRepliesTest : public QObject { private slots: void initTestCase() { debug_config_init(); debug_set_level(DEBUG_LEVEL_WARN); + debug_set_category_level(DEBUG_CATEGORY_DEBUG, DEBUG_LEVEL_INFO); QVERIFY(directory.isValid()); ua = uasync_create(); QVERIFY(ua); const QByteArray config = "[global]\ndb_path=" + directory.path().toUtf8() + "\n[chatserver]\ngroup_autoconnect=0\nstorage_autoload=0\n"; @@ -56,6 +57,18 @@ private slots: } void signedReferenceAndRestart() { + sqlite3_stmt* identity = nullptr; + QCOMPARE(sqlite3_prepare_v2(inst->topo_sqlite_db, "SELECT node_id,x25519_pubkey,ed25519_pubkey FROM local_identity WHERE id=1", + -1, &identity, nullptr), SQLITE_OK); + const int identityRc = sqlite3_step(identity); + const auto local = quint64(sqlite3_column_int64(identity, 0)); + const auto x25519 = DbManager::colBlob(identity, 1), ed25519 = DbManager::colBlob(identity, 2); + sqlite3_finalize(identity); + DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "first chat startup: local_identity rc=%d node=%016llx expected=%016llx x25519=%d ed25519=%d", + identityRc, (unsigned long long)local, (unsigned long long)inst->node_id, int(x25519.size()), int(ed25519.size())); + QCOMPARE(identityRc, SQLITE_ROW); QCOMPARE(local, quint64(inst->node_id)); + QCOMPARE(x25519, QByteArray(reinterpret_cast(inst->my_keys.public_key), 32)); + QCOMPARE(ed25519, QByteArray(reinterpret_cast(inst->my_ed25519_pubkey), 32)); const auto reply = send("Answer", original); QCOMPARE(reply.size(), 32); QCOMPARE(db.getMessageByKey(channel, reply).replyTo, original); const auto sql = QString("SELECT timestamp,data,author_signature FROM msg_%1 WHERE message_id=?").arg(channel).toUtf8(); @@ -67,6 +80,21 @@ private slots: QByteArray body(reinterpret_cast(&ts), 8); body += DbManager::colBlob(statement, 1); QByteArray signature = DbManager::colBlob(statement, 2); sqlite3_finalize(statement); QCOMPARE(sc_ed25519_verify(inst->my_ed25519_pubkey, (uint8_t*)body.data(), body.size(), (uint8_t*)signature.data()), SC_OK); + const auto byKey = db.getMessageByKey(channel, reply); + const auto latest = db.getMessages(channel, 1); QCOMPARE(latest.size(), 1); + const auto since = db.getMessagesSince(channel, byKey.id - 1); QCOMPARE(since.size(), 1); + const auto byId = db.getMessageById(channel, byKey.id); + const MessageRow readers[] = {byKey, byId, latest.first(), since.first()}; + const char* names[] = {"key", "id", "latest", "since"}; + for (unsigned i = 0; i < 4; i++) { + const auto& row = readers[i]; + DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "history reader=%s author=%016llx local=%016llx outgoing=%d signature=%d id=%d attrs=%d", + names[i], (unsigned long long)row.authorNodeId, (unsigned long long)db.myNodeId(), row.isOutgoing, + int(row.signature.size()), int(row.messageId.size()), int(row.localAttrs.size())); + QCOMPARE(row.messageId, reply); QCOMPARE(row.replyTo, original); QCOMPARE(row.signature, signature); + QCOMPARE(row.timestamp, qint64(ts)); QCOMPARE(row.data, QByteArray("Answer")); + QVERIFY(row.isOutgoing); QVERIFY(row.localAttrs.isEmpty()); + } const int reference = body.indexOf(original.toHex()); QVERIFY(reference >= 0); body[reference] = body[reference] == 'a' ? 'b' : 'a'; QVERIFY(sc_ed25519_verify(inst->my_ed25519_pubkey, (uint8_t*)body.data(), body.size(), (uint8_t*)signature.data()) != SC_OK); chat_service_stop(inst); QCOMPARE(chat_service_start(inst), 0); @@ -143,6 +171,16 @@ private slots: void cleanupTestCase() { db.setDb(nullptr); if (inst) utun_instance_destroy(inst); if (ua) uasync_destroy(ua, 0); } + + void identityWriteFailureRejectsStartup() { + chat_service_stop(inst); + QCOMPARE(sqlite3_exec(inst->topo_sqlite_db, "CREATE TEMP TRIGGER reject_identity BEFORE INSERT ON local_identity" + " BEGIN SELECT RAISE(ABORT,'injected local identity failure'); END", nullptr, nullptr, nullptr), SQLITE_OK); + QVERIFY(chat_service_start(inst) < 0); + QVERIFY(!inst->chat_started); QVERIFY(!inst->chat_core); + QCOMPARE(sqlite3_exec(inst->topo_sqlite_db, "DROP TRIGGER reject_identity", nullptr, nullptr, nullptr), SQLITE_OK); + QCOMPARE(chat_service_start(inst), 0); QVERIFY(inst->chat_started); + } }; QTEST_MAIN(ChannelRepliesTest)