You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
174 lines
9.1 KiB
174 lines
9.1 KiB
/** test_member_sync.c — standalone hash tree protocol test with 1000 members */ |
|
#include <stdio.h> |
|
#include <string.h> |
|
#include <stdlib.h> |
|
#include <assert.h> |
|
#include <sqlite3.h> |
|
#include <openssl/evp.h> |
|
|
|
#define HASH_SZ 32 |
|
#define BUCKETS 32 |
|
#define MAX_LVL 5 |
|
|
|
static void _sanitize(const char* ch_id, char* out, size_t sz) { |
|
size_t i = 0; |
|
while (*ch_id && i < sz - 1) { |
|
char c = *ch_id++; |
|
if ((c>='a'&&c<='z')||(c>='A'&&c<='Z')||(c>='0'&&c<='9')||c=='_') out[i++]=c; else out[i++]='_'; |
|
} |
|
out[i]=0; |
|
} |
|
|
|
static void _init_db(sqlite3* db) { |
|
sqlite3_exec(db, |
|
"CREATE TABLE IF NOT EXISTS nodes(node_id INTEGER PRIMARY KEY," |
|
" x25519_pubkey BLOB NOT NULL, ed25519_pubkey BLOB, online INTEGER DEFAULT 0);" |
|
"CREATE TABLE IF NOT EXISTS member_tree_hash(channel_id TEXT NOT NULL," |
|
" level INTEGER NOT NULL, prefix64 INTEGER NOT NULL, hash BLOB NOT NULL," |
|
" member_count INTEGER NOT NULL, PRIMARY KEY(channel_id, level, prefix64));", |
|
NULL, NULL, NULL); |
|
} |
|
|
|
static void _create_peers(sqlite3* db, const char* ch_id) { |
|
char san[64]; _sanitize(ch_id, san, sizeof(san)); |
|
char sql[256]; snprintf(sql, sizeof(sql), |
|
"CREATE TABLE IF NOT EXISTS peers_%s(node_id INTEGER NOT NULL," |
|
" join_sig BLOB NOT NULL, PRIMARY KEY(node_id))", san); |
|
sqlite3_exec(db, sql, NULL, NULL, NULL); |
|
} |
|
|
|
static void _member_hash(uint64_t nid, const uint8_t* x25, const uint8_t* ed, |
|
const uint8_t* sig, uint8_t out[HASH_SZ]) { |
|
EVP_MD_CTX* ctx = EVP_MD_CTX_new(); |
|
EVP_DigestInit_ex(ctx, EVP_sha256(), NULL); |
|
EVP_DigestUpdate(ctx, &nid, 8); |
|
EVP_DigestUpdate(ctx, x25, 32); |
|
EVP_DigestUpdate(ctx, ed, 32); |
|
EVP_DigestUpdate(ctx, sig, 64); |
|
EVP_DigestFinal_ex(ctx, out, NULL); |
|
EVP_MD_CTX_free(ctx); |
|
} |
|
|
|
static uint64_t _mask(int lvl) { int s=63-lvl*5; return s>=0 ? ~0ULL<<s : UINT64_MAX; } |
|
|
|
static void _tree_hash(sqlite3* db, const char* ch_id, int lvl, uint64_t pref, uint8_t out[HASH_SZ]) { |
|
char san[64]; _sanitize(ch_id, san, sizeof(san)); |
|
char sql[512]; snprintf(sql, sizeof(sql), |
|
"SELECT p.node_id, n.x25519_pubkey, n.ed25519_pubkey, p.join_sig" |
|
" FROM peers_%s p JOIN nodes n ON p.node_id=n.node_id" |
|
" WHERE (p.node_id & %lld)==%lld ORDER BY p.node_id ASC", |
|
san, (long long)_mask(lvl), (long long)pref); |
|
sqlite3_stmt* st=NULL; |
|
if (sqlite3_prepare_v2(db, sql, -1, &st, NULL)!=SQLITE_OK) { memset(out,0,HASH_SZ); return; } |
|
EVP_MD_CTX* ctx = EVP_MD_CTX_new(); |
|
EVP_DigestInit_ex(ctx, EVP_sha256(), NULL); |
|
int cnt=0; |
|
while (sqlite3_step(st)==SQLITE_ROW) { |
|
uint64_t nid=(uint64_t)sqlite3_column_int64(st,0); |
|
const uint8_t* x=sqlite3_column_blob(st,1),*e=sqlite3_column_blob(st,2),*s=sqlite3_column_blob(st,3); |
|
if(!x||!e||!s)continue; |
|
uint8_t mh[HASH_SZ]; _member_hash(nid,x,e,s,mh); EVP_DigestUpdate(ctx,mh,HASH_SZ); cnt++; |
|
} |
|
sqlite3_finalize(st); |
|
if(cnt==0){EVP_MD_CTX_free(ctx);memset(out,0,HASH_SZ);return;} |
|
EVP_DigestFinal_ex(ctx,out,NULL); |
|
EVP_MD_CTX_free(ctx); |
|
} |
|
|
|
static void _insert(sqlite3* db, const char* ch_id, uint64_t nid, |
|
const uint8_t* x, const uint8_t* e, const uint8_t* s) { |
|
char san[64]; _sanitize(ch_id, san, sizeof(san)); |
|
sqlite3_stmt* st=NULL; |
|
sqlite3_prepare_v2(db,"INSERT OR REPLACE INTO nodes(node_id,x25519_pubkey,ed25519_pubkey) VALUES(?,?,?)",-1,&st,NULL); |
|
if(st){sqlite3_bind_int64(st,1,(sqlite3_int64)nid);sqlite3_bind_blob(st,2,x,32,SQLITE_STATIC);sqlite3_bind_blob(st,3,e,32,SQLITE_STATIC);sqlite3_step(st);sqlite3_finalize(st);} |
|
char sql[256]; snprintf(sql,sizeof(sql),"INSERT OR REPLACE INTO peers_%s(node_id,join_sig) VALUES(?,?)",san); |
|
st=NULL; sqlite3_prepare_v2(db,sql,-1,&st,NULL); |
|
if(st){sqlite3_bind_int64(st,1,(sqlite3_int64)nid);sqlite3_bind_blob(st,2,s,64,SQLITE_STATIC);sqlite3_step(st);sqlite3_finalize(st);} |
|
} |
|
|
|
static void _gen(uint64_t id, uint8_t x[32], uint8_t e[32], uint8_t s[64]) { |
|
memset(x,0,32); *(uint64_t*)x=id; memset(e,0,32); *(uint64_t*)e=id+1; memset(s,0,64); *(uint64_t*)s=id; |
|
} |
|
|
|
static int _count(sqlite3* db, const char* ch) { |
|
char san[64]; _sanitize(ch,san,sizeof(san)); char sql[256]; snprintf(sql,sizeof(sql),"SELECT COUNT(*) FROM peers_%s",san); |
|
sqlite3_stmt* st=NULL; sqlite3_prepare_v2(db,sql,-1,&st,NULL); int c=0; |
|
if(st&&sqlite3_step(st)==SQLITE_ROW)c=sqlite3_column_int(st,0); if(st)sqlite3_finalize(st); return c; |
|
} |
|
|
|
/* protocol resolve — recursively compare trees, exchange DATA for differences */ |
|
static void _resolve(sqlite3* a, sqlite3* b, const char* ch, int lvl, uint64_t pref) { |
|
uint8_t ha[BUCKETS][HASH_SZ], hb[BUCKETS][HASH_SZ]; |
|
uint32_t bm_a=0,bm_b=0; |
|
int nl=lvl+1; if(nl>MAX_LVL)nl=MAX_LVL; |
|
for(int i=0;i<BUCKETS;i++) { |
|
uint64_t cp=pref|((uint64_t)i<<(63-nl*5>0?63-nl*5:0)); |
|
_tree_hash(a,ch,nl,cp,ha[i]);_tree_hash(b,ch,nl,cp,hb[i]); |
|
int ae=1,be=1; for(int j=0;j<HASH_SZ;j++){if(ha[i][j])ae=0;if(hb[i][j])be=0;} |
|
if(!ae)bm_a|=(1u<<i); if(!be)bm_b|=(1u<<i); |
|
} |
|
for(int i=0;i<BUCKETS;i++) { |
|
int ah=(bm_a>>i)&1,bh=(bm_b>>i)&1; if(!ah&&!bh)continue; |
|
if(ah&&bh&&memcmp(ha[i],hb[i],HASH_SZ)==0)continue; |
|
uint64_t cp=pref|((uint64_t)i<<(63-nl*5>0?63-nl*5:0)); |
|
char san[64];_sanitize(ch,san,sizeof(san)); int ca=0,cb=0; |
|
sqlite3_stmt* st=NULL; char sql[512]; |
|
snprintf(sql,sizeof(sql),"SELECT COUNT(*) FROM peers_%s WHERE (node_id & %lld)==%lld", |
|
san,(long long)_mask(nl),(long long)cp); |
|
sqlite3_prepare_v2(a,sql,-1,&st,NULL);if(st&&sqlite3_step(st)==SQLITE_ROW)ca=sqlite3_column_int(st,0);if(st)sqlite3_finalize(st); |
|
st=NULL;sqlite3_prepare_v2(b,sql,-1,&st,NULL);if(st&&sqlite3_step(st)==SQLITE_ROW)cb=sqlite3_column_int(st,0);if(st)sqlite3_finalize(st); |
|
if(nl>=MAX_LVL||ca<8||cb<8) { |
|
for(int side=0;side<2;side++) { sqlite3*src=side?a:b,*dst=side?b:a; sqlite3_stmt*r=NULL; |
|
sqlite3_prepare_v2(src,sql,-1,&r,NULL); /* count query already has our WHERE — rebuild for SELECT */ |
|
char sq2[512]; snprintf(sq2,sizeof(sq2), |
|
"SELECT p.node_id,n.x25519_pubkey,n.ed25519_pubkey,p.join_sig" |
|
" FROM peers_%s p JOIN nodes n ON p.node_id=n.node_id" |
|
" WHERE (p.node_id & %lld)==%lld ORDER BY p.node_id", |
|
san,(long long)_mask(nl),(long long)cp); |
|
r=NULL; sqlite3_prepare_v2(src,sq2,-1,&r,NULL); |
|
if(r){while(sqlite3_step(r)==SQLITE_ROW){uint64_t nid=(uint64_t)sqlite3_column_int64(r,0); |
|
const uint8_t* x=sqlite3_column_blob(r,1),*e=sqlite3_column_blob(r,2),*s=sqlite3_column_blob(r,3); |
|
if(x&&e&&s)_insert(dst,ch,nid,x,e,s);} sqlite3_finalize(r);} |
|
} |
|
} else _resolve(a,b,ch,nl,cp); |
|
} |
|
} |
|
|
|
/* ── Tests ── */ |
|
static void t_empty(void) { |
|
printf("t_empty... "); sqlite3 *a,*b; |
|
sqlite3_open(":memory:",&a);sqlite3_open(":memory:",&b); _init_db(a);_init_db(b); _create_peers(a,"e");_create_peers(b,"e"); |
|
uint8_t x[32],e[32],s[64]; _gen(1,x,e,s); _insert(a,"e",1,x,e,s); _insert(b,"e",1,x,e,s); |
|
assert(_count(a,"e")==1&&_count(b,"e")==1); |
|
uint8_t ha[HASH_SZ],hb[HASH_SZ]; _tree_hash(a,"e",1,_mask(1)&1,ha);_tree_hash(b,"e",1,_mask(1)&1,hb); |
|
assert(memcmp(ha,hb,HASH_SZ)==0); sqlite3_close(a);sqlite3_close(b); printf("OK\n"); |
|
} |
|
static void t_1000(void) { |
|
printf("t_1000 (500+500)... "); sqlite3 *a,*b; |
|
sqlite3_open(":memory:",&a);sqlite3_open(":memory:",&b); _init_db(a);_init_db(b); _create_peers(a,"B");_create_peers(b,"B"); |
|
for(int i=0;i<1000;i++){uint64_t id=((uint64_t)(i+1))&0x7FFFFFFFFFFFFFFFULL; uint8_t x[32],e[32],s[64]; _gen(id,x,e,s); |
|
if(i<500)_insert(a,"B",id,x,e,s); else _insert(b,"B",id,x,e,s);} |
|
assert(_count(a,"B")==500&&_count(b,"B")==500); _resolve(a,b,"B",1,0); |
|
printf("A=%d B=%d ",_count(a,"B"),_count(b,"B")); assert(_count(a,"B")==1000&&_count(b,"B")==1000); |
|
for(int i=0;i<BUCKETS;i++){uint64_t cp=(uint64_t)i<<58; uint8_t ha[HASH_SZ],hb[HASH_SZ]; |
|
_tree_hash(a,"B",1,cp,ha);_tree_hash(b,"B",1,cp,hb); assert(memcmp(ha,hb,HASH_SZ)==0);} |
|
sqlite3_close(a);sqlite3_close(b); printf("OK\n"); |
|
} |
|
static void t_overlap(void) { |
|
printf("t_overlap (300 shared, 200 unique each)... "); sqlite3 *a,*b; |
|
sqlite3_open(":memory:",&a);sqlite3_open(":memory:",&b); _init_db(a);_init_db(b); _create_peers(a,"o");_create_peers(b,"o"); |
|
for(int i=0;i<700;i++){uint64_t id=((uint64_t)(i+1))&0x7FFFFFFFFFFFFFFFULL; uint8_t x[32],e[32],s[64]; _gen(id,x,e,s); |
|
if(i<500)_insert(a,"o",id,x,e,s); if(i>=200)_insert(b,"o",id,x,e,s);} |
|
assert(_count(a,"o")==500&&_count(b,"o")==500); _resolve(a,b,"o",1,0); |
|
assert(_count(a,"o")==700&&_count(b,"o")==700); |
|
int all_ok=1; for(int i=0;i<BUCKETS;i++){uint64_t cp=(uint64_t)i<<58; uint8_t ha[HASH_SZ],hb[HASH_SZ]; |
|
_tree_hash(a,"o",1,cp,ha);_tree_hash(b,"o",1,cp,hb); if(memcmp(ha,hb,HASH_SZ)){all_ok=0;break;}} assert(all_ok); |
|
sqlite3_close(a);sqlite3_close(b); printf("OK\n"); |
|
} |
|
|
|
int main(void) { |
|
printf("=== test_member_sync ===\n"); |
|
t_empty(); t_overlap(); t_1000(); |
|
printf("=== ALL TESTS PASSED ===\n"); |
|
return 0; |
|
}
|
|
|