diff --git a/doc/tasks.md b/doc/tasks.md index 4dc557bf2..0db5b25ec 100644 --- a/doc/tasks.md +++ b/doc/tasks.md @@ -4,6 +4,13 @@ ## Текущие задачи +[+] **chat: атомарное глобальное имя и подписанный снимок** — 27.10.2026. + Один commit сохраняет local_identity,nodes,NODEINFO и полные собственные профили каналов/Merkle. + Память/LAN/уведомления меняются после commit;отказ сохраняет все представления,no-op не меняет версии. + Чтение timestamp различает отсутствие/SQL,запись snapshot проверяет SQL/finalize под savepoint. + test_chat_profile:32 SQL fault/retry,crypto/veto/no-op/foreign txn,два профиля и независимый observer PASS; + standalone snapshot finalize failure также откатывается. Контракт:[chat_core.h](../src/chat/chat_core.h). + [+] **chat_whisper: per-instance модель,повтор после отказа и безопасная остановка** — 27.10.2026. Глобальный init/trigger заменён прямым API:ленивая загрузка в worker,кеш модели,одна задача и явный BUSY. Stop отсоединяет владельца;модель освобождается после join,каждый запрос получает один callback. diff --git a/src/chat/chat_core.h b/src/chat/chat_core.h index 5eafc1c29..780ade4f2 100644 --- a/src/chat/chat_core.h +++ b/src/chat/chat_core.h @@ -177,8 +177,12 @@ void chat_core_delete_channel_trampoline(void* arg); // ── Утилиты ── -// Сохранить имя узла и обновить собственные member-записи каналов. -void chat_core_update_my_name(struct UTUN_INSTANCE* inst, const char* name); +/* Имя (NUL,<=127байт,включая пустое):0 — committed/no-op,-1 — отказ без изменения runtime. + * Один BEGIN IMMEDIATE сохраняет local_identity,nodes,подписанный NODEINFO и собственные полные member-блоки/Merkle. + * Ожидающий JOIN без полного member пропускается. No-op не меняет версии и не вызывает commit hook. + * Только после commit обновляются inst/LAN и идут NODEINFO/member уведомления;внешняя транзакция запрещена. + * Неудачный rollback с открытой транзакцией запрашивает stop/закрытие БД владельцем. Конфиг не сохраняется. */ +int chat_core_update_my_name(struct UTUN_INSTANCE* inst, const char* name); struct update_my_name_arg { struct UTUN_INSTANCE* inst; char name[128]; }; void chat_core_update_my_name_trampoline(void* arg); diff --git a/src/chat/chat_profile.c b/src/chat/chat_profile.c index 5a79e83cb..1e53d9c27 100644 --- a/src/chat/chat_profile.c +++ b/src/chat/chat_profile.c @@ -26,21 +26,11 @@ #include #include -void chat_core_update_my_name(struct UTUN_INSTANCE* inst, const char* name) { - struct chat_core_ctx* cc = CC(inst); - if (!cc || !cc->initialized || !name) return; - topo_node_sqlite_node_update_verified(cc->db, cc->my_node_id, - name, inst->my_keys.public_key, inst->my_ed25519_pubkey, - (uint64_t)ntp_time_get_seconds(inst), 0); - snprintf(inst->name, sizeof(inst->name), "%s", name); - if (inst->local_transport) local_transport_set_name(inst->local_transport, inst->name); - chat_core_update_my_member(inst); -} - void chat_core_update_my_name_trampoline(void* arg) { struct update_my_name_arg* a = (struct update_my_name_arg*)arg; - if (!a) return; - chat_core_update_my_name(a->inst, a->name); + if (!a) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "name update: null request"); return; } + if (!memchr(a->name, 0, sizeof(a->name))) DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "name update: unterminated request"); + else chat_core_update_my_name(a->inst, a->name); u_free(arg); } @@ -219,20 +209,20 @@ static int profile_equal(sqlite3* db, const char* a, const char* b) { return same; } -/* Вызывается под собственной блокировкой;возвращает commit-флаги member_sync либо-1,без callbacks. */ -static int profile_update_locked(struct UTUN_INSTANCE* inst, const char* ch, int avatar, uint64_t server, const char* file, +/* Под транзакцией caller:commit-флаги member_sync,-2 — нет полного member,-1 — ошибка;без callbacks. */ +static int profile_update_locked(struct UTUN_INSTANCE* inst, const char* ch, const char* name, int avatar, uint64_t server, const char* file, const struct chat_profile_option* options, size_t option_count) { struct profile_current current = {0}; int found = profile_read_current(inst, ch, ¤t); if (found <= 0) { if (!found) DEBUG_WARN(DEBUG_CATEGORY_CHAT, "profile: own full member unavailable channel=%s", ch); - return -1; + return found ? -1 : -2; } if (avatar && server && !member_files_member(inst, strtoull(ch, NULL, 10), server, 1)) { DEBUG_WARN(DEBUG_CATEGORY_CHAT, "profile: avatar server has no storage role channel=%s", ch); return -1; } struct profile_json profile = {.text = "{", .used = 1, .avatar = avatar, .options = options, .option_count = option_count}; - if (profile_pair(&profile, "name", inst->name) < 0 || + if (profile_pair(&profile, "name", name) < 0 || (current.signed_profile && chat_json_strings(CC(inst)->db, current.json, MS_USERINFO_MAX, profile_copy_pair, &profile))) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "profile: cannot preserve/encode signed attributes channel=%s", ch); return -1; } @@ -279,7 +269,7 @@ static int profile_update_channel(struct UTUN_INSTANCE* inst, const char* channe char ch[20]; memcpy(ch, channel, strlen(channel) + 1); int rc = sqlite3_exec(cc->db, "BEGIN IMMEDIATE /* profile */", NULL, NULL, NULL); if (rc != SQLITE_OK) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "profile: begin ch=%s rc=%d: %s", ch, rc, sqlite3_errmsg(cc->db)); return -1; } - int changes = profile_update_locked(inst, ch, avatar, server, file, options, option_count); + int changes = profile_update_locked(inst, ch, inst->name, avatar, server, file, options, option_count); if (changes > 0) { rc = sqlite3_exec(cc->db, "COMMIT /* profile */", NULL, NULL, NULL); if (rc == SQLITE_OK) { @@ -300,6 +290,129 @@ static int profile_update_channel(struct UTUN_INSTANCE* inst, const char* channe return changes < 0 ? -1 : 0; } +/* Прочитать оба локальных представления;bit0/bit1 означают необходимость записи identity/nodes. */ +static int profile_name_changes(struct UTUN_INSTANCE* inst, const char* name) { + sqlite3* db = CC(inst)->db; sqlite3_stmt* st = NULL; int result = 0; + int rc = sqlite3_prepare_v2(db, "SELECT local_identity.name,nodes.name FROM local_identity" + " JOIN nodes ON nodes.node_id=local_identity.node_id WHERE id=1 AND local_identity.node_id=?", -1, &st, NULL); + if (rc == SQLITE_OK) rc = sqlite3_bind_int64(st, 1, (sqlite3_int64)inst->node_id); + if (rc == SQLITE_OK) rc = sqlite3_step(st); + if (rc == SQLITE_ROW) for (unsigned i = 0; i < 2; i++) { + int type = sqlite3_column_type(st, i), length = sqlite3_column_bytes(st, i); + const char* saved = (const char*)sqlite3_column_text(st, i); + if (type != SQLITE_TEXT || !saved || length >= (int)sizeof(inst->name) || strlen(saved) != (size_t)length) { + rc = SQLITE_CORRUPT; break; + } + if (strcmp(saved, name)) result |= 1 << i; + } + int final = sqlite3_finalize(st); + if (rc != SQLITE_ROW || final != SQLITE_OK) { + DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "name update: stored names rc=%d finalize=%d: %s", rc, final, sqlite3_errmsg(db)); return -1; + } + return result; +} + +/* Запись под транзакцией rename;changes/finalize проверяются до общего commit. */ +static int profile_name_write(struct UTUN_INSTANCE* inst, const char* sql, const char* name, sqlite3_int64 now) { + sqlite3* db = CC(inst)->db; sqlite3_stmt* st = NULL; + int rc = sqlite3_prepare_v2(db, sql, -1, &st, NULL); + if (rc == SQLITE_OK) rc = sqlite3_bind_text(st, 1, name, -1, SQLITE_STATIC); + if (rc == SQLITE_OK) rc = sqlite3_bind_int64(st, 2, now); + if (rc == SQLITE_OK) rc = sqlite3_bind_int64(st, 3, (sqlite3_int64)inst->node_id); + if (rc == SQLITE_OK) rc = sqlite3_step(st); + if (rc == SQLITE_DONE && sqlite3_changes(db) != 1) rc = SQLITE_CONSTRAINT; + int final = sqlite3_finalize(st); + if (rc != SQLITE_DONE || final != SQLITE_OK) { + DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "name update: write rc=%d finalize=%d sql=%.80s: %s", rc, final, sql, sqlite3_errmsg(db)); return -1; + } + return 0; +} + +/* Имя identity,подписанный NODEINFO и все свои полные member-блоки фиксируются одним commit. */ +int chat_core_update_my_name(struct UTUN_INSTANCE* inst, const char* name) { + struct chat_core_ctx* cc = inst ? CC(inst) : NULL; + if (!cc || !cc->initialized || !cc->db || cc->my_node_id != inst->node_id || !name || + strlen(name) >= sizeof(inst->name) || !sqlite3_get_autocommit(cc->db)) { + DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "name update: inactive context/invalid name/external transaction"); return -1; + } + char desired[sizeof(inst->name)]; memcpy(desired, name, strlen(name) + 1); + struct TOPO_NODE* node = inst->topo_groups ? topo_node_registry_find(inst->topo_groups, inst->node_id) : NULL; + if (!node || topo_node_verify(node)) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "name update: own signed node unavailable"); return -1; } + int snapshot_changed = strcmp(node->node_name ? node->node_name : "", desired) != 0; + struct TOPO_NODE candidate = *node; char* prepared_name = snapshot_changed ? u_strdup(desired) : NULL; + if (snapshot_changed && !prepared_name) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "name update: name allocation failed"); return -1; } + char** channels = NULL; int rows = 0, columns = 0, rc, committed = 0; + unsigned* changes = NULL; const char* stage = "begin"; + rc = sqlite3_exec(cc->db, "BEGIN IMMEDIATE /* name */", NULL, NULL, NULL); + if (rc != SQLITE_OK) goto done; + stage = "stored names"; int names_changed = profile_name_changes(inst, desired); + if (names_changed < 0) goto rollback; + stage = "channel catalog"; + rc = sqlite3_get_table(cc->db, "SELECT channel_id,length(CAST(channel_id AS BLOB)) FROM channels ORDER BY channel_id", + &channels, &rows, &columns, NULL); + if (rc != SQLITE_OK || (rows && columns != 2) || (size_t)rows > UINT32_MAX / sizeof(*changes)) goto rollback; + if (rows) { + changes = u_calloc((uint32_t)rows, sizeof(*changes)); + if (!changes) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "name update: channel snapshot allocation failed rows=%d", rows); goto rollback; } + } + int changed = names_changed || snapshot_changed; + stage = "member profiles"; + for (int i = 0; i < rows; i++) { + const char *channel = channels[(i + 1) * columns], *length = channels[(i + 1) * columns + 1]; + if (!chat_channel_id(channel) || !length || strlen(channel) != strtoul(length, NULL, 10)) goto rollback; + int flags = profile_update_locked(inst, channel, desired, 0, 0, NULL, NULL, 0); + if (flags == -2) continue; /* JOIN ещё не создал собственного полного member. */ + if (flags < 0) goto rollback; + changes[i] = (unsigned)flags; if (flags) changed = 1; + } + if (changed) { + sqlite3_int64 now = (sqlite3_int64)ntp_time_get_seconds(inst); + stage = "identity"; + if (now <= 0 || ((names_changed & 1) && profile_name_write(inst, + "UPDATE local_identity SET name=?1,updated_at=?2 WHERE id=1 AND node_id=?3", desired, now))) goto rollback; + stage = "node projection"; + if ((names_changed & 2) && profile_name_write(inst, "UPDATE nodes SET name=?1," + "update_ts=CASE WHEN update_ts IS NULL OR update_tsver % 255) + 1; + if (topo_node_sign_self(inst, &candidate)) goto rollback; + } + stage = "commit"; rc = sqlite3_exec(cc->db, "COMMIT /* name */", NULL, NULL, NULL); + if (rc != SQLITE_OK) goto rollback; + } else { + stage = "no-op rollback"; rc = sqlite3_exec(cc->db, "ROLLBACK", NULL, NULL, NULL); + if (rc != SQLITE_OK) goto rollback; + } + committed = 1; + if (snapshot_changed) { char* old = node->node_name; *node = candidate; prepared_name = NULL; u_free(old); } + memcpy(inst->name, desired, strlen(desired) + 1); + if (inst->local_transport) local_transport_set_name(inst->local_transport, inst->name); + DEBUG_INFO(DEBUG_CATEGORY_CHAT, "name update: %s channels=%d snapshot=%d", changed ? "committed" : "unchanged", rows, snapshot_changed); + /* NODEINFO отправляет актуальный реестр;групповые local_node/подсети не требуют замены. */ + if (snapshot_changed) for (struct ll_entry* e = inst->topo_groups->group_list->head; e; e = e->next) { + struct TOPO_GROUP* group = (struct TOPO_GROUP*)e; + if (!group->local_node || !group->senders_list) continue; + for (struct ll_entry* s = group->senders_list->head; s; s = s->next) { + struct TOPO_GROUP_CONN_ITEM* peer = (struct TOPO_GROUP_CONN_ITEM*)s->data; + if (peer && peer->conn) topo_group_send_nodeinfo(group, group->local_node, peer->conn); + } + } + for (int i = 0; i < rows; i++) if (changes[i]) + member_sync_record_committed(inst, channels[(i + 1) * columns], inst->node_id, changes[i]); + goto done; +rollback: + DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "name update: failed stage=%s rc=%d: %s", stage, rc, sqlite3_errmsg(cc->db)); + if (!sqlite3_get_autocommit(cc->db)) { + rc = sqlite3_exec(cc->db, "ROLLBACK", NULL, NULL, NULL); + if (rc != SQLITE_OK) DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "name update: rollback rc=%d: %s", rc, sqlite3_errmsg(cc->db)); + if (!sqlite3_get_autocommit(cc->db)) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "name update: rollback incomplete;requesting owner stop"); utun_instance_stop(inst); } + } +done: + if (!committed && !strcmp(stage, "begin")) DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "name update: begin rc=%d: %s", rc, sqlite3_errmsg(cc->db)); + sqlite3_free_table(channels); u_free(changes); u_free(prepared_name); return committed ? 0 : -1; +} + /* Проверить заменяемые ключи до построения/подписи нового блока мембера. */ int chat_core_set_my_profile_options(struct UTUN_INSTANCE* inst, const char* channel, const struct chat_profile_option* options, size_t count) { diff --git a/src/routing_layer/topo_node.c b/src/routing_layer/topo_node.c index bd959907a..e8010e4ac 100644 --- a/src/routing_layer/topo_node.c +++ b/src/routing_layer/topo_node.c @@ -436,7 +436,8 @@ int topo_node_sign_self(struct UTUN_INSTANCE* instance, struct TOPO_NODE* ni) { uint8_t old_sig[64]; memcpy(old_sig, ni->x25519_self_sig, sizeof(old_sig)); uint64_t previous = ni->timestamp; if (instance->topo_sqlite_db) { - uint64_t saved = topo_node_sqlite_snapshot_timestamp(instance->topo_sqlite_db, ni->node_id); + uint64_t saved; + if (topo_node_sqlite_snapshot_timestamp(instance->topo_sqlite_db, ni->node_id, &saved)) return -1; if (saved > previous) previous = saved; } int64_t now = ntp_time_get_us(instance); diff --git a/src/routing_layer/topo_node_sqlite.c b/src/routing_layer/topo_node_sqlite.c index e5e12f59a..cd7431051 100644 --- a/src/routing_layer/topo_node_sqlite.c +++ b/src/routing_layer/topo_node_sqlite.c @@ -1260,18 +1260,23 @@ void topo_node_sqlite_update_rtt(sqlite3* db, uint64_t node_id, uint16_t rtt) { } } -uint64_t topo_node_sqlite_snapshot_timestamp(sqlite3* db, uint64_t node_id) { +int topo_node_sqlite_snapshot_timestamp(sqlite3* db, uint64_t node_id, uint64_t* out) { + if (!db || !node_id || !out) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "snapshot timestamp: invalid arguments"); return -1; } sqlite3_stmt* stmt = NULL; uint64_t timestamp = 0; - if (sqlite3_prepare_v2(db, "SELECT timestamp FROM node_snapshots WHERE node_id=?", -1, &stmt, NULL) != SQLITE_OK) { - DEBUG_ERROR(DEBUG_CATEGORY_BGP, "snapshot timestamp: %s", sqlite3_errmsg(db)); return 0; + int rc = sqlite3_prepare_v2(db, "SELECT timestamp FROM node_snapshots WHERE node_id=?", -1, &stmt, NULL); + if (rc == SQLITE_OK) rc = sqlite3_bind_int64(stmt, 1, (sqlite3_int64)node_id); + if (rc == SQLITE_OK) rc = sqlite3_step(stmt); + if (rc == SQLITE_ROW) { + if (sqlite3_column_type(stmt, 0) != SQLITE_INTEGER || sqlite3_column_int64(stmt, 0) <= 0) rc = SQLITE_CORRUPT; + else timestamp = (uint64_t)sqlite3_column_int64(stmt, 0); } - sqlite3_bind_int64(stmt, 1, (sqlite3_int64)node_id); - int rc = sqlite3_step(stmt); - if (rc == SQLITE_ROW) timestamp = (uint64_t)sqlite3_column_int64(stmt, 0); - else if (rc != SQLITE_DONE) DEBUG_ERROR(DEBUG_CATEGORY_BGP, "snapshot timestamp read: %s", sqlite3_errmsg(db)); - sqlite3_finalize(stmt); - return timestamp; + int final = sqlite3_finalize(stmt); + if ((rc != SQLITE_ROW && rc != SQLITE_DONE) || final != SQLITE_OK) { + DEBUG_ERROR(DEBUG_CATEGORY_BGP, "snapshot timestamp: node=%016llx rc=%d finalize=%d: %s", + (unsigned long long)node_id, rc, final, sqlite3_errmsg(db)); return -1; + } + *out = timestamp; return 0; } int topo_node_sqlite_snapshot_put(sqlite3* db, struct TOPO_NODE* ni) { @@ -1280,20 +1285,35 @@ int topo_node_sqlite_snapshot_put(sqlite3* db, struct TOPO_NODE* ni) { struct TOPO_GROUP_NODE nq = {0}; int len = topo_node_serialize(ni, &nq, 0, 0, record, sizeof(record), 0); if (len < 0) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "snapshot serialization failed"); return -1; } + int own = sqlite3_get_autocommit(db); + int rc = sqlite3_exec(db, "SAVEPOINT node_snapshot", NULL, NULL, NULL); + if (rc != SQLITE_OK) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "snapshot begin rc=%d: %s", rc, sqlite3_errmsg(db)); return -1; } sqlite3_stmt* stmt = NULL; const char* sql = "INSERT INTO node_snapshots(node_id,timestamp,record) VALUES(?,?,?) " "ON CONFLICT(node_id) DO UPDATE SET timestamp=excluded.timestamp,record=excluded.record " "WHERE excluded.timestamp > node_snapshots.timestamp"; - int rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL); - if (rc == SQLITE_OK) { - sqlite3_bind_int64(stmt, 1, (sqlite3_int64)ni->node_id); - sqlite3_bind_int64(stmt, 2, (sqlite3_int64)ni->timestamp); - sqlite3_bind_blob(stmt, 3, record, len, SQLITE_STATIC); - rc = sqlite3_step(stmt); + rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL); + if (rc == SQLITE_OK) rc = sqlite3_bind_int64(stmt, 1, (sqlite3_int64)ni->node_id); + if (rc == SQLITE_OK) rc = sqlite3_bind_int64(stmt, 2, (sqlite3_int64)ni->timestamp); + if (rc == SQLITE_OK) rc = sqlite3_bind_blob(stmt, 3, record, len, SQLITE_STATIC); + if (rc == SQLITE_OK) rc = sqlite3_step(stmt); + int final = sqlite3_finalize(stmt); + if (rc == SQLITE_DONE && final == SQLITE_OK) { + rc = sqlite3_exec(db, "RELEASE node_snapshot", NULL, NULL, NULL); + if (rc == SQLITE_OK) return 0; } - if (rc != SQLITE_DONE) DEBUG_ERROR(DEBUG_CATEGORY_BGP, "snapshot write: %s", sqlite3_errmsg(db)); - sqlite3_finalize(stmt); - return rc == SQLITE_DONE ? 0 : -1; + DEBUG_ERROR(DEBUG_CATEGORY_BGP, "snapshot write: node=%016llx rc=%d finalize=%d: %s", + (unsigned long long)ni->node_id, rc, final, sqlite3_errmsg(db)); + if (!sqlite3_get_autocommit(db)) { + int rollback = sqlite3_exec(db, "ROLLBACK TO node_snapshot", NULL, NULL, NULL); + if (rollback == SQLITE_OK) rollback = sqlite3_exec(db, "RELEASE node_snapshot", NULL, NULL, NULL); + if (rollback != SQLITE_OK) DEBUG_ERROR(DEBUG_CATEGORY_BGP, "snapshot rollback rc=%d own=%d: %s", rollback, own, sqlite3_errmsg(db)); + if (own && !sqlite3_get_autocommit(db)) { + rollback = sqlite3_exec(db, "ROLLBACK", NULL, NULL, NULL); + if (rollback != SQLITE_OK) DEBUG_ERROR(DEBUG_CATEGORY_BGP, "snapshot full rollback rc=%d: %s", rollback, sqlite3_errmsg(db)); + } + } + return -1; } struct TOPO_NODE* topo_node_sqlite_snapshot_load(sqlite3* db, struct TOPO_GROUPS* groups, uint64_t node_id) { diff --git a/src/routing_layer/topo_node_sqlite.h b/src/routing_layer/topo_node_sqlite.h index dac65644a..60f440e5c 100644 --- a/src/routing_layer/topo_node_sqlite.h +++ b/src/routing_layer/topo_node_sqlite.h @@ -26,10 +26,13 @@ /* Создать/обновить nodes, node_addresses, node_snapshots, channels; 0/-1. db открывает и закрывает вызывающий. */ int topo_node_sqlite_init(sqlite3* db); -/* Проверить и сохранить полный подписанный NODEINFO без групповых полей; старый/равный timestamp даёт 0 без замены. */ +/* Проверить и сохранить полный подписанный NODEINFO без групповых полей;старый/равный timestamp даёт0 без замены. + * Возврат0/-1,свой savepoint проверяет bind/step/finalize/RELEASE;отказ откатывает запись. + * При неудачном rollback открытая транзакция требует rollback/закрытия БД владельцем. */ int topo_node_sqlite_snapshot_put(sqlite3* db, struct TOPO_NODE* ni); -/* Максимальный сохранённый timestamp (Unix microseconds), в том числе после инвалидирования record; 0 — нет/ошибка. */ -uint64_t topo_node_sqlite_snapshot_timestamp(sqlite3* db, uint64_t node_id); +/* Сохранённый timestamp (Unix microseconds),в том числе после инвалидирования record;отсутствие даёт0. + * Возвращает0/-1;при SQL/форматной ошибке *out не меняется. out обязателен. */ +int topo_node_sqlite_snapshot_timestamp(sqlite3* db, uint64_t node_id, uint64_t* out); /* Незарегистрированный проверенный снимок, включая снимок без адресов; NULL — нет/невалиден/ошибка. * groups и groups->instance->topo_groups должны обозначать один контейнер с пулами. */ struct TOPO_NODE* topo_node_sqlite_snapshot_load(sqlite3* db, struct TOPO_GROUPS* groups, uint64_t node_id); diff --git a/tests/test_chat_profile.c b/tests/test_chat_profile.c index 1aa480d3c..ec0b6874d 100644 --- a/tests/test_chat_profile.c +++ b/tests/test_chat_profile.c @@ -30,6 +30,9 @@ static sqlite3* reader; static char channel[32]; static struct UTUN_INSTANCE* active; static uint8_t original_join[64]; +static const char* rename_expected; +static char rename_child[32]; +static void name_consistent(struct UTUN_INSTANCE* inst, const char* desired); static int matches(const char* sql) { return target && !strncmp(sql, target, strlen(target)); } static int take(enum fault point, const char* sql, int index) { @@ -139,9 +142,10 @@ int __wrap_member_sync_add_apply_cbk(struct UTUN_INSTANCE* inst, member_apply_cb static void snapshot(sqlite3* db, uint8_t out[32]) { char peers[128]; snprintf(peers, sizeof(peers), "SELECT * FROM peers_%s ORDER BY node_id", channel); const char* queries[] = {peers, "SELECT * FROM nodes ORDER BY node_id", - "SELECT namespace,level,prefix64,hash FROM merkle_tree_hash ORDER BY namespace,level,prefix64"}; + "SELECT namespace,level,prefix64,hash FROM merkle_tree_hash ORDER BY namespace,level,prefix64", + "SELECT * FROM local_identity", "SELECT * FROM node_snapshots ORDER BY node_id"}; EVP_MD_CTX* hash = EVP_MD_CTX_new(); assert(hash && EVP_DigestInit_ex(hash, EVP_sha256(), NULL) == 1); - for (unsigned i = 0; i < 3; i++) { + for (unsigned i = 0; i < sizeof(queries) / sizeof(queries[0]); i++) { sqlite3_stmt* st = NULL; assert(sqlite3_prepare_v2(db, queries[i], -1, &st, NULL) == SQLITE_OK); int rc; while ((rc = sqlite3_step(st)) == SQLITE_ROW) for (int c = 0; c < sqlite3_column_count(st); c++) { int type = sqlite3_column_type(st, c), bytes = sqlite3_column_bytes(st, c); const void* value = sqlite3_column_blob(st, c); @@ -162,6 +166,7 @@ static void event(struct UTUN_INSTANCE* inst, int type, const uint8_t* data, int char profile[MS_USERINFO_MAX + 1]; uint8_t signature[64], writer[32], committed[32]; uint64_t timestamp; assert(!chat_core_get_my_profile(inst, channel, profile, sizeof(profile), signature, ×tamp)); snapshot(inst->topo_sqlite_db, writer); snapshot(reader, committed); assert(!memcmp(writer, committed, 32)); events++; + if (rename_expected) name_consistent(inst, rename_expected); if (delete_on_event) { delete_on_event = 0; chat_core_delete_channel(inst, channel); } } @@ -545,6 +550,84 @@ static void tags_callback_deletion(struct UTUN_INSTANCE* inst) { puts("PASS tags:channel deletion from committed notification keeps editor ownership valid"); } +/* Каждый observer видит одновременно identity,NODEINFO,проекцию и оба подписанных профиля. */ +static void name_consistent(struct UTUN_INSTANCE* inst, const char* desired) { + assert(!strcmp(inst->name, desired)); + struct TOPO_NODE* node = topo_node_registry_find(inst->topo_groups, inst->node_id); + assert(node && !strcmp(node->node_name, desired) && !topo_node_verify(node)); + struct TOPO_NODE* saved = topo_node_sqlite_snapshot_load(reader, inst->topo_groups, inst->node_id); + assert(saved && saved->timestamp == node->timestamp && !strcmp(saved->node_name ? saved->node_name : "", desired)); + topo_node_destroy(inst->topo_groups, saved); + sqlite3_stmt* st = NULL; + assert(sqlite3_prepare_v2(reader, "SELECT name FROM local_identity UNION ALL SELECT name FROM nodes WHERE node_id=?", -1, &st, NULL) == SQLITE_OK); + assert(sqlite3_bind_int64(st, 1, (sqlite3_int64)inst->node_id) == SQLITE_OK); + for (unsigned i = 0; i < 2; i++) assert(sqlite3_step(st) == SQLITE_ROW && !strcmp((const char*)sqlite3_column_text(st, 0), desired)); + assert(sqlite3_step(st) == SQLITE_DONE && sqlite3_finalize(st) == SQLITE_OK); + const char* channels[] = {channel, rename_child}; + for (unsigned i = 0; i < 2; i++) { + char json[MS_USERINFO_MAX + 1], name[128]; uint8_t signature[64]; uint64_t timestamp; + assert(!chat_core_get_my_profile(inst, channels[i], json, sizeof(json), signature, ×tamp)); + assert(!json_flat_get(json, "name", name, sizeof(name)) && !strcmp(name, desired)); + } +} + +static void name_refused(struct UTUN_INSTANCE* inst, const char* desired) { + uint8_t before[32], after[32]; snapshot(reader, before); unsigned count = events; + char original[128]; strcpy(original, inst->name); + struct TOPO_NODE* node = topo_node_registry_find(inst->topo_groups, inst->node_id); uint64_t timestamp = node->timestamp; + assert(chat_core_update_my_name(inst, desired) < 0 && fault == F_NONE && !fail_sign && sqlite3_get_autocommit(inst->topo_sqlite_db)); + snapshot(reader, after); assert(!memcmp(before, after, 32) && events == count && node->timestamp == timestamp); + name_consistent(inst, original); + rename_expected = desired; assert(!chat_core_update_my_name(inst, desired)); rename_expected = NULL; + name_consistent(inst, desired); +} + +static void rename_faults(struct UTUN_INSTANCE* inst) { + uint64_t child = chat_core_create_channel_auto(inst, "rename second channel"); assert(child); + snprintf(rename_child, sizeof(rename_child), "%llu", (unsigned long long)child); + struct TOPO_NODE candidate = *topo_node_registry_find(inst->topo_groups, inst->node_id); + uint8_t saved_before[32], saved_after[32]; snapshot(reader, saved_before); + target = "INSERT INTO node_snapshots"; fault = F_FINALIZE; + assert(topo_node_sign_self(inst, &candidate) < 0 && fault == F_NONE && sqlite3_get_autocommit(inst->topo_sqlite_db)); + snapshot(reader, saved_after); assert(!memcmp(saved_before, saved_after, 32)); + const char* queries[] = {"SELECT local_identity.name,nodes.name", "UPDATE local_identity SET name=", "UPDATE nodes SET name=", + "INSERT INTO node_snapshots", "SELECT timestamp FROM node_snapshots"}; + unsigned cases = 0; + for (unsigned q = 0; q < sizeof(queries) / sizeof(queries[0]); q++) { + for (enum fault point = F_PREPARE; point <= F_FINALIZE; point++) { + if (point == F_CHANGES && (q == 0 || q >= 3)) continue; + for (int index = 1; index <= (point == F_BIND && q >= 1 && q <= 3 ? 3 : 1); index++) { + char desired[64]; snprintf(desired, sizeof(desired), "rename case %u", ++cases); + target = queries[q]; fault = point; index_number = index; name_refused(inst, desired); + } + } + } + const char* commands[] = {"BEGIN IMMEDIATE /* name */", "COMMIT /* name */", "SAVEPOINT node_snapshot", "RELEASE node_snapshot"}; + for (unsigned i = 0; i < sizeof(commands) / sizeof(commands[0]); i++) { + char desired[64]; snprintf(desired, sizeof(desired), "rename SQL %u", i); + target = commands[i]; fault = F_EXEC; name_refused(inst, desired); cases++; + } + target = NULL; fail_sign = 1; name_refused(inst, "rename signing retry"); + uint8_t before[32], after[32]; snapshot(reader, before); unsigned count = events, commits = 0; + sqlite3_commit_hook(inst->topo_sqlite_db, veto, &commits); + assert(!chat_core_update_my_name(inst, inst->name) && !commits && events == count); + assert(chat_core_update_my_name(inst, "commit veto") < 0 && commits == 1 && events == count); + sqlite3_commit_hook(inst->topo_sqlite_db, NULL, NULL); + snapshot(reader, after); assert(!memcmp(before, after, 32)); + assert(sqlite3_exec(inst->topo_sqlite_db, "BEGIN", NULL, NULL, NULL) == SQLITE_OK); + assert(chat_core_update_my_name(inst, "foreign") < 0 && !sqlite3_get_autocommit(inst->topo_sqlite_db)); + assert(sqlite3_exec(inst->topo_sqlite_db, "ROLLBACK", NULL, NULL, NULL) == SQLITE_OK); + char long_name[129]; memset(long_name, 'x', 128); long_name[128] = 0; + assert(chat_core_update_my_name(inst, long_name) < 0 && chat_core_update_my_name(NULL, "name") < 0); + struct update_my_name_arg* malformed = u_calloc(1, sizeof(*malformed)); assert(malformed); + malformed->inst = inst; memset(malformed->name, 'x', sizeof(malformed->name)); chat_core_update_my_name_trampoline(malformed); + name_consistent(inst, inst->name); + assert(!chat_core_update_my_name(inst, "quoted\"\\name\x01")); name_consistent(inst, "quoted\"\\name\x01"); + assert(!chat_core_update_my_name(inst, "")); name_consistent(inst, ""); + chat_core_delete_channel(inst, rename_child); rename_child[0] = 0; + printf("PASS rename:%u SQL faults/retries,crypto/veto/no-op,foreign txn,validation;two profiles/identity/NODEINFO committed together\n", cases); +} + int main(void) { debug_config_init(); debug_set_level(DEBUG_LEVEL_ERROR); size_t baseline = u_get_allocated_count(); unicode(); @@ -569,6 +652,7 @@ int main(void) { chat_event_set_handler(inst, event); reader_faults(inst); writer_faults(inst); damaged_profile(inst); semantic_profile(inst); owner_faults(inst); avatar(inst); tags_reader_failure(inst); tags_faults(inst); tags_allocation_faults(inst); tags_semantics(inst); tags_long_history(inst); tags_callback_deletion(inst); + rename_faults(inst); registration_failure(inst, ua); callback_deletion(inst); uint8_t before[32], after[32]; snapshot(reader, before); unsigned count = events; target = "COMMIT /* profile */"; fault = F_EXEC; fail_rollback = 1; inst->running = 1; diff --git a/tests/test_node_snapshot.c b/tests/test_node_snapshot.c index c6d3abdaf..243a7bcc0 100644 --- a/tests/test_node_snapshot.c +++ b/tests/test_node_snapshot.c @@ -202,7 +202,8 @@ static void test_compact_addresses(struct UTUN_INSTANCE *inst) { assert(sqlite3_exec(db, "ALTER TABLE node_addresses ADD COLUMN nat_evidence INTEGER DEFAULT 1;" "ALTER TABLE node_addresses ADD COLUMN nat_checked INTEGER DEFAULT 1", NULL, NULL, NULL) == SQLITE_OK); assert(!topo_node_sqlite_init(db)); - assert(topo_node_sqlite_snapshot_timestamp(db, inst->node_id) == node->timestamp); + uint64_t saved_timestamp; + assert(!topo_node_sqlite_snapshot_timestamp(db, inst->node_id, &saved_timestamp) && saved_timestamp == node->timestamp); assert(!topo_node_sqlite_snapshot_load(db, inst->topo_groups, inst->node_id)); loaded = topo_node_sqlite_node_load(db, inst->topo_groups, inst->node_id); assert(loaded && !loaded->timestamp && loaded->v4_addrs && loaded->v6_addrs && loaded->reality_socks);