Browse Source

Refactor Merkle sync into bounded bidirectional rounds with atomic member updates

proxy
evgeny 4 days ago
parent
commit
3e25bcef02
  1. 4
      src/Makefile.am
  2. 2
      src/chat/chat_channel.c
  3. 1
      src/chat/chat_core.c
  4. 1
      src/chat/chat_join.c
  5. 1
      src/chat/chat_profile.c
  6. 10
      src/chat/chat_sync.c
  7. 558
      src/chat/member_sync.c
  8. 29
      src/chat/member_sync.h
  9. 179
      src/chat/member_sync_doc.md
  10. 1343
      src/chat/merkle_sync.c
  11. 340
      src/chat/merkle_sync.h
  12. 220
      src/chat/merkle_sync_doc.md
  13. 122
      src/chat/merkle_tree.c
  14. 24
      src/chat/merkle_tree.h
  15. 8
      src/transport_layer/etcp_api.c
  16. 8
      tests/Makefile.am
  17. 8
      tests/test_chat_join_e2e.c
  18. 8
      tests/test_media_delivery_full.c
  19. 68
      tests/test_member_adapter.c
  20. 215
      tests/test_merkle_protocol.c
  21. 284
      tests/test_merkle_sync.c

4
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 \

2
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);

1
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);

1
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);

1
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);

10
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);

558
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);
}

29
src/chat/member_sync.h

@ -3,7 +3,7 @@
*
* Тонкая прослойка над merkle_sync, адаптированная под мемберов каналов.
* Данные хранятся в таблицах: nodes, node_addresses, peers_<channel_id>.
* Хеш мембера = 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_<ch_id>). */
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 любого узла (включая себя).

179
src/chat/member_sync_doc.md

@ -1,149 +1,60 @@
# member_sync — синхронизация участников канала
# member_sync — адаптер участников каналов
## 1. Назначение
Поверх [merkle_sync](merkle_sync_doc.md) синхронизирует source=0 записи таблиц peers_<channel_id>.
Транспортный движок не занимается подписями и версиями участников.
Тонкая прослойка над `merkle_sync`, адаптированная под участников (мемберов) чат-каналов.
Хранит мемберов в SQLite (`peers_<ch_id>`, `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_<ch_id>` (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_<ch_id>` |
| `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; адаптер никогда не перечисляет все записи для внутреннего узла.

1343
src/chat/merkle_sync.c

File diff suppressed because it is too large Load Diff

340
src/chat/merkle_sync.h

@ -1,319 +1,61 @@
#ifndef MERKLE_SYNC_H
#define MERKLE_SYNC_H
#include <stdint.h>
#include "merkle_tree.h"
#include <stddef.h>
#include <openssl/evp.h>
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_<ns> 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

220
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 и повторное подключение.

122
src/chat/merkle_tree.c

@ -0,0 +1,122 @@
#include "merkle_tree.h"
#include "../../lib/debug_config.h"
#include <string.h>
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);
}

24
src/chat/merkle_tree.h

@ -0,0 +1,24 @@
#ifndef MERKLE_TREE_H
#define MERKLE_TREE_H
#include <stdint.h>
#include <openssl/evp.h>
#include <sqlite3.h>
#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

8
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);
}

8
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)

8
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");

8
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;

68
tests/test_member_adapter.c

@ -0,0 +1,68 @@
/* Реальный адаптер, SQLite и транзакции; без копии алгоритма и без сетевых таймингов. */
#include "../src/chat/member_sync.c"
#include <assert.h>
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;
}

215
tests/test_merkle_protocol.c

@ -0,0 +1,215 @@
/* Проверяем границы автомата на точных wire-пакетах. Включение реализации позволяет
* задать состояние без копирования private-структур и без подмены очередей/таймеров. */
#include "../src/chat/merkle_sync.c"
#include <assert.h>
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;
}

284
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);

Loading…
Cancel
Save