# Личные сообщения (DM) — спецификация v1 ## Статус документа Этот файл — актуальная логическая спецификация DM-протокола SHiNE v1. Важно: - это актуальная реализованная спецификация; - код и документы по DM должны изменяться синхронно; - при любых будущих изменениях DM сначала обновляется эта спецификация и соседний документ формата. Документ фиксирует: - модель DM и смысл типов сообщений; - правила редактирования, перешифровки и удаления; - роли существующих API-методов; - правила межсерверной маршрутизации через `access_servers`; - общее поведение сервера и БД. Точный байтовый формат контейнера вынесен отдельно: - `Dev_Docs/Personal_Messages/Формат_DM_v1.md` Устаревшая предыдущая версия сохранена отдельно: - `Dev_Docs/Personal_Messages/Спецификация_DM_v0.5_устаревшая.md` ## 1. Основная модель Личное сообщение в SHiNE хранится как две логические копии одного сообщения: - `type=1` — входящее сообщение для получателя; - `type=2` — исходящая копия сообщения для отправителя. Обе копии имеют общий логический идентификатор: - `baseKey = fromLogin|toLogin|timeMs|nonce` Идентификатор конкретной копии: - `messageKey = baseKey|messageType` Поля `timeMs` и `nonce` не меняются никогда. `nonce` обязателен, потому что одного `timeMs` недостаточно для гарантированной уникальности. ## 2. Ключи и шифрование ### 2.1. Общий принцип У пользователя в PDA публикуется один публичный `clientKey` в формате `Ed25519`. Из того же ключевого материала стандартным способом выводится ключ `X25519` для E2EE-шифрования DM. Отдельный публичный `dmEncKey` в текущей архитектуре не хранится. Подробности стандартного преобразования: - `Dev_Docs/Протоколы/Преобразование_ED25519_в_X25519.md` ### 2.2. Правило шифрования копий - `type=1` шифруется на ключ получателя; - `type=2` шифруется на ключ отправителя. Следствия: - входящую копию может прочитать только получатель; - исходящую копию может прочитать только отправитель; - сервер не должен расшифровывать DM; - ciphertext у `type=1` и `type=2` обычно разный и не обязан совпадать побайтно. ### 2.3. Источник истины по пользователю Для DM-проверки сервер использует: - локальную `solana_users` как кэш; - Solana PDA как источник истины по `clientKey` и `access_servers`. Если нужного пользователя нет локально, сервер обязан попытаться lazy-import из Solana PDA: - в `GetUser`; - при серверной верификации DM-подписи; - перед межсерверной маршрутизацией через `access_servers`. ## 3. Типы сообщений В версии v1 используются следующие `messageType`: - `1` — входящее сообщение; - `2` — исходящая копия сообщения; - `3` — входящее подтверждение прочтения; - `4` — исходящая копия подтверждения прочтения; - `5` — сообщение удалено отправителем; - `6` — сообщение удалено получателем; - `7` — переписка удалена отправителем; - `8` — переписка удалена получателем. Типы `1/2` — это обычные контентные DM, в том числе их последующие ревизии. Типы `3/4` — служебные подтверждения прочтения. Типы `5/6/7/8` — служебные события удаления без зашифрованного тела. ## 4. Базовые операции ### 4.1. Создание нового сообщения Новое сообщение отправляется парой блоков через: - `SendMessagePair` - `ReceiveOutcomingMessage` как алиас Пару создаёт автор сообщения. Пара содержит: - одну входящую копию `type=1`; - одну исходящую копию `type=2`; - одинаковые `fromLogin`, `toLogin`, `timeMs`, `nonce`; - одинаковый логический plaintext; - разные ciphertext для разных владельцев копий. ### 4.2. Редактирование сообщения Редактирование общего текста делает автор сообщения. При редактировании: - `baseKey` остаётся тем же; - `messageType` остаётся тем же; - `revisionTimeMs` увеличивается; - автор заново шифрует обе копии и снова отправляет пару через `SendMessagePair`. Если `revisionTimeMs = 0`, это исходная версия. Если `revisionTimeMs > 0`, это новая ревизия сообщения. ### 4.3. Перешифровка Перешифровка нужна для будущего сценария полной или частичной перепаковки истории сообщений при сохранении тех же логических идентификаторов сообщений. При перешифровке: - `baseKey` остаётся прежним; - `messageKey` остаётся прежним; - plaintext может остаться тем же; - ciphertext меняется; - `reencryptedAtMs` получает ненулевое значение; - `revisionTimeMs` может остаться прежним, в том числе нулевым, если менялось только шифрование без редактирования текста; - сервер при выборе актуальной версии обязан учитывать обе метки времени. Если сообщение уже удалено одним из типов `5/6`, его перешифровывать больше нельзя. Если переписка уже удалена типом `7/8`, более старые сообщения этой пары тоже перешифровывать нельзя. ### 4.4. Приём входящей копии Для server-to-server доставки одной входящей копии используется: - `ReceiveIncomingMessage` Этот метод должен принимать: - новые входящие сообщения; - входящие обновления/редактирования; - будущие входящие перешифрованные копии. ## 5. Удаление одного сообщения ### 5.1. Общая логика Удаление одного сообщения в v1 всегда глобальное у обеих сторон. Локального удаления только у себя в этой версии протокола не вводится. ### 5.2. Типы удаления - `type=5` — сообщение удалено отправителем; - `type=6` — сообщение удалено получателем. Удаляющее сообщение: - не содержит зашифрованного тела; - хранится в БД как tombstone; - терминально закрывает это сообщение; - не даёт больше принять никакую более позднюю содержательную версию этого же `messageKey`. ### 5.3. Метод удаления Для удаления одного сообщения нужен отдельный метод, условно: - `DeleteMessage` Если сервер впервые получает валидное удаляющее сообщение: - сохраняет tombstone в БД; - удаляет или замещает прежнюю версию сообщения tombstone-записью; - распространяет это же удаление на серверы доступа обеих сторон; - не принимает в будущем попытки "оживить" это сообщение. ## 6. Удаление всей переписки ### 6.1. Отдельное служебное сообщение Удаление всей переписки между двумя пользователями — это отдельное служебное сообщение без ciphertext. Типы: - `type=7` — переписка удалена отправителем; - `type=8` — переписка удалена получателем. ### 6.2. Граница удаления Границей удаления считается: - `timeMs` самого служебного сообщения удаления переписки Отдельное `deleteBeforeTimeMs` в этой версии не вводится. ### 6.3. Правило применения Если сервер получает такое сообщение впервые: - сохраняет его в БД как tombstone переписки; - удаляет из БД все сообщения этой пары пользователей с `timeMs` меньше времени служебного сообщения; - больше не принимает новые или повторно доставленные сообщения с `timeMs` раньше этой границы; - распространяет это же сообщение удаления переписки на серверы доступа обеих сторон. ### 6.4. Поведение при позднем старом сообщении Если после удаления переписки приходит старое сообщение, у которого: - `timeMs < deleteConversationMessage.timeMs` то сервер: - не принимает это сообщение; - распространяет уже известный tombstone удаления переписки на серверы доступа обеих сторон; - ожидает, что вторая сторона обработает его как обычное уже известное удаление переписки. ### 6.5. Будущие новые сообщения После удаления всей переписки новые сообщения между этими же пользователями разрешены, если: - их `timeMs` больше времени служебного сообщения удаления переписки. ## 7. Серверы и маршрутизация ### 7.1. `access_servers` Для обычного пользователя список серверов доставки и доступа задаётся через: - `access_servers` Это: - сервера доступа пользователя; - сервера relay; - сервера, через которые пользователь получает личные сообщения и другие пользовательские операции. ### 7.2. `sync_servers` `sync_servers` относятся не к обычной пользовательской маршрутизации DM, а к server-to-server партнёрству серверного узла. Они используются для: - синхронизации серверных данных; - синхронизации пользовательских блокчейнов SHiNE; - общей межсерверной координации. `sync_servers` не являются списком пользовательских серверов доставки DM. ### 7.3. Несколько серверов у отправителя и получателя Протокол должен поддерживать ситуацию, когда: - у отправителя несколько `access_servers`; - у получателя несколько `access_servers`; - часть серверов у сторон совпадает; - часть серверов уникальна. Из этого следуют требования: - все DM-операции должны быть идемпотентны; - повторное получение уже известного события не должно ломать состояние; - дубль tombstone должен быть безопасен; - сервер не должен "оживлять" более старую версию сообщения после уже принятого tombstone. ## 8. Методы и их роли ### 8.1. Существующие методы, которые сохраняются - `SendMessagePair` - `ReceiveOutcomingMessage` - `ReceiveIncomingMessage` - `DeleteMessage` - `DeleteConversation` Их роли в v1: - `SendMessagePair` — новая пара сообщений и редактирование старой пары автором; - `ReceiveIncomingMessage` — приём одной входящей копии, входящих редактирований и входящего read-receipt; - `ReceiveOutcomingMessage` — алиас `SendMessagePair`. ### 8.2. Новые методы, которые нужны Отдельный legacy-метод `SendDirectMessage` в DM v1 не используется и должен оставаться отключённым, чтобы не было параллельного старого стека доставки. ## 9. Правила валидации и применения ### 9.1. Общее правило по ревизиям Для одного и того же контентного сообщения сервер сравнивает пару: - `revisionTimeMs`; - `reencryptedAtMs`. Правило: - если новый `revisionTimeMs` больше сохранённого, сообщение применяется; - если `revisionTimeMs` равен, но новый `reencryptedAtMs` больше сохранённого, сообщение применяется; - если обе величины равны, сообщение не применяется; - если новая пара (`revisionTimeMs`, `reencryptedAtMs`) меньше или равна сохранённой, сообщение не применяется. Содержимое `body` при этом сравнении не участвует. То есть если сервер уже видел ту же пару (`revisionTimeMs`, `reencryptedAtMs`), он считает, что такая версия у него уже есть. ### 9.2. Повторный tombstone Повторный tombstone означает ситуацию, когда сервер повторно получает то же самое событие удаления: - того же сообщения; - или той же переписки. Это может произойти из-за: - повторной межсерверной доставки; - нескольких `access_servers`; - сетевых retry; - дублирующей пересылки с разных маршрутов. Правило: - повторный tombstone должен быть полностью безопасен; - если соответствующее удаление уже сохранено, сервер ничего не меняет и просто игнорирует повтор. ### 9.3. Сообщения старше границы удалённой переписки Если для пары пользователей уже есть сохранённая граница удаления переписки, и приходит: - обычное сообщение; - удаление одного сообщения; - повторное удаление переписки; - любое другое DM-событие; у которого `timeMs` меньше этой границы, сервер: - ничего не меняет в БД; - не восстанавливает старую историю; - не применяет это событие повторно. Если это старое контентное сообщение или входящая копия, сервер дополнительно перерассылает уже известный tombstone удаления переписки на `access_servers` обеих сторон, чтобы отстающие серверы сами подчистили историю. Такие сообщения считаются частью уже удалённой истории. ### 9.4. Приоритет удаления переписки Если сначала пришло удаление переписки, а потом удаление одного старого сообщения из этой переписки, сервер должен: - проигнорировать это удаление одного сообщения; - не создавать новых изменений поверх уже удалённой истории. То же правило действует и для обычных сообщений, и для редактирований старых сообщений. ## 10. JSON API v1 ### 10.1. `SendMessagePair` Назначение: - клиент отправляет новый DM; - клиент отправляет редактирование старого DM; - клиент отправляет парную новую ревизию типов `1/2`. Request: ```json { "op": "SendMessagePair", "requestId": "req-123", "payload": { "incomingBlobB64": "...", "outgoingBlobB64": "..." } } ``` Правила: - клиенту достаточно отправить пару на один любой доступный сервер; - сервер после принятия сам отвечает за дальнейшую межсерверную доставку. ### 10.2. `ReceiveIncomingMessage` Назначение: - приём одной входящей копии по схеме server-to-server; - приём входящего редактирования; - приём входящего read-receipt. Request: ```json { "op": "ReceiveIncomingMessage", "requestId": "req-456", "payload": { "incomingBlobB64": "..." } } ``` ### 10.3. `DeleteMessage` Назначение: - удалить одно сообщение у обеих сторон. Request: ```json { "op": "DeleteMessage", "requestId": "req-789", "payload": { "blobB64": "..." } } ``` Ожидается контейнер типа: - `5`, если удаление инициировал отправитель; - `6`, если удаление инициировал получатель. ### 10.4. `DeleteConversation` Назначение: - удалить всю переписку до времени самого служебного сообщения. Request: ```json { "op": "DeleteConversation", "requestId": "req-790", "payload": { "blobB64": "..." } } ``` Ожидается контейнер типа: - `7`, если удаление инициировал отправитель; - `8`, если удаление инициировал получатель. ## 11. Межсерверная доставка ### 11.1. Клиентская сторона Клиенту достаточно отправить сообщение на: - любой один доступный сервер. ### 11.2. Серверная сторона После принятия валидного события сервер должен отправлять его: - на все серверы из `access_servers` отправителя; - на все серверы из `access_servers` получателя. Если часть серверов совпадает, это допустимо. Если один и тот же сервер присутствует у обеих сторон, он не должен слать сообщение сам себе повторно, но обязан локально сохранить событие и доставить его в нужные пользовательские сессии. Идемпотентность обязательна. ### 11.3. Ошибки доставки Если часть серверов временно недоступна: - это не должно отменять локальное принятие уже валидного сообщения; - повторная доставка может делаться отдельным retry-механизмом; - повторное получение того же события должно быть безопасным. ## 12. Хранение в БД Основная таблица остаётся: - `signed_messages_v2` В ней должны сохраняться: - обычные контентные DM; - tombstone одного сообщения; - tombstone удаления переписки. Сообщение об удалении одного сообщения хранится в БД и не удаляется физически, чтобы: - защищать от повторного приёма старых версий; - не терять факт удаления; - корректно синхронизировать событие между серверами. Сообщение об удалении переписки тоже хранится в БД, а старые сообщения до его времени из БД удаляются. ## 13. Что обязательно должно измениться в коде относительно v0.5 - сервер не должен требовать одинаковый `encryptedBody` у `type=1` и `type=2`; - сервер не должен трактовать `encryptedBody` как обычный UTF-8 текст; - DM должны реально шифроваться end-to-end; - удаление одного сообщения должно стать терминальным tombstone; - удаление всей переписки должно стать отдельным служебным событием; - межсерверная маршрутизация DM должна идти через `access_servers`; - сервер должен добирать отсутствующих пользователей из Solana PDA до проверки подписи DM; - при выборе актуальной версии должен учитываться `reencryptedAtMs`, если `revisionTimeMs` совпадает; - legacy `SendDirectMessage` должен быть отключён; - логика должна быть безопасна для нескольких серверов у каждой стороны. ## 14. Что в v1 пока не входит - вложения в DM; - хранение отдельного `keyId` шифрования в DM; - ротация `clientKey`; - финальная конкретная UI-реализация массовой перешифровки; - физическая полная реализация DM federation в текущем коде.