# db_sync — Distributed append-only table with SQLite + P2P sync ## 1. Назначение Реплицировать append-only таблицу JSON-записей между всеми пирами P2P-сети. Каждый пир в итоге должен иметь идентичный набор записей. Модуль поддерживает несколько независимых инстансов (таблиц), каждый идентифицируется хешем `SHA256(name || id_be)[0:8]`. ## 2. Ключевые свойства - **Криптографическая цепь.** `chain_hash[N] = SHA256(chain_hash[N-1] || id || timestamp || author || author_signature)`. Записи упорядочены `ORDER BY timestamp, author_signature`. Первые 8 байт — `chain_hash8` — используется для быстрого сравнения в протоколе. - **Ed25519-подписи.** Каждая запись подписана автором. Записи без подписи или с неверной подписью отвергаются. - **Append-only.** Записи не редактируются и не удаляются явно, только TTL-очистка собственных неотправленных записей. - **PUSH — мгновенная доставка.** При локальном `insert_signed` запись немедленно шлётся всем пирам с `sync_state >= 1` (steady state). - **Sync — сравнение цепей.** При старте/реконнекте стороны обмениваются хешами цепей для поиска расхождений. - **Multi-instance.** Несколько независимых таблиц внутри одного процесса (разные hash). ## 3. Архитектура протокола ### 3.1. На что оптимизирован 99% времени новые записи просто дописываются в конец. Самый частый сценарий — пир A добавил сообщение, пир B получил его и вставил в конец своей цепи. Протокол должен: - Доставлять новые записи немедленно (PUSH) - При реконнекте быстро понять "у нас всё совпадает до позиции N, добрось хвост" (hash_MATCH) - При реальном расхождении бинарным поиском найти точку и слить (divergence + REFINE) ### 3.2. Общие идеи реализации **Криптографическая цепь** — гарантия целостности. Если `chain_hash8(N)` совпадает на двух пирах, цепь идентична до позиции N (вероятность коллизии 2^-64). При вставке не в конец — каскадный пересчёт `chain_hash` всех последующих записей через `db_cascade_from`. **PUSH** — рабочий механизм доставки в steady state. Запись, вставленная локально, немедленно уходит всем синхронизированным пирам. ACK_PUSH подтверждает получение и обновляет delivery_chain. **Sync** — полное сравнение цепей при старте/реконнекте. Три ветки: peer_empty (пир пуст — отдать всё), hash_MATCH (цепи совпали до позиции N — отдать хвост), divergence (цепи разошлись — найти точку расхождения). **Sparse checkpoints** — бинарный поиск расхождения. INIT_RESP возвращает хеши в степенях двойки от tp (tp-1, tp-2, tp-4, ..., до 16 шт). Это позволяет за O(log N) сравнений сузить диапазон, не передавая хеш каждой записи. **REFINE** — финальное сужение. Если sparse-хешей INIT_RESP недостаточно (диапазон >1), REFINE запрашивает до 16 дополнительных хешей. Когда диапазон ≤1 — сразу SEND_DATA. **Каскадное уведомление.** При вставке записи не в конец `synced_pos` всех остальных пиров сбрасывается до позиции вставки — им потребуется пересинхронизация. ### 3.3. Фазы протокола **Фаза А — Инициализация.** `db_sync_instance_add` читает таблицу из SQLite, проверяет целостность цепи (`db_verify_chain` — автофикс при расхождении), и немедленно шлёт `INIT_SYNC(my_count)` всем подключённым пирам с `sync_state == 0`. Если связь появилась позже — `conn_up` делает то же самое. **Фаза Б — Сравнение цепей.** Получатель INIT_SYNC вычисляет `tp = min(my_count, peer_count)`, tp-- если >0 (последняя гарантированно общая позиция), и возвращает INIT_RESP: `peer_ch8` на позиции tp, `sc` (количество sparse-хешей), sparse-хеши на позициях tp-2^k. **Фаза В — Три ветки:** | Ветка | Условие | Сценарий | Действие | |-------|---------|----------|----------| | peer_empty | `peer_ch8==0 && sc==0` | Пир пуст (0 записей) | Отправить все свои записи через SEND_DATA | | hash_MATCH | `my_ch8 == peer_ch8` | Цепи идентичны до tp | Отправить хвост [tp+1..mc) через SEND_DATA | | divergence | `my_ch8 != peer_ch8` | Разные истории | Анализ sparse-хешей → REFINE → SEND_DATA | **Фаза Г — REFINE.** Инициатор анализирует sparse-хеши: `ds` — последняя совпавшая позиция, `de` — первая разошедшаяся. Если `de-ds ≤ 1` → сразу SEND_DATA (hc=0). Иначе → REFINE с до 16 своих хешей, равномерно распределённых в [ds..de]. Получатель сравнивает со своей цепью, находит точку совпадения, шлёт SEND_DATA от этой точки. **Фаза Д — SEND_DATA.** Получатель вставляет записи с проверкой Ed25519-подписи, делает `db_cascade_from(fix_from)`, уведомляет остальных пиров о сдвиге цепи (сброс их synced_pos). Если у получателя после вставки записей больше чем у отправителя — proactive push-back (шлёт свой хвост). Когда все записи получены → SYNC_DONE с итоговым count и chain_hash8. **Фаза Е — SYNC_DONE.** Сравнение итогового count и chain_hash8. Не совпало — ретрай всей процедуры с начала (до 3 раз, потом give up с partial sync). Совпало — `sync_state=2`, синхронизация завершена. ### 3.4. PUSH — отдельный от sync механизм PUSH матчится по `hash` — если у пира нет si с таким же hash, PUSH не доставляется. Это позволяет изолировать тестирование sync-протокола от PUSH: вставлять данные через si с уникальным hash (PUSH не уходит — нет получателя), затем удалять tmp si (данные в SQLite сохраняются), создавать si с общим hash — instance_add запускает чистый sync. ## 4. Peer management - При поднятии ETCP-соединения для каждого инстанса добавляется `SI_PEER` и запускается синхронизация. - При разрыве соединения `sync_state` пира сбрасывается в 0. - `peer_check` таймер (каждые 5с) перебирает активных пиров и запускает синхронизацию для тех, у кого `sync_state == 0`. Выбирается пир с минимальным `synced_pos` — двигаемся от самого старого несинхронизированного участка. - PUSH рассылается только пирам в состоянии `sync_state >= 1`. ## 5. Сообщения протокола | Сообщение | Wire-формат | Описание | |-----------|-------------|----------| | `INIT_SYNC (0x01)` | `[type:1][count:4]` | Инициатор шлёт количество записей | | `INIT_RESP (0x02)` | `[type:1][tp:4][ch8:8][sc:1][(pos:4,ch8:8)*sc]` | tp, chain_hash8 на tp, sc sparse-хешей | | `REFINE (0x03)` | `[type:1][from:4][to:4][hc:1][(pos:4,ch8:8)*hc]` | hc=0 → сразу SEND_DATA; hc>0 → до 16 хешей в диапазоне | | `SEND_DATA (0x04)` | `[type:1][from:4][count:2][(id:8,ts:8,author:8,dlen:4,data,sig_len:1,sig)*count]` | Пакет до 32 записей | | `PUSH (0x05)` | `[type:1][id:8][ts:8][author:8][dlen:4][data][sig_len:1=64][sig:64]` | Рассылка одной записи synced-пирам | | `ACK_PUSH (0x06)` | `[type:1][ts:8][author:8]` | Подтверждение PUSH, обновление delivery_chain | | `SYNC_DONE (0x07)` | `[type:1][count:4][ch8:8]` | Финальный count + chain_hash8 | | `ERROR (0x08)` | `[type:1][code:1]` | Коды: 0x01=NOT_FOUND, 0x02=DISABLED | Все сообщения маршрутизируются через ETCP service `0x20` с префиксом `[svc:1][hash_be:8][payload]`. ## 6. Wire-формат записи `[id:8][ts:8][author_node_id:8][dlen:4][data:json][sig_len:1=64][sig:ed25519:64]` Дубликаты определяются по `(timestamp, author_signature)`. ## 7. Структуры данных ```c struct DB_SYNC { struct UTUN_INSTANCE* inst; sqlite3* db; uint8_t shared_db; uint64_t last_connected_tb; struct DB_SYNC_INSTANCE* instances; int instance_count, instance_capacity; void* peer_check_timer; uint8_t enabled; }; struct DB_SYNC_INSTANCE { struct DB_SYNC* db_sync; uint64_t hash; char table_name[64]; uint64_t next_id; uint64_t last_timestamp_ms; uint8_t enabled; void* ttl_timer; struct SI_PEER* peers; int peer_count, peer_capacity; db_sync_insert_cb on_insert; void* on_insert_arg; }; struct SI_PEER { uint64_t node_id; uint32_t synced_pos; // последняя общая позиция (0-based) uint8_t sync_state; // 0=idle, 1=syncing, 2=synced uint8_t sync_retry_count; // счётчик retry SYNC_DONE mismatch uint64_t sync_start_tb; // время начала sync (для timeout) }; ``` ## 8. API ### Глобальный жизненный цикл ```c int db_sync_init(struct UTUN_INSTANCE* inst); void db_sync_destroy(struct UTUN_INSTANCE* inst); ``` `db_sync_init` — открывает SQLite по пути `/chats.db`, биндит ETCP service `0x20`, вешает коллбэки соединений, стартует `peer_check` таймер (5с). Если `db_sync_enabled = 0` — disabled-режим. `db_sync_destroy` — отменяет таймеры, анбиндит сервис, снимает коллбэки, закрывает SQLite. ### Управление инстансами ```c struct DB_SYNC_INSTANCE* db_sync_instance_add(struct UTUN_INSTANCE* inst, const char* table_name, uint64_t hash); void db_sync_instance_remove(struct DB_SYNC_INSTANCE* si); ``` `db_sync_instance_add` — создаёт/регистрирует инстанс. Создаёт SQLite-таблицу (если нет), читает цепь, `db_verify_chain` (автофикс), стартует TTL-таймер. Если есть подключённые пиры с `sync_state == 0` — немедленно шлёт `INIT_SYNC`. `db_sync_instance_remove` — деактивирует: `enabled=0`, отменяет TTL-таймер, освобождает peers, удаляет из массива. Таблица БД не удаляется. ### Операции с данными ```c int db_sync_insert_signed(struct DB_SYNC_INSTANCE* si, const char* json_data, size_t len, const uint8_t* sig, size_t sig_len, uint64_t ts); uint32_t db_sync_count(struct DB_SYNC_INSTANCE* si); uint64_t db_sync_get_last_timestamp(struct DB_SYNC_INSTANCE* si); uint64_t db_sync_next_timestamp(struct DB_SYNC_INSTANCE* si); ``` `db_sync_insert_signed` — вставляет подписанную запись. `sig` — 64 байта Ed25519. Проверяет подпись, проверяет дубликат, вычисляет chain_hash, вставляет с каскадным пересчётом, рассылает PUSH synced-пирам. Возвращает: 0=успех, 1=дубликат, -1=ошибка, -2=неверная подпись. ### Чтение ```c typedef void (*db_sync_select_cb)(void* arg, uint64_t id, uint64_t timestamp, const char* data, size_t data_len, uint64_t author, const uint8_t* author_sig, size_t sig_len, int delivered_peers, const char* delivery_chain); int db_sync_select(struct DB_SYNC_INSTANCE* si, uint32_t offset, uint32_t limit, db_sync_select_cb cb, void* arg); ``` ### Callback на вставку ```c typedef void (*db_sync_insert_cb)(struct DB_SYNC_INSTANCE* si, uint64_t record_timestamp, const char* json_data, size_t len, uint64_t author_node_id, void* arg); void db_sync_set_insert_cb(struct DB_SYNC_INSTANCE* si, db_sync_insert_cb cb, void* arg); ``` ### Верификация цепи ```c int db_sync_chain_verify(struct DB_SYNC_INSTANCE* si); ``` Возвращает 0 если все chain_hash корректны, 1 если расхождение. ## 9. Конфигурация | Параметр | По умолчанию | Описание | |----------|-------------|----------| | `db_sync_enabled` | `0` | Включить модуль синхронизации | | `db_sync_ttl` | `86400` | TTL неотправленных собственных записей (сек) | | `db_path` | — | Путь к БД; файл `/chats.db` | ## 10. Константы | Константа | Значение | Описание | |-----------|---------|----------| | `ETCP_RT_ID_DB_SYNC` | `0x20` | ETCP service ID | | `DB_REFINE_HASHES` | `16` | Макс. хешей в REFINE | | `DB_SEND_DATA_MAX` | `32` | Макс. записей в SEND_DATA | | `DB_SIG_SIZE` | `64` | Размер Ed25519 подписи | | `DB_SYNC_PEER_CHECK_INTERVAL` | `5` | Интервал проверки пиров (сек) | | `DB_SYNC_SYNC_TIMEOUT` | `15` | Таймаут ожидания ответа при sync (сек) | | `DB_SYNC_TTL_INTERVAL` | `3600` | Интервал TTL-очистки (сек) | ## 11. Тестирование sync-протокола PUSH доставляет записи немедленно и независимо от sync. Чтобы тестировать чистый sync-протокол, нужно чтобы PUSH не вмешивался: 1. Вставить данные через si с **уникальным hash** — PUSH уходит, но не доставляется (нет получателя с таким hash) 2. `remove_si()` — данные сохраняются в SQLite, si деактивирован 3. Создать si с **общим hash** на обеих сторонах — `instance_add` запускает чистый sync-протокол Этот паттерн используется для тестирования всех трёх веток INIT_RESP: - **peer_empty**: одна сторона с данными, другая пустая - **hash_MATCH**: обе имеют общий префикс, но у одной больше записей - **divergence**: независимые вставки на обеих сторонах до синхронизации