diff --git a/doc/dm_arch.md b/doc/dm_arch.md index 50535c6d..27e23fbd 100644 --- a/doc/dm_arch.md +++ b/doc/dm_arch.md @@ -44,7 +44,10 @@ AES-256-CCM: nonce 13 байт, tag 16 байт. Максимальная пор Автор подписывает все поля до sig. conv связывает автора с получателем. Для `ct=file` перед подписью добавляется descriptor `[UUID:16][plaintext_size:8][SHA256(ciphertext):32]`. В зашифрованном теле находится `attachment_info`; пути хранения строятся только из UUID. -Размер plaintext сообщения ограничен 1024 байтами. seq — положительное int64. +Размер data в API отправки ограничен 1024 байтами; для file сюда входит открытый +descriptor (56 байт), поэтому на зашифрованные метаданные остаётся до 968 байт. +Парсер входящего wire допускает до 1024 байт ciphertext plaintext отдельно от descriptor. +seq — положительное int64, ts — миллисекунды NTP-времени instance. ```text [kind:1][name_len:2][duration_ms:4][width:2][height:2][UTF8_name:name_len][waveform:100, только voice] @@ -74,6 +77,7 @@ tags, точный размер и хеш; владелец публикует ## Владение и данные - `dm_core`: беседы, история, outbox и подтверждение получателя. +- `dm_history`: поколения, hidden, точные отпечатки удалённых сообщений и манифест локальных путей. - `dm_mailbox`: промежуточные сообщения суперузла и квитанции. - `dm_crypto`: вывод conv_id, content_key и nonce сообщений без БД/сети. - `media_crypto`: общие CCM и потоковые файловые примитивы для DM и групповых вложений. @@ -91,6 +95,9 @@ dm_messages(conv_id, dir, seq, ts, author, ct, data, sig) dm_outbox(conv_id, seq, body, retry_at) dm_pending_messages(recipient, sender, seq, msg, group_id, retry_at) dm_mail_receipts(recipient, sender, seq, receipt, group_id) +dm_history(conv_id, generation, hidden) +dm_deleted_messages(conv_id, dir, seq, body_hash, media_id, media_receipt) +dm_local_files(path, conv_id, generation, busy, retry_at) ``` Запись исходящего сообщения, увеличение seq и запись неизменяемого body в outbox — @@ -121,10 +128,21 @@ Dedup — точный ключ (conv,dir,seq) с проверкой сохра беседы и сброса счётчиков. Полный снимок выдаётся только после успешной загрузки. Исправление и отказные проверки описаны в [dm_audit_findings.txt](dm_audit_findings.txt). -У каждой локальной очереди таймер 1с; порция обработки до 32 задач, повтор задачи -через 5с. retry_at предотвращает голодание хвоста очереди. Перед отправкой проверяется -backpressure router. Потеря пути сохраняет durable задачу; остановка отменяет таймеры, -но сохраняет БД. При старте очередь продолжает работу. +Таймеры работают раз в 1с: outbox A и текстовая очередь S обрабатывают до 32 задач; +локальные файлы endpoint, custody M и media jobs S — до 8 задач каждый. +Обычный retry_at — через 5с; исходящий файл с квитанцией проверяется через 3600с, +неудачная уборка локального пути повторяется через 30с. Это время допуска следующей +попытки, не гарантия доставки за указанный срок. Перед отправкой проверяется backpressure router. +Потеря пути сохраняет durable задачу; stop отменяет таймеры, но сохраняет БД. +При старте медиа сбрасывает retry_at/downloading; текстовые очереди используют сохранённый retry_at. + +Все DM API вызываются в uasync-потоке. Порядок запуска в `chat_service_start`: +mailbox (включая media coordinator), core/history, media/file_transfer; к этому времени +SQLite, router и media_async уже живы. Порядок остановки: media_quiesce прекращает +новые задачи и закрывает transfer/CM handles; media_async_destroy завершает workers +и callbacks с живыми owners; затем уничтожаются media, core и mailbox, после них группы/чат. +SQLite принадлежит ядру и остановкой DM не закрывается. `dm_media_destroy` при живой +работе логирует ошибку и оставляет owner; заменять drain таким вызовом нельзя. ## Подтверждения и протокол @@ -200,16 +218,44 @@ TTL текстовых сообщений не введён; недельный Текстовая история, подписи и сроки хранения M этим переходом не меняются. Подробности и регрессии: [dm_audit_findings.txt](dm_audit_findings.txt), раздел11. -Control `ETCP_RT_ID_DM_MEDIA=0x38`: OFFER, STORE, STORED, RECEIPT, DELETE, -DELETE_ACK, EXPIRED, REJECT; точные форматы заданы в `dm_media.h` и реализациях owners. +Control `ETCP_RT_ID_DM_MEDIA=0x38`: перед body идёт один байт команды. +Все многобайтные поля DM/control имеют native-endian порядок; исключение — +зашифрованный attachment_info с big-endian полями, описанный выше. + +| Команда | Body | +| --- | --- | +| OFFER=1 | recipient:8, holder:8, expires:8, file-message | +| STORE=2 | recipient:8, file-message | +| STORED=3 | UUID:16, expires:8 | +| RECEIPT=4 / DELETE=5 | файловая квитанция:152 | +| DELETE_ACK=6 / EXPIRED=7 | UUID:16 | +| REJECT=8 | UUID:16, reason:1 (1 — недостаток квоты) | + Подпись router обязательна. Media receipt содержит `DMFILE01`, conv, seq, author, recipient, UUID и SHA256 полного подписанного сообщения; B подписывает первые 88 байт. Отдельные таблицы `dm_files`, `dm_custody`, `dm_custody_done`, `dm_media_jobs`, `dm_media_holders` сохраняют состояния и tombstones. Все выданные grants M учитываются при удалении; повторный STORE не продлевает TTL. -Qt и Android получают историю через события ядра: 41 — conv + JSON сообщений, -45 — JSON бесед. Снимки ограничены 200 записями. Отправка вложения вызывает +A предлагает собственный файл с holder=A и expires=0. S выбирает M с storage=yes +в общей группе A/B, исключая A/B и все прежние назначения этого UUID. Назначение +фиксируется до STORE; S/M могут быть одним узлом, local control обрабатывается синхронно. +Успешная передача control в router или локальный обработчик не означает durable успех. + +EXPIRED относится к промежуточной копии. B помечает ещё не готовый файл expired, +но продолжает попытки получить оригинал у A. После проверки исходного ciphertext, +публикации и сохранения квитанции файл снова ready; поздний EXPIRED его не меняет. +Готовый файл A/B остаётся в локальной истории до явного удаления беседы. +При удалении busy-счётчик каждого пути удерживает его до callback worker/transfer; +уборщик работает по старым поколениям, использует busy=-1 для своего захвата +и синхронизирует каталог после удаления. Пользовательский исходник не входит в уборку, +если он не был явно передан как временный файл приложения. + +Qt и Android получают историю через события ядра: 41 — +`[conv_len:1][conv:decimal][generation:8][JSON сообщений]`, 45 — JSON бесед. +Запрос истории с ненулевым generation читается только для текущей видимой беседы; +0 захватывает текущее поколение в uasync. UI отбрасывает снимок другого поколения. +Снимки ограничены 200 записями. Отправка вложения вызывает `dm_send_file` в uasync; длительная обработка выполняется в `media_async`. Android копирует выбранный URI и сохраняет полученный файл через `Dispatchers.IO`; Qt сохраняет файл из контекстного меню в отдельном потоке. Выбор «как файл» сохраняет @@ -239,14 +285,16 @@ Android использует общие `ConversationComposer`/`ConversationMess ## Проверки -Аудит 26.10.2026 частичный: полностью сверены `dm_crypto.c/.h`, прочитаны `dm_core.c` -и `dm_history.c`; контракты остальных owners и платформенные callers ещё не проверены полностью. +Аудит контрактов DM завершён 27.10.2026: все семь заголовков `src/dm/`, этот документ, +`pm.txt` и `pm_invites.md` сверены с реализациями owners и относящимися к ним callers. +Это проверка соответствия документации; свежая интерактивная проверка клиентов и +новые платформенные сборки не выполнялись. Подробности и границы проверки — +[dm_audit_findings.txt](dm_audit_findings.txt), раздел 12. Первоначальная проверка отказа SQLite выявила сброс счётчиков; дефект исправлен. `test_dm_load` проверяет 79 отказных сценариев: чтение, bind/step/finalize, короткие ключи и X25519; открытие/приём/отправку/JSON/invite до и после удаления; отдельные отказы подготовки файла, отсутствие беседы, restart и продолжение seq. При отказе сравниваются все поля DM-таблиц и отсутствие SQL-записей/событий успеха. -Исправление не означает завершения аудита остальных owners. `test_dm_names`: 68 случаев с точным сравнением имени в SQL и JSON при создании, загрузке, штатной записи, fallback из nodes и перезапуске. ASCII и 2/3/4-байтовые @@ -278,17 +326,34 @@ SQL rollback регистрации, состояния JSON и отмена о обоих UPDATE и commit, точный rollback назначения/job, успешный повтор, дубликат и поздний STORED. Сетевой сценарий теряет EXPIRED, блокирует SQL-сброс, перезапускает S и проверяет восстановление из сохранённого expires, состояние M и отсутствие ложной квитанции. +На M проверяются 11 SQL-отказов retirement: rollback сохраняет custody и не выдаёт ACK; +после restart M/S потерянный DELETE_ACK восстанавливается по tombstone и повтору DELETE. +После EXPIRED и возврата A проверяются точный оригинал на B, настоящая файловая +квитанция, очистка S и невлияние позднего EXPIRED на готовый файл. `test_dm_worker`: повторное шифрование 8 МиБ не менее 0,5 с и проверка расшифровки; work выполняется вне uasync, done в его потоке, heartbeat продолжает работать. Destroy отменяет уже posted completion, callback вызывается ровно один раз. +Linux `test_dm_media_cleanup`: 13 отказов SQL при чтении назначений, записи ACK +и очистке body; сохранность квитанции, повтор, restart S, потерянный/повторный/чужой ACK, +несколько M и поздний STORED. Сеть в этом component-тесте перехвачена; подпись B и +реальное удаление M проверяет `test_dm_media`. Ошибка finalize после успешного +autocommit не означает rollback уже выполненной одношаговой записи. + `test_attachment`: строгий формат, повреждения и граничные длины, реальное кодирование/декодирование 50 Opus-кадров, длительность и waveform. `test_attachment_send`: выбранная беседа после смены текущего target, освобождение PCM в worker, остановка до completion без регистрации и удаление отклонённого cache. Qt `test_voice_file`: реальные PCM/Opus, повреждённые файлы и отмена async decoder. +Заключительный прогон 27.10.2026: штатные `test_dm`, `test_dm_history`, `test_dm_names`, +`test_dm_load`, `test_dm_e2e`, `test_dm_invite`, `test_dm_media`, `test_dm_worker` +и Linux `test_dm_media_cleanup` — 9/9 PASS. +Отдельный строгий JSON-парсер прочитал все 273 снимка имён. Заголовки проверены в C99/C++20; +в заключительном этапе менялись только документация, комментарии и форматирование деклараций. +Результаты старых сборок ниже относятся к указанной дате, а не к этому прогону. + Проверка 01.10.2026: чистые сборки Qt и Android APK; полный `check.sh` — 117 PASS, 0 FAIL, 1 SKIP (auto_socket_dynamic требует root), proxy/burst/load PASS. Qt CTest: 7 PASS, 0 FAIL; Android APK собран после `clean`. diff --git a/doc/dm_audit_findings.txt b/doc/dm_audit_findings.txt index e4bb0c82..71b276c2 100644 --- a/doc/dm_audit_findings.txt +++ b/doc/dm_audit_findings.txt @@ -1,8 +1,11 @@ -DM — аудит документации и контрактов, 26.10.2026 +DM — аудит документации и контрактов, 26–27.10.2026 База: /home/user/utun2, HEAD 03b050949cf36cc4868324be94a54eaddfdd494c. Задание: /home/user/utun-doc-audit-checklist.txt, раздел src/dm/ и связанные документы. +Текущий статус на 27.10.2026: аудит контрактов DM завершён, 10/10 файлов [V]. +Итог — раздел12; разделы1–11 сохраняют историю промежуточных проверок и исправлений. + 1. Объём и статус Полностью прочитаны dm_core.c/.h, dm_crypto.c/.h и dm_history.c/.h, doc/dm_arch.md, @@ -506,3 +509,101 @@ __real_sqlite3_bind_blob. После окончания правок повто /tmp/dm-expiry-fix-{sanitizers-build,sanitizers-results,sanitizers-test_dm*,full-check,netns}.log. Задача завершения TTL закрыта. Общий аудит DM частичный;прежние отметки сохраняются. GUI/Android/Windows/FreeBSD не проверялись;DELETE_ACK fault matrix отдельно не добавлялась. + +12. Завершение аудита контрактов DM — 27.10.2026 + +База заключительного этапа: /home/user/utun4, master, исходный HEAD07f63c3047b4a9bef6c568ca590d3fefde95816f. +Полностью сверены все семь заголовков src/dm/ с реализациями core, crypto, history, +mailbox, mailbox_media и media; doc/dm_arch.md, doc/pm.txt и doc/pm_invites.md. +Это завершение проверки описаний/API, не утверждение об отсутствии всех возможных +ошибок программы. Новых подтверждённых дефектов выполнения в этом этапе нет. + +В заголовках уточнены: +- Заимствование аргументов, один освобождаемый блок запросов UI, жизнь instance, + поток uasync и запрет доступа workers к SQLite/сети. +- Проверка тела/подписи отдельно от области router/ролей, точные повторы и конфликты, + границы транзакций при send/accept, подпись квитанции после commit входящего сообщения. +- Независимые квитанции сообщения и файла; STORED не удаляет исходящий outbox. +- Форматы DM/mailbox/media control, native-endian чисел и big-endian attachment_info, + лимиты send data с descriptor и размеры выходных буферов квитанций. +- Поколения/hidden, сохранение ключей и seq, отпечатки удалённых тел, отмена старых + invite/transfer и отбрасывание завершения worker старого поколения. +- Роли S/M, выбор нового storage без прежних назначений, фиксация назначения до STORE, + local S/M control и отличие передачи пакета от durable приёма. +- File holds, манифест путей, busy=-1 уборщика, повтор удаления и fsync каталога; + пользовательский исходник не передаётся уборке без явного remove_source. +- Запуск mailbox/core/media и остановка quiesce -> media_async drain -> destroy + с живыми SQLite/группами, сохранение данных между stop/restart. + +dm_arch согласован с порциями таймеров32/8, обычным retry5с и отдельными retry3600с/30с, +сохранённым retry_at текстовых очередей и сбросом медиа при restart. Добавлены wire +таблица, порядок владельцев и восстановление оригинала после EXPIRED: срок копии M +не запрещает B получить исходный файл A; поздний EXPIRED не портит готовый файл. +pm.txt дополнен привязкой требований к текущим owners/ролям/настройкам, dm_arch +больше не называет этот аудит частичным. Исторические результаты сборок помечены датой. + +Сверены относящиеся к контрактам callers: chat_service_start/stop, attachment_send, +file_transfer lookup/cancel, Qt DmInviteDialog/MainWindow/MessageList и bridge, +Android JNI/ViewModel/PmInviteDialog, headless DM handlers, регистрация JOIN-ключей +и планирование member_sync после JOIN_READY/принятия участника. +Клиентский код просмотрен для описанных DM-событий/выбора адресата/подготовки файла; +полный аудит Qt/Android и остальных общих модулей этим не заявляется. +pm_invites описывает владение запросом, поколение до регистрации и отсутствие +отката открытой беседы при последующей ошибке; отмена PM не отзывает JOIN-ключ. + +Проверки текущего этапа: +- make -C src -j4 и сборка всех девяти существующих DM-целей: PASS. +- Native test_dm/history/names/load/e2e/invite/media/worker/media_cleanup:9/9 PASS. + test_dm_load:79 проверок; names:68 случаев и273 снимка, независимый Python JSON + parser прочитал каждый полный снимок. Worker:25 запусков,50 heartbeat,max_gap11.3мс. + Media:phase12,6266 heartbeat,allocations0->0; проверены11 retirement SQL faults, + 18 release faults, потерянные control/restart и возврат точного оригинала после TTL. +- Linux cleanup component:13 SQL faults для scan/ACK/body, повтор после restart, + несколько M, потерянный/повторный/чужой ACK и поздний STORED; exit0. + Сеть перехвачена, receipt задан фикстурой; криптографию и физическое удаление M + проверяет отдельный test_dm_media. Одношаговый autocommit может уже завершиться + до искусственного finalize failure; тест проверяет это состояние и идемпотентный повтор. +- Каждый из семи заголовков: C99 и C++20 с -Wall -Wextra -Werror,14/14 PASS. + Для private/media C API C++ использует extern "C", как существующие клиенты; + include path lib/ соответствует проекту и предоставляет sqlite3.h. +- Все C-файлы DM побайтно совпадают с начальным снимком; C-токены всех заголовков + совпадают после исключения комментариев/пробелов. Алгоритмы, API/ABI, wire и SQL + этим этапом не менялись. git diff --check PASS; новые тесты не добавлялись. + +Логи и снимки: /tmp/dm-contract-finish-before/sha256.json, +/tmp/dm-contract-finish-{build,final-core-build,final-tests-build,headers,validation}.log, +/tmp/dm-contract-finish-native-{results,test_dm*}.log; исходные тела/JSON — в test artifacts. +Сетевой профиль выполнен вне sandbox. Первый sandbox-запуск cleanup вернул exit0, +но uasync сообщил Operation not permitted при создании служебного fd; окончательный +повтор вне sandbox прошёл без этих сообщений. Сетевые настройки хоста не менялись. +Новые full check.sh, ASAN/UBSan/LSan, сборки/интерактив Qt/Android/Windows/FreeBSD +не выполнялись; прежние результаты не приписываются заключительному прогону. +Изменения radio и doc/tasks.md сохранены отдельно от DM-коммитов; push не выполнялся. + +13. Rebase перед push — 27.10.2026 + +После отказа push (origin содержал новые коммиты) пользователь запросил rebase. +Получен origin/master492f6adc; все шесть DM-коммитов перенесены на эту базу. +Единственный конфликт — dm_messages_req в dm_core.h: сохранены generation из origin +и проверенные комментарии. Остальные пять патчей перенесены без изменения по range-diff. +Старые хеши и результаты раздела12 относятся к состоянию до переноса. + +В origin появился generation в запросе истории и CHAT_EVT_DM_MESSAGES: +[conv_len:1][conv][generation:8][JSON]. Сверены полный trampoline, проверка current, +новая регрессия test_dm_history и получение снимка Qt/Android. dm_arch уточнён под +этот формат. Все C-файлы DM побайтно совпадают с новым origin; токены семи заголовков +совпадают после исключения комментариев/пробелов. C99/C++20:14/14 PASS. + +Перед rebase сохранены21 незакоммиченных radio-файл, включая untracked, в stash +и отдельной резервной копии с SHA256/режимами. После переноса восстановлены все21: +18 файлов побайтно, doc/tasks.md/tests/Makefile.am/tools/chatgui/CMakeLists.txt +слиты с origin; списки добавленных/удалённых строк radio точно совпали с сохранёнными. +Повторная проверка после сборки также PASS; radio не включается в DM-коммиты. + +Свежий ./build.sh --no-gui -j4:PASS. После пересборки native DM9/9 PASS, +включая скрытые/устаревшие запросы истории и точное поколение опубликованного снимка. +Дополнительно radio/radio_mesh/radio_headless/radio_headless_queue4/4 PASS. +Это профиль13 бинарников, не полный check.sh; новые GUI/Android/sanitizer прогоны +не выполнялись. Радио EOS C++-профиль не повторялся, его исходники сохранены побайтно. +Логи:/tmp/utun4-dm-rebase-{build,results,radio-preservation,test_*}.log. +Резервная копия указана в /tmp/utun4-dm-rebase-backup-path.txt. diff --git a/doc/pm.txt b/doc/pm.txt index 3052a56c..4da0009c 100644 --- a/doc/pm.txt +++ b/doc/pm.txt @@ -1,4 +1,4 @@ -Работа лички +Работа лички — требования и реализация 1. отдели логику лички от логики групповых чатов. не смешивай. @@ -9,4 +9,25 @@ время хранения медиафайлов на промежуточных узлах ограничено - 1 неделя по умолчанию (задачется в конфиге этих узлов). для доставки медиа поднимаем линк (conn_mgr). -и посмотри реализацию медиа для групповых чатов. +Контракты реализации проверены 27.10.2026: src/dm/ и doc/dm_arch.md. + +«Напрямую» означает доставку endpoint через живой маршрут общей CHAT-группы, +в том числе с BGP-транзитом; физический линк между A и B не обязателен. +Беседа имеет собственные историю/seq/outbox и не создаёт отдельную TOPO_GROUP. +Общие с групповыми вложениями примитивы: media_async, шифрование потоков, +attachment_info, voice_file; DM не использует групповой HAVE_BLOCK и его хранилище. + +S выбирается по подписанному adm_tags supernode=yes, M — storage=yes в общей группе A/B. +Роли независимы и могут совмещаться на одном узле. A сохраняет исходящую задачу +до подписанной квитанции B; STORED от S/M не означает доставку получателю. +Текстовая квитанция подтверждает commit сообщения, файловая — проверку ciphertext, +публикацию файла и сохранение результата. S повторяет удаление у M до DELETE_ACK +либо завершения подтверждённого срока назначения; сохраняются служебные tombstones. + +На M настройки чата dm_media_ttl_sec=604800 и dm_media_storage_mb=1024 задают TTL +в секундах и квоту в МиБ. Повтор STORE не продлевает TTL; при нехватке квоты новое +размещение отклоняется, подтверждённая копия не вытесняется до срока. +Текстовые pending не имеют TTL. Истечение копии M не удаляет оригинал A и не мешает +B получить его после возвращения A; локальное удаление истории не отзывает копии S/M. + +Подробные wire-форматы, жизненный цикл и проверки: doc/dm_arch.md и src/dm/*.h. diff --git a/doc/pm_invites.md b/doc/pm_invites.md index 5e9a60c7..29d47f96 100644 --- a/doc/pm_invites.md +++ b/doc/pm_invites.md @@ -40,6 +40,17 @@ ID получателя, выбранную группу и исходную о Повтор во время ожидания возвращает BUSY. Закрытие диалога отменяет только соответствующий `request_id`; уже отправленное сообщение не отзывается. Отмена и остановка DM завершают принятый запрос ровно один раз. +Отмена уже завершённого request_id ничего не отправляет и не создаёт повторный результат. +Отмена касается ожидания PM, уже зарегистрированный JOIN-ключ живёт по своему TTL. + +Запрос — один обнулённый блок `u_calloc(dm_invite_req)` с NUL-строками; +после передачи в uasync им владеет обработчик до финального callback. +Instance, chat_sync и SQLite должны оставаться живыми до завершения отмены. +`dm_core_destroy` отменяет ожидающий invite до освобождения owner. +Поколение беседы захватывается перед регистрацией и проверяется перед `dm_send`: +удалённая и повторно открытая PM не принимает старый результат. +Создание/открытие беседы происходит до регистрации; ошибка регистрации или отправки +не откатывает это открытие, но не создаёт invite-сообщение. `CHAT_EVT_DM_INVITE_RESULT` содержит 36 байт: `[request_id:u64][result:i32][peer:u64][conv_id:u64][generation:u64]` в порядке байтов ядра. @@ -53,6 +64,11 @@ JOIN во вторую группу может использовать уже ## Проверка и диагностика +Контракт сверён 27.10.2026 с DM, регистрацией JOIN-ключей и обработчиками результата +Qt/Android. Свежий `test_dm_invite` прошёл вместе с остальными восемью DM-тестами. +Приведённые ниже GUI-тесты описывают существующее покрытие; в этом этапе новые +сборки Qt/Android и интерактивная проверка интерфейса не выполнялись. + - `tests/test_dm_invite`: настоящая UDP-доставка ссылки в другую группу, JOIN, репликация участника и отправка группового сообщения после вступления; отмена, BUSY, неверная группа, rollback при ошибке commit и остановка DM; diff --git a/src/dm/dm_core.h b/src/dm/dm_core.h index 3bd92a6e..68c5bb9f 100644 --- a/src/dm/dm_core.h +++ b/src/dm/dm_core.h @@ -9,7 +9,11 @@ * при недоступности пира — через dm_mailbox (суперузлы). * - содержимое шифруется E2E (AES-256-CCM), запись подписывается Ed25519. * - * Два независимых направленных потока (я→пир и пир→я) с монотонным seq. + * Два независимых направленных потока (я→пир и пир→я) с монотонным исходящим seq. + * Все API и трамплины выполняются в uasync-потоке с живым instance/SQLite/router. + * Входные строки/тела заимствуются на время вызова; функции не забирают их владение. + * Запросы трамплинов — один блок u_malloc/u_calloc с NUL-строками; обработчик освобождает блок. + * Instance должен пережить запрос и его отложенное завершение; остановка сериализуется через uasync. */ #ifndef DM_CORE_H @@ -29,7 +33,9 @@ extern "C" { /* ── Каноническое тело DM-сообщения (общий wire-формат dm_core и dm_mailbox) ── * [conv_id:8][seq:8][ts:8][author:8][ct_len:1][ct:ct_len][data_len:2][data_enc:data_len][media?:56][sig:64] * media присутствует только для ct="file", подписан вместе с сообщением. - * data_enc — AES-256-CCM (шифротекст + tag), sig — Ed25519 автора по всем полям до sig. */ + * data_enc — AES-256-CCM (шифротекст + tag), sig — Ed25519 автора по всем полям до sig. + * Числа native-endian; seq=1..INT64_MAX, ts — миллисекунды NTP-времени instance. + * ETCP_RT_ID_DM=0x34: [MSG=1][тело] либо [ACK=2][квитанция:136]; router-пакет подписан. */ #define DM_MSG_FIXED_HDR 33 /* conv_id+seq+ts+author(32) + ct_len(1) — до ct */ #define DM_MSG_SIG_SIZE 64 #define DM_DATA_MAX 1024 @@ -54,22 +60,29 @@ static inline int dm_msg_len(const uint8_t* msg, size_t avail, size_t* out_len) return 1; } -/* Общие операции протокола DM; все вызовы в uasync-потоке. - * group_id — только область маршрутизации, идентичность беседы от неё не зависит. - * route_group выбирает общую группу с живым путём, возвращает 1/0. - * verify_message проверяет каноническое тело и подпись автора для recipient. - * accept_message возвращает 0 только после commit (включая точный повтор). - * Квитанция: ["DMACK001":8][conv:8][seq:8][author:8][recipient:8][SHA256(msg):32][sig:64]. - * verify_receipt проверяет подпись получателя и при msg!=NULL точное соответствие телу. - * accept_receipt удаляет только соответствующее исходящее pending-сообщение. */ +/* Выбрать общую CHAT-группу с путём router к peer, сначала preferred (0=без предпочтения). + * 1 и *group_id при успехе; 0 при отсутствии пути/ошибке, выход не меняется. + * group_id обязателен; наличие пути не гарантирует успешной последующей отправки. */ int dm_route_group(struct UTUN_INSTANCE* inst, uint64_t peer, uint64_t preferred, uint64_t* group_id); +/* Проверить точную длину msg/len, conv/author/recipient/seq, descriptor и подпись автора из nodes. + * 0=корректно, -1=отказ. Нужна SQLite, владелец dm_core не требуется. + * Не расшифровывает тело и не проверяет членство/роль router-отправителя: это обязанность вызывающего. */ int dm_verify_message(struct UTUN_INSTANCE* inst, uint64_t recipient, const uint8_t* msg, size_t len); +/* Проверить и расшифровать входящее тело для локального узла, атомарно сохранить историю/беседу/media. + * 0 и receipt после commit либо точного повтора; -1 при отказе. Для file нужен живой dm_media. + * Точный удалённый повтор подтверждается по отпечатку без восстановления истории; конфликт отклоняется. + * receipt — обязательные 136 байт; при -1 буфер не использовать. Ошибка подписи квитанции возможна после commit. */ int dm_accept_message(struct UTUN_INSTANCE* inst, const uint8_t* msg, size_t len, uint8_t receipt[DM_RECEIPT_SIZE]); +/* receipt — ровно 136 доступных байт: ["DMACK001":8][conv:8][seq:8][author:8][recipient:8][SHA256(msg):32][sig:64]. + * Проверяет идентичность и подпись B по первым 72 байтам; при msg!=NULL также точное тело msg/len. + * 0=корректно, -1=отказ. Не проверяет наличие outbox и ничего не записывает. */ int dm_verify_receipt(struct UTUN_INSTANCE* inst, const uint8_t receipt[DM_RECEIPT_SIZE], const uint8_t* msg, size_t len); +/* Для локального автора проверить receipt и удалить только точный outbox; история/file receipt сохраняются. + * 0=удалено либо outbox уже отсутствует, -1=ошибка/конфликт. Требуется запущенный dm_core. */ int dm_accept_receipt(struct UTUN_INSTANCE* inst, const uint8_t receipt[DM_RECEIPT_SIZE]); /* Жизненный цикл в uasync: init после chat_core_init с SQLite/router/ua ядра; 0=успех, -1=ошибка. - * Повторный init запущенного владельца возвращает 0. Для send/accept нужен также dm_media_init. + * Повторный init запущенного владельца возвращает 0. Для file-send/accept нужен также dm_media_init. * destroy отменяет invite/таймер и снимает bind; БД/outbox сохраняются. Вызывать до chat_core_destroy. */ int dm_core_init(struct UTUN_INSTANCE* inst); void dm_core_destroy(struct UTUN_INSTANCE* inst); @@ -99,9 +112,16 @@ int dm_start(struct UTUN_INSTANCE* inst, uint64_t peer_node_id, int dm_send(struct UTUN_INSTANCE* inst, const char* conv_id, const char* content_type, const uint8_t* data, uint32_t data_len); -/* Локальное удаление; generation защищает повторно открытую беседу от старого запроса. */ -struct dm_delete_req { struct UTUN_INSTANCE* inst; char conv_id[64]; uint64_t generation, request_id; }; +/* Локальное удаление по dm_history_delete; generation защищает повторно открытую беседу. + * CHAT_EVT_DM_DELETE_RESULT: [request_id:8][conv:u64][generation:8][success:u8], всего 25 байт native-endian. */ +struct dm_delete_req { + struct UTUN_INSTANCE* inst; /* Заимствованное ядро до завершения запроса. */ + char conv_id[64]; /* Десятичный ID беседы с NUL. */ + uint64_t generation, /* Ненулевое ожидаемое поколение. */ + request_id; /* Корреляция результата с запросом UI. */ +}; void dm_delete_trampoline(void* arg); +/* После commit удаления отменить ожидающий invite этой беседы; квитанции/другие беседы не меняет. */ void dm_cancel_pending_history(struct UTUN_INSTANCE* inst, const char* conv); /* До 200 видимых бесед по last_ts DESC: [{conv_id,peer_node_id,peer_name,last_ts,out_seq,in_seq,generation}]. @@ -113,7 +133,9 @@ int dm_list_conversations_json(struct UTUN_INSTANCE* inst, char* buf, size_t buf * count=1..200 задаёт предел; count<=0 или count>200 означает 200. * offset>=0 отсчитывается от новых сообщений; offset=0 выбирает последние count. * Выбранная страница возвращается по возрастанию ts, seq, dir. - * buf — выходной буфер с NUL; 0 и *out_len байт без NUL при успехе, -1 при ошибке. */ + * buf — выходной буфер с NUL; 0 и *out_len байт без NUL при успехе, -1 при ошибке. + * pending относится к квитанции сообщения. media_status: delivered/ready/downloading/expired/pending. + * hidden здесь не фильтруется; интерфейс выбирает видимую беседу по списку/поколению. */ int dm_list_messages_json(struct UTUN_INSTANCE* inst, const char* conv_id, int count, int offset, char* buf, size_t buf_size, size_t* out_len); @@ -123,20 +145,20 @@ int dm_list_messages_json(struct UTUN_INSTANCE* inst, const char* conv_id, int c * пира в таблице nodes (как headless hc_handle_dm_start), после чего вызывает * dm_start(). Результат — событие CHAT_EVT_DM_CONV_UPDATED. */ struct dm_start_req { - struct UTUN_INSTANCE* inst; - uint64_t peer_node_id; - char source_ch_id[64]; - char peer_name[128]; + struct UTUN_INSTANCE* inst; /* Заимствованное ядро. */ + uint64_t peer_node_id; /* Другой участник общей группы; ключи читаются из nodes. */ + char source_ch_id[64]; /* Десятичный ID общей группы либо пустая строка. */ + char peer_name[128]; /* UTF-8 имя с NUL; dm_start ограничит до 63 байт. */ }; void dm_start_trampoline(void* arg); /* Отправить текстовое сообщение в беседу (content_type="text"). */ struct dm_send_req { - struct UTUN_INSTANCE* inst; - char conv_id[64]; + struct UTUN_INSTANCE* inst; /* Заимствованное ядро. */ + char conv_id[64]; /* Десятичный ID беседы с NUL. */ uint64_t generation; /* 0: захватить при входе в uasync, до любой фоновой работы. */ - uint32_t data_len; - uint8_t* data; /* data_len байт (выделяется в хвосте структуры) */ + uint32_t data_len; /* Число байт текста, до DM_DATA_MAX. */ + uint8_t* data; /* Указывает на data_len байт в том же блоке после структуры. */ }; void dm_send_trampoline(void* arg); @@ -151,12 +173,15 @@ enum dm_invite_result { DM_INVITE_SEND_ERROR = -3, DM_INVITE_CANCELLED = -4, DM_INVITE_BUSY = -5 }; struct dm_invite_req { - struct UTUN_INSTANCE* inst; - uint64_t request_id, peer_node_id, channel_id, generation; + struct UTUN_INSTANCE* inst; /* Заимствованное ядро до итогового callback. */ + uint64_t request_id, /* Ненулевой ID; cancel использует тот же ID. */ + peer_node_id, /* Фиксированный получатель, не локальный узел. */ + channel_id, /* Выбранная активная CHAT-группа отправителя. */ + generation; /* Заполняет uasync до регистрации; проверяет callback. */ char source_ch_id[64]; /* общая группа для новой PM; из шапки PM может быть пустой */ - char peer_name[128]; - int cancel; - struct chat_invite_build* pending; /* только uasync */ + char peer_name[128]; /* UTF-8 имя для новой беседы с NUL. */ + int cancel; /* 0=создать, 1=отменить ожидающий request_id. */ + struct chat_invite_build* pending; /* Внутренний handle; запрос создаётся обнулённым. */ }; void dm_invite_trampoline(void* arg); @@ -164,15 +189,18 @@ void dm_invite_trampoline(void* arg); * [conv_id_len:1][conv_id:var][generation:8][JSON:utf8]. Все чтения в uasync. * Ненулевое generation проверяется до чтения; 0 захватывает текущее поколение. GUI отбрасывает старый снимок. */ struct dm_messages_req { - struct UTUN_INSTANCE* inst; - char conv_id[64]; + struct UTUN_INSTANCE* inst; /* Заимствованное ядро. */ + char conv_id[64]; /* Десятичный ID с NUL, также для запроса маршрута звонка. */ uint64_t generation; /* Поколение выбранного PM; для запроса маршрута не используется. */ }; void dm_messages_trampoline(void* arg); -/* Живой BGP-маршрут PM; group_id=0 в снимке означает недоступность. Все вызовы в uasync. */ +/* 1 при живом BGP-маршруте общей группы, 0 при недоступности/ошибке. peer/group_id обязательны. + * При корректных аргументах выходы сначала обнуляются; peer может быть известен и без маршрута. + * Трамплин выдаёт CHAT_EVT_DM_CALL_ROUTE: [conv_len:1][conv][peer:8][group:8]; group=0 без маршрута. */ int dm_call_route(struct UTUN_INSTANCE* inst, const char* conv_id, uint64_t* peer, uint64_t* group_id); void dm_call_route_trampoline(void* arg); /* аргумент: dm_messages_req */ -struct dm_conversations_req { struct UTUN_INSTANCE* inst; }; +/* Снимок бесед публикуется как CHAT_EVT_DM_CONVERSATIONS с JSON без NUL. */ +struct dm_conversations_req { struct UTUN_INSTANCE* inst; /* Заимствованное ядро. */ }; void dm_conversations_trampoline(void* arg); #ifdef __cplusplus diff --git a/src/dm/dm_history.h b/src/dm/dm_history.h index 22eb4212..9c906de5 100644 --- a/src/dm/dm_history.h +++ b/src/dm/dm_history.h @@ -1,7 +1,9 @@ /* Локальная история DM: поколения, скрытые беседы и точные отпечатки удалённых сообщений. * Все операции выполняются в uasync с SQLite ядра. init вызывается после создания DM-таблиц. * Удаление скрывает беседу и сохраняет её ключи/счётчики в dm_conversations; show открывает её снова. - * Поколение передавать в отложенные операции и проверять current перед применением результата. */ + * Поколение передавать в отложенные операции и проверять current перед применением результата. + * Беседой/seq владеет dm_core, файлами/загрузками — dm_media; этот модуль связывает их удаление. + * Аргументы заимствованы на время вызова; самостоятельные транзакции выполняются без внешней транзакции. */ #ifndef DM_HISTORY_H #define DM_HISTORY_H #include @@ -10,7 +12,8 @@ extern "C" { #endif struct UTUN_INSTANCE; -/* Создать служебные таблицы, сбросить busy файлов после restart. 0=успех, -1=ошибка. +/* Создать dm_history, dm_deleted_messages и dm_local_files; сбросить busy после restart. + * 0=успех, -1=ошибка. Вызывает dm_core_init; повторять при живых workers нельзя: они ещё держат пути. * inst и его topo_sqlite_db обязательны; частично созданные таблицы при ошибке остаются. */ int dm_history_init(struct UTUN_INSTANCE* inst); /* Поколение существующей беседы: 1 до первого удаления; 0 при отсутствии/ошибке. @@ -28,8 +31,11 @@ int dm_history_show(struct UTUN_INSTANCE* inst, const char* conv); int dm_history_deleted(struct UTUN_INSTANCE* inst, const uint8_t* body, size_t len, int dir, uint8_t* media_receipt); /* Удалить видимую беседу текущего generation