Browse Source
- SoundManager: add currentPcmCursor()/isCurrentPcmPlaying() via ma_sound_get_cursor_in_pcm_frames / ma_sound_is_playing - messagedelegate: replace wall-clock position tracking with real miniaudio cursor, remove 200ms hardcoded delay - messagedelegate: end-of-playback detection via isCurrentPcmPlaying() instead of wall-clock comparison (was killing audio ~200ms early) - messagedelegate: EMA smoothing of displayFrac/displayElapsed (0.15 factor, 16ms timer) - messagedelegate: timer continues after audio stops to animate EMA decay to 0 (not mouse-move dependent) - media_delivery: download progress callback, Ed25519 sig verify, author fallback, content hash check - conn_mgr: already-connected returns OK with callback instead of error - chatgui: voice download-pending auto-play, file download progress bar, media bar colorstopo_upd
23 changed files with 1165 additions and 162 deletions
@ -0,0 +1,530 @@
|
||||
# db_sync — децентрализованная реплицируемая таблица с цепным хешированием |
||||
|
||||
## 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`) |
||||
- `hash` — instance_hash = первые 8 байт SHA256(name || id_be) для маршрутизации сообщений |
||||
- `next_id` — автоинкрементный ID записей |
||||
- `last_timestamp_ms` — последний использованный timestamp (монотонный) |
||||
- `peers[]` — динамический массив SI_PEER (состояние синхронизации с каждым пиром) |
||||
- `recalc` — отложенный пересчёт цепных хешей (асинхронно, батчами по 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 байта |
||||
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** | Пересчёт `chain_hash` батчами по 50 записей (через `uasync_set_timeout`) — не блокирует event loop | |
||||
| **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_RT_ID_DB_SYNC = 0x20` |
||||
- **Биндинг:** `etcp_bind(inst, 0x20, 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 │ |
||||
│ └──────────────┘ └───────────────┘ |
||||
│ │ |
||||
└─────────────────────────────────────────────┘ |
||||
CONN DOWN / ERROR (DB_ERR_NOT_FOUND, DB_ERR_DISABLED) |
||||
``` |
||||
|
||||
### 2.2 Диаграмма протокола синхронизации |
||||
|
||||
``` |
||||
MASTER (node_id > peer) SLAVE (node_id < peer) |
||||
─────────────────────── ────────────────────── |
||||
|
||||
conn UP → 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) │ |
||||
│ ├─ etcp_add_conn_status_cbk(conn_status) │ |
||||
│ └─ запускает peer_check_timer (5s) │ |
||||
│ │ |
||||
│ ... позже, при создании канала ... │ |
||||
│ │ |
||||
│ db_sync_instance_add(inst, "msg_GENERAL", hash, 1) │ |
||||
│ ├─ CREATE TABLE IF NOT EXISTS... │ |
||||
│ ├─ загружает next_id = MAX(id)+1 │ |
||||
│ ├─ db_verify_chain() — проверка целостности цепных хешей │ |
||||
│ ├─ запускает TTL-таймер (1 час) │ |
||||
│ └─ для всех активных соединений: si_peer_add() + initiate_sync() │ |
||||
├────────────────────────────────────────────────────────────────────────────┤ |
||||
│ │ |
||||
│ ┌─── DB_SYNC_INSTANCE активен ───────────────────────────────────────────┐│ |
||||
│ │ ││ |
||||
│ │ conn UP ──► on_conn_up() ││ |
||||
│ │ └─ для всех enabled инстансов: ││ |
||||
│ │ si_peer_add() → initiate_sync() ││ |
||||
│ │ ││ |
||||
│ │ conn DOWN ──► on_conn_down() ││ |
||||
│ │ └─ сброс sync_state для всех инстансов ││ |
||||
│ │ ││ |
||||
│ │ recv_cb() — приём сообщений ││ |
||||
│ │ ├─ извлекает instance_hash, type, payload ││ |
||||
│ │ ├─ lazy-register если инстанс не найден ││ |
||||
│ │ └─ диспетчеризация по 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) │ hash_be (8) │ payload (variable) │ |
||||
│ ETCP_RT_ID=0x20 │ instance_hash│ зависит от type │ |
||||
└─────────────────┴──────────────┴──────────────────────────────┘ |
||||
``` |
||||
|
||||
**instance_hash** = первые 8 байт `SHA256(table_name || id_be)` в big-endian. |
||||
Это позволяет приёмной стороне найти `DB_SYNC_INSTANCE` по хешу. |
||||
|
||||
### 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) | |
||||
| `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` — длина JSON-данных (uint32, network byte order) |
||||
- `data` — JSON-данные (dlen байт) |
||||
- `sig_len` — длина подписи, всегда 64 (uint8) |
||||
- `sig` — Ed25519-подпись: `Ed25519(ts[8] || data[dlen])`, 64 байта |
||||
|
||||
**Note:** поля `id`, `ts`, `author` в host byte order (LE на x86), а `dlen` в network byte order (BE). |
||||
|
||||
### 3.6 DB_MSG_PUSH (0x05) |
||||
|
||||
**Отправитель:** автор записи → все synced-пиры |
||||
**Назначение:** рассылка новой записи в реальном времени |
||||
|
||||
Формат идентичен одной record из SEND_DATA: |
||||
|
||||
``` |
||||
┌──────┬───────┬───────┬──────────┬──────────┬────────────┬──────────┬──────────┐ |
||||
│ 0x05 │ id(8) │ ts(8) │ author(8)│ dlen(4BE)│ data(dlen) │ sig_len │ sig(64) │ |
||||
│ │ │ │ │ │ │ =64 (1) │ │ |
||||
└──────┴───────┴───────┴──────────┴──────────┴────────────┴──────────┴──────────┘ |
||||
минимальный размер: 29 + 1 + 64 = 94 bytes (при dlen=0) |
||||
``` |
||||
|
||||
### 3.7 DB_MSG_ACK_PUSH (0x06) |
||||
|
||||
**Отправитель:** получатель PUSH → автору |
||||
**Назначение:** подтверждение получения PUSH-записи |
||||
|
||||
``` |
||||
┌──────┬──────────┬──────────────┐ |
||||
│ 0x06 │ ts (8) │ author (8) │ |
||||
└──────┴──────────┴──────────────┘ |
||||
total: 17 bytes |
||||
``` |
||||
|
||||
- `ts` — timestamp записи (uint64, host byte order) |
||||
- `author` — node_id автора (uint64, host byte order) |
||||
|
||||
При получении ACK_PUSH автор помечает запись флагом `DB_REC_FLAG_WAS_SENT` (0x01) и обновляет `delivery_chain`/`delivered_peers`. |
||||
|
||||
### 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 | Инстанс с таким hash не найден | |
||||
| `DB_ERR_DISABLED` | 0x02 | Инстанс существует, но disabled | |
||||
|
||||
При получении ERROR сбрасывается `sync_state` пира в 0. |
||||
|
||||
### 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()` |
||||
- Проверка Ed25519-подписи (если неверна — удаление записи) |
||||
- Проверка дубликата по `(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` |
||||
|
||||
### 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` |
||||
- Обновляет `delivery_chain` для каждого пира |
||||
2. При получении PUSH: |
||||
- Вставляет запись через `db_record_insert()` |
||||
- Шлёт `ACK_PUSH` обратно автору |
||||
- Если запись вставлена в середину (не в конец) — сбрасывает `synced_pos` всех пиров до позиции вставки-1 |
||||
3. При получении ACK_PUSH: |
||||
- Помечает запись флагом `WAS_SENT` |
||||
- Обновляет `delivery_chain` |
||||
|
||||
--- |
||||
|
||||
## 5. Безопасность |
||||
|
||||
1. **Ed25519-подписи:** каждая запись подписана `Ed25519(timestamp || json_data)`. Проверяется всегда — и при локальной вставке, и при приёме от пира. Невалидные записи отбрасываются и удаляются из БД. |
||||
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-очистка неподтверждённых. |
||||
|
||||
--- |
||||
|
||||
## 6. Конфигурация |
||||
|
||||
```ini |
||||
[global] |
||||
db_sync_enabled = 1 # включить модуль (по умолчанию 0) |
||||
db_sync_ttl = 86400 # TTL неподтверждённых записей в секундах (по умолчанию 86400) |
||||
db_path = /var/lib/utun # путь к БД (по умолчанию /tmp/utun_db_sync) |
||||
``` |
||||
@ -0,0 +1,158 @@
|
||||
/**
|
||||
* @file test_conn_mgr_already_connected.c |
||||
* @brief conn_mgr: подключение к уже-соединённому узлу через instance_find_conn |
||||
* |
||||
* Проверяет, что conn_mgr_connect_node находит существующее ETCP-соединение |
||||
* и сразу вызывает callback с CONN_MGR_OK, а не уходит в фазы установки нового соединения. |
||||
*/ |
||||
#include <stdio.h> |
||||
#include <stdlib.h> |
||||
#include <string.h> |
||||
#include <stdarg.h> |
||||
#include "../lib/platform_compat.h" |
||||
#include "test_utils.h" |
||||
#ifndef _WIN32 |
||||
#include <unistd.h> |
||||
#endif |
||||
|
||||
#include "etcp.h" |
||||
#include "etcp_connections.h" |
||||
#include "../src/config_parser.h" |
||||
#include "../src/config_updater.h" |
||||
#include "../src/utun_instance.h" |
||||
#include "topo_group.h" |
||||
#include "conn_mgr.h" |
||||
#include "../lib/u_async.h" |
||||
#include "../lib/debug_config.h" |
||||
#include "../lib/mem.h" |
||||
|
||||
#define TOTAL_TIMEOUT_TB 10000 /* 1 sec */ |
||||
#define POLL_MS 5 |
||||
static struct UTUN_INSTANCE *g_a = NULL, *g_b = NULL; |
||||
static struct UASYNC *ua = NULL; |
||||
static volatile int result = 0, cb_fired = 0, cb_ok = 0; |
||||
static char tdir[] = "/tmp/utun_cm2_XXXXXX"; |
||||
static char ca[256], cb[256]; |
||||
static int pa = 0, pb = 0; |
||||
|
||||
static int wf(const char* p, const char* f, ...) { |
||||
va_list ap; FILE* fp = fopen(p, "w"); if (!fp) return -1; |
||||
va_start(ap, f); vfprintf(fp, f, ap); va_end(ap); fclose(fp); return 0; |
||||
} |
||||
static char* gv(const char* p, const char* k) { |
||||
struct utun_config* c = parse_config(p); if (!c) return NULL; |
||||
char* r = (strcmp(k, "pub") == 0) ? u_strdup(c->global.my_public_key_hex) : u_strdup(c->global.my_private_key_hex); |
||||
free_config(c); return r; |
||||
} |
||||
static void fail(const char* msg) { fprintf(stderr, "FAIL: %s\n", msg); fflush(stderr); result = 2; } |
||||
static void done(void) { if (result == 0) result = 1; } |
||||
static void test_mgr_connect(void* arg); |
||||
static void test_check(void* arg); |
||||
static void test3_preconnect(void* arg); |
||||
static void test3_check(void* arg); |
||||
|
||||
static void conn_cb(int r, uint64_t id, void* arg) { |
||||
(void)arg; |
||||
cb_fired = 1; cb_ok = (r == CONN_MGR_OK); |
||||
fprintf(stderr, "conn_cb: result=%d node=0x%llx ok=%d\n", r, (unsigned long long)id, cb_ok); fflush(stderr); |
||||
} |
||||
|
||||
static void test_find_conn(void* arg) { |
||||
(void)arg; |
||||
struct ETCP_CONN* c = instance_find_conn(g_a, g_b->node_id); |
||||
if (!c || !c->links) { uasync_set_timeout(ua, 50, NULL, (timeout_callback_t)test_find_conn, "t1w"); return; } |
||||
int has = 0; struct ETCP_LINK* l = c->links; |
||||
while (l) { if (l->link_state == 3 && l->initialized) { has = 1; break; } l = l->next; } |
||||
if (!has) { uasync_set_timeout(ua, 50, NULL, (timeout_callback_t)test_find_conn, "t1w2"); return; } |
||||
fprintf(stderr, "Test 1: instance_find_conn OK (link_state=%d)\n", l->link_state); fflush(stderr); |
||||
uasync_call_soon(ua, NULL, test_mgr_connect); |
||||
} |
||||
|
||||
static void test_mgr_connect(void* arg) { |
||||
(void)arg; |
||||
cb_fired = 0; cb_ok = 0; |
||||
struct CONN_MGR* mgr = g_a->conn_mgr; |
||||
struct TOPO_NODEQ* nq = topo_node_find_by_id(mgr->group, g_b->node_id); |
||||
if (nq) { fprintf(stderr, "node already in UTUN group\n"); fflush(stderr); } |
||||
fprintf(stderr, "Test 2: conn_mgr_connect_node(B)\n"); fflush(stderr); |
||||
conn_mgr_connect_node(mgr, g_b->node_id, 0, conn_cb, NULL); |
||||
uasync_call_soon(ua, NULL, test_check); |
||||
} |
||||
|
||||
static void test_check(void* arg) { |
||||
(void)arg; |
||||
if (!cb_fired) { fail("callback not fired"); done(); return; } |
||||
if (!cb_ok) { fail("callback result != CONN_MGR_OK"); done(); return; } |
||||
uint8_t st, ty; |
||||
conn_mgr_get_status(g_a->conn_mgr, g_b->node_id, &st, &ty); |
||||
if (st != CONN_MGR_STATE_CONNECTED) { fail("state != CONNECTED"); done(); return; } |
||||
fprintf(stderr, "Test 2: OK state=%d type=%d\n", st, ty); fflush(stderr); |
||||
fprintf(stderr, "=== ALL DONE ===\n"); fflush(stderr); |
||||
done(); |
||||
} |
||||
|
||||
static void test3_preconnect(void* arg) { |
||||
/* Test 3: conn_mgr_connect_node BEFORE ETCP link is up (no instance_find_conn match) */ |
||||
(void)arg; |
||||
struct CONN_MGR* mgr = g_a->conn_mgr; |
||||
cb_fired = 0; cb_ok = 0; |
||||
struct ETCP_CONN* c = instance_find_conn(g_a, g_b->node_id); |
||||
if (c) { fprintf(stderr, "Test3: SKIP — conn already established\n"); return; } |
||||
fprintf(stderr, "Test 3: conn_mgr_connect_node BEFORE link up\n"); fflush(stderr); |
||||
conn_mgr_connect_node(mgr, g_b->node_id, 0, conn_cb, NULL); |
||||
uasync_call_soon(ua, NULL, test3_check); |
||||
} |
||||
|
||||
static void test3_check(void* arg) { |
||||
(void)arg; |
||||
if (cb_fired) { |
||||
if (cb_ok) { fprintf(stderr, "Test 3: OK — callback CONN_MGR_OK\n"); fflush(stderr); return; } |
||||
fail("callback fired with error"); done(); return; |
||||
} |
||||
/* no callback yet — check if conn_mgr gave up (state reset) */ |
||||
uint8_t st, ty; |
||||
if (conn_mgr_get_status(g_a->conn_mgr, g_b->node_id, &st, &ty) == CONN_MGR_OK && st == CONN_MGR_STATE_DISCONNECTED) { |
||||
/* entry was cleaned up — conn not possible in this test setup with this timing */ |
||||
fprintf(stderr, "Test 3: SKIP — entry cleaned up (conn not yet established in time)\n"); fflush(stderr); |
||||
return; |
||||
} |
||||
uasync_set_timeout(ua, 30, NULL, (timeout_callback_t)test3_check, "t3w"); |
||||
} |
||||
|
||||
static void to_cb(void* arg) { (void)arg; fail("timeout"); done(); } |
||||
|
||||
int main(void) { |
||||
debug_config_init(); debug_set_level(DEBUG_LEVEL_WARN); |
||||
utun_instance_set_tun_init_enabled(0); |
||||
|
||||
test_mkdtemp(tdir); |
||||
int base = 49000 + (getpid() % 10000); pa = base; pb = base + 1; |
||||
snprintf(ca, sizeof(ca), "%s/a.conf", tdir); snprintf(cb, sizeof(cb), "%s/b.conf", tdir); |
||||
wf(ca, "[global]\ntun_ip=10.97.0.1/24\ntun_ifname=tun95\n[server: s1]\naddr=127.0.0.1:%d\ntype=public\n[allowed_keys]\nallow_all=1\n", pa); |
||||
wf(cb, "[global]\ntun_ip=10.97.0.2/24\ntun_ifname=tun94\n[server: s1]\naddr=127.0.0.1:%d\ntype=public\n[allowed_keys]\nallow_all=1\n", pb); |
||||
config_ensure_keys_and_node_id(ca); config_ensure_keys_and_node_id(cb); |
||||
{ struct utun_config* cfa = parse_config(ca); struct utun_config* cfb = parse_config(cb); |
||||
free_config(cfa); free_config(cfb); } |
||||
char *r0 = gv(ca,"priv"), *p0 = gv(ca,"pub"), *p1 = gv(cb,"pub"); |
||||
wf(ca, "[global]\nmy_private_key=%s\nmy_public_key=%s\ntun_ip=10.97.0.1/24\ntun_ifname=tun95\n[server: s1]\naddr=127.0.0.1:%d\ntype=public\n[client: to_b]\nkeepalive=1\npeer_public_key=%s\nlink=s1:127.0.0.1:%d\n[allowed_keys]\nallow_all=1\n", r0, p0, pa, p1, pb); |
||||
u_free(r0); u_free(p0); u_free(p1); |
||||
|
||||
ua = uasync_create(); |
||||
g_a = utun_instance_create(ua, ca); g_b = utun_instance_create(ua, cb); |
||||
if (!g_a || !g_b) { fprintf(stderr,"create failed\n"); result=2; goto done; } |
||||
utun_instance_init(g_a); utun_instance_init(g_b); |
||||
|
||||
/* Test 3: call conn_mgr BEFORE link is up */ |
||||
uasync_call_soon(ua, NULL, test3_preconnect); |
||||
/* Test 1+2: run after link is established */ |
||||
uasync_call_soon(ua, NULL, test_find_conn); |
||||
uasync_set_timeout(ua, TOTAL_TIMEOUT_TB, NULL, to_cb, "to"); |
||||
{ int el = 0; while (!result && el < TOTAL_TIMEOUT_TB / 10 + 500) { uasync_poll(ua, POLL_MS); el += POLL_MS; } } |
||||
|
||||
done: |
||||
if (g_a) { g_a->running = 0; utun_instance_destroy(g_a); } |
||||
if (g_b) { g_b->running = 0; utun_instance_destroy(g_b); } |
||||
if (ua) uasync_destroy(ua, 0); |
||||
test_unlink(ca); test_unlink(cb); test_rmdir(tdir); |
||||
return (result == 1) ? 0 : 1; |
||||
} |
||||
Loading…
Reference in new issue