18 KiB
db_sync — подписанная история и MESSAGES Merkle
История хранится в общей SQLite ядра. DB_SYNC_INSTANCE связывает одну таблицу
с CHAT-группой; namespace — десятичный group_id. Полный обмен выполняет общий
merkle_sync, dataset MT_DATA_MESSAGES=2. На ETCP_ID_DB_SYNC остаются PUSH
и адресный ACK_PUSH. Старый обмен цепочками, sparse-поиск и сетевой OFFSET удалены.
Все вызовы и callbacks выполняются в uasync-потоке экземпляра.
Идентичность и корень
Подпись 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-v2" || BE64(group_id) || BE64(timestamp) || BE64(author) || nonce[16]).
Случайный nonce задаётся при создании. ID и timestamp создания не меняются при редактировании.
Порядок набора — лексикографический по полному message_id[32].
Лист хеширует канонические записи в этом порядке. В запись входят message_id,
timestamp, author node_id, nonce, ver, edited_at, длина и точные байты тела, подпись. Дерево и границы
листьев описаны в merkle_sync_doc.md.
Локальные id, flags, local_attrs, delivered_peers и delivery_chain в корень не входят. Поэтому одинаковая подписанная история даёт одинаковый корень при разных локальных 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 исходного автора не фильтрует его корректно подписанную историю, корень и выдачу. Исключённому участнику закрыт вход в группу; разрешённые участники продолжают обмен историей всех авторов. Политика отображения реакций/голосов относится к FEEDBACK, а не MESSAGES.
Проверка и безопасное удаление
Сохранённая запись различает пять результатов: VALID, BAD_FORMAT, BAD_SIGNATURE, DEFERRED (ключ ещё неизвестен), ERROR (операционная ошибка). Удаляется только строка, чьи собственные байты имеют недопустимый формат либо чья подпись не совпала при успешно выполненной криптографической проверке. SQL, NOMEM, ошибки OpenSSL и неизвестный ключ сохраняют данные и откладывают обмен. Ошибка никогда не превращается в пустой лист или успешное подтверждение.
Неверная входящая копия не доказывает повреждение сохранённой. При совпадении ключа дубликата проверяются собственные сохранённые байты. Корректная копия сохраняется; доказанно повреждённая удаляется. Отказ удаления, индекса или COMMIT откатывает всю транзакцию.
Ключ автора ищется в локальной идентичности, проверенном node_registry, общей таблице nodes и текущем соединении. Ожидание ключа не мешает запуску чата, но блокирует публикацию корня соответствующей истории. Поступление MEMBERS, включая другой канал, ускоряет повтор ожидания общего ключа.
Подготовка индекса и короткие транзакции
После регистрации таблицы её индекс pending, сетевой обмен закрыт.
После создания общего движка db_sync_register_model подключает MESSAGES и запускает:
- Проверку до 16 сохранённых строк по rowid за один шаг. Формат/подписи читаются и проверяются до write-транзакции. Внутри короткой транзакции строки перечитываются и сверяются с проверенными байтами; доказанная порча удаляется, message_id/bucket восстанавливаются. Курсор сдвигается только после COMMIT.
- Пересчёт до 16 непустых листьев и их путей за транзакцию.
- Публикацию готовности корня и запуск разрешённых READY-пиров.
Между шагами таймер на 1 tick (0,1 мс) возвращает управление сетевому циклу.
При ошибке сохраняются pending и последний подтверждённый курсор; повтор через 5 с.
Изменение данных во время подготовки требует нового обхода.
db_sync_verify — явно синхронная диагностическая перепроверка; рабочая синхронизация
использует таймеры. db_sync_read_root возвращает 1, пока подготовка не закончена.
Обычная вставка и изменение её Merkle-пути фиксируются одной короткой транзакцией. Входящая страница содержит максимум 16 записей и ограничена размером пакета; её формат, ключи, порядок, курсоры и подписи проверяются до BEGIN IMMEDIATE. Данные страницы и индекс затем фиксируются вместе. Сеть и ожидание ответа находятся вне транзакций. Одна транзакция на всю историю не создаётся.
SQLite допускает одного writer: другие записи ждут короткий commit; при WAL читатели могут продолжать чтение. Ограничение по числу строк/листьев не является гарантией времени в миллисекундах: оно зависит также от диска и наполнения листа. Пересчёт родителя/листа не выполняет повторную проверку Ed25519 под write-lock. Существующий отличающийся дубликат требует независимой проверки сохранённой копии.
Обмен и критерий завершения
Общий wire v4 использует свежие round_id и accept_id обеих сторон. Два прохода находят различия по полным хешам и передают страницы листьев. Курсор — полный message_id; продолжение выбирает key > after, без OFFSET. Итоговые CHECK/DONE сверяют полный корень[32] и локальную ревизию. Успех означает подтверждённую контрольную точку конкретного раунда. Совпадение числа строк само по себе не завершает обмен.
Вставка перед уже пройденным курсором может потребовать следующего раунда. Изменения дерева отмечаются после COMMIT и автоматически запускают повтор. При прекращении изменений, доступных ключах и исправном транспорте обмен объединяет допустимые записи, выбирая наибольшую версию каждого ID. Постоянный конфликт/ошибка возвращается как ошибка, без ложного done. TTL не создаёт распределённого tombstone: другая копия может вернуть ранее удалённую локально запись.
PUSH и доставка
Заголовок: service:1 | group_id:8BE | type:1.
PUSH (0x05) содержит одну каноническую запись:
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 | ver:8BE. После успешной вставки/правки либо допустимого
повтора получатель отправляет ACK. Постановка PUSH в очередь не считается доставкой.
Один идемпотентный UPDATE учитывает уникальный peer_id в delivery_chain и увеличивает
delivered_peers только один раз. WAS_SENT выставляется для локально созданной записи.
ACK прежней версии не подтверждает новую. При правке доставка сбрасывается.
Bulk-сходимость не подменяет адресное подтверждение конкретного PUSH.
PUSH ускоряет распространение; при отказе очереди committed изменение остаётся dirty и будет найдено MESSAGES. Неверная запись не пересылается.
Политика запуска и жизненный цикл
Таблица начинает с gated=1; чат открывает историю после успешного MEMBERS. Нужны готовый проверенный индекс, READY группы и допуск транспортного пира. Для истории sound_on разрешает обмен со спящим пиром; sound_off откладывает его. MEMBERS и FEEDBACK по умолчанию откладываются при peer_sleep_phase. Смена sleep/sound/членства переоценивает сессии; возврат допуска возобновляет историю на прежнем READY-соединении. Пауза диагностического пира блокирует весь обмен истории. На Android периодический retry ждёт активной фазы standby; ожидание отменяется при destroy.
on_result модели получает результат и автоматических раундов.
Глобальный done вызывается только для MT_OK. Самоудаление подписки, удаление следующей
подписки либо канала во время callback безопасны.
Для bulk уведомления on_insert отложены до выхода из обработчика Merkle и выполняются
после COMMIT. При rollback они отменяются. Callback видит зафиксированную страницу.
Для локальной вставки/PUSH уведомление выполняется после commit в текущем обработчике.
Удаление экземпляра отменяет только MESSAGES соответствующего namespace, таймеры и отложенные уведомления; исходная таблица остаётся. destroy освобождает модуль, но не общую SQLite. Уничтожение всего ядра из callback должно быть отложено согласно общему контракту сервисов.
Схема и TTL
Таблица содержит timestamp, node_id, id, flags, data, author_signature, message_id, 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; для правки возраст считается от edited_at, для оригинала — от timestamp. За транзакцию удаляется максимум 16 строк. В том же commit очищается производный MESSAGES-индекс канала; до завершения повторной проверки корень остаётся pending. Полная пачка продолжается через 1 tick, следующая плановая проверка — через час. Это локальная политика хранения, а не удаление чужой истории или отмена сообщения.
Диагностика и проверки
Категория chat_sync показывает регистрацию, preflight/удаления/готовность, commit страниц, TTL, ACK и причины отказов. Общий автомат/TX/RX/контрольные точки — member_sync. DEBUG содержит namespace, dataset, peer, round, размеры и состояние; несовпавшие handshake-токены диагностируются без изменения прогресса.
test_db_sync проверяет реальный транспорт 2/3 узлов, PUSH/ACK и длинную историю с равным числом строк, отличием в начале и разными локальными id. test_db_sync_integrity проверяет реальные подписи, SQL/crypto/NOMEM/COMMIT/timer faults, атомарность страниц, курсоры, поздние ключи, исключённого автора, TTL и callbacks. test_db_sync_schema проверяет правки после restart, сохранность ответов/реакций и атомарный сброс старого формата. test_db_sync/test_db_sync_integrity проверяют скачки версий, конфликты, старые ACK и сходимость offline/live-правок. test_dm_invite проверяет настоящий JOIN и восстановление истории после таймаута.
Индекс предполагает запись содержимого через модель. Произвольное внешнее изменение SQLite не выявляется непрерывно: оно обнаруживается при перепроверке/чтении соответствующих данных. Это не заменяет отдельную проверку целостности файла SQLite.