You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

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 не вмешивался:

  1. Вставить данные через si с уникальным hash — PUSH уходит, но не доставляется (нет получателя с таким hash)
  2. remove_si() — данные сохраняются в SQLite, si деактивирован
  3. Создать si с общим hash на обеих сторонах — instance_add запускает чистый sync-протокол

Этот паттерн используется для тестирования всех трёх веток INIT_RESP:

  • peer_empty: одна сторона с данными, другая пустая
  • hash_MATCH: обе имеют общий префикс, но у одной больше записей
  • divergence: независимые вставки на обеих сторонах до синхронизации