docs(dm): reconcile architecture and PM delivery requirements

This commit is contained in:
evgeny committed 2026-10-27 01:06:45 +01:00
1 parent ad577318d9
commit a00db6229f
3 files changed
+94 -18

No files matched your search

+65 -10
View File
@@ -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,14 +218,39 @@ 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.
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 + JSON сообщений,
45 — JSON бесед. Снимки ограничены 200 записями. Отправка вложения вызывает
`dm_send_file` в uasync; длительная обработка выполняется в `media_async`.
@@ -239,14 +282,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,6 +323,10 @@ 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 продолжает работать.
@@ -289,6 +338,12 @@ Destroy отменяет уже posted completion, callback вызывается
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` — 8/8 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`.
+23 -2
View File
@@ -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.
+6 -6
View File
@@ -117,8 +117,8 @@ int dm_send(struct UTUN_INSTANCE* inst, const char* conv_id, const char* content
struct dm_delete_req {
struct UTUN_INSTANCE* inst; /* Заимствованное ядро до завершения запроса. */
char conv_id[64]; /* Десятичный ID беседы с NUL. */
uint64_t generation; /* Ненулевое ожидаемое поколение. */
uint64_t request_id; /* Корреляция результата с запросом UI. */
uint64_t generation, /* Ненулевое ожидаемое поколение. */
request_id; /* Корреляция результата с запросом UI. */
};
void dm_delete_trampoline(void* arg);
/* После commit удаления отменить ожидающий invite этой беседы; квитанции/другие беседы не меняет. */
@@ -174,10 +174,10 @@ enum dm_invite_result {
};
struct dm_invite_req {
struct UTUN_INSTANCE* inst; /* Заимствованное ядро до итогового callback. */
uint64_t request_id; /* Ненулевой ID; cancel использует тот же ID. */
uint64_t peer_node_id; /* Фиксированный получатель, не локальный узел. */
uint64_t channel_id; /* Выбранная активная CHAT-группа отправителя. */
uint64_t generation; /* Заполняет uasync до регистрации; проверяет 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]; /* UTF-8 имя для новой беседы с NUL. */
int cancel; /* 0=создать, 1=отменить ожидающий request_id. */