diff --git a/src/chat/db_sync_arch.md b/src/chat/db_sync_arch.md index 0ee8d2e1..fd72b043 100644 --- a/src/chat/db_sync_arch.md +++ b/src/chat/db_sync_arch.md @@ -8,18 +8,32 @@ ## Идентичность и корень -Подпись Ed25519 покрывает `timestamp[8 в порядке хоста] || точные байты data`. -Тело имеет длину 1–4000 байт, подпись — 64 байта. Формат подписанного тела сохранён. +Подпись Ed25519 покрывает `"utun-chat-record-v2" | group_id:8BE | message_id:32 | +timestamp:8BE | author:8BE | nonce:16 | ver:8BE | edited_at:8BE | data_len:4BE | data`. +Тело имеет длину 1–4000 байт, подпись — 64 байта. Числа конверта кодируются канонически. -`message_id = SHA256("utun-chat-message-v1" || BE64(group_id) || BE64(timestamp) || signature)`. +`message_id = SHA256("utun-chat-message-v2" || BE64(group_id) || BE64(timestamp) || BE64(author) || nonce[16])`. +Случайный nonce задаётся при создании. ID и timestamp создания не меняются при редактировании. Порядок набора — лексикографический по полному message_id[32]. Лист хеширует канонические записи в этом порядке. В запись входят message_id, -timestamp, author node_id, длина и точные байты тела, подпись. Дерево и границы +timestamp, author node_id, nonce, ver, edited_at, длина и точные байты тела, подпись. Дерево и границы листьев описаны в [merkle_sync_doc.md](merkle_sync_doc.md). Локальные id, flags, local_attrs, delivered_peers и delivery_chain в корень не входят. Поэтому одинаковая подписанная история даёт одинаковый корень при разных локальных id. -Два сообщения одного автора в одной миллисекунде различаются подписью и message_id. +Два сообщения одного автора в одной миллисекунде различаются nonce и message_id. + +Новая запись имеет ver=1, edited_at=0. Правка получает ver+1 и Unix ms последнего +редактирования (строго позже создания и предыдущей локальной правки). Победителя +определяет ver, а не время. Большая версия заменяет содержимое и подпись атомарно, +сохраняя локальный id, порядок, local_attrs и ссылки ответов/реакций. Можно впервые +получить сразу ver=5. Меньшая версия игнорируется; точный повтор идемпотентен. +Разные подписанные байты одного ID/ver — конфликт: ошибка без ACK или успешного DONE. +Одновременные писатели одной идентичности не объединяют конфликтующие версии автоматически. + +chat_core_edit_message разрешает правку собственного text/text/plain через expected_ver; +устаревший редактор получает отказ. Меняется только d, reply_to и тип сохраняются. +Тот же текст не создаёт версию. CHAT_EVT_MSG_UPDATED обновляет строку и цитаты без unread/звука. Допуск пересылающего пира проверяется отдельно: требуется READY группы и членство, привязанное к X25519-ключу соединения. Статус deleted исходного автора не фильтрует @@ -88,7 +102,7 @@ SQLite допускает одного writer: другие записи жду Вставка перед уже пройденным курсором может потребовать следующего раунда. Изменения дерева отмечаются после COMMIT и автоматически запускают повтор. При прекращении изменений, доступных ключах и исправном транспорте обмен объединяет -неизменяемые допустимые записи. Постоянный конфликт/ошибка возвращается как ошибка, +допустимые записи, выбирая наибольшую версию каждого ID. Постоянный конфликт/ошибка возвращается как ошибка, без ложного done. TTL не создаёт распределённого tombstone: другая копия может вернуть ранее удалённую локально запись. @@ -97,12 +111,13 @@ SQLite допускает одного writer: другие записи жду Заголовок: `service:1 | group_id:8BE | type:1`. PUSH (0x05) содержит одну каноническую запись: -`message_id:32 | timestamp:8BE | author:8BE | data_len:4BE | data | signature:64`. +`message_id:32 | timestamp:8BE | author:8BE | nonce:16 | ver:8BE | edited_at:8BE | data_len:4BE | data | signature:64`. -ACK_PUSH (0x06) содержит ровно message_id[32]. После успешной вставки либо допустимого +ACK_PUSH (0x06) содержит `message_id:32 | ver:8BE`. После успешной вставки/правки либо допустимого повтора получатель отправляет ACK. Постановка PUSH в очередь не считается доставкой. Один идемпотентный UPDATE учитывает уникальный peer_id в delivery_chain и увеличивает delivered_peers только один раз. WAS_SENT выставляется для локально созданной записи. +ACK прежней версии не подтверждает новую. При правке доставка сбрасывается. Bulk-сходимость не подменяет адресное подтверждение конкретного PUSH. PUSH ускоряет распространение; при отказе очереди committed изменение остаётся dirty @@ -133,14 +148,15 @@ destroy освобождает модуль, но не общую SQLite. Уни ## Схема и TTL Таблица содержит timestamp, node_id, id, flags, data, author_signature, message_id, -bucket, local_attrs, delivered_peers, delivery_chain. -PRIMARY KEY — (timestamp, author_signature), message_id UNIQUE, bucket — старшие 25 бит ключа. -chain_hash удалён. При перестройке схемы подписанные и локальные поля сохраняются; -производные поля вычисляются заново. DDL/copy/rename/COMMIT атомарны. -Записи с доказанно неверной длиной подписи удаляются с диагностикой; подписи корректной -длины проверяет последующий preflight. +bucket, local_attrs, delivered_peers, delivery_chain, nonce, ver, edited_at. +message_id NOT NULL UNIQUE — постоянный ключ; bucket — старшие 25 бит ключа. +Новый dev-формат не переносит старую историю: атомарно пересоздаётся только таблица +группы и очищаются её feedback, производный индекс, ссылки/ключи старых вложений и +позиции чтения. PM, идентичность, участники и ключи группы сохраняются. +Повторный запуск с новым форматом сохраняет историю и версии. -TTL сравнивает миллисекунды Unix и удаляет только старые собственные записи без WAS_SENT. +TTL сравнивает миллисекунды Unix и удаляет только старые собственные записи без WAS_SENT; +для правки возраст считается от edited_at, для оригинала — от timestamp. За транзакцию удаляется максимум 16 строк. В том же commit очищается производный MESSAGES-индекс канала; до завершения повторной проверки корень остаётся pending. Полная пачка продолжается через 1 tick, следующая плановая проверка — через час. @@ -157,7 +173,8 @@ test_db_sync проверяет реальный транспорт 2/3 узло числом строк, отличием в начале и разными локальными id. test_db_sync_integrity проверяет реальные подписи, SQL/crypto/NOMEM/COMMIT/timer faults, атомарность страниц, курсоры, поздние ключи, исключённого автора, TTL и callbacks. -test_db_sync_schema проверяет сохранность полей и rollback миграции. +test_db_sync_schema проверяет правки после restart, сохранность ответов/реакций и атомарный сброс старого формата. +test_db_sync/test_db_sync_integrity проверяют скачки версий, конфликты, старые ACK и сходимость offline/live-правок. test_dm_invite проверяет настоящий JOIN и восстановление истории после таймаута. Индекс предполагает запись содержимого через модель. Произвольное внешнее изменение diff --git a/tests/test_media_delivery_chat.c b/tests/test_media_delivery_chat.c index 3649120e..51a7e157 100644 --- a/tests/test_media_delivery_chat.c +++ b/tests/test_media_delivery_chat.c @@ -276,8 +276,8 @@ static int parse_media_body(const char* body, struct media_index_result* out) { /* ── insert-callback: storage-узлы запускают auto-download ── */ static void storage_on_insert(struct DB_SYNC_INSTANCE* si, const uint8_t message_id[32], uint64_t ts, - const char* data, size_t len, uint64_t author, int initial_sync, void* arg) { - (void)si; (void)message_id; (void)ts; (void)initial_sync; + const char* data, size_t len, uint64_t author, int initial_sync, int updated, void* arg) { + (void)si; (void)message_id; (void)ts; (void)initial_sync; (void)updated; int idx = (int)(intptr_t)arg; struct UTUN_INSTANCE* inst = g_inst[idx]; if (author == inst->node_id) return;