Browse Source

Document Merkle v3 and signed feedback state contracts

master
evgeny 1 week ago
parent
commit
04ff801531
  1. 46
      src/chat/merkle_sync_doc.md
  2. 2
      src/routing_layer/topo_node_sqlite.h

46
src/chat/merkle_sync_doc.md

@ -1,17 +1,23 @@
# merkle_sync — протокол 2 # merkle_sync — протокол 3
## Разделение ответственности ## Разделение ответственности
- merkle_tree: производный индекс SQLite, хеши листьев и родителей. - merkle_tree: производный индекс SQLite, хеши листьев и родителей.
- merkle_sync: обмен, backpressure и жизненный цикл сессий. - merkle_sync: обмен, backpressure и жизненный цикл сессий.
- member_sync: формат записей, подписи, версии, применение страниц. - member_sync: формат участников, подписи, версии, применение страниц.
- chat_feedback: подписанные состояния реакций/ответов, ver, отмены и применение страниц.
Движок не знает CHAT-групп. Namespace — каноническая десятичная строка uint64_t. Движок не знает CHAT-групп. Namespace — каноническая десятичная строка uint64_t.
Все операции выполняются в одном потоке uasync экземпляра. Все операции выполняются в одном потоке uasync экземпляра.
Сессия определяется тройкой (peer, dataset, namespace). MEMBERS=0, FEEDBACK=1.
В индексе SQLite namespace хранится как `dataset:channel_id`; корни наборов независимы.
## Дерево и транзакции ## Дерево и транзакции
Корень имеет уровень 0. Уровни 1–5 разбивают старшие 25 бит ключа на группы по 5 бит. Корень имеет уровень 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 || item_hashes`, записи упорядочены по unsigned key.
SHA256 родителя: `0x4d || level:1 || child_hash[0] || ... || child_hash[31]`. SHA256 родителя: `0x4d || level:1 || child_hash[0] || ... || child_hash[31]`.
Хеш пустого поддерева — 32 нулевых байта; строка индекса для него отсутствует. Хеш пустого поддерева — 32 нулевых байта; строка индекса для него отсутствует.
@ -24,19 +30,22 @@ SHA256 родителя: `0x4d || level:1 || child_hash[0] || ... || child_hash[
## API ## 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. необязательные validate_peer и finish.
update_bucket_hash добавляет хеши одного листа в переданный SHA256-контекст. update_bucket_hash добавляет хеши одного листа в переданный SHA256-контекст.
get_page получает ёмкость буфера и курсор; возвращает размер, последний ключ и флаг продолжения. get_page получает ёмкость буфера и курсор; возвращает размер, последний ключ и флаг продолжения.
apply_items проверяет весь формат и применяет страницу вместе с индексом атомарно. apply_items проверяет весь формат и применяет страницу вместе с индексом атомарно.
finish проверяет отложенные зависимости модели перед подтверждением корня. finish проверяет отложенные зависимости модели перед подтверждением корня.
start подключает namespace к текущему соединению и запрашивает контрольную точку. start подключает (dataset, namespace) к текущему соединению и запрашивает контрольную точку.
Повторные start объединяются, обход не сбрасывается, каждый принятый callback вызывается один раз. Повторные start объединяются, обход не сбрасывается, каждый принятый callback вызывается один раз.
После подключения изменения синхронизируются автоматически. Broadcast/send-back не требуются. После подключения изменения синхронизируются автоматически. Broadcast/send-back не требуются.
MT_OK означает совпадение корней конкретного раунда, а не отсутствие будущих изменений. 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 завершают запросы ошибкой и освобождают сессию. DOWN/DELETE/REINIT завершают запросы ошибкой и освобождают сессию.
После восстановления соединения интеграционный слой повторяет start. После восстановления соединения интеграционный слой повторяет start.
Callback может отменить сессию; уничтожение всего экземпляра следует отложить через call_soon. Callback может отменить сессию; уничтожение всего экземпляра следует отложить через call_soon.
@ -66,16 +75,16 @@ WAKE также перезапускает незавершённый раунд
## Wire ## 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 байта. Все целые big-endian. Максимальный пакет вместе с заголовком — 16384 байта.
Типы 0x10–0x19: WAKE, BEGIN, READY, QUERY, HASHES, PAGE, TURN, CHECK, DONE, ERROR. Типы 0x10–0x19: WAKE, BEGIN, READY, QUERY, HASHES, PAGE, TURN, CHECK, DONE, ERROR.
| Тип | Payload | | Тип | Payload |
|---|---| |---|---|
| WAKE, BEGIN, READY, TURN | пустой; request=0; у WAKE также round=0 | | 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 байта в порядке индексов | | HASHES | 32 хеша детей по 32 байта в порядке индексов |
| PAGE | more:1, next:8, model_data | | PAGE | more:1, next:32, model_data |
| CHECK, DONE | корень 32 байта; request=0 | | CHECK, DONE | корень 32 байта; request=0 |
| ERROR | положительный код ошибки 4 байта; request=0 | | ERROR | положительный код ошибки 4 байта; request=0 |
@ -97,4 +106,23 @@ DEBUG — TX/RX, номера запросов, размеры, состояни
test_merkle_sync проверяет индекс, односторонний start, двустороннее слияние, несколько узлов, test_merkle_sync проверяет индекс, односторонний start, двустороннее слияние, несколько узлов,
параллельные изменения, большой лист с пагинацией и автоматическое обновление. параллельные изменения, большой лист с пагинацией и автоматическое обновление.
test_merkle_protocol проверяет ошибочные пакеты/курсоры, сохранение соседних ветвей, 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 хранит подписанные байты независимо от наличия исходного сообщения;
семантику выбранных пунктов и эмодзи проверяет вызывающий модуль до учёта результата.

2
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); const uint8_t* signature);
/* Полное локальное удаление канала: DROP peers_<id>/msg_<id>, DELETE channels, /* Полное локальное удаление канала: DROP peers_<id>/msg_<id>, DELETE channels,
* DELETE merkle_tree_hash WHERE namespace=<channel_id>. 0=ок. */ * Удаляются feedback_<channel_id> и merkle_tree_hash всех datasets этого канала. 0=ок. */
int topo_node_sqlite_channel_delete(sqlite3* db, const char* channel_id); int topo_node_sqlite_channel_delete(sqlite3* db, const char* channel_id);
/* Упрощённый плейсхолдер мембера от topo_group (source=1, не участвует в merkle-хеше). */ /* Упрощённый плейсхолдер мембера от topo_group (source=1, не участвует в merkle-хеше). */

Loading…
Cancel
Save