From 3e25bcef0251bc79c66ea11081f364aa6571a146 Mon Sep 17 00:00:00 2001 From: evgeny Date: Sun, 27 Sep 2026 14:44:39 +0300 Subject: [PATCH] Refactor Merkle sync into bounded bidirectional rounds with atomic member updates --- src/Makefile.am | 4 + src/chat/chat_channel.c | 2 - src/chat/chat_core.c | 1 - src/chat/chat_join.c | 1 - src/chat/chat_profile.c | 1 - src/chat/chat_sync.c | 10 +- src/chat/member_sync.c | 558 +++++++------ src/chat/member_sync.h | 29 +- src/chat/member_sync_doc.md | 179 +--- src/chat/merkle_sync.c | 1343 +++++++++++------------------- src/chat/merkle_sync.h | 340 +------- src/chat/merkle_sync_doc.md | 220 ++--- src/chat/merkle_tree.c | 122 +++ src/chat/merkle_tree.h | 24 + src/transport_layer/etcp_api.c | 8 + tests/Makefile.am | 8 + tests/test_chat_join_e2e.c | 8 + tests/test_media_delivery_full.c | 8 +- tests/test_member_adapter.c | 68 ++ tests/test_merkle_protocol.c | 215 +++++ tests/test_merkle_sync.c | 284 +++---- 21 files changed, 1562 insertions(+), 1871 deletions(-) create mode 100644 src/chat/merkle_tree.c create mode 100644 src/chat/merkle_tree.h create mode 100644 tests/test_member_adapter.c create mode 100644 tests/test_merkle_protocol.c diff --git a/src/Makefile.am b/src/Makefile.am index 6388bd10..6ebe70db 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -91,6 +91,8 @@ utun_CORE_SOURCES = \ chat/chat_join.c \ chat/member_sync.c \ chat/merkle_sync.c \ + chat/merkle_tree.c \ + chat/merkle_tree.h \ chat/invite_link.c \ chat/invite_build.c \ chat/chat_headless_control.c \ @@ -193,6 +195,8 @@ libutun_a_SOURCES = \ chat/member_sync.c \ chat/merkle_sync.c \ chat/invite_link.c \ + chat/merkle_tree.c \ + chat/merkle_tree.h \ chat/invite_build.c \ chat/chat_headless_control.c \ dm/dm_core.c \ diff --git a/src/chat/chat_channel.c b/src/chat/chat_channel.c index ed39cfec..733d401f 100644 --- a/src/chat/chat_channel.c +++ b/src/chat/chat_channel.c @@ -137,8 +137,6 @@ void chat_core_create_channel(struct UTUN_INSTANCE* inst, struct chat_channel_cr if (mrc < 0) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: member_sync_put(self) FAILED ch=%s rc=%d", CC_ID, req->channel_id, mrc); - } else if (mrc & MS_APPLY_CHANGED) { - member_sync_broadcast_one(inst, req->channel_id, myid); } /* адреса + update_ts + подпись + синк — единой функцией */ chat_core_update_my_member(inst); diff --git a/src/chat/chat_core.c b/src/chat/chat_core.c index 9339c968..cde2e50b 100644 --- a/src/chat/chat_core.c +++ b/src/chat/chat_core.c @@ -701,7 +701,6 @@ int chat_member_tags_commit(struct UTUN_INSTANCE* inst, struct chat_member_tags* uinfo ? uinfo : "", json, sig, stg, 0, NULL); sqlite3_finalize(st); if (rc < 0) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit — member_sync_put failed rc=%d", CC_ID, rc); return -1; } - if (rc & MS_APPLY_CHANGED) member_sync_broadcast_one(inst, t->ch_id, t->node_id); DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit ch=%s nid=0x%016llx ver=%d keys=%d json=%s", CC_ID, t->ch_id, (unsigned long long)t->node_id, ver, t->key_count, json); diff --git a/src/chat/chat_join.c b/src/chat/chat_join.c index 64a27a31..926643b5 100644 --- a/src/chat/chat_join.c +++ b/src/chat/chat_join.c @@ -200,7 +200,6 @@ void chat_join_process_request(struct UTUN_INSTANCE* inst, uint64_t group_id, return; } if (rc & MS_APPLY_CHANGED) { - member_sync_broadcast_one(inst, ch_id, m.node_id); /* джойнер авторизован — добавляем в CHAT-группу сразу, чтобы BGP подключился */ uint64_t gid = strtoull(ch_id, NULL, 10); struct TOPO_GROUP* g = topo_groups_find(inst->topo_groups, gid); diff --git a/src/chat/chat_profile.c b/src/chat/chat_profile.c index 53f3947a..0df356ba 100644 --- a/src/chat/chat_profile.c +++ b/src/chat/chat_profile.c @@ -99,7 +99,6 @@ void chat_core_update_my_member(struct UTUN_INSTANCE* inst) { inst->my_ed25519_pubkey, join_sig, join_ts, update_sig, update_ts, juser, NULL, NULL, 0, 0, NULL); if (r2 < 0) DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: member_sync_put(self) FAILED ch=%s rc=%d", CC_ID, ch, r2); - else if (r2 & MS_APPLY_CHANGED) member_sync_broadcast_one(inst, ch, myid); DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: update my member ch=%s ts=%llu", CC_ID, ch, (unsigned long long)update_ts); diff --git a/src/chat/chat_sync.c b/src/chat/chat_sync.c index ebe5938c..38183730 100644 --- a/src/chat/chat_sync.c +++ b/src/chat/chat_sync.c @@ -602,6 +602,10 @@ static void cs_on_conn_down(struct ETCP_CONN* conn, int event, void* arg) { (voi static void cs_on_conn_status(struct ETCP_CONN* conn, int status, void* arg) { switch (status) { case ETCP_CONN_STATUS_UP: cs_on_conn_up(conn, ETCP_CBK_EVENT_UP, arg); break; + case ETCP_CONN_STATUS_REINIT: + cs_on_conn_down(conn, ETCP_CBK_EVENT_DOWN, arg); + cs_on_conn_up(conn, ETCP_CBK_EVENT_UP, arg); + break; case ETCP_CONN_STATUS_DOWN: case ETCP_CONN_STATUS_DELETE: cs_on_conn_down(conn, ETCP_CBK_EVENT_DOWN, arg); break; default: break; @@ -683,6 +687,11 @@ int chat_sync_init(struct UTUN_INSTANCE* inst) { cs->initialized = 1; inst->chat_sync = cs; + if (member_sync_init(inst) < 0) { + DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: member synchronization initialization failed", CS_ID); + inst->chat_sync = NULL; u_free(cs); return -1; + } + etcp_bind(inst, ETCP_RT_ID_CHAT_SYNC, chat_sync_recv_cb); etcp_add_conn_status_cbk(inst, cs_on_conn_status, cs); @@ -693,7 +702,6 @@ int chat_sync_init(struct UTUN_INSTANCE* inst) { cs->info_req_timer = NULL; cs->join_timer = NULL; - member_sync_init(inst); member_sync_add_apply_cbk(inst, cs_on_member_applied, cs); utun_add_peer_sleep_cbk(inst, cs_on_peer_sleep, cs); chat_admin_init(inst); diff --git a/src/chat/member_sync.c b/src/chat/member_sync.c index b05bc6dd..1b3f218d 100644 --- a/src/chat/member_sync.c +++ b/src/chat/member_sync.c @@ -34,35 +34,54 @@ static void _peers_table(const char* ch_id, char* buf, size_t sz) { snprintf(buf, sz, "%s", tbl); } -/* ── Member hash (identical to old _compute_member_hash) ── */ - -static void _compute_member_hash(uint64_t node_id, const uint8_t* x25519, - const uint8_t* ed25519, - const uint8_t* join_sig, uint64_t join_ts, - const uint8_t* update_sig, uint64_t update_ts, - const char* adm_tags, const uint8_t* adm_tags_sig, - uint64_t signed_by, const uint8_t* signature, - uint8_t hash_out[MT_HASH_SIZE]) { - EVP_MD_CTX* ctx = EVP_MD_CTX_new(); - EVP_DigestInit_ex(ctx, EVP_sha256(), NULL); - EVP_DigestUpdate(ctx, &node_id, 8); - EVP_DigestUpdate(ctx, x25519, 32); - EVP_DigestUpdate(ctx, ed25519, 32); - EVP_DigestUpdate(ctx, join_sig, 64); - EVP_DigestUpdate(ctx, &join_ts, 8); - if (update_sig) EVP_DigestUpdate(ctx, update_sig, 64); else { static const uint8_t z[64]; EVP_DigestUpdate(ctx, z, 64); } - EVP_DigestUpdate(ctx, &update_ts, 8); - - uint8_t atl = adm_tags ? (uint8_t)strnlen(adm_tags, 255) : 0; - EVP_DigestUpdate(ctx, &atl, 1); - if (atl) EVP_DigestUpdate(ctx, adm_tags, atl); - if (adm_tags_sig) EVP_DigestUpdate(ctx, adm_tags_sig, 64); else { static const uint8_t z64[64]; EVP_DigestUpdate(ctx, z64, 64); } - - EVP_DigestUpdate(ctx, &signed_by, 8); - if (signature) EVP_DigestUpdate(ctx, signature, 64); else { static const uint8_t zsig[64]; EVP_DigestUpdate(ctx, zsig, 64); } - - EVP_DigestFinal_ex(ctx, hash_out, NULL); - EVP_MD_CTX_free(ctx); +/* Канонические целые для Merkle и wire; формат подписываемых сообщений задаётся отдельно. */ +static void member_write64(uint8_t* p, uint64_t n) { + for (int i = 7; i >= 0; i--) { p[i] = (uint8_t)n; n >>= 8; } +} + +static uint64_t member_read64(const uint8_t* p) { + uint64_t n = 0; + for (int i = 0; i < 8; i++) n = (n << 8) | p[i]; + return n; +} + +static int _compute_member_hash(uint64_t node_id, const uint8_t* x25519, const uint8_t* ed25519, + const uint8_t* join_sig, uint64_t join_ts, const uint8_t* update_sig, uint64_t update_ts, + const char* userinfo, const char* adm_tags, const uint8_t* adm_tags_sig, + uint64_t signed_by, const uint8_t* signature, uint8_t hash_out[MT_HASH_SIZE]) { + uint8_t data[1024]; + size_t off = 0; + member_write64(data + off, node_id); off += 8; + memcpy(data + off, x25519, 32); off += 32; + memcpy(data + off, ed25519, 32); off += 32; + if (join_sig) memcpy(data + off, join_sig, 64); else memset(data + off, 0, 64); + off += 64; member_write64(data + off, join_ts); off += 8; + if (update_sig) memcpy(data + off, update_sig, 64); else memset(data + off, 0, 64); + off += 64; member_write64(data + off, update_ts); off += 8; + size_t ul = userinfo ? strlen(userinfo) : 0, atl = adm_tags ? strlen(adm_tags) : 0; + if (ul > 255 || atl > 255) { + DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: member strings exceed wire limits node=%016llx", MS_ID, (unsigned long long)node_id); + return -1; + } + data[off++] = (uint8_t)ul; + if (ul) { memcpy(data + off, userinfo, ul); off += ul; } + data[off++] = (uint8_t)atl; + if (atl) { memcpy(data + off, adm_tags, atl); off += atl; } + if (adm_tags_sig) memcpy(data + off, adm_tags_sig, 64); else memset(data + off, 0, 64); + off += 64; member_write64(data + off, signed_by); off += 8; + if (signature) memcpy(data + off, signature, 64); else memset(data + off, 0, 64); + off += 64; + if (EVP_Digest(data, off, hash_out, NULL, EVP_sha256(), NULL) != 1) { + DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: member hash failed node=%016llx", MS_ID, (unsigned long long)node_id); + return -1; + } + return 0; +} + +/* SQLite BLOB pointers are only safe after checking their exact length. */ +static int member_blob(sqlite3_stmt* st, int col, int size, int optional) { + int bytes = sqlite3_column_bytes(st, col); + return bytes == size || (optional && bytes == 0); } int member_sync_build_update_msg(const uint8_t* join_sig, uint64_t update_ts, @@ -114,7 +133,9 @@ static int _member_update_bucket_hash(void* ctx, const char* ns, uint8_t level, uint64_t prefix64, EVP_MD_CTX* sha_ctx) { struct UTUN_INSTANCE* inst = (struct UTUN_INSTANCE*)ctx; sqlite3* db = _db(inst); if (!db) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: bucket_hash — db is NULL", MS_ID); return -1; } - if (level == 0) return 0; + if (level != MT_MAX_LEVEL) { + DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: expected leaf, level=%u", MS_ID, level); return -1; + } DEBUG_TRACE(DEBUG_CATEGORY_MEMBER_SYNC, "%s: bucket_hash ns=%s L%d/P%016llx", MS_ID, ns, level, (unsigned long long)prefix64); char peers_tbl[128]; _peers_table(ns, peers_tbl, sizeof(peers_tbl)); @@ -123,7 +144,7 @@ static int _member_update_bucket_hash(void* ctx, const char* ns, uint8_t level, char sql[512]; snprintf(sql, sizeof(sql), "SELECT node_id, x25519_pubkey, ed25519_pubkey," - " join_sig, join_ts, update_sig, update_ts, adm_tags, adm_tags_sig, signed_by, signature, source" + " join_sig, join_ts, update_sig, update_ts, adm_tags, adm_tags_sig, signed_by, signature, source, userinfo" " FROM \"%s\"" " WHERE (node_id & %lld) == %lld ORDER BY node_id ASC", peers_tbl, (long long)mask, (long long)prefix64); @@ -132,7 +153,8 @@ static int _member_update_bucket_hash(void* ctx, const char* ns, uint8_t level, if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: bucket_hash prepare failed ns=%s", MS_ID, ns); return -1; } int count = 0; - while (sqlite3_step(stmt) == SQLITE_ROW) { + int step; + while ((step = sqlite3_step(stmt)) == SQLITE_ROW) { uint64_t nid = (uint64_t)sqlite3_column_int64(stmt, 0); const uint8_t* x25 = (const uint8_t*)sqlite3_column_blob(stmt, 1); const uint8_t* ed = (const uint8_t*)sqlite3_column_blob(stmt, 2); @@ -145,23 +167,35 @@ static int _member_update_bucket_hash(void* ctx, const char* ns, uint8_t level, uint64_t sb = (uint64_t)sqlite3_column_int64(stmt, 9); const uint8_t* sgn = (const uint8_t*)sqlite3_column_blob(stmt, 10); int src = sqlite3_column_int(stmt, 11); - if (!x25 || !ed || src != 0) continue; + if (src != 0) continue; + if (!member_blob(stmt, 1, 32, 0) || !member_blob(stmt, 2, 32, 0) || !member_blob(stmt, 3, 64, 1) + || !member_blob(stmt, 5, 64, 1) || !member_blob(stmt, 8, 64, 1) || !member_blob(stmt, 10, 64, 1)) { + DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: invalid hash record ns=%s node=%016llx", MS_ID, ns, (unsigned long long)nid); + sqlite3_finalize(stmt); return -1; + } uint8_t mh[MT_HASH_SIZE]; - _compute_member_hash(nid, x25, ed, sig, jts, usig, uts, atags, atsig, sb, sgn, mh); - EVP_DigestUpdate(sha_ctx, mh, MT_HASH_SIZE); + const char* userinfo = (const char*)sqlite3_column_text(stmt, 12); + if (_compute_member_hash(nid, x25, ed, sig, jts, usig, uts, userinfo, atags, atsig, sb, sgn, mh) < 0 + || EVP_DigestUpdate(sha_ctx, mh, MT_HASH_SIZE) != 1) { + DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: leaf digest failed ns=%s", MS_ID, ns); + sqlite3_finalize(stmt); return -1; + } count++; } sqlite3_finalize(stmt); + if (step != SQLITE_DONE) { + DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: leaf read failed ns=%s: %s", MS_ID, ns, sqlite3_errmsg(db)); return -1; + } return count; } -static int _member_get_items(void* ctx, const char* ns, uint8_t level, - uint64_t prefix, uint8_t prefix_bytes, - uint8_t* buf, size_t* len) { +static int _member_get_page(void* ctx, const char* ns, uint64_t prefix, int after_valid, uint64_t after, + uint8_t* buf, size_t* len, uint64_t* next, int* more) { struct UTUN_INSTANCE* inst = (struct UTUN_INSTANCE*)ctx; sqlite3* db = _db(inst); if (!db || !buf || !len) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: get_items — invalid args db=%p buf=%p len=%p", MS_ID, (void*)db, (void*)buf, (void*)len); return -1; } - if (level == 0) { *len = 0; return 0; } + const uint8_t level = MT_MAX_LEVEL; + *next = 0; *more = 0; DEBUG_TRACE(DEBUG_CATEGORY_MEMBER_SYNC, "%s: get_items ns=%s L%d/P%016llx", MS_ID, ns, level, (unsigned long long)prefix); char peers_tbl[128]; _peers_table(ns, peers_tbl, sizeof(peers_tbl)); @@ -173,17 +207,20 @@ static int _member_get_items(void* ctx, const char* ns, uint8_t level, " join_sig, join_ts, update_sig, update_ts, userinfo, adm_tags, adm_tags_sig," " signed_by, signature, source" " FROM \"%s\"" - " WHERE (node_id & %lld) == %lld ORDER BY node_id ASC", + " WHERE (node_id & %lld) == %lld AND (?=0 OR node_id>?) ORDER BY node_id ASC", peers_tbl, (long long)mask, (long long)prefix); sqlite3_stmt* stmt = NULL; if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: get_items prepare failed ns=%s", MS_ID, ns); return -1; } + sqlite3_bind_int(stmt, 1, after_valid); + sqlite3_bind_int64(stmt, 2, (sqlite3_int64)after); size_t off = 0; if (off + 2 > *len) { sqlite3_finalize(stmt); return -2; } - uint16_t* cnt = (uint16_t*)(buf + off); off += 2; *cnt = 0; + uint16_t cnt = 0; off += 2; - while (sqlite3_step(stmt) == SQLITE_ROW) { + int step; + while ((step = sqlite3_step(stmt)) == SQLITE_ROW) { uint64_t nid = (uint64_t)sqlite3_column_int64(stmt, 0); const uint8_t* x25 = (const uint8_t*)sqlite3_column_blob(stmt, 1); const uint8_t* ed = (const uint8_t*)sqlite3_column_blob(stmt, 2); @@ -197,24 +234,34 @@ static int _member_get_items(void* ctx, const char* ns, uint8_t level, uint64_t sb = (uint64_t)sqlite3_column_int64(stmt, 10); const uint8_t* sgn = (const uint8_t*)sqlite3_column_blob(stmt, 11); int src = sqlite3_column_int(stmt, 12); - if (!x25 || !ed || src != 0) continue; + if (src != 0) continue; + if (!member_blob(stmt, 1, 32, 0) || !member_blob(stmt, 2, 32, 0) || !member_blob(stmt, 3, 64, 1) + || !member_blob(stmt, 5, 64, 1) || !member_blob(stmt, 9, 64, 1) || !member_blob(stmt, 11, 64, 1) + || (nm && strlen(nm) > 255) || (atags && strlen(atags) > 255)) { + DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: invalid page record ns=%s node=%016llx", MS_ID, ns, (unsigned long long)nid); + sqlite3_finalize(stmt); return -1; + } uint8_t nl = nm ? (uint8_t)strnlen(nm, 255) : 0; uint8_t atl = atags ? (uint8_t)strnlen(atags, 255) : 0; uint8_t flags = (sig && jts) ? PEERS_FLAG_HAS_JOIN : 0; size_t need = 8 + 32 + 32 + 1 + (flags ? 72ULL : 0ULL) + 64 + 8 + 1 + (size_t)nl + 1 + (size_t)atl + 64 + 8 + 64; - if (off + need > *len) { sqlite3_finalize(stmt); return -2; } - memcpy(buf + off, &nid, 8); off += 8; + if (off + need > *len) { + if (!cnt) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: record exceeds page capacity ns=%s", MS_ID, ns); sqlite3_finalize(stmt); return -1; } + *more = 1; + break; + } + member_write64(buf + off, nid); off += 8; memcpy(buf + off, x25, 32); off += 32; memcpy(buf + off, ed, 32); off += 32; buf[off++] = flags; if (flags & PEERS_FLAG_HAS_JOIN) { memcpy(buf + off, sig, 64); off += 64; - memcpy(buf + off, &jts, 8); off += 8; + member_write64(buf + off, jts); off += 8; } if (usig && uts) { memcpy(buf + off, usig, 64); off += 64; - memcpy(buf + off, &uts, 8); off += 8; + member_write64(buf + off, uts); off += 8; } else { memset(buf + off, 0, 64); off += 64; uint64_t z = 0; memcpy(buf + off, &z, 8); off += 8; @@ -225,15 +272,18 @@ static int _member_get_items(void* ctx, const char* ns, uint8_t level, if (atl) { memcpy(buf + off, atags, atl); off += atl; } if (atsig && sqlite3_column_bytes(stmt, 9) >= 64) { memcpy(buf + off, atsig, 64); off += 64; } else { memset(buf + off, 0, 64); off += 64; } - memcpy(buf + off, &sb, 8); off += 8; + member_write64(buf + off, sb); off += 8; if (sgn && sqlite3_column_bytes(stmt, 11) >= 64) { memcpy(buf + off, sgn, 64); off += 64; } else { memset(buf + off, 0, 64); off += 64; } - (*cnt)++; + cnt++; + *next = nid; } + if (step != SQLITE_DONE && !*more) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: page read failed ns=%s: %s", MS_ID, ns, sqlite3_errmsg(db)); sqlite3_finalize(stmt); return -1; } sqlite3_finalize(stmt); + buf[0] = (uint8_t)(cnt >> 8); buf[1] = (uint8_t)cnt; *len = off; DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "%s: get_items ns=%s L%d/P%016llx mask=%016llx count=%u off=%zu", - MS_ID, ns, level, (unsigned long long)prefix, (unsigned long long)mask, *cnt, off); + MS_ID, ns, level, (unsigned long long)prefix, (unsigned long long)mask, cnt, off); return 0; } @@ -416,7 +466,7 @@ int member_sync_validate_pubkey(struct UTUN_INSTANCE* inst, const char* ch_id, return topo_node_sqlite_member_in_channel(db, ch_id, node_id); } -int member_sync_apply_record(struct UTUN_INSTANCE* inst, const char* ch_id, +static int member_apply_record(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t from_peer, const struct ms_member_rec* m) { if (!inst || !ch_id || !m || !m->x25519 || !m->ed25519) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: apply_record — invalid args", MS_ID); @@ -443,9 +493,12 @@ int member_sync_apply_record(struct UTUN_INSTANCE* inst, const char* ch_id, sqlite3_stmt* st = NULL; char sql[512]; snprintf(sql, sizeof(sql), "SELECT join_sig, join_ts, update_ts, adm_tags, signed_by, signature, source FROM \"%s\" WHERE node_id=?", peers_tbl); - if (sqlite3_prepare_v2(db, sql, -1, &st, NULL) == SQLITE_OK) { - sqlite3_bind_int64(st, 1, (sqlite3_int64)m->node_id); - if (sqlite3_step(st) == SQLITE_ROW) { + int rc = sqlite3_prepare_v2(db, sql, -1, &st, NULL); + if (rc == SQLITE_OK) rc = sqlite3_bind_int64(st, 1, (sqlite3_int64)m->node_id); + if (rc == SQLITE_OK) { + rc = sqlite3_step(st); + if (rc == SQLITE_ROW && (!member_blob(st, 0, 64, 1) || !member_blob(st, 5, 64, 1))) rc = SQLITE_CORRUPT; + if (rc == SQLITE_ROW) { has_local = 1; const uint8_t* js = (const uint8_t*)sqlite3_column_blob(st, 0); if (js) memcpy(local_join_sig, js, 64); @@ -458,7 +511,11 @@ int member_sync_apply_record(struct UTUN_INSTANCE* inst, const char* ch_id, if (ls && sqlite3_column_bytes(st, 5) >= 64) memcpy(local_signature, ls, 64); local_src = sqlite3_column_int(st, 6); } - sqlite3_finalize(st); + } + sqlite3_finalize(st); + if (rc != SQLITE_ROW && rc != SQLITE_DONE) { + DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: local record read failed ns=%s node=%016llx rc=%d: %s", + MS_ID, ch_id, (unsigned long long)m->node_id, rc, sqlite3_errmsg(db)); return MT_ERR_IO; } } @@ -578,6 +635,12 @@ int member_sync_apply_record(struct UTUN_INSTANCE* inst, const char* ch_id, DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: adm_tags change on placeholder member (source=%d) — ignored by merkle, won't propagate nid=0x%016llx ns=%s", MS_ID, local_src, (unsigned long long)m->node_id, ch_id); + if (!identity_ok || !block_a_ok || (!is_local && m->adm_tags && m->adm_tags[0] && !block_b_ok)) { + DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: rejected record ns=%s node=%016llx identity=%d member=%d owner=%d", + MS_ID, ch_id, (unsigned long long)m->node_id, identity_ok, block_a_ok, block_b_ok); + return MT_ERR_DATA; + } + /* ── запись изменённых блоков ── */ if (block_a_changed) { const uint8_t* jsig = m->join_sig ? m->join_sig : (has_local ? local_join_sig : NULL); @@ -594,9 +657,12 @@ int member_sync_apply_record(struct UTUN_INSTANCE* inst, const char* ch_id, /* узел в nodes пишет только merkle-путь (verified identity) */ char node_name[128] = ""; if (m->userinfo) json_flat_get(m->userinfo, "name", node_name, sizeof(node_name)); - topo_node_sqlite_node_update_verified(db, m->node_id, node_name, m->x25519, m->ed25519, + if (topo_node_sqlite_node_update_verified(db, m->node_id, node_name, m->x25519, m->ed25519, m->update_ts ? m->update_ts : m->join_ts, - ntp_time_get_seconds(inst)); + ntp_time_get_seconds(inst)) < 0) { + DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: node update failed ns=%s node=%016llx", MS_ID, ch_id, + (unsigned long long)m->node_id); return MT_ERR_IO; + } } if (block_b_changed) { if (topo_node_sqlite_member_owner_put(db, ch_id, m->node_id, m->adm_tags, m->adm_tags_sig, adm_storage) != 0) { @@ -609,8 +675,10 @@ int member_sync_apply_record(struct UTUN_INSTANCE* inst, const char* ch_id, if (block_a_stale || block_b_stale) stale = 1; /* пересчёт классификации (node_type/storage) — DB-хелпер, читает node_addresses (адреса BGP) */ - if (changed) - topo_node_sqlite_nodeinfo_updated(db, m->node_id); + if (changed && topo_node_sqlite_nodeinfo_updated(db, m->node_id) < 0) { + DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: classification update failed ns=%s node=%016llx", MS_ID, ch_id, + (unsigned long long)m->node_id); return MT_ERR_IO; + } /* ── recompute и подтвердить реальное изменение по хешу ── */ if (changed) { @@ -619,21 +687,39 @@ int member_sync_apply_record(struct UTUN_INSTANCE* inst, const char* ch_id, if (rc == 0) changed = 0; /* запись была no-op (данные идентичны) */ } - /* ── пассивное добавление: если узел уже подключён напрямую — добавить в CHAT-группу ── */ - if (changed && inst->topo_groups) { - uint64_t gid = strtoull(ch_id, NULL, 10); - struct TOPO_GROUP* g = topo_groups_find(inst->topo_groups, gid); - if (g && g->group_type == TOPO_GROUP_TYPE_CHAT) { - struct ETCP_CONN* c = instance_find_conn(inst, m->node_id); - if (c) topo_group_new_conn(g, c); - } - } - DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "%s: apply_record ch=%s nid=0x%016llx → changed=%d stale=%d", MS_ID, ch_id, (unsigned long long)m->node_id, changed, stale); return (changed ? MS_APPLY_CHANGED : 0) | (stale ? MS_APPLY_STALE : 0); } +static void member_committed(struct UTUN_INSTANCE* inst, const char* ns, uint64_t node_id) { + merkle_sync_changed(inst, ns); + struct TOPO_GROUP* g = inst->topo_groups ? topo_groups_find(inst->topo_groups, strtoull(ns, NULL, 10)) : NULL; + struct ETCP_CONN* conn = instance_find_conn(inst, node_id); + if (g && g->group_type == TOPO_GROUP_TYPE_CHAT && conn) topo_group_new_conn(g, conn); +} + +int member_sync_apply_record(struct UTUN_INSTANCE* inst, const char* ch_id, + uint64_t from_peer, const struct ms_member_rec* m) { + sqlite3* db = _db(inst); + if (!db || !ch_id || !m) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: invalid apply args", MS_ID); return -1; } + int own = sqlite3_get_autocommit(db); + if (sqlite3_exec(db, "SAVEPOINT member_record", NULL, NULL, NULL) != SQLITE_OK) { + DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: record begin: %s", MS_ID, sqlite3_errmsg(db)); return -1; + } + int rc = member_apply_record(inst, ch_id, from_peer, m); + if (rc < 0 && sqlite3_exec(db, "ROLLBACK TO member_record", NULL, NULL, NULL) != SQLITE_OK) + DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: record rollback: %s", MS_ID, sqlite3_errmsg(db)); + if (sqlite3_exec(db, "RELEASE member_record", NULL, NULL, NULL) != SQLITE_OK) { + DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: record commit: %s", MS_ID, sqlite3_errmsg(db)); + if (sqlite3_exec(db, "ROLLBACK TO member_record; RELEASE member_record", NULL, NULL, NULL) != SQLITE_OK) + DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: record cleanup: %s", MS_ID, sqlite3_errmsg(db)); + return -1; + } + if (own && rc >= 0 && (rc & MS_APPLY_CHANGED)) member_committed(inst, ch_id, m->node_id); + return rc; +} + /* ── Верификация локальной записи при чтении из БД ── Единственный сценарий удаления мембера: подпись присутствует и невалидна, либо node_id != derive(x25519). Отсутствие подписи = «не верифицировано», не удаляем. */ @@ -789,74 +875,102 @@ void member_sync_verify_and_purge_all(struct UTUN_INSTANCE* inst) { sqlite3_finalize(st); } -static int _member_apply_items(void* ctx, const char* ns, uint64_t from_peer, - const uint8_t* data, size_t len) { - struct UTUN_INSTANCE* inst = (struct UTUN_INSTANCE*)ctx; - if (len < 2) { DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "%s: apply_items — no items from peer=%016llx ns=%.*s (empty, sync complete)", MS_ID, (unsigned long long)from_peer, (int)strnlen(ns, 32), ns); return 0; } - - uint16_t count; memcpy(&count, data, 2); - DEBUG_TRACE(DEBUG_CATEGORY_MEMBER_SYNC, "%s: apply_items ns=%s count=%u from=%016llx", MS_ID, ns, count, (unsigned long long)from_peer); - const uint8_t* mp = data + 2; size_t mrem = len - 2; - sqlite3* db = _db(inst); - - int changed_count = 0; - - for (uint16_t i = 0; i < count && mrem >= 73; i++) { - uint64_t nid; memcpy(&nid, mp, 8); mp += 8; mrem -= 8; - const uint8_t* x25 = mp; mp += 32; mrem -= 32; - const uint8_t* ed = mp; mp += 32; mrem -= 32; - uint8_t flags = *mp++; mrem--; - const uint8_t* jsig = NULL; uint64_t jts = 0; - if (flags & PEERS_FLAG_HAS_JOIN) { - if (mrem < 72) break; jsig = mp; mp += 64; mrem -= 64; memcpy(&jts, mp, 8); mp += 8; mrem -= 8; - } else { - /* lookup join from local DB */ - uint8_t buf_jsig[64]; uint64_t buf_jts; - if (db && topo_node_sqlite_member_get_join(db, ns, nid, buf_jsig, &buf_jts) == 0) { jsig = buf_jsig; jts = buf_jts; } - } - if (mrem < 72) break; - const uint8_t* usig = mp; mp += 64; mrem -= 64; uint64_t uts; memcpy(&uts, mp, 8); mp += 8; mrem -= 8; - uint8_t nl = *mp++; mrem--; - char nm[256] = ""; - if (nl && mrem >= nl) { memcpy(nm, mp, nl); nm[nl] = '\0'; mp += nl; mrem -= nl; } - if (mrem < 1) break; - uint8_t atl = *mp++; mrem--; - char atags[256] = ""; - const uint8_t* atsig = NULL; - if (atl && mrem >= atl) { memcpy(atags, mp, atl); atags[atl] = '\0'; mp += atl; mrem -= atl; } - if (mrem >= 64) { atsig = mp; mp += 64; mrem -= 64; } - uint64_t sb = 0; const uint8_t* sgn = NULL; - if (mrem >= 72) { memcpy(&sb, mp, 8); mp += 8; mrem -= 8; sgn = mp; mp += 64; mrem -= 64; } +struct member_wire { + struct ms_member_rec rec; + char userinfo[256], tags[256]; + uint8_t local_join[64]; +}; - struct ms_member_rec m; - memset(&m, 0, sizeof(m)); - m.node_id = nid; - m.x25519 = x25; m.ed25519 = ed; - m.join_sig = jsig; m.join_ts = jts; - m.update_sig = usig; m.update_ts = uts; - m.userinfo = nm; - m.adm_tags = atags[0] ? atags : NULL; - m.adm_tags_sig = atsig; - m.signed_by = sb; m.signature = sgn; +/* Одна строгая раскладка для проверки и применения. Нельзя молча принимать усечённую запись. */ +static int member_decode(const uint8_t* p, size_t len, struct member_wire* w, size_t* used) { + memset(w, 0, sizeof(*w)); + if (len < 73) return -1; + struct ms_member_rec* m = &w->rec; + size_t off = 0; + m->node_id = member_read64(p + off); off += 8; + m->x25519 = p + off; off += 32; m->ed25519 = p + off; off += 32; + uint8_t flags = p[off++]; + if (flags & ~PEERS_FLAG_HAS_JOIN) return -1; + if (flags & PEERS_FLAG_HAS_JOIN) { + if (len - off < 72) return -1; + m->join_sig = p + off; off += 64; m->join_ts = member_read64(p + off); off += 8; + } + if (len - off < 73) return -1; + m->update_sig = p + off; off += 64; m->update_ts = member_read64(p + off); off += 8; + uint8_t n = p[off++]; + if (len - off < (size_t)n + 1 || memchr(p + off, 0, n)) return -1; + memcpy(w->userinfo, p + off, n); off += n; + n = p[off++]; + if (len - off < (size_t)n + 136 || memchr(p + off, 0, n)) return -1; + memcpy(w->tags, p + off, n); off += n; + m->adm_tags_sig = p + off; off += 64; + m->signed_by = member_read64(p + off); off += 8; + m->signature = p + off; off += 64; + m->userinfo = w->userinfo; m->adm_tags = w->tags; + *used = off; + return 0; +} - int r = member_sync_apply_record(inst, ns, from_peer, &m); - DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "%s: member_sync recv ns=%s nid=0x%016llx r=%d", MS_ID, ns, (unsigned long long)nid, r); - if (r < 0) continue; - if (r & MS_APPLY_CHANGED) { - changed_count++; - /* fire node_props_changed callbacks */ - _fire_props_changed(inst, nid, atags[0] ? atags : NULL, ns); - /* fire apply callbacks (ready-хендлинг join на connection-узле) */ - _fire_apply_cbk(inst, ns, nid, x25, sb, sgn); +static int _member_apply_items(void* ctx, const char* ns, uint64_t from_peer, const uint8_t* data, size_t len) { + struct UTUN_INSTANCE* inst = ctx; + sqlite3* db = _db(inst); + if (!db || len < 2) { + DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: invalid member page ns=%s bytes=%zu", MS_ID, ns, len); return MT_ERR_DATA; + } + uint16_t count = ((uint16_t)data[0] << 8) | data[1]; + if (count > (len - 2) / 283) { + DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: impossible page count=%u bytes=%zu", MS_ID, count, len); return MT_ERR_DATA; + } + size_t off = 2; + for (unsigned i = 0; i < count; i++) { + struct member_wire w; size_t used; + if (member_decode(data + off, len - off, &w, &used) < 0) { + DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: malformed member page ns=%s record=%u offset=%zu", MS_ID, ns, i, off); + return MT_ERR_DATA; } - if (r & MS_APPLY_STALE) { - /* у нас версия новее — отправить наш полный рекорд автору */ - member_sync_send_to(inst, ns, nid, from_peer); + off += used; + } + if (off != len) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: trailing page bytes ns=%s bytes=%zu", MS_ID, ns, len - off); return MT_ERR_DATA; } + if (!count) return 0; + uint8_t* changed = u_calloc(count, 1); + if (!changed) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: page allocation failed", MS_ID); return MT_ERR_IO; } + if (sqlite3_exec(db, "SAVEPOINT member_page", NULL, NULL, NULL) != SQLITE_OK) { + DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: page begin: %s", MS_ID, sqlite3_errmsg(db)); u_free(changed); return MT_ERR_IO; + } + int rc = 0; + off = 2; + for (unsigned i = 0; i < count; i++) { + struct member_wire w; size_t used; + if (member_decode(data + off, len - off, &w, &used) < 0) { rc = MT_ERR_DATA; break; } + if (!w.rec.join_sig && topo_node_sqlite_member_get_join(db, ns, w.rec.node_id, w.local_join, &w.rec.join_ts) == 0) + w.rec.join_sig = w.local_join; + int r = member_sync_apply_record(inst, ns, from_peer, &w.rec); + if (r < 0) { rc = r; break; } + changed[i] = (r & MS_APPLY_CHANGED) != 0; + off += used; + } + if (rc < 0 && sqlite3_exec(db, "ROLLBACK TO member_page", NULL, NULL, NULL) != SQLITE_OK) + DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: page rollback: %s", MS_ID, sqlite3_errmsg(db)); + if (sqlite3_exec(db, "RELEASE member_page", NULL, NULL, NULL) != SQLITE_OK) { + DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: page commit: %s", MS_ID, sqlite3_errmsg(db)); rc = MT_ERR_IO; + if (sqlite3_exec(db, "ROLLBACK TO member_page; RELEASE member_page", NULL, NULL, NULL) != SQLITE_OK) + DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: page cleanup: %s", MS_ID, sqlite3_errmsg(db)); + } + if (rc == 0) { + off = 2; + for (unsigned i = 0; i < count; i++) { + struct member_wire w; size_t used; + if (member_decode(data + off, len - off, &w, &used) < 0) { rc = MT_ERR_DATA; break; } + if (changed[i]) { + member_committed(inst, ns, w.rec.node_id); + _fire_props_changed(inst, w.rec.node_id, w.tags[0] ? w.tags : NULL, ns); + _fire_apply_cbk(inst, ns, w.rec.node_id, w.rec.x25519, w.rec.signed_by, w.rec.signature); + } + off += used; } } - if (changed_count > 0) - merkle_sync_broadcast(inst, ns, from_peer, data, len); - return 0; + u_free(changed); + return rc; } static int _member_validate_peer(void* ctx, const char* ns, uint64_t peer) { @@ -866,45 +980,83 @@ static int _member_validate_peer(void* ctx, const char* ns, uint64_t peer) { return topo_node_sqlite_member_in_channel(db, ns, peer); } +static int member_finish(void* ctx, const char* ns) { + sqlite3* db = _db(ctx); + char table[128], sql[320]; _peers_table(ns, table, sizeof(table)); + snprintf(sql, sizeof(sql), "SELECT node_id,x25519_pubkey,signed_by,signature FROM \"%s\" WHERE source=0", table); + sqlite3_stmt* st = NULL; + int rc = sqlite3_prepare_v2(db, sql, -1, &st, NULL); + if (rc != SQLITE_OK) { + DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: finish query ns=%s: %s", MS_ID, ns, sqlite3_errmsg(db)); return -1; + } + while ((rc = sqlite3_step(st)) == SQLITE_ROW) { + struct ms_member_rec m = {0}; + m.node_id = (uint64_t)sqlite3_column_int64(st, 0); + m.x25519 = sqlite3_column_blob(st, 1); + m.signed_by = (uint64_t)sqlite3_column_int64(st, 2); + m.signature = sqlite3_column_blob(st, 3); + if (!m.signature || sqlite3_column_bytes(st, 3) == 0) continue; + if (sqlite3_column_bytes(st, 1) != 32 || sqlite3_column_bytes(st, 3) != 64) { rc = SQLITE_CORRUPT; break; } + if (_sig_is_zero64(m.signature)) continue; + int verified = member_sync_verify_signature(db, ns, &m); + if (verified != 1) { + DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: cannot confirm ns=%s node=%016llx signer=%016llx verification=%d", + MS_ID, ns, (unsigned long long)m.node_id, (unsigned long long)m.signed_by, verified); + rc = SQLITE_CONSTRAINT; break; + } + } + sqlite3_finalize(st); + if (rc != SQLITE_DONE) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: finish failed ns=%s rc=%d", MS_ID, ns, rc); return -1; } + return 0; +} + static const struct merkle_sync_data_ops g_member_ops = { .update_bucket_hash = _member_update_bucket_hash, - .get_items = _member_get_items, + .get_page = _member_get_page, .apply_items = _member_apply_items, - .apply_update = NULL, /* online-статус больше не рассылается через merkle */ .validate_peer = _member_validate_peer, + .finish = member_finish, }; /* ── Public API ── */ -static void _rebuild_all_trees(struct UTUN_INSTANCE* inst) { - sqlite3* db = _db(inst); if (!db) return; - sqlite3_stmt* st = NULL; - if (sqlite3_prepare_v2(db, - "SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'peers_%'", - -1, &st, NULL) != SQLITE_OK) return; - while (sqlite3_step(st) == SQLITE_ROW) { - const char* tbl = (const char*)sqlite3_column_text(st, 0); - if (!tbl || strncmp(tbl, "peers_", 6) != 0) continue; - const char* ch_id = tbl + 6; - /* полный пересчёт: снести всё дерево канала, затем собрать заново из peers */ - sqlite3_stmt* del = NULL; - if (sqlite3_prepare_v2(db, "DELETE FROM merkle_tree_hash WHERE namespace=?", -1, &del, NULL) == SQLITE_OK) { - sqlite3_bind_text(del, 1, ch_id, -1, SQLITE_STATIC); - sqlite3_step(del); - sqlite3_finalize(del); - } - sqlite3_stmt* ns = NULL; - char sql[256]; snprintf(sql, sizeof(sql), "SELECT node_id FROM \"%s\"", tbl); - if (sqlite3_prepare_v2(db, sql, -1, &ns, NULL) == SQLITE_OK) { - while (sqlite3_step(ns) == SQLITE_ROW) { - uint64_t nid = (uint64_t)sqlite3_column_int64(ns, 0); - merkle_sync_recompute_path(inst, ch_id, nid); - DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "%s: rebuild tree ns=%s nid=%016llx", MS_ID, ch_id, (unsigned long long)nid); +static int _rebuild_all_trees(struct UTUN_INSTANCE* inst) { + sqlite3* db = _db(inst); + sqlite3_stmt* tables = NULL; + int rc = sqlite3_prepare_v2(db, "SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'peers_%'", -1, &tables, NULL); + if (rc != SQLITE_OK) goto fail; + if (sqlite3_exec(db, "SAVEPOINT member_rebuild", NULL, NULL, NULL) != SQLITE_OK) goto fail; + while ((rc = sqlite3_step(tables)) == SQLITE_ROW) { + const char* table = (const char*)sqlite3_column_text(tables, 0); + if (!table || strncmp(table, "peers_", 6)) continue; + const char* ns = table + 6; + uint64_t mask = merkle_sync_level_prefix(UINT64_MAX, MT_MAX_LEVEL); + char sql[256]; + snprintf(sql, sizeof(sql), "SELECT DISTINCT (node_id & %lld) FROM \"%s\" WHERE source=0", (long long)mask, table); + sqlite3_stmt* leaves = NULL; + int lr = sqlite3_prepare_v2(db, sql, -1, &leaves, NULL); + if (lr == SQLITE_OK) { + while ((lr = sqlite3_step(leaves)) == SQLITE_ROW) { + uint64_t key = (uint64_t)sqlite3_column_int64(leaves, 0); + if (merkle_sync_recompute_path(inst, ns, key) < 0) { lr = SQLITE_ERROR; break; } } - sqlite3_finalize(ns); } + sqlite3_finalize(leaves); + if (lr != SQLITE_DONE) { rc = lr; break; } } - sqlite3_finalize(st); + if (rc != SQLITE_DONE) { + if (sqlite3_exec(db, "ROLLBACK TO member_rebuild", NULL, NULL, NULL) != SQLITE_OK) + DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: rebuild rollback failed: %s", MS_ID, sqlite3_errmsg(db)); + } + int release = sqlite3_exec(db, "RELEASE member_rebuild", NULL, NULL, NULL); + sqlite3_finalize(tables); tables = NULL; + if (rc != SQLITE_DONE || release != SQLITE_OK) goto fail; + DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: trees rebuilt from distinct leaves", MS_ID); + return 0; +fail: + sqlite3_finalize(tables); + DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: tree rebuild failed: %s", MS_ID, db ? sqlite3_errmsg(db) : "no DB"); + return -1; } /* Подписаться на BGP node-события chat-группы (BGP → member_sync → node_props_changed). */ @@ -920,7 +1072,7 @@ int member_sync_init(struct UTUN_INSTANCE* inst) { if (rc != 0) return rc; /* очистка битых записей во всех каналах перед построением дерева */ member_sync_verify_and_purge_all(inst); - _rebuild_all_trees(inst); + if (_rebuild_all_trees(inst) < 0) { merkle_sync_destroy(inst); return -1; } /* подписка на BGP-события существующих chat-групп */ if (inst->topo_groups && inst->topo_groups->group_list) { struct ll_entry* ge = inst->topo_groups->group_list->head; @@ -972,85 +1124,6 @@ void member_sync_cancel_channel(struct UTUN_INSTANCE* inst, const char* ch_id) { merkle_sync_cancel_ns(inst, ch_id); } -/* Сериализует одного мембера в wire-формат [count:2][member...]. - Возвращает 0 и *out_len, или -1 если мембер не найден/нет БД. */ -static int _serialize_member(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t member_id, - uint8_t* buf, size_t buf_sz, size_t* out_len) { - if (!inst || !ch_id || !buf || !out_len) return -1; - sqlite3* db = _db(inst); if (!db) return -1; - char peers_tbl[128]; _peers_table(ch_id, peers_tbl, sizeof(peers_tbl)); - - sqlite3_stmt* stmt = NULL; - char sql[512]; snprintf(sql, sizeof(sql), - "SELECT node_id, x25519_pubkey, ed25519_pubkey," - " join_sig, join_ts, update_sig, update_ts, userinfo, adm_tags, adm_tags_sig," - " signed_by, signature" - " FROM \"%s\" WHERE node_id=?", peers_tbl); - if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) { - DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: serialize_member — query failed ch=%s nid=0x%016llx", MS_ID, ch_id, (unsigned long long)member_id); - return -1; - } - sqlite3_bind_int64(stmt, 1, (sqlite3_int64)member_id); - if (sqlite3_step(stmt) != SQLITE_ROW) { sqlite3_finalize(stmt); return -1; } - - uint64_t nid = (uint64_t)sqlite3_column_int64(stmt, 0); - const uint8_t* x25 = (const uint8_t*)sqlite3_column_blob(stmt, 1); - const uint8_t* ed = (const uint8_t*)sqlite3_column_blob(stmt, 2); - const uint8_t* sig = (const uint8_t*)sqlite3_column_blob(stmt, 3); - uint64_t jts = (uint64_t)sqlite3_column_int64(stmt, 4); - const uint8_t* usig = (const uint8_t*)sqlite3_column_blob(stmt, 5); - uint64_t uts = (uint64_t)sqlite3_column_int64(stmt, 6); - const char* nm = (const char*)sqlite3_column_text(stmt, 7); - const char* atags = (const char*)sqlite3_column_text(stmt, 8); - const uint8_t* atsig = (const uint8_t*)sqlite3_column_blob(stmt, 9); - uint64_t sb = (uint64_t)sqlite3_column_int64(stmt, 10); - const uint8_t* sgn = (const uint8_t*)sqlite3_column_blob(stmt, 11); - if (!x25 || !ed) { sqlite3_finalize(stmt); return -1; } - uint8_t nl = nm ? (uint8_t)strnlen(nm, 255) : 0; - uint8_t atl = atags ? (uint8_t)strnlen(atags, 255) : 0; - - uint8_t flags = (sig && jts) ? PEERS_FLAG_HAS_JOIN : 0; - size_t need = 2 + 8 + 32 + 32 + 1 + (flags & PEERS_FLAG_HAS_JOIN ? 72 : 0) - + 72 + 1 + (size_t)nl + 1 + (size_t)atl + 64 + 8 + 64; - if (need > buf_sz) { sqlite3_finalize(stmt); return -1; } - size_t off = 0; - uint16_t wcnt = 1; memcpy(buf, &wcnt, 2); off += 2; - memcpy(buf + off, &nid, 8); off += 8; - memcpy(buf + off, x25, 32); off += 32; - memcpy(buf + off, ed, 32); off += 32; - buf[off++] = flags; - if (flags & PEERS_FLAG_HAS_JOIN) { memcpy(buf + off, sig, 64); off += 64; memcpy(buf + off, &jts, 8); off += 8; } - if (usig && uts) { memcpy(buf + off, usig, 64); off += 64; memcpy(buf + off, &uts, 8); off += 8; } - else { memset(buf + off, 0, 72); off += 72; } - buf[off++] = nl; if (nl) { memcpy(buf + off, nm, nl); off += nl; } - buf[off++] = atl; if (atl) { memcpy(buf + off, atags, atl); off += atl; } - if (atsig && sqlite3_column_bytes(stmt, 9) >= 64) { memcpy(buf + off, atsig, 64); } - else { memset(buf + off, 0, 64); } - off += 64; - memcpy(buf + off, &sb, 8); off += 8; - if (sgn && sqlite3_column_bytes(stmt, 11) >= 64) { memcpy(buf + off, sgn, 64); } - else { memset(buf + off, 0, 64); } - off += 64; - sqlite3_finalize(stmt); - - *out_len = off; - return 0; -} - -void member_sync_broadcast_one(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t member_id) { - uint8_t buf[8192]; size_t off = 0; - if (_serialize_member(inst, ch_id, member_id, buf, sizeof(buf), &off) != 0) return; - merkle_sync_broadcast(inst, ch_id, inst->node_id, buf, off); - DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: broadcast_one ch=%s nid=0x%016llx size=%zu", - MS_ID, ch_id, (unsigned long long)member_id, off); -} - -int member_sync_send_to(struct UTUN_INSTANCE* inst, const char* ch_id, - uint64_t member_id, uint64_t target_peer) { - uint8_t buf[8192]; size_t off = 0; - if (_serialize_member(inst, ch_id, member_id, buf, sizeof(buf), &off) != 0) return -1; - return merkle_sync_send_to(inst, ch_id, target_peer, buf, off); -} int member_sync_put(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t member_id, const uint8_t* x25519, @@ -1088,8 +1161,3 @@ int member_sync_count(struct UTUN_INSTANCE* inst, const char* ch_id) { sqlite3_finalize(stmt); return c; } - -const uint8_t* member_sync_get_hash(struct UTUN_INSTANCE* inst, const char* ch_id, - uint8_t level, uint64_t prefix64) { - return merkle_sync_get_hash(inst, ch_id, level, prefix64); -} diff --git a/src/chat/member_sync.h b/src/chat/member_sync.h index 185ee408..0ec6fa4d 100644 --- a/src/chat/member_sync.h +++ b/src/chat/member_sync.h @@ -3,7 +3,7 @@ * * Тонкая прослойка над merkle_sync, адаптированная под мемберов каналов. * Данные хранятся в таблицах: nodes, node_addresses, peers_. - * Хеш мембера = SHA256(node_id || x25519 || ed25519 || join_sig || join_ts || update_sig || update_ts || adm_tags_len || adm_tags || adm_tags_sig). + * Хеш включает все реплицируемые поля, длины строк и целые в big-endian (см. member_sync_doc.md). * * ── Использование ── * @@ -23,7 +23,7 @@ * member_sync_put(inst, ch_id, node_id, x25519, ed25519, * join_sig, join_ts, update_sig, update_ts, userinfo, * adm_tags, adm_tags_sig, storage); - * // дерево автоматически пересчитано + * // запись и дерево изменены атомарно; после commit запускается автоматическая синхронизация * * // отмена синхронизации (коллбэк НЕ вызывается) * member_sync_cancel(inst, peer, ch_id); @@ -52,8 +52,8 @@ struct TOPO_GROUP; struct sqlite3; /* Результат применения рекорда мембера (compare+update по версиям двух блоков). */ -#define MS_APPLY_CHANGED 0x01 /* блок стал новее — обновлён, нужен recompute + relay остальным */ -#define MS_APPLY_STALE 0x02 /* у нас версия новее — отправить наш полный рекорд автору */ +#define MS_APPLY_CHANGED 0x01 /* запись и дерево обновлены */ +#define MS_APPLY_STALE 0x02 /* локальная версия новее; обратный проход доставит её пиру */ /* Распарсенный рекорд мембера (два подписанных блока). */ struct ms_member_rec { @@ -191,7 +191,7 @@ int member_sync_build_join_msg(const uint8_t* ch_x25519, const uint8_t* ch_ed25 * - блок B (владелец): ver = adm_tags.ver, подписан канальным ключом (adm_tags_sig). * Каждый блок обновляется независимо, только если его ver увеличился. * Если ver пришедшего блока меньше локального — блок игнорируется и выставляется - * MS_APPLY_STALE (нужно отправить нашу свежую запись автору from_peer). + * MS_APPLY_STALE (локальная версия новее; её доставит обратный проход). * Возвращает битовую маску MS_APPLY_*, <0 — ошибка. */ int member_sync_apply_record(struct UTUN_INSTANCE* inst, const char* ch_id, @@ -212,23 +212,8 @@ int member_sync_verify_and_purge(struct UTUN_INSTANCE* inst, const char* ch_id) /* Прогнать очистку битых записей по всем каналам (при init). */ void member_sync_verify_and_purge_all(struct UTUN_INSTANCE* inst); -/* - * Отправить наш полный рекорд мембера конкретному пиру (send-back при stale). - * Возвращает 0 при успехе, -1 если нет сессии/соединения. - */ -int member_sync_send_to(struct UTUN_INSTANCE* inst, const char* ch_id, - uint64_t member_id, uint64_t target_peer); - -/* Количество мемберов в канале (SELECT COUNT из peers_). */ -int member_sync_count(struct UTUN_INSTANCE* inst, const char* ch_id); - -/* Сериализовать одного мембера и отправить всем подключённым пирам через merkle_sync_broadcast. - Вызывается после локального изменения данных (adm_tags, имя, адреса). */ -void member_sync_broadcast_one(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t member_id); - -/* Получить хеш бакета из merkle_tree_hash (для тестов). */ -const uint8_t* member_sync_get_hash(struct UTUN_INSTANCE* inst, - const char* ch_id, uint8_t level, uint64_t prefix64); +/* Количество мемберов в канале. */ +int member_sync_count(struct UTUN_INSTANCE* inst, const char* ch_id); /* * Коллбэк: изменились adm_tags любого узла (включая себя). diff --git a/src/chat/member_sync_doc.md b/src/chat/member_sync_doc.md index 34552cdf..2ed749b3 100644 --- a/src/chat/member_sync_doc.md +++ b/src/chat/member_sync_doc.md @@ -1,149 +1,60 @@ -# member_sync — синхронизация участников канала +# member_sync — адаптер участников каналов -## 1. Назначение +Поверх [merkle_sync](merkle_sync_doc.md) синхронизирует source=0 записи таблиц peers_. +Транспортный движок не занимается подписями и версиями участников. -Тонкая прослойка над `merkle_sync`, адаптированная под участников (мемберов) чат-каналов. -Хранит мемберов в SQLite (`peers_`, `nodes`, `node_addresses`), пересчитывает Merkle-дерево -при каждом изменении и через merkle_sync рассылает дельту другим узлам. +## Использование -Данные мембера защищены двумя независимо подписанными блоками: -- **Блок A (мембер)** — идентичность и профиль: `node_id`, `x25519`, `ed25519`, `userinfo` (имя), - подпись `update_sig` **ключом самого мембера** (`ed25519`); `ver = update_ts` внутри эпохи `join_ts`. -- **Блок B (владелец)** — атрибуты, управляемые владельцем канала: `adm_tags` (JSON с `ver`, - `storage`), подпись `adm_tags_sig` **канальным ключом** (`ed25519` канала); `ver = adm_tags.ver`. +- member_sync_init вызывается при инициализации chat_sync и перестраивает производный индекс. +- member_sync_start подключает канал к пиру и запрашивает подтверждение совпадения данных. +- member_sync_put изменяет локальную запись, атомарно обновляет индекс и после commit уведомляет движок. +- Прямые broadcast/send_to не нужны: локальная версия возвращается второму узлу при обратном проходе. +- cancel/destroy снимают ожидания; отдельного публичного API рассылки нет. -Оба блока обновляются независимо — только если их `ver` увеличился. Устаревший блок помечается -stale и автору отправляется свежая запись (send-back). +## Записи и применение -## 2. Как пользоваться +У записи два независимо версионируемых блока: участник (join_ts, update_ts) и владелец (adm_tags.ver). +Удаление используется для повреждённых записей. Конфликт одинаковых версий с разным содержимым +не разрешается порядком доставки: отсутствие прогресса завершает Merkle-раунд ошибкой конфликта. -```c -// ── разово (из chat_sync_init) ── -member_sync_init(inst); // merkle_sync_init(0x31) + verify/purge + rebuild + подписка +Входящая страница сначала целиком проверяется на корректность раскладки, длин и строк. +Затем записи применяются под savepoint member_page. Ошибка откатывает всю страницу вместе с индексом. +Уведомления GUI/топологии и changed выполняются после commit. +Каждый member_sync_apply_record также имеет savepoint, поэтому ошибка пересчёта не оставляет запись без индекса. +При внешней транзакции вызывающая сторона отвечает за changed после commit. -// ── подписка на BGP-события канала (BGP → member_sync → node_props_changed) ── -member_sync_subscribe_group(inst, group); // при создании канала +Проверяются привязка node_id к X25519, доверенный Ed25519, подписи блока участника и владельца. +Для дерева приглашений родитель может прийти позже ребёнка; finish повторно проверяет зависимости +перед подтверждением корня. Это не отдельная карантинная таблица: промежуточные записи существуют в БД +до finish, поэтому MT_OK нельзя заменять фактом их присутствия. -// ── запуск синхронизации (из cs_on_conn_up) ── -member_sync_start(inst, peer, ch_id, on_done, ch); +## Канонический хеш -// ── коллбэк завершения ── -static void on_done(uint64_t peer, const char* ch_id, int result, void* arg) { - if (result == MT_OK) ch->synced = CS_SYNC_DONE; -} +SHA256 по полям: +node_id:8, x25519:32, ed25519:32, join_sig:64, join_ts:8, +update_sig:64, update_ts:8, userinfo_len:1, userinfo, +adm_tags_len:1, adm_tags, adm_tags_sig:64, signed_by:8, signature:64. -// ── добавление/обновление мембера (локальное, без верификации подписей) ── -member_sync_put(inst, ch_id, node_id, x25519, ed25519, - join_sig, join_ts, update_sig, update_ts, userinfo, - adm_tags, adm_tags_sig, storage); +Целые big-endian; отсутствующие подписи представлены нулями. +Обе строки ограничены 255 байтами. Хеш включает userinfo и дерево приглашений. +Формат сообщений, подписываемых моделью, определяется build_join_msg/build_update_msg/build_sign_msg отдельно. -// ── онлайн-статус: только локальная запись, возвращает 1 если изменилось ── -if (member_sync_set_online(inst, node_id, 1)) - merkle_sync_push_update(inst, ch_id, node_id, 0x01, (uint8_t[]){1}, 1); // рассылает caller +## Страница на проводе -// ── отмена (коллбэк не вызовется) ── -member_sync_cancel(inst, peer, ch_id); +Начинается с count:2 big-endian. Далее count записей: +node_id:8, x25519:32, ed25519:32, flags:1, +при HAS_JOIN — join_sig:64 и join_ts:8, +update_sig:64, update_ts:8, userinfo_len:1, userinfo, +adm_tags_len:1, adm_tags, adm_tags_sig:64, signed_by:8, signature:64. -// ── количество мемберов ── -int n = member_sync_count(inst, ch_id); +Целые big-endian. Встроенные NUL и хвостовые байты запрещены. +get_page перечисляет только один лист, в возрастающем порядке node_id, строго после курсора. +SQLite использует signed INTEGER; внутри одного листа знак ключей одинаков, поэтому SQL-порядок совпадает с unsigned. +Следующая запись, не помещающаяся в буфер, остаётся для следующей страницы. +Ошибки SQL и повреждённые размеры BLOB возвращаются вызывающему коду. -// ── разово (из chat_sync_destroy) ── -member_sync_destroy(inst); -``` +## Диагностика -## 3. Модель данных - -- **Таблицы**: `peers_` (per-channel мемберы), `nodes`/`node_addresses` (глобальная - идентичность и адреса, обновляются отдельно через `topo_node_sqlite_*`). -- **Хеш мембера** (участвует в Merkle-дереве): - `SHA256(node_id || x25519 || ed25519 || join_sig || join_ts || update_sig || update_ts || adm_tags_len || adm_tags || adm_tags_sig)`. - Поля `userinfo` (имя) и `source` в хеш **не** входят. -- **Колонка `source`**: `source != 0` — плейсхолдер, созданный topo_group (не верифицирован); - такие записи **исключаются** из хеша и сериализации. `source == 0` — обычный верифицированный - меркл-рекорд. - -### Wire-формат мембера (get_items / apply_items / _serialize_member) - -``` -[count:2][node_id:8][x25519:32][ed25519:32][flags:1] - (flags&HAS_JOIN → [join_sig:64][join_ts:8]) - [update_sig:64][update_ts:8] - [userinfo_len:1][userinfo] - [adm_tags_len:1][adm_tags][adm_tags_sig:64] -``` - -`flags = PEERS_FLAG_HAS_JOIN` — `join_sig`/`join_ts` присутствуют (join-запись). - -## 4. apply_record — по-блочное сравнение и применение - -`member_sync_apply_record(inst, ch_id, from_peer, &rec)` — единая точка записи мембера -(и локальная `put`, и приёмная). Возвращает битовую маску `MS_APPLY_CHANGED | MS_APPLY_STALE`. - -1. **Идентичность** (только для приёмных записей): `node_id == sc_derive_node_id_from_pubkey(x25519)` - и `ed25519` совпадает с доверенным из `nodes`. Несовпадение → forgery, блок A отклоняется. -2. **Блок A** — проверка `update_sig` (или `join_sig` для join-only). Локальная запись - (`from_peer == inst->node_id`) доверенная, без верификации. Сравнение `ver`: - - нет локальной записи → changed; - - `join_ts > local_join_ts` → changed (**re-join / смена ключа**); - - `join_ts < local_join_ts` → stale (старая эпоха); - - иначе по `update_ts`. -3. **Блок B** — проверка `adm_tags_sig` канальным ключом, `adm_storage = (storage=="yes")`. - Сравнение `adm_tags.ver` с локальным. -4. **Запись**: changed-блоки пишутся (`member_block_put` / `member_owner_put`), узел обновляется - через `node_update_verified`, пересчитывается классификация `nodeinfo_updated` (из - `node_addresses`, полученных по BGP), затем `merkle_sync_recompute_path`. -5. **Реальная сверка**: если `recompute_path` вернул 0 (данные идентичны) — `changed` сбрасывается - в 0 (no-op), чтобы не рассылать. - -На приёме (`_member_apply_items`) каждый `MS_APPLY_CHANGED` генерирует `node_props_changed` и -включается в relay (`merkle_sync_broadcast`); `MS_APPLY_STALE` вызывает `member_sync_send_to` -(отправить нашу свежую запись автору). - -## 5. Подписки и коллбэки - -- **`member_sync_subscribe_group(inst, group)`** — подписывает на BGP node-события chat-группы - (`topo_group_add_node_cbk`). При событии BGP (`_ms_on_bgp_node`) рассылается `node_props_changed` - с текущим `adm_tags`. REMOVE **не удаляет** мембера. -- **`member_sync_add_props_cbk/remove_props_cbk`** — многоподписочный `node_props_changed_fn` - (изменились `adm_tags` любого узла, включая себя). -- **`member_sync_set_node_updated_cb`** — одиночный `member_sync_node_updated_fn` (online-статус - узла изменился по инициативе удалённого пира, вызывается из uasync-потока). - -## 6. Верификация и удаление - -Единственный механизм удаления мембера — битая запись: -- `member_sync_verify_local_record` — проверяет привязку `node_id = derive(x25519)`, подписи - `update_sig`/`join_sig`/`adm_tags_sig`. Отсутствие подписи = «не верифицировано» → **не удаляем**. -- `member_sync_verify_and_purge` / `_all` — прогоняют записи канала (всех `peers_%`), битые удаляют - через `topo_node_sqlite_member_del` + `recompute_path`. -- Вызываются при `member_sync_init` и при старте синхронизации канала. Отдельного - `member_sync_del` нет — conn_down ≠ удаление мембера. - -## 7. API - -| Функция | Назначение | -|---|---| -| `member_sync_init(inst)` | `merkle_sync_init(0x31, &g_member_ops, inst)` + verify/purge + rebuild всех деревьев + подписка на существующие chat-группы | -| `member_sync_destroy(inst)` | Отписка от групп, `merkle_sync_destroy` | -| `member_sync_subscribe_group(inst, group)` | Подписка на BGP-события CHAT-группы | -| `member_sync_start(inst, peer, ch_id, done_cb, arg)` | `verify_and_purge` + `merkle_sync_start` | -| `member_sync_cancel(inst, peer, ch_id)` | `merkle_sync_cancel` | -| `member_sync_put(...)` | Локальная запись мембера (trusted) → `apply_record` | -| `member_sync_apply_record(inst, ch_id, from_peer, &rec)` | Приём/применение рекорда (по-блочно) | -| `member_sync_send_to(inst, ch_id, member_id, target_peer)` | Send-back нашей свежей записи конкретному пиру | -| `member_sync_broadcast_one(inst, ch_id, member_id)` | Сериализовать мембера и разослать всем (relay после локального изменения) | -| `member_sync_set_online(inst, node_id, online)` | Записать `nodes.online`. Возвращает 1 (изменилось) / 0 | -| `member_sync_count(inst, ch_id)` | `COUNT(*)` из `peers_` | -| `member_sync_build_join_msg(...)` / `build_update_msg(...)` | Канонические сообщения для подписи/верификации join/update (112 байт и 64+8+userinfo) | -| `member_sync_get_hash(inst, ch_id, level, prefix64)` | Хеш бакета (для тестов) | -| `member_sync_add/remove_props_cbk`, `set_node_updated_cb` | Подписки на изменения | - -## 8. Нюансы - -- **Онлайн-статус не участвует в Merkle-дереве**: не хешируется и не версионируется. `set_online` - только пишет в БД и возвращает 1/0; рассылку `MSG_ITEM_UPDATE` выполняет **caller** (chat_sync). -- **`member_sync_put` = локальный `apply_record`** с `from_peer = inst->node_id` — без верификации - подписей (свои данные доверенные). -- **`source`-плейсхолдеры** topo_group пропускаются (`src != 0`), в дереве не участвуют. -- **Re-join / смена ключа** распознаётся по `join_ts` (перекрывает сравнение `update_ts`). -- Категория логов — `DEBUG_CATEGORY_MEMBER_SYNC`. +Категория member_sync: ошибки содержат канал, node_id и причину отказа. +DEBUG показывает применение версий и размеры страниц, TRACE — входы и расчёт листа. +Хеши родителей вычисляет merkle_tree; адаптер никогда не перечисляет все записи для внутреннего узла. diff --git a/src/chat/merkle_sync.c b/src/chat/merkle_sync.c index 14a5824c..08048476 100644 --- a/src/chat/merkle_sync.c +++ b/src/chat/merkle_sync.c @@ -1,974 +1,573 @@ #include "merkle_sync.h" - #include "../utun_instance.h" #include "../transport_layer/etcp_api.h" #include "../transport_layer/etcp.h" -#include "../routing_layer/topo_group.h" #include "../../lib/debug_config.h" #include "../../lib/mem.h" #include "../../lib/u_async.h" #include "../../lib/ll_queue.h" #include "../../lib/platform_compat.h" -#ifdef UTUN_HAVE_STANDBY -#include "standby.h" -#endif - #include #include -#include - -#define MS_ID "merkle_sync" -#define MS_WAL_INTERVAL_MS 60000 -#define MS_PENDING_MAX 128 - -struct bucket_entry { uint8_t level; uint8_t prefix_bytes; uint64_t prefix; }; - -enum { SESS_SYNCING, SESS_SYNCED }; -enum { MS_PEND_WAITING = 0, MS_PEND_RESOLVED = 1 }; +#include + +/* Wire v2: svc:1, namespace:8, type:1, round:8, request:4, payload. + * Все целые заголовка big-endian. Один запрос на проход и один исходящий пакет на сессию. + * Меньший node_id ведёт раунд: pull -> TURN -> reverse pull -> CHECK -> DONE. */ +#define MS_HEADER 22 +#define MS_QUERY_SIZE 18 +enum { MS_WAKE = 0x10, MS_BEGIN, MS_READY, MS_QUERY, MS_HASHES, MS_PAGE, MS_TURN, MS_CHECK, MS_DONE, MS_ERROR }; +enum { MS_IDLE, MS_BEGIN_WAIT, MS_PULL, MS_SERVE, MS_DONE_WAIT, MS_FAILED }; + +struct ms_request { + struct ms_request* next; + merkle_sync_done_cb cb; + void* arg; +}; -struct ms_pending { - uint8_t level; +struct ms_frame { uint64_t prefix; - int8_t parent; - int sub_count; - uint8_t state; + uint32_t children; + uint8_t level; }; struct ms_session { struct ms_session* next; struct merkle_sync* ms; - char ns[64]; - uint64_t peer; - uint8_t sess_state; - merkle_sync_done_cb done_cb; - void* cb_arg; - struct ms_pending pending[MS_PENDING_MAX]; - int pending_count; - struct ll_queue* out_q; + struct ETCP_CONN* conn; + struct ll_queue* send_q; struct queue_waiter_handle waiter; + struct ll_entry* tx; + void* work; + struct ms_request* requests; + uint64_t ns_id, peer, round, revision, verified_revision; + char ns[21]; + int state, leader, dirty, depth, after_valid, waiting; + uint32_t request_id, remote_request_id; + uint64_t after; + struct ms_frame stack[MT_MAX_LEVEL + 1]; + uint8_t previous_roots[2 * MT_HASH_SIZE]; + int previous_valid; + uint64_t sent_bytes, received_bytes; }; struct merkle_sync { - struct UTUN_INSTANCE* inst; - uint8_t svc_id; + struct UTUN_INSTANCE* inst; + uint8_t svc_id; const struct merkle_sync_data_ops* ops; - void* data_ctx; - struct ms_session* sessions; - uint8_t initialized; - void* wal_timer; - void* wal_wait; /* standby_wait handle (Android) */ + void* data_ctx; + struct ms_session* sessions; + uint64_t round_seq; + struct ETCP_CONN* closing_conn; }; -/* ── DB access ── */ - -static sqlite3* _db(struct UTUN_INSTANCE* inst) { - return inst ? inst->topo_sqlite_db : NULL; -} - -/* ── Prefix arithmetic ── */ - -uint64_t merkle_sync_level_prefix(uint64_t key, uint8_t level) { - if (level == 0) return 0; - int shift = 64 - (int)level * 5; if (shift < 0) shift = 0; - return (key >> shift) << shift; -} - -uint8_t merkle_sync_prefix_bytes(uint8_t level) { int bits = level * 5; return (uint8_t)((bits + 7) / 8); } - -static void _prefix_write(uint8_t* out, uint64_t prefix, uint8_t pb) { - for (int i = 0; i < (int)pb; i++) - out[i] = (uint8_t)(prefix >> (8 * (7 - i))); -} - -static uint64_t _prefix_read(const uint8_t* in, uint8_t pb) { - uint64_t v = 0; - for (uint8_t i = 0; i < pb && i < 8; i++) v = (v << 8) | in[i]; - return v << (8 * (8 - (int)pb)); +static uint64_t ms_read64(const uint8_t* p) { uint64_t n; memcpy(&n, p, 8); return be64toh(n); } +static void ms_write64(uint8_t* p, uint64_t n) { n = htobe64(n); memcpy(p, &n, 8); } +static uint32_t ms_read32(const uint8_t* p) { uint32_t n; memcpy(&n, p, 4); return ntohl(n); } +static void ms_write32(uint8_t* p, uint32_t n) { n = htonl(n); memcpy(p, &n, 4); } + +static int ms_namespace(const char* ns, uint64_t* id) { + if (!ns || !ns[0] || strlen(ns) > 20) return -1; + for (const char* p = ns; *p; p++) if (*p < '0' || *p > '9') return -1; + if (ns[0] == '0' && ns[1]) return -1; + char* end; + errno = 0; + unsigned long long n = strtoull(ns, &end, 10); + if (errno || *end) return -1; + *id = (uint64_t)n; + return 0; } -static int _popcount_u32(uint32_t v) { return __builtin_popcount(v); } - -/* ── Ensure merkle_tree_hash table ── */ - -static void _ensure_table(struct merkle_sync* ms) { - sqlite3* db = _db(ms->inst); if (!db) return; - sqlite3_exec(db, - "CREATE TABLE IF NOT EXISTS merkle_tree_hash (" - " namespace TEXT NOT NULL," - " level INTEGER NOT NULL CHECK(level BETWEEN 1 AND 5)," - " prefix64 INTEGER NOT NULL," - " hash BLOB NOT NULL," - " member_count INTEGER NOT NULL," - " PRIMARY KEY (namespace, level, prefix64))", - NULL, NULL, NULL); +static struct ms_session* ms_find(struct merkle_sync* ms, struct ETCP_CONN* conn, uint64_t ns_id) { + for (struct ms_session* s = ms->sessions; s; s = s->next) + if (s->conn == conn && s->ns_id == ns_id) return s; + return NULL; } -/* ── Tree operations ── */ - -static int _recompute_bucket(struct merkle_sync* ms, const char* ns, - uint8_t level, uint64_t prefix64) { - DEBUG_TRACE(DEBUG_CATEGORY_MEMBER_SYNC, "%s: ns=%s L%d/P%016llx", MS_ID, ns, level, (unsigned long long)prefix64); - sqlite3* db = _db(ms->inst); if (!db) return -1; - - EVP_MD_CTX* ctx = EVP_MD_CTX_new(); - EVP_DigestInit_ex(ctx, EVP_sha256(), NULL); - int count = ms->ops->update_bucket_hash(ms->data_ctx, ns, level, prefix64, ctx); - - if (count < 0) { DEBUG_TRACE(DEBUG_CATEGORY_MEMBER_SYNC, "%s: → error", MS_ID); EVP_MD_CTX_free(ctx); return -1; } - if (count == 0) { - DEBUG_TRACE(DEBUG_CATEGORY_MEMBER_SYNC, "%s: → empty", MS_ID); - EVP_MD_CTX_free(ctx); - sqlite3_stmt* cs = NULL; - sqlite3_prepare_v2(db, - "SELECT COUNT(*) FROM merkle_tree_hash WHERE namespace=? AND level=? AND prefix64=?", -1, &cs, NULL); - if (cs) { sqlite3_bind_text(cs, 1, ns, -1, SQLITE_STATIC); sqlite3_bind_int(cs, 2, level); - sqlite3_bind_int64(cs, 3, (sqlite3_int64)prefix64); - if (sqlite3_step(cs) == SQLITE_ROW && sqlite3_column_int(cs, 0) > 0) { - sqlite3_finalize(cs); - DEBUG_TRACE(DEBUG_CATEGORY_MEMBER_SYNC, "%s: → cleared (was non-empty)", MS_ID); - sqlite3_stmt* ds = NULL; - sqlite3_prepare_v2(db, - "DELETE FROM merkle_tree_hash WHERE namespace=? AND level=? AND prefix64=?", -1, &ds, NULL); - if (ds) { sqlite3_bind_text(ds, 1, ns, -1, SQLITE_STATIC); sqlite3_bind_int(ds, 2, level); - sqlite3_bind_int64(ds, 3, (sqlite3_int64)prefix64); sqlite3_step(ds); sqlite3_finalize(ds); } - return 1; - } else { sqlite3_finalize(cs); } - } - return 0; - } - - uint8_t new_hash[MT_HASH_SIZE]; - EVP_DigestFinal_ex(ctx, new_hash, NULL); - EVP_MD_CTX_free(ctx); - - const uint8_t* old_hash = merkle_sync_get_hash(ms->inst, ns, level, prefix64); - if (memcmp(old_hash, new_hash, MT_HASH_SIZE) == 0) { - DEBUG_TRACE(DEBUG_CATEGORY_MEMBER_SYNC, "%s: → unchanged (count=%d)", MS_ID, count); - return 0; +static void ms_notify(struct ms_session* s, int result) { + /* Все данные копируются до callback: callback вправе отменить сессию. */ + struct ms_request* r = s->requests; + s->requests = NULL; + uint64_t peer = s->peer; + char ns[sizeof(s->ns)]; memcpy(ns, s->ns, sizeof(ns)); + while (r) { + struct ms_request* next = r->next; + merkle_sync_done_cb cb = r->cb; void* arg = r->arg; + u_free(r); + cb(peer, ns, result, arg); + r = next; } - - DEBUG_TRACE(DEBUG_CATEGORY_MEMBER_SYNC, "%s: → saved, count=%d", MS_ID, count); - sqlite3_stmt* is = NULL; - sqlite3_prepare_v2(db, - "INSERT OR REPLACE INTO merkle_tree_hash(namespace, level, prefix64, hash, member_count)" - " VALUES(?,?,?,?,?)", -1, &is, NULL); - if (is) { - sqlite3_bind_text(is, 1, ns, -1, SQLITE_STATIC); - sqlite3_bind_int(is, 2, level); - sqlite3_bind_int64(is, 3, (sqlite3_int64)prefix64); - sqlite3_bind_blob(is, 4, new_hash, MT_HASH_SIZE, SQLITE_STATIC); - sqlite3_bind_int(is, 5, count); - sqlite3_step(is); sqlite3_finalize(is); - } - return 1; } -int merkle_sync_recompute_path(struct UTUN_INSTANCE* inst, const char* ns, uint64_t key) { - struct merkle_sync* ms = inst ? inst->msync : NULL; - if (!ms || !ms->initialized) { DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: recompute_path called but msync not initialized", MS_ID); return -1; } - DEBUG_TRACE(DEBUG_CATEGORY_MEMBER_SYNC, "%s: ns=%s key=%016llx", MS_ID, ns, (unsigned long long)key); - int changed = 0; - for (uint8_t level = 1; level <= MT_MAX_LEVEL; level++) { - int r = _recompute_bucket(ms, ns, level, merkle_sync_level_prefix(key, level)); - if (r < 0) return -1; - if (r > 0) changed = 1; - } - return changed; -} +static int ms_schedule(struct ms_session* s); +static void ms_work(void* arg); +static void ms_fail(struct ms_session* s, int error, const char* reason, int report); -const uint8_t* merkle_sync_get_hash(struct UTUN_INSTANCE* inst, const char* ns, - uint8_t level, uint64_t prefix64) { - static uint8_t zero[MT_HASH_SIZE]; - sqlite3* db = _db(inst); if (!db) { memset(zero, 0, MT_HASH_SIZE); return zero; } - sqlite3_stmt* stmt = NULL; - sqlite3_prepare_v2(db, - "SELECT hash FROM merkle_tree_hash WHERE namespace=? AND level=? AND prefix64=?", - -1, &stmt, NULL); - if (!stmt) { memset(zero, 0, MT_HASH_SIZE); return zero; } - sqlite3_bind_text(stmt, 1, ns, -1, SQLITE_STATIC); - sqlite3_bind_int(stmt, 2, level); - sqlite3_bind_int64(stmt, 3, (sqlite3_int64)prefix64); - memset(zero, 0, MT_HASH_SIZE); - if (sqlite3_step(stmt) == SQLITE_ROW) { - const void* h = sqlite3_column_blob(stmt, 0); - if (h) memcpy(zero, h, MT_HASH_SIZE); - } - sqlite3_finalize(stmt); - return zero; +static void ms_discard_tx(struct ms_session* s) { + queue_waiter_cancel(s->send_q, &s->waiter); + if (s->tx) { queue_dgram_free(s->tx); queue_entry_free(s->tx); s->tx = NULL; } } -static int _get_level_hashes(struct merkle_sync* ms, const char* ns, - uint8_t level, uint64_t prefix, uint8_t prefix_bytes, - uint32_t* bitmap, uint8_t hashes[MT_BUCKETS][MT_HASH_SIZE]) { - (void)prefix_bytes; - memset(hashes, 0, sizeof(uint8_t) * MT_BUCKETS * MT_HASH_SIZE); - *bitmap = 0; - if (level >= MT_MAX_LEVEL) return 0; - - sqlite3* db = _db(ms->inst); if (!db) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: _get_level_hashes — db is NULL", MS_ID); return -1; } - - int next_shift = 64 - ((int)level + 1) * 5; if (next_shift < 0) next_shift = 0; - - sqlite3_stmt* stmt = NULL; - int query_level = (int)(level + 1); - const char* sql = (level == 0) - ? "SELECT prefix64, hash FROM merkle_tree_hash" - " WHERE namespace=? AND level=? ORDER BY prefix64" - : "SELECT prefix64, hash FROM merkle_tree_hash" - " WHERE namespace=? AND level=? AND prefix64>=? AND prefix64<=?" - " ORDER BY prefix64"; - if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK || !stmt) { - DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: _get_level_hashes SQL error ns=%s L%d — %s", MS_ID, ns, level, sqlite3_errmsg(db)); - return -1; +static void ms_free(struct ms_session* s) { + struct merkle_sync* ms = s->ms; + struct ms_session** p = &ms->sessions; + while (*p && *p != s) p = &(*p)->next; + if (*p) *p = s->next; + if (s->work) uasync_call_soon_cancel(ms->inst->ua, s->work); + ms_discard_tx(s); + while (s->requests) { + struct ms_request* r = s->requests; + s->requests = r->next; u_free(r); + } + etcp_conn_ref_free(s->conn); + u_free(s); +} + +static struct ms_session* ms_create(struct merkle_sync* ms, struct ETCP_CONN* conn, uint64_t ns_id) { + if (!conn || !conn->send_input_q || !conn->initialized || !conn->links_up || conn->state == 2) return NULL; + struct ms_session* s = u_calloc(1, sizeof(*s)); + if (!s) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "merkle_sync: session allocation failed"); return NULL; } + if (etcp_conn_ref_take(conn) != 0) { u_free(s); return NULL; } + s->ms = ms; s->conn = conn; s->send_q = conn->send_input_q; + s->peer = conn->peer_node_id; s->ns_id = ns_id; + snprintf(s->ns, sizeof(s->ns), "%llu", (unsigned long long)ns_id); + s->leader = ms->inst->node_id < s->peer; + s->next = ms->sessions; ms->sessions = s; + DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "merkle_sync: attach ns=%s peer=%016llx conn=%p leader=%d", + s->ns, (unsigned long long)s->peer, (void*)conn, s->leader); + return s; +} + +int merkle_sync_read_hash(struct UTUN_INSTANCE* inst, const char* ns, uint8_t level, uint64_t prefix, uint8_t hash[MT_HASH_SIZE]) { + return merkle_tree_get(inst ? inst->topo_sqlite_db : NULL, ns, level, prefix, hash); +} + +static int ms_root(struct ms_session* s, uint8_t root[MT_HASH_SIZE]) { + return merkle_sync_read_hash(s->ms->inst, s->ns, 0, 0, root); +} + +static void ms_sent(struct ll_queue* q, void* arg) { + struct ms_session* s = arg; + struct ll_entry* entry = s->tx; + s->tx = NULL; + if (!entry) return; + int type = entry->dgram[9]; + size_t len = entry->len; + if (!s->conn->links_up || s->conn->state == 2 || s->conn->close_requested || q != s->conn->send_input_q) { + queue_dgram_free(entry); queue_entry_free(entry); + ms_fail(s, MT_ERR_DISCONNECTED, "connection unavailable", 0); + return; } - sqlite3_bind_text(stmt, 1, ns, -1, SQLITE_STATIC); - sqlite3_bind_int(stmt, 2, query_level); - if (level > 0) { - uint64_t range_end = prefix | (0x1FULL << (next_shift > 0 ? next_shift : 0)); - sqlite3_bind_int64(stmt, 3, (sqlite3_int64)prefix); - sqlite3_bind_int64(stmt, 4, (sqlite3_int64)range_end); + if (etcp_send(s->conn, entry) != 0) { + queue_dgram_free(entry); queue_entry_free(entry); + ms_fail(s, MT_ERR_IO, "transport rejected packet", 0); + return; } - while (sqlite3_step(stmt) == SQLITE_ROW) { - sqlite3_int64 row_pfx = sqlite3_column_int64(stmt, 0); - const void* h = sqlite3_column_blob(stmt, 1); - if (!h || sqlite3_column_bytes(stmt, 1) < MT_HASH_SIZE) continue; - int bucket = (int)(((uint64_t)row_pfx >> next_shift) & 0x1F); - *bitmap |= (1u << bucket); - memcpy(hashes[bucket], h, MT_HASH_SIZE); + s->sent_bytes += len; + DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "merkle_sync: TX ns=%s peer=%016llx round=%llu type=%u bytes=%zu", + s->ns, (unsigned long long)s->peer, (unsigned long long)s->round, type, len); + if (type == MS_DONE) { + s->state = MS_IDLE; + if (s->revision == s->verified_revision) { + s->dirty = 0; + DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "merkle_sync: converged ns=%s peer=%016llx round=%llu tx=%llu rx=%llu", + s->ns, (unsigned long long)s->peer, (unsigned long long)s->round, + (unsigned long long)s->sent_bytes, (unsigned long long)s->received_bytes); + ms_notify(s, MT_OK); + return; + } } - sqlite3_finalize(stmt); - DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "%s: _get_level_hashes ns=%s L%d/P%016llx bitmap=%08x", - MS_ID, ns, level, (unsigned long long)prefix, *bitmap); - return 0; -} - -/* ── group_id в wire: [group_id:8] вместо [ns_len:1][ns] ── */ - -static uint64_t ms_ns_to_group_id(const char* ns) { - return strtoull(ns, NULL, 10); -} - -static void ms_write_group_id(uint8_t* p, const char* ns) { - uint64_t gb = htobe64(ms_ns_to_group_id(ns)); - memcpy(p, &gb, 8); + if (s->state == MS_IDLE && s->dirty && ms_schedule(s) < 0) ms_fail(s, MT_ERR_IO, "work allocation", 1); } -/* group_id → ns через группу канала (требует существующую CHAT-группу). 0=ок, -1=нет группы */ -static int ms_group_id_to_ns(struct UTUN_INSTANCE* inst, uint64_t group_id, char* ns, size_t sz) { - if (!inst || !inst->topo_groups) return -1; - struct TOPO_GROUP* g = topo_groups_find(inst->topo_groups, group_id); - if (!g || !g->channel_id[0]) { - DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: recv — no CHAT group for group=%016llx, drop", MS_ID, (unsigned long long)group_id); +static int ms_send(struct ms_session* s, int type, uint32_t request, const uint8_t* payload, size_t len) { + if (s->tx || len > MT_PAGE_SIZE - MS_HEADER) { + DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "merkle_sync: invalid TX ns=%s type=%d busy=%d size=%zu", s->ns, type, s->tx != NULL, len); return -1; } - snprintf(ns, sz, "%s", g->channel_id); + struct ll_entry* e = ll_alloc_lldgram((uint16_t)(MS_HEADER + len)); + if (!e) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "merkle_sync: packet allocation failed ns=%s", s->ns); return -1; } + e->len = (uint16_t)(MS_HEADER + len); + e->dgram[0] = s->ms->svc_id; + ms_write64(e->dgram + 1, s->ns_id); + e->dgram[9] = (uint8_t)type; + ms_write64(e->dgram + 10, type == MS_WAKE ? 0 : s->round); + ms_write32(e->dgram + 18, request); + if (len) memcpy(e->dgram + MS_HEADER, payload, len); + s->tx = e; + /* Transport normalizer configures deferred waiters. Не вызываем callback рекурсивно. */ + if (queue_waiter_wait(s->send_q, &s->waiter, ms_sent, s) < 0) { ms_discard_tx(s); return -1; } return 0; } -/* ── Send helpers ── */ - -static struct ETCP_CONN* ms_find_conn_for_node(struct UTUN_INSTANCE* inst, uint64_t node_id) { - if (!inst->connections) { DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "%s: find_conn — inst->connections is NULL", MS_ID); return NULL; } - struct ETCP_CONN* conn = instance_find_conn(inst, node_id); - if (!conn) { DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "%s: find_conn — node=%016llx NOT FOUND", MS_ID, (unsigned long long)node_id); return NULL; } - DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "%s: find_conn — node=%016llx found=%d initialized=%d links_up=%d", - MS_ID, (unsigned long long)node_id, 1, conn->initialized, conn->links_up); - if (conn->initialized && conn->links_up) return conn; - return NULL; -} - -/* ── Per-session out queue (broadcast/push с backpressure) ── */ - -static void ms_session_outq_free(struct ms_session* s) { - if (!s) return; - if (s->out_q && s->ms && s->ms->inst) { - struct ETCP_CONN* conn = ms_find_conn_for_node(s->ms->inst, s->peer); - if (conn && conn->send_input_q) queue_waiter_cancel(conn->send_input_q, &s->waiter); - } - if (s->out_q) { - struct ll_entry* e; - while ((e = queue_data_get(s->out_q)) != NULL) { - if (e->dgram) u_free(e->dgram); - queue_entry_free(e); - } - queue_free(s->out_q); - s->out_q = NULL; +static void ms_fail(struct ms_session* s, int error, const char* reason, int report) { + DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "merkle_sync: failed ns=%s peer=%016llx round=%llu error=%d reason=%s", + s->ns, (unsigned long long)s->peer, (unsigned long long)s->round, error, reason); + s->state = MS_FAILED; s->dirty = 0; s->waiting = 0; + if (s->work) { uasync_call_soon_cancel(s->ms->inst->ua, s->work); s->work = NULL; } + ms_discard_tx(s); + if (report) { + uint8_t code[4]; ms_write32(code, (uint32_t)(-error)); + if (ms_send(s, MS_ERROR, 0, code, sizeof(code)) < 0) + DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "merkle_sync: could not send error ns=%s", s->ns); } + ms_notify(s, error); } -/* Инициализировать out-очередь сессии (вызывается при создании сессии). */ -static void ms_session_setup(struct merkle_sync* ms, struct ms_session* s) { - s->ms = ms; - s->out_q = queue_new(ms->inst->ua, 0, 0, 0, "ms_out"); - if (!s->out_q) DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: out_q queue_new failed", MS_ID); -} - -static void ms_outq_drain_cb(struct ll_queue* q, void* arg) { - struct ms_session* s = (struct ms_session*)arg; - struct merkle_sync* ms = s ? s->ms : NULL; - if (!ms || !s->out_q) return; - struct ll_entry* e = queue_data_get(s->out_q); - if (!e) return; - struct ETCP_CONN* conn = ms_find_conn_for_node(ms->inst, s->peer); - if (!conn) { - DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: outq drain — no conn for peer=%016llx, drop %u bytes", - MS_ID, (unsigned long long)s->peer, e->len); - if (e->dgram) u_free(e->dgram); - queue_entry_free(e); - } else { - uint16_t len = e->len; /* etcp_send может синхронно освободить e (ownership переходит в очередь) */ - int r = etcp_send(conn, e); - if (r != 0) { if (e->dgram) u_free(e->dgram); queue_entry_free(e); } - DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "%s: outq drain peer=%016llx len=%u rc=%d left=%d", - MS_ID, (unsigned long long)s->peer, len, r, queue_entry_count(s->out_q)); +static int ms_schedule(struct ms_session* s) { + if (s->work || s->state == MS_FAILED) return 0; + s->work = uasync_call_soon(s->ms->inst->ua, s, ms_work); + if (!s->work) { + DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "merkle_sync: work allocation ns=%s", s->ns); return MT_ERR_IO; } - if (queue_entry_count(s->out_q) > 0 && conn && conn->send_input_q) - queue_waiter_wait(conn->send_input_q, &s->waiter, ms_outq_drain_cb, s); -} - -/* Собрать [svc_id][payload] и положить в out_q сессии (с backpressure на send_input_q). */ -static int ms_outq_enqueue(struct ms_session* s, const uint8_t* payload, size_t plen) { - struct merkle_sync* ms = s ? s->ms : NULL; - if (!ms || !s->out_q) return -1; - struct ETCP_CONN* conn = ms_find_conn_for_node(ms->inst, s->peer); - if (!conn) { DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "%s: outq enqueue — no conn for peer=%016llx", MS_ID, (unsigned long long)s->peer); return -1; } - - uint8_t* buf = u_malloc(1 + plen); - if (!buf) return -1; - buf[0] = ms->svc_id; - memcpy(buf + 1, payload, plen); - struct ll_entry* e = queue_entry_new(0); - if (!e) { u_free(buf); return -1; } - e->dgram = buf; e->len = (uint16_t)(1 + plen); - - int was_empty = (queue_entry_count(s->out_q) == 0); - // При провале queue_data_put сам освобождает entry+dgram, повторный free дал бы double-free. - if (queue_data_put(s->out_q, e) != 0) return -1; - if (was_empty && conn->send_input_q) - queue_waiter_wait(conn->send_input_q, &s->waiter, ms_outq_drain_cb, s); return 0; } -static int _send_msg(struct merkle_sync* ms, uint64_t peer, - const uint8_t* payload, size_t len) { - if (!ms->inst || len < 1) return -1; - uint8_t* buf = u_malloc(1 + len); - if (!buf) return -1; - buf[0] = ms->svc_id; - memcpy(buf + 1, payload, len); - struct ll_entry* entry = queue_entry_new(0); - if (!entry) { u_free(buf); return -1; } - entry->dgram = buf; entry->len = 1 + len; - struct ETCP_CONN* conn = ms_find_conn_for_node(ms->inst, peer); - if (!conn) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: no conn for node %016llx", MS_ID, (unsigned long long)peer); u_free(buf); queue_entry_free(entry); return -1; } - int r = etcp_send(conn, entry); - if (r != 0) { u_free(buf); queue_entry_free(entry); } - DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "%s: send OK peer=%016llx len=%zu rc=%d", MS_ID, (unsigned long long)peer, len, r); - return r; -} - -static int _broadcast_to_session(struct merkle_sync* ms, struct ms_session* s, - const char* ns, const uint8_t* item_data, size_t item_len) { - size_t sz = 1 + 8 + 1 + 1 + 1 + 1 + 2 + item_len; - uint8_t* buf = u_malloc(sz); if (!buf) return -1; - uint8_t* p = buf; - ms_write_group_id(p, ns); p += 8; - *p++ = 0x01; /* MSG_HASHES */ - *p++ = 0; /* level=0 */ - *p++ = 0; /* prefix_bytes=0 */ - *p++ = 1; /* is_data=1 */ - uint16_t dlen = (uint16_t)item_len; memcpy(p, &dlen, 2); p += 2; - memcpy(p, item_data, item_len); p += item_len; - int r = ms_outq_enqueue(s, buf, (size_t)(p - buf)); - u_free(buf); - return r; -} - -static int _send_hashes(struct merkle_sync* ms, uint64_t peer, const char* ns, - uint8_t level, uint64_t prefix, uint8_t prefix_bytes, int is_data) { - DEBUG_TRACE(DEBUG_CATEGORY_MEMBER_SYNC, "%s: send_hashes peer=%016llx ns=%s L%d P%016llx is_data=%d", MS_ID, (unsigned long long)peer, ns, level, (unsigned long long)prefix, is_data); - size_t max_sz = 1 + 8 + 1 + 1 + prefix_bytes + 1 + 4 + MT_BUCKETS * MT_HASH_SIZE + 2 + 65536; - uint8_t* buf = u_malloc(max_sz); - if (!buf) return -1; - uint8_t* p = buf; - ms_write_group_id(p, ns); p += 8; - *p++ = 0x01; /* MS_MSG_HASHES */ - *p++ = level; - *p++ = prefix_bytes; _prefix_write(p, prefix, prefix_bytes); p += prefix_bytes; - *p++ = (uint8_t)(is_data ? 1 : 0); - - if (is_data) { - size_t mlen = 65536; uint8_t* mbuf = u_malloc(mlen); - if (mbuf) { - if (ms->ops->get_items(ms->data_ctx, ns, level, prefix, prefix_bytes, mbuf, &mlen) == 0) { - uint16_t dlen = (uint16_t)mlen; memcpy(p, &dlen, 2); p += 2; - memcpy(p, mbuf, mlen); p += mlen; - } else { uint16_t zero = 0; memcpy(p, &zero, 2); p += 2; } - u_free(mbuf); +static int ms_query(struct ms_session* s) { + struct ms_frame* f = &s->stack[s->depth]; + uint8_t body[MS_QUERY_SIZE]; + body[0] = f->level; ms_write64(body + 1, f->prefix); + body[9] = (uint8_t)s->after_valid; ms_write64(body + 10, s->after); + if (++s->request_id == 0) return -1; + s->waiting = 1; + return ms_send(s, MS_QUERY, s->request_id, body, sizeof(body)); +} + +static int ms_pull_start(struct ms_session* s) { + s->state = MS_PULL; s->depth = 0; s->after_valid = 0; s->after = 0; + memset(s->stack, 0, sizeof(s->stack)); + return ms_query(s); +} + +static int ms_walk_next(struct ms_session* s) { + while (s->depth >= 0) { + struct ms_frame* f = &s->stack[s->depth]; + if (f->children) { + unsigned i = (unsigned)__builtin_ctz(f->children); + f->children &= ~(UINT32_C(1) << i); + uint8_t level = f->level + 1; + uint64_t prefix = f->prefix | ((uint64_t)i << (64 - level * 5)); + s->stack[++s->depth] = (struct ms_frame){ .prefix = prefix, .level = level }; + s->after_valid = 0; s->after = 0; + return ms_query(s); + } + s->depth--; + } + s->waiting = 0; + if (s->leader) { s->state = MS_SERVE; return ms_send(s, MS_TURN, 0, NULL, 0); } + if (s->ms->ops->finish && s->ms->ops->finish(s->ms->data_ctx, s->ns) < 0) return MT_ERR_DATA; + uint8_t root[MT_HASH_SIZE]; + if (ms_root(s, root) < 0) return -1; + s->state = MS_DONE_WAIT; + return ms_send(s, MS_CHECK, 0, root, sizeof(root)); +} + +static int ms_begin(struct ms_session* s) { + s->round = ++s->ms->round_seq; + if (!s->round) return -1; + s->request_id = s->remote_request_id = 0; + s->state = MS_BEGIN_WAIT; s->dirty = 0; s->waiting = 0; + DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "merkle_sync: begin ns=%s peer=%016llx round=%llu", + s->ns, (unsigned long long)s->peer, (unsigned long long)s->round); + return ms_send(s, MS_BEGIN, 0, NULL, 0); +} + +static void ms_work(void* arg) { + struct ms_session* s = arg; + s->work = NULL; + if (s->state != MS_IDLE || !s->dirty || s->tx) return; + int rc; + if (s->leader) rc = ms_begin(s); + else { s->dirty = 0; rc = ms_send(s, MS_WAKE, 0, NULL, 0); } + if (rc < 0) ms_fail(s, MT_ERR_IO, "start send", 1); +} + +void merkle_sync_changed(struct UTUN_INSTANCE* inst, const char* ns) { + struct merkle_sync* ms = inst ? inst->msync : NULL; + if (!ms || !ns) return; + for (struct ms_session* s = ms->sessions; s; s = s->next) { + if (strcmp(s->ns, ns)) continue; + s->revision++; s->dirty = 1; + if (s->state == MS_FAILED) { s->state = MS_IDLE; s->previous_valid = 0; } + if (s->state == MS_IDLE && ms_schedule(s) < 0) { + ms_fail(s, MT_ERR_IO, "change scheduling", 1); return; } - } else { - uint32_t bitmap; uint8_t hashes[MT_BUCKETS][MT_HASH_SIZE]; - _get_level_hashes(ms, ns, level, prefix, prefix_bytes, &bitmap, hashes); - DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "%s: send_hashes peer=%016llx ns=%s level=%d prefix=%016llx bitmap=%08x", MS_ID, (unsigned long long)peer, ns, level, (unsigned long long)prefix, bitmap); - memcpy(p, &bitmap, 4); p += 4; - for (int i = 0; i < MT_BUCKETS; i++) - if (bitmap & (1u << i)) { memcpy(p, hashes[i], MT_HASH_SIZE); p += MT_HASH_SIZE; } } - int r = _send_msg(ms, peer, buf, (size_t)(p - buf)); - u_free(buf); - return r; } -static int _send_batch(struct merkle_sync* ms, uint64_t peer, const char* ns, - struct bucket_entry* buckets, int count) { - DEBUG_TRACE(DEBUG_CATEGORY_MEMBER_SYNC, "%s: send_batch peer=%016llx ns=%s count=%d", MS_ID, (unsigned long long)peer, ns, count); - size_t max_sz = 1 + 8 + 1 + (size_t)count * (1 + 1 + 8 + 1 + 4 + MT_BUCKETS * MT_HASH_SIZE + 65536); - uint8_t* buf = u_malloc(max_sz); - if (!buf) return -1; - uint8_t* p = buf; - ms_write_group_id(p, ns); p += 8; - *p++ = 0x03; /* MS_MSG_BATCH */ - *p++ = (uint8_t)count; - - for (int i = 0; i < count; i++) { - *p++ = buckets[i].level; - *p++ = buckets[i].prefix_bytes; - _prefix_write(p, buckets[i].prefix, buckets[i].prefix_bytes); p += buckets[i].prefix_bytes; - - EVP_MD_CTX* tctx = EVP_MD_CTX_new(); - EVP_DigestInit_ex(tctx, EVP_sha256(), NULL); - int bcount = ms->ops->update_bucket_hash(ms->data_ctx, ns, - buckets[i].level, buckets[i].prefix, tctx); - if (bcount < 0) bcount = 0; - EVP_MD_CTX_free(tctx); - - int is_terminal = (buckets[i].level >= MT_MAX_LEVEL || bcount < 8); - *p++ = (uint8_t)(is_terminal ? 1 : 0); - - if (is_terminal) { - size_t mlen = 65536; uint8_t* mbuf = u_malloc(mlen); - if (mbuf) { - if (ms->ops->get_items(ms->data_ctx, ns, buckets[i].level, - buckets[i].prefix, buckets[i].prefix_bytes, mbuf, &mlen) == 0) { - uint16_t dlen = (uint16_t)mlen; memcpy(p, &dlen, 2); p += 2; - memcpy(p, mbuf, mlen); p += mlen; - } else { uint16_t z = 0; memcpy(p, &z, 2); p += 2; } - u_free(mbuf); - } - } else { - uint32_t bm; uint8_t hs[MT_BUCKETS][MT_HASH_SIZE]; - _get_level_hashes(ms, ns, buckets[i].level, - buckets[i].prefix, buckets[i].prefix_bytes, &bm, hs); - memcpy(p, &bm, 4); p += 4; - for (int j = 0; j < MT_BUCKETS; j++) - if (bm & (1u << j)) { memcpy(p, hs[j], MT_HASH_SIZE); p += MT_HASH_SIZE; } +int merkle_sync_recompute_path(struct UTUN_INSTANCE* inst, const char* ns, uint64_t key) { + struct merkle_sync* ms = inst ? inst->msync : NULL; + sqlite3* db = inst ? inst->topo_sqlite_db : NULL; + if (!ms || !db || !ns) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "merkle_sync: invalid recompute"); return -1; } + int own = sqlite3_get_autocommit(db); + if (own && sqlite3_exec(db, "SAVEPOINT merkle_update", NULL, NULL, NULL) != SQLITE_OK) { + DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "merkle_sync: begin update ns=%s: %s", ns, sqlite3_errmsg(db)); return -1; + } + int rc = merkle_tree_update(db, ns, key, ms->ops->update_bucket_hash, ms->data_ctx); + if (own) { + if (rc < 0 && sqlite3_exec(db, "ROLLBACK TO merkle_update", NULL, NULL, NULL) != SQLITE_OK) + DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "merkle_sync: rollback ns=%s: %s", ns, sqlite3_errmsg(db)); + if (sqlite3_exec(db, "RELEASE merkle_update", NULL, NULL, NULL) != SQLITE_OK) { + DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "merkle_sync: commit ns=%s: %s", ns, sqlite3_errmsg(db)); + if (sqlite3_exec(db, "ROLLBACK TO merkle_update; RELEASE merkle_update", NULL, NULL, NULL) != SQLITE_OK) + DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "merkle_sync: commit cleanup ns=%s: %s", ns, sqlite3_errmsg(db)); + rc = -1; } + if (rc > 0) merkle_sync_changed(inst, ns); + } + if (rc < 0) DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "merkle_sync: recompute failed ns=%s key=%016llx", ns, (unsigned long long)key); + return rc; +} + +static int ms_serve_query(struct ms_session* s, uint32_t request, const uint8_t* p, size_t len) { + if (s->state != MS_SERVE || len != MS_QUERY_SIZE || request != s->remote_request_id + 1) return MT_ERR_PROTOCOL; + uint8_t level = p[0]; + uint64_t prefix = ms_read64(p + 1), after = ms_read64(p + 10); + if (level > MT_MAX_LEVEL || prefix != merkle_sync_level_prefix(prefix, level) || p[9] > 1) return MT_ERR_PROTOCOL; + if (level < MT_MAX_LEVEL && (p[9] || after)) return MT_ERR_PROTOCOL; + if (p[9] && merkle_sync_level_prefix(after, MT_MAX_LEVEL) != prefix) return MT_ERR_PROTOCOL; + s->remote_request_id = request; + if (level < MT_MAX_LEVEL) { + uint8_t hashes[MT_BUCKETS][MT_HASH_SIZE]; + if (merkle_tree_children(s->ms->inst->topo_sqlite_db, s->ns, level, prefix, hashes) < 0) return MT_ERR_IO; + return ms_send(s, MS_HASHES, request, &hashes[0][0], sizeof(hashes)) < 0 ? MT_ERR_IO : 0; + } + uint8_t body[MT_PAGE_SIZE - MS_HEADER]; + size_t size = sizeof(body) - 9; + uint64_t next = 0; int more = 0; + int rc = s->ms->ops->get_page(s->ms->data_ctx, s->ns, prefix, p[9], after, body + 9, &size, &next, &more); + if (rc < 0) return MT_ERR_DATA; + if (size > sizeof(body) - 9 || (more != 0 && more != 1) + || (more && (merkle_sync_level_prefix(next, MT_MAX_LEVEL) != prefix || (p[9] && next <= after)))) return MT_ERR_DATA; + body[0] = (uint8_t)more; ms_write64(body + 1, next); + return ms_send(s, MS_PAGE, request, body, size + 9) < 0 ? MT_ERR_IO : 0; +} + +static int ms_receive_hashes(struct ms_session* s, uint32_t request, const uint8_t* p, size_t len) { + if (s->state != MS_PULL || !s->waiting || request != s->request_id || len != MT_BUCKETS * MT_HASH_SIZE) return MT_ERR_PROTOCOL; + struct ms_frame* f = &s->stack[s->depth]; + if (f->level >= MT_MAX_LEVEL) return MT_ERR_PROTOCOL; + uint8_t local[MT_BUCKETS][MT_HASH_SIZE]; + if (merkle_tree_children(s->ms->inst->topo_sqlite_db, s->ns, f->level, f->prefix, local) < 0) return MT_ERR_IO; + f->children = 0; + for (unsigned i = 0; i < MT_BUCKETS; i++) + if (memcmp(local[i], p + i * MT_HASH_SIZE, MT_HASH_SIZE)) f->children |= UINT32_C(1) << i; + s->waiting = 0; + return ms_walk_next(s); +} + +static int ms_receive_page(struct ms_session* s, uint32_t request, const uint8_t* p, size_t len) { + if (s->state != MS_PULL || !s->waiting || request != s->request_id || len < 9 || p[0] > 1) return MT_ERR_PROTOCOL; + struct ms_frame* f = &s->stack[s->depth]; + uint64_t next = ms_read64(p + 1); + if (f->level != MT_MAX_LEVEL || (p[0] && (len == 9 || merkle_sync_level_prefix(next, MT_MAX_LEVEL) != f->prefix + || (s->after_valid && next <= s->after)))) return MT_ERR_PROTOCOL; + int rc = s->ms->ops->apply_items(s->ms->data_ctx, s->ns, s->peer, p + 9, len - 9); + if (rc < 0) return rc == MT_ERR_CONFLICT ? rc : MT_ERR_DATA; + s->waiting = 0; + if (p[0]) { s->after = next; s->after_valid = 1; return ms_query(s) < 0 ? MT_ERR_IO : 0; } + return ms_walk_next(s); +} + +static void ms_receive(struct ETCP_CONN* conn, struct ll_entry* entry) { + if (!entry) return; + struct merkle_sync* ms = conn && conn->instance ? conn->instance->msync : NULL; + if (!ms || !entry->dgram || entry->len < MS_HEADER || entry->len > MT_PAGE_SIZE) { + DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "merkle_sync: invalid packet size=%u", entry->len); + queue_dgram_free(entry); queue_entry_free(entry); return; } - int r = _send_msg(ms, peer, buf, (size_t)(p - buf)); - u_free(buf); - return r; -} - -/* ── Sessions ── */ - -static struct ms_session* _session_find(struct merkle_sync* ms, uint64_t peer, const char* ns) { - for (struct ms_session* s = ms->sessions; s; s = s->next) - if (s->peer == peer && strcmp(s->ns, ns) == 0) return s; - return NULL; -} - -static int _find_pending(struct ms_session* s, uint8_t level, uint64_t prefix) { - for (int i = 0; i < s->pending_count; i++) - if (s->pending[i].level == level && s->pending[i].prefix == prefix) - return i; - return -1; -} - -static void _session_done(struct ms_session* s, int result) { - if (s->sess_state == SESS_SYNCED) { DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: session ALREADY DONE peer=%016llx ns=%s", MS_ID, (unsigned long long)s->peer, s->ns); return; } - merkle_sync_done_cb cb = s->done_cb; void* arg = s->cb_arg; - s->done_cb = NULL; s->cb_arg = NULL; - if (result == MT_OK) { s->sess_state = SESS_SYNCED; DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: session SYNCED peer=%016llx ns=%s", MS_ID, (unsigned long long)s->peer, s->ns); } - else { s->sess_state = SESS_SYNCING; DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: session FAILED peer=%016llx ns=%s result=%d", MS_ID, (unsigned long long)s->peer, s->ns, result); } - if (cb) cb(s->peer, s->ns, result, arg); -} - -/* ── Recv handlers ── */ - -static void _handle_hashes(struct merkle_sync* ms, uint64_t peer, const char* ns, - const uint8_t* pl, size_t plen, int parent_idx) { - if (plen < 3) return; - uint8_t level = pl[0]; uint8_t pb = pl[1]; - if (level > MT_MAX_LEVEL || pb > 8 || (size_t)(3 + pb) > plen) { DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: handle_hashes bad level=%u prefix_bytes=%u plen=%zu", MS_ID, level, pb, plen); return; } - uint64_t prefix = _prefix_read(pl + 2, pb); - uint8_t is_data = pl[2 + pb]; - const uint8_t* payload = pl + 3 + pb; - size_t paylen = plen - 3 - pb; - DEBUG_TRACE(DEBUG_CATEGORY_MEMBER_SYNC, "%s: handle_hashes peer=%016llx ns=%s L%d/P%016llx is_data=%d", MS_ID, (unsigned long long)peer, ns, level, (unsigned long long)prefix, is_data); - - struct ms_session* s = _session_find(ms, peer, ns); - - if (is_data) { - if (paylen < 2) return; - uint16_t dlen; memcpy(&dlen, payload, 2); - const uint8_t* pd = payload + 2; - if (paylen - 2 < dlen) return; - ms->ops->apply_items(ms->data_ctx, ns, peer, pd, dlen); - if (s && s->sess_state == SESS_SYNCING) _session_done(s, MT_OK); - return; + const uint8_t* d = entry->dgram; + uint64_t ns_id = ms_read64(d + 1), round = ms_read64(d + 10); + uint32_t request = ms_read32(d + 18); + uint8_t type = d[9]; + char ns[21]; snprintf(ns, sizeof(ns), "%llu", (unsigned long long)ns_id); + if (ms->ops->validate_peer && ms->ops->validate_peer(ms->data_ctx, ns, conn->peer_node_id) != 1) { + DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "merkle_sync: unauthorized ns=%s peer=%016llx", ns, (unsigned long long)conn->peer_node_id); + queue_dgram_free(entry); queue_entry_free(entry); return; } - + struct ms_session* s = ms_find(ms, conn, ns_id); + if (!s && (type == MS_WAKE || type == MS_BEGIN)) s = ms_create(ms, conn, ns_id); if (!s) { - s = u_calloc(1, sizeof(*s)); if (!s) return; - snprintf(s->ns, sizeof(s->ns), "%s", ns); s->peer = peer; - s->next = ms->sessions; ms->sessions = s; - ms_session_setup(ms, s); - } else { - s->pending_count = 0; - } - s->sess_state = SESS_SYNCING; - - if (paylen < 4) return; - uint32_t remote_bm; memcpy(&remote_bm, payload, 4); - const uint8_t* rh = payload + 4; - - uint32_t local_bm; uint8_t lh[MT_BUCKETS][MT_HASH_SIZE]; - _get_level_hashes(ms, ns, level, prefix, pb, &local_bm, lh); - - uint32_t differs = remote_bm ^ local_bm; - for (int i = 0; i < MT_BUCKETS; i++) { - if (!(local_bm & (1u << i)) && !(remote_bm & (1u << i))) continue; - if ((local_bm & (1u << i)) && (remote_bm & (1u << i))) { - int rh_idx = 0; - for (int j = 0; j < i; j++) if (remote_bm & (1u << j)) rh_idx++; - if (memcmp(rh + (size_t)rh_idx * MT_HASH_SIZE, lh[i], MT_HASH_SIZE) != 0) - differs |= (1u << i); + DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "merkle_sync: no session ns=%s type=%u", ns, type); + queue_dgram_free(entry); queue_entry_free(entry); return; + } + s->received_bytes += entry->len; + const uint8_t* p = d + MS_HEADER; + size_t len = entry->len - MS_HEADER; + DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "merkle_sync: RX ns=%s peer=%016llx round=%llu type=%u request=%u bytes=%zu state=%d", + ns, (unsigned long long)s->peer, (unsigned long long)round, type, request, len, s->state); + int rc = 0, notify = 0; + if (type != MS_WAKE && type != MS_BEGIN && round != s->round) { + DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "merkle_sync: discard old round=%llu current=%llu", (unsigned long long)round, + (unsigned long long)s->round); + goto done; + } + if (type == MS_WAKE) { + if (!s->leader || round || request || len) rc = MT_ERR_PROTOCOL; + else { + /* Follower мог отменить и заново подключить namespace. Старый запрос уже некому обслужить. */ + ms_discard_tx(s); + s->previous_valid = 0; + rc = ms_begin(s); } - } - - DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "%s: handle_hashes peer=%016llx ns=%s level=%d remote_bm=%08x local_bm=%08x differs=%08x is_data=%d", - MS_ID, (unsigned long long)peer, ns, level, remote_bm, local_bm, differs, is_data); - - if (differs == 0 && level == 0) { - _send_hashes(ms, peer, ns, 0, 0, 0, 1); - _session_done(s, MT_OK); - return; - } - if (differs == 0) return; - - int next_shift = 64 - ((int)level + 1) * 5; - struct bucket_entry requests[MT_BUCKETS]; int rcount = 0; - for (int i = 0; i < MT_BUCKETS && rcount < 32; i++) { - if (!(differs & (1u << i))) continue; - uint8_t nl = (uint8_t)(level + 1); if (nl > MT_MAX_LEVEL) nl = MT_MAX_LEVEL; - requests[rcount].level = nl; - requests[rcount].prefix_bytes = merkle_sync_prefix_bytes(nl); - requests[rcount].prefix = prefix | ((uint64_t)i << (next_shift > 0 ? next_shift : 0)); - rcount++; - } - - if (rcount > 0) { - if (s) { - for (int i = 0; i < rcount && s->pending_count < MS_PENDING_MAX; i++) { - int idx = s->pending_count++; - s->pending[idx].level = requests[i].level; - s->pending[idx].prefix = requests[i].prefix; - s->pending[idx].parent = (int8_t)parent_idx; - s->pending[idx].sub_count = 0; - s->pending[idx].state = MS_PEND_WAITING; - } - if (parent_idx >= 0 && parent_idx < MS_PENDING_MAX) - s->pending[parent_idx].sub_count++; + } else if (type == MS_BEGIN) { + if (s->leader || !round || request || len) rc = MT_ERR_PROTOCOL; + else if (round <= s->round) { + DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "merkle_sync: discard duplicate BEGIN ns=%s", ns); + } else { + ms_discard_tx(s); + s->round = round; s->state = MS_SERVE; s->waiting = 0; + s->request_id = s->remote_request_id = 0; s->dirty = 0; + rc = ms_send(s, MS_READY, 0, NULL, 0) < 0 ? MT_ERR_IO : 0; } - size_t rs = 1 + 8 + 1; - for (int i = 0; i < rcount; i++) rs += 1 + 1 + requests[i].prefix_bytes + 1; - uint8_t* rbuf = u_malloc(rs); - if (rbuf) { - uint8_t* wr = rbuf; - ms_write_group_id(wr, ns); wr += 8; - *wr++ = 0x02; /* MS_MSG_REQUEST */ - *wr++ = (uint8_t)rcount; - for (int i = 0; i < rcount; i++) { - *wr++ = requests[i].level; - *wr++ = requests[i].prefix_bytes; - _prefix_write(wr, requests[i].prefix, requests[i].prefix_bytes); - wr += requests[i].prefix_bytes; - EVP_MD_CTX* tctx = EVP_MD_CTX_new(); - EVP_DigestInit_ex(tctx, EVP_sha256(), NULL); - int cnt = ms->ops->update_bucket_hash(ms->data_ctx, ns, - requests[i].level, requests[i].prefix, tctx); - if (cnt < 0) cnt = 0; - EVP_MD_CTX_free(tctx); - *wr++ = (uint8_t)(cnt < 8 || requests[i].level >= MT_MAX_LEVEL ? 1 : 0); + } else if (type == MS_READY) { + if (!s->leader || s->state != MS_BEGIN_WAIT || request || len) rc = MT_ERR_PROTOCOL; + else rc = ms_pull_start(s) < 0 ? MT_ERR_IO : 0; + } else if (type == MS_QUERY) rc = ms_serve_query(s, request, p, len); + else if (type == MS_HASHES) rc = ms_receive_hashes(s, request, p, len); + else if (type == MS_PAGE) rc = ms_receive_page(s, request, p, len); + else if (type == MS_TURN) { + if (s->leader || s->state != MS_SERVE || request || len) rc = MT_ERR_PROTOCOL; + else rc = ms_pull_start(s) < 0 ? MT_ERR_IO : 0; + } else if (type == MS_CHECK) { + uint8_t roots[2 * MT_HASH_SIZE]; + if (!s->leader || s->state != MS_SERVE || request || len != MT_HASH_SIZE) rc = MT_ERR_PROTOCOL; + else if (ms_root(s, roots) < 0) rc = MT_ERR_IO; + else if (!memcmp(roots, p, MT_HASH_SIZE)) { + if (ms->ops->finish && ms->ops->finish(ms->data_ctx, ns) < 0) rc = MT_ERR_DATA; + else { + s->previous_valid = 0; s->verified_revision = s->revision; + rc = ms_send(s, MS_DONE, 0, roots, MT_HASH_SIZE) < 0 ? MT_ERR_IO : 0; + } + } else { + memcpy(roots + MT_HASH_SIZE, p, MT_HASH_SIZE); + if (s->previous_valid && !memcmp(s->previous_roots, roots, sizeof(roots))) rc = MT_ERR_CONFLICT; + else { + memcpy(s->previous_roots, roots, sizeof(roots)); s->previous_valid = 1; + s->state = MS_IDLE; s->dirty = 1; rc = ms_schedule(s); } - _send_msg(ms, peer, rbuf, (size_t)(wr - rbuf)); - u_free(rbuf); } - } else { - if (s) _session_done(s, MT_OK); - } -} - -static void _handle_request(struct merkle_sync* ms, uint64_t peer, const char* ns, - const uint8_t* pl, size_t plen) { - if (plen < 1) return; - uint8_t count = pl[0]; const uint8_t* bp = pl + 1; size_t off = 0; - DEBUG_TRACE(DEBUG_CATEGORY_MEMBER_SYNC, "%s: handle_request peer=%016llx ns=%s count=%d", MS_ID, (unsigned long long)peer, ns, count); - - struct bucket_entry buckets[32]; int bc = 0; - for (uint8_t i = 0; i < count && bc < 32; i++) { - if (off + 2 > plen - 1) break; - uint8_t lvl = bp[off++]; uint8_t pb_i = bp[off++]; - if (off + pb_i > plen - 1) break; - uint64_t pr = _prefix_read(bp + off, pb_i); off += pb_i; - buckets[bc].level = lvl; buckets[bc].prefix_bytes = pb_i; buckets[bc].prefix = pr; bc++; - off++; /* skip is_terminal byte */ - } - if (bc > 0) { - _send_batch(ms, peer, ns, buckets, bc); - } -} - -static void _handle_batch(struct merkle_sync* ms, uint64_t peer, const char* ns, - const uint8_t* pl, size_t plen) { - if (plen < 1) return; - uint8_t count = pl[0]; const uint8_t* bp = pl + 1; size_t rem = plen - 1; - DEBUG_TRACE(DEBUG_CATEGORY_MEMBER_SYNC, "%s: handle_batch peer=%016llx ns=%s count=%d len=%zu", MS_ID, (unsigned long long)peer, ns, count, plen - 1); - - for (uint8_t i = 0; i < count && rem >= 3; i++) { - uint8_t lvl = bp[0]; uint8_t pb_i = bp[1]; rem -= 2; bp += 2; - if (rem < pb_i + 1) break; - uint64_t pr = _prefix_read(bp, pb_i); bp += pb_i; rem -= pb_i; - uint8_t is_data = *bp++; rem--; - - if (is_data && rem >= 4) { - uint16_t dlen; memcpy(&dlen, bp, 2); bp += 2; rem -= 2; - if (rem < dlen) break; - ms->ops->apply_items(ms->data_ctx, ns, peer, bp, dlen); - bp += dlen; rem -= dlen; - struct ms_session* sb = _session_find(ms, peer, ns); - if (sb) { int pidx = _find_pending(sb, lvl, pr); if (pidx >= 0) sb->pending[pidx].state = MS_PEND_RESOLVED; } - } else if (!is_data && rem >= 4) { - struct ms_session* sc = _session_find(ms, peer, ns); - int pidx = sc ? _find_pending(sc, lvl, pr) : -1; - if (pidx >= 0) sc->pending[pidx].state = MS_PEND_RESOLVED; - uint8_t sub_pl[4096]; size_t sub_len = 0; - sub_pl[sub_len++] = lvl; - sub_pl[sub_len++] = merkle_sync_prefix_bytes(lvl); - _prefix_write(sub_pl + sub_len, pr, merkle_sync_prefix_bytes(lvl)); - sub_len += merkle_sync_prefix_bytes(lvl); - sub_pl[sub_len++] = 0; /* is_data=0 */ - uint32_t bm; memcpy(&bm, bp, 4); bp += 4; rem -= 4; - memcpy(sub_pl + sub_len, &bm, 4); sub_len += 4; - int nh = _popcount_u32(bm); - if (rem >= (size_t)nh * MT_HASH_SIZE) { - memcpy(sub_pl + sub_len, bp, (size_t)nh * MT_HASH_SIZE); - sub_len += (size_t)nh * MT_HASH_SIZE; - bp += (size_t)nh * MT_HASH_SIZE; rem -= (size_t)nh * MT_HASH_SIZE; + } else if (type == MS_DONE) { + uint8_t root[MT_HASH_SIZE]; + if (s->leader || s->state != MS_DONE_WAIT || request || len != MT_HASH_SIZE) rc = MT_ERR_PROTOCOL; + else if (ms_root(s, root) < 0) rc = MT_ERR_IO; + else { + s->state = MS_IDLE; + s->dirty = memcmp(root, p, sizeof(root)) != 0; + if (s->dirty) rc = ms_schedule(s); + else { + DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "merkle_sync: converged ns=%s peer=%016llx round=%llu", + s->ns, (unsigned long long)s->peer, (unsigned long long)s->round); + notify = 1; } - _handle_hashes(ms, peer, ns, sub_pl, sub_len, pidx); } - } - { - struct ms_session* s = _session_find(ms, peer, ns); - if (!s) return; - int all_done = (s->pending_count > 0); - for (int i = 0; i < s->pending_count; i++) { - if (s->pending[i].state != MS_PEND_RESOLVED) { all_done = 0; break; } + } else if (type == MS_ERROR) { + if (request || len != 4 || ms_read32(p) < 1 || ms_read32(p) > 5) rc = MT_ERR_PROTOCOL; + else { + rc = -(int)ms_read32(p); + queue_dgram_free(entry); queue_entry_free(entry); + ms_fail(s, rc, "remote error", 0); return; } - if (all_done && s->sess_state == SESS_SYNCING) { - _send_hashes(ms, peer, ns, 0, 0, 0, 1); - _session_done(s, MT_OK); + } else rc = MT_ERR_PROTOCOL; +done: + queue_dgram_free(entry); queue_entry_free(entry); + if (rc < 0) { ms_fail(s, rc, "receive", 1); return; } + if (notify) ms_notify(s, MT_OK); +} + +static void ms_conn_status(struct ETCP_CONN* conn, int event, void* arg) { + struct merkle_sync* ms = arg; + if (event != ETCP_CONN_STATUS_DOWN && event != ETCP_CONN_STATUS_DELETE && event != ETCP_CONN_STATUS_REINIT) return; + ms->closing_conn = conn; + /* DELETE приходит до освобождения normalizer/input. Отмена использует сохранённую очередь. */ + for (;;) { + struct ms_session* s = ms->sessions; + while (s && s->conn != conn) s = s->next; + if (!s) break; + /* Удаляем до callback, чтобы повторный start не мог сохранить умирающую сессию. */ + struct ms_request* requests = s->requests; s->requests = NULL; + uint64_t peer = s->peer; char ns[21]; memcpy(ns, s->ns, sizeof(ns)); + ms_free(s); + DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "merkle_sync: detach ns=%s peer=%016llx event=%d", ns, (unsigned long long)peer, event); + while (requests) { + struct ms_request* next = requests->next; + merkle_sync_done_cb cb = requests->cb; void* cb_arg = requests->arg; + u_free(requests); cb(peer, ns, MT_ERR_DISCONNECTED, cb_arg); requests = next; } } + ms->closing_conn = NULL; } -static void _recv_cb(struct ETCP_CONN* conn, struct ll_entry* entry) { - if (!entry || entry->len < 10) { - if (entry) { if (entry->dgram) u_free(entry->dgram); queue_entry_free(entry); } - return; - } - struct UTUN_INSTANCE* inst = conn ? conn->instance : NULL; - struct merkle_sync* ms = inst ? inst->msync : NULL; - if (!ms || !ms->initialized) { u_free(entry->dgram); queue_entry_free(entry); return; } - uint64_t peer = conn ? conn->peer_node_id : 0; - const uint8_t* d = entry->dgram; - size_t dlen = entry->len; - - uint64_t group_id = be64toh(*(const uint64_t*)(d + 1)); - char ns[64]; - if (ms_group_id_to_ns(inst, group_id, ns, sizeof(ns)) != 0) { u_free(entry->dgram); queue_entry_free(entry); return; } - uint8_t type = d[9]; - - /* валидация пира: только авторизованные узлы (мемберы канала) могут синхронизироваться */ - if (ms->ops->validate_peer && !ms->ops->validate_peer(ms->data_ctx, ns, peer)) { - DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: recv DROP — peer=0x%016llx not authorized for ns=%s type=%02x", - MS_ID, (unsigned long long)peer, ns, type); - u_free(entry->dgram); queue_entry_free(entry); return; - } - DEBUG_TRACE(DEBUG_CATEGORY_MEMBER_SYNC, "%s: recv type=%02x from=%016llx group=%016llx ns=%s len=%zu", MS_ID, type, (unsigned long long)peer, (unsigned long long)group_id, ns, dlen); - const uint8_t* pl = d + 10; - size_t plen = dlen - 10; - - switch (type) { - case 0x01: _handle_hashes(ms, peer, ns, pl, plen, -1); break; - case 0x02: _handle_request(ms, peer, ns, pl, plen); break; - case 0x03: _handle_batch(ms, peer, ns, pl, plen); break; - case 0x04: - if (plen >= 9 && ms->ops->apply_update) { - uint64_t key; memcpy(&key, pl, 8); - uint8_t utype = pl[8]; - DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "%s: recv ITEM_UPDATE key=%016llx type=%02x from=%016llx ns=%s", - MS_ID, (unsigned long long)key, utype, (unsigned long long)peer, ns); - ms->ops->apply_update(ms->data_ctx, ns, key, utype, pl + 9, plen - 9); - } - break; +int merkle_sync_init(struct UTUN_INSTANCE* inst, uint8_t svc_id, const struct merkle_sync_data_ops* ops, void* data_ctx) { + if (!inst || inst->msync || !inst->ua || !inst->topo_sqlite_db || !ops || !ops->update_bucket_hash || !ops->get_page || !ops->apply_items) { + DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "merkle_sync: invalid initialization"); return -1; } - - u_free(entry->dgram); queue_entry_free(entry); -} - -/* ── Push lightweight update to synced peers ── */ - -void merkle_sync_push_update(struct UTUN_INSTANCE* inst, const char* ns, - uint64_t key, uint8_t type, const uint8_t* data, size_t len) { - struct merkle_sync* ms = inst ? inst->msync : NULL; - if (!ms || !ms->initialized || !ns || !data) return; - DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "%s: push_update ns=%s key=%016llx type=%02x len=%zu", - MS_ID, ns, (unsigned long long)key, type, len); - - size_t pkt = 1 + 8 + 1 + 8 + 1 + len; - uint8_t* buf = u_malloc(pkt); - if (!buf) return; - uint8_t* p = buf; - ms_write_group_id(p, ns); p += 8; - *p++ = 0x04; /* MSG_ITEM_UPDATE */ - memcpy(p, &key, 8); p += 8; - *p++ = type; - memcpy(p, data, len); - - for (struct ms_session* s = ms->sessions; s; s = s->next) { - if (s->sess_state != SESS_SYNCED || strcmp(s->ns, ns) != 0) continue; - ms_outq_enqueue(s, buf, pkt); - } - u_free(buf); -} - -/* ── Broadcast to synced peers ── */ - -void merkle_sync_broadcast(struct UTUN_INSTANCE* inst, const char* ns, - uint64_t from_peer, const uint8_t* data, size_t len) { - struct merkle_sync* ms = inst ? inst->msync : NULL; - if (!ms || !ms->initialized || !ns || !data || len < 2) return; - DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "%s: broadcast ns=%s from=%016llx len=%zu", - MS_ID, ns, (unsigned long long)from_peer, len); - for (struct ms_session* s = ms->sessions; s; s = s->next) { - if (s->peer == from_peer || strcmp(s->ns, ns) != 0) continue; - _broadcast_to_session(ms, s, ns, data, len); - } -} - -int merkle_sync_send_to(struct UTUN_INSTANCE* inst, const char* ns, - uint64_t peer, const uint8_t* data, size_t len) { - struct merkle_sync* ms = inst ? inst->msync : NULL; - if (!ms || !ms->initialized || !ns || !data || len < 2) return -1; - struct ms_session* s = _session_find(ms, peer, ns); - if (!s) { DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "%s: send_to — no session peer=%016llx ns=%s", MS_ID, (unsigned long long)peer, ns); return -1; } - return _broadcast_to_session(ms, s, ns, data, len); -} - -/* ── Background consistency check ── */ - -int merkle_sync_bg_check(struct UTUN_INSTANCE* inst, const char* ns) { - struct merkle_sync* ms = inst ? inst->msync : NULL; - if (!ms || !ms->initialized) return -1; - sqlite3* db = _db(inst); if (!db || !ns) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: bg_check — db or ns is NULL db=%p ns=%s", MS_ID, (void*)db, ns ? ns : "(null)"); return -1; } - - sqlite3_stmt* stmt = NULL; - if (sqlite3_prepare_v2(db, - "SELECT prefix64 FROM merkle_tree_hash WHERE namespace=? AND level=? LIMIT 1", - -1, &stmt, NULL) != SQLITE_OK) return -1; - sqlite3_bind_text(stmt, 1, ns, -1, SQLITE_STATIC); - sqlite3_bind_int(stmt, 2, MT_MAX_LEVEL); - - if (sqlite3_step(stmt) != SQLITE_ROW) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: bg_check — no hash row for ns=%s", MS_ID, ns); sqlite3_finalize(stmt); return -1; } - uint64_t pref = (uint64_t)sqlite3_column_int64(stmt, 0); - sqlite3_finalize(stmt); - - uint8_t old_hash[MT_HASH_SIZE]; - memcpy(old_hash, merkle_sync_get_hash(inst, ns, MT_MAX_LEVEL, pref), MT_HASH_SIZE); - - _recompute_bucket(ms, ns, MT_MAX_LEVEL, pref); - - const uint8_t* new_hash = merkle_sync_get_hash(inst, ns, MT_MAX_LEVEL, pref); - if (memcmp(old_hash, new_hash, MT_HASH_SIZE) != 0) { - for (uint8_t lv = (uint8_t)(MT_MAX_LEVEL - 1); lv >= 1; lv--) - _recompute_bucket(ms, ns, lv, merkle_sync_level_prefix(pref, lv)); - return 1; - } - return 0; -} - -static void _wal_timer_cb(void* arg) { - struct merkle_sync* ms = (struct merkle_sync*)arg; - if (!ms || !ms->initialized || !ms->inst) return; -#ifdef UTUN_HAVE_STANDBY - if (standby_get_sleep_tb() > 0) { - ms->wal_wait = standby_wait(ms, _wal_timer_cb); - return; - } - ms->wal_wait = NULL; -#endif - sqlite3* db = _db(ms->inst); if (!db) { ms->wal_timer = NULL; return; } - - int pn = 0, ckpt = 0; - sqlite3_wal_checkpoint_v2(db, NULL, SQLITE_CHECKPOINT_PASSIVE, &pn, &ckpt); - if (pn > 0 || ckpt > 0) - DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "%s: WAL checkpoint done: %d frames in log, %d checkpointed", MS_ID, pn, ckpt); - - ms->wal_timer = uasync_set_timeout(ms->inst->ua, (uint32_t)(MS_WAL_INTERVAL_MS * 10), - ms, _wal_timer_cb, "ms_wal"); -} - -/* ── Public lifecycle ── */ - -int merkle_sync_init(struct UTUN_INSTANCE* inst, uint8_t svc_id, - const struct merkle_sync_data_ops* ops, void* data_ctx) { - if (!inst || !ops) return -1; - DEBUG_TRACE(DEBUG_CATEGORY_MEMBER_SYNC, "%s: init svc=%02x", MS_ID, svc_id); + if (merkle_tree_init(inst->topo_sqlite_db) < 0) return -1; struct merkle_sync* ms = u_calloc(1, sizeof(*ms)); - if (!ms) return -1; + if (!ms) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "merkle_sync: allocation failed"); return -1; } ms->inst = inst; ms->svc_id = svc_id; ms->ops = ops; ms->data_ctx = data_ctx; - ms->initialized = 1; + /* Round counter is local to a connection lifetime; monotonic across session cancellation. */ + ms->round_seq = get_time_tb(); inst->msync = ms; - - _ensure_table(ms); - - etcp_bind(inst, svc_id, _recv_cb); - - ms->wal_timer = uasync_set_timeout(inst->ua, (uint32_t)(MS_WAL_INTERVAL_MS * 10), - ms, _wal_timer_cb, "ms_wal"); - - DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: initialized svc=%02x", MS_ID, svc_id); + etcp_bind(inst, svc_id, ms_receive); + etcp_add_conn_status_cbk(inst, ms_conn_status, ms); + DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "merkle_sync: initialized service=%u protocol=2", svc_id); return 0; } void merkle_sync_destroy(struct UTUN_INSTANCE* inst) { - if (!inst) return; - struct merkle_sync* ms = inst->msync; + struct merkle_sync* ms = inst ? inst->msync : NULL; if (!ms) return; - ms->initialized = 0; inst->msync = NULL; - + etcp_remove_conn_status_cbk(inst, ms_conn_status, ms); etcp_unbind(inst, ms->svc_id); - - if (ms->wal_timer) { uasync_cancel_timeout(inst->ua, ms->wal_timer); ms->wal_timer = NULL; } -#ifdef UTUN_HAVE_STANDBY - if (ms->wal_wait) { standby_wait_cancel(ms->wal_wait); ms->wal_wait = NULL; } -#endif - - struct ms_session* s = ms->sessions; - while (s) { struct ms_session* next = s->next; - ms_session_outq_free(s); - u_free(s); s = next; } - - u_free(ms); + while (ms->sessions) ms_free(ms->sessions); + inst->msync = NULL; u_free(ms); } -/* ── Public async sync ── */ - -int merkle_sync_start(struct UTUN_INSTANCE* inst, uint64_t peer, - const char* ns, merkle_sync_done_cb done_cb, void* arg) { +int merkle_sync_start(struct UTUN_INSTANCE* inst, uint64_t peer, const char* ns, merkle_sync_done_cb cb, void* arg) { struct merkle_sync* ms = inst ? inst->msync : NULL; - if (!inst || !ns || !ms || !ms->initialized) return -1; - _ensure_table(ms); - - /* пир в SLEEP-фазе — не инициируем синхронизацию (server-side throttling) */ - { - struct ETCP_CONN* pc = ms_find_conn_for_node(inst, peer); - if (pc && pc->peer_sleep_phase) { - DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "%s: start SKIP — peer=0x%016llx sleeping", MS_ID, (unsigned long long)peer); - return 0; - } - } - - /* я в SLEEP-фазе — не инициирую синхронизацию (client-side throttling) */ -#ifdef UTUN_HAVE_STANDBY - if (standby_get_sleep_tb() > 0) return 0; -#endif - - struct ms_session* s = _session_find(ms, peer, ns); - DEBUG_TRACE(DEBUG_CATEGORY_MEMBER_SYNC, "%s: start peer=%016llx ns=%s new=%d", MS_ID, (unsigned long long)peer, ns, s ? 0 : 1); - if (!s) { - s = u_calloc(1, sizeof(*s)); - if (!s) return -1; - snprintf(s->ns, sizeof(s->ns), "%s", ns); - s->peer = peer; - s->next = ms->sessions; - ms->sessions = s; - ms_session_setup(ms, s); - } else { - s->pending_count = 0; - DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "%s: start OVERWRITE peer=%016llx ns=%s", MS_ID, (unsigned long long)peer, ns); + uint64_t ns_id; + struct ETCP_CONN* conn = inst ? instance_find_conn(inst, peer) : NULL; + if (!ms || ms_namespace(ns, &ns_id) < 0 || !conn || !conn->initialized || !conn->links_up || conn->state == 2 + || conn->peer_sleep_phase || peer == inst->node_id || conn == ms->closing_conn) { + DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "merkle_sync: start unavailable ns=%s peer=%016llx", ns ? ns : "-", (unsigned long long)peer); + return -1; } - s->sess_state = SESS_SYNCING; - s->done_cb = done_cb; s->cb_arg = arg; - - _send_hashes(ms, peer, ns, 0, 0, 0, 0); + struct ms_request* r = NULL; + if (cb) { + r = u_calloc(1, sizeof(*r)); + if (!r) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "merkle_sync: request allocation failed"); return -1; } + r->cb = cb; r->arg = arg; + } + struct ms_session* s = ms_find(ms, conn, ns_id); + if (!s) s = ms_create(ms, conn, ns_id); + if (!s) { u_free(r); return -1; } + if (s->state == MS_FAILED) { s->state = MS_IDLE; s->previous_valid = 0; } + s->dirty = 1; + if (s->state == MS_IDLE && ms_schedule(s) < 0) { + u_free(r); ms_fail(s, MT_ERR_IO, "start scheduling", 1); return -1; + } + if (r) { r->next = s->requests; s->requests = r; } return 0; } void merkle_sync_cancel(struct UTUN_INSTANCE* inst, uint64_t peer, const char* ns) { struct merkle_sync* ms = inst ? inst->msync : NULL; if (!ms || !ns) return; - DEBUG_TRACE(DEBUG_CATEGORY_MEMBER_SYNC, "%s: cancel peer=%016llx ns=%s", MS_ID, (unsigned long long)peer, ns); - struct ms_session** p = &ms->sessions; - while (*p) { - struct ms_session* s = *p; - if (s->peer == peer && strcmp(s->ns, ns) == 0) { - *p = s->next; - ms_session_outq_free(s); - u_free(s); - return; - } - p = &(*p)->next; - } + struct ms_session* s = ms->sessions; + while (s) { struct ms_session* next = s->next; if (s->peer == peer && !strcmp(s->ns, ns)) ms_free(s); s = next; } } void merkle_sync_cancel_ns(struct UTUN_INSTANCE* inst, const char* ns) { struct merkle_sync* ms = inst ? inst->msync : NULL; if (!ms || !ns) return; - DEBUG_TRACE(DEBUG_CATEGORY_MEMBER_SYNC, "%s: cancel_ns ns=%s", MS_ID, ns); - struct ms_session** p = &ms->sessions; - int removed = 0; - while (*p) { - struct ms_session* s = *p; - if (strcmp(s->ns, ns) == 0) { - *p = s->next; - ms_session_outq_free(s); - u_free(s); - removed++; - continue; - } - p = &(*p)->next; - } - if (removed) DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: cancel_ns ns=%s removed=%d", MS_ID, ns, removed); + struct ms_session* s = ms->sessions; + while (s) { struct ms_session* next = s->next; if (!strcmp(s->ns, ns)) ms_free(s); s = next; } } void merkle_sync_cancel_peer(struct UTUN_INSTANCE* inst, uint64_t peer) { struct merkle_sync* ms = inst ? inst->msync : NULL; if (!ms) return; - struct ms_session** p = &ms->sessions; - int removed = 0; - while (*p) { - struct ms_session* s = *p; - if (s->peer == peer) { - *p = s->next; - ms_session_outq_free(s); - u_free(s); - removed++; - continue; - } - p = &(*p)->next; - } - if (removed) DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: cancel_peer peer=%016llx removed=%d", MS_ID, (unsigned long long)peer, removed); + struct ms_session* s = ms->sessions; + while (s) { struct ms_session* next = s->next; if (s->peer == peer) ms_free(s); s = next; } } diff --git a/src/chat/merkle_sync.h b/src/chat/merkle_sync.h index 2236bb7e..64f7c546 100644 --- a/src/chat/merkle_sync.h +++ b/src/chat/merkle_sync.h @@ -1,319 +1,61 @@ #ifndef MERKLE_SYNC_H #define MERKLE_SYNC_H -#include +#include "merkle_tree.h" #include -#include struct UTUN_INSTANCE; -#define MT_MAX_LEVEL 5 -#define MT_BUCKETS 32 -#define MT_HASH_SIZE 32 - -/* - * ── Архитектура ── - * - * merkle_sync — универсальный протокол синхронизации на Merkle-деревьях. - * Группирует элементы (items) в префиксное дерево: 5 уровней × 32 бакета, - * SHA256-хеши. Два пира обмениваются хешами уровней, находят различающиеся - * бакеты и передают только их содержимое. - * - * merkle_sync не знает, что такое "элемент" — потребитель (member_sync) - * предоставляет четыре коллбэка, описывающих модель данных: - * - * update_bucket_hash — перечислить элементы в диапазоне ключей, - * вычислить хеш каждого и подать в SHA256-контекст - * get_items — сериализовать элементы бакета в wire-формат - * apply_items — десериализовать и сохранить полученные элементы - * apply_update — применить лёгкое обновление (MSG_ITEM_UPDATE) - * - * Потребитель (chat_sync/chat_core) работает только с member_sync - * и не видит merkle_sync напрямую: - * - * // ── разово ── - * member_sync_init(inst); - * - * // ── запустил синхронизацию — забыл ── - * member_sync_start(inst, peer, ch_id, on_done, my_ctx); - * - * // ── получил результат ── - * static void on_done(uint64_t peer, const char* ch_id, int result, void* arg) { - * if (result == MT_OK) printf("sync ok\n"); - * if (result == MT_ERR_TIMEOUT) printf("timeout\n"); - * } - * - * // ── изменил данные — дерево само пересчиталось ── - * member_sync_put(inst, ch_id, node_id, x25519, ed25519, join_sig, addrs, ac); - * - * // ── отменил — коллбэк не вызовется ── - * member_sync_cancel(inst, peer, ch_id); - * - * // ── разово ── - * member_sync_destroy(inst); - * - * ── Формат дерева ── - * - * 5-уровневое префиксное дерево над 64-битными ключами (node_id). - * Каждый уровень берёт 5 старших бит ключа: уровень 1 — биты 59-63, - * уровень 5 — биты 39-63. Каждый узел дерева разбивается на 32 бакета - * (по 5 бит = 32 комбинации). - * - * level 1 [0..31] корень: 32 бакета по 5 бит - * level 2 [0..31]...[0..31] каждый — ещё 32 бакета - * ... - * level 5 [0..31].........[0..31] листья: 32^5 = 33M бакетов макс - * - * Хеш бакета = SHA256(хеш_элемента_1 || ... || хеш_элемента_N). - * Бакет считается терминальным (leaf) на уровне 5 или если в нём < 8 элементов. - * - * ── Wire-протокол (сервис ETCP, id задаётся при init) ── - * - * Каждое сообщение: [svc_id:1][ns_len:1][ns:var][type:1][payload:var] - * - * MSG_HASHES (0x01): level,prefix,is_data, [bitmap+hashes | member_data] - * MSG_REQUEST (0x02): count, [level,prefix,is_terminal]* - * MSG_BATCH (0x03): count, [level,prefix,is_terminal,[member_data|bitmap+hashes]]* - * - * Алгоритм: - * A → MSG_HASHES(level=1, bitmap+hashes всех 32 бакетов уровня 2) → B - * B сравнивает со своим деревом, находит различающиеся бакеты - * B → MSG_REQUEST(level=2, prefix=X, is_terminal) → A - * A → MSG_BATCH(данные бакета) → B - * B сохраняет, пересчитывает хеши - * Рекурсивно для подбакетов, пока хеши не совпадут. - * - * ── Сессии ── - * - * Протокол работает поверх надёжного транспорта (ETCP). - * Таймаутов и ретраев нет — при ошибке сессия завершается. - */ - -/* ── Data model callbacks ── */ - +#define MT_OK 0 +#define MT_ERR_IO -1 +#define MT_ERR_PROTOCOL -2 +#define MT_ERR_DATA -3 +#define MT_ERR_CONFLICT -4 +#define MT_ERR_DISCONNECTED -5 +#define MT_PAGE_SIZE 16384 + +/* Namespace — каноническая десятичная строка uint64_t. Движок не знает CHAT-групп. + * SHA256 листа обновляется моделью в порядке unsigned key; родителей считает merkle_tree. + * Все операции выполняются в uasync-потоке экземпляра. */ struct merkle_sync_data_ops { - /* - * Подать хеши всех элементов в префиксном диапазоне в sha_ctx. - * - * ctx — data_ctx, переданный в merkle_sync_init - * ns — namespace (например channel_id) - * level — уровень дерева (1..5) - * prefix64 — префикс ключа (старшие level*5 бит, остальные нули) - * sha_ctx — уже инициализирован (EVP_DigestInit_ex), потребитель - * делает только EVP_DigestUpdate(sha_ctx, item_hash, 32) - * для каждого элемента - * - * Возвращает количество элементов (0 = бакет пуст, -1 = ошибка). - * - * Пример реализации для мемберов: - * SELECT ... FROM peers_ JOIN nodes - * WHERE (node_id & mask) == prefix64 ORDER BY node_id - * для каждой строки: _compute_member_hash() → EVP_DigestUpdate() - */ - int (*update_bucket_hash)(void* ctx, const char* ns, uint8_t level, - uint64_t prefix64, EVP_MD_CTX* sha_ctx); - - /* - * Сериализовать элементы бакета в wire-формат. - * - * buf — буфер для записи (выделяет merkle_sync, мин. 64K) - * *len — [in] размер буфера, [out] записанный размер - * - * Wire-формат для мемберов: - * [count:2][node_id:8][x25519:32][ed25519:32][flags:1]([join_sig:64][join_ts:8])[update_sig:64][update_ts:8][userinfo_len:1][userinfo:var][adm_tags_len:1][adm_tags:var][adm_tags_sig:64]... - * - * Возвращает 0 при успехе, <0 при ошибке, -2 если буфер мал. - */ - int (*get_items)(void* ctx, const char* ns, uint8_t level, - uint64_t prefix, uint8_t pbytes, uint8_t* buf, size_t* len); - - /* - * Десериализовать и сохранить элементы из wire-формата. - * - * data, len — данные в том же формате, что выдаёт get_items - * - * Вызывается при приёме MSG_HASHES(is_data=1) или MSG_BATCH(is_terminal=1). - * Должна сохранить элементы в БД и вызвать merkle_sync_recompute_path - * для каждого изменённого ключа (через member_sync_put). - * - * Возвращает 0 при успехе, <0 при ошибке. - */ - int (*apply_items)(void* ctx, const char* ns, uint64_t from_peer, - const uint8_t* data, size_t len); - - /* - * Применить лёгкое обновление (не влияющее на Merkle-хеш). - * Вызывается при приёме MSG_ITEM_UPDATE(0x04) — online-статус и т.п. - * - * ns — namespace (channel_id) - * key — node_id изменившегося узла - * type — тип обновления (0x01 = online-статус) - * data,len — данные обновления (type=0x01: [online:1]) - * - * Возвращает 0 при успехе, <0 при ошибке. - */ - int (*apply_update)(void* ctx, const char* ns, uint64_t key, - uint8_t type, const uint8_t* data, size_t len); - - /* - * Проверить, что пир авторизован для namespace ns (напр. его pubkey есть - * в таблице мемберов канала). Вызывается в _recv_cb для КАЖДОГО входящего - * пакета до диспатча. Возвращает 1 если разрешено, 0 если нет. - * Может быть NULL — тогда проверка пропускается (generic-потребитель). - */ + merkle_leaf_hash_fn update_bucket_hash; + /* Одна страница листа: after_valid=0 начинает обход, иначе только key > after. + * len: ёмкость -> размер; next — последний ключ; more — есть продолжение. + * Пустой результат разрешён только с more=0. Ошибка не кодируется пустой страницей. */ + int (*get_page)(void* ctx, const char* ns, uint64_t prefix, int after_valid, uint64_t after, + uint8_t* buf, size_t* len, uint64_t* next, int* more); + /* Проверить весь формат, применить записи и обновить дерево в одной DB-транзакции. + * <0 — ошибка. Никаких broadcast/send-back из этого callback. */ + int (*apply_items)(void* ctx, const char* ns, uint64_t from_peer, const uint8_t* data, size_t len); int (*validate_peer)(void* ctx, const char* ns, uint64_t peer); + /* Проверить отложенные зависимости модели после обоих проходов, до подтверждения корня. */ + int (*finish)(void* ctx, const char* ns); }; -/* - * Коллбэк завершения синхронизации. - * - * peer — node_id пира - * ns — namespace (channel_id) - * result — MT_OK (0) = успех, MT_ERR_TIMEOUT (-1) = зарезервировано (в протоколе не возникает) - * arg — пользовательский контекст - * - * При отмене через merkle_sync_cancel() коллбэк НЕ вызывается. - */ - -#define MT_OK 0 -#define MT_ERR_TIMEOUT -1 - typedef void (*merkle_sync_done_cb)(uint64_t peer, const char* ns, int result, void* arg); -/* ── Lifecycle ── */ - -/* - * Инициализировать модуль. Создаёт таблицу merkle_tree_hash, биндит - * ETCP-сервис svc_id (напр. 0x31), запускает фоновую проверку (bg_timer). - * Вызывается один раз, обычно из member_sync_init(). - * - * inst — экземпляр uTun (нужен для etcp_send, uasync, sqlite3) - * svc_id — ID сервиса на ETCP-роутере - * ops — коллбэки модели данных (update_bucket_hash, get_items, apply_items) - * data_ctx — прозрачный контекст, передаваемый в коллбэки первым аргументом - * - * Возвращает 0 при успехе, -1 при ошибке. - */ -int merkle_sync_init(struct UTUN_INSTANCE* inst, uint8_t svc_id, - const struct merkle_sync_data_ops* ops, void* data_ctx); - -/* - * Завершить модуль. Отвязывает ETCP-сервис, останавливает bg_timer, - * удаляет все активные сессии (коллбэки НЕ вызываются). - */ +int merkle_sync_init(struct UTUN_INSTANCE* inst, uint8_t svc_id, + const struct merkle_sync_data_ops* ops, void* data_ctx); void merkle_sync_destroy(struct UTUN_INSTANCE* inst); -/* ── Async sync ── */ - -/* - * Запустить синхронизацию namespace ns с пиром peer. - * Отправляет MSG_HASHES(level=0). Таймаута нет — сессия завершается при - * совпадении корневого хеша либо удаляется вручную (cancel/destroy). - * - * Если сессия для (peer, ns) уже существует — перезапускает её - * с новым коллбэком (старый коллбэк теряется без вызова). - * - * Возвращает 0 при успехе, -1 при ошибке. - */ -int merkle_sync_start(struct UTUN_INSTANCE* inst, uint64_t peer, - const char* ns, merkle_sync_done_cb done_cb, void* arg); - -/* - * Отменить синхронизацию. Удаляет сессию, done_cb НЕ вызывается. - * Безопасно вызывать, если сессия не существует. - */ +/* Подключить namespace к текущему ETCP-соединению и запросить контрольную точку. + * Успех — подтверждение одинакового корня конкретного раунда, не вечный статус. + * Повторные запросы объединяются; каждый callback вызывается один раз, обход не сбрасывается. + * После первого start изменения дерева автоматически синхронизируются с этим пиром. + * <0: запрос не принят, callback не вызывается. DOWN/DELETE завершают запросы ошибкой. + * Явные cancel/destroy освобождают запросы без callback. + * Callback может отменить сессию; уничтожение всего экземпляра должно быть отложенным. */ +int merkle_sync_start(struct UTUN_INSTANCE* inst, uint64_t peer, const char* ns, merkle_sync_done_cb cb, void* arg); void merkle_sync_cancel(struct UTUN_INSTANCE* inst, uint64_t peer, const char* ns); - -/* - * Отменить и удалить ВСЕ сессии для namespace ns (независимо от пира). - * Используется при локальном удалении канала. done_cb НЕ вызывается. - */ void merkle_sync_cancel_ns(struct UTUN_INSTANCE* inst, const char* ns); - -/* - * Отменить и удалить ВСЕ сессии с конкретным пиром peer (независимо от ns). - * Используется при уходе пира в спячку (server-side throttling). done_cb НЕ вызывается. - */ void merkle_sync_cancel_peer(struct UTUN_INSTANCE* inst, uint64_t peer); -/* ── Recompute tree path after data change ── */ - -/* - * Пересчитать Merkle-дерево для ключа key — все уровни от 1 до 5. - * Вызывается после изменения данных: merkle_sync сам не знает, - * когда данные изменились — потребитель должен вызвать явно. - * member_sync делает это внутри put/del. - * - * Возвращает 1 если хеш хотя бы одного бакета изменился (данные реально - * изменились), 0 если всё совпало, -1 при ошибке. - */ -int merkle_sync_recompute_path(struct UTUN_INSTANCE* inst, const char* ns, uint64_t key); - -/* - * Отправить лёгкое обновление всем synced-пирам в namespace ns. - * Не влияет на Merkle-дерево — используется для online-статуса и т.п. - * - * ns — namespace (channel_id) - * key — node_id изменившегося узла - * type — тип обновления (0x01 = online-статус) - * data,len — данные обновления - */ -void merkle_sync_push_update(struct UTUN_INSTANCE* inst, const char* ns, - uint64_t key, uint8_t type, const uint8_t* data, size_t len); - -/* - * Переслать данные всем SYNCED-сессиям (кроме from_peer) в namespace ns. - * Используется для relay: когда apply_items обнаружил реальные изменения, - * потребитель вызывает эту функцию чтобы разослать дельту остальным пирам. - * - * data, len — wire-формат [count:2][key:8][val:4]... как от get_items. - */ -void merkle_sync_broadcast(struct UTUN_INSTANCE* inst, const char* ns, - uint64_t from_peer, const uint8_t* data, size_t len); - -/* - * Отправить данные одного мембера конкретному пиру (а не всем). - * Используется для "send back" при обнаружении stale-версии. - * data, len — wire-формат [count:2][key:8][val:4]... как от get_items. - * Возвращает 0 при успехе, -1 если нет сессии/соединения к пиру. - */ -int merkle_sync_send_to(struct UTUN_INSTANCE* inst, const char* ns, - uint64_t peer, const uint8_t* data, size_t len); - -/* ── Background consistency check ── */ - -/* - * Проверить консистентность одного случайного level-5 бакета в ns. - * Пересчитывает хеш из данных, сравнивает с сохранённым в merkle_tree_hash. - * При расхождении — пересчитывает весь путь вверх до корня. - * Возвращает 1 если были изменения, 0 если совпало, -1 ошибка. - */ -int merkle_sync_bg_check(struct UTUN_INSTANCE* inst, const char* ns); - -/* ── Test helper ── */ - -/* - * Получить сохранённый хеш бакета из merkle_tree_hash. - * Возвращает указатель на статический буфер (32 байта), перезаписывается - * при следующем вызове. Если бакет не найден — возвращает нулевой хеш. - */ -const uint8_t* merkle_sync_get_hash(struct UTUN_INSTANCE* inst, - const char* ns, uint8_t level, uint64_t prefix64); - -/* ── Prefix arithmetic (pure, exported for convenience) ── */ - -/* - * Выделить level*5 старших бит из 64-битного ключа. - * Уровень 1: биты 59-63 (сдвиг 59) - * Уровень 5: биты 39-63 (сдвиг 39) - * Пример: merkle_sync_level_prefix(0x1234567890ABCDEF, 1) = 0x1000000000000000 - */ -uint64_t merkle_sync_level_prefix(uint64_t key, uint8_t level); - -/* - * Количество байт, необходимое для хранения префикса уровня level. - * Уровень 1: 1 байт (5 бит), уровень 4: 3 байта (20 бит), уровень 5: 4 байта (25 бит). - */ -uint8_t merkle_sync_prefix_bytes(uint8_t level); +/* Обновление индекса вызывается моделью данных. + * При внешней транзакции caller вызывает changed ПОСЛЕ commit. + * Без внешней транзакции пересчёт сам создаёт savepoint и уведомляет после commit. + * Возвращает 1=изменилось, 0=no-op, <0=ошибка. */ +int merkle_sync_recompute_path(struct UTUN_INSTANCE* inst, const char* ns, uint64_t key); +void merkle_sync_changed(struct UTUN_INSTANCE* inst, const char* ns); +int merkle_sync_read_hash(struct UTUN_INSTANCE* inst, const char* ns, uint8_t level, uint64_t prefix, uint8_t hash[MT_HASH_SIZE]); -#endif /* MERKLE_SYNC_H */ +#endif diff --git a/src/chat/merkle_sync_doc.md b/src/chat/merkle_sync_doc.md index 52a297f6..dbe02f62 100644 --- a/src/chat/merkle_sync_doc.md +++ b/src/chat/merkle_sync_doc.md @@ -1,158 +1,100 @@ -# merkle_sync — синхронизация данных на Merkle-деревьях +# merkle_sync — протокол 2 -## 1. Назначение +## Разделение ответственности -Универсальный движок синхронизации между двумя узлами. Группирует элементы в префиксное -Merkle-дерево (5 уровней × 32 бакета, SHA256). Два пира обмениваются хешами уровней, находят -различающиеся бакеты и передают **только их содержимое** — вместо полной пересылки всех данных. +- merkle_tree: производный индекс SQLite, хеши листьев и родителей. +- merkle_sync: обмен, backpressure и жизненный цикл сессий. +- member_sync: формат записей, подписи, версии, применение страниц. -merkle_sync **не знает**, что такое «элемент». Потребитель (member_sync) предоставляет набор -коллбэков `merkle_sync_data_ops`, описывающих модель данных (как перечислить элементы бакета -и посчитать хеш, как сериализовать элементы в wire-формат, как применить полученные). +Движок не знает CHAT-групп. Namespace — каноническая десятичная строка uint64_t. +Все операции выполняются в одном потоке uasync экземпляра. -Работает поверх надёжного транспорта (ETCP), поэтому **таймаутов и ретраев в протоколе нет**: -сессия завершается при совпадении корневого хеша, либо удаляется вручную (`cancel`/`destroy`). +## Дерево и транзакции -## 2. Как пользоваться +Корень имеет уровень 0. Уровни 1–5 разбивают старшие 25 бит ключа на группы по 5 бит. +SHA256 листа: `0x4d || level:1 || item_hashes`, записи упорядочены по unsigned key. +SHA256 родителя: `0x4d || level:1 || child_hash[0] || ... || child_hash[31]`. +Хеш пустого поддерева — 32 нулевых байта; строка индекса для него отсутствует. +Пересчитывается один лист и его предки, до первого неизменившегося хеша. -Напрямую merkle_sync обычно не используют — его потребляет member_sync. Типовой сценарий -потребителя выглядит так: +Индекс создаётся заново при init и заполняется моделью из исходных записей. +Модель меняет данные и вызывает recompute_path в одной транзакции, затем changed после commit. +Без внешней транзакции recompute_path создаёт savepoint и уведомляет самостоятельно. +Ошибки не заменяются пустыми страницами или нулевыми хешами. -```c -// ── разово: инициализация (из member_sync_init) ── -merkle_sync_init(inst, 0x31, &ops, data_ctx); // svc_id — ID ETCP-сервиса +## API -// ── запустил синхронизацию — забыл ── -merkle_sync_start(inst, peer, ns, on_done, my_arg); +init принимает service и data_ops: update_bucket_hash, get_page, apply_items, +необязательные validate_peer и finish. +update_bucket_hash добавляет хеши одного листа в переданный SHA256-контекст. +get_page получает ёмкость буфера и курсор; возвращает размер, последний ключ и флаг продолжения. +apply_items проверяет весь формат и применяет страницу вместе с индексом атомарно. +finish проверяет отложенные зависимости модели перед подтверждением корня. -// ── коллбэк завершения ── -static void on_done(uint64_t peer, const char* ns, int result, void* arg) { - if (result == MT_OK) /* хеши сошлись, данные идентичны */; - if (result == MT_ERR_TIMEOUT) /* (зарезервировано, в протоколе не возникает) */; -} +start подключает namespace к текущему соединению и запрашивает контрольную точку. +Повторные start объединяются, обход не сбрасывается, каждый принятый callback вызывается один раз. +После подключения изменения синхронизируются автоматически. Broadcast/send-back не требуются. +MT_OK означает совпадение корней конкретного раунда, а не отсутствие будущих изменений. -// ── изменил данные — дерево само пересчиталось ── -int changed = merkle_sync_recompute_path(inst, ns, key); // 1 = изменилось, 0 = no-op +cancel, cancel_ns, cancel_peer и destroy освобождают запросы без callback. +DOWN/DELETE/REINIT завершают запросы ошибкой и освобождают сессию. +После восстановления соединения интеграционный слой повторяет start. +Callback может отменить сессию; уничтожение всего экземпляра следует отложить через call_soon. -// ── рассылка дельты остальным пирам (relay) ── -merkle_sync_broadcast(inst, ns, from_peer, data, len); -merkle_sync_push_update(inst, ns, key, type, data, len); // лёгкое обновление (online и т.п.) +## Обмен -// ── отменил — коллбэк не вызовется ── -merkle_sync_cancel(inst, peer, ns); +Меньший node_id ведёт раунд; второй запрашивает его через WAKE. +WAKE также перезапускает незавершённый раунд после cancel/start второго узла. -// ── разово: завершение ── -merkle_sync_destroy(inst); -``` +1. BEGIN(new round) → READY. +2. Ведущий запрашивает хеши детей и обходит различающиеся ветви в глубину. +3. Лист запрашивается страницами с возрастающим курсором. +4. TURN меняет направление: второй узел выполняет такой же pull. +5. Второй проверяет finish и отправляет CHECK(root). +6. Ведущий сверяет корень, проверяет finish и отправляет DONE(root). +7. Второй сверяет DONE со своим текущим корнем. -## 3. Модель данных (коллбэки `merkle_sync_data_ops`) +Ведущий сообщает MT_OK после передачи DONE в надёжный транспорт, если локальная ревизия +не изменилась после проверки. Второй сообщает MT_OK после проверки полученного DONE. +Изменения во время обмена вызывают следующий раунд. +Два завершённых раунда с одной и той же несовпадающей парой корней дают MT_ERR_CONFLICT. +Детерминированное слияние обеспечивает модель; движок не выбирает победителя конфликта. -| Коллбэк | Что делает | -|---|---| -| `update_bucket_hash(ctx, ns, level, prefix64, sha_ctx)` | Перечислить элементы в префиксном диапазоне, вычислить хеш каждого и подать в `sha_ctx` через `EVP_DigestUpdate(sha_ctx, item_hash, 32)`. Возвращает число элементов (0 = бакет пуст, -1 = ошибка) | -| `get_items(ctx, ns, level, prefix, pbytes, buf, len)` | Сериализовать элементы бакета в wire-формат. Возвращает 0 / -2 (буфер мал) / <0 (ошибка) | -| `apply_items(ctx, ns, from_peer, data, len)` | Десериализовать и сохранить полученные элементы. Должна пересчитать дерево через `merkle_sync_recompute_path` для каждого изменённого ключа | -| `apply_update(ctx, ns, key, type, data, len)` | Применить лёгкое обновление (не влияет на Merkle-хеш) — MSG_ITEM_UPDATE | - -`ctx` — прозрачный контекст, передаваемый в `merkle_sync_init` и возвращаемый первым аргументом -в каждый коллбэк. `ns` (namespace) — строка-имя домена данных, обычно `channel_id`. - -## 4. Формат дерева - -5-уровневое префиксное дерево над 64-битными ключами. Каждый уровень берёт 5 старших бит ключа: -уровень 1 — биты 59-63, уровень 5 — биты 39-63. Каждый узел разбивается на 32 бакета (5 бит = 32). - -``` -level 1 [0..31] корень: 32 бакета по 5 бит -level 2 [0..31]...[0..31] каждый — ещё 32 бакета -... -level 5 [0..31].........[0..31] листья: 32^5 = 33M бакетов макс -``` - -- Хеш бакета = `SHA256(хеш_элемента_1 || ... || хеш_элемента_N)`. -- Бакет считается **терминальным** (leaf) на уровне 5 или если в нём `< 8` элементов. -- Хеши бакетов хранятся в таблице `merkle_tree_hash(namespace, level, prefix64, hash, member_count)`. - -Вспомогательные функции префикса: `merkle_sync_level_prefix(key, level)` (выделить `level*5` -старших бит) и `merkle_sync_prefix_bytes(level)` (байт на хранение префикса: уровень 1 → 1 байт, -уровень 5 → 4 байта). - -## 5. Wire-протокол - -Каждое сообщение: `[svc_id:1][ns_len:1][ns:var][type:1][payload:var]`. - -| Тип | #define | Назначение | -|---|---|---| -| 0x01 | MSG_HASHES | Сравнение бакетов: bitmap + хеши, либо данные (is_data) | -| 0x02 | MSG_REQUEST | Запрос содержимого различающихся бакетов | -| 0x03 | MSG_BATCH | Передача данных/подхешей запрошенных бакетов | -| 0x04 | MSG_ITEM_UPDATE | Лёгкое обновление (online-статус), вне дерева | - -Форматы payload: -- **MSG_HASHES**: `[level:1][prefix_bytes:1][prefix:pb][is_data:1]` + (`[bitmap:4][hash:32]*` | `[dlen:2][data]`) -- **MSG_REQUEST**: `[count:1][ {level:1, prefix_bytes:1, prefix:pb, is_terminal:1} × count ]` -- **MSG_BATCH**: `[count:1][ {level:1, prefix_bytes:1, prefix:pb, is_terminal:1, [data | bitmap+hashes]} × count ]` -- **MSG_ITEM_UPDATE**: `[key:8][type:1][data]` - -### Алгоритм обмена - -``` -A → MSG_HASHES(level=0, bitmap+хеши всех 32 бакетов уровня 1) → B -B сравнивает со своим деревом, находит различающиеся бакеты (differs) -B → MSG_REQUEST(level=2, prefix=X, is_terminal) → A -A → MSG_BATCH(данные бакета или подхеши) → B -B сохраняет, пересчитывает хеши -Рекурсивно для подбакетов, пока хеши не совпадут. -differs==0 и level==0 → корневой хеш совпал → сессия SYNCED. -``` - -## 6. Сессии и out-очередь (backpressure) - -`struct ms_session` — сессия на пару (peer, ns): `sess_state` (`SESS_SYNCING`/`SESS_SYNCED`), -`pending[128]` (ожидающие бакеты: `MS_PEND_WAITING`/`MS_PEND_RESOLVED`, с родительским индексом -и счётчиком подбакетов), `done_cb`/`cb_arg`, `out_q` + `waiter`. - -**По-пировая очередь рассылки** `out_q`: рассылка (`broadcast`/`push_update`/`send_to`) не шлёт -немедленно, а кладёт каждый элемент в `out_q` подходящей сессии. Дренаж через `ms_outq_drain_cb`: -`queue_data_get(out_q)` → `etcp_send(conn, entry)`. Перед отправкой проверяется занятость -`conn->send_input_q`; при превышении порога — `queue_waiter_wait(send_input_q, &s->waiter, ...)` -приостанавливает дренаж до освобождения (backpressure, без дропов). - -Отправка: -- `_send_msg` — синхронно одному пиру (служебные HASHES/REQUEST/BATCH — по прямому соединению). -- `_broadcast_to_session` — кладёт в `out_q` конкретной сессии (обёртка в MSG_HASHES is_data=1). - -## 7. WAL-таймер +Ожидается только один ответ QUERY. Стек — максимум шесть кадров с сохранёнными соседними ветвями. +Номера запросов проверяются, ответы другого раунда игнорируются. +Повторную передачу обеспечивает ETCP, отдельных таймеров повторов нет. -Отдельный редкий таймер `MS_WAL_INTERVAL_MS = 60000` (60 с) — `sqlite3_wal_checkpoint_v2(..., -PASSIVE)`. Локальная операция без сети. На Android (`UTUN_HAVE_STANDBY`) при спячке откладывается -через `standby_wait`. Периодической сверки деревьев (bg-таймера) нет — пересчёт событийный. +## Wire -## 8. API +Заголовок: `service:1 | namespace:8 | type:1 | round:8 | request:4` (22 байта). +Все целые big-endian. Максимальный пакет вместе с заголовком — 16384 байта. +Типы 0x10–0x19: WAKE, BEGIN, READY, QUERY, HASHES, PAGE, TURN, CHECK, DONE, ERROR. -| Функция | Назначение | +| Тип | Payload | |---|---| -| `merkle_sync_init(inst, svc_id, ops, data_ctx)` | Создать контекст, `_ensure_table`, `etcp_bind(svc_id, _recv_cb)`, WAL-таймер. `inst->msync = ms` | -| `merkle_sync_destroy(inst)` | Отвязать сервис, отменить WAL-таймер, удалить все сессии (коллбэки не вызываются) | -| `merkle_sync_start(inst, peer, ns, done_cb, arg)` | Создать/перезапустить сессию, отправить MSG_HASHES(level=0). Перезапуск теряет старый коллбэк без вызова | -| `merkle_sync_cancel(inst, peer, ns)` | Удалить сессию, `done_cb` не вызывается | -| `merkle_sync_recompute_path(inst, ns, key)` | Пересчитать все 5 уровней для ключа. Возвращает 1 (изменилось) / 0 (no-op) / -1 | -| `merkle_sync_push_update(inst, ns, key, type, data, len)` | MSG_ITEM_UPDATE всем **SYNCED**-сессиям ns | -| `merkle_sync_broadcast(inst, ns, from_peer, data, len)` | Relay: всем сессиям ns, кроме `from_peer` | -| `merkle_sync_send_to(inst, ns, peer, data, len)` | Данные одному пиру (send-back при stale) | -| `merkle_sync_bg_check(inst, ns)` | Проверить один level-5 бакет (сравнение с БД), при расхождении — пересчёт пути вверх. 1/0/-1 | -| `merkle_sync_get_hash(inst, ns, level, prefix64)` | Прочитать хеш бакета (статический буфер, нули если нет) | -| `merkle_sync_level_prefix(key, level)` / `merkle_sync_prefix_bytes(level)` | Арифметика префикса | - -## 9. Нюансы - -- **svc_id** задаётся при init (member_sync использует `0x31`); единственный приёмник `_recv_cb`. -- **Нет таймаута сессии**: `MT_ERR_TIMEOUT` зарезервирован, но не возникает — сессия живёт до - совпадения корневого хеша или явной отмены. -- **Перезапуск**: `merkle_sync_start` на существующую пару (peer, ns) перезаписывает `done_cb` - (старый коллбэк теряется без вызова). -- **Сессия создаётся и на приёме**: при неожиданном MSG_HASHES от неизвестного пира сессия - создаётся автоматически (symmetrical start). -- **Результат recompute** важен для подавления no-op рассылки: `changed==0` означает, что данные - фактически не изменились. -- Категория логов — `DEBUG_CATEGORY_MEMBER_SYNC`. +| WAKE, BEGIN, READY, TURN | пустой; request=0; у WAKE также round=0 | +| QUERY | level:1, prefix:8, after_valid:1, after:8 | +| HASHES | 32 хеша детей по 32 байта в порядке индексов | +| PAGE | more:1, next:8, model_data | +| CHECK, DONE | корень 32 байта; request=0 | +| ERROR | положительный код ошибки 4 байта; request=0 | + +Курсор продолжения должен возрастать и принадлежать запрошенному листу. +Длины, состояние, тип, префикс и номер запроса проверяются до применения. +Формат model_data определяет адаптер. Старый протокол не поддерживается. + +## Очереди, диагностика, тесты + +Сессия удерживает ETCP_CONN и исходную очередь. У неё один подготовленный пакет, +один waiter и один отложенный вызов. Передача идёт только после разрешения waiter. +Изменения объединяются флагом dirty; неограниченной out-очереди нет. +Отмена снимает ожидающий и отложенный callback waiter независимо от links_up. + +Категория member_sync: INFO — attach, начало/завершение раунда, detach; +DEBUG — TX/RX, номера запросов, размеры, состояние; ERROR — причина отказа. +Ведущий выводит счётчики переданных и полученных байтов при подтверждении. + +test_merkle_sync проверяет индекс, односторонний start, двустороннее слияние, несколько узлов, +параллельные изменения, большой лист с пагинацией и автоматическое обновление. +test_merkle_protocol проверяет ошибочные пакеты/курсоры, сохранение соседних ветвей, +отмену waiter, ошибки apply/SQLite, backpressure, disconnect и повторное подключение. diff --git a/src/chat/merkle_tree.c b/src/chat/merkle_tree.c new file mode 100644 index 00000000..8d199774 --- /dev/null +++ b/src/chat/merkle_tree.c @@ -0,0 +1,122 @@ +#include "merkle_tree.h" +#include "../../lib/debug_config.h" +#include + +static int tree_error(sqlite3* db, const char* op, const char* ns) { + DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "merkle_tree: %s ns=%s: %s", op, ns ? ns : "-", db ? sqlite3_errmsg(db) : "no DB"); + return -1; +} + +uint64_t merkle_sync_level_prefix(uint64_t key, uint8_t level) { + if (level == 0) return 0; + if (level > MT_MAX_LEVEL) return 0; + return key & (~UINT64_C(0) << (64 - level * 5)); +} + +uint8_t merkle_sync_prefix_bytes(uint8_t level) { + return level <= MT_MAX_LEVEL ? (uint8_t)((level * 5 + 7) / 8) : 0; +} + +int merkle_tree_init(sqlite3* db) { + const char* sql = "DROP TABLE IF EXISTS merkle_tree_hash;" + "CREATE TABLE merkle_tree_hash(namespace TEXT NOT NULL, level INTEGER NOT NULL CHECK(level BETWEEN 0 AND 5)," + "prefix64 INTEGER NOT NULL, hash BLOB NOT NULL CHECK(length(hash)=32)," + "PRIMARY KEY(namespace,level,prefix64))"; + if (!db || sqlite3_exec(db, sql, NULL, NULL, NULL) != SQLITE_OK) return tree_error(db, "init", NULL); + DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "merkle_tree: empty derived index initialized"); + return 0; +} + +int merkle_tree_get(sqlite3* db, const char* ns, uint8_t level, uint64_t prefix, uint8_t hash[MT_HASH_SIZE]) { + if (!db || !ns || !hash || level > MT_MAX_LEVEL || prefix != merkle_sync_level_prefix(prefix, level)) + return tree_error(db, "invalid get", ns); + memset(hash, 0, MT_HASH_SIZE); + sqlite3_stmt* st = NULL; + int rc = sqlite3_prepare_v2(db, "SELECT hash FROM merkle_tree_hash WHERE namespace=? AND level=? AND prefix64=?", -1, &st, NULL); + if (rc == SQLITE_OK) rc = sqlite3_bind_text(st, 1, ns, -1, SQLITE_STATIC); + if (rc == SQLITE_OK) rc = sqlite3_bind_int(st, 2, level); + if (rc == SQLITE_OK) rc = sqlite3_bind_int64(st, 3, (sqlite3_int64)prefix); + if (rc == SQLITE_OK) rc = sqlite3_step(st); + if (rc == SQLITE_ROW && sqlite3_column_bytes(st, 0) == MT_HASH_SIZE) memcpy(hash, sqlite3_column_blob(st, 0), MT_HASH_SIZE); + else if (rc != SQLITE_DONE) { sqlite3_finalize(st); return tree_error(db, "read hash", ns); } + sqlite3_finalize(st); + return 0; +} + +int merkle_tree_children(sqlite3* db, const char* ns, uint8_t level, uint64_t prefix, + uint8_t hashes[MT_BUCKETS][MT_HASH_SIZE]) { + if (level >= MT_MAX_LEVEL || prefix != merkle_sync_level_prefix(prefix, level)) return tree_error(db, "invalid children", ns); + if (!db || !ns) return tree_error(db, "invalid children DB", ns); + memset(hashes, 0, MT_BUCKETS * MT_HASH_SIZE); + int shift = 64 - (level + 1) * 5; + const char* sql = level == 0 ? "SELECT prefix64,hash FROM merkle_tree_hash WHERE namespace=? AND level=?" + : "SELECT prefix64,hash FROM merkle_tree_hash WHERE namespace=? AND level=? AND prefix64>=? AND prefix64<=?"; + sqlite3_stmt* st = NULL; + int rc = sqlite3_prepare_v2(db, sql, -1, &st, NULL); + if (rc == SQLITE_OK) rc = sqlite3_bind_text(st, 1, ns, -1, SQLITE_STATIC); + if (rc == SQLITE_OK) rc = sqlite3_bind_int(st, 2, level + 1); + if (level && rc == SQLITE_OK) rc = sqlite3_bind_int64(st, 3, (sqlite3_int64)prefix); + if (level && rc == SQLITE_OK) rc = sqlite3_bind_int64(st, 4, (sqlite3_int64)(prefix | (UINT64_C(31) << shift))); + if (rc != SQLITE_OK) { sqlite3_finalize(st); return tree_error(db, "children query", ns); } + while ((rc = sqlite3_step(st)) == SQLITE_ROW) { + if (sqlite3_column_bytes(st, 1) != MT_HASH_SIZE) { sqlite3_finalize(st); return tree_error(db, "corrupt child", ns); } + unsigned i = (unsigned)(((uint64_t)sqlite3_column_int64(st, 0) >> shift) & 31); + memcpy(hashes[i], sqlite3_column_blob(st, 1), MT_HASH_SIZE); + } + sqlite3_finalize(st); + if (rc != SQLITE_DONE) return tree_error(db, "children read", ns); + return 0; +} + +static int tree_store(sqlite3* db, const char* ns, uint8_t level, uint64_t prefix, const uint8_t hash[MT_HASH_SIZE]) { + static const uint8_t empty[MT_HASH_SIZE]; + uint8_t old[MT_HASH_SIZE]; + if (merkle_tree_get(db, ns, level, prefix, old) < 0) return -1; + if (!memcmp(old, hash, MT_HASH_SIZE)) return 0; + int is_empty = !memcmp(hash, empty, MT_HASH_SIZE); + const char* sql = is_empty ? "DELETE FROM merkle_tree_hash WHERE namespace=? AND level=? AND prefix64=?" + : "INSERT OR REPLACE INTO merkle_tree_hash(namespace,level,prefix64,hash) VALUES(?,?,?,?)"; + sqlite3_stmt* st = NULL; + int rc = sqlite3_prepare_v2(db, sql, -1, &st, NULL); + if (rc == SQLITE_OK) rc = sqlite3_bind_text(st, 1, ns, -1, SQLITE_STATIC); + if (rc == SQLITE_OK) rc = sqlite3_bind_int(st, 2, level); + if (rc == SQLITE_OK) rc = sqlite3_bind_int64(st, 3, (sqlite3_int64)prefix); + if (rc == SQLITE_OK && !is_empty) rc = sqlite3_bind_blob(st, 4, hash, MT_HASH_SIZE, SQLITE_STATIC); + if (rc == SQLITE_OK) rc = sqlite3_step(st); + sqlite3_finalize(st); + if (rc != SQLITE_DONE) return tree_error(db, "write hash", ns); + return 1; +} + +int merkle_tree_update(sqlite3* db, const char* ns, uint64_t key, merkle_leaf_hash_fn leaf_hash, void* ctx) { + EVP_MD_CTX* sha = EVP_MD_CTX_new(); + uint8_t hash[MT_HASH_SIZE], children[MT_BUCKETS][MT_HASH_SIZE]; + static const uint8_t empty[sizeof(children)]; + int changed = 0; + if (!db || !ns || !leaf_hash || !sha) goto fail; + for (int level = MT_MAX_LEVEL; level >= 0; level--) { + uint64_t prefix = merkle_sync_level_prefix(key, (uint8_t)level); + uint8_t domain[2] = { 0x4d, (uint8_t)level }; + if (EVP_DigestInit_ex(sha, EVP_sha256(), NULL) != 1 || EVP_DigestUpdate(sha, domain, sizeof(domain)) != 1) goto fail; + int count; + if (level == MT_MAX_LEVEL) { + count = leaf_hash(ctx, ns, (uint8_t)level, prefix, sha); + if (count < 0) goto fail; + } else { + if (merkle_tree_children(db, ns, (uint8_t)level, prefix, children) < 0) goto fail; + count = memcmp(children, empty, sizeof(children)) != 0; + if (EVP_DigestUpdate(sha, children, sizeof(children)) != 1) goto fail; + } + if (EVP_DigestFinal_ex(sha, hash, NULL) != 1) goto fail; + if (!count) memset(hash, 0, sizeof(hash)); + int rc = tree_store(db, ns, (uint8_t)level, prefix, hash); + if (rc < 0) goto fail; + if (rc == 0) break; + changed = 1; + } + EVP_MD_CTX_free(sha); + return changed; +fail: + EVP_MD_CTX_free(sha); + return tree_error(db, "recompute", ns); +} diff --git a/src/chat/merkle_tree.h b/src/chat/merkle_tree.h new file mode 100644 index 00000000..95dcde65 --- /dev/null +++ b/src/chat/merkle_tree.h @@ -0,0 +1,24 @@ +#ifndef MERKLE_TREE_H +#define MERKLE_TREE_H + +#include +#include +#include + +#define MT_MAX_LEVEL 5 +#define MT_BUCKETS 32 +#define MT_HASH_SIZE 32 + +typedef int (*merkle_leaf_hash_fn)(void* ctx, const char* ns, uint8_t level, uint64_t prefix, EVP_MD_CTX* hash); + +/* Дерево — производный индекс. При открытии очищается и восстанавливается из записей модели. */ +int merkle_tree_init(sqlite3* db); +int merkle_tree_get(sqlite3* db, const char* ns, uint8_t level, uint64_t prefix, uint8_t hash[MT_HASH_SIZE]); +int merkle_tree_children(sqlite3* db, const char* ns, uint8_t level, uint64_t prefix, + uint8_t hashes[MT_BUCKETS][MT_HASH_SIZE]); +/* Работает внутри транзакции вызывающей стороны. Сначала лист, затем родители до корня. */ +int merkle_tree_update(sqlite3* db, const char* ns, uint64_t key, merkle_leaf_hash_fn leaf_hash, void* ctx); +uint64_t merkle_sync_level_prefix(uint64_t key, uint8_t level); +uint8_t merkle_sync_prefix_bytes(uint8_t level); + +#endif diff --git a/src/transport_layer/etcp_api.c b/src/transport_layer/etcp_api.c index ca430013..0b185a10 100644 --- a/src/transport_layer/etcp_api.c +++ b/src/transport_layer/etcp_api.c @@ -178,6 +178,14 @@ int etcp_send(struct ETCP_CONN* conn, struct ll_entry* entry) { /* Владение entry: при успехе entry уходит в очередь (может быть освобождён * синхронно); при ошибке (-1) entry НЕ освобождается — вызывающий владеет. */ if (!conn || !entry || !conn->send_input_q || conn->state == 2 || conn->close_requested) return -1; + /* queue_data_put освобождает entry при отказе. Проверяем до передачи владения, + * чтобы сохранить контракт etcp_send: при -1 entry остаётся у вызывающего. */ + struct ll_queue* q = conn->send_input_q; + if (q->hash_size || (q->size_limit >= 0 && q->count >= q->size_limit)) { + DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "etcp_send: invalid/full input queue conn=%p count=%d limit=%d hash_size=%zu", + (void*)conn, q->count, q->size_limit, q->hash_size); + return -1; + } return queue_data_put(conn->send_input_q, entry); } diff --git a/tests/Makefile.am b/tests/Makefile.am index 48aa567e..b0dbb14b 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -66,6 +66,8 @@ check_PROGRAMS = \ test_node_conn_direct \ test_db_sync \ test_merkle_sync \ + test_merkle_protocol \ + test_member_adapter \ test_chat_sync_stress \ test_chat_join \ test_chat_join_e2e \ @@ -390,6 +392,12 @@ test_db_sync_CFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/lib test_db_sync_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS) test_merkle_sync_SOURCES = test_merkle_sync.c +test_merkle_protocol_SOURCES = test_merkle_protocol.c +test_merkle_protocol_CFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/src/chat -I$(top_srcdir)/lib +test_merkle_protocol_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS) +test_member_adapter_SOURCES = test_member_adapter.c +test_member_adapter_CFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/src/chat -I$(top_srcdir)/lib +test_member_adapter_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS) test_merkle_sync_CFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/src/chat -I$(top_srcdir)/lib test_merkle_sync_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS) diff --git a/tests/test_chat_join_e2e.c b/tests/test_chat_join_e2e.c index 41306959..be134c84 100644 --- a/tests/test_chat_join_e2e.c +++ b/tests/test_chat_join_e2e.c @@ -556,10 +556,18 @@ static int child_main(const char* role, const char* dir, int invalid_key) { if (!wait_join_result(inst, 8000)) { fprintf(stderr, "J: no JOIN_READY\n"); goto out; } + char ready[512]; snprintf(ready, sizeof(ready), "%s/join_ready", dir); + wf(ready, "ready"); rc = 0; } } + /* Локальная запись не означает доставку: участники обслуживают обмен до JOIN_READY у J. */ + if (rc == 0 && !invalid_key && strcmp(role, "j") != 0) { + char ready[512]; snprintf(ready, sizeof(ready), "%s/join_ready", dir); + if (!wait_file(inst, ready, 8000)) { fprintf(stderr, "%s: join delivery not confirmed\n", role); rc = 1; } + } + out: if (early_link || delayed_registration) rc = 1; fprintf(stderr, "%s: %s\n", role, rc == 0 ? "OK" : "FAIL"); diff --git a/tests/test_media_delivery_full.c b/tests/test_media_delivery_full.c index ce3066a0..4c659631 100644 --- a/tests/test_media_delivery_full.c +++ b/tests/test_media_delivery_full.c @@ -461,6 +461,7 @@ int main(void) { g_inst[i] = utun_instance_create(g_ua, g_cfg[i]); if (!g_inst[i]) { printf("FAIL: create n%d\n", i); goto done; } utun_instance_init(g_inst[i]); + if (member_sync_init(g_inst[i]) < 0) { FAIL("member_sync_init n%d", i); goto done; } } mon(NULL); @@ -491,7 +492,12 @@ int main(void) { done: g_result = 1; - for (int i = 0; i < N_NODES; i++) { if (g_inst[i]) { g_inst[i]->running = 0; utun_instance_destroy(g_inst[i]); g_inst[i] = NULL; } } + for (int i = 0; i < N_NODES; i++) { + if (g_inst[i]) { + member_sync_destroy(g_inst[i]); + g_inst[i]->running = 0; utun_instance_destroy(g_inst[i]); g_inst[i] = NULL; + } + } if (g_ua) { uasync_destroy(g_ua, 0); g_ua = NULL; } t_rmdir(g_tdir); return G_FAILED > 0 ? 1 : 0; diff --git a/tests/test_member_adapter.c b/tests/test_member_adapter.c new file mode 100644 index 00000000..fd1b25fd --- /dev/null +++ b/tests/test_member_adapter.c @@ -0,0 +1,68 @@ +/* Реальный адаптер, SQLite и транзакции; без копии алгоритма и без сетевых таймингов. */ +#include "../src/chat/member_sync.c" +#include + +static void setup(struct UTUN_INSTANCE* inst, struct UASYNC* ua) { + memset(inst, 0, sizeof(*inst)); inst->ua = ua; inst->node_id = 1; + assert(sqlite3_open(":memory:", &inst->topo_sqlite_db) == SQLITE_OK); + assert(topo_node_sqlite_init(inst->topo_sqlite_db) == 0); + uint8_t key[32] = {1}, signature[64] = {0}; + assert(topo_node_sqlite_channel_put(inst->topo_sqlite_db, "1001", "test", 1, key, NULL, key, NULL, signature) == 0); + assert(member_sync_init(inst) == 0); +} + +static void put(struct UTUN_INSTANCE* inst, uint64_t id) { + uint8_t key[32] = {1}; + assert(member_sync_put(inst, "1001", id, key, key, NULL, 0, NULL, 0, "{}", NULL, NULL, 0, 0, NULL) >= 0); +} + +static void root(struct UTUN_INSTANCE* inst, uint8_t hash[32]) { + assert(merkle_sync_read_hash(inst, "1001", 0, 0, hash) == 0); +} + +int main(void) { + debug_config_init(); debug_set_level(DEBUG_LEVEL_ERROR); + size_t baseline = u_get_allocated_count(); + struct UASYNC* ua = uasync_create(); assert(ua); + struct UTUN_INSTANCE source, target; + setup(&source, ua); setup(&target, ua); + put(&source, 1); put(&source, 2); + + uint8_t raw[2049], *page = raw + 1; /* проверяем невыровненный буфер */ + size_t len = 300; uint64_t next; int more; + assert(_member_get_page(&source, "1001", 0, 0, 0, page, &len, &next, &more) == 0); + assert(page[0] == 0 && page[1] == 1 && next == 1 && more == 1 && len <= 300); + struct member_wire w; size_t used; + assert(member_decode(page + 2, len - 2, &w, &used) == 0 && w.rec.node_id == 1 && used == len - 2); + len = 300; + assert(_member_get_page(&source, "1001", 0, 1, next, page, &len, &next, &more) == 0 && next == 2 && !more); + puts("PASS: bounded member pages, canonical integers and unaligned buffers"); + + len = 2048; + assert(_member_get_page(&source, "1001", 0, 0, 0, page, &len, &next, &more) == 0 && page[1] == 2 && !more); + assert(_member_apply_items(&target, "1001", 1, page, len - 1) < 0 && member_sync_count(&target, "1001") == 0); + sqlite3* db = target.topo_sqlite_db; + assert(sqlite3_exec(db, "CREATE TRIGGER fail_second BEFORE INSERT ON peers_1001 WHEN NEW.node_id=2 " + "BEGIN SELECT RAISE(ABORT,'injected second record failure'); END", NULL, NULL, NULL) == SQLITE_OK); + assert(_member_apply_items(&target, "1001", 1, page, len) < 0); + uint8_t hash[32], empty[32] = {0}; root(&target, hash); + assert(member_sync_count(&target, "1001") == 0 && !memcmp(hash, empty, 32) && sqlite3_get_autocommit(db)); + assert(sqlite3_exec(db, "DROP TRIGGER fail_second", NULL, NULL, NULL) == SQLITE_OK); + assert(_member_apply_items(&target, "1001", 1, page, len) == 0 && member_sync_count(&target, "1001") == 2); + uint8_t expected[32]; root(&source, expected); root(&target, hash); assert(!memcmp(hash, expected, 32)); + puts("PASS: malformed page rejected; second-record error rolls back data and tree; complete page converges"); + + assert(sqlite3_exec(db, "UPDATE peers_1001 SET x25519_pubkey=X'01' WHERE node_id=1", NULL, NULL, NULL) == SQLITE_OK); + assert(merkle_sync_recompute_path(&target, "1001", 1) < 0); + root(&target, hash); assert(!memcmp(hash, expected, 32)); + len = 2048; + assert(_member_get_page(&target, "1001", 0, 0, 0, page, &len, &next, &more) < 0); + puts("PASS: corrupt BLOB cannot be hashed or serialized"); + + member_sync_destroy(&target); member_sync_destroy(&source); + assert(sqlite3_close(target.topo_sqlite_db) == SQLITE_OK && sqlite3_close(source.topo_sqlite_db) == SQLITE_OK); + uasync_poll(ua, 0); uasync_destroy(ua, 0); + assert(u_get_allocated_count() == baseline); + puts("ALL PASS: member adapter"); + return 0; +} diff --git a/tests/test_merkle_protocol.c b/tests/test_merkle_protocol.c new file mode 100644 index 00000000..fbf2d036 --- /dev/null +++ b/tests/test_merkle_protocol.c @@ -0,0 +1,215 @@ +/* Проверяем границы автомата на точных wire-пакетах. Включение реализации позволяет + * задать состояние без копирования private-структур и без подмены очередей/таймеров. */ +#include "../src/chat/merkle_sync.c" +#include + +struct fixture { + struct UTUN_INSTANCE inst; + struct ETCP_CONN conn; + int callbacks, result, apply_error; +}; + +static int t_hash(void* arg, const char* ns, uint8_t level, uint64_t prefix, EVP_MD_CTX* hash) { + (void)arg; (void)ns; (void)level; (void)prefix; + return EVP_DigestUpdate(hash, "record", 6) == 1 ? 1 : -1; +} + +static int t_page(void* arg, const char* ns, uint64_t prefix, int has_after, uint64_t after, + uint8_t* data, size_t* len, uint64_t* next, int* more) { + (void)arg; (void)ns; (void)prefix; (void)has_after; (void)after; + memset(data, 0, *len); *next = 1; *more = 1; return 0; +} + +static int t_apply(void* arg, const char* ns, uint64_t peer, const uint8_t* data, size_t len) { + (void)ns; (void)peer; (void)data; (void)len; + return ((struct fixture*)arg)->apply_error; +} + +static const struct merkle_sync_data_ops t_ops = { + .update_bucket_hash = t_hash, .get_page = t_page, .apply_items = t_apply +}; + +static void t_done(uint64_t peer, const char* ns, int result, void* arg) { + (void)peer; (void)ns; + struct fixture* f = arg; + f->callbacks++; f->result = result; +} + +static void t_init(struct fixture* f) { + memset(f, 0, sizeof(*f)); + f->inst.node_id = 1; + f->inst.ua = uasync_create(); assert(f->inst.ua); + assert(sqlite3_open(":memory:", &f->inst.topo_sqlite_db) == SQLITE_OK); + f->inst.connections = queue_new(f->inst.ua, 16, 0, 8, "test connections"); assert(f->inst.connections); + f->conn.instance = &f->inst; f->conn.peer_node_id = 2; f->conn.initialized = 1; f->conn.links_up = 1; + f->conn.send_input_q = queue_new(f->inst.ua, 0, 0, 0, "test send"); assert(f->conn.send_input_q); + queue_set_waiter_defer(f->conn.send_input_q, 1); + struct ll_entry* e = queue_entry_new(sizeof(struct conn_queue_entry)); assert(e); + struct conn_queue_entry* ce = (struct conn_queue_entry*)e->data; + ce->peer_node_id = 2; ce->conn = &f->conn; + assert(queue_data_put_with_index(f->inst.connections, e) == 0); + assert(merkle_sync_init(&f->inst, 0x72, &t_ops, f) == 0); +} + +static void t_clear_queue(struct ll_queue* q) { + struct ll_entry* e; + while ((e = queue_data_get(q))) { queue_dgram_free(e); queue_entry_free(e); } + queue_free(q); +} + +static void t_destroy(struct fixture* f) { + merkle_sync_destroy(&f->inst); + assert(f->conn.ref_count == 0); + t_clear_queue(f->conn.send_input_q); t_clear_queue(f->inst.connections); + sqlite3_close(f->inst.topo_sqlite_db); + uasync_poll(f->inst.ua, 0); uasync_destroy(f->inst.ua, 0); +} + +static struct ms_session* t_session(struct fixture* f) { + struct ms_session* s = ms_create(f->inst.msync, &f->conn, 1001); assert(s); + s->round = 7; s->state = MS_PULL; s->request_id = 1; s->waiting = 1; + s->requests = u_calloc(1, sizeof(*s->requests)); assert(s->requests); + s->requests->cb = t_done; s->requests->arg = f; + return s; +} + +static void t_receive(struct fixture* f, uint8_t type, uint64_t round, uint32_t request, const uint8_t* data, size_t len) { + struct ll_entry* e = ll_alloc_lldgram((uint16_t)(MS_HEADER + len)); assert(e); + e->len = (uint16_t)(MS_HEADER + len); e->dgram[0] = 0x72; + ms_write64(e->dgram + 1, 1001); e->dgram[9] = type; + ms_write64(e->dgram + 10, round); ms_write32(e->dgram + 18, request); + if (len) memcpy(e->dgram + MS_HEADER, data, len); + ms_receive(&f->conn, e); +} + +static void test_malformed_and_old_round(void) { + struct fixture f; t_init(&f); + struct ms_session* s = t_session(&f); + uint8_t short_hashes[4] = {1}; + t_receive(&f, MS_HASHES, 6, 1, short_hashes, sizeof(short_hashes)); + assert(!f.callbacks && s->waiting && s->round == 7); + t_receive(&f, MS_HASHES, 7, 1, short_hashes, sizeof(short_hashes)); + assert(f.callbacks == 1 && f.result == MT_ERR_PROTOCOL && s->state == MS_FAILED); + t_destroy(&f); + puts("PASS: stale round ignored; truncated hashes rejected before reading"); +} + +static void test_rejected_data(void) { + struct fixture f; t_init(&f); + struct ms_session* s = t_session(&f); s->stack[0].level = MT_MAX_LEVEL; + f.apply_error = MT_ERR_DATA; + uint8_t page[11] = {0}; + t_receive(&f, MS_PAGE, 7, 1, page, sizeof(page)); + assert(f.callbacks == 1 && f.result == MT_ERR_DATA && s->state == MS_FAILED); + t_destroy(&f); + puts("PASS: failed apply cannot produce success"); +} + +static void test_sibling_walk(void) { + struct fixture f; t_init(&f); + struct ms_session* s = t_session(&f); + uint8_t hashes[32 * 32] = {0}; hashes[0] = 1; hashes[32] = 1; + assert(ms_receive_hashes(s, 1, hashes, sizeof(hashes)) == 0); + assert(s->depth == 1 && s->stack[1].prefix == 0 && s->stack[0].children == 2); + ms_discard_tx(s); + memset(hashes, 0, sizeof(hashes)); + assert(ms_receive_hashes(s, 2, hashes, sizeof(hashes)) == 0); + assert(s->depth == 1 && s->stack[1].prefix == (UINT64_C(1) << 59)); + assert(s->waiting && !f.callbacks); + t_destroy(&f); + puts("PASS: completing one branch preserves the sibling request"); +} + +static void test_cancel_waiters(void) { + for (int blocked = 0; blocked < 2; blocked++) { + struct fixture f; t_init(&f); + struct ms_session* s = t_session(&f); + if (blocked) { struct ll_entry* e = queue_entry_new(0); assert(e); assert(queue_data_put(f.conn.send_input_q, e) == 0); } + assert(ms_send(s, MS_QUERY, 1, NULL, 0) == 0); + assert(s->waiter.internal || s->waiter.call_soon_id); + f.conn.links_up = 0; + merkle_sync_cancel_peer(&f.inst, 2); + assert(!f.inst.msync->sessions && !f.conn.send_input_q->waiter_head); + uasync_poll(f.inst.ua, 0); + assert(!f.callbacks); + t_destroy(&f); + } + puts("PASS: cancel after links_down removes queued and deferred waiters"); +} + +static void test_page_size_and_cursor(void) { + struct fixture f; t_init(&f); + struct ms_session* s = t_session(&f); s->state = MS_SERVE; + uint8_t query[MS_QUERY_SIZE] = { MT_MAX_LEVEL }; + assert(ms_serve_query(s, 1, query, sizeof(query)) == 0); + assert(s->tx && s->tx->len == MT_PAGE_SIZE); + ms_discard_tx(s); + query[9] = 1; ms_write64(query + 10, 1); + assert(ms_serve_query(s, 2, query, sizeof(query)) == MT_ERR_DATA); + query[0] = 255; + assert(ms_serve_query(s, 3, query, sizeof(query)) == MT_ERR_PROTOCOL); + t_destroy(&f); + puts("PASS: page bounded; non-advancing cursor and invalid level rejected"); +} + +static void test_readonly_tree(void) { + struct fixture f; t_init(&f); + assert(sqlite3_exec(f.inst.topo_sqlite_db, "PRAGMA query_only=ON", NULL, NULL, NULL) == SQLITE_OK); + assert(merkle_sync_recompute_path(&f.inst, "1001", 0) < 0); + uint8_t hash[32], empty[32] = {0}; + assert(merkle_sync_read_hash(&f.inst, "1001", 0, 0, hash) == 0 && !memcmp(hash, empty, 32)); + t_destroy(&f); + puts("PASS: SQLite failure is reported and cannot publish a root"); +} + +static void test_backpressure_and_disconnect(void) { + struct fixture f; t_init(&f); + struct ll_entry* blocker = queue_entry_new(0); assert(blocker); + assert(queue_data_put(f.conn.send_input_q, blocker) == 0); + assert(merkle_sync_start(&f.inst, 2, "1001", t_done, &f) == 0); + uasync_poll(f.inst.ua, 0); + struct ms_session* s = f.inst.msync->sessions; + assert(s->tx && s->waiter.internal); + for (int i = 0; i < 1000; i++) merkle_sync_changed(&f.inst, "1001"); + assert(s->tx && queue_entry_count(f.conn.send_input_q) == 1 && s->dirty); + f.conn.links_up = 0; + ms_conn_status(&f.conn, ETCP_CONN_STATUS_DOWN, f.inst.msync); + assert(!f.inst.msync->sessions && f.callbacks == 1 && f.result == MT_ERR_DISCONNECTED); + t_destroy(&f); + puts("PASS: 1000 changes coalesce behind backpressure; disconnect completes request with error"); +} + +static void test_peer_restart(void) { + struct fixture f; t_init(&f); + struct ms_session* s = t_session(&f); + assert(ms_send(s, MS_QUERY, 1, NULL, 0) == 0); + t_receive(&f, MS_WAKE, 0, 0, NULL, 0); + assert(s->round > 7 && s->state == MS_BEGIN_WAIT && s->tx && s->tx->dgram[9] == MS_BEGIN); + uint8_t hashes[32 * 32] = {0}; + t_receive(&f, MS_HASHES, 7, 1, hashes, sizeof(hashes)); + assert(!f.callbacks && s->state == MS_BEGIN_WAIT); + t_destroy(&f); + puts("PASS: reattaching follower restarts the round; old responses cannot affect it"); +} + +static void test_send_rejection(void) { + struct fixture f; t_init(&f); + struct ms_session* s = t_session(&f); + queue_set_size_limit(f.conn.send_input_q, 0); + assert(ms_send(s, MS_QUERY, 1, NULL, 0) == 0); + uasync_poll(f.inst.ua, 0); + assert(f.callbacks == 1 && f.result == MT_ERR_IO && !s->tx); + t_destroy(&f); + puts("PASS: transport rejection retains ownership; packet is freed exactly once"); +} + +int main(void) { + debug_config_init(); debug_set_level(DEBUG_LEVEL_ERROR); + size_t baseline = u_get_allocated_count(); + test_malformed_and_old_round(); test_rejected_data(); test_sibling_walk(); test_cancel_waiters(); + test_page_size_and_cursor(); test_readonly_tree(); test_backpressure_and_disconnect(); test_peer_restart(); + test_send_rejection(); + assert(u_get_allocated_count() == baseline); + puts("ALL PASS: protocol regressions, no tracked allocations leaked"); + return 0; +} diff --git a/tests/test_merkle_sync.c b/tests/test_merkle_sync.c index 4b79370f..dcb0ca16 100644 --- a/tests/test_merkle_sync.c +++ b/tests/test_merkle_sync.c @@ -35,27 +35,6 @@ static void _ms_ensure_groups(struct UTUN_INSTANCE* inst) { } } -/* merkle_sync private struct — needed for Stage 2 unit tests. - * Must match struct ms_session prefix in merkle_sync.c exactly. */ -struct ms_session { - struct ms_session* next; - char ns[64]; - uint64_t peer; - uint8_t sess_state; -}; - -/* merkle_sync private struct — needed for Stage 2 unit tests. - * Must match struct merkle_sync in merkle_sync.c exactly. */ -struct merkle_sync { - struct UTUN_INSTANCE* inst; - uint8_t svc_id; - const struct merkle_sync_data_ops* ops; - void* data_ctx; - struct ms_session* sessions; - uint8_t initialized; - void* bg_timer; -}; - static int tests_run = 0; static int tests_failed = 0; static int stage_failures = 0; @@ -66,7 +45,7 @@ static int stage_failures = 0; /* ── Stage 2: mock data store ── */ -#define MS_MAX_ITEMS 4096 +#define MS_MAX_ITEMS 8192 struct ms_item { uint64_t key; uint32_t val; }; @@ -133,80 +112,83 @@ static int _bucket_hash(void* ctx, const char* ns, uint8_t level, uint64_t prefi return count; } -static int _get_items(void* ctx, const char* ns, uint8_t level, uint64_t prefix, - uint8_t pbytes, uint8_t* buf, size_t* len) { - (void)ns; (void)pbytes; +static int _get_page(void* ctx, const char* ns, uint64_t prefix, int after_valid, uint64_t after, + uint8_t* buf, size_t* len, uint64_t* next, int* more) { + (void)ns; struct ms_data* d = ((struct ms_test_ctx*)ctx)->data; - int mask_shift = 64 - (int)level * 5; - uint64_t mask = (level == 0) ? 0 : ((mask_shift >= 0 && mask_shift < 64) ? (~0ULL << mask_shift) : UINT64_MAX); uint16_t count = 0; size_t off = 2; + *next = 0; *more = 0; for (int i = 0; i < d->count; i++) { - if ((d->items[i].key & mask) != prefix) continue; - if (off + 12 > *len) return -2; + if (merkle_sync_level_prefix(d->items[i].key, MT_MAX_LEVEL) != prefix + || (after_valid && d->items[i].key <= after)) continue; + if (off + 12 > *len) { if (!count) return -1; *more = 1; break; } memcpy(buf + off, &d->items[i].key, 8); off += 8; memcpy(buf + off, &d->items[i].val, 4); off += 4; - count++; + *next = d->items[i].key; count++; } - memcpy(buf, &count, 2); - *len = off; - return 0; + memcpy(buf, &count, 2); *len = off; return 0; } -static int _apply_items(void* ctx, const char* ns, uint64_t from_peer, - const uint8_t* data, size_t len) { - struct ms_test_ctx* tc = (struct ms_test_ctx*)ctx; +static int _apply_items(void* ctx, const char* ns, uint64_t from_peer, const uint8_t* data, size_t len) { + (void)from_peer; + struct ms_test_ctx* tc = ctx; struct ms_data* d = tc->data; - if (len < 2) return 0; + if (len < 2) return -1; uint16_t count; memcpy(&count, data, 2); - const uint8_t* p = data + 2; + if (len != 2 + (size_t)count * 12) return -1; int changed = 0; - uint8_t relay_buf[65536]; uint16_t relay_count = 0; size_t relay_off = 2; /* [count:2][items] */ for (uint16_t i = 0; i < count; i++) { - uint64_t key; memcpy(&key, p, 8); p += 8; - uint32_t val; memcpy(&val, p, 4); p += 4; - /* check if item is new or different */ + uint64_t key; uint32_t val; + memcpy(&key, data + 2 + (size_t)i * 12, 8); + memcpy(&val, data + 10 + (size_t)i * 12, 4); int found = 0; for (int j = 0; j < d->count; j++) { - if (d->items[j].key == key) { - if (d->items[j].val != val) { d->items[j].val = val; changed = 1; } - found = 1; break; - } - } - if (!found) { _data_insert(d, key, val); changed = 1; } - if (changed && relay_off + 12 < sizeof(relay_buf)) { - memcpy(relay_buf + relay_off, &key, 8); relay_off += 8; - memcpy(relay_buf + relay_off, &val, 4); relay_off += 4; - relay_count++; + if (d->items[j].key != key) continue; + if (d->items[j].val < val) { d->items[j].val = val; changed = 1; } + found = 1; break; } + if (!found) { if (d->count == MS_MAX_ITEMS) return -1; _data_insert(d, key, val); changed = 1; } } - if (changed && tc->inst) { + if (changed) { _data_sort(d); - for (uint16_t i = 0; i < count; i++) { - uint64_t k; memcpy(&k, data + 2 + (size_t)i * 12, 8); - merkle_sync_recompute_path(tc->inst, ns, k); - } - /* relay changed items to other SYNCED sessions */ - if (relay_count > 0) { - memcpy(relay_buf, &relay_count, 2); - merkle_sync_broadcast(tc->inst, ns, from_peer, relay_buf, relay_off); - } + /* Страница всегда относится к одному листу: пересчитываем его один раз. */ + uint64_t key; memcpy(&key, data + 2, 8); + if (merkle_sync_recompute_path(tc->inst, ns, key) < 0) return -1; } return 0; } -static int _apply_update(void* ctx, const char* ns, uint64_t key, uint8_t type, - const uint8_t* data, size_t len) { - (void)ctx; (void)ns; (void)key; (void)type; (void)data; (void)len; - return 0; -} - static const struct merkle_sync_data_ops g_test_ops = { - .update_bucket_hash = _bucket_hash, - .get_items = _get_items, - .apply_items = _apply_items, - .apply_update = _apply_update, + .update_bucket_hash = _bucket_hash, .get_page = _get_page, .apply_items = _apply_items }; +static const uint8_t* _test_hash(struct UTUN_INSTANCE* inst, const char* ns, uint8_t level, uint64_t prefix) { + static uint8_t hash[MT_HASH_SIZE]; + if (merkle_sync_read_hash(inst, ns, level, prefix, hash) < 0) abort(); + return hash; +} + +/* Независимое восстановление дерева из набора записей, без чтения индекса. */ +static void _expected_hash(struct ms_test_ctx* tc, const char* ns, uint8_t level, uint64_t prefix, uint8_t hash[MT_HASH_SIZE]) { + memset(hash, 0, MT_HASH_SIZE); + int present = 0; + for (int i = 0; i < tc->data->count; i++) + if (merkle_sync_level_prefix(tc->data->items[i].key, level) == prefix) { present = 1; break; } + if (!present) return; + EVP_MD_CTX* c = EVP_MD_CTX_new(); + EVP_DigestInit_ex(c, EVP_sha256(), NULL); + uint8_t domain[2] = { 0x4d, level }; EVP_DigestUpdate(c, domain, sizeof(domain)); + if (level == MT_MAX_LEVEL) _bucket_hash(tc, ns, level, prefix, c); + else { + for (unsigned i = 0; i < MT_BUCKETS; i++) { + uint8_t child[MT_HASH_SIZE]; + _expected_hash(tc, ns, level + 1, prefix | ((uint64_t)i << (64 - (level + 1) * 5)), child); + EVP_DigestUpdate(c, child, sizeof(child)); + } + } + EVP_DigestFinal_ex(c, hash, NULL); EVP_MD_CTX_free(c); +} + /* ── Stage 1: prefix arithmetic ── */ static void test_prefix_bytes(void) { @@ -276,17 +258,7 @@ static void test_level_prefix_bucket_partition(void) { /* ── Stage 2: tree building tests ── */ -static void _ensure_table(sqlite3* db) { - sqlite3_exec(db, - "CREATE TABLE IF NOT EXISTS merkle_tree_hash (" - " namespace TEXT NOT NULL," - " level INTEGER NOT NULL CHECK(level BETWEEN 1 AND 5)," - " prefix64 INTEGER NOT NULL," - " hash BLOB NOT NULL," - " member_count INTEGER NOT NULL," - " PRIMARY KEY (namespace, level, prefix64))", - NULL, NULL, NULL); -} +static void _ensure_table(sqlite3* db) { if (merkle_tree_init(db) < 0) abort(); } static int _db_count_rows(sqlite3* db, const char* ns) { sqlite3_stmt* st = NULL; @@ -300,9 +272,9 @@ static int _db_count_rows(sqlite3* db, const char* ns) { static int _db_compare_trees(sqlite3* da, sqlite3* db, const char* ns) { sqlite3_stmt* sa = NULL, *sb = NULL; - sqlite3_prepare_v2(da, "SELECT level,prefix64,hash,member_count FROM merkle_tree_hash WHERE namespace=? ORDER BY level,prefix64", + sqlite3_prepare_v2(da, "SELECT level,prefix64,hash FROM merkle_tree_hash WHERE namespace=? ORDER BY level,prefix64", -1, &sa, NULL); - sqlite3_prepare_v2(db, "SELECT level,prefix64,hash,member_count FROM merkle_tree_hash WHERE namespace=? ORDER BY level,prefix64", + sqlite3_prepare_v2(db, "SELECT level,prefix64,hash FROM merkle_tree_hash WHERE namespace=? ORDER BY level,prefix64", -1, &sb, NULL); sqlite3_bind_text(sa, 1, ns, -1, SQLITE_STATIC); sqlite3_bind_text(sb, 1, ns, -1, SQLITE_STATIC); @@ -316,7 +288,6 @@ static int _db_compare_trees(sqlite3* da, sqlite3* db, const char* ns) { (uint64_t)sqlite3_column_int64(sa,1) != (uint64_t)sqlite3_column_int64(sb,1)) { mismatch++; break; } - if (sqlite3_column_int(sa,3) != sqlite3_column_int(sb,3)) { mismatch++; break; } if (memcmp(sqlite3_column_blob(sa,2), sqlite3_column_blob(sb,2), MT_HASH_SIZE) != 0) { mismatch++; break; } } sqlite3_finalize(sa); sqlite3_finalize(sb); @@ -332,14 +303,13 @@ static void test_tree_empty_ns(void) { inst.topo_sqlite_db = db; struct ms_data data; _data_init(&data); struct ms_test_ctx ctx = { .data = &data, .inst = &inst }; - struct merkle_sync ms; memset(&ms, 0, sizeof(ms)); - ms.inst = &inst; ms.ops = &g_test_ops; ms.data_ctx = &ctx; ms.initialized = 1; - inst.msync = &ms; + inst.ua = uasync_create(); + if (merkle_sync_init(&inst, 0x72, &g_test_ops, &ctx) != 0) abort(); merkle_sync_recompute_path(&inst, MS_NS_TEST, 0x1234ULL); if (_db_count_rows(db, MS_NS_TEST) == 0) PASS(); else FAIL("got %d rows", _db_count_rows(db, MS_NS_TEST)); - sqlite3_close(db); + merkle_sync_destroy(&inst); uasync_destroy(inst.ua, 0); sqlite3_close(db); } static void test_tree_single_item(void) { @@ -354,26 +324,25 @@ static void test_tree_single_item(void) { _data_insert(&data, key, 42); _data_sort(&data); struct ms_test_ctx ctx = { .data = &data, .inst = &inst }; - struct merkle_sync ms; memset(&ms, 0, sizeof(ms)); - ms.inst = &inst; ms.ops = &g_test_ops; ms.data_ctx = &ctx; ms.initialized = 1; - inst.msync = &ms; + inst.ua = uasync_create(); + if (merkle_sync_init(&inst, 0x72, &g_test_ops, &ctx) != 0) abort(); merkle_sync_recompute_path(&inst, MS_NS_TEST, key); int rows = _db_count_rows(db, MS_NS_TEST); - if (rows != 5) { FAIL("expected 5 rows got %d", rows); sqlite3_close(db); return; } + if (rows != 6) { FAIL("expected 6 rows got %d", rows); merkle_sync_destroy(&inst); uasync_destroy(inst.ua, 0); sqlite3_close(db); return; } /* check hashes are non-zero */ int ok = 1; for (uint8_t lv = 1; lv <= 5 && ok; lv++) { uint64_t pf = merkle_sync_level_prefix(key, lv); - const uint8_t* h = merkle_sync_get_hash(&inst, MS_NS_TEST, lv, pf); + const uint8_t* h = _test_hash(&inst, MS_NS_TEST, lv, pf); int zero = 1; for (int i = 0; i < MT_HASH_SIZE; i++) if (h[i] != 0) zero = 0; if (zero) { FAIL("L%d zero hash", lv); ok = 0; } } if (ok) PASS(); - sqlite3_close(db); + merkle_sync_destroy(&inst); uasync_destroy(inst.ua, 0); sqlite3_close(db); } static void test_tree_multi_item_same_bucket(void) { @@ -389,9 +358,8 @@ static void test_tree_multi_item_same_bucket(void) { _data_insert(&data, k1, 1); _data_insert(&data, k2, 2); _data_sort(&data); struct ms_test_ctx ctx = { .data = &data, .inst = &inst }; - struct merkle_sync ms; memset(&ms, 0, sizeof(ms)); - ms.inst = &inst; ms.ops = &g_test_ops; ms.data_ctx = &ctx; ms.initialized = 1; - inst.msync = &ms; + inst.ua = uasync_create(); + if (merkle_sync_init(&inst, 0x72, &g_test_ops, &ctx) != 0) abort(); /* recompute for both */ merkle_sync_recompute_path(&inst, MS_NS_TEST, k1); @@ -399,22 +367,15 @@ static void test_tree_multi_item_same_bucket(void) { uint64_t pf1 = merkle_sync_level_prefix(k1, 1); uint64_t pf2 = merkle_sync_level_prefix(k2, 1); - if (pf1 != pf2) { FAIL("L1 prefixes should be equal"); sqlite3_close(db); return; } + if (pf1 != pf2) { FAIL("L1 prefixes should be equal"); merkle_sync_destroy(&inst); uasync_destroy(inst.ua, 0); sqlite3_close(db); return; } /* verify: both items contribute to same bucket hash */ uint8_t expected_hash[MT_HASH_SIZE]; - { - EVP_MD_CTX* ctx = EVP_MD_CTX_new(); - EVP_DigestInit_ex(ctx, EVP_sha256(), NULL); - uint8_t ih[MT_HASH_SIZE]; _compute_item_hash(k1, 1, ih); EVP_DigestUpdate(ctx, ih, MT_HASH_SIZE); - _compute_item_hash(k2, 2, ih); EVP_DigestUpdate(ctx, ih, MT_HASH_SIZE); - EVP_DigestFinal_ex(ctx, expected_hash, NULL); - EVP_MD_CTX_free(ctx); - } - const uint8_t* stored = merkle_sync_get_hash(&inst, MS_NS_TEST, 1, pf1); + _expected_hash(&ctx, MS_NS_TEST, 1, pf1, expected_hash); + const uint8_t* stored = _test_hash(&inst, MS_NS_TEST, 1, pf1); if (memcmp(expected_hash, stored, MT_HASH_SIZE) == 0) PASS(); else FAIL("hash mismatch"); - sqlite3_close(db); + merkle_sync_destroy(&inst); uasync_destroy(inst.ua, 0); sqlite3_close(db); } static void test_tree_different_buckets(void) { @@ -430,24 +391,23 @@ static void test_tree_different_buckets(void) { _data_insert(&data, kA, 100); _data_insert(&data, kB, 200); _data_sort(&data); struct ms_test_ctx ctx = { .data = &data, .inst = &inst }; - struct merkle_sync ms; memset(&ms, 0, sizeof(ms)); - ms.inst = &inst; ms.ops = &g_test_ops; ms.data_ctx = &ctx; ms.initialized = 1; - inst.msync = &ms; + inst.ua = uasync_create(); + if (merkle_sync_init(&inst, 0x72, &g_test_ops, &ctx) != 0) abort(); merkle_sync_recompute_path(&inst, "ns", kA); merkle_sync_recompute_path(&inst, "ns", kB); uint64_t pfA = merkle_sync_level_prefix(kA, 1); uint64_t pfB = merkle_sync_level_prefix(kB, 1); - if (pfA == pfB) { FAIL("same L1 prefix"); sqlite3_close(db); return; } + if (pfA == pfB) { FAIL("same L1 prefix"); merkle_sync_destroy(&inst); uasync_destroy(inst.ua, 0); sqlite3_close(db); return; } - const uint8_t* hA = merkle_sync_get_hash(&inst, "ns", 1, pfA); + const uint8_t* hA = _test_hash(&inst, "ns", 1, pfA); uint8_t copyA[MT_HASH_SIZE]; memcpy(copyA, hA, MT_HASH_SIZE); - const uint8_t* hB = merkle_sync_get_hash(&inst, "ns", 1, pfB); + const uint8_t* hB = _test_hash(&inst, "ns", 1, pfB); uint8_t copyB[MT_HASH_SIZE]; memcpy(copyB, hB, MT_HASH_SIZE); if (memcmp(copyA, copyB, MT_HASH_SIZE) != 0) PASS(); else FAIL("hashes should differ"); - sqlite3_close(db); + merkle_sync_destroy(&inst); uasync_destroy(inst.ua, 0); sqlite3_close(db); } static void test_tree_delete_empty_bucket(void) { @@ -461,25 +421,24 @@ static void test_tree_delete_empty_bucket(void) { uint64_t k = 0xCCCC000000000000ULL; _data_insert(&data, k, 7); _data_sort(&data); struct ms_test_ctx ctx = { .data = &data, .inst = &inst }; - struct merkle_sync ms; memset(&ms, 0, sizeof(ms)); - ms.inst = &inst; ms.ops = &g_test_ops; ms.data_ctx = &ctx; ms.initialized = 1; - inst.msync = &ms; + inst.ua = uasync_create(); + if (merkle_sync_init(&inst, 0x72, &g_test_ops, &ctx) != 0) abort(); merkle_sync_recompute_path(&inst, MS_NS_TEST, k); - if (_db_count_rows(db, MS_NS_TEST) != 5) { FAIL("expected 5 rows"); sqlite3_close(db); return; } + if (_db_count_rows(db, MS_NS_TEST) != 6) { FAIL("expected 6 rows"); merkle_sync_destroy(&inst); uasync_destroy(inst.ua, 0); sqlite3_close(db); return; } /* remove item and recompute */ data.count = 0; merkle_sync_recompute_path(&inst, MS_NS_TEST, k); - if (_db_count_rows(db, MS_NS_TEST) != 0) { FAIL("expected 0 rows got %d", _db_count_rows(db, MS_NS_TEST)); sqlite3_close(db); return; } + if (_db_count_rows(db, MS_NS_TEST) != 0) { FAIL("expected 0 rows got %d", _db_count_rows(db, MS_NS_TEST)); merkle_sync_destroy(&inst); uasync_destroy(inst.ua, 0); sqlite3_close(db); return; } uint64_t pf = merkle_sync_level_prefix(k, 3); - const uint8_t* h = merkle_sync_get_hash(&inst, MS_NS_TEST, 3, pf); + const uint8_t* h = _test_hash(&inst, MS_NS_TEST, 3, pf); int zero = 1; for (int i = 0; i < MT_HASH_SIZE; i++) if (h[i] != 0) zero = 0; if (zero) PASS(); else FAIL("hash not zero after delete"); - sqlite3_close(db); + merkle_sync_destroy(&inst); uasync_destroy(inst.ua, 0); sqlite3_close(db); } static void test_tree_consistency(void) { @@ -500,9 +459,8 @@ static void test_tree_consistency(void) { _data_sort(&data); struct ms_test_ctx ctx = { .data = &data, .inst = &inst }; - struct merkle_sync ms; memset(&ms, 0, sizeof(ms)); - ms.inst = &inst; ms.ops = &g_test_ops; ms.data_ctx = &ctx; ms.initialized = 1; - inst.msync = &ms; + inst.ua = uasync_create(); + if (merkle_sync_init(&inst, 0x72, &g_test_ops, &ctx) != 0) abort(); for (int i = 0; i < data.count; i++) merkle_sync_recompute_path(&inst, "ns", data.items[i].key); @@ -519,14 +477,10 @@ static void test_tree_consistency(void) { uint64_t pf = (uint64_t)sqlite3_column_int64(st, 1); /* compute expected hash from data */ - EVP_MD_CTX* c = EVP_MD_CTX_new(); - EVP_DigestInit_ex(c, EVP_sha256(), NULL); - _bucket_hash(&ctx, "ns", lv, pf, c); uint8_t expected[MT_HASH_SIZE]; - EVP_DigestFinal_ex(c, expected, NULL); - EVP_MD_CTX_free(c); + _expected_hash(&ctx, "ns", lv, pf, expected); - const uint8_t* stored = merkle_sync_get_hash(&inst, "ns", lv, pf); + const uint8_t* stored = _test_hash(&inst, "ns", lv, pf); if (memcmp(expected, stored, MT_HASH_SIZE) != 0) { FAIL("mismatch L%d P%016llx", lv, (unsigned long long)pf); ok = 0; @@ -535,7 +489,7 @@ static void test_tree_consistency(void) { sqlite3_finalize(st); if (ok) PASS(); - sqlite3_close(db); + merkle_sync_destroy(&inst); uasync_destroy(inst.ua, 0); sqlite3_close(db); } static int run_stage1(void) { @@ -584,6 +538,7 @@ static int _cond_done(void) { return done_sync; } static void _on_sync_done(uint64_t peer, const char* ns, int result, void* arg) { (void)arg; done_sync = 1; printf(" sync_done: peer=%016llx ns=%s result=%d\n", (unsigned long long)peer, ns, result); + if (result != MT_OK) { tests_failed++; stage_failures++; } } static int _write_cfg(const char* path, const char* fmt, ...) { @@ -860,12 +815,9 @@ static void test_randomized_two(void) { while (sqlite3_step(st) == SQLITE_ROW && ok) { uint8_t lv = (uint8_t)sqlite3_column_int(st, 0); uint64_t pf = (uint64_t)sqlite3_column_int64(st, 1); - EVP_MD_CTX* c = EVP_MD_CTX_new(); - EVP_DigestInit_ex(c, EVP_sha256(), NULL); - _bucket_hash(&ctx_a, MS_NS_RND, lv, pf, c); uint8_t expected[MT_HASH_SIZE]; - EVP_DigestFinal_ex(c, expected, NULL); EVP_MD_CTX_free(c); - const uint8_t* stored = merkle_sync_get_hash(i_a, MS_NS_RND, lv, pf); + _expected_hash(&ctx_a, MS_NS_RND, lv, pf, expected); + const uint8_t* stored = _test_hash(i_a, MS_NS_RND, lv, pf); uint8_t scopy[MT_HASH_SIZE]; memcpy(scopy, stored, MT_HASH_SIZE); if (memcmp(expected, scopy, MT_HASH_SIZE) != 0) { ok = 0; } } @@ -935,8 +887,10 @@ static void test_randomized_three(void) { /* C→A: C syncs with A, gets A∪B, A broadcasts C's new items to B (relay) */ merkle_sync_start(i_c, i_a->node_id, MS_NS_RND, _on_sync_done, NULL); if (!_wait_for("sync C", _cond_done, PHASE_TIMEOUT_TB)) { FAIL("timeout C iter %d", iter); _intg_cleanup(); break; } - /* wait for broadcast relay */ - for (int k = 0; k < 100; k++) uasync_poll(i_ua, 10); + /* Автоматический следующий раунд A↔B завершается по условию, не по числу poll(). */ + if (!_wait_for("automatic convergence of all peers", _cond_all_synced, PHASE3_TIMEOUT_TB)) { + FAIL("automatic sync timeout iter %d", iter); _intg_cleanup(); break; + } if (_intg_compare_data(&data_a, &data_b) != 0 || _intg_compare_data(&data_b, &data_c) != 0) { FAIL("data mismatch iter %d", iter); _intg_cleanup(); break; } if (_db_compare_trees(i_a->topo_sqlite_db, i_b->topo_sqlite_db, MS_NS_RND) != 0 || @@ -955,7 +909,6 @@ static void test_cancel(void) { _intg_ins_many(&data_b, i_b, 0, 5); done_sync = 0; merkle_sync_start(i_a, i_b->node_id, MS_NS_TEST, _on_sync_done, NULL); - uasync_poll(i_ua, 1); merkle_sync_cancel(i_a, i_b->node_id, MS_NS_TEST); for (int i = 0; i < 100 && i_phase == 0; i++) uasync_poll(i_ua, 10); if (!done_sync) PASS(); else FAIL("done_cb was called"); @@ -967,14 +920,14 @@ static void _sw_done1(uint64_t p, const char* n, int r, void* a) { (void)p;(void static void _sw_done2(uint64_t p, const char* n, int r, void* a) { (void)p;(void)n;(void)r;(void)a; sw_cb2_called = 1; } static void test_start_overwrite(void) { - TEST("start overwrite — cb1 replaced by cb2, only cb2 fires"); + TEST("concurrent starts — both callbacks complete"); if (_intg_init_two() != 0) { FAIL("setup failed"); return; } _intg_ins_many(&data_b, i_b, 0, 3); sw_cb1_called = sw_cb2_called = 0; merkle_sync_start(i_a, i_b->node_id, MS_NS_TEST, _sw_done1, NULL); merkle_sync_start(i_a, i_b->node_id, MS_NS_TEST, _sw_done2, NULL); for (int i = 0; i < 500 && !sw_cb2_called && i_phase == 0; i++) uasync_poll(i_ua, 10); - if (sw_cb2_called) PASS(); else FAIL("cb1=%d cb2=%d", sw_cb1_called, sw_cb2_called); + if (sw_cb1_called && sw_cb2_called) PASS(); else FAIL("cb1=%d cb2=%d", sw_cb1_called, sw_cb2_called); _intg_cleanup(); } @@ -1277,10 +1230,10 @@ static void _str_verify(void) { } /* root (level=1, prefix=0) hashes identical */ - const uint8_t* root0 = merkle_sync_get_hash(s->inst[STRESS_HUB], MS_NS_STRESS, 1, 0); + const uint8_t* root0 = _test_hash(s->inst[STRESS_HUB], MS_NS_STRESS, 1, 0); uint8_t root_copy[MT_HASH_SIZE]; memcpy(root_copy, root0, MT_HASH_SIZE); for (int i = 1; i < STRESS_N && ok; i++) { - const uint8_t* ri = merkle_sync_get_hash(s->inst[i], MS_NS_STRESS, 1, 0); + const uint8_t* ri = _test_hash(s->inst[i], MS_NS_STRESS, 1, 0); if (memcmp(root_copy, ri, MT_HASH_SIZE) != 0) { printf(" ROOT HASH MISMATCH inst[%d]\n", i); ok = 0; } } @@ -1294,12 +1247,9 @@ static void _str_verify(void) { while (sqlite3_step(st) == SQLITE_ROW && ok) { uint8_t lv = (uint8_t)sqlite3_column_int(st, 0); uint64_t pf = (uint64_t)sqlite3_column_int64(st, 1); - EVP_MD_CTX* c = EVP_MD_CTX_new(); - EVP_DigestInit_ex(c, EVP_sha256(), NULL); - _bucket_hash(&s->ctx[i], MS_NS_STRESS, lv, pf, c); uint8_t expected[MT_HASH_SIZE]; - EVP_DigestFinal_ex(c, expected, NULL); EVP_MD_CTX_free(c); - const uint8_t* stored = merkle_sync_get_hash(s->inst[i], MS_NS_STRESS, lv, pf); + _expected_hash(&s->ctx[i], MS_NS_STRESS, lv, pf, expected); + const uint8_t* stored = _test_hash(s->inst[i], MS_NS_STRESS, lv, pf); uint8_t scopy[MT_HASH_SIZE]; memcpy(scopy, stored, MT_HASH_SIZE); if (memcmp(expected, scopy, MT_HASH_SIZE) != 0) { printf(" consistency fail inst[%d] L%d P%016llx\n", i, lv, (unsigned long long)pf); @@ -1320,12 +1270,12 @@ static void _str_verify(void) { static void _str_check_roots(void) { struct str_state* s = gs; if (!s) return; - const uint8_t* root0 = merkle_sync_get_hash(s->inst[STRESS_HUB], MS_NS_STRESS, 1, 0); + const uint8_t* root0 = _test_hash(s->inst[STRESS_HUB], MS_NS_STRESS, 1, 0); uint8_t root_copy[MT_HASH_SIZE]; memcpy(root_copy, root0, MT_HASH_SIZE); int converged = 1; for (int i = 1; i < STRESS_N; i++) { - const uint8_t* ri = merkle_sync_get_hash(s->inst[i], MS_NS_STRESS, 1, 0); + const uint8_t* ri = _test_hash(s->inst[i], MS_NS_STRESS, 1, 0); if (memcmp(root_copy, ri, MT_HASH_SIZE) != 0) { converged = 0; break; } } if (converged) { @@ -1387,11 +1337,38 @@ static void test_stress_spam(void) { PASS(); _str_cleanup(); } +static int _cond_pair_equal(void) { + if (data_a.count != data_b.count) return 0; + for (int i = 0; i < data_a.count; i++) + if (data_a.items[i].key != data_b.items[i].key || data_a.items[i].val != data_b.items[i].val) return 0; + return _db_compare_trees(i_a->topo_sqlite_db, i_b->topo_sqlite_db, MS_NS_TEST) == 0; +} + +static void test_large_leaf_and_automatic_update(void) { + TEST("6000 records in one leaf (>64K), then automatic update without start/broadcast"); + if (_intg_init_two() != 0) { FAIL("setup failed"); return; } + for (uint64_t i = 0; i < 6000; i++) _data_insert(&data_b, i, (uint32_t)i); + _data_sort(&data_b); + if (merkle_sync_recompute_path(i_b, MS_NS_TEST, 0) < 0) { FAIL("recompute failed"); _intg_cleanup(); return; } + done_sync = 0; + merkle_sync_start(i_a, i_b->node_id, MS_NS_TEST, _on_sync_done, NULL); + if (!_wait_for("large leaf", _cond_done, PHASE_TIMEOUT_TB) || !_cond_pair_equal()) { + FAIL("large leaf did not converge"); _intg_cleanup(); return; + } + data_b.items[0].val = 9999; + merkle_sync_recompute_path(i_b, MS_NS_TEST, 0); + if (!_wait_for("automatic change", _cond_pair_equal, PHASE_TIMEOUT_TB)) { + FAIL("automatic change did not propagate"); _intg_cleanup(); return; + } + PASS(); _intg_cleanup(); +} + static int run_stage6(void) { printf("--- Stage 6: protocol edge cases ---\n"); stage_failures = 0; test_cancel(); test_start_overwrite(); + test_large_leaf_and_automatic_update(); return stage_failures; } @@ -1406,6 +1383,7 @@ int main(void) { printf("=== test_merkle_sync ===\n"); debug_config_init(); debug_set_level(DEBUG_LEVEL_ERROR); + if (getenv("UTUN_TEST_DEBUG")) debug_set_category_level(DEBUG_CATEGORY_MEMBER_SYNC, DEBUG_LEVEL_DEBUG); if (run_stage1() != 0) { printf("=== Stage 1 FAILED ===\n"); return 1; } printf("=== Stage 1 PASSED (%d tests) ===\n\n", tests_run);