# db_sync — Distributed content-addressed table with SQLite + P2P sync ## 1. Назначение Децентрализованная реплицируемая таблица JSON-записей, синхронизируемая между всеми узлами сети через ETCP. Каждый узел хранит полную копию данных каждого инстанса. Модуль поддерживает несколько независимых инстансов (таблиц), каждый идентифицируется хешем `SHA256(name || id_be)[0:8]`. **Ключевые свойства:** - **Content-addressed**: целостность цепочки гарантируется `chain_hash` — каскадным SHA256, где `chain_hash[n] = SHA256(chain_hash[n-1] || id || timestamp || author || author_signature)`. - **Ed25519-подписи**: каждая запись обязательно подписана автором (`Ed25519(timestamp || json_data)`). Записи без подписи или с неверной подписью отвергаются. - **Multi-instance**: несколько независимых таблиц внутри одного процесса (напр. `("chats", 1)` и `("chats", 2)`). - **Ordered**: записи упорядочены по `(timestamp, author_signature)`. Первичный ключ — та же пара, определяющая уникальность (дубликаты по тому же автору в ту же миллисекунду невозможны). - **Append-only**: записи не редактируются и не удаляются явно, только TTL-очистка собственных неотправленных записей. - **Sync protocol**: 8 типов сообщений (INIT_SYNC → INIT_RESP → REFINE → SEND_DATA → SYNC_DONE, плюс PUSH/ACK_PUSH/ERROR). Используется бинарный поиск расхождений по chain_hash. - **Push**: новые записи немедленно рассылаются (PUSH) всем синхронизированным пирам. ## 2. Как пользоваться ### Типовой сценарий ``` 1. В конфиге: db_sync_enabled = 1, db_sync_ttl = 86400 2. db_sync_init(inst) — вызывается автоматически при старте utun_instance 3. struct DB_SYNC_INSTANCE* si = db_sync_instance_add(inst, "chats", 1); → Создаёт таблицу SQLite, верифицирует цепочку, запускает TTL-таймер. → Если есть активные ETCP-соединения — автоматически запускает синхронизацию. 4. Вставка записи: uint64_t ts = db_sync_next_timestamp(si); // монотонно возрастающий timestamp (ms) uint8_t sig[64]; Ed25519_sign(my_privkey, ts || my_node_id || json, sig); db_sync_insert_signed(si, json_str, json_len, sig, 64, ts); → Подпись ОБЯЗАТЕЛЬНА (sig=NULL → ошибка). Запись автоматически push-ится всем synced пирам. 5. Чтение: db_sync_select(si, offset, limit, my_callback, ctx); 6. db_sync_instance_remove(si) — деактивировать инстанс (таблица БД не удаляется). 7. db_sync_destroy(inst) — вызывается автоматически при завершении. ``` ### Ключевые концепции #### Multi-instance Каждый экземпляр `DB_SYNC_INSTANCE` идентифицируется 64-битным хешем `hash = SHA256(name || htobe64(id))[0:8]`. Этот хеш используется как routing key в синхронизационных сообщениях. На приёмной стороне по хешу находится нужный инстанс. #### Chain hash Каскадный хеш цепочки записей: ``` chain_hash[0] = SHA256(0x00...00 || id_0 || ts_0 || author_0 || sig_0) chain_hash[n] = SHA256(chain_hash[n-1] || id_n || ts_n || author_n || sig_n) ``` Для протокола синхронизации используются первые 8 байт (`chain_hash8`). Сравнивая эти хеши на разных позициях, узлы находят точку расхождения. #### Sync protocol (8 message types) | Message | Direction | Описание | |---------|-----------|----------| | `DB_MSG_INIT_SYNC (0x01)` | A→B | Инициатор шлёт своё количество записей (`my_count`) | | `DB_MSG_INIT_RESP (0x02)` | B→A | Truncation point `tp = min(counts)-1`, `chain_hash8[tp]`, + до 16 sparse-хешей на позициях `tp-2^k` | | `DB_MSG_REFINE (0x03)` | A→B, B→A | Бинарный поиск: до 16 равномерно распределённых хешей в диапазоне расхождения | | `DB_MSG_SEND_DATA (0x04)` | A→B, B→A | Пакетная передача записей (до 32 за раз). Wire: `[id:8][ts:8][author:8][dlen:4][data][sig_len:1=64][sig:64]` | | `DB_MSG_PUSH (0x05)` | A→B | Рассылка одной новой записи всем synced-пирам | | `DB_MSG_ACK_PUSH (0x06)` | B→A | Подтверждение получения PUSH; обновляет delivery_chain и флаг WAS_SENT | | `DB_MSG_SYNC_DONE (0x07)` | B→A | Финальное подтверждение: peer_count + last_chain_hash8 | | `DB_MSG_ERROR (0x08)` | B→A | Ошибка. Коды: `0x01` — instance not found, `0x02` — instance disabled | **Алгоритм синхронизации:** 1. `INIT_SYNC`: A → B: `my_count_a`; B вычисляет `tp = min(count_a, count_b) - 1`. 2. `INIT_RESP`: B → A: `tp`, `chain_hash8_b[tp]`, + sparse-хеши на позициях `tp-1, tp-2, tp-4, tp-8, ..., tp-32768`. 3. Если `hash8_b[tp] == hash8_a[tp]` — цепочки совпадают до `tp`. Более длинная сторона шлёт «хвост» (записи после `tp`). 4. Если sparse-хеши показывают расхождение: A определяет диапазон `[ds, de]` где хеши не совпадают. Если `de-ds ≤ 1` — пустой `REFINE` (запрос данных). Иначе — `REFINE` с до 16 хешами, равномерно распределёнными в диапазоне. 5. `REFINE`: B ищет первую позицию, где хеши разошлись, шлёт `SEND_DATA` начиная с этой позиции. 6. `SEND_DATA`: A вставляет полученные записи (с верификацией Ed25519 подписи), пересчитывает каскадный chain_hash, шлёт `SYNC_DONE`. 7. `SYNC_DONE`: если counts/hashes не совпали — реинициируется sync. #### Защита от подделок - Ed25519-подпись автора проверяется для КАЖДОЙ вставляемой записи (и локальной, и от пиров). - Публичный ключ автора ищется: (1) в своих ключах (если self), (2) в `topo_node_sqlite`, (3) в `peer_ed25519_pubkey` активного ETCP-соединения. - Если подпись невалидна — запись отвергается с логом "discarding as forgery". #### Peer management - При поднятии ETCP-соединения для каждого инстанса добавляется `SI_PEER` и запускается синхронизация. - При разрыве соединения `sync_state` пира сбрасывается в 0. - `peer_check` таймер (каждые 5с) перебирает `topo_group->senders_list` и запускает синхронизацию для пиров с `sync_state == 0`. - `PUSH` рассылается только пирам в состоянии `sync_state >= 1`. #### TTL cleanup - Периодический таймер (каждые 3600с). - Удаляет записи authored by self, где НЕ установлен флаг `DB_REC_FLAG_WAS_SENT` (0x01) и `timestamp < now - db_sync_ttl`. - Чужие записи и свои отправленные не удаляются. ## 3. API ### Глобальный жизненный цикл ```c int db_sync_init(struct UTUN_INSTANCE* inst); void db_sync_destroy(struct UTUN_INSTANCE* inst); ``` - `db_sync_init` — инициализирует DB_SYNC, открывает SQLite по пути `/sync`, биндит ETCP service `0x20`, вешает коллбэки на существующие и новые соединения, стартует `peer_check` таймер. Если `db_sync_enabled = 0` — создаёт структуру в disabled-режиме. - `db_sync_destroy` — отменяет все таймеры, анбиндит ETCP service, снимает коллбэки со всех соединений, закрывает SQLite, освобождает память. ### Управление инстансами ```c struct DB_SYNC_INSTANCE* db_sync_instance_add(struct UTUN_INSTANCE* inst, const char* name, uint64_t id); void db_sync_instance_remove(struct DB_SYNC_INSTANCE* si); ``` - `db_sync_instance_add` — создаёт/регистрирует инстанс. Проверяет валидность имени (`[a-zA-Z0-9_]`, макс 48 символов). Вычисляет hash, создаёт SQLite-таблицу `db_sync__`, верифицирует цепочку хешей, запускает TTL-таймер. Если уже есть активные соединения — автоматически инициирует 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). Проверяет подпись, проверяет дубликат по `(timestamp, author_signature)`, вычисляет chain_hash, вставляет, каскадно пересчитывает хеши последующих записей, рассылает PUSH всем synced-пирам. Возвращает 0 (успех), 1 (уже существует), -1 (ошибка), -2 (неверная подпись). - `db_sync_count` — количество записей в локальной БД. - `db_sync_get_last_timestamp` — последний выданный `db_sync_next_timestamp`. - `db_sync_next_timestamp` — возвращает монотонно возрастающий timestamp в миллисекундах. Гарантирует `last_timestamp_ms < returned`. ### Чтение (select) ```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); ``` - `db_sync_select` — итератор по записям, упорядоченным `ORDER BY timestamp, author_signature`. `limit=0` — без ограничения. Для каждой записи вызывает `cb` с полями: id, timestamp, data (JSON), author (node_id), author_sig (64 байта), delivered_peers (счётчик доставок), delivery_chain (hex-идентификаторы пиров через запятую). Возвращает количество переданных в callback записей. ### 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); ``` - `db_sync_set_insert_cb` — устанавливает callback, вызываемый после успешной вставки записи (локальной или от пира). `author_node_id = self` для локальных вставок, `= peer_node_id` для записей от пиров. ### Внутренние структуры ```c struct DB_SYNC { struct UTUN_INSTANCE* inst; // обратная ссылка на инстанс sqlite3* db; // SQLite handle uint64_t last_connected_tb; // время последнего подключения (timebase) struct DB_SYNC_INSTANCE* instances; // динамический массив инстансов int instance_count, instance_capacity; void* peer_check_timer; // handle uasync-таймера uint8_t enabled; // глобальный флаг из конфига }; struct DB_SYNC_INSTANCE { struct DB_SYNC* db_sync; // обратная ссылка uint64_t hash; // routing key = SHA256(name||id_be)[0:8] char table_name[64]; // "db_sync__" uint64_t next_id; // монотонно возрастающий id записей uint64_t last_timestamp_ms; // последний выданный timestamp (ms) uint8_t enabled; // per-instance enable/disable void* ttl_timer; // handle TTL-таймера struct SI_PEER* peers; // динамический массив пиров int peer_count, peer_capacity; db_sync_insert_cb on_insert; // callback на вставку void* on_insert_arg; }; struct SI_PEER { uint64_t node_id; // идентификатор пира uint32_t synced_pos; // позиция, до которой синхронизированы uint8_t sync_state; // 0=idle, 1=syncing, 2=synced }; ``` ### Конфигурация | Параметр | Файл конфига | По умолчанию | Описание | |----------|-------------|--------------|----------| | `db_sync_enabled` | `global` | `0` | Включить модуль синхронизации | | `db_sync_ttl` | `global` | `86400` | TTL неотправленных собственных записей (секунды) | | `db_path` | `global` | — | Путь к БД; файл создаётся как `/sync` | ### Константы синхронизации | Константа | Значение | Описание | |-----------|---------|----------| | `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_TTL_INTERVAL` | `3600` | Интервал TTL-очистки (секунды) | ### Зависимости - **etcp_api.h / etcp.h** — P2P-обмен сообщениями, коллбэки соединений - **topo_group.h / topo_node_sqlite.h** — топология, discovery пиров, Ed25519 pubkeys - **secure_channel.h** — SHA256 (`sc_sha256_*`), Ed25519 verify (`sc_ed25519_verify`) - **lib/u_async.h** — асинхронный event loop, таймеры (`uasync_set_timeout`, `uasync_cancel_timeout`) - **lib/sqlite3.h** — SQLite3 (WAL, synchronous=NORMAL, auto-checkpoint 10000) - **lib/mem.h** — `u_malloc`, `u_calloc`, `u_realloc`, `u_free` - **lib/debug_config.h** — `DEBUG_INFO`, `DEBUG_WARN`, `DEBUG_ERROR` (категория `DEBUG_CATEGORY_DEBUG`) - **lib/sha256.h** — `SC_SHA256_CTX` (если не USE_OPENSSL) - **lib/platform_compat.h** — `htobe64`, `get_time_tb`, `utun_gettimeofday` - **openssl/evp.h** — низкоуровневый Ed25519 в `sc_ed25519_verify` ### Внутренние функции | Функция | Назначение | |---------|-----------| | `db_sqlite_open/db_sqlite_close` | Открытие/закрытие SQLite с WAL-режимом | | `db_sha256/db_hash64/db_chain_hash_compute` | SHA256 и вычисление chain_hash | | `db_instance_find/db_instance_alloc` | Поиск/выделение инстанса по hash | | `si_peer_find/si_peer_add` | Поиск/добавление пира в инстансе | | `db_count` | `SELECT COUNT(*)` из таблицы инстанса | | `db_chain_hash_at/db_chain_hash8_at` | Чтение chain_hash/chain_hash8 на позиции | | `db_prev_chain_hash` | Поиск chain_hash записи, предшествующей данной | | `db_cascade_from` | Пересчёт chain_hash начиная с позиции `from_pos` | | `db_get_ed25519_pubkey` | Получение Ed25519 pubkey пира | | `db_verify_author_sig` | Проверка Ed25519 подписи автора записи | | `db_record_insert` | Полный цикл вставки: проверка подписи, проверка дубликата, вставка, каскад chain_hash | | `db_sync_send/db_sync_send_hash` | Отправка сообщения пиру через ETCP | | `si_delivery_update` | Обновление delivery_chain при доставке | | `si_parse_record` | Разбор одной записи из wire-формата | | `si_send_data_batch` | Формирование и отправка SEND_DATA пакета | | `db_sync_recv_cb` | Центральный callback приёма ETCP-сообщений, маршрутизация по instance_hash | | `db_handle_init_sync/_resp/_refine/_send_data/_push/_ack_push/_sync_done/_error` | Обработчики каждого типа сообщения | | `db_sync_initiate_sync` | Отправка INIT_SYNC пиру | | `db_verify_chain` | Полная верификация цепочки chain_hash (при старте инстанса) | | `db_sync_peer_check_cb` | Таймер: периодический поиск активных пиров и запуск sync | | `db_sync_instance_ttl_cb` | Таймер: TTL-очистка неотправленных собственных записей |