17 KiB
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. Структуры данных
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
Глобальный жизненный цикл
int db_sync_init(struct UTUN_INSTANCE* inst);
void db_sync_destroy(struct UTUN_INSTANCE* inst);
db_sync_init — открывает SQLite по пути <db_path>/chats.db, биндит ETCP service 0x20, вешает коллбэки соединений, стартует peer_check таймер (5с). Если db_sync_enabled = 0 — disabled-режим.
db_sync_destroy — отменяет таймеры, анбиндит сервис, снимает коллбэки, закрывает SQLite.
Управление инстансами
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, удаляет из массива. Таблица БД не удаляется.
Операции с данными
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=неверная подпись.
Чтение
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 на вставку
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);
Верификация цепи
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 |
— | Путь к БД; файл <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 не вмешивался:
- Вставить данные через si с уникальным hash — PUSH уходит, но не доставляется (нет получателя с таким hash)
remove_si()— данные сохраняются в SQLite, si деактивирован- Создать si с общим hash на обеих сторонах —
instance_addзапускает чистый sync-протокол
Этот паттерн используется для тестирования всех трёх веток INIT_RESP:
- peer_empty: одна сторона с данными, другая пустая
- hash_MATCH: обе имеют общий префикс, но у одной больше записей
- divergence: независимые вставки на обеих сторонах до синхронизации