docs(dm): reconcile architecture and PM delivery requirements
This commit is contained in:
1 parent
ad577318d9
commit
a00db6229f
3 files changed
+94
-18
No files matched your search
+65
-10
@@ -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
@@ -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
@@ -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. */
|
||||
|
||||
Reference in new issue
Block a user