diff --git a/src/chat/merkle_sync_doc.md b/src/chat/merkle_sync_doc.md index dbe02f62..bd287855 100644 --- a/src/chat/merkle_sync_doc.md +++ b/src/chat/merkle_sync_doc.md @@ -1,17 +1,23 @@ -# merkle_sync — протокол 2 +# merkle_sync — протокол 3 ## Разделение ответственности - merkle_tree: производный индекс SQLite, хеши листьев и родителей. - merkle_sync: обмен, backpressure и жизненный цикл сессий. -- member_sync: формат записей, подписи, версии, применение страниц. +- member_sync: формат участников, подписи, версии, применение страниц. +- chat_feedback: подписанные состояния реакций/ответов, ver, отмены и применение страниц. Движок не знает CHAT-групп. Namespace — каноническая десятичная строка uint64_t. Все операции выполняются в одном потоке uasync экземпляра. +Сессия определяется тройкой (peer, dataset, namespace). MEMBERS=0, FEEDBACK=1. +В индексе SQLite namespace хранится как `dataset:channel_id`; корни наборов независимы. ## Дерево и транзакции Корень имеет уровень 0. Уровни 1–5 разбивают старшие 25 бит ключа на группы по 5 бит. +Полный ключ курсора — 32 байта с лексикографическим порядком; ветвь задаётся первыми 8 байтами. +Для участника ключ — BE64(node_id) и 24 нулевых байта. Для feedback — SHA256 идентичности записи. +Совпадение первых 64 бит не объединяет записи и не пропускает их при пагинации. SHA256 листа: `0x4d || level:1 || item_hashes`, записи упорядочены по unsigned key. SHA256 родителя: `0x4d || level:1 || child_hash[0] || ... || child_hash[31]`. Хеш пустого поддерева — 32 нулевых байта; строка индекса для него отсутствует. @@ -24,19 +30,22 @@ SHA256 родителя: `0x4d || level:1 || child_hash[0] || ... || child_hash[ ## API -init принимает service и data_ops: update_bucket_hash, get_page, apply_items, +init принимает service и data_ops начальной модели MEMBERS. register добавляет другие наборы. +Каждая модель задаёт update_bucket_hash, get_page, apply_items, необязательные validate_peer и finish. update_bucket_hash добавляет хеши одного листа в переданный SHA256-контекст. get_page получает ёмкость буфера и курсор; возвращает размер, последний ключ и флаг продолжения. apply_items проверяет весь формат и применяет страницу вместе с индексом атомарно. finish проверяет отложенные зависимости модели перед подтверждением корня. -start подключает namespace к текущему соединению и запрашивает контрольную точку. +start подключает (dataset, namespace) к текущему соединению и запрашивает контрольную точку. Повторные start объединяются, обход не сбрасывается, каждый принятый callback вызывается один раз. После подключения изменения синхронизируются автоматически. Broadcast/send-back не требуются. MT_OK означает совпадение корней конкретного раунда, а не отсутствие будущих изменений. -cancel, cancel_ns, cancel_peer и destroy освобождают запросы без callback. +cancel отменяет только заданный dataset/peer/namespace. +cancel_ns отменяет все datasets канала, cancel_peer — все datasets пира. +Все cancel и destroy освобождают запросы без callback. DOWN/DELETE/REINIT завершают запросы ошибкой и освобождают сессию. После восстановления соединения интеграционный слой повторяет start. Callback может отменить сессию; уничтожение всего экземпляра следует отложить через call_soon. @@ -66,16 +75,16 @@ WAKE также перезапускает незавершённый раунд ## Wire -Заголовок: `service:1 | namespace:8 | type:1 | round:8 | request:4` (22 байта). +Заголовок: `service:1 | namespace:8 | type:1 | round:8 | request:4 | dataset:1` (23 байта). Все целые big-endian. Максимальный пакет вместе с заголовком — 16384 байта. Типы 0x10–0x19: WAKE, BEGIN, READY, QUERY, HASHES, PAGE, TURN, CHECK, DONE, ERROR. | Тип | Payload | |---|---| | WAKE, BEGIN, READY, TURN | пустой; request=0; у WAKE также round=0 | -| QUERY | level:1, prefix:8, after_valid:1, after:8 | +| QUERY | level:1, prefix:8, after_valid:1, after:32 | | HASHES | 32 хеша детей по 32 байта в порядке индексов | -| PAGE | more:1, next:8, model_data | +| PAGE | more:1, next:32, model_data | | CHECK, DONE | корень 32 байта; request=0 | | ERROR | положительный код ошибки 4 байта; request=0 | @@ -97,4 +106,23 @@ DEBUG — TX/RX, номера запросов, размеры, состояни test_merkle_sync проверяет индекс, односторонний start, двустороннее слияние, несколько узлов, параллельные изменения, большой лист с пагинацией и автоматическое обновление. test_merkle_protocol проверяет ошибочные пакеты/курсоры, сохранение соседних ветвей, -отмену waiter, ошибки apply/SQLite, backpressure, disconnect и повторное подключение. +отмену waiter, ошибки apply/SQLite, backpressure, disconnect и повторное подключение, +изоляцию datasets и пагинацию разных ключей с одинаковым 64-битным префиксом. +test_chat_feedback проверяет подписи, монотонный ver, отмены, старые записи, +конфликты одного ver, атомарность страниц, восстановление дерева и удаление канала. + +## Подписанные состояния feedback + +Таблица feedback_ хранит message_id, node_id, kind, ver, data и signature. +Идентичность записи — (message_id, node_id, kind); kind=1 — reaction, kind=2 — answer. +ver положителен, максимум INT64_MAX. Меньшая версия игнорируется. +При одинаковом ver выбирается лексикографически большее data; конфликт логируется. +Пустое data — сохраняемая отмена, её нельзя удалять TTL-очисткой. +Подпись покрывает domain `utun-chat-feedback-v1`, BE64(channel_id) и wire-тело без подписи. +Wire-запись: message_id:32, node_id:8, kind:1, ver:8, data_len:2, data, signature:64. +Страница: count:2 и count записей; все числа big-endian, data максимум 2048 байт. +Хеш листа включает канонические тела без signature: разные корректные подписи одного тела +не создают расхождения корней. Входящие и сохранённые подписи всё равно проверяются. +Членство проверяется через подписи дерева приглашений и join, привязывающий Ed25519 к узлу. +Слой feedback хранит подписанные байты независимо от наличия исходного сообщения; +семантику выбранных пунктов и эмодзи проверяет вызывающий модуль до учёта результата. diff --git a/src/routing_layer/topo_node_sqlite.h b/src/routing_layer/topo_node_sqlite.h index 92c5c4c1..41d33887 100644 --- a/src/routing_layer/topo_node_sqlite.h +++ b/src/routing_layer/topo_node_sqlite.h @@ -42,7 +42,7 @@ int topo_node_sqlite_channel_put(sqlite3* db, const char* channel_id, const uint8_t* signature); /* Полное локальное удаление канала: DROP peers_/msg_, DELETE channels, - * DELETE merkle_tree_hash WHERE namespace=. 0=ок. */ + * Удаляются feedback_ и merkle_tree_hash всех datasets этого канала. 0=ок. */ int topo_node_sqlite_channel_delete(sqlite3* db, const char* channel_id); /* Упрощённый плейсхолдер мембера от topo_group (source=1, не участвует в merkle-хеше). */