From 8a7f4469dcf36abe62eea409987f24e6b8aa86e3 Mon Sep 17 00:00:00 2001 From: evgeny Date: Tue, 27 Oct 2026 00:10:58 +0100 Subject: [PATCH] Commit author attachment decisions and delivery queue atomically --- src/chat/chat_attachment.c | 142 +++++++++++++++++++++++++++++-------- src/chat/chat_attachment.h | 10 +-- 2 files changed, 117 insertions(+), 35 deletions(-) diff --git a/src/chat/chat_attachment.c b/src/chat/chat_attachment.c index b20f4b96..b3b3d856 100644 --- a/src/chat/chat_attachment.c +++ b/src/chat/chat_attachment.c @@ -366,13 +366,40 @@ static int ak_send(struct UTUN_INSTANCE* inst, const char* channel, uint64_t pee return etcp_router_conn_send(conn, body, length, ROUTER_FLAG_SIGNED | ROUTER_FLAG_ENCRYPTED); } -/* Очередь содержит только команду; ключ читается при отправке из отдельной таблицы. */ +/* Записать ровно одну команду; транзакцией владеет вызывающий, ключ читается при отправке. */ static int ak_queue(struct UTUN_INSTANCE* inst, const char* channel, const uint8_t message[32], uint64_t peer, int cmd) { - sqlite3_stmt* st = ak_prepare(inst, "INSERT INTO attachment_key_outbox(channel,message_id,peer,command) VALUES(?,?,?,?)" - " ON CONFLICT(channel,message_id,peer) DO UPDATE SET command=excluded.command,retry_at=0"); - if (!st) return -1; - sqlite3_bind_text(st, 1, channel, -1, SQLITE_STATIC); sqlite3_bind_blob(st, 2, message, 32, SQLITE_STATIC); - sqlite3_bind_int64(st, 3, (sqlite3_int64)peer); sqlite3_bind_int(st, 4, cmd); return ak_step(inst, st); + sqlite3* db = CC(inst)->db; sqlite3_stmt* st = NULL; + const char* stage = "prepare"; + int rc = sqlite3_prepare_v2(db, "INSERT INTO attachment_key_outbox(channel,message_id,peer,command) VALUES(?,?,?,?)" + " ON CONFLICT(channel,message_id,peer) DO UPDATE SET command=excluded.command,retry_at=0", -1, &st, NULL); + if (rc != SQLITE_OK) goto fail; + stage = "bind channel"; rc = sqlite3_bind_text(st, 1, channel, -1, SQLITE_STATIC); + if (rc != SQLITE_OK) goto fail; + stage = "bind message"; rc = sqlite3_bind_blob(st, 2, message, 32, SQLITE_STATIC); + if (rc != SQLITE_OK) goto fail; + stage = "bind peer"; rc = sqlite3_bind_int64(st, 3, (sqlite3_int64)peer); + if (rc != SQLITE_OK) goto fail; + stage = "bind command"; rc = sqlite3_bind_int(st, 4, cmd); + if (rc != SQLITE_OK) goto fail; + stage = "step"; rc = sqlite3_step(st); + if (rc != SQLITE_DONE) goto fail; + int changed = sqlite3_changes(db); + stage = "finalize"; rc = sqlite3_finalize(st); st = NULL; + if (rc != SQLITE_OK) goto fail; + if (changed != 1) { + DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "attachment keys: queue rows=%d expected=1 channel=%s peer=%llu command=%d", + changed, channel, (unsigned long long)peer, cmd); + stage = "row count"; rc = SQLITE_CONSTRAINT; goto fail; + } + DEBUG_DEBUG(DEBUG_CATEGORY_MEDIA, "attachment keys: queue staged channel=%s peer=%llu command=%d rows=%d", + channel, (unsigned long long)peer, cmd, changed); + return 0; +fail: + DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "attachment keys: queue channel=%s peer=%llu command=%d stage=%s rc=%d (%s) db=%s", + channel, (unsigned long long)peer, cmd, stage, rc, sqlite3_errstr(rc), sqlite3_errmsg(db)); + int final_rc = sqlite3_finalize(st); + if (final_rc != SQLITE_OK) DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "attachment keys: queue cleanup finalize rc=%d", final_rc); + return -1; } /* Разрешение проверяет полное подписанное членство и immutable историю. */ @@ -381,14 +408,29 @@ static int ak_authorized(struct UTUN_INSTANCE* inst, const char* channel, const if (member_sync_verified_ed25519(inst, channel, peer, key)) return 0; char sql[256]; snprintf(sql, sizeof(sql), "SELECT 1 FROM msg_%s m JOIN attachment_keys a ON a.message_id=m.message_id" " AND a.channel=? WHERE m.message_id=? AND m.node_id=? AND a.author=? AND length(a.key)=32", channel); - sqlite3_stmt* st = ak_prepare(inst, sql); - if (!st) return 0; - sqlite3_bind_text(st, 1, channel, -1, SQLITE_STATIC); sqlite3_bind_blob(st, 2, message, 32, SQLITE_STATIC); - sqlite3_bind_int64(st, 3, (sqlite3_int64)inst->node_id); sqlite3_bind_int64(st, 4, (sqlite3_int64)inst->node_id); - int rc = sqlite3_step(st); sqlite3_finalize(st); - if (rc != SQLITE_ROW) DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "attachment keys: request has no local author key/message channel=%s peer=%llu rc=%d", - channel, (unsigned long long)peer, rc); - return rc == SQLITE_ROW; + sqlite3* db = CC(inst)->db; sqlite3_stmt* st = NULL; + const char* stage = "prepare"; + int rc = sqlite3_prepare_v2(db, sql, -1, &st, NULL); + if (rc != SQLITE_OK) goto fail; + stage = "bind channel"; rc = sqlite3_bind_text(st, 1, channel, -1, SQLITE_STATIC); + if (rc != SQLITE_OK) goto fail; + stage = "bind message"; rc = sqlite3_bind_blob(st, 2, message, 32, SQLITE_STATIC); + if (rc != SQLITE_OK) goto fail; + stage = "bind node"; rc = sqlite3_bind_int64(st, 3, (sqlite3_int64)inst->node_id); + if (rc != SQLITE_OK) goto fail; + stage = "bind author"; rc = sqlite3_bind_int64(st, 4, (sqlite3_int64)inst->node_id); + if (rc != SQLITE_OK) goto fail; + stage = "step"; rc = sqlite3_step(st); + if (rc != SQLITE_ROW) goto fail; + stage = "finalize"; rc = sqlite3_finalize(st); st = NULL; + if (rc != SQLITE_OK) goto fail; + return 1; +fail: + DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "attachment keys: author authorization channel=%s peer=%llu stage=%s rc=%d (%s) db=%s", + channel, (unsigned long long)peer, stage, rc, sqlite3_errstr(rc), sqlite3_errmsg(db)); + int final_rc = sqlite3_finalize(st); + if (final_rc != SQLITE_OK) DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "attachment keys: authorization cleanup finalize rc=%d", final_rc); + return 0; } /* Подтверждение протокола посылается только после durable записи. */ @@ -569,30 +611,70 @@ done: /* Только явное решение автора создаёт GRANT в durable outbox. */ int chat_attachment_decide(struct UTUN_INSTANCE* inst, const char* channel, const uint8_t message[32], uint64_t peer, int approve) { - if (!inst || !CC(inst) || CC(inst)->attachment_closing || !ak_channel(channel) || !ak_authorized(inst, channel, message, peer)) { - DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "attachment keys: decision rejected by authorization"); return -1; + if (!inst || !CC(inst) || !CC(inst)->attachment_initialized || CC(inst)->attachment_closing || + !ak_channel(channel) || !message || !peer || peer == inst->node_id) { + DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "attachment keys: invalid decision arguments/state"); return -1; } - if (ak_exec(inst, "SAVEPOINT attachment_decision")) return -1; - sqlite3_stmt* st = ak_prepare(inst, "UPDATE attachment_key_requests SET state=? WHERE channel=? AND message_id=? AND peer=? AND incoming=1 AND state=0"); - if (!st) goto rollback; - sqlite3_bind_int(st, 1, approve ? 2 : 3); sqlite3_bind_text(st, 2, channel, -1, SQLITE_STATIC); - sqlite3_bind_blob(st, 3, message, 32, SQLITE_STATIC); sqlite3_bind_int64(st, 4, (sqlite3_int64)peer); - if (ak_step(inst, st) || sqlite3_changes(CC(inst)->db) != 1 || ak_queue(inst, channel, message, peer, approve ? AK_GRANT : AK_DENY) || - ak_exec(inst, "RELEASE attachment_decision")) goto rollback; - DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "attachment keys: author decision channel=%s peer=%llu approve=%d", channel, (unsigned long long)peer, approve); + sqlite3* db = CC(inst)->db; + if (!sqlite3_get_autocommit(db)) { + DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "attachment keys: decision rejected inside external transaction channel=%s peer=%llu", + channel, (unsigned long long)peer); return -1; + } + sqlite3_stmt* st = NULL; + int next = approve ? 2 : 3, command = approve ? AK_GRANT : AK_DENY; + const char* stage = "begin"; + int rc = sqlite3_exec(db, "BEGIN IMMEDIATE", NULL, NULL, NULL); + if (rc != SQLITE_OK) goto fail; + stage = "authorization"; + if (!ak_authorized(inst, channel, message, peer)) { rc = SQLITE_AUTH; goto fail; } + stage = "state prepare"; + rc = sqlite3_prepare_v2(db, "UPDATE attachment_key_requests SET state=?" + " WHERE channel=? AND message_id=? AND peer=? AND incoming=1 AND state=0", -1, &st, NULL); + if (rc != SQLITE_OK) goto fail; + stage = "state bind state"; rc = sqlite3_bind_int(st, 1, next); + if (rc != SQLITE_OK) goto fail; + stage = "state bind channel"; rc = sqlite3_bind_text(st, 2, channel, -1, SQLITE_STATIC); + if (rc != SQLITE_OK) goto fail; + stage = "state bind message"; rc = sqlite3_bind_blob(st, 3, message, 32, SQLITE_STATIC); + if (rc != SQLITE_OK) goto fail; + stage = "state bind peer"; rc = sqlite3_bind_int64(st, 4, (sqlite3_int64)peer); + if (rc != SQLITE_OK) goto fail; + stage = "state step"; rc = sqlite3_step(st); + if (rc != SQLITE_DONE) goto fail; + int changed = sqlite3_changes(db); + stage = "state finalize"; rc = sqlite3_finalize(st); st = NULL; + if (rc != SQLITE_OK) goto fail; + if (changed != 1) { + DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "attachment keys: decision pending rows=%d expected=1 channel=%s peer=%llu", + changed, channel, (unsigned long long)peer); + stage = "state row count"; rc = SQLITE_CONSTRAINT; goto fail; + } + stage = "outbox"; + if (ak_queue(inst, channel, message, peer, command)) { rc = SQLITE_ERROR; goto fail; } + stage = "commit"; rc = sqlite3_exec(db, "COMMIT", NULL, NULL, NULL); + if (rc != SQLITE_OK) goto fail; + DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "attachment keys: decision committed channel=%s peer=%llu message=%02x%02x%02x%02x state=%d command=%d", + channel, (unsigned long long)peer, message[0], message[1], message[2], message[3], next, command); ak_changed(inst, channel); return 0; -rollback: - DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "attachment keys: decision failed channel=%s peer=%llu", channel, (unsigned long long)peer); - ak_exec(inst, "ROLLBACK TO attachment_decision"); ak_exec(inst, "RELEASE attachment_decision"); return -1; +fail: + DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "attachment keys: decision channel=%s peer=%llu command=%d stage=%s rc=%d (%s) db=%s", + channel, (unsigned long long)peer, command, stage, rc, sqlite3_errstr(rc), sqlite3_errmsg(db)); + int final_rc = sqlite3_finalize(st); + if (final_rc != SQLITE_OK) DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "attachment keys: decision cleanup finalize rc=%d", final_rc); + ak_rollback(inst, "decision", channel); + return -1; } /* GUI передаёт зафиксированные ID, результат возвращается после записи решения. */ void chat_attachment_decision_trampoline(void* arg) { struct chat_attachment_decision* request = arg; int rc = chat_attachment_decide(request->inst, request->channel, request->message, request->peer, request->approve); - uint8_t event[34]; size_t n = strlen(request->channel); - event[0] = (uint8_t)n; memcpy(event + 1, request->channel, n); event[n + 1] = rc ? 0 : 1; - chat_event_post(request->inst, CHAT_EVT_ATTACHMENT_KEYS_CHANGED, event, (int)n + 2); u_free(request); + if (rc) { + uint8_t event[34]; size_t n = strlen(request->channel); + event[0] = (uint8_t)n; memcpy(event + 1, request->channel, n); event[n + 1] = 0; + chat_event_post(request->inst, CHAT_EVT_ATTACHMENT_KEYS_CHANGED, event, (int)n + 2); + } + u_free(request); } /* Пользовательский интерес хранится отдельно от фоновой storage-копии. */ diff --git a/src/chat/chat_attachment.h b/src/chat/chat_attachment.h index 09ab97da..98f67d32 100644 --- a/src/chat/chat_attachment.h +++ b/src/chat/chat_attachment.h @@ -61,10 +61,10 @@ void chat_attachment_downloaded(struct UTUN_INSTANCE* inst, const char* channel, void chat_attachment_want(struct UTUN_INSTANCE* inst, const char* channel, const uint8_t message[32]); /* Сохранить решение автора по ожидающему запросу peer: approve!=0 — выдать ключ, 0 — отказать. - * 0 — операция сообщила успех, -1 — ошибка/нет прав или ожидающего запроса; доставка идёт позже. - * Известный дефект: SAVEPOINT внутри внешней транзакции не фиксирует решение до события UI; - * пропуск INSERT в outbox также может дать 0 с решением без ответа. См. doc/chat_audit_findings.txt, - * «Аудит решения автора»: проверенный commit решения+очереди здесь пока не гарантирован. */ + * 0 — собственная транзакция решения+outbox завершена, затем послано одно уведомление изменения; + * -1 — ошибка/нет прав или ожидающего запроса, прежнее состояние сохранено. Доставка идёт позже. + * Внешняя транзакция отклоняется без изменений; проверка прав выполняется внутри собственной. + * Открытая после отказа rollback транзакция требует остановки/закрытия БД владельцем после poll. */ int chat_attachment_decide(struct UTUN_INSTANCE* inst, const char* channel, const uint8_t message[32], uint64_t peer, int approve); // ── Команда интерфейса ── @@ -76,7 +76,7 @@ struct chat_attachment_decision { uint64_t peer; int approve; }; -// arg — u_malloc/u_calloc chat_attachment_decision, освобождается здесь; результат — CHAT_EVT_ATTACHMENT_KEYS_CHANGED. +// arg — u_malloc/u_calloc chat_attachment_decision, освобождается здесь; успех уже уведомлён decide, ошибка — событие с success=0. void chat_attachment_decision_trampoline(void* arg); // ── Локальное удаление канала ──