From 43c89ca1ed5747eb96eedc470c2f09d264f0145f Mon Sep 17 00:00:00 2001 From: evgeny Date: Thu, 1 Oct 2026 14:46:24 +0300 Subject: [PATCH] Document implemented DM guarantees and remaining media integration --- doc/dm_arch.md | 234 +++++++++++++++++++++++++++++------------------- tests/test_dm.c | 1 + 2 files changed, 142 insertions(+), 93 deletions(-) diff --git a/doc/dm_arch.md b/doc/dm_arch.md index 1cfc3996..e818eb24 100644 --- a/doc/dm_arch.md +++ b/doc/dm_arch.md @@ -1,93 +1,141 @@ -# DM — прямой p2p чат между двумя пользователями группы - -## 1. Цель - -Возможность начать личный чат с любым пользователем доступных групп. -Пока target offline сообщения временно хранятся на storage-узлах. - -## 2. Ключевые решения - -- Отдельная DM-подсистема (НЕ канал: без TOPO_GROUP/member_sync/merkle). -- Идентичность детерминированная: одна ветка на пару, согласование без handshake. -- E2E-шифрование содержимого сразу. -- Offline-хранение: dm_mailbox на storage-узлах с флагом dm_storage. -- Доставка: fallback — сперва проверка наличия узла в BGP-группе; доступен → - прямой send; нет связи → storage. - -## 3. Идентичность и ключи (детерминированные) - - conv_id = SHA256("utun_dm_v1" || min(node_a,node_b) || max(node_a,node_b))[..63] - shared = X25519(my_x25519_priv, peer_x25519_pub) - content_key = SHA256(shared || "utun_dm_content") - -Обе стороны выводят одинаковые значения из своего privkey и pubkey пира -(известен из общей группы). Смена x25519 у пира = новая ветка. - -## 4. Модель данных (SQLite, общий chats.db) - - dm_conversations(conv_id TEXT PK, peer_node_id, peer_x25519 BLOB, peer_ed25519 BLOB, - peer_name TEXT, last_out_seq INT, last_in_seq INT, created_at INT, last_ts INT) - dm_messages(conv_id TEXT, dir INT, seq INT, ts INT, author INT, ct TEXT, - data BLOB, sig BLOB, PRIMARY KEY(conv_id,dir,seq)) - dm_mail(recipient INT, sender INT, conv_id TEXT, seq INT, ts INT, data BLOB, sig BLOB, - ttl INT, PRIMARY KEY(recipient,sender,seq)) - -Два независимых направленных потока (A→B, B→A) с монотонным seq. -Без chain-хеша: для 2 пиров достаточно seq + catch-up по диапазону. - -## 5. Доставка - -1. Проверка доступности: peer присутствует в BGP-группе (общая группа). -2. Доступен → прямой send (сервис ETCP_RT_ID_DM = 0x34), ждём DM_ACK{conv_id,seq}. - Нет ACK за таймаут → fallback в mailbox. -3. Нет связи → MAIL_PUT на 1..N storage-узлов. -4. Catch-up при connect: DM_HELLO{conv_id,last_out_seq,last_in_seq} → пир досылает - (in_seq+1 .. out_seq). -5. Dedup: вставка только при seq > last_in_seq. - -## 6. E2E-шифрование - - data_enc = nonce(13B, счётчик по seq) || AES-CCM_ct || tag - -secure_channel уже умеет AES-CCM. Storage/relay видят только шифротекст. - -## 7. Storage-узлы (dm_storage) - -- Новый флаг dm_storage=yes в adm_tags (блок B владельца), распространяется - существующим member_sync без нового синка. -- DM читает adm_tags мемберов общих групп (peers_) и кэширует storage-узлы. -- Протокол mailbox (сервис ETCP_RT_ID_DM_MAILBOX = 0x35): - - MAIL_PUT {recipient,sender,conv_id,seq,ts,data,sig} - MAIL_PULL {recipient, per-sender cursor} - MAIL_ACK {up_to_seq} - - TTL 7 дней + лимит объёма, dedup по (recipient,sender,seq). - -## 8. Старт и anti-spam - -- dm_start(peer): создать беседу (вывести conv_id/ключи), обеспечить прямое - соединение (переиспользовать chat_sync_connect_node/node_conn_direct). -- Пир автопринимает на первом DM_HELLO/сообщении, только если отправитель в - общей группе. - -## 9. Файлы и интеграция - -| Файл | Назначение | -|------|-----------| -| src/dm/dm_core.c/h | беседы, send/recv, таблицы, bind 0x34, conn-события, fallback | -| src/dm/dm_crypto.c/h | derive + AES-CCM | -| src/dm/dm_mailbox.c/h | storage-узел, push/pull/ack, bind 0x35 | - -Плюс: - -- src/chat/chat_member.c — бит CHAT_MEMBER_FLAG_DM_STORAGE + парсинг dm_storage. -- utun_instance.c — dm_core_init/destroy. -- chat_core — API dm_start/send/list + события CHAT_EVT_DM_*. -- headless CLI — dm list/send/messages. -- src/Makefile.am — новые источники. - -## 10. Сервисы ETCP - - ETCP_RT_ID_DM = 0x34 - ETCP_RT_ID_DM_MAILBOX = 0x35 +# DM: доставка сообщений и подготовка медиа + +Личные беседы принадлежат `src/dm/`. Общие CHAT-группы используются для знакомства, +проверки ролей и маршрутизации; группа не становится идентичностью беседы. + +## Реализовано + +- История и постоянная исходящая очередь сообщений в общей SQLite ядра. +- Доставка через живой маршрут router, включая BGP-транзит. +- При отсутствии маршрута — подписанный PUT доступному суперузлу общей группы сторон. +- Приём после проверки подписи и расшифровки, подтверждение только после commit. +- Доставка с суперузла после возвращения получателя, независимо от отправителя. +- Точное удаление промежуточного сообщения и сохранение квитанции для поздних повторов. +- Шифрование медиа порциями в worker; сетевой жизненный цикл медиа пока не подключён. + +## Идентичность и шифрование + +```text +conv_id = SHA256("utun_dm_v1" || min(node_a,node_b) || max(node_a,node_b))[..63] +content_key = SHA256(X25519(my_private, target_public) || "utun_dm_content") +msg_nonce = SHA256(conv_id || author || seq)[..13] +media_nonce = SHA256("utun_dm_media" || author || media_id || part)[..13] +``` + +Сообщения и медиа используют один content_key target, без ключей отдельных файлов. +AES-256-CCM: nonce 13 байт, tag 16 байт. Максимальная порция plaintext — 65535 байт; +порция медиа — 32768 байт. UUID медиа новый для каждой подготовки. Повтор доставки +использует неизменяемый ciphertext. Повторная подготовка после ошибки требует нового UUID. + +Каноническое тело сообщения: + +```text +[conv:8][seq:8][ts:8][author:8][ct_len:1][ct][enc_len:2][ciphertext+tag][author_sig:64] +``` + +Автор подписывает все поля до sig. conv связывает автора с получателем. +Размер plaintext сообщения ограничен 1024 байтами. seq — положительное int64. + +Медиа ciphertext — последовательность порций с отдельным tag каждой порции. +Последняя порция короче; пустой файл содержит один tag. Размер plaintext и SHA256 +ciphertext должны входить в подписанный manifest. Потоковая расшифровка проверяет все +tags, точный размер и хеш; владелец публикует временный файл только после успеха +и собственного durable сохранения. + +## Владение и данные + +- `dm_core`: беседы, история, outbox и подтверждение получателя. +- `dm_mailbox`: промежуточные сообщения суперузла и квитанции. +- `dm_crypto`: вывод ключей, nonce, CCM и файловые примитивы без БД/сети. + +```text +dm_conversations(conv_id, peer_node_id, peer_x25519, peer_ed25519, peer_name, + group_id, last_out_seq, last_in_seq, created_at, last_ts) +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) +``` + +Запись исходящего сообщения, увеличение seq и запись неизменяемого body в outbox — +одна транзакция. При ошибке откатывается всё, сообщение не выходит в сеть. +Приём, создание/обновление беседы и история — также одна транзакция. + +Dedup — точный ключ (conv,dir,seq) с проверкой сохранённой подписи. last_in_seq — +статистика максимального номера, не курсор подтверждения: seq=99 после seq=100 +сохраняется. Тело с другой подписью для уже сохранённого seq отклоняется. + +У каждой локальной очереди таймер 1с; порция обработки до 32 задач, повтор задачи +через 5с. retry_at предотвращает голодание хвоста очереди. Перед отправкой проверяется +backpressure router. Потеря пути сохраняет durable задачу; остановка отменяет таймеры, +но сохраняет БД. При старте очередь продолжает работу. + +## Подтверждения и протокол + +Сервис `ETCP_RT_ID_DM=0x34`: MSG=1, RECEIPT=2. + +```text +["DMACK001":8][conv:8][seq:8][author:8][recipient:8][SHA256(message):32][recipient_sig:64] +``` + +Квитанцию создаёт только получатель после commit. Подпись охватывает первые 72 байта. +Отправитель удаляет outbox только после проверки подписи и точного хеша тела. +История беседы сохраняется. Подтверждение транспорта router не заменяет квитанцию. + +Сервис `ETCP_RT_ID_DM_MAILBOX=0x35`: + +```text +PUT=1 [recipient:8][message] +DELIVER=2 [message] +RECEIPT=3 [receipt:136] +STORED=4 [conv:8][seq:8] +``` + +Все пакеты идут с подписью router; unsigned control отклоняется. +PUT принимается только от автора, при роли supernode=yes у локального узла +в общей группе автора и получателя. Роль берётся из подписанного adm_tags; +node_type=4 — производная сетевой классификации, для авторизации не используется. +storage=yes само по себе не даёт права хранить сообщения. + +STORED подтверждает durable запись суперузлом, outbox до квитанции получателя остаётся +у автора. Суперузел повторяет DELIVER при появлении живого маршрута. RECEIPT проверяется +по точному сохранённому телу; запись квитанции и удаление копии — одна транзакция. +Квитанция пересылается автору. Поздний PUT возвращает её и не возрождает задачу. + +dm_mail_receipts содержит только служебные квитанции; доставленное содержимое +с промежуточного узла удаляется. Ранее использовавшийся dm_mail/PULL/HELLO и +диапазонные ACK не поддерживаются: это devel, совместимость не сохраняется. +TTL текстовых сообщений не введён; недельный TTL из pm.txt относится к медиа. + +## Следующая интеграция медиа по pm.txt + +1. Общая передача байтов: явные peer/group/file/size, backpressure и CM handle + на всю активную операцию. Владение handle сохраняется после DOWN/TIMEOUT. + Передавать через собственный svc router; conn_mgr_send использует управляющий сервис CM. +2. Отдельный owner dm_media: подготовка ciphertext, manifest, отправка/скачивание, + временные файлы, TTL и лимит хранения. Каталоги dm_media/ и dm_pending/ вне + группового обхода очистки orphan-файлов; групповые объявления HAVE_BLOCK не используются. +3. Суперузел сначала durable регистрирует сообщение и media job, затем выбирает M. + M скачивает ciphertext у A и сообщает S подтверждённое хранение, место и expiry. +4. Текстовая квитанция и квитанция медиа независимы. Получатель подтверждает медиа + после проверки, публикации файла и commit. S хранит задачу удаления у M до DELETE_ACK; + поздний STORED не возрождает завершённую доставку. +5. TTL медиа по умолчанию 7 дней, конфиг на M. Новая custody отклоняется при + недостатке квоты; уже подтверждённые копии не вытесняются молча раньше срока. +6. Stop снимает новые задачи, ждёт media_async с живыми owner-контекстами, затем + закрывает операции/handles до уничтожения групп. + +## Проверки + +`test_dm`: симметрия ключа target, CCM round-trip, пустое тело, пределы длины, +неверный ключ/tag, разделение nonce; потоковые файлы на границах 32 КиБ, +неверные author/UUID/hash, обрезание и лишние байты. + +`test_dm_e2e`: реальные A–C–B соединения и BGP; нет физического A–B линка. +Проверяет out-of-order, dedup/conflict, подписи, SQL rollback при приёме и enqueue, +ACK чужого тела, отказ mailbox без роли, точное удаление и поздний PUT. +Первый сценарий перезапускает источник с pending outbox; второй перезапускает +суперузел и доставляет первое сообщение при выключенном источнике. + +Сетевые тесты CM-передачи медиа, media custody/TTL/quota и независимых квитанций +нужно добавить вместе с сетевой интеграцией медиа. diff --git a/tests/test_dm.c b/tests/test_dm.c index d09fb606..d93f71f3 100644 --- a/tests/test_dm.c +++ b/tests/test_dm.c @@ -30,6 +30,7 @@ static int test_media_stream(const uint8_t key1[32], const uint8_t key2[32], uin return 1; } uint8_t pattern[4096], actual[4096], id[16] = {1}, hash[32]; + memcpy(id + 2, &size, sizeof(size)); /* Разные файлы получают разные ID, даже в тесте. */ for (size_t i = 0; i < sizeof(pattern); i++) pattern[i] = (uint8_t)(i * 17 + 3); int failed = 0; size_t remaining = size;