Refactor Merkle sync into bounded bidirectional rounds with atomic member updates
This commit is contained in:
1 parent
1b92b4885b
commit
3e25bcef02
21 files changed
+1550
-1859
No files matched your search
@@ -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 \
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
File diff suppressed because it is too large.
Load diff
+40
-298
@@ -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
@@ -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 и повторное подключение.
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user