Refactor Merkle sync into bounded bidirectional rounds with atomic member updates

This commit is contained in:
evgeny committed 2026-09-27 14:44:39 +03:00
1 parent 1b92b4885b
commit 3e25bcef02
21 files changed
+1550 -1859

No files matched your search

+4
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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);
+9 -1
View File
@@ -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);
+310 -242
View File
@@ -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) ── */
/* Канонические целые для 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 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);
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;
}
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); }
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;
}
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);
/* 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; }
struct member_wire {
struct ms_member_rec rec;
char userinfo[256], tags[256];
uint8_t local_join[64];
};
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;
/* Одна строгая раскладка для проверки и применения. Нельзя молча принимать усечённую запись. */
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;
}
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);
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 (!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 (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 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;
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);
}
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);
}
+7 -22
View File
@@ -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 любого узла (включая себя).
+45 -134
View File
@@ -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; адаптер никогда не перечисляет все записи для внутреннего узла.
+465 -866
View File
File diff suppressed because it is too large. Load diff
+40 -298
View File
@@ -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 ── */
/* Обновление индекса вызывается моделью данных.
* При внешней транзакции 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]);
/*
* Пересчитать 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);
#endif /* MERKLE_SYNC_H */
#endif
+79 -137
View File
@@ -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.
Детерминированное слияние обеспечивает модель; движок не выбирает победителя конфликта.
| Коллбэк | Что делает |
Ожидается только один ответ QUERY. Стек — максимум шесть кадров с сохранёнными соседними ветвями.
Номера запросов проверяются, ответы другого раунда игнорируются.
Повторную передачу обеспечивает ETCP, отдельных таймеров повторов нет.
## Wire
Заголовок: `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 |
|---|---|
| `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 |
| 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 |
`ctx` — прозрачный контекст, передаваемый в `merkle_sync_init` и возвращаемый первым аргументом
в каждый коллбэк. `ns` (namespace) — строка-имя домена данных, обычно `channel_id`.
Курсор продолжения должен возрастать и принадлежать запрошенному листу.
Длины, состояние, тип, префикс и номер запроса проверяются до применения.
Формат model_data определяет адаптер. Старый протокол не поддерживается.
## 4. Формат дерева
## Очереди, диагностика, тесты
5-уровневое префиксное дерево над 64-битными ключами. Каждый уровень берёт 5 старших бит ключа:
уровень 1 — биты 59-63, уровень 5 — биты 39-63. Каждый узел разбивается на 32 бакета (5 бит = 32).
Сессия удерживает ETCP_CONN и исходную очередь. У неё один подготовленный пакет,
один waiter и один отложенный вызов. Передача идёт только после разрешения waiter.
Изменения объединяются флагом dirty; неограниченной out-очереди нет.
Отмена снимает ожидающий и отложенный callback waiter независимо от links_up.
```
level 1 [0..31] корень: 32 бакета по 5 бит
level 2 [0..31]...[0..31] каждый — ещё 32 бакета
...
level 5 [0..31].........[0..31] листья: 32^5 = 33M бакетов макс
```
Категория member_sync: INFO — attach, начало/завершение раунда, detach;
DEBUG — TX/RX, номера запросов, размеры, состояние; ERROR — причина отказа.
Ведущий выводит счётчики переданных и полученных байтов при подтверждении.
- Хеш бакета = `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-таймер
Отдельный редкий таймер `MS_WAL_INTERVAL_MS = 60000` (60 с) — `sqlite3_wal_checkpoint_v2(...,
PASSIVE)`. Локальная операция без сети. На Android (`UTUN_HAVE_STANDBY`) при спячке откладывается
через `standby_wait`. Периодической сверки деревьев (bg-таймера) нет — пересчёт событийный.
## 8. API
| Функция | Назначение |
|---|---|
| `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`.
test_merkle_sync проверяет индекс, односторонний start, двустороннее слияние, несколько узлов,
параллельные изменения, большой лист с пагинацией и автоматическое обновление.
test_merkle_protocol проверяет ошибочные пакеты/курсоры, сохранение соседних ветвей,
отмену waiter, ошибки apply/SQLite, backpressure, disconnect и повторное подключение.
+122
View File
@@ -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
View File
@@ -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
View File
@@ -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);
}