Document shared history Merkle protocol and bounded transactions
This commit is contained in:
1 parent
df39999624
commit
669ad86e57
2 files changed
+185
-563
No files matched your search
+165
-558
@@ -1,558 +1,165 @@
|
||||
# db_sync — децентрализованная реплицируемая таблица с цепным хешированием
|
||||
|
||||
Аудит 26.10.2026 частичный. Исправлены удаление корректной истории при отказе
|
||||
проверки/неверном входящем повторе и SQL доставки. ACK_PUSH теперь содержит
|
||||
message_id; старый формат не поддерживается. Условия удаления, проверки и
|
||||
оставшиеся границы аудита — [отчёт, разделы10–11](../../doc/member_merkle_audit_findings.txt).
|
||||
|
||||
## 1. Архитектура
|
||||
|
||||
### 1.1 Обзор
|
||||
|
||||
```
|
||||
UTUN_INSTANCE
|
||||
│
|
||||
┌────▼────┐
|
||||
│ DB_SYNC │ (глобальный контекст, один на инстанс)
|
||||
└────┬────┘
|
||||
│
|
||||
┌────────────────┼────────────────┐
|
||||
▼ ▼ ▼
|
||||
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
|
||||
│DB_SYNC │ │DB_SYNC │ │DB_SYNC │
|
||||
│INSTANCE │ │INSTANCE │ │INSTANCE ... │
|
||||
│msg_GENERAL │ │msg_SECRET │ │ │
|
||||
└──────┬───────┘ └──────┬───────┘ └──────────────┘
|
||||
│ │
|
||||
┌────────┼────────┐ │
|
||||
▼ ▼ ▼ ▼
|
||||
┌────────┐┌────────┐┌──────┐
|
||||
│SI_PEER ││SI_PEER ││SI_...│ (per-instance peer state)
|
||||
│node_A ││node_B ││ │
|
||||
└────────┘└────────┘└──────┘
|
||||
```
|
||||
|
||||
**DB_SYNC** — глобальный модуль (struct, один на UTUN_INSTANCE):
|
||||
- `inst` — указатель на UTUN_INSTANCE
|
||||
- `db` — SQLite-соединение (своё или shared `inst->topo_sqlite_db`)
|
||||
- `instances[]` — динамический массив DB_SYNC_INSTANCE
|
||||
- `peer_check_timer` — периодическая проверка (5с), ищет несинхронизированных пиров
|
||||
- `done_cbks` — связанный список коллбэков завершения синхронизации
|
||||
|
||||
**DB_SYNC_INSTANCE** — одна таблица синхронизации:
|
||||
- `table_name` — имя SQLite-таблицы (например `msg_GENERAL`)
|
||||
- `group_id` — идентификатор группы для маршрутизации сообщений
|
||||
- `sync_gated` — ожидание успешной синхронизации участников; блокирует входящий и исходящий обмен
|
||||
- `next_id` — автоинкрементный ID записей
|
||||
- `last_timestamp_ms` — последний использованный timestamp (монотонный)
|
||||
- `peers[]` — динамический массив SI_PEER (состояние синхронизации с каждым пиром)
|
||||
- `recalc` — позиция, pending и таймер пересчёта; ошибка сохраняет pending, батчи по50
|
||||
- `ttl_timer` — периодическая TTL-очистка (каждый час)
|
||||
- `on_insert` / `on_insert_arg` — коллбэк при вставке записи
|
||||
|
||||
**SI_PEER** — состояние пира в контексте инстанса:
|
||||
- `node_id` — идентификатор пира
|
||||
- `synced_pos` — позиция (0-based) до которой данные синхронизированы
|
||||
- `verified_pos` — последняя подтверждённая позиция (совпадение хешей)
|
||||
- `last_peer_count` — последнее известное количество записей у пира
|
||||
- `sync_state` — 0=idle, 1=syncing, 2=synced
|
||||
- `sync_start_tb` — время начала/последнего прогресса обмена; устанавливается в обеих ролях
|
||||
- `retry_count` — число повторов после таймаута
|
||||
|
||||
### 1.2 Структура данных (SQLite)
|
||||
|
||||
Каждый инстанс — это SQLite-таблица:
|
||||
|
||||
```sql
|
||||
CREATE TABLE "msg_GENERAL" (
|
||||
timestamp INTEGER NOT NULL, -- мс, монотонный (NTP или local)
|
||||
node_id INTEGER NOT NULL, -- author node_id (8 байт)
|
||||
id INTEGER NOT NULL, -- автоинкремент внутри инстанса
|
||||
chain_hash BLOB NOT NULL, -- SHA256(prev_hash || id || ts || author || sig)
|
||||
flags INTEGER DEFAULT 0, -- DB_REC_FLAG_WAS_SENT (0x01)
|
||||
data BLOB, -- JSON-данные записи
|
||||
author_signature BLOB NOT NULL, -- Ed25519(ts || data), 64 байта
|
||||
message_id BLOB NOT NULL UNIQUE CHECK(length(message_id)=32),
|
||||
local_attrs TEXT DEFAULT '', -- локальные атрибуты (не реплицируются)
|
||||
delivered_peers INTEGER DEFAULT 0, -- счётчик пиров, принявших PUSH
|
||||
delivery_chain TEXT DEFAULT '', -- цепочка peer_id hex через запятую
|
||||
PRIMARY KEY (timestamp, author_signature)
|
||||
);
|
||||
CREATE INDEX idx_msg_GENERAL_ttl ON msg_GENERAL (node_id, timestamp);
|
||||
```
|
||||
|
||||
**Цепной хеш:**
|
||||
```
|
||||
chain_hash[pos] = SHA256(chain_hash[pos-1] || id || timestamp || author || author_signature)
|
||||
chain_hash[0] = SHA256(0x00..00 [32] || id_0 || ts_0 || author_0 || sig_0)
|
||||
```
|
||||
|
||||
Упорядочение: `ORDER BY timestamp, author_signature`.
|
||||
|
||||
### 1.3 Ключевые механизмы
|
||||
|
||||
| Механизм | Описание |
|
||||
|----------|----------|
|
||||
| **Цепной хеш** | Каждая запись ссылается на хеш предыдущей — защита от вставки/удаления в середине цепочки |
|
||||
| **Ed25519-подписи** | Каждая запись подписана автором. Проверяется при вставке (и локальной, и от пира) |
|
||||
| **Async recalc** | Батчи по50 с атомарным продвижением после COMMIT; перед обменом flush завершается синхронно, ошибка прерывает раунд |
|
||||
| **Master/Slave** | Узел с бОльшим `node_id` — мастер (шлёт `INIT_SYNC`). Узел с меньшим — слейв (шлёт `REQUEST_SYNC`) |
|
||||
| **Периодическая проверка** | Каждые 5 секунд — поиск пиров с sync_state=0 и запуск синхронизации, детекция таймаутов (15с) |
|
||||
| **TTL-очистка** | Каждый час — удаление неподтверждённых (`flags&1==0`) записей локального автора старше `db_sync_ttl` |
|
||||
| **Lazy-register** | При получении сообщения с неизвестным instance_hash — поиск таблицы `msg_<ch_id>` с совпадающим хешем |
|
||||
| **Push** | Новая локальная запись немедленно рассылается всем пирам в sync_state >= 1 |
|
||||
| **Delivery tracking** | `delivered_peers` / `delivery_chain` — отслеживание доставки PUSH каждому пиру |
|
||||
|
||||
### 1.4 Интеграция с ETCP
|
||||
|
||||
- **Service ID:** `ETCP_ID_DB_SYNC = 0x20`
|
||||
- **Биндинг:** `etcp_bind(inst, ETCP_ID_DB_SYNC, db_sync_recv_cb)` — приём сообщений
|
||||
- **Connection callbacks:** `etcp_add_conn_status_cbk(inst, db_sync_on_conn_status, NULL)` — отслеживание UP/DOWN соединений
|
||||
- **Отправка:** через `etcp_send()` в прямое P2P-соединение
|
||||
- **Маршрутизация:** каждый пакет начинается с `[svc:1][hash_be:8]`, hash идентифицирует инстанс на приёмной стороне
|
||||
|
||||
---
|
||||
|
||||
## 2. Диаграмма состояний синхронизации
|
||||
|
||||
### 2.1 Состояния пира (sync_state)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ │
|
||||
▼ │
|
||||
┌───────┐ INIT_SYNC/REQUEST_SYNC ┌─────────┐ │
|
||||
│ IDLE │ ──────────────────────────► │ SYNCING │ │
|
||||
│ (0) │ │ (1) │ │
|
||||
└───────┘ └────┬────┘ │
|
||||
▲ │ │
|
||||
│ ┌─────────┼──────┘
|
||||
│ │ │
|
||||
│ ┌───────────▼──┐ ┌───▼──────────┐
|
||||
│ │ SYNC_DONE │ │ TIMEOUT+ERROR │
|
||||
│ │ matched ✓ │ │ (15s) │
|
||||
│ │ (2) │ │ reset → IDLE │
|
||||
│ └──────────────┘ └───────────────┘
|
||||
│ │
|
||||
└─────────────────────────────────────────────┘
|
||||
потеря READY группы / ERROR (NOT_FOUND, DISABLED, NOT_READY)
|
||||
```
|
||||
|
||||
### 2.2 Диаграмма протокола синхронизации
|
||||
|
||||
```
|
||||
MASTER (node_id > peer) SLAVE (node_id < peer)
|
||||
─────────────────────── ──────────────────────
|
||||
|
||||
участники синхронизированы + READY группы → initiate sync
|
||||
│
|
||||
├─ INIT_SYNC ──────────────────────────►
|
||||
│ [my_count:4] │ flush recalc
|
||||
│ │ определяет tp = min(peer_c, my_c) - 1
|
||||
│ │ вычисляет ch8_at_tp
|
||||
│ │ строит sparse hashes (интервалы: 1,1,1,2,2,4×8)
|
||||
│ │
|
||||
│ ◄────────────────────────── INIT_RESP │
|
||||
│ [tp:4][ch8:8][has_tail:1] │
|
||||
│ [sparse_cnt:1][(pos,ch8)*] │
|
||||
│ │
|
||||
│ flush recalc │
|
||||
│ ┌─ peer ch8 == 0? ──► send ALL data │
|
||||
│ ├─ ch8 match + no_tail? ──► DONE │
|
||||
│ ├─ ch8 match + has_tail? ──► send │
|
||||
│ │ from tp+1, want_from=tp+1 │
|
||||
│ └─ ch8 mismatch? ──► находим │
|
||||
│ первую разницу через sparse │
|
||||
│ hashes → fm (first mismatch) │
|
||||
│ vp = fm-1, send from fm │
|
||||
│ │
|
||||
│ SEND_DATA ──────────────────────────► │
|
||||
│ [from:4][count:2][vp:4] │ вставляет записи, проверяет подписи
|
||||
│ [want_from:4][records...] │ проверяет want_from:
|
||||
│ │ want≠NONE → отправляет встречный SEND_DATA
|
||||
│ │ want=NONE + ss=1 → SYNC_DONE
|
||||
│ │
|
||||
│ ◄────────────────────────── SEND_DATA │
|
||||
│ (встречные данные при want→NONE) │
|
||||
│ │
|
||||
│ ┌─ want=NONE + ss=1 → SYNC_DONE ────►│
|
||||
│ │ [mc:4][ch8_last:8] │ сверяет mc==pc и ch8==pch8
|
||||
│ │ │ match → ss=2, DONE
|
||||
│ │ │ mc<pc → request tail
|
||||
│ │ │ mc>pc → send tail
|
||||
│ └─────────────────────────────────────│
|
||||
```
|
||||
|
||||
### 2.3 Временная диаграмма жизненного цикла
|
||||
|
||||
```
|
||||
┌─ init ─────────────────────────────────────────────────────────────────────┐
|
||||
│ db_sync_init(inst) │
|
||||
│ ├─ открывает SQLite (shared или свой chats.db) │
|
||||
│ ├─ etcp_bind(inst, 0x20, recv_cb) │
|
||||
│ └─ таймер повторов запускается при появлении работы │
|
||||
│ │
|
||||
│ ... позже, при создании канала ... │
|
||||
│ │
|
||||
│ db_sync_instance_add(inst, "msg_GENERAL", group_id) │
|
||||
│ ├─ CREATE TABLE IF NOT EXISTS... │
|
||||
│ ├─ загружает next_id = MAX(id)+1 │
|
||||
│ ├─ db_verify_chain() — проверка целостности цепных хешей │
|
||||
│ ├─ запускает TTL-таймер (1 час) │
|
||||
│ └─ подписка на READY группы; gated=1, сетевого обмена нет │
|
||||
├────────────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌─── DB_SYNC_INSTANCE активен ───────────────────────────────────────────┐│
|
||||
│ │ ││
|
||||
│ │ member_sync OK + READY группы → initiate_sync() ││
|
||||
│ │ └─ пир должен быть участником именно этой группы ││
|
||||
│ │ ││
|
||||
│ │ потеря READY (DOWN/REINIT/LEAVE/ошибка отправки снимка) ││
|
||||
│ │ └─ сброс состояния обмена только этой групповой сессии ││
|
||||
│ │ ││
|
||||
│ │ recv_cb() — приём сообщений ││
|
||||
│ │ ├─ извлекает group_id, type, payload ││
|
||||
│ │ ├─ проверяет регистрацию, членство, gated и READY ││
|
||||
│ │ └─ диспетчеризация по type ││
|
||||
│ │ ││
|
||||
│ │ peer_check_timer (каждые 5с) ││
|
||||
│ │ ├─ находит пиров c sync_state=0, запускает sync ││
|
||||
│ │ └─ детектит таймауты (15s), сбрасывает sync_state ││
|
||||
│ │ ││
|
||||
│ │ ttl_timer (каждый час) ││
|
||||
│ │ └─ удаляет неподтверждённые локальные записи старше TTL ││
|
||||
│ │ ││
|
||||
│ │ recalc_timer (асинхронно, батчи по 50) ││
|
||||
│ │ └─ пересчитывает chain_hash для записей после вставки ││
|
||||
│ └─────────────────────────────────────────────────────────────────────────┘│
|
||||
│ │
|
||||
│ db_sync_instance_remove(si) │
|
||||
│ ├─ снимает подписку на готовность группы │
|
||||
│ ├─ отменяет таймеры │
|
||||
│ ├─ освобождает peers[] │
|
||||
│ └─ сдвигает массив instances[] (таблица БД не удаляется) │
|
||||
│ │
|
||||
│ db_sync_destroy(inst) │
|
||||
│ ├─ etcp_unbind(0x20) │
|
||||
│ ├─ останавливает все таймеры │
|
||||
│ ├─ освобождает все инстансы + done_cbks │
|
||||
│ ├─ закрывает SQLite │
|
||||
│ └─ u_free(db) │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Протокол (форматы кодограмм)
|
||||
|
||||
### 3.1 Общий заголовок ETCP
|
||||
|
||||
Каждое сообщение db_sync инкапсулируется в ETCP-дейтаграмму:
|
||||
|
||||
```
|
||||
┌─────────────────┬──────────────┬──────────────────────────────┐
|
||||
│ svc_id (1 byte) │ group_id (8) │ payload (variable) │
|
||||
│ ETCP_RT_ID=0x20 │ big-endian │ зависит от type │
|
||||
└─────────────────┴──────────────┴──────────────────────────────┘
|
||||
```
|
||||
|
||||
**group_id** — идентификатор группы в big-endian. Приёмная сторона находит
|
||||
зарегистрированную таблицу этой группы; входящий пакет не создаёт таблицы или callbacks.
|
||||
|
||||
### 3.2 Типы сообщений
|
||||
|
||||
| #define | Value | Направление | Назначение |
|
||||
|---------|-------|-------------|------------|
|
||||
| `DB_MSG_INIT_SYNC` | 0x01 | Master→Slave | Инициирует синхронизацию |
|
||||
| `DB_MSG_INIT_RESP` | 0x02 | Slave→Master | Ответ с хешами на точке расхождения |
|
||||
| `DB_MSG_SEND_DATA` | 0x04 | ↔ | Передача батча записей |
|
||||
| `DB_MSG_PUSH` | 0x05 | →Peers | Рассылка новой записи всем synced-пирам |
|
||||
| `DB_MSG_ACK_PUSH` | 0x06 | →Author | Подтверждение получения PUSH |
|
||||
| `DB_MSG_SYNC_DONE` | 0x07 | ↔ | Завершение синхронизации |
|
||||
| `DB_MSG_ERROR` | 0x08 | ↔ | Ошибка (NOT_FOUND, DISABLED, NOT_READY) |
|
||||
| `DB_MSG_REQUEST_SYNC` | 0x09 | Slave→Master | Запрос на инициацию синхронизации |
|
||||
|
||||
### 3.3 DB_MSG_INIT_SYNC (0x01)
|
||||
|
||||
**Отправитель:** мастер (node_id > peer_id)
|
||||
**Назначение:** сообщить пиру своё количество записей для поиска точки расхождения
|
||||
|
||||
```
|
||||
┌──────┬──────────────┐
|
||||
│ 0x01 │ count (4 BE) │
|
||||
└──────┴──────────────┘
|
||||
total: 5 bytes
|
||||
```
|
||||
|
||||
- `count` — количество записей в таблице мастера (uint32, big-endian)
|
||||
|
||||
### 3.4 DB_MSG_INIT_RESP (0x02)
|
||||
|
||||
**Отправитель:** слейв (node_id < peer_id)
|
||||
**Назначение:** вернуть хеш на точке пересечения tp и разреженные хеши для бинарного поиска расхождения
|
||||
|
||||
```
|
||||
┌──────┬──────────────┬──────────────┬─────────────┬─────────────┬──────────────────────────────────┐
|
||||
│ 0x02 │ tp (4 BE) │ ch8_at_tp(8) │ has_tail(1) │ sparse_cnt │ sparse_entries[sparse_cnt] │
|
||||
│ │ │ │ │ (1) │ { pos(4BE), ch8(8) } × sparse_cnt│
|
||||
└──────┴──────────────┴──────────────┴─────────────┴─────────────┴──────────────────────────────────┘
|
||||
total: 14 + 12×sparse_cnt bytes (max 206)
|
||||
```
|
||||
|
||||
- `tp` — точка пересечения: `min(peer_count, my_count) - 1` (uint32, network byte order)
|
||||
- `ch8_at_tp` — первые 8 байт chain_hash на позиции tp (uint64, host byte order)
|
||||
- `has_tail` — 1 если у слейва есть записи после tp (uint8)
|
||||
- `sparse_cnt` — количество разреженных точек (uint8)
|
||||
- `sparse_entries` — массив `{pos:4, ch8:8}` от tp назад с интервалами: 1, 1, 1, 2, 2, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4 (макс. 16)
|
||||
|
||||
**Пример sparse-интервалов при tp=10:**
|
||||
```
|
||||
tp=10 → entry[0]: pos=9 (tp - 1)
|
||||
entry[1]: pos=8 (tp - 2)
|
||||
entry[2]: pos=7 (tp - 3)
|
||||
entry[3]: pos=5 (tp - 5)
|
||||
entry[4]: pos=3 (tp - 7)
|
||||
entry[5]: pos=0 (tp - 11) — только если tp достаточно велико
|
||||
```
|
||||
|
||||
### 3.5 DB_MSG_SEND_DATA (0x04)
|
||||
|
||||
**Отправитель:** любой узел
|
||||
**Назначение:** передать батч записей (до 32) и/или запросить встречные данные
|
||||
|
||||
```
|
||||
┌──────┬───────────┬───────────┬───────────┬──────────────┬──────────────────────────┐
|
||||
│ 0x04 │ from(4BE) │ count(2) │ vp(4BE) │ want_from(4) │ records[count] │
|
||||
│ │ │ │ │ │ ≤32 штук │
|
||||
└──────┴───────────┴───────────┴───────────┴──────────────┴──────────────────────────┘
|
||||
header: 14 bytes + records
|
||||
```
|
||||
|
||||
- `from` — начальная позиция записей (uint32, network byte order)
|
||||
- `count` — количество записей в этом батче (uint16, network byte order), ≤ 32
|
||||
- `vp` — verified position (uint32, BE): последняя позиция, где хеши совпали. `0xFFFFFFFF` если нет
|
||||
- `want_from` — запрос встречных данных от пира начиная с этой позиции. `0xFFFFFFFF` = `DB_WANT_FROM_NONE` (не запрашивать)
|
||||
- `records` — массив записей
|
||||
|
||||
#### Формат одной записи (record):
|
||||
|
||||
```
|
||||
┌───────┬───────┬──────────┬──────────┬────────────┬──────────┬──────────┐
|
||||
│ id(8) │ ts(8) │ author(8)│ dlen(4BE)│ data(dlen) │ sig_len │ sig(64) │
|
||||
│ │ │ │ │ │ =64 (1) │ │
|
||||
└───────┴───────┴──────────┴──────────┴────────────┴──────────┴──────────┘
|
||||
минимальный размер: 28 + 1 + 64 = 93 bytes (при dlen=0)
|
||||
```
|
||||
|
||||
- `id` — идентификатор записи (uint64, host byte order)
|
||||
- `ts` — timestamp в мс (uint64, host byte order)
|
||||
- `author` — node_id автора (uint64, host byte order)
|
||||
- `dlen` — длина данных от1 до4000 байт (uint32, host byte order)
|
||||
- `data` — JSON-данные (dlen байт)
|
||||
- `sig_len` — длина подписи, всегда 64 (uint8)
|
||||
- `sig` — Ed25519-подпись: `Ed25519(ts[8] || data[dlen])`, 64 байта
|
||||
|
||||
**Note:** поля `id`, `ts`, `author`, `dlen` в host byte order (LE на x86).
|
||||
|
||||
### 3.6 DB_MSG_PUSH (0x05)
|
||||
|
||||
**Отправитель:** автор записи → все synced-пиры
|
||||
**Назначение:** рассылка новой записи в реальном времени
|
||||
|
||||
Формат идентичен одной record из SEND_DATA:
|
||||
|
||||
```
|
||||
┌──────┬───────┬───────┬──────────┬──────────┬────────────┬──────────┬──────────┐
|
||||
│ 0x05 │ id(8) │ ts(8) │ author(8)│ dlen(4) │ data(dlen) │ sig_len │ sig(64) │
|
||||
│ │ │ │ │ │ │ =64 (1) │ │
|
||||
└──────┴───────┴───────┴──────────┴──────────┴────────────┴──────────┴──────────┘
|
||||
минимальный размер: 1 + 28 + 1 + 1 + 64 = 95 bytes (при dlen=1)
|
||||
```
|
||||
|
||||
### 3.7 DB_MSG_ACK_PUSH (0x06)
|
||||
|
||||
**Отправитель:** получатель PUSH → переславшему пиру
|
||||
**Назначение:** подтверждение получения PUSH-записи
|
||||
|
||||
```
|
||||
┌──────┬──────────────────┐
|
||||
│ 0x06 │ message_id (32) │
|
||||
└──────┴──────────────────┘
|
||||
total: 33 bytes
|
||||
```
|
||||
|
||||
- `message_id` — SHA256 идентичности записи (group_id, timestamp, подпись; см. chat_feedback.h).
|
||||
|
||||
Получатель ACK одним UPDATE находит ровно эту запись, добавляет peer_id в delivery_chain
|
||||
и увеличивает delivered_peers только при первом ACK от этого пира. Для локального автора
|
||||
также устанавливается DB_REC_FLAG_WAS_SENT. Постановка PUSH в очередь доставкой не считается.
|
||||
ACK отправляется и при корректном дубликате; записи с одинаковыми автором/временем независимы.
|
||||
|
||||
### 3.8 DB_MSG_SYNC_DONE (0x07)
|
||||
|
||||
**Отправитель:** любой узел
|
||||
**Назначение:** финальная сверка — сравнение количества записей и последнего chain_hash
|
||||
|
||||
```
|
||||
┌──────┬─────────────┬──────────────────┐
|
||||
│ 0x07 │ count (4BE) │ chain_hash8 (8) │
|
||||
└──────┴─────────────┴──────────────────┘
|
||||
total: 13 bytes
|
||||
```
|
||||
|
||||
- `count` — количество записей в таблице отправителя (uint32, network byte order)
|
||||
- `chain_hash8` — первые 8 байт последнего chain_hash (uint64, host byte order)
|
||||
|
||||
**Обработка:**
|
||||
- `count == my_count && ch8 == my_ch8` → sync завершён, ss→2, fire `db_sync_done_cb`
|
||||
- `my_count < peer_count` → запрашиваем хвост (SEND_DATA from=my_count, want=my_count)
|
||||
- `my_count > peer_count` → отправляем хвост (SEND_DATA from=peer_count)
|
||||
- `count == my_count && ch8 != my_ch8` → принимаем как сошедшееся (конвергенция)
|
||||
|
||||
### 3.9 DB_MSG_ERROR (0x08)
|
||||
|
||||
**Отправитель:** любой узел
|
||||
**Назначение:** сигнализировать об ошибке (инстанс не найден или выключен)
|
||||
|
||||
```
|
||||
┌──────┬──────────┐
|
||||
│ 0x08 │ code (1) │
|
||||
└──────┴──────────┘
|
||||
total: 2 bytes
|
||||
```
|
||||
|
||||
Коды ошибок:
|
||||
| #define | Value | Значение |
|
||||
|---------|-------|----------|
|
||||
| `DB_ERR_NOT_FOUND` | 0x01 | Таблица этой группы не зарегистрирована |
|
||||
| `DB_ERR_DISABLED` | 0x02 | Инстанс существует, но disabled |
|
||||
| `DB_ERR_NOT_READY` | 0x03 | Участники или групповая сессия ещё не готовы |
|
||||
|
||||
При получении ERROR сбрасываются `sync_state` и ожидание ответа; планируется повтор.
|
||||
Ошибка отправки также возвращает обмен в IDLE. Обе роли ждут прогресс не более 15 секунд;
|
||||
таймер повторов работает с периодом 5 секунд, пока есть несинхронизированные READY-пиры.
|
||||
Появление нового READY запускает обмен независимо от того, работал ли таймер.
|
||||
|
||||
При JOIN запись connection-узла из JOIN_INFO_RESP применяется через `member_sync_apply_record()`:
|
||||
подписи проверяются, модель и Merkle-индекс обновляются в одной транзакции. JOIN_READY
|
||||
подтверждает вступление, но история разблокируется только после успешного member_sync.
|
||||
|
||||
### 3.10 DB_MSG_REQUEST_SYNC (0x09)
|
||||
|
||||
**Отправитель:** слейв → мастер
|
||||
**Назначение:** запросить инициацию синхронизации (слейв говорит мастеру «начни sync»)
|
||||
|
||||
```
|
||||
┌──────┐
|
||||
│ 0x09 │
|
||||
└──────┘
|
||||
total: 1 byte (без payload)
|
||||
```
|
||||
|
||||
При получении REQUEST_SYNC мастер вызывает `initiate_sync()` → шлёт `INIT_SYNC`.
|
||||
|
||||
---
|
||||
|
||||
## 4. Алгоритм синхронизации (по шагам)
|
||||
|
||||
### 4.1 Запуск
|
||||
|
||||
1. При conn UP для каждого enabled инстанса: `si_peer_add()` → `initiate_sync()`
|
||||
2. `initiate_sync()`:
|
||||
- Если `node_id > peer_id` (мастер): шлёт `INIT_SYNC` с `my_count`
|
||||
- Если `node_id < peer_id` (слейв): шлёт `REQUEST_SYNC`
|
||||
|
||||
### 4.2 Обработка INIT_SYNC (слейв)
|
||||
|
||||
1. `db_sync_flush_recalc()` — синхронно завершает все отложенные пересчёты
|
||||
2. Вычисляет `tp = min(peer_count, my_count) - 1` (точка пересечения)
|
||||
3. Вычисляет `ch8_at_tp` — chain_hash8 на позиции tp
|
||||
4. Определяет `has_tail = (my_count > tp + 1)`
|
||||
5. Строит разреженные хеши: от tp назад с интервалами 1,1,1,2,2,4×8
|
||||
6. Отправляет `INIT_RESP` с `tp`, `ch8_at_tp`, `has_tail`, `sparse_hashes`
|
||||
7. Устанавливает `sync_state = 1` для этого пира
|
||||
|
||||
### 4.3 Обработка INIT_RESP (мастер)
|
||||
|
||||
1. `db_sync_flush_recalc()` — синхронно завершает все отложенные пересчёты
|
||||
2. Извлекает `tp`, `peer_ch8`, `has_tail`, `sc`, `sparse_hashes`
|
||||
3. Вычисляет `my_ch8_at_tp`
|
||||
|
||||
**Ветки:**
|
||||
- **peer_ch8 == 0 && sc == 0** (пир пуст): шлёт **все** свои данные батчами по 32 записи. Последний батч с `want_from = my_count` (запрос встречных данных, которые будут пустыми → SYNC_DONE)
|
||||
- **my_ch8 == peer_ch8** (совпадение на tp):
|
||||
- `!has_tail && my_count <= tp+1`: обе стороны идентичны → сразу `SYNC_DONE`
|
||||
- `has_tail`: отправляем свои записи после tp и запрашиваем записи пира (`want_from = tp+1`)
|
||||
- **my_ch8 != peer_ch8** (расхождение): ищем первую различающуюся позицию `fm` через sparse-хеши. `vp = fm-1`. Отправляем данные с fm и запрашиваем данные пира с fm.
|
||||
|
||||
### 4.4 Обработка SEND_DATA
|
||||
|
||||
1. `db_sync_flush_recalc()` — синхронно завершает все отложенные пересчёты
|
||||
2. Парсит `from`, `count`, `vp`, `want_from`
|
||||
3. Для каждой записи (макс. 32): `si_parse_record()` → `db_record_insert()`
|
||||
- Проверка длины тела и подписи до чтения64 байт; проверка подписи конкретных входящих байтов
|
||||
- Если строка уже есть, независимо проверяются сохранённые байты: только доказанная порча допускает DELETE
|
||||
- Проверка дубликата по `(timestamp, author_signature)`
|
||||
- Вычисление chain_hash
|
||||
- Вставка в SQLite, запуск async recalc
|
||||
- Вызов `on_insert` коллбэка
|
||||
4. Корректирует `synced_pos` (и соседних пиров, если запись вставлена не в конец)
|
||||
5. Если `want_from != DB_WANT_FROM_NONE`: отправляет встречный SEND_DATA
|
||||
6. Если `want_from == DB_WANT_FROM_NONE` и `sync_state == 1`: отправляет `SYNC_DONE`
|
||||
|
||||
Ошибка формата, подписи, ключа, SQL или пересчёта прерывает раунд без подтверждения его позиций.
|
||||
Уже успешно вставленные записи остаются в БД; повторный раунд принимает их как дубликаты.
|
||||
При выдаче пачки сохранённая порча удаляется после завершения SELECT в своей транзакции;
|
||||
пачка старого раунда не отправляется. После COMMIT сбрасываются позиции всех пиров и пересчёт начинается с0.
|
||||
|
||||
### 4.5 Обработка SYNC_DONE
|
||||
|
||||
1. `db_sync_flush_recalc()`
|
||||
2. Сравнивает `my_count`, `my_ch8_last` с `peer_count`, `peer_ch8_last`
|
||||
3. **Совпало** → sync_state=2, fire `db_sync_done_cb`
|
||||
4. **my < peer** → запрашиваем хвост SEND_DATA(from=my_count, want=my_count)
|
||||
5. **my > peer** → отправляем хвост SEND_DATA(from=peer_count)
|
||||
6. **count совпало но хеши разные** → принимаем как converged
|
||||
|
||||
### 4.6 PUSH (реальное время)
|
||||
|
||||
1. При локальной вставке (`db_sync_insert_signed`):
|
||||
- Формирует PUSH-сообщение
|
||||
- Рассылает всем пирам с `sync_state >= 1`
|
||||
2. При получении PUSH:
|
||||
- Вставляет запись через `db_record_insert()`
|
||||
- После успешной вставки/корректного повтора шлёт `ACK_PUSH(message_id)` переславшему пиру
|
||||
- Если запись вставлена в середину (не в конец) — сбрасывает `synced_pos` всех пиров до позиции вставки-1
|
||||
3. При получении ACK_PUSH:
|
||||
- Для локального автора помечает запись флагом `WAS_SENT`
|
||||
- Идемпотентно обновляет `delivery_chain`/`delivered_peers` по message_id
|
||||
|
||||
---
|
||||
|
||||
## 5. Безопасность
|
||||
|
||||
1. **Ed25519-подписи:** каждая запись подписана `Ed25519(timestamp || json_data)`. Проверяется при локальной вставке и при приёме от пира.
|
||||
Требуемый контракт удаления: завершённая проверка установила неверность подписи самих сохранённых байтов
|
||||
либо доказан повреждённый формат этой записи. Отсутствие ключа, отказ OpenSSL/памяти/SQL не доказывают порчу.
|
||||
Неверная входящая копия не доказывает неверность корректной строки с тем же ключом.
|
||||
db_verify_author_sig различает VALID/BAD_SIGNATURE/BAD_FORMAT/DEFERRED/ERROR.
|
||||
Операционные ошибки, включая SQLITE_NOMEM при чтении BLOB, сохраняют строки.
|
||||
DELETE и замена повреждённой строки атомарны; сброс позиций и планирование цепочки выполняются после COMMIT.
|
||||
2. **Цепной хеш:** SHA256-цепочка защищает от вставки/удаления записей в середине истории. При проверке (`db_sync_chain_verify`) или при обнаружении расхождения во время отправки — запускается пересчёт с позиции ошибки.
|
||||
3. **Публичные ключи:** Ed25519 публичный ключ пира получается из трёх источников (по приоритету):
|
||||
- Локальный `inst->my_ed25519_pubkey` (для своих записей)
|
||||
- `topo_node_sqlite_get_ed25519_pubkey()` (из БД узлов)
|
||||
- `conn->peer_ed25519_pubkey` (из ETCP handshake)
|
||||
4. **Удаление:** кроме TTL неподтверждённых, удаляются доказанно повреждённые сохранённые строки.
|
||||
Полный аудит TTL, финального сравнения хешей и callbacks ещё не завершён.
|
||||
|
||||
---
|
||||
|
||||
## 6. Конфигурация
|
||||
|
||||
```ini
|
||||
[global]
|
||||
db_sync_enabled = 1 # включить модуль (по умолчанию 0)
|
||||
db_sync_ttl = 86400 # TTL неподтверждённых записей в секундах (по умолчанию 86400)
|
||||
db_path = /var/lib/utun # путь к БД (по умолчанию /tmp/utun_db_sync)
|
||||
```
|
||||
# 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 покрывает `timestamp[8 в порядке хоста] || точные байты data`.
|
||||
Тело имеет длину 1–4000 байт, подпись — 64 байта. Формат подписанного тела сохранён.
|
||||
|
||||
`message_id = SHA256("utun-chat-message-v1" || BE64(group_id) || BE64(timestamp) || signature)`.
|
||||
Порядок набора — лексикографический по полному message_id[32].
|
||||
Лист хеширует канонические записи в этом порядке. В запись входят message_id,
|
||||
timestamp, author node_id, длина и точные байты тела, подпись. Дерево и границы
|
||||
листьев описаны в [merkle_sync_doc.md](merkle_sync_doc.md).
|
||||
|
||||
Локальные id, flags, local_attrs, delivered_peers и delivery_chain в корень не входят.
|
||||
Поэтому одинаковая подписанная история даёт одинаковый корень при разных локальных id.
|
||||
Два сообщения одного автора в одной миллисекунде различаются подписью и message_id.
|
||||
|
||||
Допуск пересылающего пира проверяется отдельно: требуется 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 и запускает:
|
||||
|
||||
1. Проверку до 16 сохранённых строк по rowid за один шаг. Формат/подписи читаются
|
||||
и проверяются до write-транзакции. Внутри короткой транзакции строки перечитываются
|
||||
и сверяются с проверенными байтами; доказанная порча удаляется, message_id/bucket
|
||||
восстанавливаются. Курсор сдвигается только после COMMIT.
|
||||
2. Пересчёт до 16 непустых листьев и их путей за транзакцию.
|
||||
3. Публикацию готовности корня и запуск разрешённых 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 и автоматически запускают повтор.
|
||||
При прекращении изменений, доступных ключах и исправном транспорте обмен объединяет
|
||||
неизменяемые допустимые записи. Постоянный конфликт/ошибка возвращается как ошибка,
|
||||
без ложного done. TTL не создаёт распределённого tombstone: другая копия может вернуть
|
||||
ранее удалённую локально запись.
|
||||
|
||||
## PUSH и доставка
|
||||
|
||||
Заголовок: `service:1 | group_id:8BE | type:1`.
|
||||
PUSH (0x05) содержит одну каноническую запись:
|
||||
|
||||
`message_id:32 | timestamp:8BE | author:8BE | data_len:4BE | data | signature:64`.
|
||||
|
||||
ACK_PUSH (0x06) содержит ровно message_id[32]. После успешной вставки либо допустимого
|
||||
повтора получатель отправляет ACK. Постановка PUSH в очередь не считается доставкой.
|
||||
Один идемпотентный UPDATE учитывает уникальный peer_id в delivery_chain и увеличивает
|
||||
delivered_peers только один раз. WAS_SENT выставляется для локально созданной записи.
|
||||
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.
|
||||
PRIMARY KEY — (timestamp, author_signature), message_id UNIQUE, bucket — старшие 25 бит ключа.
|
||||
chain_hash удалён. При перестройке схемы подписанные и локальные поля сохраняются;
|
||||
производные поля вычисляются заново. DDL/copy/rename/COMMIT атомарны.
|
||||
Записи с доказанно неверной длиной подписи удаляются с диагностикой; подписи корректной
|
||||
длины проверяет последующий preflight.
|
||||
|
||||
TTL сравнивает миллисекунды Unix и удаляет только старые собственные записи без WAS_SENT.
|
||||
За транзакцию удаляется максимум 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 проверяет сохранность полей и rollback миграции.
|
||||
test_dm_invite проверяет настоящий JOIN и восстановление истории после таймаута.
|
||||
|
||||
Индекс предполагает запись содержимого через модель. Произвольное внешнее изменение
|
||||
SQLite не выявляется непрерывно: оно обнаруживается при перепроверке/чтении соответствующих
|
||||
данных. Это не заменяет отдельную проверку целостности файла SQLite.
|
||||
@@ -1,4 +1,4 @@
|
||||
# merkle_sync — протокол 3
|
||||
# merkle_sync — протокол 4
|
||||
|
||||
Аудит 26.10.2026 частичный: движок и адаптер участников прочитаны полностью,
|
||||
контракты ниже уточнены; дефекты очистки member_sync и запуска feedback исправлены.
|
||||
@@ -12,17 +12,18 @@
|
||||
- merkle_sync: обмен, backpressure и жизненный цикл сессий.
|
||||
- member_sync: формат участников, подписи, версии, применение страниц.
|
||||
- chat_feedback: подписанные состояния реакций/ответов, ver, отмены и применение страниц.
|
||||
- db_sync: подписанная история MESSAGES, первоначальная проверка SQLite, страницы и PUSH/ACK.
|
||||
|
||||
Движок не знает CHAT-групп. Namespace — каноническая десятичная строка uint64_t.
|
||||
Все операции выполняются в одном потоке uasync экземпляра.
|
||||
Сессия определяется тройкой (peer, dataset, namespace). MEMBERS=0, FEEDBACK=1.
|
||||
Сессия определяется тройкой (peer, dataset, namespace). MEMBERS=0, FEEDBACK=1, MESSAGES=2.
|
||||
В индексе SQLite namespace хранится как `dataset:channel_id`; корни наборов независимы.
|
||||
|
||||
## Дерево и транзакции
|
||||
|
||||
Корень имеет уровень 0. Уровни 1–5 разбивают старшие 25 бит ключа на группы по 5 бит.
|
||||
Полный ключ курсора — 32 байта с лексикографическим порядком; ветвь задаётся первыми 8 байтами.
|
||||
Для участника ключ — BE64(node_id) и 24 нулевых байта. Для feedback — SHA256 идентичности записи.
|
||||
Для участника ключ — BE64(node_id) и 24 нулевых байта. Для feedback — SHA256 идентичности записи, для истории — message_id.
|
||||
Совпадение первых 64 бит не объединяет записи и не пропускает их при пагинации.
|
||||
SHA256 листа: `0x4d || level:1 || item_hashes`, записи упорядочены по unsigned key.
|
||||
SHA256 родителя: `0x4d || level:1 || child_hash[0] || ... || child_hash[31]`.
|
||||
@@ -37,11 +38,19 @@ SHA256 родителя: `0x4d || level:1 || child_hash[0] || ... || child_hash[
|
||||
## API
|
||||
|
||||
init принимает service и data_ops начальной модели MEMBERS. register добавляет другие наборы.
|
||||
Каждая модель задаёт update_bucket_hash, get_page, apply_items,
|
||||
необязательные validate_peer и finish.
|
||||
Каждая модель задаёт update_bucket_hash, get_page и способ применения страницы,
|
||||
необязательные validate_peer, can_run, on_result и finish.
|
||||
update_bucket_hash добавляет хеши одного листа в переданный SHA256-контекст.
|
||||
get_page получает ёмкость буфера и курсор; возвращает размер, последний ключ и флаг продолжения.
|
||||
apply_items проверяет весь формат и применяет страницу вместе с индексом атомарно.
|
||||
Вместо apply_items можно задать apply_page: он также получает prefix, after, next и more
|
||||
и проверяет принадлежность фактических записей запрошенному листу/диапазону.
|
||||
validate_peer разрешает обмен только при возврате 1; проверяется и перед отправкой.
|
||||
can_run задаёт политику работы модели; без него peer_sleep_phase откладывает обмен.
|
||||
История разрешает sound_on для спящего пира. recheck_peer завершает запрещённые
|
||||
попытки, освобождая пакет/waiter/таймер и сохраняя подписку для последующего start/changed.
|
||||
on_result получает результаты всех раундов, включая автоматические, до пользовательских callbacks;
|
||||
он может отменить сессию. Указатели и список пользовательских callbacks захватываются заранее.
|
||||
finish проверяет отложенные зависимости модели перед подтверждением корня.
|
||||
|
||||
Вместо непосредственного apply_items модель может задать все четыре callback:
|
||||
@@ -58,6 +67,7 @@ MT_OK означает совпадение корней конкретного
|
||||
|
||||
cancel отменяет только заданный dataset/peer/namespace.
|
||||
cancel_ns отменяет все datasets канала, cancel_peer — все datasets пира.
|
||||
cancel_dataset_ns/cancel_dataset_peer ограничивают отмену выбранным набором.
|
||||
Все cancel и destroy освобождают запросы без callback.
|
||||
DOWN/DELETE/REINIT завершают запросы ошибкой и освобождают сессию.
|
||||
После восстановления соединения интеграционный слой повторяет start.
|
||||
@@ -102,6 +112,8 @@ WAKE также перезапускает незавершённый раунд
|
||||
Все дальнейшие пакеты проверяют оба идентификатора, номер запроса и фазу. После cancel/start старый BEGIN может получить
|
||||
новый challenge, но прежние PAGE/DONE/ERROR не соответствуют новому accept_id. Дубликат BEGIN не меняет текущий challenge.
|
||||
WAKE содержит нулевые идентификаторы, BEGIN — нулевой accept_id. Старый wire не поддерживается.
|
||||
Ошибка генератора случайных байтов завершает запрос MT_ERR_IO до установки нового раунда.
|
||||
Дубликат READY с тем же accept_id не перезапускает текущий обход.
|
||||
Все целые big-endian. Максимальный пакет вместе с заголовком — 16384 байта.
|
||||
Типы 0x10–0x19: WAKE, BEGIN, READY, QUERY, HASHES, PAGE, TURN, CHECK, DONE, ERROR.
|
||||
|
||||
@@ -134,6 +146,9 @@ test_merkle_sync проверяет индекс, односторонний sta
|
||||
test_merkle_protocol проверяет ошибочные пакеты/курсоры, сохранение соседних ветвей,
|
||||
отмену waiter, ошибки apply/SQLite, backpressure, disconnect и повторное подключение,
|
||||
изоляцию datasets и пагинацию разных ключей с одинаковым 64-битным префиксом.
|
||||
Дополнительные регрессии: cancel/start и старые PAGE/DONE/ERROR, дубликаты BEGIN/READY,
|
||||
отказ RNG обеих сторон, политика спящего пира и восстановление допуска на прежнем соединении.
|
||||
История и её ограниченные транзакции описаны в [db_sync_arch.md](db_sync_arch.md).
|
||||
test_chat_feedback проверяет подписи, монотонный ver, отмены, старые записи,
|
||||
конфликты одного ver, атомарность страниц, восстановление дерева и удаление канала.
|
||||
|
||||
|
||||
Reference in new issue
Block a user