Document Merkle v3 and signed feedback state contracts

This commit is contained in:
evgeny committed 2026-10-06 20:11:25 +02:00
1 parent 6437bf780b
commit 04ff801531
2 files changed
+38 -10

No files matched your search

+37 -9
View File
@@ -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_<channel> хранит 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 хранит подписанные байты независимо от наличия исходного сообщения;
семантику выбранных пунктов и эмодзи проверяет вызывающий модуль до учёта результата.