diff --git a/src/chat/db_sync.c b/src/chat/db_sync.c index 06e8246f..a0564d37 100644 --- a/src/chat/db_sync.c +++ b/src/chat/db_sync.c @@ -1,143 +1,112 @@ -// db_sync.c — Distributed content-addressed table with SQLite + peer sync via etcp_api (direct P2P) - +/* db_sync — подписанная история SQLite, модель MESSAGES общего Merkle и адресные PUSH/ACK. */ #include "db_sync.h" -#include "../transport_layer/etcp_api.h" -#include "../transport_layer/etcp.h" +#include "merkle_sync.h" +#include "member_sync.h" +#include "chat_feedback.h" #include "../utun_instance.h" #include "../routing_layer/topo_group.h" #include "../routing_layer/topo_node_sqlite.h" -#include "member_sync.h" -#include "chat_feedback.h" -#include "../transport_layer/etcp_dump.h" +#include "../transport_layer/etcp_api.h" +#include "../transport_layer/etcp.h" #include "../transport_layer/secure_channel.h" #include "../../lib/debug_config.h" #include "../../lib/mem.h" #include "../../lib/u_async.h" -#ifdef UTUN_HAVE_STANDBY -#include "standby.h" -#endif -#include -#include "../../lib/sqlite3.h" #include "../../lib/platform_compat.h" -#include #include -#include +#include -// ---- Forward declarations ---- -struct DB_SYNC; -struct DB_SYNC_INSTANCE; -struct SI_PEER; - -static void db_sync_recv_cb(struct ETCP_CONN* conn, struct ll_entry* entry); -static void db_sync_on_peer_ready(struct TOPO_GROUP* group, uint64_t pid, int ready, void* arg); -static void db_sync_peer_check_cb(void* arg); -static void db_sync_resume_peer_check(struct DB_SYNC* db); -static void db_sync_instance_ttl_cb(void* arg); -static int db_sync_send(struct DB_SYNC_INSTANCE* si, uint64_t dst_node_id, const uint8_t* payload, size_t len); -static int db_sync_send_gid(struct DB_SYNC* db, uint64_t node_id, uint64_t group_id, const uint8_t* payload, size_t len); -static int db_sync_initiate_sync(struct DB_SYNC_INSTANCE* si, uint64_t peer_node_id); -static int si_send_data_batch(struct DB_SYNC_INSTANCE* si, uint64_t dst, uint32_t from, uint32_t count, uint32_t vp, uint32_t want_from); -static int si_delivery_update(struct DB_SYNC_INSTANCE* si, const uint8_t message_id[32], uint64_t peer_id); -static int db_sync_peer_throttled(struct DB_SYNC* db, struct SI_PEER* p); -static int db_delete_bad_row(struct DB_SYNC_INSTANCE* si, sqlite3_int64 rowid); -static void db_history_deleted(struct DB_SYNC_INSTANCE* si); - -// ============================================================ -// Structures -// ============================================================ +#define DB_BATCH_RECORDS 16 +#define DB_WIRE_FIXED (32 + 8 + 8 + 4 + DB_SIG_SIZE) +#define DB_WIRE_MAX (DB_WIRE_FIXED + DB_SYNC_MESSAGE_MAX) +#define SI_DB(si) ((si)->db_sync->db) +#define SI_TBL(si) ((si)->table_name) struct SI_PEER { uint64_t node_id; - uint32_t synced_pos; - uint32_t verified_pos; - uint32_t last_peer_count; - uint8_t sync_state; - uint64_t sync_start_tb; - uint32_t retry_count; - uint8_t sound_on; /* 1 = пиру важен этот чат (play sound on message), не троттлим */ + uint8_t sync_state, sound_on, paused; }; - struct DB_SYNC_INSTANCE { struct DB_SYNC* db_sync; - uint64_t group_id; - char table_name[64]; - uint64_t next_id; - uint64_t last_timestamp_ms; - uint8_t enabled; - void* ttl_timer; + uint64_t group_id, next_id, last_timestamp_ms; + char table_name[64], ns[21]; + uint8_t enabled, sync_gated; + unsigned holds; + void* ttl_timer; struct { - uint32_t pos; - void* timer; - uint8_t pending; /* Работа сохраняется и при отказе SQL/таймера. */ - } recalc; + uint8_t pending, restart, phase, after_valid, waiting_key; + sqlite3_int64 after; + void* timer; + } index; struct SI_PEER* peers; int peer_count, peer_capacity; db_sync_insert_cb on_insert; - void* on_insert_arg; - uint8_t sync_gated; /* 1 = не запускать авто-синхронизацию (ждём member_sync/pubkeys) */ + void* on_insert_arg; }; - struct db_sync_done_cbk_entry { db_sync_done_fn fn; void* arg; + uint8_t removed; struct db_sync_done_cbk_entry* next; }; - +struct db_notice { + struct db_notice* next; + struct DB_SYNC* db; + uint64_t group_id; + uint8_t initial; + unsigned count; + void* work; + /* Записи и тела лежат непосредственно после этой структуры. */ +}; struct DB_SYNC { struct UTUN_INSTANCE* inst; - sqlite3* db; - uint8_t shared_db; /* db == inst->topo_sqlite_db, do not close */ + sqlite3* db; + uint8_t enabled; struct DB_SYNC_INSTANCE** instances; int instance_count, instance_capacity; void* peer_check_timer; - void* peer_check_wait; /* standby_wait handle (Android) */ - uint8_t enabled; struct db_sync_done_cbk_entry* done_cbks; + unsigned dispatch_depth; + struct db_notice* notices; +}; +struct db_record { + sqlite3_int64 rowid; + uint64_t id, timestamp, author; + size_t data_len, sig_len; + uint8_t signature[64], message_id[32]; + const char* data; + int cache_valid; +}; +struct db_checked { + struct db_record record; + int check; + char data[DB_SYNC_MESSAGE_MAX]; }; -// ============================================================ -// Done callback chain (global, like etcp_status_cbk_entry) -// ============================================================ +static void db_sync_recv_cb(struct ETCP_CONN* conn, struct ll_entry* entry); +static void db_sync_on_peer_ready(struct TOPO_GROUP* group, uint64_t peer, int ready, void* arg); +static void db_sync_peer_check_cb(void* arg); +static void db_sync_resume_peer_check(struct DB_SYNC* db); +static void db_index_tick(void* arg); +static void db_index_request(struct DB_SYNC_INSTANCE* si); +static int db_start_peer(struct DB_SYNC_INSTANCE* si, uint64_t peer); +static int db_sync_peer_throttled(struct DB_SYNC* db, struct SI_PEER* peer); +static int db_message_hash(void*, const char*, uint8_t, uint64_t, EVP_MD_CTX*); +static const struct merkle_sync_data_ops db_message_ops; -static void db_sync_done_cbk_add_chain(struct db_sync_done_cbk_entry** head, db_sync_done_fn fn, void* arg) { - if (!head || !fn) return; - struct db_sync_done_cbk_entry* e = u_malloc(sizeof(struct db_sync_done_cbk_entry)); - if (!e) return; - e->fn = fn; e->arg = arg; e->next = *head; - *head = e; -} -static void db_sync_done_cbk_remove_chain(struct db_sync_done_cbk_entry** head, db_sync_done_fn fn, void* arg) { - if (!head || !fn) return; - struct db_sync_done_cbk_entry** p = head; - while (*p) { - if ((*p)->fn == fn && (*p)->arg == arg) { struct db_sync_done_cbk_entry* rm = *p; *p = rm->next; u_free(rm); return; } - p = &(*p)->next; - } -} -static void si_fire_sync_done(struct DB_SYNC_INSTANCE* si, uint64_t peer_node_id) { - struct DB_SYNC* db = si->db_sync; - struct db_sync_done_cbk_entry* cb = db->done_cbks; - while (cb) { struct db_sync_done_cbk_entry* n = cb->next; cb->fn(si, peer_node_id, cb->arg); cb = n; } -} -void db_sync_add_done_cbk(struct UTUN_INSTANCE* inst, db_sync_done_fn fn, void* arg) { - if (inst && inst->db_sync) db_sync_done_cbk_add_chain(&inst->db_sync->done_cbks, fn, arg); -} -void db_sync_remove_done_cbk(struct UTUN_INSTANCE* inst, db_sync_done_fn fn, void* arg) { - if (inst && inst->db_sync) db_sync_done_cbk_remove_chain(&inst->db_sync->done_cbks, fn, arg); -} - -// ============================================================ -// SQL helpers -// ============================================================ - -#define SI_DB(si) ((si)->db_sync->db) -#define SI_TBL(si) ((si)->table_name) -#define SI_SHRT(si) ({ \ - static char _tbuf[72]; \ - snprintf(_tbuf, sizeof(_tbuf), "%s.%04llX", SI_TBL(si), (unsigned long long)((si)->group_id & 0xFFFF)); \ - _tbuf; \ -}) +static uint64_t db_read64(const uint8_t* p) { uint64_t n; memcpy(&n, p, 8); return be64toh(n); } +static void db_write64(uint8_t* p, uint64_t n) { n = htobe64(n); memcpy(p, &n, 8); } +static uint32_t db_read32(const uint8_t* p) { uint32_t n; memcpy(&n, p, 4); return ntohl(n); } +static void db_write32(uint8_t* p, uint32_t n) { n = htonl(n); memcpy(p, &n, 4); } +static int db_error(struct DB_SYNC_INSTANCE* si, const char* stage) { + DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "history tbl=%s stage=%s: %s", SI_TBL(si), stage, sqlite3_errmsg(SI_DB(si))); + return -1; +} +static void db_rollback(struct DB_SYNC_INSTANCE* si, const char* stage) { + db_error(si, stage); + if (sqlite3_exec(SI_DB(si), "ROLLBACK", NULL, NULL, NULL) != SQLITE_OK) db_error(si, "rollback"); +} // Форматирует SQL-запрос, подставляя имя таблицы из инстанса (через %s) static void si_sql(char* buf, size_t sz, struct DB_SYNC_INSTANCE* si, const char* fmt) { @@ -152,240 +121,25 @@ static int si_prep(struct DB_SYNC_INSTANCE* si, sqlite3_stmt** stmt, const char* return sqlite3_prepare_v2(SI_DB(si), sql, -1, stmt, NULL); } -// Одна схема для новых таблиц и однократного преобразования сохранённой истории. -static int si_table_create(struct DB_SYNC_INSTANCE* si, const char* name, int if_not_exists) { - char sql[768]; - snprintf(sql, sizeof(sql), - "CREATE TABLE %s\"%s\" (" - " timestamp INTEGER NOT NULL, node_id INTEGER NOT NULL, id INTEGER NOT NULL," - " chain_hash BLOB NOT NULL, flags INTEGER NOT NULL DEFAULT 0, data BLOB," - " author_signature BLOB NOT NULL, message_id BLOB NOT NULL UNIQUE CHECK(length(message_id)=32)," - " local_attrs TEXT DEFAULT '', delivered_peers INTEGER NOT NULL DEFAULT 0," - " delivery_chain TEXT NOT NULL DEFAULT '', PRIMARY KEY(timestamp,author_signature))", - if_not_exists ? "IF NOT EXISTS " : "", name); - if (sqlite3_exec(SI_DB(si), sql, NULL, NULL, NULL) == SQLITE_OK) return 0; - DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "history schema: create table=%s: %s", name, sqlite3_errmsg(SI_DB(si))); return -1; -} -// Перестройка атомарна: сохраняем подписи, цепочку, локальные атрибуты и доставку без изменений. -static int si_table_init(struct DB_SYNC_INSTANCE* si) { - sqlite3* db = SI_DB(si); - if (si_table_create(si, SI_TBL(si), 1) < 0) return -1; - sqlite3_stmt *rows = NULL, *insert = NULL; - const char* stage = "inspect columns"; - int rc = si_prep(si, &rows, "PRAGMA table_info(\"%s\")"), has_id = 0; - if (rc != SQLITE_OK) goto error; - while ((rc = sqlite3_step(rows)) == SQLITE_ROW) { - const char* name = (const char*)sqlite3_column_text(rows, 1); - if (name && !strcmp(name, "message_id")) has_id = 1; - } - if (rc != SQLITE_DONE) goto error; - sqlite3_finalize(rows); rows = NULL; - if (has_id) return 0; - - stage = "begin"; - if (sqlite3_exec(db, "BEGIN IMMEDIATE", NULL, NULL, NULL) != SQLITE_OK) goto error; - DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "history schema: adding message_id table=%s group=%016llx", - SI_TBL(si), (unsigned long long)si->group_id); - char replacement[64], sql[768]; - snprintf(replacement, sizeof(replacement), "new_%.48s", SI_TBL(si)); - stage = "create replacement"; - if (si_table_create(si, replacement, 0) < 0) goto rollback; - stage = "read history"; - if (si_prep(si, &rows, "SELECT timestamp,author_signature FROM \"%s\"") != SQLITE_OK) goto rollback; - snprintf(sql, sizeof(sql), - "INSERT INTO \"%s\" SELECT timestamp,node_id,id,chain_hash,flags,data,author_signature,?1," - "local_attrs,delivered_peers,delivery_chain FROM \"%s\" WHERE timestamp=?2 AND author_signature=?3", - replacement, SI_TBL(si)); - stage = "prepare copy"; - if (sqlite3_prepare_v2(db, sql, -1, &insert, NULL) != SQLITE_OK) goto rollback; - sqlite3_int64 count = 0; - while ((rc = sqlite3_step(rows)) == SQLITE_ROW) { - sqlite3_int64 ts = sqlite3_column_int64(rows, 0); - const uint8_t* signature = sqlite3_column_blob(rows, 1); - int size = sqlite3_column_bytes(rows, 1); - uint8_t message_id[32]; - stage = "compute message_id"; - if (!signature || size != DB_SIG_SIZE || chat_message_id(si->group_id, (uint64_t)ts, signature, message_id) < 0) { - DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "history schema: invalid identity table=%s ts=%lld signature_bytes=%d", - SI_TBL(si), (long long)ts, size); goto rollback; - } - stage = "copy record"; - rc = sqlite3_bind_blob(insert, 1, message_id, sizeof(message_id), SQLITE_STATIC); - if (rc == SQLITE_OK) rc = sqlite3_bind_int64(insert, 2, ts); - if (rc == SQLITE_OK) rc = sqlite3_bind_blob(insert, 3, signature, DB_SIG_SIZE, SQLITE_STATIC); - if (rc != SQLITE_OK || sqlite3_step(insert) != SQLITE_DONE) goto rollback; - if (sqlite3_changes(db) != 1) { - DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "history schema: copy table=%s ts=%lld changed=%d expected=1", - SI_TBL(si), (long long)ts, sqlite3_changes(db)); goto rollback; - } - if (sqlite3_reset(insert) != SQLITE_OK) goto rollback; - count++; - } - stage = "read history"; - if (rc != SQLITE_DONE) goto rollback; - sqlite3_finalize(rows); rows = NULL; sqlite3_finalize(insert); insert = NULL; - stage = "replace table"; - snprintf(sql, sizeof(sql), "DROP TABLE \"%s\"; ALTER TABLE \"%s\" RENAME TO \"%s\"", - SI_TBL(si), replacement, SI_TBL(si)); - if (sqlite3_exec(db, sql, NULL, NULL, NULL) != SQLITE_OK) goto rollback; - stage = "commit"; - if (sqlite3_exec(db, "COMMIT", NULL, NULL, NULL) != SQLITE_OK) goto rollback; - DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "history schema: message_id ready table=%s records=%lld", SI_TBL(si), (long long)count); - return 0; -rollback: - DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "history schema: table=%s stage=%s: %s; rolling back", - SI_TBL(si), stage, sqlite3_errmsg(db)); - sqlite3_finalize(rows); sqlite3_finalize(insert); - if (sqlite3_exec(db, "ROLLBACK", NULL, NULL, NULL) != SQLITE_OK) - DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "history schema: rollback table=%s: %s", SI_TBL(si), sqlite3_errmsg(db)); - return -1; -error: - DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "history schema: table=%s stage=%s: %s", SI_TBL(si), stage, sqlite3_errmsg(db)); - sqlite3_finalize(rows); return -1; -} - -// ============================================================ -// SQLite open/close -// ============================================================ - -// Открывает SQLite БД, включает WAL, mmap и прочие оптимизации для высокой производительности -static int db_sqlite_open(struct DB_SYNC* db, const char* path) -{ - DEBUG_TRACE(DEBUG_CATEGORY_CHAT_SYNC, "path=%s", path); - int rc = sqlite3_open(path, &db->db); - if (rc != SQLITE_OK) { - DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "db_sync: sqlite3_open(%s): %s", path, sqlite3_errmsg(db->db)); - sqlite3_close(db->db); - db->db = NULL; - return -1; - } - char* err = NULL; - rc = sqlite3_exec(db->db, "PRAGMA journal_mode=WAL", NULL, NULL, &err); - if (rc != SQLITE_OK) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "db_sync: WAL pragma: %s", err); sqlite3_free(err); } - sqlite3_exec(db->db, "PRAGMA synchronous=NORMAL", NULL, NULL, NULL); - sqlite3_exec(db->db, "PRAGMA wal_autocheckpoint=10000", NULL, NULL, NULL); - sqlite3_exec(db->db, "PRAGMA cache_size=-32768", NULL, NULL, NULL); - sqlite3_exec(db->db, "PRAGMA mmap_size=134217728", NULL, NULL, NULL); - sqlite3_exec(db->db, "PRAGMA temp_store=MEMORY", NULL, NULL, NULL); - DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "db_sync: SQLite opened at %s", path); - return 0; -} - -// Закрывает SQLite БД (только свою, shared не трогает) -static void db_sqlite_close(struct DB_SYNC* db) -{ - DEBUG_TRACE(DEBUG_CATEGORY_CHAT_SYNC, ""); - if (!db->db || db->shared_db) return; - sqlite3_close(db->db); - db->db = NULL; - DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "db_sync: SQLite closed"); -} - -// ============================================================ -// SHA256 helpers -// ============================================================ - -// Вычисляет SHA256 хеш данных (обёртка над OpenSSL SHA256) -static void db_sha256(const uint8_t* data, size_t len, uint8_t hash[32]) -{ - SHA256(data, len, hash); -} - -// Возвращает первые 8 байт SHA256 как uint64 — короткий идентификатор для сравнения -static uint64_t db_hash64(const uint8_t* data, size_t len) -{ - uint8_t hash[32]; db_sha256(data, len, hash); - uint64_t h; memcpy(&h, hash, 8); - return h; -} - -// Вычисляет цепной хеш: SHA256(prev_chain_hash[32] || id[8] || timestamp[8] || author[8] || sig[64]) -static int db_chain_hash_compute(const uint8_t prev_chain_hash[32], - uint64_t id, uint64_t timestamp, - uint64_t author, const uint8_t author_signature[DB_SIG_SIZE], - uint8_t out[32]) -{ - uint8_t buf[32 + 8 + 8 + 8 + DB_SIG_SIZE]; // 32 + 8 + 8 + 8 + 64 = 120 - memcpy(buf, prev_chain_hash, 32); - memcpy(buf + 32, &id, 8); - memcpy(buf + 40, ×tamp, 8); - memcpy(buf + 48, &author, 8); - memcpy(buf + 56, author_signature, DB_SIG_SIZE); - if (SHA256(buf, sizeof(buf), out)) return 0; - DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "history chain SHA256 failed"); return -1; -} - -// ============================================================ -// Instance management -// ============================================================ - -// Ищет активный инстанс синхронизации по group_id канала -static struct DB_SYNC_INSTANCE* db_instance_find(struct DB_SYNC* db, uint64_t group_id) -{ - for (int i = 0; i < db->instance_count; i++) { - if (db->instances[i]->group_id == group_id && db->instances[i]->enabled) return db->instances[i]; - } +static struct DB_SYNC_INSTANCE* db_instance_find(struct DB_SYNC* db, uint64_t group) { + for (int i = 0; db && i < db->instance_count; i++) + if (db->instances[i]->enabled && db->instances[i]->group_id == group) return db->instances[i]; return NULL; } - -struct DB_SYNC_INSTANCE* db_sync_instance_find(struct UTUN_INSTANCE* inst, uint64_t group_id) -{ - if (!inst || !inst->db_sync) return NULL; - return db_instance_find(inst->db_sync, group_id); +struct DB_SYNC_INSTANCE* db_sync_instance_find(struct UTUN_INSTANCE* inst, uint64_t group) { + return db_instance_find(inst ? inst->db_sync : NULL, group); } - -uint64_t db_sync_instance_group_id(struct DB_SYNC_INSTANCE* si) -{ - return si ? si->group_id : 0; +uint64_t db_sync_instance_group_id(struct DB_SYNC_INSTANCE* si) { return si ? si->group_id : 0; } +static void db_release(struct DB_SYNC_INSTANCE* si) { + if (--si->holds == 0 && !si->enabled) u_free(si); } - -// Выделяет новый инстанс в куче (стабильный указатель) и регистрирует его в массиве -static struct DB_SYNC_INSTANCE* db_instance_alloc(struct DB_SYNC* db) -{ - DEBUG_TRACE(DEBUG_CATEGORY_CHAT_SYNC, "count=%d cap=%d", db->instance_count, db->instance_capacity); - if (db->instance_count >= db->instance_capacity) { - int nc = db->instance_capacity ? db->instance_capacity * 2 : 4; - struct DB_SYNC_INSTANCE** np = u_realloc(db->instances, nc * sizeof(*db->instances)); - if (!np) return NULL; - db->instances = np; - db->instance_capacity = nc; - } - struct DB_SYNC_INSTANCE* si = u_calloc(1, sizeof(*si)); - if (!si) return NULL; - si->db_sync = db; - si->enabled = 1; - si->sync_gated = 1; - db->instances[db->instance_count++] = si; - return si; -} - -// Проверяет допустимость имени таблицы: только A-Za-z0-9_, до 48 символов -static int si_name_valid(const char* name) -{ - if (!name || !name[0] || strlen(name) > 48) return 0; - for (const char* p = name; *p; p++) { - if (!((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z') - || (*p >= '0' && *p <= '9') || *p == '_')) return 0; - } +static int si_name_valid(const char* name) { + if (!name || !*name || strlen(name) > 48) return 0; + for (const char* p = name; *p; p++) + if (!((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z') || (*p >= '0' && *p <= '9') || *p == '_')) return 0; return 1; } - -// Вычисляет 64-битный хеш инстанса: db_hash64(name[256] || id_be[8]) -static uint64_t db_instance_hash_compute(const char* name, uint64_t id) -{ - size_t nl = strlen(name); - uint8_t buf[264]; if (nl > 256) nl = 256; - memcpy(buf, name, nl); - uint64_t id_be = htobe64(id); - memcpy(buf + nl, &id_be, 8); - return db_hash64(buf, nl + 8); -} - -// ============================================================ -// Peer management -// ============================================================ - // Ищет пира в инстансе по node_id static struct SI_PEER* si_peer_find(struct DB_SYNC_INSTANCE* si, uint64_t node_id) { @@ -414,217 +168,6 @@ static struct SI_PEER* si_peer_add(struct DB_SYNC_INSTANCE* si, uint64_t node_id return p; } -/* Ошибка раунда не является подтверждением позиции или доставки. */ -static void si_abort_sync(struct DB_SYNC_INSTANCE* si, uint64_t src, const char* reason) { - struct SI_PEER* peer = si_peer_find(si, src); - if (peer) { peer->sync_state = 0; peer->sync_start_tb = 0; } - db_sync_resume_peer_check(si->db_sync); - DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "history round aborted tbl=%s peer=%016llx reason=%s; retry scheduled", - SI_TBL(si), (unsigned long long)src, reason); -} - -// ============================================================ -// Data access -// ============================================================ - -// Возвращает количество записей в таблице инстанса -static uint32_t db_count(struct DB_SYNC_INSTANCE* si) -{ - sqlite3_stmt* stmt; - if (si_prep(si, &stmt, "SELECT COUNT(*) FROM \"%s\"") != SQLITE_OK) return 0; - uint32_t c = (sqlite3_step(stmt) == SQLITE_ROW) ? (uint32_t)sqlite3_column_int64(stmt, 0) : 0; - sqlite3_finalize(stmt); - return c; -} - -/* Внутренний count не подменяет отказ SQL пустой историей. */ -static int db_count_checked(struct DB_SYNC_INSTANCE* si, uint32_t* count) { - sqlite3_stmt* stmt = NULL; - int rc = si_prep(si, &stmt, "SELECT COUNT(*) FROM \"%s\""); - if (rc == SQLITE_OK) rc = sqlite3_step(stmt); - if (rc == SQLITE_ROW) *count = (uint32_t)sqlite3_column_int64(stmt, 0); - int final_rc = sqlite3_finalize(stmt); - if (rc == SQLITE_ROW && final_rc == SQLITE_OK) return 0; - DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "history count tbl=%s rc=%d finalize=%d: %s", - SI_TBL(si), rc, final_rc, sqlite3_errmsg(SI_DB(si))); return -1; -} - -// Читает цепной хеш (32 байта) записи на позиции pos в отсортированной таблице -static int db_chain_hash_at(struct DB_SYNC_INSTANCE* si, uint32_t pos, uint8_t out[32]) -{ - sqlite3_stmt* stmt; - if (si_prep(si, &stmt, - "SELECT chain_hash FROM \"%s\"" - " ORDER BY timestamp, author_signature" - " LIMIT 1 OFFSET ?") != SQLITE_OK) - { - DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "chain_hash_at prepare: %s", sqlite3_errmsg(SI_DB(si))); - return -1; - } - sqlite3_bind_int64(stmt, 1, (sqlite3_int64)pos); - if (sqlite3_step(stmt) != SQLITE_ROW) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "chain_hash_at no row pos=%u tbl=%s", pos, SI_TBL(si)); sqlite3_finalize(stmt); return -1; } - const void* b = sqlite3_column_blob(stmt, 0); - if (!b || sqlite3_column_bytes(stmt, 0) < 32) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "chain_hash_at blob too small pos=%u tbl=%s", pos, SI_TBL(si)); sqlite3_finalize(stmt); return -1; } - memcpy(out, b, 32); - sqlite3_finalize(stmt); - return 0; -} - -// Находит позицию записи (0-based) в отсортированной таблице по (timestamp, author_signature) -static uint32_t si_find_pos(struct DB_SYNC_INSTANCE* si, uint64_t ts, const uint8_t* author_sig) -{ - sqlite3_stmt* stmt; - if (si_prep(si, &stmt, - "SELECT COUNT(*) FROM \"%s\"" - " WHERE timestamprecalc.pending || from_pos < si->recalc.pos) si->recalc.pos = from_pos; - si->recalc.pending = 1; - if (!si->recalc.timer) - si->recalc.timer = uasync_set_timeout(si->db_sync->inst->ua, 100, si, db_recalc_tick, "db_recalc"); - if (!si->recalc.timer) DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "history recalc timer failed tbl=%s; work retained", SI_TBL(si)); -} - -/* Один атомарный батч. Только COMMIT продвигает позицию; ошибка оставляет работу pending. */ -static int db_recalc_batch(struct DB_SYNC_INSTANCE* si) { - sqlite3* db = SI_DB(si); - sqlite3_stmt *sel = NULL, *upd = NULL; - const char* stage = "begin"; - if (sqlite3_exec(db, "BEGIN IMMEDIATE", NULL, NULL, NULL) != SQLITE_OK) goto error; - uint32_t total; - stage = "count"; - if (db_count_checked(si, &total) < 0) goto rollback; - uint8_t prev_ch[32] = {0}; - stage = "previous hash"; - if (si->recalc.pos && si->recalc.pos < total && db_chain_hash_at(si, si->recalc.pos - 1, prev_ch) < 0) goto rollback; - stage = "prepare"; - if (si_prep(si, &sel, "SELECT id,timestamp,node_id,author_signature,rowid FROM \"%s\"" - " ORDER BY timestamp,author_signature LIMIT 50 OFFSET ?") != SQLITE_OK) goto rollback; - if (sqlite3_bind_int64(sel, 1, si->recalc.pos) != SQLITE_OK) goto rollback; - if (si_prep(si, &upd, "UPDATE \"%s\" SET chain_hash=? WHERE timestamp=? AND author_signature=?") != SQLITE_OK) goto rollback; - uint32_t processed = 0; - int rc; - stage = "rows"; - while ((rc = sqlite3_step(sel)) == SQLITE_ROW) { - sqlite3_int64 n_id = sqlite3_column_int64(sel, 0); - sqlite3_int64 n_ts = sqlite3_column_int64(sel, 1); - sqlite3_int64 n_auth = sqlite3_column_int64(sel, 2); - const uint8_t* sig = sqlite3_column_blob(sel, 3); - uint8_t ch[32]; - if (!sig && sqlite3_errcode(db) == SQLITE_NOMEM) goto rollback; - if (!sig || sqlite3_column_bytes(sel, 3) != DB_SIG_SIZE) { - sqlite3_int64 rowid = sqlite3_column_int64(sel, 4); - DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "history recalc malformed stored signature tbl=%s ts=%lld bytes=%d", - SI_TBL(si), (long long)n_ts, sqlite3_column_bytes(sel, 3)); - sqlite3_finalize(upd); upd = NULL; sqlite3_finalize(sel); sel = NULL; - stage = "delete malformed signature"; - if (db_delete_bad_row(si, rowid) < 0 || sqlite3_exec(db, "COMMIT", NULL, NULL, NULL) != SQLITE_OK) goto rollback; - db_history_deleted(si); return 0; - } - if (db_chain_hash_compute(prev_ch, (uint64_t)n_id, (uint64_t)n_ts, (uint64_t)n_auth, sig, ch) < 0) goto rollback; - if (sqlite3_bind_blob(upd, 1, ch, 32, SQLITE_TRANSIENT) != SQLITE_OK || - sqlite3_bind_int64(upd, 2, n_ts) != SQLITE_OK || - sqlite3_bind_blob(upd, 3, sig, DB_SIG_SIZE, SQLITE_TRANSIENT) != SQLITE_OK || - sqlite3_step(upd) != SQLITE_DONE || sqlite3_changes(db) != 1 || sqlite3_reset(upd) != SQLITE_OK) goto rollback; - memcpy(prev_ch, ch, 32); - processed++; - } - if (rc != SQLITE_DONE) goto rollback; - sqlite3_finalize(upd); upd = NULL; sqlite3_finalize(sel); sel = NULL; - stage = "commit"; - rc = sqlite3_exec(db, "COMMIT", NULL, NULL, NULL); - if (rc != SQLITE_OK) { - DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "history recalc COMMIT tbl=%s rc=%d", SI_TBL(si), rc); goto rollback; - } - si->recalc.pos += processed; - si->recalc.pending = si->recalc.pos < total; - DEBUG_DEBUG(DEBUG_CATEGORY_CHAT_SYNC, "history recalc committed tbl=%s pos=%u processed=%u total=%u pending=%u", - SI_TBL(si), si->recalc.pos, processed, total, si->recalc.pending); - return 0; -rollback: - DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "history recalc tbl=%s stage=%s: %s; retaining position=%u", - SI_TBL(si), stage, sqlite3_errmsg(db), si->recalc.pos); - sqlite3_finalize(upd); sqlite3_finalize(sel); - if (sqlite3_exec(db, "ROLLBACK", NULL, NULL, NULL) != SQLITE_OK) - DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "history recalc rollback tbl=%s: %s", SI_TBL(si), sqlite3_errmsg(db)); - return -1; -error: - DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "history recalc tbl=%s stage=%s: %s", SI_TBL(si), stage, sqlite3_errmsg(db)); return -1; -} - -static void db_recalc_tick(void* arg) { - struct DB_SYNC_INSTANCE* si = arg; - si->recalc.timer = NULL; - db_recalc_batch(si); - if (si->recalc.pending) db_sync_schedule_recalc(si, si->recalc.pos); -} - -/* Перед публикацией хешей ошибка прекращает обмен, сохраняя отложенный пересчёт. */ -static int db_sync_flush_recalc(struct DB_SYNC_INSTANCE* si) { - while (si->recalc.pending) { - if (db_recalc_batch(si) < 0) { db_sync_schedule_recalc(si, si->recalc.pos); return -1; } - } - if (si->recalc.timer) uasync_cancel_timeout(si->db_sync->inst->ua, si->recalc.timer); - si->recalc.timer = NULL; - return 0; -} - -// ── Ed25519 verification ── - // Получает Ed25519 публичный ключ узла: сначала из локального, затем из topo_node БД, затем из ETCP-соединения static int db_get_ed25519_pubkey(struct DB_SYNC* db, uint64_t node_id, uint8_t out[32]) { @@ -695,214 +238,593 @@ static int db_delete_bad_row(struct DB_SYNC_INSTANCE* si, sqlite3_int64 rowid) { SI_TBL(si), (long long)rowid, rc, final_rc, changed, sqlite3_errmsg(SI_DB(si))); return -1; } -/* Удаление сдвигает позиции: старый раунд больше не может подтверждать текущую историю. */ -static void db_history_deleted(struct DB_SYNC_INSTANCE* si) { - db_sync_schedule_recalc(si, 0); - for (int i = 0; i < si->peer_count; i++) { - struct SI_PEER* p = &si->peers[i]; - p->synced_pos = 0; p->verified_pos = 0; p->sync_state = 0; p->sync_start_tb = 0; - } - db_sync_resume_peer_check(si->db_sync); - DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "history corrupt stored row deleted tbl=%s; rebuilding chain and restarting %d peers", - SI_TBL(si), si->peer_count); +// Одна схема для новых таблиц и однократного преобразования сохранённой истории. +static int si_table_create(struct DB_SYNC_INSTANCE* si, const char* name, int if_not_exists) { + char sql[768]; + snprintf(sql, sizeof(sql), + "CREATE TABLE %s\"%s\" (" + " timestamp INTEGER NOT NULL, node_id INTEGER NOT NULL, id INTEGER NOT NULL," + " flags INTEGER NOT NULL DEFAULT 0, data BLOB," + " author_signature BLOB NOT NULL, message_id BLOB NOT NULL UNIQUE CHECK(length(message_id)=32)," + " bucket INTEGER NOT NULL, local_attrs TEXT DEFAULT '', delivered_peers INTEGER NOT NULL DEFAULT 0," + " delivery_chain TEXT NOT NULL DEFAULT '', PRIMARY KEY(timestamp,author_signature))", + if_not_exists ? "IF NOT EXISTS " : "", name); + if (sqlite3_exec(SI_DB(si), sql, NULL, NULL, NULL) == SQLITE_OK) return 0; + DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "history schema: create table=%s: %s", name, sqlite3_errmsg(SI_DB(si))); return -1; } -// Вставляет запись в таблицу: проверяет подпись, дубликаты, вычисляет chain_hash, возвращает 0/1/-1/-2 -static int db_record_insert(struct DB_SYNC_INSTANCE* si, - uint64_t id, uint64_t ts, uint64_t author_node_id, - const char* json, size_t jlen, - const uint8_t* author_sig, - const char* local_attrs) -{ - DEBUG_TRACE(DEBUG_CATEGORY_CHAT_SYNC, "id=%llu ts=%llu author=%N len=%zu", (unsigned long long)id, (unsigned long long)ts, (unsigned long long)author_node_id, jlen); +// Перестройка атомарна: сохраняем подписи, локальные атрибуты и доставку без изменений. +static int si_table_init(struct DB_SYNC_INSTANCE* si) { sqlite3* db = SI_DB(si); - sqlite3_stmt* stmt = NULL; - enum db_record_check incoming = db_verify_author_sig(si, ts, author_node_id, json, jlen, author_sig, DB_SIG_SIZE); - if (incoming == DB_CHECK_ERROR || incoming == DB_CHECK_DEFERRED) return -1; - if (!author_sig) return -2; - const char* stage = "begin"; - if (sqlite3_exec(db, "BEGIN IMMEDIATE", NULL, NULL, NULL) != SQLITE_OK) { - DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "history insert BEGIN tbl=%s: %s", SI_TBL(si), sqlite3_errmsg(db)); return -1; - } - int ret = incoming == DB_CHECK_VALID ? 0 : -2, deleted = 0, exists = 0; - stage = "read stored copy"; - if (si_prep(si, &stmt, "SELECT rowid,node_id,data,author_signature FROM \"%s\" WHERE timestamp=? AND author_signature=?") - != SQLITE_OK) goto rollback; - if (sqlite3_bind_int64(stmt, 1, (sqlite3_int64)ts) != SQLITE_OK || - sqlite3_bind_blob(stmt, 2, author_sig, DB_SIG_SIZE, SQLITE_STATIC) != SQLITE_OK) goto rollback; - int rc = sqlite3_step(stmt); - sqlite3_int64 rowid = 0; - if (rc == SQLITE_ROW) { - exists = 1; rowid = sqlite3_column_int64(stmt, 0); - uint64_t author = (uint64_t)sqlite3_column_int64(stmt, 1); - const char* data = sqlite3_column_blob(stmt, 2); - if (!data && sqlite3_errcode(db) == SQLITE_NOMEM) goto rollback; - size_t len = (size_t)sqlite3_column_bytes(stmt, 2); - const uint8_t* sig = sqlite3_column_blob(stmt, 3); - if (!sig && sqlite3_errcode(db) == SQLITE_NOMEM) goto rollback; - size_t sig_len = (size_t)sqlite3_column_bytes(stmt, 3); - enum db_record_check stored = db_verify_author_sig(si, ts, author, data, len, sig, sig_len); - if (stored == DB_CHECK_ERROR || stored == DB_CHECK_DEFERRED) goto rollback; - deleted = stored == DB_CHECK_BAD_SIGNATURE || stored == DB_CHECK_BAD_FORMAT; - } else if (rc != SQLITE_DONE) goto rollback; - if (sqlite3_finalize(stmt) != SQLITE_OK) { stmt = NULL; goto rollback; } - stmt = NULL; - if (deleted) { - stage = "delete stored corruption"; - if (db_delete_bad_row(si, rowid) < 0) goto rollback; - } - if (incoming != DB_CHECK_VALID || (exists && !deleted)) { - if (incoming == DB_CHECK_VALID) ret = 1; - goto commit; + if (si_table_create(si, SI_TBL(si), 1) < 0) return -1; + sqlite3_stmt *rows = NULL, *insert = NULL; + const char* stage = "inspect columns"; + int rc = si_prep(si, &rows, "PRAGMA table_info(\"%s\")"), has_id = 0, has_bucket = 0, has_chain = 0; + if (rc != SQLITE_OK) goto error; + while ((rc = sqlite3_step(rows)) == SQLITE_ROW) { + const char* name = (const char*)sqlite3_column_text(rows, 1); + if (name && !strcmp(name, "message_id")) has_id = 1; + if (name && !strcmp(name, "bucket")) has_bucket = 1; + if (name && !strcmp(name, "chain_hash")) has_chain = 1; } + if (rc != SQLITE_DONE) goto error; + sqlite3_finalize(rows); rows = NULL; + if (has_id && has_bucket && !has_chain) return 0; - // Compute chain hash - uint8_t prev_ch[32], ch[32], message_id[32]; - stage = "identity and chain hash"; - if (chat_message_id(si->group_id, ts, author_sig, message_id) < 0 || - db_prev_chain_hash(si, ts, author_sig, prev_ch) < 0 || - db_chain_hash_compute(prev_ch, id, ts, author_node_id, author_sig, ch) < 0) goto rollback; - - // Insert new record - stage = "insert"; - if (si_prep(si, &stmt, - "INSERT INTO \"%s\"" - " (timestamp,node_id,id,chain_hash,flags,data," - " author_signature,local_attrs,delivered_peers,delivery_chain,message_id)" - " VALUES (?,?,?,?,0,?,?,?,0,'',?)") != SQLITE_OK) - goto rollback; - if (sqlite3_bind_int64(stmt, 1, (sqlite3_int64)ts) != SQLITE_OK || - sqlite3_bind_int64(stmt, 2, (sqlite3_int64)author_node_id) != SQLITE_OK || - sqlite3_bind_int64(stmt, 3, (sqlite3_int64)id) != SQLITE_OK || - sqlite3_bind_blob(stmt, 4, ch, 32, SQLITE_TRANSIENT) != SQLITE_OK || - sqlite3_bind_blob(stmt, 5, json, (int)jlen, SQLITE_STATIC) != SQLITE_OK || - sqlite3_bind_blob(stmt, 6, author_sig, DB_SIG_SIZE, SQLITE_STATIC) != SQLITE_OK || - sqlite3_bind_text(stmt, 7, local_attrs ? local_attrs : "", -1, SQLITE_STATIC) != SQLITE_OK || - sqlite3_bind_blob(stmt, 8, message_id, sizeof(message_id), SQLITE_TRANSIENT) != SQLITE_OK || - sqlite3_step(stmt) != SQLITE_DONE) goto rollback; - if (sqlite3_finalize(stmt) != SQLITE_OK) { stmt = NULL; goto rollback; } - stmt = NULL; -commit: + stage = "begin"; + if (sqlite3_exec(db, "BEGIN IMMEDIATE", NULL, NULL, NULL) != SQLITE_OK) goto error; + DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "history schema: preparing Merkle table=%s group=%016llx", + SI_TBL(si), (unsigned long long)si->group_id); + char replacement[64], sql[768]; + snprintf(replacement, sizeof(replacement), "new_%.48s", SI_TBL(si)); + stage = "create replacement"; + if (si_table_create(si, replacement, 0) < 0) goto rollback; + stage = "read history"; + if (si_prep(si, &rows, "SELECT timestamp,author_signature FROM \"%s\"") != SQLITE_OK) goto rollback; + snprintf(sql, sizeof(sql), + "INSERT INTO \"%s\" SELECT timestamp,node_id,id,flags,data,author_signature,?1,?4," + "local_attrs,delivered_peers,delivery_chain FROM \"%s\" WHERE timestamp=?2 AND author_signature=?3", + replacement, SI_TBL(si)); + stage = "prepare copy"; + if (sqlite3_prepare_v2(db, sql, -1, &insert, NULL) != SQLITE_OK) goto rollback; + sqlite3_int64 count = 0; + while ((rc = sqlite3_step(rows)) == SQLITE_ROW) { + sqlite3_int64 ts = sqlite3_column_int64(rows, 0); + const uint8_t* signature = sqlite3_column_blob(rows, 1); + int size = sqlite3_column_bytes(rows, 1); + uint8_t message_id[32]; + stage = "compute message_id"; + if (!signature && sqlite3_errcode(db) == SQLITE_NOMEM) goto rollback; + if (!signature || size != DB_SIG_SIZE) { + DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "history schema: removing malformed signature table=%s ts=%lld bytes=%d", + SI_TBL(si), (long long)ts, size); + continue; + } + if (chat_message_id(si->group_id, (uint64_t)ts, signature, message_id) < 0) goto rollback; + stage = "copy record"; + rc = sqlite3_bind_blob(insert, 1, message_id, sizeof(message_id), SQLITE_STATIC); + if (rc == SQLITE_OK) rc = sqlite3_bind_int64(insert, 2, ts); + if (rc == SQLITE_OK) rc = sqlite3_bind_blob(insert, 3, signature, DB_SIG_SIZE, SQLITE_STATIC); + if (rc == SQLITE_OK) rc = sqlite3_bind_int64(insert, 4, (sqlite3_int64)(db_read64(message_id) >> 39)); + if (rc != SQLITE_OK || sqlite3_step(insert) != SQLITE_DONE) goto rollback; + if (sqlite3_changes(db) != 1) { + DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "history schema: copy table=%s ts=%lld changed=%d expected=1", + SI_TBL(si), (long long)ts, sqlite3_changes(db)); goto rollback; + } + if (sqlite3_reset(insert) != SQLITE_OK) goto rollback; + count++; + } + stage = "read history"; + if (rc != SQLITE_DONE) goto rollback; + sqlite3_finalize(rows); rows = NULL; sqlite3_finalize(insert); insert = NULL; + stage = "replace table"; + snprintf(sql, sizeof(sql), "DROP TABLE \"%s\"; ALTER TABLE \"%s\" RENAME TO \"%s\"", + SI_TBL(si), replacement, SI_TBL(si)); + if (sqlite3_exec(db, sql, NULL, NULL, NULL) != SQLITE_OK) goto rollback; stage = "commit"; - rc = sqlite3_exec(db, "COMMIT", NULL, NULL, NULL); - if (rc != SQLITE_OK) { - DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "history insert COMMIT tbl=%s rc=%d", SI_TBL(si), rc); goto rollback; - } - if (deleted) db_history_deleted(si); - if (ret == 0) { - if (id >= si->next_id) si->next_id = id + 1; - db_sync_schedule_recalc(si, si_find_pos(si, ts, author_sig)); - } - DEBUG_DEBUG(DEBUG_CATEGORY_CHAT_SYNC, "history insert tbl=%s ts=%llu incoming=%d stored=%d deleted=%d result=%d", - SI_TBL(si), (unsigned long long)ts, incoming, exists, deleted, ret); - return ret; + if (sqlite3_exec(db, "COMMIT", NULL, NULL, NULL) != SQLITE_OK) goto rollback; + DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "history schema: message_id ready table=%s records=%lld", SI_TBL(si), (long long)count); + return 0; rollback: - DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "history insert tbl=%s stage=%s: %s; rolling back", + DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "history schema: table=%s stage=%s: %s; rolling back", SI_TBL(si), stage, sqlite3_errmsg(db)); - sqlite3_finalize(stmt); + sqlite3_finalize(rows); sqlite3_finalize(insert); if (sqlite3_exec(db, "ROLLBACK", NULL, NULL, NULL) != SQLITE_OK) - DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "history insert rollback tbl=%s: %s", SI_TBL(si), sqlite3_errmsg(db)); + DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "history schema: rollback table=%s: %s", SI_TBL(si), sqlite3_errmsg(db)); return -1; +error: + DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "history schema: table=%s stage=%s: %s", SI_TBL(si), stage, sqlite3_errmsg(db)); + sqlite3_finalize(rows); return -1; } -// Вставляет запись + каскад: on_insert + PUSH всем connected пирам (кроме источника) -static int db_record_insert_cascade(struct DB_SYNC_INSTANCE* si, - uint64_t rid, uint64_t rts, uint64_t rauthor, - const char* rdata, uint32_t rdlen, - const uint8_t* rsig, uint64_t source_peer, - const char* local_attrs, int initial_sync) -{ - int ret = db_record_insert(si, rid, rts, rauthor, rdata, rdlen, rsig, local_attrs); - if (ret != 0) return ret; - uint8_t message_id[32]; - if (chat_message_id(si->group_id, rts, rsig, message_id) < 0) return -1; - if (si->on_insert) { - si->on_insert(si, message_id, rts, rdata, rdlen, rauthor, initial_sync, si->on_insert_arg); +#define DB_COLUMNS "rowid,id,timestamp,node_id,data,author_signature,message_id,bucket" + +/* Чтение конкретной строки. cache_valid — производные поля, их можно восстановить без удаления сообщения. */ +static enum db_record_check db_read_record(struct DB_SYNC_INSTANCE* si, sqlite3_stmt* st, struct db_record* r) { + memset(r, 0, sizeof(*r)); + r->rowid = sqlite3_column_int64(st, 0); r->id = (uint64_t)sqlite3_column_int64(st, 1); + r->timestamp = (uint64_t)sqlite3_column_int64(st, 2); r->author = (uint64_t)sqlite3_column_int64(st, 3); + r->data = sqlite3_column_blob(st, 4); + if (!r->data && sqlite3_errcode(SI_DB(si)) == SQLITE_NOMEM) return DB_CHECK_ERROR; + r->data_len = (size_t)sqlite3_column_bytes(st, 4); + const uint8_t* sig = sqlite3_column_blob(st, 5); + if (!sig && sqlite3_errcode(SI_DB(si)) == SQLITE_NOMEM) return DB_CHECK_ERROR; + r->sig_len = (size_t)sqlite3_column_bytes(st, 5); + if (!r->data || !r->data_len || r->data_len > DB_SYNC_MESSAGE_MAX || !sig || r->sig_len != 64) { + DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "history malformed stored row tbl=%s rowid=%lld body=%zu signature=%zu", + SI_TBL(si), (long long)r->rowid, r->data_len, r->sig_len); return DB_CHECK_BAD_FORMAT; } + memcpy(r->signature, sig, 64); + if (chat_message_id(si->group_id, r->timestamp, r->signature, r->message_id) < 0) return DB_CHECK_ERROR; + const uint8_t* key = sqlite3_column_blob(st, 6); + if (!key && sqlite3_errcode(SI_DB(si)) == SQLITE_NOMEM) return DB_CHECK_ERROR; + r->cache_valid = key && sqlite3_column_bytes(st, 6) == 32 && !memcmp(key, r->message_id, 32) && + (uint64_t)sqlite3_column_int64(st, 7) == (db_read64(r->message_id) >> 39); + return DB_CHECK_VALID; +} - uint8_t pbuf[4096]; uint32_t poff = 1; pbuf[0] = DB_MSG_PUSH; - memcpy(pbuf + poff, &rid, 8); poff += 8; - memcpy(pbuf + poff, &rts, 8); poff += 8; - memcpy(pbuf + poff, &rauthor, 8); poff += 8; - memcpy(pbuf + poff, &rdlen, 4); poff += 4; - if (rdlen > 0) { memcpy(pbuf + poff, rdata, rdlen); poff += rdlen; } - uint32_t sl = DB_SIG_SIZE; pbuf[poff++] = (uint8_t)sl; - memcpy(pbuf + poff, rsig, sl); poff += sl; - - uint64_t my_id = si->db_sync->inst->node_id; - int push_count = 0; - for (int i = 0; i < si->peer_count; i++) { - uint64_t pid = si->peers[i].node_id; - if (si->peers[i].sync_state >= 1 && pid != source_peer && pid != my_id) { - if (db_sync_peer_throttled(si->db_sync, &si->peers[i])) continue; /* спящий пир в «тихом» чате — не пушим */ - if (db_sync_send(si, pid, pbuf, poff) >= 0) - { push_count++; } - } +static int db_encode_record(const struct db_record* r, uint8_t* buf) { + memcpy(buf, r->message_id, 32); db_write64(buf + 32, r->timestamp); db_write64(buf + 40, r->author); + db_write32(buf + 48, (uint32_t)r->data_len); memcpy(buf + 52, r->data, r->data_len); + memcpy(buf + 52 + r->data_len, r->signature, 64); + return (int)(DB_WIRE_FIXED + r->data_len); +} +static int db_parse_record(struct DB_SYNC_INSTANCE* si, const uint8_t** ptr, const uint8_t* end, struct db_record* r) { + if ((size_t)(end - *ptr) < DB_WIRE_FIXED) { DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "history truncated record"); return -1; } + const uint8_t* p = *ptr; + memset(r, 0, sizeof(*r)); + r->timestamp = db_read64(p + 32); r->author = db_read64(p + 40); r->data_len = db_read32(p + 48); + if (!r->data_len || r->data_len > DB_SYNC_MESSAGE_MAX || r->data_len > (size_t)(end - p) - DB_WIRE_FIXED) { + DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "history invalid wire body=%zu", r->data_len); return -1; } - if (push_count > 0) - DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "sync [%s:ME--] → PUSH: id=%llu => %d peers", - SI_SHRT(si), (unsigned long long)rid, push_count); + r->data = (const char*)p + 52; r->sig_len = 64; memcpy(r->signature, p + 52 + r->data_len, 64); + if (chat_message_id(si->group_id, r->timestamp, r->signature, r->message_id) < 0) return -1; + if (memcmp(r->message_id, p, 32)) { DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "history wire identity mismatch"); return -1; } + *ptr = p + DB_WIRE_FIXED + r->data_len; return 0; } -// ============================================================ -// Send -// ============================================================ - -// Отправляет сообщение узлу через ETCP с заголовком: [ETCP_ID_DB_SYNC:1][group_id_be:8][payload]. Возвращает код etcp_send -static int db_sync_send_gid(struct DB_SYNC* db, uint64_t node_id, uint64_t group_id, const uint8_t* payload, size_t plen) -{ - struct ll_entry* entry = queue_entry_new(0); - if (!entry) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "queue_entry_new"); return -1; } - uint8_t* buf = u_malloc(plen + 9); - if (!buf) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "db_sync_send_gid: u_malloc(%zu) failed for dst=%016llx", plen + 9, (unsigned long long)node_id); queue_entry_free(entry); return -1; } - buf[0] = ETCP_ID_DB_SYNC; - uint64_t gb = htobe64(group_id); - memcpy(buf + 1, &gb, 8); - memcpy(buf + 9, payload, plen); - entry->dgram = buf; - entry->len = plen + 9; - - struct ETCP_CONN* conn = instance_find_conn(db->inst, node_id); - if (!conn || !conn->links_up) { - DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "history send: transport unavailable peer=%016llx group=%016llx type=%02x", - (unsigned long long)node_id, (unsigned long long)group_id, plen > 0 ? payload[0] : 0); - queue_dgram_free(entry); queue_entry_free(entry); - return -1; - } - int ret = etcp_send(conn, entry); - if (ret != 0) { - DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "history send rejected peer=%016llx group=%016llx type=%02x ret=%d", - (unsigned long long)node_id, (unsigned long long)group_id, plen > 0 ? payload[0] : 0, ret); - queue_dgram_free(entry); queue_entry_free(entry); - } - DEBUG_DEBUG(DEBUG_CATEGORY_CHAT_SYNC, "sync: send → etcp_send ret=%d conn=%s l_up=%d init=%d q=%p", - ret, conn->log_name, conn->links_up, conn->initialized, (void*)conn->send_input_q); - return ret; +static int db_index_clear(struct DB_SYNC_INSTANCE* si) { + char ns[25]; snprintf(ns, sizeof(ns), "%u:%s", MT_DATA_MESSAGES, si->ns); + sqlite3_stmt* st = NULL; + int rc = sqlite3_prepare_v2(SI_DB(si), "DELETE FROM merkle_tree_hash WHERE namespace=?", -1, &st, NULL); + if (rc == SQLITE_OK) rc = sqlite3_bind_text(st, 1, ns, -1, SQLITE_STATIC); + if (rc == SQLITE_OK) rc = sqlite3_step(st); + int end_rc = sqlite3_finalize(st); + return rc == SQLITE_DONE && end_rc == SQLITE_OK ? 0 : db_error(si, "clear message index"); } -// Отправляет сообщение через ETCP от имени конкретного инстанса (подставляет group_id инстанса) -static int db_sync_send(struct DB_SYNC_INSTANCE* si, uint64_t dst_node_id, const uint8_t* payload, size_t len) -{ - struct TOPO_GROUP* group = topo_groups_find(si->db_sync->inst->topo_groups, si->group_id); - int ret = -1; - if (!si->sync_gated && topo_group_peer_ready(group, dst_node_id)) - ret = db_sync_send_gid(si->db_sync, dst_node_id, si->group_id, payload, len); - else DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "history send blocked group=%016llx peer=%016llx members_ready=%d peer_ready=%d", - si->group_id, dst_node_id, !si->sync_gated, topo_group_peer_ready(group, dst_node_id)); - if (ret != 0) { - struct SI_PEER* p = si_peer_find(si, dst_node_id); - if (p) { p->sync_state = 0; p->sync_start_tb = 0; } - db_sync_resume_peer_check(si->db_sync); - } - return ret; +static int db_update_path(struct DB_SYNC_INSTANCE* si, const uint8_t key[32]) { + if (si->index.pending || !merkle_sync_registered(si->db_sync->inst, MT_DATA_MESSAGES)) return 0; + return merkle_sync_recompute_path(si->db_sync->inst, MT_DATA_MESSAGES, si->ns, db_read64(key)) < 0 ? -1 : 0; } -// ============================================================ -// Delivery chain helpers (local fields) -// ============================================================ +static void db_committed(struct DB_SYNC_INSTANCE* si, int changed) { + if (si->index.pending || (changed & 2)) db_index_request(si); + else merkle_sync_changed(si->db_sync->inst, MT_DATA_MESSAGES, si->ns); +} +static int db_same_record(const struct db_record* a, const struct db_record* b) { + return a->timestamp == b->timestamp && a->author == b->author && a->data_len == b->data_len && + a->sig_len == b->sig_len && !memcmp(a->signature, b->signature, 64) && !memcmp(a->data, b->data, a->data_len); +} + +/* Внешняя транзакция обязательна. Входящая подпись уже проверена до захвата write-lock. */ +static int db_put_record(struct DB_SYNC_INSTANCE* si, struct db_record* r, enum db_record_check incoming, + const char* attrs, int* changed) { + sqlite3_stmt* st = NULL; + if (si_prep(si, &st, "SELECT " DB_COLUMNS " FROM \"%s\" WHERE timestamp=? AND author_signature=?") != SQLITE_OK) + return db_error(si, "read duplicate"); + int rc = sqlite3_bind_int64(st, 1, (sqlite3_int64)r->timestamp); + if (rc == SQLITE_OK) rc = sqlite3_bind_blob(st, 2, r->signature, 64, SQLITE_STATIC); + if (rc == SQLITE_OK) rc = sqlite3_step(st); + struct db_record stored; + int exists = rc == SQLITE_ROW, remove = 0; + if (exists) { + enum db_record_check check = db_read_record(si, st, &stored); + if (check == DB_CHECK_VALID && !(incoming == DB_CHECK_VALID && db_same_record(&stored, r))) + check = db_verify_author_sig(si, stored.timestamp, stored.author, stored.data, stored.data_len, stored.signature, stored.sig_len); + if (check == DB_CHECK_ERROR || check == DB_CHECK_DEFERRED) { sqlite3_finalize(st); return -1; } + remove = check == DB_CHECK_BAD_FORMAT || check == DB_CHECK_BAD_SIGNATURE; + if (!remove && !stored.cache_valid) { + sqlite3_finalize(st); db_index_request(si); return db_error(si, "derived identity needs rebuild"); + } + } else if (rc != SQLITE_DONE) { sqlite3_finalize(st); return db_error(si, "duplicate step"); } + if (sqlite3_finalize(st) != SQLITE_OK) return db_error(si, "duplicate finalize"); + if (remove) { + if (db_delete_bad_row(si, stored.rowid) < 0) return -1; + if (merkle_sync_registered(si->db_sync->inst, MT_DATA_MESSAGES) && db_index_clear(si) < 0) return -1; + *changed |= 3; + DEBUG_DEBUG(DEBUG_CATEGORY_CHAT_SYNC, "history staged deletion of proven corrupt duplicate tbl=%s rowid=%lld", + SI_TBL(si), (long long)stored.rowid); + } + if (incoming != DB_CHECK_VALID) return -2; + if (exists && !remove) return 1; + if (si_prep(si, &st, "INSERT INTO \"%s\" (timestamp,node_id,id,flags,data,author_signature,message_id,bucket,local_attrs)" + " VALUES(?,?,?,0,?,?,?,?,?)") != SQLITE_OK) return db_error(si, "insert prepare"); + rc = sqlite3_bind_int64(st, 1, (sqlite3_int64)r->timestamp); + if (rc == SQLITE_OK) rc = sqlite3_bind_int64(st, 2, (sqlite3_int64)r->author); + if (rc == SQLITE_OK) rc = sqlite3_bind_int64(st, 3, (sqlite3_int64)r->id); + if (rc == SQLITE_OK) rc = sqlite3_bind_blob(st, 4, r->data, (int)r->data_len, SQLITE_STATIC); + if (rc == SQLITE_OK) rc = sqlite3_bind_blob(st, 5, r->signature, 64, SQLITE_STATIC); + if (rc == SQLITE_OK) rc = sqlite3_bind_blob(st, 6, r->message_id, 32, SQLITE_STATIC); + if (rc == SQLITE_OK) rc = sqlite3_bind_int64(st, 7, (sqlite3_int64)(db_read64(r->message_id) >> 39)); + if (rc == SQLITE_OK) rc = sqlite3_bind_text(st, 8, attrs ? attrs : "", -1, SQLITE_STATIC); + if (rc == SQLITE_OK) rc = sqlite3_step(st); + int final_rc = sqlite3_finalize(st); + if (rc != SQLITE_DONE || final_rc != SQLITE_OK) return db_error(si, "insert step"); + if (!(*changed & 2) && db_update_path(si, r->message_id) < 0) return -1; + *changed |= 1; return 0; +} + +/* Предварительная проверка идёт без write-lock; внутри короткой транзакции сверяем прочитанные байты. */ +static int db_index_batch(struct DB_SYNC_INSTANCE* si) { + sqlite3* db = SI_DB(si); + sqlite3_stmt* st = NULL; + if (!merkle_sync_registered(si->db_sync->inst, MT_DATA_MESSAGES)) return 1; + if (si->index.restart) { + merkle_sync_cancel_dataset_ns(si->db_sync->inst, MT_DATA_MESSAGES, si->ns); + if (sqlite3_exec(db, "BEGIN IMMEDIATE", NULL, NULL, NULL) != SQLITE_OK) return db_error(si, "index begin"); + if (db_index_clear(si) < 0 || sqlite3_exec(db, "COMMIT", NULL, NULL, NULL) != SQLITE_OK) { + db_rollback(si, "index reset"); return -1; + } + si->index.restart = 0; si->index.phase = 0; si->index.after_valid = 0; si->index.after = 0; + } + if (si->index.phase == 0) { + struct db_checked* rows = u_calloc(DB_BATCH_RECORDS, sizeof(*rows)); + if (!rows) return db_error(si, "preflight allocation"); + const char* query = si->index.after_valid + ? "SELECT " DB_COLUMNS " FROM \"%s\" WHERE rowid>? ORDER BY rowid LIMIT 16" + : "SELECT " DB_COLUMNS " FROM \"%s\" ORDER BY rowid LIMIT 16"; + int rc = si_prep(si, &st, query), count = 0, removed = 0; + if (rc == SQLITE_OK && si->index.after_valid) rc = sqlite3_bind_int64(st, 1, si->index.after); + if (rc != SQLITE_OK) goto verify_error; + while ((rc = sqlite3_step(st)) == SQLITE_ROW) { + struct db_checked* item = &rows[count]; + item->check = db_read_record(si, st, &item->record); + if (item->check == DB_CHECK_VALID) { + memcpy(item->data, item->record.data, item->record.data_len); item->record.data = item->data; + item->check = db_verify_author_sig(si, item->record.timestamp, item->record.author, item->record.data, + item->record.data_len, item->record.signature, item->record.sig_len); + } + if (item->check == DB_CHECK_ERROR || item->check == DB_CHECK_DEFERRED) { + si->index.waiting_key = item->check == DB_CHECK_DEFERRED; + goto verify_error; + } + count++; + } + int final_rc = sqlite3_finalize(st); st = NULL; + if (rc != SQLITE_DONE || final_rc != SQLITE_OK) goto verify_error; + st = NULL; + if (!count) { + si->index.phase = 1; si->index.after_valid = 0; si->index.after = -1; + u_free(rows); return 0; + } + if (sqlite3_exec(db, "BEGIN IMMEDIATE", NULL, NULL, NULL) != SQLITE_OK) goto verify_error; + for (int i = 0; i < count; i++) { + struct db_checked* item = &rows[i]; struct db_record current; + if (si_prep(si, &st, "SELECT " DB_COLUMNS " FROM \"%s\" WHERE rowid=?") != SQLITE_OK) goto verify_rollback; + if (sqlite3_bind_int64(st, 1, item->record.rowid) != SQLITE_OK || sqlite3_step(st) != SQLITE_ROW) goto verify_rollback; + enum db_record_check now = db_read_record(si, st, ¤t); + int matches = now == DB_CHECK_BAD_FORMAT && item->check == DB_CHECK_BAD_FORMAT; + if (now == DB_CHECK_VALID && item->check != DB_CHECK_BAD_FORMAT) matches = db_same_record(&item->record, ¤t); + if (sqlite3_finalize(st) != SQLITE_OK) { st = NULL; goto verify_rollback; } + st = NULL; + if (!matches) { DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "history preflight source changed tbl=%s; retry", SI_TBL(si)); goto verify_rollback; } + if (item->check == DB_CHECK_BAD_FORMAT || item->check == DB_CHECK_BAD_SIGNATURE) { + if (db_delete_bad_row(si, item->record.rowid) < 0) goto verify_rollback; + removed++; + } else { + if (si_prep(si, &st, "UPDATE \"%s\" SET message_id=?,bucket=? WHERE rowid=?") != SQLITE_OK) goto verify_rollback; + if (sqlite3_bind_blob(st, 1, item->record.message_id, 32, SQLITE_STATIC) != SQLITE_OK || + sqlite3_bind_int64(st, 2, (sqlite3_int64)(db_read64(item->record.message_id) >> 39)) != SQLITE_OK || + sqlite3_bind_int64(st, 3, item->record.rowid) != SQLITE_OK || sqlite3_step(st) != SQLITE_DONE) goto verify_rollback; + if (sqlite3_finalize(st) != SQLITE_OK) { st = NULL; goto verify_rollback; } + st = NULL; + } + } + if (sqlite3_exec(db, "COMMIT", NULL, NULL, NULL) != SQLITE_OK) goto verify_rollback; + si->index.after = rows[count - 1].record.rowid; si->index.after_valid = 1; si->index.waiting_key = 0; + DEBUG_DEBUG(DEBUG_CATEGORY_CHAT_SYNC, "history preflight committed tbl=%s checked=%d removed=%d after=%lld", + SI_TBL(si), count, removed, (long long)si->index.after); + if (removed) DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "history removed %d proven invalid stored records tbl=%s", removed, SI_TBL(si)); + u_free(rows); return 0; +verify_rollback: + sqlite3_finalize(st); st = NULL; db_rollback(si, "preflight commit"); +verify_error: + sqlite3_finalize(st); u_free(rows); return db_error(si, si->index.waiting_key ? "preflight waiting for author key" : "preflight failed"); + } + + /* Все сохранённые подписи проверены; в write-транзакции остаётся только хеширование ограниченной пачки листьев. */ + sqlite3_int64 buckets[DB_BATCH_RECORDS]; int count = 0, rc; + if (si_prep(si, &st, "SELECT DISTINCT bucket FROM \"%s\" WHERE bucket>? ORDER BY bucket LIMIT 16") != SQLITE_OK) + return db_error(si, "index buckets"); + if (sqlite3_bind_int64(st, 1, si->index.after) != SQLITE_OK) { sqlite3_finalize(st); return db_error(si, "bucket bind"); } + while ((rc = sqlite3_step(st)) == SQLITE_ROW) buckets[count++] = sqlite3_column_int64(st, 0); + int end_rc = sqlite3_finalize(st); + if (rc != SQLITE_DONE || end_rc != SQLITE_OK) return db_error(si, "bucket step"); + if (sqlite3_exec(db, "BEGIN IMMEDIATE", NULL, NULL, NULL) != SQLITE_OK) return db_error(si, "hash begin"); + for (int i = 0; i < count; i++) + if (merkle_sync_recompute_path(si->db_sync->inst, MT_DATA_MESSAGES, si->ns, (uint64_t)buckets[i] << 39) < 0) { + db_rollback(si, "hash batch"); return -1; + } + if (sqlite3_exec(db, "COMMIT", NULL, NULL, NULL) != SQLITE_OK) { db_rollback(si, "hash commit"); return -1; } + if (count) si->index.after = buckets[count - 1]; + else { + si->index.pending = 0; + DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "history verified index ready tbl=%s group=%s", SI_TBL(si), si->ns); + merkle_sync_changed(si->db_sync->inst, MT_DATA_MESSAGES, si->ns); + for (int i = 0; i < si->peer_count; i++) db_start_peer(si, si->peers[i].node_id); + } + return 0; +} + +static void db_index_tick(void* arg) { + struct DB_SYNC_INSTANCE* si = arg; si->index.timer = NULL; + if (!si->enabled || !si->index.pending) return; + int rc = db_index_batch(si); + if (si->index.pending && merkle_sync_registered(si->db_sync->inst, MT_DATA_MESSAGES)) { + si->index.timer = uasync_set_timeout(si->db_sync->inst->ua, rc ? 50000 : 0, si, db_index_tick, "history_preflight"); + if (!si->index.timer) { db_error(si, "preflight timer"); db_sync_resume_peer_check(si->db_sync); } + } +} +static void db_index_request(struct DB_SYNC_INSTANCE* si) { + si->index.pending = 1; si->index.restart = 1; si->index.waiting_key = 0; + if (!si->index.timer && merkle_sync_registered(si->db_sync->inst, MT_DATA_MESSAGES)) + si->index.timer = uasync_set_timeout(si->db_sync->inst->ua, 0, si, db_index_tick, "history_preflight"); + db_sync_resume_peer_check(si->db_sync); +} + +/* Чтение листа — одинаковый канонический отбор для корня и передачи. Проверка авторства уже выполнена preflight/put. */ +static int db_message_hash(void* ctx, const char* ns, uint8_t level, uint64_t prefix, EVP_MD_CTX* hash) { + struct DB_SYNC_INSTANCE* si = db_sync_instance_find(ctx, strtoull(ns, NULL, 10)); + if (!si || level != MT_MAX_LEVEL) return -1; + sqlite3_stmt* st = NULL; + if (si_prep(si, &st, "SELECT " DB_COLUMNS " FROM \"%s\" WHERE bucket=? ORDER BY message_id") != SQLITE_OK) + return db_error(si, "leaf prepare"); + int rc = sqlite3_bind_int64(st, 1, (sqlite3_int64)(prefix >> 39)), count = 0, bad = 0; + if (rc != SQLITE_OK) { sqlite3_finalize(st); return db_error(si, "leaf bind"); } + while ((rc = sqlite3_step(st)) == SQLITE_ROW) { + struct db_record r; uint8_t wire[DB_WIRE_MAX]; + enum db_record_check check = db_read_record(si, st, &r); + if (check != DB_CHECK_VALID || !r.cache_valid) { bad = check != DB_CHECK_ERROR; rc = SQLITE_ERROR; break; } + int bytes = db_encode_record(&r, wire); + if (EVP_DigestUpdate(hash, wire, (size_t)bytes) != 1) { rc = SQLITE_ERROR; break; } + count++; + } + int end_rc = sqlite3_finalize(st); + if (bad) db_index_request(si); + if (rc != SQLITE_DONE || end_rc != SQLITE_OK) return db_error(si, "leaf read/hash"); + return count; +} + +static int db_message_page(void* ctx, const char* ns, uint64_t prefix, int av, const merkle_key_t* after, + uint8_t* buf, size_t* len, merkle_key_t* next, int* more) { + struct DB_SYNC_INSTANCE* si = db_sync_instance_find(ctx, strtoull(ns, NULL, 10)); + if (!si || si->index.pending) return -1; + sqlite3_stmt* st = NULL; + const char* query = av + ? "SELECT " DB_COLUMNS " FROM \"%s\" WHERE bucket=? AND message_id>? ORDER BY message_id LIMIT 17" + : "SELECT " DB_COLUMNS " FROM \"%s\" WHERE bucket=? ORDER BY message_id LIMIT 17"; + int rc = si_prep(si, &st, query), count = 0, bad = 0; + if (rc == SQLITE_OK) rc = sqlite3_bind_int64(st, 1, (sqlite3_int64)(prefix >> 39)); + if (rc == SQLITE_OK && av) rc = sqlite3_bind_blob(st, 2, after->bytes, 32, SQLITE_STATIC); + if (rc != SQLITE_OK) { sqlite3_finalize(st); return db_error(si, "page prepare"); } + size_t used = 0; *more = 0; memset(next, 0, sizeof(*next)); + while ((rc = sqlite3_step(st)) == SQLITE_ROW) { + struct db_record r; + enum db_record_check check = db_read_record(si, st, &r); + if (check == DB_CHECK_VALID) check = db_verify_author_sig(si, r.timestamp, r.author, r.data, r.data_len, r.signature, r.sig_len); + if (check != DB_CHECK_VALID || !r.cache_valid) { + bad = check == DB_CHECK_BAD_SIGNATURE || check == DB_CHECK_BAD_FORMAT || (check == DB_CHECK_VALID && !r.cache_valid); + rc = SQLITE_ERROR; break; + } + size_t bytes = DB_WIRE_FIXED + r.data_len; + if (count == DB_BATCH_RECORDS || used + bytes > *len) { + if (!count) { rc = SQLITE_TOOBIG; break; } + *more = 1; break; + } + used += (size_t)db_encode_record(&r, buf + used); memcpy(next->bytes, r.message_id, 32); count++; + } + int end_rc = sqlite3_finalize(st); + if (bad) db_index_request(si); + if ((rc != SQLITE_DONE && !(rc == SQLITE_ROW && *more)) || end_rc != SQLITE_OK) return db_error(si, "page read"); + *len = used; return 0; +} + +static void db_done_sweep(struct DB_SYNC* db) { + if (db->dispatch_depth) return; + struct db_sync_done_cbk_entry** link = &db->done_cbks; + while (*link) { + struct db_sync_done_cbk_entry* e = *link; + if (e->removed) { *link = e->next; u_free(e); } else link = &e->next; + } +} +void db_sync_add_done_cbk(struct UTUN_INSTANCE* inst, db_sync_done_fn fn, void* arg) { + struct DB_SYNC* db = inst ? inst->db_sync : NULL; + if (!db || !fn) return; + struct db_sync_done_cbk_entry* e = u_calloc(1, sizeof(*e)); + if (!e) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "history done subscription allocation"); return; } + e->fn = fn; e->arg = arg; e->next = db->done_cbks; db->done_cbks = e; +} +void db_sync_remove_done_cbk(struct UTUN_INSTANCE* inst, db_sync_done_fn fn, void* arg) { + struct DB_SYNC* db = inst ? inst->db_sync : NULL; + if (!db) return; + for (struct db_sync_done_cbk_entry* e = db->done_cbks; e; e = e->next) + if (!e->removed && e->fn == fn && e->arg == arg) { e->removed = 1; break; } + db_done_sweep(db); +} +static void db_message_result(void* ctx, const char* ns, uint64_t peer, int result) { + struct DB_SYNC_INSTANCE* si = db_sync_instance_find(ctx, strtoull(ns, NULL, 10)); + if (!si) return; + struct SI_PEER* p = si_peer_add(si, peer); + if (!p) return; + p->sync_state = result == MT_OK ? 2 : 0; + if (result != MT_OK) { db_sync_resume_peer_check(si->db_sync); return; } + struct DB_SYNC* db = si->db_sync; + si->holds++; db->dispatch_depth++; + for (struct db_sync_done_cbk_entry* e = db->done_cbks; e && si->enabled; e = e->next) + if (!e->removed) e->fn(si, peer, e->arg); + db->dispatch_depth--; db_done_sweep(db); db_release(si); +} + +static int db_message_peer(void* ctx, const char* ns, uint64_t peer) { + struct UTUN_INSTANCE* inst = ctx; + struct DB_SYNC_INSTANCE* si = db_sync_instance_find(inst, strtoull(ns, NULL, 10)); + struct TOPO_GROUP* group = si ? topo_groups_find(inst->topo_groups, si->group_id) : NULL; + struct ETCP_CONN* conn = instance_find_conn(inst, peer); + return si && !si->sync_gated && !si->index.pending && topo_group_peer_ready(group, peer) && conn && + member_sync_validate_pubkey(inst, ns, conn->crypto_ctx.peer_public_key); +} +static int db_sync_peer_throttled(struct DB_SYNC* db, struct SI_PEER* p) { + struct ETCP_CONN* conn = p && !p->sound_on ? instance_find_conn(db->inst, p->node_id) : NULL; + return conn && conn->peer_sleep_phase; +} +static int db_message_can_run(void* ctx, const char* ns, uint64_t peer) { + struct DB_SYNC_INSTANCE* si = db_sync_instance_find(ctx, strtoull(ns, NULL, 10)); + if (!si || !db_message_peer(ctx, ns, peer)) return 0; + struct SI_PEER* p = si_peer_find(si, peer); + return !p || (!p->paused && !db_sync_peer_throttled(si->db_sync, p)); +} +static int db_start_peer(struct DB_SYNC_INSTANCE* si, uint64_t peer) { + if (!merkle_sync_registered(si->db_sync->inst, MT_DATA_MESSAGES) || !db_message_can_run(si->db_sync->inst, si->ns, peer)) return 0; + struct SI_PEER* p = si_peer_add(si, peer); + if (!p) return -1; + if (merkle_sync_start(si->db_sync->inst, MT_DATA_MESSAGES, peer, si->ns, NULL, NULL) < 0) { + p->sync_state = 0; db_sync_resume_peer_check(si->db_sync); return -1; + } + p->sync_state = 1; db_sync_resume_peer_check(si->db_sync); return 0; +} + +static void db_notice_unlink(struct db_notice* n) { + struct db_notice** link = &n->db->notices; + while (*link && *link != n) link = &(*link)->next; + if (*link) *link = n->next; +} +static void db_notice_run(void* arg) { + struct db_notice* n = arg; n->work = NULL; db_notice_unlink(n); + struct DB_SYNC_INSTANCE* si = db_instance_find(n->db, n->group_id); + if (si) { + si->holds++; + struct db_record* records = (struct db_record*)(n + 1); + for (unsigned i = 0; i < n->count && si->enabled; i++) + if (si->on_insert) si->on_insert(si, records[i].message_id, records[i].timestamp, records[i].data, + records[i].data_len, records[i].author, n->initial, si->on_insert_arg); + db_release(si); + } + u_free(n); +} +static void db_notice_abort(struct db_notice* n) { + if (!n) return; + uasync_call_soon_cancel(n->db->inst->ua, n->work); db_notice_unlink(n); u_free(n); +} +static int db_notice_stage(struct DB_SYNC_INSTANCE* si, struct db_record* records, int count, const uint8_t* added, struct db_notice** out) { + size_t bodies = 0; unsigned total = 0; *out = NULL; + for (int i = 0; i < count; i++) if (added[i]) { total++; bodies += records[i].data_len; } + if (!total || !si->on_insert) return 0; + struct db_notice* n = u_calloc(1, sizeof(*n) + total * sizeof(struct db_record) + bodies); + if (!n) return db_error(si, "notification allocation"); + n->db = si->db_sync; n->group_id = si->group_id; n->initial = 1; n->count = total; + struct db_record* saved = (struct db_record*)(n + 1); char* data = (char*)(saved + total); unsigned index = 0; + for (int i = 0; i < count; i++) if (added[i]) { + saved[index] = records[i]; saved[index].data = data; memcpy(data, records[i].data, records[i].data_len); + data += records[i].data_len; index++; + } + n->work = uasync_call_soon(si->db_sync->inst->ua, n, db_notice_run); + if (!n->work) { u_free(n); return db_error(si, "notification scheduling"); } + n->next = si->db_sync->notices; si->db_sync->notices = n; *out = n; return 0; +} + +/* Полная проверка входящей страницы — до BEGIN; write-lock охватывает только небольшой commit данных и индекса. */ +static int db_message_apply(void* ctx, const char* ns, uint64_t peer, const uint8_t* data, size_t len) { + (void)peer; + struct DB_SYNC_INSTANCE* si = db_sync_instance_find(ctx, strtoull(ns, NULL, 10)); + if (!si || si->index.pending) return -1; + struct db_record records[DB_BATCH_RECORDS]; uint8_t added[DB_BATCH_RECORDS] = {0}; + const uint8_t* p = data; int count = 0; + while (p < data + len) { + if (count == DB_BATCH_RECORDS || db_parse_record(si, &p, data + len, &records[count]) < 0) return -1; + struct db_record* r = &records[count]; + if (db_verify_author_sig(si, r->timestamp, r->author, r->data, r->data_len, r->signature, r->sig_len) != DB_CHECK_VALID) return -1; + if (count && memcmp(records[count - 1].message_id, r->message_id, 32) >= 0) { + DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "history incoming page not strictly ordered ns=%s", ns); return -1; + } + count++; + } + if (!count) return 0; + sqlite3* db = SI_DB(si); + if (sqlite3_exec(db, "BEGIN IMMEDIATE", NULL, NULL, NULL) != SQLITE_OK) return db_error(si, "page begin"); + uint64_t next = si->next_id; int changed = 0; struct db_notice* notice = NULL; + for (int i = 0; i < count; i++) { + records[i].id = next; + int result = db_put_record(si, &records[i], DB_CHECK_VALID, NULL, &changed); + if (result < 0) goto rollback; + if (!result) { added[i] = 1; next++; } + } + if (db_notice_stage(si, records, count, added, ¬ice) < 0) goto rollback; + if (sqlite3_exec(db, "COMMIT", NULL, NULL, NULL) != SQLITE_OK) goto rollback; + si->next_id = next; + if (changed) db_committed(si, changed); + DEBUG_DEBUG(DEBUG_CATEGORY_CHAT_SYNC, "history page committed ns=%s records=%d changed=%d", ns, count, changed); + return 0; +rollback: + db_notice_abort(notice); db_rollback(si, "page apply"); return -1; +} + +static const struct merkle_sync_data_ops db_message_ops = { + .update_bucket_hash = db_message_hash, .get_page = db_message_page, .apply_items = db_message_apply, + .validate_peer = db_message_peer, .can_run = db_message_can_run, .on_result = db_message_result +}; + +int db_sync_register_model(struct UTUN_INSTANCE* inst) { + if (!inst || !inst->db_sync || !inst->db_sync->enabled) return -1; + if (merkle_sync_registered(inst, MT_DATA_MESSAGES)) return 0; + if (merkle_sync_register(inst, MT_DATA_MESSAGES, &db_message_ops, inst) < 0) return -1; + for (int i = 0; i < inst->db_sync->instance_count; i++) db_index_request(inst->db_sync->instances[i]); + DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "history registered shared Merkle model"); + return 0; +} + +static int db_send(struct DB_SYNC_INSTANCE* si, uint64_t peer, uint8_t type, const uint8_t* data, size_t len) { + if (!db_message_can_run(si->db_sync->inst, si->ns, peer)) return -1; + struct ETCP_CONN* conn = instance_find_conn(si->db_sync->inst, peer); + if (!conn || !conn->links_up) { DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "history transport unavailable peer=%016llx", peer); return -1; } + struct ll_entry* e = ll_alloc_lldgram((uint16_t)(10 + len)); + if (!e) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "history packet allocation"); return -1; } + e->dgram[0] = ETCP_ID_DB_SYNC; db_write64(e->dgram + 1, si->group_id); e->dgram[9] = type; + if (len) memcpy(e->dgram + 10, data, len); + e->len = (uint16_t)(10 + len); + int rc = etcp_send(conn, e); + if (rc != 0) { queue_dgram_free(e); queue_entry_free(e); DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "history send rejected peer=%016llx rc=%d", peer, rc); } + return rc; +} + +static void db_emit_push(struct DB_SYNC_INSTANCE* si, const struct db_record* r, uint64_t source) { + si->holds++; + if (si->on_insert) si->on_insert(si, r->message_id, r->timestamp, r->data, r->data_len, r->author, 0, si->on_insert_arg); + if (si->enabled) { + uint8_t wire[DB_WIRE_MAX]; size_t bytes = (size_t)db_encode_record(r, wire); + for (int i = 0; i < si->peer_count; i++) + if (si->peers[i].node_id != source && si->peers[i].sync_state && !si->peers[i].paused && + !db_sync_peer_throttled(si->db_sync, &si->peers[i])) + db_send(si, si->peers[i].node_id, DB_MSG_PUSH, wire, bytes); + } + db_release(si); +} +static int db_insert_one(struct DB_SYNC_INSTANCE* si, struct db_record* r, const char* attrs) { + enum db_record_check incoming = db_verify_author_sig(si, r->timestamp, r->author, r->data, r->data_len, r->signature, r->sig_len); + if (incoming == DB_CHECK_ERROR || incoming == DB_CHECK_DEFERRED) return -1; + if (chat_message_id(si->group_id, r->timestamp, r->signature, r->message_id) < 0) return -1; + sqlite3* db = SI_DB(si); + if (sqlite3_exec(db, "BEGIN IMMEDIATE", NULL, NULL, NULL) != SQLITE_OK) return db_error(si, "insert begin"); + int changed = 0; r->id = si->next_id; + int result = db_put_record(si, r, incoming, attrs, &changed); + if (result == -1 || sqlite3_exec(db, "COMMIT", NULL, NULL, NULL) != SQLITE_OK) { db_rollback(si, "insert"); return -1; } + if (!result) si->next_id++; + if (changed) db_committed(si, changed); + return result; +} /* ACK конкретного сообщения: повтор не увеличивает счётчик, очередь отправки не считается доставкой. */ static int si_delivery_update(struct DB_SYNC_INSTANCE* si, const uint8_t message_id[32], uint64_t peer_id) { @@ -927,1105 +849,301 @@ static int si_delivery_update(struct DB_SYNC_INSTANCE* si, const uint8_t message return 0; } -// ============================================================ -// Sync protocol handlers -// ============================================================ -// Определяет направление инициации синхронизации: узел с бОльшим node_id — мастер -static int is_master(uint64_t a, uint64_t b) { return a > b; } - -// Обрабатывает REQUEST_SYNC: запускает синхронизацию как слейв (мастер сам пришлёт INIT_SYNC) -static void db_handle_request_sync(struct DB_SYNC_INSTANCE* si, uint64_t src, const uint8_t* p, size_t len) -{ - (void)p; (void)len; - struct SI_PEER* sp = si_peer_add(si, src); - if (sp && sp->sync_state != 1 && is_master(si->db_sync->inst->node_id, src)) { - db_sync_initiate_sync(si, src); - } +static void db_sync_recv_cb(struct ETCP_CONN* conn, struct ll_entry* e) { + if (!e) return; + struct UTUN_INSTANCE* inst = conn ? conn->instance : NULL; + struct DB_SYNC_INSTANCE* si = e->dgram && e->len >= 10 ? db_sync_instance_find(inst, db_read64(e->dgram + 1)) : NULL; + if (!si || !db_message_peer(inst, si->ns, conn->peer_node_id)) goto done; + const uint8_t* data = e->dgram + 10; size_t len = e->len - 10; + si->holds++; + if (e->dgram[9] == DB_MSG_ACK_PUSH) { + if (len == 32) si_delivery_update(si, data, conn->peer_node_id); + else DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "history invalid ACK bytes=%zu", len); + } else if (e->dgram[9] == DB_MSG_PUSH) { + const uint8_t* p = data; struct db_record r; + if (db_parse_record(si, &p, data + len, &r) == 0 && p == data + len) { + int result = db_insert_one(si, &r, NULL); + if (!result) db_emit_push(si, &r, conn->peer_node_id); + if (result >= 0 && si->enabled) db_send(si, conn->peer_node_id, DB_MSG_ACK_PUSH, r.message_id, 32); + } + } else DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "history unknown message type=%u", e->dgram[9]); + db_release(si); +done: + queue_dgram_free(e); queue_entry_free(e); } -// Обрабатывает ERROR от пира: логирует причину, сбрасывает sync_state пира -static void db_handle_error(struct DB_SYNC_INSTANCE* si, uint64_t src, const uint8_t* p, size_t len) -{ - if (len < 1 || !si) return; - const char* what = (p[0] == DB_ERR_NOT_FOUND) ? "NOT_FOUND" : - (p[0] == DB_ERR_DISABLED) ? "DISABLED" : - (p[0] == DB_ERR_NOT_READY) ? "NOT_READY" : "UNKNOWN"; - DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, - "sync [%s:%04llX] ← ERROR: code=%u (%s) — peer rejected our sync", - SI_SHRT(si), (unsigned long long)(src >> 16), p[0], what); - struct SI_PEER* sp = si_peer_find(si, src); - if (sp) { - DEBUG_DEBUG(DEBUG_CATEGORY_CHAT_SYNC, "sync [%s:%04llX] ← ERROR: state=%u→0, synced_pos=%u", - SI_SHRT(si), (unsigned long long)(src >> 16), sp->sync_state, sp->synced_pos); - sp->sync_state = 0; - sp->sync_start_tb = 0; - db_sync_resume_peer_check(si->db_sync); - } +static uint64_t db_now_ms(struct DB_SYNC_INSTANCE* si) { + int64_t us; + if (si->db_sync->inst->ntp.synced) us = ntp_time_get_us(si->db_sync->inst); + else { struct timeval tv; utun_gettimeofday(&tv, NULL); us = (int64_t)tv.tv_sec * 1000000LL + tv.tv_usec; } + if (us < 0) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "history wall clock before epoch"); return 0; } + return (uint64_t)us / 1000; } - -// Обрабатывает INIT_SYNC: сбрасывает pending recalc, определяет точку расхождения (tp), формирует INIT_RESP -// с разреженными хешами (интервалы: 1,1,1,2,2,4,4,4,4,4,4,4,4,4,4,4) и флагом has_tail -static void db_handle_init_sync(struct DB_SYNC_INSTANCE* si, uint64_t src, const uint8_t* p, size_t len) -{ - if (len < 4) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "INIT_SYNC too short %zu from %016llx", len, (unsigned long long)src); return; } - if (db_sync_flush_recalc(si) < 0) { si_abort_sync(si, src, "chain rebuild failed"); return; } - uint32_t pc; memcpy(&pc, p, 4); - uint32_t mc; - if (db_count_checked(si, &mc) < 0) { si_abort_sync(si, src, "count failed"); return; } - uint32_t tp = (pc < mc ? pc : mc); - if (tp > 0) tp--; - uint64_t my_ch8_at_tp = 0; - if (mc > 0 && db_chain_hash8_at(si, tp, &my_ch8_at_tp) < 0) { si_abort_sync(si, src, "chain read failed"); return; } - uint8_t has_tail = (mc > tp + 1) ? 1 : 0; - static const uint16_t iv[16] = {1, 1, 1, 2, 2, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4}; - uint32_t accum = 0; int sparse_cnt = 0; - for (int i = 0; i < 16; i++) { if (tp < accum + iv[i]) break; accum += iv[i]; sparse_cnt++; } - DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "sync [%s:%04llX] ← INIT_SYNC: peer=%u my=%u → tp=%u ch8=%016llX has_tail=%u sparse=%d", - SI_SHRT(si), (unsigned long long)(src >> 16), pc, mc, tp, my_ch8_at_tp, has_tail, sparse_cnt); - - uint8_t resp[4096]; - uint32_t off = 0; - resp[off++] = DB_MSG_INIT_RESP; - memcpy(resp + off, &tp, 4); off += 4; - uint64_t my_ch8 = my_ch8_at_tp; - memcpy(resp + off, &my_ch8, 8); off += 8; - resp[off++] = has_tail; - uint32_t sp = off; - off++; - int scnt = 0; - accum = 0; - for (int i = 0; i < 16; i++) { - if (tp < accum + iv[i]) break; - accum += iv[i]; - uint32_t pos = tp - accum; - uint64_t sch8; - if (db_chain_hash8_at(si, pos, &sch8) != 0) { si_abort_sync(si, src, "sparse chain read failed"); return; } - if (off + 12 > sizeof(resp)) break; - memcpy(resp + off, &pos, 4); off += 4; - memcpy(resp + off, &sch8, 8); off += 8; - scnt++; - } - resp[sp] = (uint8_t)scnt; - int sret = db_sync_send(si, src, resp, off); - { - struct SI_PEER* sp = si_peer_add(si, src); - if (sp && sret == 0) { sp->sync_state = 1; sp->sync_start_tb = get_time_tb(); } - } - DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "sync [%s:%04llX] → INIT_RESP: sent %u bytes to peer=%016llx ret=%d flow=%u,%u", - SI_SHRT(si), (unsigned long long)(src >> 16), off, (unsigned long long)src, sret, - resp[0], resp[1]); -} - -// Обрабатывает INIT_RESP: сравнивает хеши на точке tp, находит первую расходящуюся позицию, -// отправляет наши данные начиная с неё и запрашивает данные пира с той же позиции -static void db_handle_init_resp(struct DB_SYNC_INSTANCE* si, uint64_t src, const uint8_t* p, size_t len) -{ - if (len < 14) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "INIT_RESP too short %zu from %016llx", len, (unsigned long long)src); return; } - if (db_sync_flush_recalc(si) < 0) { si_abort_sync(si, src, "chain rebuild failed"); return; } - uint32_t tp; memcpy(&tp, p, 4); - uint64_t peer_ch8; memcpy(&peer_ch8, p + 4, 8); - uint8_t has_tail = p[12]; - uint8_t sc = p[13]; - const uint8_t* spr = p + 14; - - uint64_t my_ch8 = 0; - uint32_t mc; - if (db_count_checked(si, &mc) < 0) { si_abort_sync(si, src, "count failed"); return; } - if (mc > 0 && tp < mc && db_chain_hash8_at(si, tp, &my_ch8) < 0) { si_abort_sync(si, src, "chain read failed"); return; } - DEBUG_DEBUG(DEBUG_CATEGORY_CHAT_SYNC, "sync [%s:%04llX] ← INIT_RESP: tp=%u my=%u my_ch8=%016llX peer_ch8=%016llX has_tail=%u sc=%d", - SI_SHRT(si), (unsigned long long)(src >> 16), tp, mc, my_ch8, peer_ch8, has_tail, sc); - - struct SI_PEER* sp = si_peer_find(si, src); - - // Peer empty → send ALL our data, then SYNC_DONE will be sent from DATA handler when slave responds. - // Если мы тоже пусты (mc==0) — обе стороны пусты: не отправляем ничего, а завершаем sync через - // SYNC_DONE в ветке "exact match" ниже (иначе обе стороны вечно ждут ответа → TIMEOUT). - if (peer_ch8 == 0 && sc == 0 && mc > 0) { - uint32_t vp = (uint32_t)-1; if (sp) sp->verified_pos = vp; - uint32_t sent = 0; - while (sent < mc) { - uint32_t b = mc - sent; if (b > DB_SEND_DATA_MAX) b = DB_SEND_DATA_MAX; - uint32_t want = (sent + b >= mc) ? mc : DB_WANT_FROM_NONE; - int batch_sent = si_send_data_batch(si, src, sent, b, vp, want); - if (batch_sent <= 0) break; - sent += (uint32_t)batch_sent; - } - DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "sync [%s:%04llX] ← INIT_RESP: peer empty → sent %u/%u records, awaiting response", - SI_SHRT(si), (unsigned long long)(src >> 16), sent, mc); - return; - } - - // Exact match: same hash at tp - if (my_ch8 == peer_ch8) { - if (!has_tail && mc <= tp + 1) { - if (sp) { sp->verified_pos = tp; sp->synced_pos = tp; sp->sync_state = 2; sp->sync_start_tb = 0; } - uint8_t sd[13]; sd[0] = DB_MSG_SYNC_DONE; - memcpy(sd + 1, &mc, 4); memcpy(sd + 5, &my_ch8, 8); - if (db_sync_send(si, src, sd, 13) != 0) return; - si_fire_sync_done(si, src); - DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "sync [%s:%04llX] ← INIT_RESP: MATCH+no_tail ss→2 → SYNC_DONE tp=%u", - SI_SHRT(si), (unsigned long long)(src >> 16), tp); - return; - } - // Has tail (slave or us): send our records after tp, request peer's after tp - uint32_t vp = tp; if (sp) sp->verified_pos = vp; - uint32_t rfrom = tp + 1; - uint32_t scnt = mc - rfrom; if (scnt > DB_SEND_DATA_MAX) scnt = DB_SEND_DATA_MAX; - if (scnt > 0) si_send_data_batch(si, src, rfrom, scnt, vp, rfrom); - else si_send_data_batch(si, src, rfrom, 0, vp, rfrom); - DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "sync [%s:%04llX] ← INIT_RESP: MATCH+tail → SEND_DATA from=%u want=%u", - SI_SHRT(si), (unsigned long long)(src >> 16), rfrom, rfrom); - return; - } - - // Mismatch: find first divergent position - uint32_t fm = tp; - for (int i = 0; i < sc && spr + 12 <= p + len; i++) { - uint32_t pos; memcpy(&pos, spr, 4); uint64_t pch8; memcpy(&pch8, spr + 4, 8); spr += 12; - uint64_t mch8; - if (db_chain_hash8_at(si, pos, &mch8) == 0) { - if (mch8 != pch8 && pos < fm) fm = pos; - } else if (pos < fm) fm = pos; - } - uint32_t vp = fm > 0 ? fm - 1 : (uint32_t)-1; if (sp) sp->verified_pos = vp; - DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "sync [%s:%04llX] ← INIT_RESP: MISMATCH fm=%u vp=%u mc=%u", - SI_SHRT(si), (unsigned long long)(src >> 16), fm, vp, mc); - - // Send our records from fm, request peer's from fm - uint32_t scnt = mc - fm; if (scnt > DB_SEND_DATA_MAX) scnt = DB_SEND_DATA_MAX; - if (scnt > 0) si_send_data_batch(si, src, fm, scnt, vp, fm); - else si_send_data_batch(si, src, fm, 0, vp, fm); - DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "sync [%s:%04llX] → SEND_DATA from=%u vp=%u cnt=%u want=%u", - SI_SHRT(si), (unsigned long long)(src >> 16), fm, vp, scnt, fm); -} - -// ---- SEND_DATA batch helper ---- -// Wire: [from:4][count:2][vp:4][want_from:4][records...] -// Record: [id:8][ts:8][node_id:8][dlen:4][data][sig_len:1=64][sig:64] -// Returns: number of records sent, or -1 on SQL error -// Формирует и отправляет батч записей (до 32) в формате SEND_DATA: -// [from:4][count:2][vp:4][want_from:4][records...]. Возвращает количество отправленных записей -static int si_send_data_batch(struct DB_SYNC_INSTANCE* si, uint64_t dst, uint32_t from, uint32_t count, uint32_t vp, uint32_t want_from) -{ - uint8_t sbuf[8192]; uint8_t* buf = sbuf; - - uint32_t off = 0; - buf[off++] = DB_MSG_SEND_DATA; - memcpy(buf + off, &from, 4); off += 4; - uint16_t rc = 0; - uint8_t* rcp = buf + off; off += 2; - memcpy(buf + off, &vp, 4); off += 4; - memcpy(buf + off, &want_from, 4); off += 4; // NEW: request peer's data from this position, DB_WANT_FROM_NONE=none - - if (count > 0) { - sqlite3_stmt* stmt = NULL; - sqlite3* db = SI_DB(si); - if (sqlite3_exec(db, "BEGIN IMMEDIATE", NULL, NULL, NULL) != SQLITE_OK) { - DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "history batch BEGIN tbl=%s: %s", SI_TBL(si), sqlite3_errmsg(db)); return -1; - } - sqlite3_int64 bad_rowid = 0; - int bad = 0, step_rc = SQLITE_DONE; - if (si_prep(si, &stmt, - "SELECT id,timestamp,node_id,data,author_signature,rowid" - " FROM \"%s\" ORDER BY timestamp, author_signature" - " LIMIT ? OFFSET ?") != SQLITE_OK) goto batch_error; - if (sqlite3_bind_int64(stmt, 1, (sqlite3_int64)count) != SQLITE_OK || - sqlite3_bind_int64(stmt, 2, (sqlite3_int64)from) != SQLITE_OK) goto batch_error; - while ((step_rc = sqlite3_step(stmt)) == SQLITE_ROW) { - uint64_t rid = (uint64_t)sqlite3_column_int64(stmt, 0); - uint64_t rts = (uint64_t)sqlite3_column_int64(stmt, 1); - uint64_t rauth = (uint64_t)sqlite3_column_int64(stmt, 2); - const uint8_t* rd = (const uint8_t*)sqlite3_column_blob(stmt, 3); - if (!rd && sqlite3_errcode(db) == SQLITE_NOMEM) goto batch_error; - uint32_t rdl = (uint32_t)sqlite3_column_bytes(stmt, 3); if (!rd) rdl = 0; - const uint8_t* rsig = (const uint8_t*)sqlite3_column_blob(stmt, 4); - if (!rsig && sqlite3_errcode(db) == SQLITE_NOMEM) goto batch_error; - int rsl = sqlite3_column_bytes(stmt, 4); if (!rsig) rsl = 0; - enum db_record_check check = db_verify_author_sig(si, rts, rauth, (const char*)rd, rdl, rsig, (size_t)rsl); - if (check == DB_CHECK_ERROR || check == DB_CHECK_DEFERRED) goto batch_error; - if (check != DB_CHECK_VALID) { - bad = 1; bad_rowid = sqlite3_column_int64(stmt, 5); break; - } - int rec_sz = 28 + rdl + 1 + rsl; - if (off + rec_sz > 8000) break; - memcpy(buf + off, &rid, 8); off += 8; - memcpy(buf + off, &rts, 8); off += 8; - memcpy(buf + off, &rauth, 8); off += 8; - memcpy(buf + off, &rdl, 4); off += 4; - if (rdl > 0) { memcpy(buf + off, rd, rdl); off += rdl; } - buf[off++] = (uint8_t)rsl; - if (rsl > 0) { memcpy(buf + off, rsig, rsl); off += rsl; } - rc++; - } - if (step_rc != SQLITE_ROW && step_rc != SQLITE_DONE) goto batch_error; - if (sqlite3_finalize(stmt) != SQLITE_OK) { stmt = NULL; goto batch_error; } - stmt = NULL; - if (bad && db_delete_bad_row(si, bad_rowid) < 0) goto batch_error; - if (sqlite3_exec(db, "COMMIT", NULL, NULL, NULL) != SQLITE_OK) goto batch_error; - if (bad) { - db_history_deleted(si); - return -1; /* Позиции изменились: не отправляем пачку старого раунда. */ - } - goto batch_ready; -batch_error: - DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "history batch aborted tbl=%s from=%u: %s; preserving data", - SI_TBL(si), from, sqlite3_errmsg(db)); - sqlite3_finalize(stmt); - if (sqlite3_exec(db, "ROLLBACK", NULL, NULL, NULL) != SQLITE_OK) - DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "history batch rollback tbl=%s: %s", SI_TBL(si), sqlite3_errmsg(db)); - struct SI_PEER* peer = si_peer_find(si, dst); - if (peer) { peer->sync_state = 0; peer->sync_start_tb = 0; } - db_sync_resume_peer_check(si->db_sync); - return -1; - } -batch_ready: - memcpy(rcp, &rc, 2); - if (db_sync_send(si, dst, buf, off) != 0) return -1; - DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "sync [%s:%04llX] → SEND_DATA: %u recs from=%u want=%u vp=%u", - SI_SHRT(si), (unsigned long long)(dst >> 16), rc, from, want_from, vp); - return (int)rc; -} - -// ---- Parse one record from SEND_DATA/PUSH wire format ---- -// Wire: [id:8][ts:8][author:8][dlen:4][data][sig_len:1][sig] -// Разбирает одну запись из бинарного формата: [id:8][ts:8][author:8][dlen:4][data][sig_len:1][sig] -// Продвигает указатель *pp, возвращает 0 при успехе, -1 при ошибке парсинга -static int si_parse_record(const uint8_t** pp, const uint8_t* end, - uint64_t* rid, uint64_t* rts, uint64_t* rauthor, - uint32_t* rdlen, const uint8_t** rdata, - const uint8_t** rsig, int* rsiglen) -{ - if (end - *pp < 28) { DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "si_parse_record: truncated header, need 28 have %td", end - *pp); return -1; } - memcpy(rid, *pp, 8); *pp += 8; - memcpy(rts, *pp, 8); *pp += 8; - memcpy(rauthor, *pp, 8); *pp += 8; - memcpy(rdlen, *pp, 4); *pp += 4; - - if (!*rdlen || *rdlen > DB_SYNC_MESSAGE_MAX || *rdlen > (size_t)(end - *pp)) { - DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "si_parse_record: invalid body dlen=%u have=%td", *rdlen, end - *pp); return -1; - } - *rdata = (*rdlen > 0) ? *pp : NULL; - *pp += *rdlen; - - if (*pp == end) { DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "si_parse_record: sig_len overrun"); return -1; } - *rsiglen = (int)(*(*pp)++); - if (*rsiglen != DB_SIG_SIZE || *rsiglen > end - *pp) { - DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "si_parse_record: invalid signature slen=%d have=%td", *rsiglen, end - *pp); return -1; - } - *rsig = *pp; - *pp += *rsiglen; - return 0; -} - -// ---- Atomic SEND_DATA exchange ---- -// Обрабатывает SEND_DATA: вставляет полученные записи (до 32), проверяет подписи, -// корректирует synced_pos, при want_from шлёт встречные данные -static void db_handle_send_data(struct DB_SYNC_INSTANCE* si, uint64_t src, const uint8_t* p, size_t len) -{ - if (len < 14) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "sync [%s:%04llX] ← SEND_DATA: truncated len=%zu (need >=14)", SI_SHRT(si), (unsigned long long)(src >> 16), len); return; } - if (db_sync_flush_recalc(si) < 0) { si_abort_sync(si, src, "chain rebuild failed"); return; } - uint32_t from; memcpy(&from, p, 4); - uint16_t count; memcpy(&count, p + 4, 2); - uint32_t vp; memcpy(&vp, p + 6, 4); - uint32_t want_from; memcpy(&want_from, p + 10, 4); - - if (count > DB_SEND_DATA_MAX) { si_abort_sync(si, src, "invalid batch count"); return; } - - struct SI_PEER* sp = si_peer_find(si, src); - - const uint8_t* ptr = p + 14; - uint16_t received = 0, duplicates = 0; - for (uint16_t i = 0; i < count && i < 32; i++) { - uint64_t rid, rts, rauthor; uint32_t rdlen; const uint8_t* rdata, *rsig; int rsiglen; - if (si_parse_record(&ptr, p + len, &rid, &rts, &rauthor, &rdlen, &rdata, &rsig, &rsiglen) != 0) { - si_abort_sync(si, src, "malformed incoming record"); return; - } - int ret = db_record_insert_cascade(si, rid, rts, rauthor, (const char*)rdata, rdlen, rsig, src, NULL, 1); - if (ret < 0) { si_abort_sync(si, src, "incoming verification or insertion failed"); return; } - if (ret >= 0) received++; - if (ret == 1) duplicates++; - } - if (ptr != p + len) { si_abort_sync(si, src, "trailing batch bytes"); return; } - if (db_sync_flush_recalc(si) < 0) { si_abort_sync(si, src, "chain rebuild after insertion failed"); return; } - - uint32_t old_spos = sp ? sp->synced_pos : 0; - if (received > 0) { - uint32_t rst = (from < old_spos) ? from : old_spos; - if (sp) sp->synced_pos = from + received - 1; - for (int j = 0; j < si->peer_count; j++) - if (si->peers[j].synced_pos >= rst && &si->peers[j] != sp) - si->peers[j].synced_pos = rst > 0 ? rst - 1 : 0; - } - - char extra[32] = ""; if (duplicates) snprintf(extra, sizeof(extra), " (%u dup)", duplicates); - DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "sync [%s:%04llX] ← DATA: from=%u cnt=%u vp=%u want=%u → ins=%u%s mc=%u sp=%u→%u", - SI_SHRT(si), (unsigned long long)(src >> 16), from, count, vp, want_from, received, extra, db_count(si), - old_spos, sp ? sp->synced_pos : 0); - - // Respond with our data if peer requested (want_from != DB_WANT_FROM_NONE) - if (want_from != DB_WANT_FROM_NONE) { - uint32_t mc; - if (db_count_checked(si, &mc) < 0) { si_abort_sync(si, src, "count failed"); return; } - if (want_from < mc) { - uint32_t scnt = mc - want_from; if (scnt > DB_SEND_DATA_MAX) scnt = DB_SEND_DATA_MAX; - if (si_send_data_batch(si, src, want_from, scnt, vp, DB_WANT_FROM_NONE) < 0) return; - } else { - if (si_send_data_batch(si, src, want_from, 0, vp, DB_WANT_FROM_NONE) < 0) return; - } - DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "sync [%s:%04llX] ← DATA: responded with %u recs from=%u", - SI_SHRT(si), (unsigned long long)(src >> 16), want_from < mc ? (mc - want_from > DB_SEND_DATA_MAX ? DB_SEND_DATA_MAX : mc - want_from) : 0, want_from); - } - - // If slave responded with want=DB_WANT_FROM_NONE and we are in active sync — complete the exchange with SYNC_DONE - if (want_from == DB_WANT_FROM_NONE && sp && sp->sync_state == 1) { - uint32_t mc; - if (db_count_checked(si, &mc) < 0) { si_abort_sync(si, src, "count failed"); return; } - uint64_t mch8 = 0; - if (mc > 0 && db_chain_hash8_at(si, mc - 1, &mch8) < 0) { si_abort_sync(si, src, "chain read failed"); return; } - uint8_t sd[13]; sd[0] = DB_MSG_SYNC_DONE; - memcpy(sd + 1, &mc, 4); memcpy(sd + 5, &mch8, 8); - if (db_sync_send(si, src, sd, 13) != 0) return; - sp->sync_state = 2; sp->sync_start_tb = 0; - si_fire_sync_done(si, src); - DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "sync [%s:%04llX] ← DATA: exchange complete, → SYNC_DONE mc=%u", - SI_SHRT(si), (unsigned long long)(src >> 16), mc); - } - -} - -// Обрабатывает SYNC_DONE: финальная сверка хешей. mc==pc → sync завершён, mcpc → отправляем хвост -static void db_handle_sync_done(struct DB_SYNC_INSTANCE* si, uint64_t src, const uint8_t* p, size_t len) -{ - if (len < 12) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "SYNC_DONE too short %zu from %016llx", len, (unsigned long long)src); return; } - if (db_sync_flush_recalc(si) < 0) { si_abort_sync(si, src, "chain rebuild failed"); return; } - uint32_t pc; memcpy(&pc, p, 4); - uint64_t pch8; memcpy(&pch8, p + 4, 8); - uint32_t mc; - if (db_count_checked(si, &mc) < 0) { si_abort_sync(si, src, "count failed"); return; } - uint64_t mch8 = 0; - if (mc > 0 && db_chain_hash8_at(si, mc - 1, &mch8) < 0) { si_abort_sync(si, src, "chain read failed"); return; } - struct SI_PEER* sp = si_peer_find(si, src); - - if (mc == pc && mch8 == pch8) { - if (sp) { sp->synced_pos = mc > 0 ? mc - 1 : 0; sp->sync_state = 2; sp->sync_start_tb = 0; } - si_fire_sync_done(si, src); - DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "sync [%s:%04llX] ← SYNC_DONE: matched ✓ mc=%u ss→2", - SI_SHRT(si), (unsigned long long)(src >> 16), mc); - } else if (mc < pc) { - // Request tail - uint32_t vp_send = mc > 0 ? mc - 1 : (uint32_t)-1; - uint8_t req[15]; req[0] = DB_MSG_SEND_DATA; - memcpy(req + 1, &mc, 4); uint16_t z = 0; memcpy(req + 5, &z, 2); - memcpy(req + 7, &vp_send, 4); memcpy(req + 11, &mc, 4); - if (db_sync_send(si, src, req, 15) != 0) return; - if (sp) { sp->sync_state = 1; sp->sync_start_tb = get_time_tb(); } - DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "sync [%s:%04llX] ← SYNC_DONE: mc=%u < pc=%u → request tail from=%u", - SI_SHRT(si), (unsigned long long)(src >> 16), mc, pc, mc); - } else if (mc > pc) { - // mc > pc: send tail - if (sp) sp->last_peer_count = pc; - uint32_t scnt = mc - pc; if (scnt > DB_SEND_DATA_MAX) scnt = DB_SEND_DATA_MAX; - if (si_send_data_batch(si, src, pc, scnt, pc > 0 ? pc - 1 : (uint32_t)-1, DB_WANT_FROM_NONE) < 0) return; - if (sp) { sp->sync_state = 1; sp->sync_start_tb = get_time_tb(); } - DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "sync [%s:%04llX] ← SYNC_DONE: mc=%u > pc=%u → send tail from=%u", - SI_SHRT(si), (unsigned long long)(src >> 16), mc, pc, pc); - } else { - // mc == pc but chain hash differs — accept as converged - if (sp) { sp->synced_pos = mc > 0 ? mc - 1 : 0; sp->sync_state = 2; sp->sync_start_tb = 0; } - si_fire_sync_done(si, src); - DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "sync [%s:%04llX] ← SYNC_DONE: mc=%u == pc=%u (hash !match) → accept as converged", - SI_SHRT(si), (unsigned long long)(src >> 16), mc, pc); - } -} - -// Обрабатывает ACK_PUSH: помечает запись флагом DB_REC_FLAG_WAS_SENT и обновляет delivery_chain -static void db_handle_ack_push(struct DB_SYNC_INSTANCE* si, uint64_t src, const uint8_t* p, size_t len) -{ - if (len != 32) { - DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "history ACK invalid tbl=%s bytes=%zu expected=32", SI_TBL(si), len); return; - } - si_delivery_update(si, p, src); -} - -// Обрабатывает PUSH: вставляет новую запись от пира, шлёт ACK_PUSH, корректирует synced_pos остальных пиров -// если запись вставлена не в конец (в середину) -static void db_handle_push(struct DB_SYNC_INSTANCE* si, uint64_t src, const uint8_t* p, size_t len) -{ - if (len < 30) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "sync [%s:%04llX] ← PUSH: truncated len=%zu (need >=30)", SI_SHRT(si), (unsigned long long)(src >> 16), len); return; } - const uint8_t* ptr = p; - uint64_t rid, rts, rauthor; - uint32_t rdlen; - const uint8_t* rdata, *rsig; - int rsiglen; - if (si_parse_record(&ptr, p + len, &rid, &rts, &rauthor, &rdlen, &rdata, &rsig, &rsiglen) != 0 || ptr != p + len) { - si_abort_sync(si, src, "malformed PUSH"); return; - } - DEBUG_TRACE(DEBUG_CATEGORY_CHAT_SYNC, "from=%016llx id=%llu author=%016llx ts=%llu len=%u", (unsigned long long)src, (unsigned long long)rid, (unsigned long long)rauthor, (unsigned long long)rts, rdlen); - - int ret = db_record_insert_cascade(si, rid, rts, rauthor, (const char*)rdata, rdlen, rsig, src, NULL, 0); - if (ret < 0) { si_abort_sync(si, src, "PUSH verification or insertion failed"); return; } - uint8_t ack[33]; ack[0] = DB_MSG_ACK_PUSH; - if (chat_message_id(si->group_id, rts, rsig, ack + 1) < 0) return; - if (db_sync_send(si, src, ack, sizeof(ack)) != 0) return; - if (ret == 0) { - uint32_t ins_pos = si_find_pos(si, rts, rsig); - uint32_t total = db_count(si); - int adj_count = 0; - for (int j = 0; j < si->peer_count; j++) { - if (si->peers[j].synced_pos >= ins_pos) { si->peers[j].synced_pos = ins_pos > 0 ? ins_pos - 1 : 0; adj_count++; } - } - DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "sync [%s:%04llX] ← PUSH: msg id=%llu author=%016llX ts=%llu → inserted at pos=%u (of %u total)", - SI_SHRT(si), (unsigned long long)(src >> 16), - (unsigned long long)rid, (unsigned long long)rauthor, (unsigned long long)rts, ins_pos, total); - if (adj_count > 0 && ins_pos < total - 1) { - DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "sync [%s:----] PUSH: inserted at pos=%u NOT at tail → reset synced_pos of %d peers from ≥%u back to %u", - SI_SHRT(si), ins_pos, adj_count, ins_pos, ins_pos > 0 ? ins_pos - 1 : 0); - } - } -} - -// ============================================================ -// Receive callback -// ============================================================ - -// Главный колбэк приёма ETCP: извлекает хеш инстанса и тип сообщения, находит/создаёт инстанс, -// маршрутизирует в соответствующий обработчик (INIT_SYNC, INIT_RESP, SEND_DATA, PUSH, ACK_PUSH, SYNC_DONE, ERROR, REQUEST_SYNC) -static void db_sync_recv_cb(struct ETCP_CONN* conn, struct ll_entry* entry) -{ - if (!entry || entry->len < 10) { if (entry) { queue_dgram_free(entry); queue_entry_free(entry); } return; } - struct DB_SYNC* db = conn ? conn->instance->db_sync : NULL; - if (!db || !db->enabled) { queue_dgram_free(entry); queue_entry_free(entry); return; } - - uint64_t src = conn ? conn->peer_node_id : 0; - uint64_t group_id; memcpy(&group_id, entry->dgram + 1, 8); group_id = be64toh(group_id); - uint8_t type = entry->dgram[9]; - const uint8_t* payload = entry->dgram + 10; - size_t plen = entry->len - 10; - DEBUG_TRACE(DEBUG_CATEGORY_CHAT_SYNC, "recv type=%02x from=%016llx group=%016llx len=%zu", type, (unsigned long long)src, (unsigned long long)group_id, plen); - - struct DB_SYNC_INSTANCE* si = db_instance_find(db, group_id); - if (!si) { - DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "history table not registered group=%016llx peer=%016llx type=%02x", - (unsigned long long)group_id, (unsigned long long)src, type); - if (type != DB_MSG_ERROR) { - uint8_t err[2]; err[0] = DB_MSG_ERROR; err[1] = DB_ERR_NOT_FOUND; - db_sync_send_gid(db, src, group_id, err, 2); - } - queue_dgram_free(entry); queue_entry_free(entry); return; - } - if (!si->enabled) { - uint8_t err[2]; err[0] = DB_MSG_ERROR; err[1] = DB_ERR_DISABLED; - db_sync_send_gid(db, src, group_id, err, 2); - queue_dgram_free(entry); queue_entry_free(entry); return; - } - - /* валидация клиента: pubkey из ETCP-соединения должен быть в таблице мемберов канала */ - { - char ch_id[32]; snprintf(ch_id, sizeof(ch_id), "%llu", (unsigned long long)group_id); - if (conn && !member_sync_validate_pubkey(conn->instance, ch_id, conn->crypto_ctx.peer_public_key)) { - DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "sync: recv DROP — peer=0x%016llx not member of ch=%s (type=%02x)", - (unsigned long long)src, ch_id, type); - queue_dgram_free(entry); queue_entry_free(entry); return; - } - } - - struct TOPO_GROUP* group = topo_groups_find(db->inst->topo_groups, group_id); - if (type != DB_MSG_ERROR && (si->sync_gated || !topo_group_peer_ready(group, src))) { - DEBUG_DEBUG(DEBUG_CATEGORY_CHAT_SYNC, "history receive deferred group=%016llx peer=%016llx type=%02x members_ready=%d peer_ready=%d", - group_id, src, type, !si->sync_gated, topo_group_peer_ready(group, src)); - uint8_t err[2] = { DB_MSG_ERROR, DB_ERR_NOT_READY }; - db_sync_send_gid(db, src, group_id, err, sizeof(err)); - queue_dgram_free(entry); queue_entry_free(entry); return; - } - struct SI_PEER* progress = si_peer_find(si, src); - if (progress && progress->sync_state == 1 && - ((type == DB_MSG_INIT_RESP && plen >= 14) || (type == DB_MSG_SEND_DATA && plen >= 14))) - progress->sync_start_tb = get_time_tb(); - - switch (type) { - case DB_MSG_INIT_SYNC: DEBUG_TRACE(DEBUG_CATEGORY_CHAT_SYNC, "→ handle INIT_SYNC"); db_handle_init_sync(si, src, payload, plen); break; - case DB_MSG_INIT_RESP: DEBUG_TRACE(DEBUG_CATEGORY_CHAT_SYNC, "→ handle INIT_RESP"); db_handle_init_resp(si, src, payload, plen); break; - case DB_MSG_SEND_DATA: DEBUG_TRACE(DEBUG_CATEGORY_CHAT_SYNC, "→ handle SEND_DATA"); db_handle_send_data(si, src, payload, plen); break; - case DB_MSG_PUSH: DEBUG_TRACE(DEBUG_CATEGORY_CHAT_SYNC, "→ handle PUSH"); db_handle_push(si, src, payload, plen); break; - case DB_MSG_ACK_PUSH: DEBUG_TRACE(DEBUG_CATEGORY_CHAT_SYNC, "→ handle ACK_PUSH"); db_handle_ack_push(si, src, payload, plen); break; - case DB_MSG_REQUEST_SYNC: DEBUG_TRACE(DEBUG_CATEGORY_CHAT_SYNC, "→ handle REQUEST_SYNC"); db_handle_request_sync(si, src, payload, plen); break; - case DB_MSG_SYNC_DONE: DEBUG_TRACE(DEBUG_CATEGORY_CHAT_SYNC, "→ handle SYNC_DONE"); db_handle_sync_done(si, src, payload, plen); break; - case DB_MSG_ERROR: DEBUG_TRACE(DEBUG_CATEGORY_CHAT_SYNC, "→ handle ERROR"); db_handle_error(si, src, payload, plen); break; - default: - DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "sync: unknown msg type 0x%02x from %04llX (group=%016llx) — dropped", - type, (unsigned long long)(src >> 16), group_id); - break; - } - queue_dgram_free(entry); - queue_entry_free(entry); -} - -// ============================================================ -// Connection callbacks -// ============================================================ - -// История наблюдает сессию своей группы и не владеет дополнительным транспортом. -static void db_sync_on_peer_ready(struct TOPO_GROUP* group, uint64_t pid, int ready, void* arg) { - struct DB_SYNC_INSTANCE* si = arg; +static void db_ttl_tick(void* arg) { + struct DB_SYNC_INSTANCE* si = arg; si->ttl_timer = NULL; if (!si->enabled) return; - struct SI_PEER* p = ready ? si_peer_add(si, pid) : si_peer_find(si, pid); - if (!p) return; - if (!ready) { - DEBUG_DEBUG(DEBUG_CATEGORY_CHAT_SYNC, "history peer unavailable group=%016llx peer=%016llx state=%u", - group->group_id, pid, p->sync_state); - p->sync_state = 0; p->sync_start_tb = 0; - } else if (p->sync_state == 0) db_sync_initiate_sync(si, pid); + sqlite3* db = SI_DB(si); sqlite3_stmt* st = NULL; + uint64_t now = db_now_ms(si); + uint64_t ttl = (uint64_t)si->db_sync->inst->config->global.db_sync_ttl * 1000; + uint64_t cut = now > ttl ? now - ttl : 0; + if (sqlite3_exec(db, "BEGIN IMMEDIATE", NULL, NULL, NULL) != SQLITE_OK) { db_error(si, "TTL begin"); goto schedule; } + char query[512]; + snprintf(query, sizeof(query), "DELETE FROM \"%s\" WHERE rowid IN (SELECT rowid FROM \"%s\"" + " WHERE node_id=? AND (flags & 1)=0 AND timestampdb_sync->inst->node_id); + if (rc == SQLITE_OK) rc = sqlite3_bind_int64(st, 2, (sqlite3_int64)cut); + if (rc == SQLITE_OK) rc = sqlite3_step(st); + int removed = sqlite3_changes(db), end_rc = sqlite3_finalize(st); st = NULL; + if (rc != SQLITE_DONE || end_rc != SQLITE_OK) { db_rollback(si, "TTL delete"); goto schedule; } + if (removed && merkle_sync_registered(si->db_sync->inst, MT_DATA_MESSAGES) && db_index_clear(si) < 0) { + db_rollback(si, "TTL index"); goto schedule; + } + if (sqlite3_exec(db, "COMMIT", NULL, NULL, NULL) != SQLITE_OK) { db_rollback(si, "TTL commit"); goto schedule; } + if (removed) { + db_index_request(si); + DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "history TTL committed tbl=%s removed=%d cutoff_ms=%llu", + SI_TBL(si), removed, (unsigned long long)cut); + } +schedule: + si->ttl_timer = uasync_set_timeout(si->db_sync->inst->ua, DB_SYNC_TTL_INTERVAL * 10000ULL, si, db_ttl_tick, "history_ttl"); + if (!si->ttl_timer) { db_error(si, "TTL timer"); db_sync_resume_peer_check(si->db_sync); } } -// ============================================================ -// Initiate sync -// ============================================================ - -// Запускает синхронизацию: мастер шлёт INIT_SYNC со своим количеством записей, слейв шлёт REQUEST_SYNC -static int db_sync_initiate_sync(struct DB_SYNC_INSTANCE* si, uint64_t pid) -{ - struct DB_SYNC* db = si->db_sync; - struct TOPO_GROUP* group = topo_groups_find(db->inst->topo_groups, si->group_id); - struct ETCP_CONN* conn = instance_find_conn(db->inst, pid); - uint8_t ek[32]; - char ch_id[32]; snprintf(ch_id, sizeof(ch_id), "%llu", (unsigned long long)si->group_id); - int ready = topo_group_peer_ready(group, pid); - if (si->sync_gated || !ready) { - DEBUG_DEBUG(DEBUG_CATEGORY_CHAT_SYNC, "history waiting group=%016llx peer=%016llx members_ready=%d peer_ready=%d", - (unsigned long long)si->group_id, (unsigned long long)pid, !si->sync_gated, ready); - return 0; - } - if (!conn || !member_sync_validate_pubkey(db->inst, ch_id, conn->crypto_ctx.peer_public_key) || - db_get_ed25519_pubkey(db, pid, ek) != 0) { - DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "history READY peer lacks transport, membership or key group=%016llx peer=%016llx", - (unsigned long long)si->group_id, (unsigned long long)pid); - return 0; - } - uint32_t mc; - if (db_sync_flush_recalc(si) < 0 || db_count_checked(si, &mc) < 0) { si_abort_sync(si, pid, "local history unavailable"); return 0; } - uint64_t my_id = si->db_sync->inst->node_id; - struct SI_PEER* p = si_peer_find(si, pid); - if (!p) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "history start without peer group=%016llx", si->group_id); return 0; } - if (db_sync_peer_throttled(db, p)) { - DEBUG_DEBUG(DEBUG_CATEGORY_CHAT_SYNC, "history sleeping peer deferred group=%016llx peer=%016llx", si->group_id, pid); - db_sync_resume_peer_check(db); return 0; - } - p->sync_start_tb = get_time_tb(); p->sync_state = 1; - int ret; - DEBUG_TRACE(DEBUG_CATEGORY_CHAT_SYNC, "pid=%016llx my=%u tbl=%s", (unsigned long long)pid, mc, SI_TBL(si)); - - if (is_master(my_id, pid)) { - uint8_t msg[5]; - msg[0] = DB_MSG_INIT_SYNC; - memcpy(msg + 1, &mc, 4); - ret = db_sync_send(si, pid, msg, 5); - } else { - uint8_t msg[1] = { DB_MSG_REQUEST_SYNC }; - ret = db_sync_send(si, pid, msg, 1); - } - DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "history start group=%016llx peer=%016llx role=%s count=%u timeout_s=%d ret=%d", - si->group_id, pid, is_master(my_id, pid) ? "master" : "requester", mc, DB_SYNC_SYNC_TIMEOUT, ret); - db_sync_resume_peer_check(si->db_sync); - return ret == 0; -} - -// Проверяет целостность цепочки хешей всей таблицы, при несовпадении запускает пересчёт с позиции ошибки -static void db_verify_chain(struct DB_SYNC_INSTANCE* si) -{ - int rc = db_sync_chain_verify(si); - if (rc == 1) { - DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "history chain mismatch tbl=%s; rebuilding", SI_TBL(si)); - db_sync_schedule_recalc(si, 0); - } -} - -// Публичная проверка целостности цепных хешей: возвращает 0 если всё верно, 1 при несовпадении, -1 при ошибке -int db_sync_chain_verify(struct DB_SYNC_INSTANCE* si) -{ - if (!si || !si->enabled) return 0; - if (db_sync_flush_recalc(si) < 0) return -1; - - uint8_t prev_ch[32]; memset(prev_ch, 0, 32); - uint8_t exp_ch[32], stored_ch[32]; - - sqlite3_stmt* stmt; - if (si_prep(si, &stmt, - "SELECT id,timestamp,node_id,author_signature,chain_hash FROM \"%s\"" - " ORDER BY timestamp, author_signature") != SQLITE_OK) { - DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "verify_chain_hash: si_prep failed tbl=%s", SI_TBL(si)); - return -1; - } - int rc; - for (uint32_t pos = 0; (rc = sqlite3_step(stmt)) == SQLITE_ROW; pos++) { - uint64_t rid = (uint64_t)sqlite3_column_int64(stmt, 0); - uint64_t rts = (uint64_t)sqlite3_column_int64(stmt, 1); - uint64_t rauth = (uint64_t)sqlite3_column_int64(stmt, 2); - const void* sig_blob = sqlite3_column_blob(stmt, 3); - const void* b = sqlite3_column_blob(stmt, 4); - if (!sig_blob || sqlite3_column_bytes(stmt, 3) != DB_SIG_SIZE || !b || sqlite3_column_bytes(stmt, 4) != 32) { - DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "history chain malformed fields tbl=%s pos=%u", SI_TBL(si), pos); - sqlite3_finalize(stmt); return 1; - } - memcpy(stored_ch, b, 32); - if (db_chain_hash_compute(prev_ch, rid, rts, rauth, sig_blob, exp_ch) < 0) { sqlite3_finalize(stmt); return -1; } - if (memcmp(exp_ch, stored_ch, 32) != 0) { - DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "history chain mismatch tbl=%s pos=%u", SI_TBL(si), pos); - sqlite3_finalize(stmt); return 1; - } - memcpy(prev_ch, exp_ch, 32); - } - int final_rc = sqlite3_finalize(stmt); - if (rc != SQLITE_DONE || final_rc != SQLITE_OK) { - DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "history chain verify tbl=%s rc=%d finalize=%d: %s", - SI_TBL(si), rc, final_rc, sqlite3_errmsg(SI_DB(si))); return -1; - } - return 0; -} - -// Устанавливает состояние синхронизации пира (для тестов) -void db_sync_peer_set_state(struct DB_SYNC_INSTANCE* si, uint64_t node_id, uint8_t state) -{ - struct SI_PEER* p = si_peer_find(si, node_id); - if (p) p->sync_state = state; -} - -// Задать per-(chat,peer) флаг «play sound on message» (троттлинг: sound_off + спящий пир → пропуск). -void db_sync_set_peer_sound(struct UTUN_INSTANCE* inst, uint64_t group_id, uint64_t peer_node_id, int sound_on) -{ - if (!inst || !inst->db_sync) return; - struct DB_SYNC* db = inst->db_sync; - struct DB_SYNC_INSTANCE* si = db_instance_find(db, group_id); - if (!si) return; /* чат ещё не создан у нас — настройка не критична */ - struct SI_PEER* p = si_peer_add(si, peer_node_id); - if (p && p->sound_on != (uint8_t)(sound_on ? 1 : 0)) { - p->sound_on = sound_on ? 1 : 0; - DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "sync [%s] peer=%016llx sound_on=%d (throttle=%s)", - SI_SHRT(si), (unsigned long long)peer_node_id, p->sound_on, - p->sound_on ? "off" : "on"); - } -} - -// После member_sync запускаем обмен только через READY-сессии этой группы. -void db_sync_instance_set_gated(struct DB_SYNC_INSTANCE* si, int gated) -{ - if (!si || !si->enabled) return; - gated = gated ? 1 : 0; - if (si->sync_gated == (uint8_t)gated) return; - si->sync_gated = (uint8_t)gated; - if (!gated) { - struct DB_SYNC* db = si->db_sync; - struct TOPO_GROUP* group = topo_groups_find(db->inst->topo_groups, si->group_id); - int launched = 0; - for (struct ll_entry* e = group && group->senders_list ? group->senders_list->head : NULL; e; e = e->next) { - struct TOPO_GROUP_CONN_ITEM* item = (struct TOPO_GROUP_CONN_ITEM*)e->data; - if (!topo_group_peer_ready(group, item->node_id)) continue; - struct SI_PEER* p = si_peer_add(si, item->node_id); - if (p && p->sync_state == 0) launched += db_sync_initiate_sync(si, p->node_id); - } - if (launched == 0) db_sync_resume_peer_check(db); - DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "sync [%s] → un-gated: launched sync for %d peers (mc=%u)", - SI_SHRT(si), launched, db_count(si)); - } else { - for (int i = 0; i < si->peer_count; i++) { si->peers[i].sync_state = 0; si->peers[i].sync_start_tb = 0; } - DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "sync [%s] → gated: auto-sync suspended (mc=%u)", - SI_SHRT(si), db_count(si)); - } -} - -// Принудительно перезапускает синхронизацию с указанным пиром (для тестов) -void db_sync_reinitiate(struct DB_SYNC_INSTANCE* si, uint64_t node_id) -{ - struct SI_PEER* p = si_peer_add(si, node_id); - if (p) { - db_sync_initiate_sync(si, node_id); - } -} - -// Возвращает последний chain_hash8 (для сверки синхронизации в тестах) -int db_sync_last_chain_hash8(struct DB_SYNC_INSTANCE* si, uint64_t* out_hash8) -{ - if (!si || !out_hash8) return -1; - if (db_sync_flush_recalc(si) < 0) return -1; - uint32_t mc; - if (db_count_checked(si, &mc) < 0) return -1; - if (mc == 0) { *out_hash8 = 0; return 0; } - return db_chain_hash8_at(si, mc - 1, out_hash8); -} - -// ============================================================ -// Timers -// ============================================================ - -// Есть ли у любого активного инстанса пир, требующий синхронизации (sync_state != 2) -static int db_sync_has_pending_work(struct DB_SYNC* db) -{ - if (!db || !db->enabled) return 0; - for (int i = 0; i < db->instance_count; i++) { - struct DB_SYNC_INSTANCE* si = db->instances[i]; - if (!si->enabled || si->sync_gated) continue; - struct TOPO_GROUP* group = topo_groups_find(db->inst->topo_groups, si->group_id); - for (int j = 0; j < si->peer_count; j++) - if (si->peers[j].sync_state != 2 && topo_group_peer_ready(group, si->peers[j].node_id)) return 1; - } - return 0; -} - -// Взвести work-driven таймер проверки пиров, если он ещё не взведён и модуль активен -static void db_sync_resume_peer_check(struct DB_SYNC* db) -{ +static void db_sync_resume_peer_check(struct DB_SYNC* db) { if (!db || !db->enabled || db->peer_check_timer) return; - db->peer_check_timer = uasync_set_timeout(db->inst->ua, DB_SYNC_PEER_CHECK_INTERVAL * 10000u, - db, db_sync_peer_check_cb, "db_sync_peer"); - if (!db->peer_check_timer) DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "history retry timer allocation failed"); + db->peer_check_timer = uasync_set_timeout(db->inst->ua, DB_SYNC_PEER_CHECK_INTERVAL * 10000ULL, db, db_sync_peer_check_cb, "history_retry"); + if (!db->peer_check_timer) DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "history retry timer allocation"); } - -// Периодическая проверка: запускает синхронизацию для пиров в состоянии sync_state==0, -// детектирует таймауты синхронизации (DB_SYNC_SYNC_TIMEOUT секунд), логирует зависшие. -// Work-driven: перевзводится только пока есть пиры с sync_state != 2. - -/* Троттлинг: пир в SLEEP-фазе И чат для него «тихий» (sound_on=0) — пропускаем. */ -static int db_sync_peer_throttled(struct DB_SYNC* db, struct SI_PEER* p) { - if (!db || !p) return 0; - if (p->sound_on) return 0; /* чат пиру важен (play sound) — не троттлим */ - struct ETCP_CONN* conn = instance_find_conn(db->inst, p->node_id); - return conn && conn->peer_sleep_phase; -} - -static void db_sync_peer_check_cb(void* arg) -{ - struct DB_SYNC* db = (struct DB_SYNC*)arg; - if (!db || !db->enabled) return; - db->peer_check_timer = NULL; -#ifdef UTUN_HAVE_STANDBY - if (standby_get_sleep_tb() > 0) { - db->peer_check_wait = standby_wait(db, db_sync_peer_check_cb); - return; - } - db->peer_check_wait = NULL; -#endif - DEBUG_TRACE(DEBUG_CATEGORY_CHAT_SYNC, "instances=%d", db->instance_count); - - uint64_t now = get_time_tb(); +static void db_sync_peer_check_cb(void* arg) { + struct DB_SYNC* db = arg; db->peer_check_timer = NULL; int pending = 0; + if (!db->enabled) return; for (int i = 0; i < db->instance_count; i++) { struct DB_SYNC_INSTANCE* si = db->instances[i]; - if (!si->enabled || si->sync_gated) continue; - struct TOPO_GROUP* group = topo_groups_find(db->inst->topo_groups, si->group_id); + if (si->index.pending) { + pending = 1; + if (!si->index.timer) db_index_request(si); + } for (int j = 0; j < si->peer_count; j++) { struct SI_PEER* p = &si->peers[j]; - if (!topo_group_peer_ready(group, p->node_id)) continue; - if (p->sync_state == 1 && now - p->sync_start_tb >= DB_SYNC_SYNC_TIMEOUT * 10000ULL) { - DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "history timeout group=%016llx peer=%016llx idle_ms=%llu pos=%u retry=%u", - si->group_id, p->node_id, (unsigned long long)((now - p->sync_start_tb) / 10), - p->synced_pos, ++p->retry_count); - p->sync_state = 0; p->sync_start_tb = 0; + if (p->sync_state != 2 && db_message_can_run(db->inst, si->ns, p->node_id)) { + pending = 1; + if (!p->sync_state) db_start_peer(si, p->node_id); } - if (p->sync_state == 0) db_sync_initiate_sync(si, p->node_id); } } - - if (db_sync_has_pending_work(db)) - db_sync_resume_peer_check(db); - else - DEBUG_DEBUG(DEBUG_CATEGORY_CHAT_SYNC, "history peer_check stopped: no pending READY peers"); + if (pending) db_sync_resume_peer_check(db); +} +static void db_sync_on_peer_ready(struct TOPO_GROUP* group, uint64_t peer, int ready, void* arg) { + (void)group; struct DB_SYNC_INSTANCE* si = arg; + if (!si->enabled) return; + struct SI_PEER* p = ready ? si_peer_add(si, peer) : si_peer_find(si, peer); + if (!p) return; + if (!ready) { + p->sync_state = 0; + merkle_sync_cancel(si->db_sync->inst, MT_DATA_MESSAGES, peer, si->ns); + } else db_start_peer(si, peer); } -// Периодическая TTL-очистка: удаляет неподтверждённые записи (flags&1==0) старше db_sync_ttl от локального узла -static void db_sync_instance_ttl_cb(void* arg) -{ - struct DB_SYNC_INSTANCE* si = (struct DB_SYNC_INSTANCE*)arg; - DEBUG_TRACE(DEBUG_CATEGORY_CHAT_SYNC, "tbl=%s enabled=%d", SI_TBL(si), si->enabled); - uint64_t interval = DB_SYNC_TTL_INTERVAL * 10000u; - - if (!si || !si->enabled || !si->db_sync || !si->db_sync->db) { - if (si) si->ttl_timer = uasync_set_timeout(si->db_sync->inst->ua, interval, si, db_sync_instance_ttl_cb, "db_sync_ttl"); - return; - } - - uint64_t my_id = si->db_sync->inst->node_id; - uint64_t nu = get_time_us(); - uint64_t ttl = (uint64_t)(si->db_sync->inst->config->global.db_sync_ttl) * 1000000uLL; - uint64_t cut = nu - ttl; - - sqlite3_stmt* stmt; - if (si_prep(si, &stmt, - "DELETE FROM \"%s\"" - " WHERE node_id=? AND (flags & 1) = 0" - " AND timestampttl_timer = uasync_set_timeout(si->db_sync->inst->ua, interval, si, db_sync_instance_ttl_cb, "db_sync_ttl"); - return; - } - sqlite3_bind_int64(stmt, 1, (sqlite3_int64)my_id); - sqlite3_bind_int64(stmt, 2, (sqlite3_int64)cut); - sqlite3_step(stmt); - int d = sqlite3_changes(SI_DB(si)); - sqlite3_finalize(stmt); - - if (d > 0) DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "TTL cleanup deleted %d unsent records (tbl=%s)", d, SI_TBL(si)); - - si->ttl_timer = uasync_set_timeout(si->db_sync->inst->ua, interval, si, db_sync_instance_ttl_cb, "db_sync_ttl"); +int db_sync_init(struct UTUN_INSTANCE* inst) { + if (!inst || inst->db_sync) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "history invalid init"); return -1; } + struct DB_SYNC* db = u_calloc(1, sizeof(*db)); + if (!db) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "history allocation"); return -1; } + db->inst = inst; db->db = inst->topo_sqlite_db; inst->db_sync = db; + return inst->config->global.db_sync_enabled ? db_sync_enable(inst) : 0; } - -// ============================================================ -// Public API -// ============================================================ - -// Инициализирует модуль db_sync: открывает БД, регистрирует ETCP-биндинг, запускает таймер проверки пиров -int db_sync_init(struct UTUN_INSTANCE* inst) -{ - if (!inst) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "NULL instance"); return -1; } - DEBUG_TRACE(DEBUG_CATEGORY_CHAT_SYNC, "inst=%p enabled=%d", (void*)inst, inst->config->global.db_sync_enabled); - struct DB_SYNC* db = u_calloc(1, sizeof(struct DB_SYNC)); - if (!db) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "u_calloc failed"); return -1; } - db->inst = inst; - - if (!inst->config->global.db_sync_enabled) { - db->enabled = 0; inst->db_sync = db; - DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "db_sync: disabled by config"); - return 0; - } - - inst->db_sync = db; - return db_sync_enable(inst); -} - int db_sync_enable(struct UTUN_INSTANCE* inst) { struct DB_SYNC* db = inst ? inst->db_sync : NULL; - if (!db) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "db_sync enable requires initialized core"); return -1; } + if (!db || !inst->topo_sqlite_db) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "history requires core SQLite"); return -1; } if (db->enabled) return 0; - const char* dp = inst->config->global.db_path; - if (inst->topo_sqlite_db) { - db->db = inst->topo_sqlite_db; db->shared_db = 1; - DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "db_sync: using shared SQLite db=%p", (void*)db->db); - } else { - char sp[512]; - if (dp[0]) snprintf(sp, sizeof(sp), "%s/chats.db", dp); - else snprintf(sp, sizeof(sp), "/tmp/utun_db_sync"); - if (db_sqlite_open(db, sp) != 0) { - DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "SQLite open failed, sync disabled"); - return -1; - } - } - - db->enabled = 1; + db->db = inst->topo_sqlite_db; db->enabled = 1; etcp_bind(inst, ETCP_ID_DB_SYNC, db_sync_recv_cb); - - DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "db_sync: initialized (enabled=%d)", db->enabled); + DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "history enabled on shared core SQLite"); return 0; } - -// Уничтожает модуль db_sync: отменяет таймеры, закрывает БД, освобождает память -void db_sync_destroy(struct UTUN_INSTANCE* inst) -{ - if (!inst || !inst->db_sync) return; - DEBUG_TRACE(DEBUG_CATEGORY_CHAT_SYNC, "inst=%p instances=%d", (void*)inst, inst->db_sync->instance_count); - struct DB_SYNC* db = inst->db_sync; - inst->db_sync = NULL; - +struct DB_SYNC_INSTANCE* db_sync_instance_add(struct UTUN_INSTANCE* inst, const char* name, uint64_t group_id) { + struct DB_SYNC* db = inst ? inst->db_sync : NULL; + if (!db || !db->enabled || !si_name_valid(name) || !topo_groups_find(inst->topo_groups, group_id)) { + DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "history invalid table registration"); return NULL; + } + struct DB_SYNC_INSTANCE* si = db_instance_find(db, group_id); + if (si) return si; + if (db->instance_count == db->instance_capacity) { + int cap = db->instance_capacity ? db->instance_capacity * 2 : 4; + void* entries = u_realloc(db->instances, (size_t)cap * sizeof(*db->instances)); + if (!entries) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "history registry allocation"); return NULL; } + db->instances = entries; db->instance_capacity = cap; + } + si = u_calloc(1, sizeof(*si)); + if (!si) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "history table allocation"); return NULL; } + si->db_sync = db; si->group_id = group_id; si->enabled = 1; si->sync_gated = 1; + snprintf(si->table_name, sizeof(si->table_name), "%s", name); + snprintf(si->ns, sizeof(si->ns), "%llu", (unsigned long long)group_id); + db->instances[db->instance_count++] = si; + if (si_table_init(si) < 0) goto error; + char sql[512]; + snprintf(sql, sizeof(sql), "CREATE INDEX IF NOT EXISTS \"idx_%s_ttl\" ON \"%s\"(node_id,timestamp);" + "CREATE INDEX IF NOT EXISTS \"idx_%s_bucket\" ON \"%s\"(bucket,message_id)", name, name, name, name); + if (sqlite3_exec(db->db, sql, NULL, NULL, NULL) != SQLITE_OK) { db_error(si, "create indexes"); goto error; } + sqlite3_stmt* st = NULL; + int rc = si_prep(si, &st, "SELECT COALESCE(MAX(id),0),COALESCE(MAX(timestamp),0) FROM \"%s\""); + if (rc == SQLITE_OK) rc = sqlite3_step(st); + if (rc == SQLITE_ROW) { + sqlite3_int64 max_id = sqlite3_column_int64(st, 0); + if (max_id < 0 || max_id == INT64_MAX) rc = SQLITE_TOOBIG; + else si->next_id = (uint64_t)max_id + 1; + si->last_timestamp_ms = (uint64_t)sqlite3_column_int64(st, 1); + } + int end_rc = sqlite3_finalize(st); + if (rc != SQLITE_ROW || end_rc != SQLITE_OK) { db_error(si, "load local sequence"); goto error; } + if (topo_group_add_peer_ready_cbk(topo_groups_find(inst->topo_groups, group_id), db_sync_on_peer_ready, si) < 0) goto error; + db_index_request(si); + si->ttl_timer = uasync_set_timeout(inst->ua, DB_SYNC_TTL_INTERVAL * 10000ULL, si, db_ttl_tick, "history_ttl"); + if (!si->ttl_timer) { db_error(si, "initial TTL timer"); goto error; } + DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "history table registered tbl=%s group=%s; preflight pending", name, si->ns); + return si; +error: + db_sync_instance_remove(si); return NULL; +} +void db_sync_instance_remove(struct DB_SYNC_INSTANCE* si) { + if (!si || !si->enabled) return; + si->enabled = 0; struct DB_SYNC* db = si->db_sync; + topo_group_remove_peer_ready_cbk(topo_groups_find(db->inst->topo_groups, si->group_id), db_sync_on_peer_ready, si); + merkle_sync_cancel_dataset_ns(db->inst, MT_DATA_MESSAGES, si->ns); + if (si->index.timer) uasync_cancel_timeout(db->inst->ua, si->index.timer); + if (si->ttl_timer) uasync_cancel_timeout(db->inst->ua, si->ttl_timer); + for (struct db_notice* n = db->notices; n;) { + struct db_notice* next = n->next; if (n->group_id == si->group_id) db_notice_abort(n); n = next; + } + for (int i = 0; i < db->instance_count; i++) if (db->instances[i] == si) { + memmove(db->instances + i, db->instances + i + 1, (size_t)(db->instance_count - i - 1) * sizeof(*db->instances)); + db->instance_count--; break; + } + u_free(si->peers); si->peers = NULL; si->peer_count = 0; + DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "history table detached tbl=%s; signed records preserved", SI_TBL(si)); + if (!si->holds) u_free(si); +} +void db_sync_destroy(struct UTUN_INSTANCE* inst) { + struct DB_SYNC* db = inst ? inst->db_sync : NULL; + if (!db) return; + db->enabled = 0; + if (db->peer_check_timer) uasync_cancel_timeout(inst->ua, db->peer_check_timer); + while (db->instance_count) db_sync_instance_remove(db->instances[db->instance_count - 1]); + while (db->notices) db_notice_abort(db->notices); + while (db->done_cbks) { + struct db_sync_done_cbk_entry* e = db->done_cbks; db->done_cbks = e->next; u_free(e); + } etcp_unbind(inst, ETCP_ID_DB_SYNC); + inst->db_sync = NULL; u_free(db->instances); u_free(db); +} - if (db->peer_check_timer) { uasync_cancel_timeout(inst->ua, db->peer_check_timer); db->peer_check_timer = NULL; } -#ifdef UTUN_HAVE_STANDBY - if (db->peer_check_wait) { standby_wait_cancel(db->peer_check_wait); db->peer_check_wait = NULL; } -#endif +int db_sync_insert_signed(struct DB_SYNC_INSTANCE* si, const char* data, size_t len, const uint8_t* sig, size_t sig_len, + uint64_t timestamp, const char* attrs) { + if (!si || !si->enabled || !sig || sig_len != 64) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "history invalid insertion"); return -1; } + struct db_record r = {.timestamp = timestamp, .author = si->db_sync->inst->node_id, .data = data, .data_len = len, .sig_len = sig_len}; + memcpy(r.signature, sig, 64); + int result = db_insert_one(si, &r, attrs); + if (!result) db_emit_push(si, &r, r.author); + return result; +} +uint32_t db_sync_count(struct DB_SYNC_INSTANCE* si) { + if (!si || !si->enabled) return 0; + sqlite3_stmt* st = NULL; int rc = si_prep(si, &st, "SELECT COUNT(*) FROM \"%s\""); + if (rc == SQLITE_OK) rc = sqlite3_step(st); + uint32_t count = rc == SQLITE_ROW ? (uint32_t)sqlite3_column_int64(st, 0) : 0; + if (rc != SQLITE_ROW) db_error(si, "count"); sqlite3_finalize(st); return count; +} +uint64_t db_sync_get_last_timestamp(struct DB_SYNC_INSTANCE* si) { return si ? si->last_timestamp_ms : 0; } +uint64_t db_sync_next_timestamp(struct DB_SYNC_INSTANCE* si) { + if (!si) return 0; uint64_t now = db_now_ms(si); + if (now <= si->last_timestamp_ms) now = si->last_timestamp_ms + 1; + return si->last_timestamp_ms = now; +} +void db_sync_set_insert_cb(struct DB_SYNC_INSTANCE* si, db_sync_insert_cb fn, void* arg) { + if (si) { si->on_insert = fn; si->on_insert_arg = arg; } +} +int db_sync_select(struct DB_SYNC_INSTANCE* si, uint32_t offset, uint32_t limit, db_sync_select_cb cb, void* arg) { + if (!si || !si->enabled || !cb) return -1; + sqlite3_stmt* st = NULL; si->holds++; + int rc = si_prep(si, &st, "SELECT id,timestamp,node_id,data,author_signature,delivered_peers,delivery_chain" + " FROM \"%s\" ORDER BY timestamp,author_signature LIMIT ? OFFSET ?"); + if (rc == SQLITE_OK) rc = sqlite3_bind_int64(st, 1, limit ? limit : -1LL); + if (rc == SQLITE_OK) rc = sqlite3_bind_int64(st, 2, offset); + int count = 0; + if (rc == SQLITE_OK) while ((rc = sqlite3_step(st)) == SQLITE_ROW && si->enabled) { + const char* data = sqlite3_column_blob(st, 3); int bytes = sqlite3_column_bytes(st, 3); + const uint8_t* signature = sqlite3_column_blob(st, 4); int sig_len = sqlite3_column_bytes(st, 4); + const char* chain = (const char*)sqlite3_column_text(st, 6); + if (sqlite3_errcode(SI_DB(si)) == SQLITE_NOMEM) { rc = SQLITE_NOMEM; break; } + cb(arg, (uint64_t)sqlite3_column_int64(st, 0), (uint64_t)sqlite3_column_int64(st, 1), data, (size_t)bytes, + (uint64_t)sqlite3_column_int64(st, 2), signature, (size_t)sig_len, sqlite3_column_int(st, 5), chain ? chain : ""); + count++; + } + int end_rc = sqlite3_finalize(st); + if ((rc != SQLITE_DONE && si->enabled) || end_rc != SQLITE_OK) { db_error(si, "select"); count = -1; } + db_release(si); return count; +} +/* Диагностический синхронный обход: в рабочем обмене preflight идёт порциями по таймеру. */ +int db_sync_verify(struct DB_SYNC_INSTANCE* si) { + if (!si || !si->enabled || !merkle_sync_registered(si->db_sync->inst, MT_DATA_MESSAGES)) return -1; + if (!si->index.pending) db_index_request(si); + while (si->index.pending) if (db_index_batch(si) != 0) return si->index.waiting_key ? 1 : -1; + if (si->index.timer) { uasync_cancel_timeout(si->db_sync->inst->ua, si->index.timer); si->index.timer = NULL; } + uint8_t root[32]; return merkle_sync_read_hash(si->db_sync->inst, MT_DATA_MESSAGES, si->ns, 0, 0, root); +} +int db_sync_read_root(struct DB_SYNC_INSTANCE* si, uint8_t root[32]) { + if (!si || !si->enabled || !root) return -1; + if (si->index.pending) return 1; + return merkle_sync_read_hash(si->db_sync->inst, MT_DATA_MESSAGES, si->ns, 0, 0, root); +} +void db_sync_peer_set_paused(struct DB_SYNC_INSTANCE* si, uint64_t peer, int paused) { + struct SI_PEER* p = si ? si_peer_add(si, peer) : NULL; + if (!p) return; + p->paused = !!paused; p->sync_state = 0; + if (paused) merkle_sync_cancel(si->db_sync->inst, MT_DATA_MESSAGES, peer, si->ns); + else db_start_peer(si, peer); +} +void db_sync_reinitiate(struct DB_SYNC_INSTANCE* si, uint64_t peer) { + if (!si) return; + struct SI_PEER* p = si_peer_add(si, peer); if (!p) return; + p->paused = 0; p->sync_state = 0; db_start_peer(si, peer); +} +void db_sync_instance_set_gated(struct DB_SYNC_INSTANCE* si, int gated) { + if (!si || !si->enabled) return; + si->sync_gated = !!gated; + if (gated) merkle_sync_cancel_dataset_ns(si->db_sync->inst, MT_DATA_MESSAGES, si->ns); + struct TOPO_GROUP* group = topo_groups_find(si->db_sync->inst->topo_groups, si->group_id); + for (struct ll_entry* e = group && group->senders_list ? group->senders_list->head : NULL; e; e = e->next) { + struct TOPO_GROUP_CONN_ITEM* item = (struct TOPO_GROUP_CONN_ITEM*)e->data; + struct SI_PEER* p = si_peer_add(si, item->node_id); if (p) p->sync_state = 0; + if (!gated) db_start_peer(si, item->node_id); + } + db_sync_resume_peer_check(si->db_sync); +} +void db_sync_peer_policy_changed(struct UTUN_INSTANCE* inst, uint64_t peer) { + struct DB_SYNC* db = inst ? inst->db_sync : NULL; if (!db) return; + merkle_sync_recheck_peer(inst, peer); + for (int i = 0; i < db->instance_count; i++) { + struct DB_SYNC_INSTANCE* si = db->instances[i]; struct SI_PEER* p = si_peer_find(si, peer); + if (p && !p->sync_state) db_start_peer(si, peer); + } +} +void db_sync_set_peer_sound(struct UTUN_INSTANCE* inst, uint64_t group, uint64_t peer, int sound) { + struct DB_SYNC_INSTANCE* si = db_sync_instance_find(inst, group); + struct SI_PEER* p = si ? si_peer_add(si, peer) : NULL; + if (p) { p->sound_on = !!sound; db_sync_peer_policy_changed(inst, peer); } +} +void db_sync_members_changed(struct UTUN_INSTANCE* inst, const char* ns) { + struct DB_SYNC* db = inst ? inst->db_sync : NULL; if (!db) return; + merkle_sync_revalidate_peers(inst, MT_DATA_MESSAGES, ns); for (int i = 0; i < db->instance_count; i++) { struct DB_SYNC_INSTANCE* si = db->instances[i]; - struct TOPO_GROUP* group = topo_groups_find(inst->topo_groups, si->group_id); - topo_group_remove_peer_ready_cbk(group, db_sync_on_peer_ready, si); - if (si->ttl_timer) { uasync_cancel_timeout(inst->ua, si->ttl_timer); si->ttl_timer = NULL; } - if (si->recalc.timer) { uasync_cancel_timeout(inst->ua, si->recalc.timer); si->recalc.timer = NULL; } - if (si->peers) u_free(si->peers); - u_free(si); + if (si->index.pending && si->index.waiting_key) { + if (si->index.timer) uasync_cancel_timeout(inst->ua, si->index.timer); + si->index.timer = uasync_set_timeout(inst->ua, 0, si, db_index_tick, "history_key_ready"); + if (!si->index.timer) db_sync_resume_peer_check(db); + } } - - - { struct db_sync_done_cbk_entry* cb = db->done_cbks; while (cb) { struct db_sync_done_cbk_entry* n = cb->next; u_free(cb); cb = n; } } - - db_sqlite_close(db); - if (db->instances) u_free(db->instances); - u_free(db); - DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "db_sync: destroyed"); -} - -// Создаёт локальное состояние таблицы, наблюдает группу. До member_sync сетевого обмена нет. -struct DB_SYNC_INSTANCE* db_sync_instance_add(struct UTUN_INSTANCE* inst, const char* table_name, uint64_t group_id) -{ - if (!inst || !inst->db_sync || !si_name_valid(table_name)) { - DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "history registration: invalid instance or table name"); return NULL; - } - DEBUG_TRACE(DEBUG_CATEGORY_CHAT_SYNC, "table=%s group=%016llx", table_name, (unsigned long long)group_id); - struct DB_SYNC* db = inst->db_sync; - if (!db->enabled || !db->db) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "history registration: module disabled"); return NULL; } - struct TOPO_GROUP* group = topo_groups_find(inst->topo_groups, group_id); - if (!group) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "history registration requires group=%016llx", group_id); return NULL; } - - struct DB_SYNC_INSTANCE* si = db_instance_find(db, group_id); - if (si) { DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "instance already exists group=%016llx", (unsigned long long)group_id); return si; } - - si = db_instance_alloc(db); - if (!si) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "history instance allocation failed"); return NULL; } - si->group_id = group_id; - snprintf(si->table_name, sizeof(si->table_name), "%s", table_name); - - if (si_table_init(si) < 0) { db_sync_instance_remove(si); return NULL; } - - // Производный TTL-индекс создаём также после перестройки таблицы. - { - char sql[768]; - snprintf(sql, sizeof(sql), - "CREATE INDEX IF NOT EXISTS \"idx_%s_ttl\"" - " ON \"%s\" (node_id, timestamp)", - si->table_name, si->table_name); - int rc = sqlite3_exec(db->db, sql, NULL, NULL, NULL); - if (rc != SQLITE_OK) DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "CREATE INDEX %s: %s", si->table_name, sqlite3_errmsg(db->db)); - } - - // Get next id - { - sqlite3_stmt* stmt; - if (si_prep(si, &stmt, "SELECT COALESCE(MAX(id),0)+1 FROM \"%s\"") == SQLITE_OK - && sqlite3_step(stmt) == SQLITE_ROW) - si->next_id = (uint64_t)sqlite3_column_int64(stmt, 0); - else - si->next_id = 1; - sqlite3_finalize(stmt); - } - - db_verify_chain(si); - - DEBUG_DEBUG(DEBUG_CATEGORY_CHAT_SYNC, "instance_add: tbl=%s mc=%u next_id=%llu group=%016llx", SI_TBL(si), db_count(si), (unsigned long long)si->next_id, (unsigned long long)si->group_id); - - si->ttl_timer = uasync_set_timeout(inst->ua, DB_SYNC_TTL_INTERVAL * 10000u, si, db_sync_instance_ttl_cb, "db_sync_ttl"); - - if (topo_group_add_peer_ready_cbk(group, db_sync_on_peer_ready, si) != 0) { - db_sync_instance_remove(si); return NULL; - } - - DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, - "history registered table=%s tbl=%s group=%016llx next_id=%llu gated=1 mc=%u", - table_name, si->table_name, (unsigned long long)group_id, - (unsigned long long)si->next_id, db_count(si)); - return si; -} - -// Удаляет инстанс: отменяет таймеры, освобождает пиров, сдвигает массив (таблица БД не удаляется) -void db_sync_instance_remove(struct DB_SYNC_INSTANCE* si) -{ - if (!si) return; - DEBUG_TRACE(DEBUG_CATEGORY_CHAT_SYNC, "tbl=%s group=%016llx", si->table_name, (unsigned long long)si->group_id); - struct DB_SYNC* db = si->db_sync; - struct UTUN_INSTANCE* inst = db->inst; - - struct TOPO_GROUP* group = topo_groups_find(inst->topo_groups, si->group_id); - topo_group_remove_peer_ready_cbk(group, db_sync_on_peer_ready, si); - si->enabled = 0; - if (si->ttl_timer) { uasync_cancel_timeout(inst->ua, si->ttl_timer); si->ttl_timer = NULL; } - if (si->recalc.timer) { uasync_cancel_timeout(inst->ua, si->recalc.timer); si->recalc.timer = NULL; } - if (si->peers) { u_free(si->peers); si->peers = NULL; si->peer_count = si->peer_capacity = 0; } - - int idx = -1; - for (int i = 0; i < db->instance_count; i++) { - if (db->instances[i] == si) { idx = i; break; } - } - if (idx >= 0) { - memmove(&db->instances[idx], &db->instances[idx + 1], - (db->instance_count - idx - 1) * sizeof(*db->instances)); - db->instance_count--; - } - DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "instance removed tbl=%s group=%016llx", si->table_name, (unsigned long long)si->group_id); - u_free(si); -} - -// Вставляет подписанную запись: проверяет подпись, вставляет в БД, рассылает PUSH всем синхронизированным пирам -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, - const char* local_attrs) -{ - if (!si || !si->enabled || !json_data || len == 0) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "insert invalid args"); return -1; } - if (!sig || sig_len != DB_SIG_SIZE) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "insert_signed: signature required (64 bytes Ed25519)"); return -1; } - if (len > UINT32_MAX) { DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "history insert body too large bytes=%zu", len); return -2; } - DEBUG_TRACE(DEBUG_CATEGORY_CHAT_SYNC, "tbl=%s id=%llu ts=%llu len=%zu", SI_TBL(si), (unsigned long long)si->next_id, (unsigned long long)ts, len); - - uint64_t id = si->next_id; - uint64_t author_node_id = si->db_sync->inst->node_id; - - return db_record_insert_cascade(si, id, ts, author_node_id, json_data, (uint32_t)len, sig, author_node_id, local_attrs, 0); -} - -// Возвращает количество записей в инстансе -uint32_t db_sync_count(struct DB_SYNC_INSTANCE* si) -{ - if (!si || !si->enabled) return 0; - return db_count(si); -} - -// Возвращает последний использованный timestamp инстанса -uint64_t db_sync_get_last_timestamp(struct DB_SYNC_INSTANCE* si) -{ - return si ? si->last_timestamp_ms : 0; -} - -// Возвращает следующий монотонно возрастающий timestamp (мкс → мс), использует NTP если синхронизировано -uint64_t db_sync_next_timestamp(struct DB_SYNC_INSTANCE* si) -{ - if (!si) return 0; - int64_t now_us; - if (si->db_sync && si->db_sync->inst && si->db_sync->inst->ntp.synced) - now_us = ntp_time_get_us(si->db_sync->inst); - else { - struct timeval tv; - utun_gettimeofday(&tv, NULL); - now_us = (int64_t)tv.tv_sec * 1000000LL + tv.tv_usec; - } - uint64_t nu = (uint64_t)(now_us / 1000LL); - if (nu <= si->last_timestamp_ms) nu = si->last_timestamp_ms + 1; - si->last_timestamp_ms = nu; - return nu; -} - -// Устанавливает колбэк, вызываемый при каждой успешной вставке записи (локальной или от пира) -void db_sync_set_insert_cb(struct DB_SYNC_INSTANCE* si, db_sync_insert_cb cb, void* arg) -{ - if (!si) return; - si->on_insert = cb; - si->on_insert_arg = arg; -} - -// Итерирует записи с offset/limit, передавая каждую в колбэк. Возвращает количество переданных записей -int db_sync_select(struct DB_SYNC_INSTANCE* si, uint32_t offset, uint32_t limit, - db_sync_select_cb cb, void* arg) -{ - if (!si || !si->enabled || !cb) return 0; - DEBUG_TRACE(DEBUG_CATEGORY_CHAT_SYNC, "tbl=%s off=%u lim=%u", SI_TBL(si), offset, limit); - - sqlite3_stmt* stmt; - if (si_prep(si, &stmt, - "SELECT id,timestamp,node_id,data," - "author_signature,delivered_peers,delivery_chain" - " FROM \"%s\" ORDER BY timestamp, author_signature" - " LIMIT ? OFFSET ?") != SQLITE_OK) - return 0; - - sqlite3_bind_int64(stmt, 1, limit > 0 ? (sqlite3_int64)limit : -1); - sqlite3_bind_int64(stmt, 2, (sqlite3_int64)offset); - - int cnt = 0; - while (sqlite3_step(stmt) == SQLITE_ROW) { - uint64_t rid = (uint64_t)sqlite3_column_int64(stmt, 0); - uint64_t rts = (uint64_t)sqlite3_column_int64(stmt, 1); - uint64_t rauth = (uint64_t)sqlite3_column_int64(stmt, 2); - const char* rd = (const char*)sqlite3_column_blob(stmt, 3); - int rdl = sqlite3_column_bytes(stmt, 3); - const uint8_t* rsig = (const uint8_t*)sqlite3_column_blob(stmt, 4); - int rsl = sqlite3_column_bytes(stmt, 4); if (!rsig) rsl = 0; - int rdp = sqlite3_column_int(stmt, 5); - const char* rdc = (const char*)sqlite3_column_text(stmt, 6); if (!rdc) rdc = ""; - - cb(arg, rid, rts, rd, (size_t)(rdl > 0 ? rdl : 0), rauth, rsig, (size_t)rsl, rdp, rdc); - cnt++; - } - sqlite3_finalize(stmt); - return cnt; } diff --git a/src/chat/db_sync.h b/src/chat/db_sync.h index ca953f13..35b4e7a2 100644 --- a/src/chat/db_sync.h +++ b/src/chat/db_sync.h @@ -1,6 +1,6 @@ /* db_sync — репликация подписанных записей SQLite по прямым ETCP-соединениям. * Каждый DB_SYNC_INSTANCE связывает таблицу с group_id. Различия находятся по - * цепочке хешей; новые записи рассылаются PUSH. Ключ и порядок записей: + * общему Merkle-дереву; новые записи рассылаются PUSH. Ключ и порядок отображения: * (timestamp, author_signature). Подпись автора покрывает timestamp || data. * Ядро создаёт модуль, чат включает его и регистрирует таблицы каналов. * Обычно используется общая SQLite ядра; remove отключает таблицу от обмена, @@ -21,32 +21,15 @@ struct DB_SYNC_INSTANCE; // etcp service ID: ETCP_ID_DB_SYNC (etcp_bind, см. etcp_api.h) // Message types -#define DB_MSG_INIT_SYNC 0x01 -#define DB_MSG_INIT_RESP 0x02 -#define DB_MSG_SEND_DATA 0x04 #define DB_MSG_PUSH 0x05 #define DB_MSG_ACK_PUSH 0x06 -#define DB_MSG_SYNC_DONE 0x07 -#define DB_MSG_ERROR 0x08 -#define DB_MSG_REQUEST_SYNC 0x09 /* PUSH имеет фиксированный буфер 4096 байт; 4000 оставляет место заголовку и подписи. */ #define DB_SYNC_MESSAGE_MAX 4000 -// SEND_DATA want_from sentinel -#define DB_WANT_FROM_NONE 0xFFFFFFFF // no request for peer's data - -// SEND_DATA wire: [from:4][count:2][vp:4][want_from:4][records...] -// Record: [id:8][ts:8][node_id:8][dlen:4][data][sig_len:1=64][sig:64] - -// SYNC_DONE wire: [count:4][chain_hash8:8] +// PUSH и записи MESSAGES: [message_id:32][ts:8BE][author:8BE][dlen:4BE][data][sig:64]. // ACK_PUSH wire: [message_id:32]. Подтверждает конкретную запись; повторы идемпотентны. -// Error codes for DB_MSG_ERROR -#define DB_ERR_NOT_FOUND 0x01 // instance not found -#define DB_ERR_DISABLED 0x02 // instance exists but disabled -#define DB_ERR_NOT_READY 0x03 // участники или групповая сессия ещё не готовы - // Defaults #define DB_SYNC_DEFAULT_TTL 86400 #define DB_SYNC_DEFAULT_MAPSIZE (100UL * 1024 * 1024) @@ -67,6 +50,8 @@ struct DB_SYNC_INSTANCE; int db_sync_init(struct UTUN_INSTANCE* inst); // Включить ранее созданный модуль независимо от флага конфига. Повтор допустим; 0/-1. int db_sync_enable(struct UTUN_INSTANCE* inst); +// Подключить MESSAGES после создания общего Merkle-движка; запланировать проверку своей истории. 0/-1. +int db_sync_register_model(struct UTUN_INSTANCE* inst); // Отменить таймеры/обмен, снять обработчики; общую SQLite не закрывает. void db_sync_destroy(struct UTUN_INSTANCE* inst); @@ -115,11 +100,14 @@ typedef void (*db_sync_insert_cb)(struct DB_SYNC_INSTANCE* si, uint64_t author_node_id, int initial_sync, void* arg); void db_sync_set_insert_cb(struct DB_SYNC_INSTANCE* si, db_sync_insert_cb cb, void* arg); -// Проверить цепочку: 0 — корректна, 1 — несовпадение/битые поля, -1 — операционная ошибка. -int db_sync_chain_verify(struct DB_SYNC_INSTANCE* si); +// Диагностика: синхронно перепроверить/очистить сохранённые записи и восстановить индекс. +// 0 — готов, 1 — ожидается ключ автора, -1 — операционная ошибка. В обычной работе обход порционный. +int db_sync_verify(struct DB_SYNC_INSTANCE* si); +// Прочитать полный корень: 0 — успех, 1 — preflight ещё не завершён, -1 — ошибка. root[32] обязателен. +int db_sync_read_root(struct DB_SYNC_INSTANCE* si, uint8_t root[32]); // Peer state control (for testing — disable PUSH to isolate sync protocol) -void db_sync_peer_set_state(struct DB_SYNC_INSTANCE* si, uint64_t node_id, uint8_t state); +void db_sync_peer_set_paused(struct DB_SYNC_INSTANCE* si, uint64_t node_id, int paused); // Per-(chat,peer) «play sound on message» flag: sound_off + sleeping peer → throttled. void db_sync_set_peer_sound(struct UTUN_INSTANCE* inst, uint64_t group_id, uint64_t peer_node_id, int sound_on); @@ -131,10 +119,11 @@ void db_sync_instance_set_gated(struct DB_SYNC_INSTANCE* si, int gated); // Force re-initiate sync to a specific peer (for testing) void db_sync_reinitiate(struct DB_SYNC_INSTANCE* si, uint64_t node_id); -// Get last chain hash8 (for cross-peer consistency check in tests) -int db_sync_last_chain_hash8(struct DB_SYNC_INSTANCE* si, uint64_t* out_hash8); +// После sleep/sound переоценить работу истории; после изменения MEMBERS также повторить ожидание общих ключей. +void db_sync_peer_policy_changed(struct UTUN_INSTANCE* inst, uint64_t peer); +void db_sync_members_changed(struct UTUN_INSTANCE* inst, const char* ns); -// Sync completion callback (global, per-instance: fires when SYNC_DONE sent/received for any group) +// Глобальная подписка на подтверждённый полный Merkle-корень истории; callback может удалить подписку/канал. typedef void (*db_sync_done_fn)(struct DB_SYNC_INSTANCE* si, uint64_t peer_node_id, void* arg); void db_sync_add_done_cbk(struct UTUN_INSTANCE* inst, db_sync_done_fn fn, void* arg); void db_sync_remove_done_cbk(struct UTUN_INSTANCE* inst, db_sync_done_fn fn, void* arg);