Document editable history protocol and update media insert callback

This commit is contained in:
evgeny committed 2026-10-26 22:54:25 +01:00
1 parent aefef51c58
commit e229811e20
2 files changed
+35 -18

No files matched your search

+33 -16
View File
@@ -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 и восстановление истории после таймаута.
Индекс предполагает запись содержимого через модель. Произвольное внешнее изменение
+2 -2
View File
@@ -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;