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.
1411 lines
59 KiB
1411 lines
59 KiB
#include <stdio.h> |
|
#include <stdlib.h> |
|
#include <string.h> |
|
#include <stdint.h> |
|
#include <stdarg.h> |
|
#include <sys/stat.h> |
|
#include "../lib/debug_config.h" |
|
#include "../lib/mem.h" |
|
#include "../lib/u_async.h" |
|
#include "../lib/platform_compat.h" |
|
#include "../lib/socket_compat.h" |
|
#include "merkle_sync.h" |
|
#include "../src/utun_instance.h" |
|
#include "../src/routing_layer/topo_group.h" |
|
#include "../src/config_updater.h" |
|
#include "../src/tun_if.h" |
|
#include "etcp.h" |
|
#include "etcp_connections.h" |
|
#include "test_utils.h" |
|
#include <sqlite3.h> |
|
#include <openssl/evp.h> |
|
|
|
/* numeric channel_id == group_id для CHAT-групп (recv требует существующую группу) */ |
|
#define MS_NS_TEST "1001" |
|
#define MS_NS_RND "1002" |
|
#define MS_NS_STRESS "1003" |
|
|
|
static void _ms_ensure_groups(struct UTUN_INSTANCE* inst) { |
|
if (!inst || !inst->topo_groups) return; |
|
const char* nss[] = { MS_NS_TEST, MS_NS_RND, MS_NS_STRESS }; |
|
for (int i = 0; i < 3; i++) { |
|
uint64_t gid = strtoull(nss[i], NULL, 10); |
|
if (!topo_groups_find(inst->topo_groups, gid)) |
|
topo_groups_create_group(inst->topo_groups, gid, TOPO_GROUP_TYPE_CHAT, nss[i]); |
|
} |
|
} |
|
|
|
static int tests_run = 0; |
|
static int tests_failed = 0; |
|
static int stage_failures = 0; |
|
|
|
#define TEST(name) do { tests_run++; printf(" %s: ", name); } while(0) |
|
#define PASS() do { printf("PASS\n"); } while(0) |
|
#define FAIL(fmt, ...) do { printf("FAIL — " fmt "\n", ##__VA_ARGS__); tests_failed++; stage_failures++; } while(0) |
|
|
|
/* ── Stage 2: mock data store ── */ |
|
|
|
#define MS_MAX_ITEMS 8192 |
|
|
|
struct ms_item { uint64_t key; uint32_t val; }; |
|
|
|
struct ms_data { |
|
struct ms_item items[MS_MAX_ITEMS]; |
|
int count; /* sorted by key */ |
|
}; |
|
|
|
static int _cmp_item(const void* a, const void* b) { |
|
uint64_t ka = ((const struct ms_item*)a)->key, kb = ((const struct ms_item*)b)->key; |
|
return ka < kb ? -1 : ka > kb ? 1 : 0; |
|
} |
|
|
|
static void _data_sort(struct ms_data* d) { qsort(d->items, (size_t)d->count, sizeof(struct ms_item), _cmp_item); } |
|
|
|
static void _data_init(struct ms_data* d) { memset(d, 0, sizeof(*d)); } |
|
|
|
static void _data_insert(struct ms_data* d, uint64_t key, uint32_t val) { |
|
for (int i = 0; i < d->count; i++) |
|
if (d->items[i].key == key) { d->items[i].val = val; return; } |
|
if (d->count >= MS_MAX_ITEMS) return; |
|
d->items[d->count].key = key; d->items[d->count].val = val; |
|
d->count++; |
|
} |
|
|
|
static int _data_count_in_prefix(struct ms_data* d, uint8_t level, uint64_t prefix64) { |
|
int mask_shift = 64 - (int)level * 5; |
|
uint64_t mask = (mask_shift >= 0 && mask_shift < 64) ? (~0ULL << mask_shift) : UINT64_MAX; |
|
int cnt = 0; |
|
for (int i = 0; i < d->count; i++) |
|
if ((d->items[i].key & mask) == prefix64) cnt++; |
|
return cnt; |
|
} |
|
|
|
/* ── Stage 2: mock merkle_sync_data_ops ── */ |
|
|
|
struct ms_test_ctx { |
|
struct ms_data* data; |
|
struct UTUN_INSTANCE* inst; |
|
char tag; /* 'A'/'B'/'C' for debug */ |
|
}; |
|
|
|
static void _compute_item_hash(uint64_t key, uint32_t val, uint8_t out[MT_HASH_SIZE]) { |
|
EVP_MD_CTX* ctx = EVP_MD_CTX_new(); |
|
EVP_DigestInit_ex(ctx, EVP_sha256(), NULL); |
|
EVP_DigestUpdate(ctx, &key, 8); |
|
EVP_DigestUpdate(ctx, &val, 4); |
|
EVP_DigestFinal_ex(ctx, out, NULL); |
|
EVP_MD_CTX_free(ctx); |
|
} |
|
|
|
static int _bucket_hash(void* ctx, const char* ns, uint8_t level, uint64_t prefix64, EVP_MD_CTX* sha_ctx) { |
|
(void)ns; |
|
struct ms_data* d = ((struct ms_test_ctx*)ctx)->data; |
|
int mask_shift = 64 - (int)level * 5; |
|
uint64_t mask = (level == 0) ? 0 : ((mask_shift >= 0 && mask_shift < 64) ? (~0ULL << mask_shift) : UINT64_MAX); |
|
int count = 0; |
|
for (int i = 0; i < d->count; i++) { |
|
if ((d->items[i].key & mask) != prefix64) continue; |
|
uint8_t ih[MT_HASH_SIZE]; _compute_item_hash(d->items[i].key, d->items[i].val, ih); |
|
EVP_DigestUpdate(sha_ctx, ih, MT_HASH_SIZE); |
|
count++; |
|
} |
|
return count; |
|
} |
|
|
|
static int _get_page(void* ctx, const char* ns, uint64_t prefix, int after_valid, uint64_t after, |
|
uint8_t* buf, size_t* len, uint64_t* next, int* more) { |
|
(void)ns; |
|
struct ms_data* d = ((struct ms_test_ctx*)ctx)->data; |
|
uint16_t count = 0; size_t off = 2; |
|
*next = 0; *more = 0; |
|
for (int i = 0; i < d->count; i++) { |
|
if (merkle_sync_level_prefix(d->items[i].key, MT_MAX_LEVEL) != prefix |
|
|| (after_valid && d->items[i].key <= after)) continue; |
|
if (off + 12 > *len) { if (!count) return -1; *more = 1; break; } |
|
memcpy(buf + off, &d->items[i].key, 8); off += 8; |
|
memcpy(buf + off, &d->items[i].val, 4); off += 4; |
|
*next = d->items[i].key; count++; |
|
} |
|
memcpy(buf, &count, 2); *len = off; return 0; |
|
} |
|
|
|
static int _apply_items(void* ctx, const char* ns, uint64_t from_peer, const uint8_t* data, size_t len) { |
|
(void)from_peer; |
|
struct ms_test_ctx* tc = ctx; |
|
struct ms_data* d = tc->data; |
|
if (len < 2) return -1; |
|
uint16_t count; memcpy(&count, data, 2); |
|
if (len != 2 + (size_t)count * 12) return -1; |
|
int changed = 0; |
|
for (uint16_t i = 0; i < count; i++) { |
|
uint64_t key; uint32_t val; |
|
memcpy(&key, data + 2 + (size_t)i * 12, 8); |
|
memcpy(&val, data + 10 + (size_t)i * 12, 4); |
|
int found = 0; |
|
for (int j = 0; j < d->count; j++) { |
|
if (d->items[j].key != key) continue; |
|
if (d->items[j].val < val) { d->items[j].val = val; changed = 1; } |
|
found = 1; break; |
|
} |
|
if (!found) { if (d->count == MS_MAX_ITEMS) return -1; _data_insert(d, key, val); changed = 1; } |
|
} |
|
if (changed) { |
|
_data_sort(d); |
|
/* Страница всегда относится к одному листу: пересчитываем его один раз. */ |
|
uint64_t key; memcpy(&key, data + 2, 8); |
|
if (merkle_sync_recompute_path(tc->inst, ns, key) < 0) return -1; |
|
} |
|
return 0; |
|
} |
|
|
|
static const struct merkle_sync_data_ops g_test_ops = { |
|
.update_bucket_hash = _bucket_hash, .get_page = _get_page, .apply_items = _apply_items |
|
}; |
|
|
|
static const uint8_t* _test_hash(struct UTUN_INSTANCE* inst, const char* ns, uint8_t level, uint64_t prefix) { |
|
static uint8_t hash[MT_HASH_SIZE]; |
|
if (merkle_sync_read_hash(inst, ns, level, prefix, hash) < 0) abort(); |
|
return hash; |
|
} |
|
|
|
/* Независимое восстановление дерева из набора записей, без чтения индекса. */ |
|
static void _expected_hash(struct ms_test_ctx* tc, const char* ns, uint8_t level, uint64_t prefix, uint8_t hash[MT_HASH_SIZE]) { |
|
memset(hash, 0, MT_HASH_SIZE); |
|
int present = 0; |
|
for (int i = 0; i < tc->data->count; i++) |
|
if (merkle_sync_level_prefix(tc->data->items[i].key, level) == prefix) { present = 1; break; } |
|
if (!present) return; |
|
EVP_MD_CTX* c = EVP_MD_CTX_new(); |
|
EVP_DigestInit_ex(c, EVP_sha256(), NULL); |
|
uint8_t domain[2] = { 0x4d, level }; EVP_DigestUpdate(c, domain, sizeof(domain)); |
|
if (level == MT_MAX_LEVEL) _bucket_hash(tc, ns, level, prefix, c); |
|
else { |
|
for (unsigned i = 0; i < MT_BUCKETS; i++) { |
|
uint8_t child[MT_HASH_SIZE]; |
|
_expected_hash(tc, ns, level + 1, prefix | ((uint64_t)i << (64 - (level + 1) * 5)), child); |
|
EVP_DigestUpdate(c, child, sizeof(child)); |
|
} |
|
} |
|
EVP_DigestFinal_ex(c, hash, NULL); EVP_MD_CTX_free(c); |
|
} |
|
|
|
/* ── Stage 1: prefix arithmetic ── */ |
|
|
|
static void test_prefix_bytes(void) { |
|
TEST("prefix_bytes(1)==1"); |
|
if (merkle_sync_prefix_bytes(1) == 1) PASS(); else FAIL("got %d", merkle_sync_prefix_bytes(1)); |
|
|
|
TEST("prefix_bytes(2)==2"); |
|
if (merkle_sync_prefix_bytes(2) == 2) PASS(); else FAIL("got %d", merkle_sync_prefix_bytes(2)); |
|
|
|
TEST("prefix_bytes(3)==2"); |
|
if (merkle_sync_prefix_bytes(3) == 2) PASS(); else FAIL("got %d", merkle_sync_prefix_bytes(3)); |
|
|
|
TEST("prefix_bytes(4)==3"); |
|
if (merkle_sync_prefix_bytes(4) == 3) PASS(); else FAIL("got %d", merkle_sync_prefix_bytes(4)); |
|
|
|
TEST("prefix_bytes(5)==4"); |
|
if (merkle_sync_prefix_bytes(5) == 4) PASS(); else FAIL("got %d", merkle_sync_prefix_bytes(5)); |
|
} |
|
|
|
static void test_level_prefix_basic(void) { |
|
uint64_t key = 0xFEDCBA9876543210ULL; |
|
|
|
TEST("level_prefix(0xFEDCBA.., 1)==0xF800.."); |
|
uint64_t got = merkle_sync_level_prefix(key, 1); |
|
if (got == 0xF800000000000000ULL) PASS(); else FAIL("expected f800000000000000 got %016llx", (unsigned long long)got); |
|
|
|
TEST("level_prefix(0,3)==0"); |
|
got = merkle_sync_level_prefix(0, 3); |
|
if (got == 0) PASS(); else FAIL("got %016llx", (unsigned long long)got); |
|
|
|
TEST("level_prefix(MAX,1)==0xF800000000000000"); |
|
got = merkle_sync_level_prefix(UINT64_MAX, 1); |
|
if (got == 0xF800000000000000ULL) PASS(); else FAIL("got %016llx", (unsigned long long)got); |
|
|
|
TEST("level_prefix(MAX,5)==0xFFFFFF8000000000"); |
|
got = merkle_sync_level_prefix(UINT64_MAX, 5); |
|
if (got == 0xFFFFFF8000000000ULL) PASS(); else FAIL("got %016llx", (unsigned long long)got); |
|
|
|
TEST("level_prefix(0,0)==0"); |
|
got = merkle_sync_level_prefix(0, 0); |
|
if (got == 0) PASS(); else FAIL("got %016llx", (unsigned long long)got); |
|
} |
|
|
|
static void test_level_prefix_idempotent(void) { |
|
TEST("level_prefix idempotent L3"); |
|
uint64_t key = 0x0123456789ABCDEFULL; |
|
uint64_t p = merkle_sync_level_prefix(key, 3); |
|
uint64_t p2 = merkle_sync_level_prefix(p, 3); |
|
if (p == p2) PASS(); else FAIL("p=%016llx p2=%016llx", (unsigned long long)p, (unsigned long long)p2); |
|
} |
|
|
|
static void test_level_prefix_bucket_partition(void) { |
|
TEST("same bucket → same prefix L1..L2"); |
|
uint64_t k1 = 0x8900000000000000ULL; |
|
uint64_t k2 = 0x8911111111111111ULL; |
|
int ok = 1; |
|
for (uint8_t l = 1; l <= 2; l++) |
|
if (merkle_sync_level_prefix(k1, l) != merkle_sync_level_prefix(k2, l)) ok = 0; |
|
if (merkle_sync_level_prefix(k1, 3) == merkle_sync_level_prefix(k2, 3)) ok = 0; |
|
if (ok) PASS(); else FAIL("partition broken"); |
|
|
|
TEST("different L1 bucket → different prefix L1"); |
|
uint64_t a = 0x0000000000000000ULL; |
|
uint64_t b = 0x0800000000000000ULL; |
|
if (merkle_sync_level_prefix(a, 1) != merkle_sync_level_prefix(b, 1)) PASS(); else FAIL("should differ"); |
|
} |
|
|
|
/* ── Stage 2: tree building tests ── */ |
|
|
|
static void _ensure_table(sqlite3* db) { if (merkle_tree_init(db) < 0) abort(); } |
|
|
|
static int _db_count_rows(sqlite3* db, const char* ns) { |
|
sqlite3_stmt* st = NULL; |
|
sqlite3_prepare_v2(db, "SELECT COUNT(*) FROM merkle_tree_hash WHERE namespace=?", -1, &st, NULL); |
|
sqlite3_bind_text(st, 1, ns, -1, SQLITE_STATIC); |
|
int n = 0; |
|
if (sqlite3_step(st) == SQLITE_ROW) n = sqlite3_column_int(st, 0); |
|
sqlite3_finalize(st); |
|
return n; |
|
} |
|
|
|
static int _db_compare_trees(sqlite3* da, sqlite3* db, const char* ns) { |
|
sqlite3_stmt* sa = NULL, *sb = NULL; |
|
sqlite3_prepare_v2(da, "SELECT level,prefix64,hash FROM merkle_tree_hash WHERE namespace=? ORDER BY level,prefix64", |
|
-1, &sa, NULL); |
|
sqlite3_prepare_v2(db, "SELECT level,prefix64,hash FROM merkle_tree_hash WHERE namespace=? ORDER BY level,prefix64", |
|
-1, &sb, NULL); |
|
sqlite3_bind_text(sa, 1, ns, -1, SQLITE_STATIC); |
|
sqlite3_bind_text(sb, 1, ns, -1, SQLITE_STATIC); |
|
int mismatch = 0; |
|
while (1) { |
|
int ra = sqlite3_step(sa), rb = sqlite3_step(sb); |
|
if (ra == SQLITE_DONE && rb == SQLITE_DONE) break; |
|
if (ra == SQLITE_DONE) { mismatch++; break; } |
|
if (rb == SQLITE_DONE) { mismatch++; break; } |
|
if (sqlite3_column_int(sa,0) != sqlite3_column_int(sb,0) || |
|
(uint64_t)sqlite3_column_int64(sa,1) != (uint64_t)sqlite3_column_int64(sb,1)) { |
|
mismatch++; break; |
|
} |
|
if (memcmp(sqlite3_column_blob(sa,2), sqlite3_column_blob(sb,2), MT_HASH_SIZE) != 0) { mismatch++; break; } |
|
} |
|
sqlite3_finalize(sa); sqlite3_finalize(sb); |
|
return mismatch; |
|
} |
|
|
|
static void test_tree_empty_ns(void) { |
|
TEST("empty namespace → no rows in merkle_tree_hash"); |
|
sqlite3* db = NULL; sqlite3_open(":memory:", &db); |
|
_ensure_table(db); |
|
|
|
static struct UTUN_INSTANCE inst; memset(&inst, 0, sizeof(inst)); |
|
inst.topo_sqlite_db = db; |
|
struct ms_data data; _data_init(&data); |
|
struct ms_test_ctx ctx = { .data = &data, .inst = &inst }; |
|
inst.ua = uasync_create(); |
|
if (merkle_sync_init(&inst, 0x72, &g_test_ops, &ctx) != 0) abort(); |
|
|
|
merkle_sync_recompute_path(&inst, MS_NS_TEST, 0x1234ULL); |
|
if (_db_count_rows(db, MS_NS_TEST) == 0) PASS(); else FAIL("got %d rows", _db_count_rows(db, MS_NS_TEST)); |
|
|
|
merkle_sync_destroy(&inst); uasync_destroy(inst.ua, 0); sqlite3_close(db); |
|
} |
|
|
|
static void test_tree_single_item(void) { |
|
TEST("single item → 5 rows (L1..L5) with non-zero hash"); |
|
sqlite3* db = NULL; sqlite3_open(":memory:", &db); |
|
_ensure_table(db); |
|
|
|
static struct UTUN_INSTANCE inst; memset(&inst, 0, sizeof(inst)); |
|
inst.topo_sqlite_db = db; |
|
struct ms_data data; _data_init(&data); |
|
uint64_t key = 0xABCD000000000000ULL; |
|
_data_insert(&data, key, 42); |
|
_data_sort(&data); |
|
struct ms_test_ctx ctx = { .data = &data, .inst = &inst }; |
|
inst.ua = uasync_create(); |
|
if (merkle_sync_init(&inst, 0x72, &g_test_ops, &ctx) != 0) abort(); |
|
|
|
merkle_sync_recompute_path(&inst, MS_NS_TEST, key); |
|
int rows = _db_count_rows(db, MS_NS_TEST); |
|
if (rows != 6) { FAIL("expected 6 rows got %d", rows); merkle_sync_destroy(&inst); uasync_destroy(inst.ua, 0); sqlite3_close(db); return; } |
|
|
|
/* check hashes are non-zero */ |
|
int ok = 1; |
|
for (uint8_t lv = 1; lv <= 5 && ok; lv++) { |
|
uint64_t pf = merkle_sync_level_prefix(key, lv); |
|
const uint8_t* h = _test_hash(&inst, MS_NS_TEST, lv, pf); |
|
int zero = 1; |
|
for (int i = 0; i < MT_HASH_SIZE; i++) if (h[i] != 0) zero = 0; |
|
if (zero) { FAIL("L%d zero hash", lv); ok = 0; } |
|
} |
|
if (ok) PASS(); |
|
|
|
merkle_sync_destroy(&inst); uasync_destroy(inst.ua, 0); sqlite3_close(db); |
|
} |
|
|
|
static void test_tree_multi_item_same_bucket(void) { |
|
TEST("two items same L1 bucket → 5 rows, L1 hash aggregates both"); |
|
sqlite3* db = NULL; sqlite3_open(":memory:", &db); |
|
_ensure_table(db); |
|
|
|
static struct UTUN_INSTANCE inst; memset(&inst, 0, sizeof(inst)); |
|
inst.topo_sqlite_db = db; |
|
struct ms_data data; _data_init(&data); |
|
uint64_t k1 = 0xAA00000000000000ULL; |
|
uint64_t k2 = 0xAB00000000000000ULL; /* bits 63-59: 10101 for both */ |
|
_data_insert(&data, k1, 1); _data_insert(&data, k2, 2); |
|
_data_sort(&data); |
|
struct ms_test_ctx ctx = { .data = &data, .inst = &inst }; |
|
inst.ua = uasync_create(); |
|
if (merkle_sync_init(&inst, 0x72, &g_test_ops, &ctx) != 0) abort(); |
|
|
|
/* recompute for both */ |
|
merkle_sync_recompute_path(&inst, MS_NS_TEST, k1); |
|
merkle_sync_recompute_path(&inst, MS_NS_TEST, k2); |
|
|
|
uint64_t pf1 = merkle_sync_level_prefix(k1, 1); |
|
uint64_t pf2 = merkle_sync_level_prefix(k2, 1); |
|
if (pf1 != pf2) { FAIL("L1 prefixes should be equal"); merkle_sync_destroy(&inst); uasync_destroy(inst.ua, 0); sqlite3_close(db); return; } |
|
|
|
/* verify: both items contribute to same bucket hash */ |
|
uint8_t expected_hash[MT_HASH_SIZE]; |
|
_expected_hash(&ctx, MS_NS_TEST, 1, pf1, expected_hash); |
|
const uint8_t* stored = _test_hash(&inst, MS_NS_TEST, 1, pf1); |
|
if (memcmp(expected_hash, stored, MT_HASH_SIZE) == 0) PASS(); else FAIL("hash mismatch"); |
|
|
|
merkle_sync_destroy(&inst); uasync_destroy(inst.ua, 0); sqlite3_close(db); |
|
} |
|
|
|
static void test_tree_different_buckets(void) { |
|
TEST("items in different L1 buckets → independent hashes"); |
|
sqlite3* db = NULL; sqlite3_open(":memory:", &db); |
|
_ensure_table(db); |
|
|
|
static struct UTUN_INSTANCE inst; memset(&inst, 0, sizeof(inst)); |
|
inst.topo_sqlite_db = db; |
|
struct ms_data data; _data_init(&data); |
|
uint64_t kA = 0x0000000000000000ULL; /* bucket 0 */ |
|
uint64_t kB = 0x0800000000000000ULL; /* bucket 1 */ |
|
_data_insert(&data, kA, 100); _data_insert(&data, kB, 200); |
|
_data_sort(&data); |
|
struct ms_test_ctx ctx = { .data = &data, .inst = &inst }; |
|
inst.ua = uasync_create(); |
|
if (merkle_sync_init(&inst, 0x72, &g_test_ops, &ctx) != 0) abort(); |
|
|
|
merkle_sync_recompute_path(&inst, "ns", kA); |
|
merkle_sync_recompute_path(&inst, "ns", kB); |
|
|
|
uint64_t pfA = merkle_sync_level_prefix(kA, 1); |
|
uint64_t pfB = merkle_sync_level_prefix(kB, 1); |
|
if (pfA == pfB) { FAIL("same L1 prefix"); merkle_sync_destroy(&inst); uasync_destroy(inst.ua, 0); sqlite3_close(db); return; } |
|
|
|
const uint8_t* hA = _test_hash(&inst, "ns", 1, pfA); |
|
uint8_t copyA[MT_HASH_SIZE]; memcpy(copyA, hA, MT_HASH_SIZE); |
|
const uint8_t* hB = _test_hash(&inst, "ns", 1, pfB); |
|
uint8_t copyB[MT_HASH_SIZE]; memcpy(copyB, hB, MT_HASH_SIZE); |
|
if (memcmp(copyA, copyB, MT_HASH_SIZE) != 0) PASS(); else FAIL("hashes should differ"); |
|
|
|
merkle_sync_destroy(&inst); uasync_destroy(inst.ua, 0); sqlite3_close(db); |
|
} |
|
|
|
static void test_tree_delete_empty_bucket(void) { |
|
TEST("delete last item → bucket deleted from DB, get_hash returns zero"); |
|
sqlite3* db = NULL; sqlite3_open(":memory:", &db); |
|
_ensure_table(db); |
|
|
|
static struct UTUN_INSTANCE inst; memset(&inst, 0, sizeof(inst)); |
|
inst.topo_sqlite_db = db; |
|
struct ms_data data; _data_init(&data); |
|
uint64_t k = 0xCCCC000000000000ULL; |
|
_data_insert(&data, k, 7); _data_sort(&data); |
|
struct ms_test_ctx ctx = { .data = &data, .inst = &inst }; |
|
inst.ua = uasync_create(); |
|
if (merkle_sync_init(&inst, 0x72, &g_test_ops, &ctx) != 0) abort(); |
|
|
|
merkle_sync_recompute_path(&inst, MS_NS_TEST, k); |
|
if (_db_count_rows(db, MS_NS_TEST) != 6) { FAIL("expected 6 rows"); merkle_sync_destroy(&inst); uasync_destroy(inst.ua, 0); sqlite3_close(db); return; } |
|
|
|
/* remove item and recompute */ |
|
data.count = 0; |
|
merkle_sync_recompute_path(&inst, MS_NS_TEST, k); |
|
if (_db_count_rows(db, MS_NS_TEST) != 0) { FAIL("expected 0 rows got %d", _db_count_rows(db, MS_NS_TEST)); merkle_sync_destroy(&inst); uasync_destroy(inst.ua, 0); sqlite3_close(db); return; } |
|
|
|
uint64_t pf = merkle_sync_level_prefix(k, 3); |
|
const uint8_t* h = _test_hash(&inst, MS_NS_TEST, 3, pf); |
|
int zero = 1; |
|
for (int i = 0; i < MT_HASH_SIZE; i++) if (h[i] != 0) zero = 0; |
|
if (zero) PASS(); else FAIL("hash not zero after delete"); |
|
|
|
merkle_sync_destroy(&inst); uasync_destroy(inst.ua, 0); sqlite3_close(db); |
|
} |
|
|
|
static void test_tree_consistency(void) { |
|
TEST("recompute vs stored hash consistency — random items"); |
|
sqlite3* db = NULL; sqlite3_open(":memory:", &db); |
|
_ensure_table(db); |
|
|
|
static struct UTUN_INSTANCE inst; memset(&inst, 0, sizeof(inst)); |
|
inst.topo_sqlite_db = db; |
|
struct ms_data data; _data_init(&data); |
|
|
|
/* random items across the keyspace */ |
|
srand(12345); |
|
for (int i = 0; i < 50; i++) { |
|
uint64_t k = ((uint64_t)rand() << 32) | (uint64_t)rand(); |
|
_data_insert(&data, k, (uint32_t)rand()); |
|
} |
|
_data_sort(&data); |
|
|
|
struct ms_test_ctx ctx = { .data = &data, .inst = &inst }; |
|
inst.ua = uasync_create(); |
|
if (merkle_sync_init(&inst, 0x72, &g_test_ops, &ctx) != 0) abort(); |
|
|
|
for (int i = 0; i < data.count; i++) |
|
merkle_sync_recompute_path(&inst, "ns", data.items[i].key); |
|
|
|
/* verify: recompute each bucket, compare with stored hash */ |
|
sqlite3_stmt* st = NULL; |
|
sqlite3_prepare_v2(db, |
|
"SELECT level, prefix64 FROM merkle_tree_hash WHERE namespace=? ORDER BY level, prefix64", |
|
-1, &st, NULL); |
|
sqlite3_bind_text(st, 1, "ns", -1, SQLITE_STATIC); |
|
int ok = 1; |
|
while (sqlite3_step(st) == SQLITE_ROW && ok) { |
|
uint8_t lv = (uint8_t)sqlite3_column_int(st, 0); |
|
uint64_t pf = (uint64_t)sqlite3_column_int64(st, 1); |
|
|
|
/* compute expected hash from data */ |
|
uint8_t expected[MT_HASH_SIZE]; |
|
_expected_hash(&ctx, "ns", lv, pf, expected); |
|
|
|
const uint8_t* stored = _test_hash(&inst, "ns", lv, pf); |
|
if (memcmp(expected, stored, MT_HASH_SIZE) != 0) { |
|
FAIL("mismatch L%d P%016llx", lv, (unsigned long long)pf); |
|
ok = 0; |
|
} |
|
} |
|
sqlite3_finalize(st); |
|
if (ok) PASS(); |
|
|
|
merkle_sync_destroy(&inst); uasync_destroy(inst.ua, 0); sqlite3_close(db); |
|
} |
|
|
|
static int run_stage1(void) { |
|
printf("--- Stage 1: prefix arithmetic ---\n"); |
|
test_prefix_bytes(); |
|
test_level_prefix_basic(); |
|
test_level_prefix_idempotent(); |
|
test_level_prefix_bucket_partition(); |
|
return tests_failed; |
|
} |
|
|
|
static int run_stage2(void) { |
|
printf("--- Stage 2: tree building with in-memory SQLite ---\n"); |
|
stage_failures = 0; |
|
test_tree_empty_ns(); |
|
test_tree_single_item(); |
|
test_tree_multi_item_same_bucket(); |
|
test_tree_different_buckets(); |
|
test_tree_delete_empty_bucket(); |
|
test_tree_consistency(); |
|
return stage_failures; |
|
} |
|
|
|
/* ── Stage 3-6: integration with UTUN_INSTANCE + ETCP ── */ |
|
|
|
#define INTG_TIMEOUT_TB 120000 |
|
#define PHASE_TIMEOUT_TB 100000 |
|
#define PHASE3_TIMEOUT_TB 200000 |
|
#define POLL_MS 1 |
|
#define MS_SVC_ID 0x72 |
|
|
|
static char intg_temp_dir[] = "/tmp/utun_msync_XXXXXX"; |
|
static struct UTUN_INSTANCE* i_a = NULL; |
|
static struct UTUN_INSTANCE* i_b = NULL; |
|
static struct UTUN_INSTANCE* i_c = NULL; |
|
static struct UASYNC* i_ua = NULL; |
|
static int i_phase = 0; |
|
static void* i_timeout_id = NULL; |
|
|
|
static struct ms_data data_a, data_b, data_c; |
|
static struct ms_test_ctx ctx_a, ctx_b, ctx_c; |
|
|
|
static int done_sync = 0; |
|
static int _cond_done(void) { return done_sync; } |
|
|
|
static void _on_sync_done(uint64_t peer, const char* ns, int result, void* arg) { |
|
(void)arg; done_sync = 1; |
|
printf(" sync_done: peer=%016llx ns=%s result=%d\n", (unsigned long long)peer, ns, result); |
|
if (result != MT_OK) { tests_failed++; stage_failures++; } |
|
} |
|
|
|
static int _write_cfg(const char* path, const char* fmt, ...) { |
|
va_list ap; FILE* f = fopen(path, "w"); |
|
if (!f) return -1; |
|
va_start(ap, fmt); vfprintf(f, fmt, ap); va_end(ap); |
|
fclose(f); return 0; |
|
} |
|
|
|
static char* _get_pubkey(const char* path) { |
|
struct utun_config* cfg = parse_config(path); |
|
if (!cfg) return NULL; |
|
char* pub = u_strdup(cfg->global.my_public_key_hex); |
|
free_config(cfg); return pub; |
|
} |
|
|
|
static void _intg_timeout(void* arg) { (void)arg; printf(" GLOBAL TIMEOUT\n"); i_phase = 2; } |
|
|
|
static int _cond_links_up(void) { |
|
if (!i_a || !i_b) return 0; |
|
int links = 0; |
|
struct ll_entry* e = i_a->connections->head; |
|
while (e) { |
|
struct conn_queue_entry* ce = (struct conn_queue_entry*)e->data; |
|
struct ETCP_LINK* l = ce->conn->links; |
|
while (l) { if (l->initialized) links++; l = l->next; } |
|
e = e->next; |
|
} |
|
return links >= (i_c ? 2 : 1); |
|
} |
|
|
|
static int _wait_for(const char* desc, int (*cond)(void), int timeout_tb) { |
|
uint64_t start = get_time_tb(); |
|
while (!cond() && (get_time_tb() - start) < (uint64_t)timeout_tb) |
|
uasync_poll(i_ua, POLL_MS); |
|
if (cond()) return 1; |
|
printf(" TIMEOUT: %s\n", desc); i_phase = 2; |
|
return 0; |
|
} |
|
|
|
static void _intg_setup_contexts(void) { |
|
memset(&data_a, 0, sizeof(data_a)); memset(&data_b, 0, sizeof(data_b)); memset(&data_c, 0, sizeof(data_c)); |
|
ctx_a.data = &data_a; ctx_a.inst = NULL; ctx_a.tag = 'A'; |
|
ctx_b.data = &data_b; ctx_b.inst = NULL; ctx_b.tag = 'B'; |
|
ctx_c.data = &data_c; ctx_c.inst = NULL; ctx_c.tag = 'C'; |
|
done_sync = 0; |
|
} |
|
|
|
static int _intg_create_cfgs(int* port_a, int* port_b, int* port_c, |
|
char* cfg_a, char* cfg_b, char* cfg_c, size_t sz) { |
|
/* reset template (mkdtemp modifies it) */ |
|
strcpy(intg_temp_dir, "/tmp/utun_msync_XXXXXX"); |
|
if (test_mkdtemp(intg_temp_dir) != 0) { printf(" mkdtemp failed\n"); return -1; } |
|
int base = 43000 + (getpid() % 15000); |
|
*port_a = base; *port_b = base + 1; *port_c = base + 2; |
|
snprintf(cfg_a, sz, "%s/a.conf", intg_temp_dir); |
|
snprintf(cfg_b, sz, "%s/b.conf", intg_temp_dir); |
|
snprintf(cfg_c, sz, "%s/c.conf", intg_temp_dir); |
|
char dbp[256]; |
|
snprintf(dbp, sizeof(dbp), "%s/db_a", intg_temp_dir); utun_mkdir(dbp, 0755); |
|
snprintf(dbp, sizeof(dbp), "%s/db_b", intg_temp_dir); utun_mkdir(dbp, 0755); |
|
snprintf(dbp, sizeof(dbp), "%s/db_c", intg_temp_dir); utun_mkdir(dbp, 0755); |
|
|
|
_write_cfg(cfg_a, |
|
"[global]\nmy_node_id=0xAAAAAAAAAAAAAAAA\ntun_ip=10.220.0.1/24\n" |
|
"tun_ifname=tun220\nkeepalive_adaptive=0\ndb_path=%s/db_a\n\n" |
|
"[server: srv_a]\naddr=127.0.0.1:%d\ntype=public\n\n" |
|
"[allowed_keys]\nallow_all=1\n", intg_temp_dir, *port_a); |
|
config_ensure_keys_and_node_id(cfg_a); |
|
char* pub_a = _get_pubkey(cfg_a); if (!pub_a) return -1; |
|
|
|
_write_cfg(cfg_b, |
|
"[global]\nmy_node_id=0xBBBBBBBBBBBBBBBB\ntun_ip=10.220.0.2/24\n" |
|
"tun_ifname=tun221\nkeepalive_adaptive=0\ndb_path=%s/db_b\n\n" |
|
"[server: srv_b]\naddr=127.0.0.1:%d\ntype=public\n\n" |
|
"[client: to_a]\nkeepalive=1\npeer_public_key=%s\nlink=srv_b:127.0.0.1:%d\n\n" |
|
"[allowed_keys]\nallow_all=1\n", intg_temp_dir, *port_b, pub_a, *port_a); |
|
config_ensure_keys_and_node_id(cfg_b); |
|
|
|
_write_cfg(cfg_c, |
|
"[global]\nmy_node_id=0xCCCCCCCCCCCCCCCC\ntun_ip=10.220.0.3/24\n" |
|
"tun_ifname=tun222\nkeepalive_adaptive=0\ndb_path=%s/db_c\n\n" |
|
"[server: srv_c]\naddr=127.0.0.1:%d\ntype=public\n\n" |
|
"[client: to_a]\nkeepalive=1\npeer_public_key=%s\nlink=srv_c:127.0.0.1:%d\n\n" |
|
"[allowed_keys]\nallow_all=1\n", intg_temp_dir, *port_c, pub_a, *port_a); |
|
config_ensure_keys_and_node_id(cfg_c); |
|
|
|
u_free(pub_a); |
|
return 0; |
|
} |
|
|
|
static void _intg_cleanup(void) { |
|
if (i_timeout_id) { uasync_cancel_timeout(i_ua, i_timeout_id); i_timeout_id = NULL; } |
|
if (i_c) { merkle_sync_destroy(i_c); i_c->running = 0; utun_instance_destroy(i_c); i_c = NULL; } |
|
if (i_b) { merkle_sync_destroy(i_b); i_b->running = 0; utun_instance_destroy(i_b); i_b = NULL; } |
|
if (i_a) { merkle_sync_destroy(i_a); i_a->running = 0; utun_instance_destroy(i_a); i_a = NULL; } |
|
if (i_ua) { uasync_destroy(i_ua, 0); i_ua = NULL; } |
|
char pa[320]; |
|
snprintf(pa, sizeof(pa), "%s/a.conf", intg_temp_dir); unlink(pa); |
|
snprintf(pa, sizeof(pa), "%s/b.conf", intg_temp_dir); unlink(pa); |
|
snprintf(pa, sizeof(pa), "%s/c.conf", intg_temp_dir); unlink(pa); |
|
for (const char* d = "abc"; *d; d++) { |
|
snprintf(pa, sizeof(pa), "%s/db_%c/chats.db", intg_temp_dir, *d); unlink(pa); |
|
snprintf(pa, sizeof(pa), "%s/db_%c/chats.db-wal", intg_temp_dir, *d); unlink(pa); |
|
snprintf(pa, sizeof(pa), "%s/db_%c/chats.db-shm", intg_temp_dir, *d); unlink(pa); |
|
snprintf(pa, sizeof(pa), "%s/db_%c", intg_temp_dir, *d); test_rmdir(pa); |
|
} |
|
test_rmdir(intg_temp_dir); |
|
} |
|
|
|
static int _intg_init_two(void) { |
|
i_phase = 0; i_timeout_id = NULL; i_a = i_b = i_c = NULL; |
|
int port_a, port_b, port_c; |
|
char cfg_a[256], cfg_b[256], cfg_c[256]; |
|
if (_intg_create_cfgs(&port_a, &port_b, &port_c, cfg_a, cfg_b, cfg_c, sizeof(cfg_a)) != 0) return -1; |
|
utun_instance_set_tun_init_enabled(0); |
|
i_ua = uasync_create(); if (!i_ua) { _intg_cleanup(); return -1; } |
|
i_a = utun_instance_create(i_ua, cfg_a); |
|
i_b = utun_instance_create(i_ua, cfg_b); |
|
if (!i_a || !i_b || utun_instance_init(i_a) != 0 || utun_instance_init(i_b) != 0) |
|
{ _intg_cleanup(); return -1; } |
|
i_timeout_id = uasync_set_timeout(i_ua, INTG_TIMEOUT_TB, NULL, _intg_timeout, "ms_intg_timeout"); |
|
if (!_wait_for("links up", _cond_links_up, PHASE_TIMEOUT_TB)) { _intg_cleanup(); return -1; } |
|
|
|
_intg_setup_contexts(); |
|
ctx_a.inst = i_a; ctx_b.inst = i_b; |
|
_ms_ensure_groups(i_a); _ms_ensure_groups(i_b); |
|
if (merkle_sync_init(i_a, MS_SVC_ID, &g_test_ops, &ctx_a) != 0 || |
|
merkle_sync_init(i_b, MS_SVC_ID, &g_test_ops, &ctx_b) != 0) |
|
{ _intg_cleanup(); return -1; } |
|
printf(" 2 instances ready A=%016llx B=%016llx\n", |
|
(unsigned long long)i_a->node_id, (unsigned long long)i_b->node_id); |
|
return 0; |
|
} |
|
|
|
static void _intg_ins_many(struct ms_data* d, struct UTUN_INSTANCE* inst, int base, int count) { |
|
for (int i = 0; i < count; i++) { |
|
uint64_t key = (uint64_t)(base + i) * 0x100000000000000ULL; |
|
_data_insert(d, key, (uint32_t)(base + i)); |
|
} |
|
_data_sort(d); |
|
for (int i = 0; i < d->count; i++) |
|
merkle_sync_recompute_path(inst, MS_NS_TEST, d->items[i].key); |
|
} |
|
|
|
static int _intg_compare_data(struct ms_data* a, struct ms_data* b) { |
|
if (a->count != b->count) { printf(" count mismatch: %d vs %d\n", a->count, b->count); return -1; } |
|
for (int i = 0; i < a->count; i++) { |
|
if (a->items[i].key != b->items[i].key || a->items[i].val != b->items[i].val) { |
|
printf(" data[%d] mismatch\n", i); return -1; |
|
} |
|
} |
|
return 0; |
|
} |
|
|
|
static int _intg_verify_all(struct UTUN_INSTANCE* a, struct UTUN_INSTANCE* b, |
|
struct ms_data* da, struct ms_data* db, const char* ns) { |
|
if (_db_compare_trees(a->topo_sqlite_db, b->topo_sqlite_db, ns) != 0) { printf(" TREE MISMATCH\n"); return -1; } |
|
if (_intg_compare_data(da, db) != 0) { printf(" DATA MISMATCH\n"); return -1; } |
|
return 0; |
|
} |
|
|
|
/* ── Stage 3: integration tests ── */ |
|
|
|
static void test_peer_empty(void) { |
|
TEST("peer empty — B has 5 items, A empty, sync A->B"); |
|
if (_intg_init_two() != 0) { FAIL("setup failed"); return; } |
|
_intg_ins_many(&data_b, i_b, 0, 5); |
|
done_sync = 0; |
|
merkle_sync_start(i_a, i_b->node_id, MS_NS_TEST, _on_sync_done, NULL); |
|
if (!_wait_for("sync done", _cond_done, PHASE_TIMEOUT_TB)) |
|
{ FAIL("sync timeout"); _intg_cleanup(); return; } |
|
printf(" A data=%d B data=%d, A tree=%d B tree=%d\n", |
|
data_a.count, data_b.count, |
|
_db_count_rows(i_a->topo_sqlite_db, MS_NS_TEST), |
|
_db_count_rows(i_b->topo_sqlite_db, MS_NS_TEST)); |
|
if (_intg_verify_all(i_a, i_b, &data_a, &data_b, MS_NS_TEST) != 0) |
|
{ FAIL("verify failed"); _intg_cleanup(); return; } |
|
PASS(); |
|
_intg_cleanup(); |
|
} |
|
|
|
static void test_hash_match(void) { |
|
TEST("hash match — identical data, sync immediate"); |
|
if (_intg_init_two() != 0) { FAIL("setup failed"); return; } |
|
_intg_ins_many(&data_a, i_a, 0, 5); |
|
_intg_ins_many(&data_b, i_b, 0, 5); |
|
done_sync = 0; |
|
merkle_sync_start(i_a, i_b->node_id, MS_NS_TEST, _on_sync_done, NULL); |
|
for (int i = 0; i < 200 && !done_sync && i_phase == 0; i++) uasync_poll(i_ua, 1); |
|
if (!done_sync) { FAIL("sync did not complete quickly"); _intg_cleanup(); return; } |
|
if (_intg_verify_all(i_a, i_b, &data_a, &data_b, MS_NS_TEST) != 0) |
|
{ FAIL("verify failed"); _intg_cleanup(); return; } |
|
PASS(); |
|
_intg_cleanup(); |
|
} |
|
|
|
static void test_divergence_merge(void) { |
|
TEST("divergence merge — A has 3 items, B has 2 different"); |
|
if (_intg_init_two() != 0) { FAIL("setup failed"); return; } |
|
_intg_ins_many(&data_a, i_a, 0, 3); |
|
_intg_ins_many(&data_b, i_b, 10, 2); |
|
done_sync = 0; |
|
merkle_sync_start(i_a, i_b->node_id, MS_NS_TEST, _on_sync_done, NULL); |
|
if (!_wait_for("sync done", _cond_done, PHASE_TIMEOUT_TB)) |
|
{ FAIL("sync timeout"); _intg_cleanup(); return; } |
|
if (_intg_verify_all(i_a, i_b, &data_a, &data_b, MS_NS_TEST) != 0) |
|
{ FAIL("verify failed"); _intg_cleanup(); return; } |
|
PASS(); |
|
_intg_cleanup(); |
|
} |
|
|
|
static int run_stage3(void) { |
|
printf("--- Stage 3: integration 2 instances, basic scenarios ---\n"); |
|
stage_failures = 0; |
|
test_peer_empty(); |
|
test_hash_match(); |
|
test_divergence_merge(); |
|
return stage_failures; |
|
} |
|
|
|
/* ── Stage 4: randomized 2-instance sync ── */ |
|
|
|
static void test_randomized_two(void) { |
|
TEST("randomized 2-instance — 25 iterations"); |
|
srand(42); |
|
int iter; |
|
for (iter = 0; iter < 25 && stage_failures == 0; iter++) { |
|
if (_intg_init_two() != 0) { FAIL("setup iter %d", iter); break; } |
|
int na = rand() % 101, nb = rand() % 101; |
|
for (int i = 0; i < na; i++) { |
|
uint64_t k = ((uint64_t)rand() << 32) | (uint64_t)rand(); |
|
_data_insert(&data_a, k, (uint32_t)rand()); |
|
} |
|
for (int i = 0; i < nb; i++) { |
|
uint64_t k = ((uint64_t)rand() << 32) | (uint64_t)rand(); |
|
_data_insert(&data_b, k, (uint32_t)rand()); |
|
} |
|
_data_sort(&data_a); _data_sort(&data_b); |
|
for (int i = 0; i < data_a.count; i++) merkle_sync_recompute_path(i_a, MS_NS_RND, data_a.items[i].key); |
|
for (int i = 0; i < data_b.count; i++) merkle_sync_recompute_path(i_b, MS_NS_RND, data_b.items[i].key); |
|
|
|
done_sync = 0; |
|
merkle_sync_start(i_a, i_b->node_id, MS_NS_RND, _on_sync_done, NULL); |
|
if (!_wait_for("sync", _cond_done, PHASE_TIMEOUT_TB)) { FAIL("timeout iter %d", iter); _intg_cleanup(); break; } |
|
if (_intg_verify_all(i_a, i_b, &data_a, &data_b, MS_NS_RND) != 0) { |
|
printf(" A data=%d B data=%d A tree=%d B tree=%d\n", |
|
data_a.count, data_b.count, |
|
_db_count_rows(i_a->topo_sqlite_db, MS_NS_RND), |
|
_db_count_rows(i_b->topo_sqlite_db, MS_NS_RND)); |
|
/* dump items only in A */ |
|
for (int ai = 0; ai < data_a.count; ai++) { |
|
int found = 0; |
|
for (int bi = 0; bi < data_b.count; bi++) |
|
if (data_a.items[ai].key == data_b.items[bi].key) { found = 1; break; } |
|
if (!found) printf(" only A: key=%016llx val=%u\n", (unsigned long long)data_a.items[ai].key, data_a.items[ai].val); |
|
} |
|
for (int bi = 0; bi < data_b.count; bi++) { |
|
int found = 0; |
|
for (int ai = 0; ai < data_a.count; ai++) |
|
if (data_b.items[bi].key == data_a.items[ai].key) { found = 1; break; } |
|
if (!found) printf(" only B: key=%016llx val=%u\n", (unsigned long long)data_b.items[bi].key, data_b.items[bi].val); |
|
} |
|
FAIL("verify iter %d", iter); _intg_cleanup(); break; |
|
} |
|
|
|
/* consistency check: recompute each bucket hash, verify stored matches */ |
|
int ok = 1; |
|
sqlite3_stmt* st = NULL; |
|
sqlite3_prepare_v2(i_a->topo_sqlite_db, |
|
"SELECT level,prefix64 FROM merkle_tree_hash WHERE namespace=? ORDER BY level,prefix64", |
|
-1, &st, NULL); |
|
sqlite3_bind_text(st, 1, MS_NS_RND, -1, SQLITE_STATIC); |
|
while (sqlite3_step(st) == SQLITE_ROW && ok) { |
|
uint8_t lv = (uint8_t)sqlite3_column_int(st, 0); |
|
uint64_t pf = (uint64_t)sqlite3_column_int64(st, 1); |
|
uint8_t expected[MT_HASH_SIZE]; |
|
_expected_hash(&ctx_a, MS_NS_RND, lv, pf, expected); |
|
const uint8_t* stored = _test_hash(i_a, MS_NS_RND, lv, pf); |
|
uint8_t scopy[MT_HASH_SIZE]; memcpy(scopy, stored, MT_HASH_SIZE); |
|
if (memcmp(expected, scopy, MT_HASH_SIZE) != 0) { ok = 0; } |
|
} |
|
sqlite3_finalize(st); |
|
if (!ok) { FAIL("tree consistency iter %d", iter); _intg_cleanup(); break; } |
|
_intg_cleanup(); |
|
} |
|
if (iter == 25) PASS(); |
|
} |
|
|
|
/* ── Stage 5: randomized 3-instance star-topology sync ── */ |
|
|
|
static int _intg_init_three(void) { |
|
i_phase = 0; i_timeout_id = NULL; i_a = i_b = i_c = NULL; |
|
int port_a, port_b, port_c; |
|
char cfg_a[256], cfg_b[256], cfg_c[256]; |
|
if (_intg_create_cfgs(&port_a, &port_b, &port_c, cfg_a, cfg_b, cfg_c, sizeof(cfg_a)) != 0) return -1; |
|
utun_instance_set_tun_init_enabled(0); |
|
i_ua = uasync_create(); if (!i_ua) { _intg_cleanup(); return -1; } |
|
i_a = utun_instance_create(i_ua, cfg_a); |
|
i_b = utun_instance_create(i_ua, cfg_b); |
|
i_c = utun_instance_create(i_ua, cfg_c); |
|
if (!i_a || !i_b || !i_c || utun_instance_init(i_a) != 0 || utun_instance_init(i_b) != 0 || utun_instance_init(i_c) != 0) |
|
{ _intg_cleanup(); return -1; } |
|
i_timeout_id = uasync_set_timeout(i_ua, INTG_TIMEOUT_TB*5, NULL, _intg_timeout, "ms_intg_timeout"); |
|
if (!_wait_for("links up", _cond_links_up, PHASE3_TIMEOUT_TB)) { _intg_cleanup(); return -1; } |
|
/* let connections settle */ |
|
for (int i = 0; i < 20; i++) uasync_poll(i_ua, 5); |
|
_intg_setup_contexts(); |
|
ctx_a.inst = i_a; ctx_b.inst = i_b; ctx_c.inst = i_c; |
|
_ms_ensure_groups(i_a); _ms_ensure_groups(i_b); _ms_ensure_groups(i_c); |
|
if (merkle_sync_init(i_a, MS_SVC_ID, &g_test_ops, &ctx_a) != 0 || |
|
merkle_sync_init(i_b, MS_SVC_ID, &g_test_ops, &ctx_b) != 0 || |
|
merkle_sync_init(i_c, MS_SVC_ID, &g_test_ops, &ctx_c) != 0) |
|
{ _intg_cleanup(); return -1; } |
|
printf(" 3 instances ready A=%016llx B=%016llx C=%016llx\n", |
|
(unsigned long long)i_a->node_id, (unsigned long long)i_b->node_id, (unsigned long long)i_c->node_id); |
|
return 0; |
|
} |
|
|
|
static int _cond_all_synced(void) { |
|
if (data_a.count != data_b.count || data_b.count != data_c.count) return 0; |
|
if (_db_compare_trees(i_a->topo_sqlite_db, i_b->topo_sqlite_db, MS_NS_RND) != 0) return 0; |
|
if (_db_compare_trees(i_b->topo_sqlite_db, i_c->topo_sqlite_db, MS_NS_RND) != 0) return 0; |
|
return 1; |
|
} |
|
|
|
static void test_randomized_three(void) { |
|
TEST("randomized 3-instance star — 5 iterations"); |
|
srand(1234); |
|
int iter; |
|
for (iter = 0; iter < 5 && stage_failures == 0; iter++) { |
|
if (_intg_init_three() != 0) { FAIL("setup iter %d", iter); break; } |
|
int na = rand() % 51, nb = rand() % 51, nc = rand() % 51; |
|
for (int i = 0; i < na; i++) { uint64_t k = ((uint64_t)rand()<<32)|(uint64_t)rand(); _data_insert(&data_a, k, (uint32_t)rand()); } |
|
for (int i = 0; i < nb; i++) { uint64_t k = ((uint64_t)rand()<<32)|(uint64_t)rand(); _data_insert(&data_b, k, (uint32_t)rand()); } |
|
for (int i = 0; i < nc; i++) { uint64_t k = ((uint64_t)rand()<<32)|(uint64_t)rand(); _data_insert(&data_c, k, (uint32_t)rand()); } |
|
_data_sort(&data_a); for (int i = 0; i < data_a.count; i++) merkle_sync_recompute_path(i_a, MS_NS_RND, data_a.items[i].key); |
|
_data_sort(&data_b); for (int i = 0; i < data_b.count; i++) merkle_sync_recompute_path(i_b, MS_NS_RND, data_b.items[i].key); |
|
_data_sort(&data_c); for (int i = 0; i < data_c.count; i++) merkle_sync_recompute_path(i_c, MS_NS_RND, data_c.items[i].key); |
|
|
|
done_sync = 0; i_phase = 0; |
|
/* B→A: B syncs with A, gets A's data, B: SYNCED */ |
|
merkle_sync_start(i_b, i_a->node_id, MS_NS_RND, _on_sync_done, NULL); |
|
if (!_wait_for("sync B", _cond_done, PHASE_TIMEOUT_TB)) { FAIL("timeout B iter %d", iter); _intg_cleanup(); break; } |
|
done_sync = 0; i_phase = 0; |
|
/* C→A: C syncs with A, gets A∪B, A broadcasts C's new items to B (relay) */ |
|
merkle_sync_start(i_c, i_a->node_id, MS_NS_RND, _on_sync_done, NULL); |
|
if (!_wait_for("sync C", _cond_done, PHASE_TIMEOUT_TB)) { FAIL("timeout C iter %d", iter); _intg_cleanup(); break; } |
|
/* Автоматический следующий раунд A↔B завершается по условию, не по числу poll(). */ |
|
if (!_wait_for("automatic convergence of all peers", _cond_all_synced, PHASE3_TIMEOUT_TB)) { |
|
FAIL("automatic sync timeout iter %d", iter); _intg_cleanup(); break; |
|
} |
|
if (_intg_compare_data(&data_a, &data_b) != 0 || _intg_compare_data(&data_b, &data_c) != 0) |
|
{ FAIL("data mismatch iter %d", iter); _intg_cleanup(); break; } |
|
if (_db_compare_trees(i_a->topo_sqlite_db, i_b->topo_sqlite_db, MS_NS_RND) != 0 || |
|
_db_compare_trees(i_b->topo_sqlite_db, i_c->topo_sqlite_db, MS_NS_RND) != 0) |
|
{ FAIL("tree mismatch iter %d", iter); _intg_cleanup(); break; } |
|
_intg_cleanup(); |
|
} |
|
if (iter == 5) PASS(); |
|
} |
|
|
|
/* ── Stage 6: protocol edge cases ── */ |
|
|
|
static void test_cancel(void) { |
|
TEST("cancel — start sync, immediately cancel, done_cb NOT called"); |
|
if (_intg_init_two() != 0) { FAIL("setup failed"); return; } |
|
_intg_ins_many(&data_b, i_b, 0, 5); |
|
done_sync = 0; |
|
merkle_sync_start(i_a, i_b->node_id, MS_NS_TEST, _on_sync_done, NULL); |
|
merkle_sync_cancel(i_a, i_b->node_id, MS_NS_TEST); |
|
for (int i = 0; i < 100 && i_phase == 0; i++) uasync_poll(i_ua, 10); |
|
if (!done_sync) PASS(); else FAIL("done_cb was called"); |
|
_intg_cleanup(); |
|
} |
|
|
|
static int sw_cb1_called = 0, sw_cb2_called = 0; |
|
static void _sw_done1(uint64_t p, const char* n, int r, void* a) { (void)p;(void)n;(void)r;(void)a; sw_cb1_called = 1; } |
|
static void _sw_done2(uint64_t p, const char* n, int r, void* a) { (void)p;(void)n;(void)r;(void)a; sw_cb2_called = 1; } |
|
|
|
static void test_start_overwrite(void) { |
|
TEST("concurrent starts — both callbacks complete"); |
|
if (_intg_init_two() != 0) { FAIL("setup failed"); return; } |
|
_intg_ins_many(&data_b, i_b, 0, 3); |
|
sw_cb1_called = sw_cb2_called = 0; |
|
merkle_sync_start(i_a, i_b->node_id, MS_NS_TEST, _sw_done1, NULL); |
|
merkle_sync_start(i_a, i_b->node_id, MS_NS_TEST, _sw_done2, NULL); |
|
for (int i = 0; i < 500 && !sw_cb2_called && i_phase == 0; i++) uasync_poll(i_ua, 10); |
|
if (sw_cb1_called && sw_cb2_called) PASS(); else FAIL("cb1=%d cb2=%d", sw_cb1_called, sw_cb2_called); |
|
_intg_cleanup(); |
|
} |
|
|
|
static int run_stage4(void) { |
|
printf("--- Stage 4: randomized 2-instance sync (50 iters) ---\n"); |
|
stage_failures = 0; |
|
test_randomized_two(); |
|
return stage_failures; |
|
} |
|
|
|
static int run_stage5(void) { |
|
printf("--- Stage 5: randomized 3-instance star sync (30 iters) ---\n"); |
|
stage_failures = 0; |
|
test_randomized_three(); |
|
return stage_failures; |
|
} |
|
|
|
/* ── Stage 7: stress test — 10 spammers + 2 A↔B observers, star topology ── */ |
|
|
|
#define STRESS_N 13 /* 1 hub + 10 spammers + 2 observers */ |
|
#define STRESS_HUB 0 |
|
#define STRESS_SPAM_BEGIN 1 |
|
#define STRESS_SPAM_END 10 /* indices 1..10 */ |
|
#define STRESS_OBS_A 11 |
|
#define STRESS_OBS_B 12 |
|
#define STRESS_SPAM_MS 5000 |
|
#define STRESS_SYNC_TB 200000 /* 20s timeout for final sync */ |
|
#define STRESS_LINK_TB 300000 /* 30s for all links up */ |
|
#define STRESS_MAX_ROUNDS 5 |
|
#define STRESS_DRAIN_TB 2000 /* 200ms drain after spam stop */ |
|
#define STRESS_ROUND_GAP_TB 500 /* 50ms gap between final rounds */ |
|
#define STRESS_SAFETY_TB 400000 /* 40s safety for spam+converge */ |
|
|
|
struct str_state { |
|
struct UASYNC* ua; |
|
struct UTUN_INSTANCE* inst[STRESS_N]; |
|
struct ms_data data[STRESS_N]; |
|
struct ms_test_ctx ctx[STRESS_N]; |
|
void* global_timeout; /* setup: links-up timeout */ |
|
void* safety_timer; /* spam+converge safety */ |
|
void* stop_timer; /* spam stop */ |
|
void* drain_timer; /* drain after spam stop */ |
|
void* round_timer; /* gap between final rounds */ |
|
int phase; /* 0=running, 1=pass, 2=fail */ |
|
int spam_active; |
|
int sync_pending; /* initial sync only (balanced) */ |
|
int final_pending; /* outstanding final-round syncs */ |
|
int final_round; /* final round number */ |
|
}; |
|
|
|
static struct str_state* gs = NULL; |
|
|
|
static void _str_cleanup(void); |
|
static int _str_cond_sync_done(void); |
|
static int _str_cond_links_up(void); |
|
static void _str_stop_spam(void* arg); |
|
static void _str_drain_cb(void* arg); |
|
static void _str_round_cb(void* arg); |
|
static void _str_start_final_round(void); |
|
static void _str_final_done(uint64_t peer, const char* ns, int result, void* arg); |
|
static void _str_check_roots(void); |
|
static void _str_verify(void); |
|
static void _str_fail_timeout(void* arg); |
|
|
|
static void _str_sync_done(uint64_t peer, const char* ns, int result, void* arg) { |
|
(void)peer; (void)ns; (void)result; |
|
struct str_state* s = (struct str_state*)arg; |
|
if (s) __sync_fetch_and_sub(&s->sync_pending, 1); |
|
} |
|
|
|
static void _str_spam_cb(void* arg); |
|
|
|
static void _str_spam_schedule(struct str_state* s, int idx) { |
|
if (!s->spam_active) return; |
|
int delay_tb = ((rand() % 30) + 1) * 10; /* 1-30ms → 10-300 timebase units */ |
|
uasync_set_timeout(s->ua, (uint32_t)delay_tb, (void*)(intptr_t)idx, _str_spam_cb, "spam"); |
|
} |
|
|
|
static void _str_spam_cb(void* arg) { |
|
if (!gs || !gs->spam_active) return; |
|
int idx = (int)(intptr_t)arg; |
|
if (idx < STRESS_SPAM_BEGIN || idx > STRESS_SPAM_END) { _str_spam_schedule(gs, idx); return; } |
|
|
|
/* spammer: bump own member, sync to hub */ |
|
uint64_t key = ((uint64_t)idx) << 60; |
|
_data_insert(&gs->data[idx], key, (uint32_t)(gs->data[idx].count + 1)); |
|
merkle_sync_recompute_path(gs->inst[idx], MS_NS_STRESS, key); |
|
merkle_sync_start(gs->inst[idx], gs->inst[STRESS_HUB]->node_id, MS_NS_STRESS, NULL, NULL); |
|
|
|
_str_spam_schedule(gs, idx); |
|
} |
|
|
|
static void _str_obs_spam_cb(void* arg) { |
|
if (!gs || !gs->spam_active) return; |
|
int idx = (int)(intptr_t)arg; |
|
|
|
merkle_sync_start(gs->inst[idx], gs->inst[STRESS_HUB]->node_id, MS_NS_STRESS, NULL, NULL); |
|
|
|
int delay_tb = ((rand() % 10) + 1) * 10; /* 1-10ms */ |
|
uasync_set_timeout(gs->ua, (uint32_t)delay_tb, arg, _str_obs_spam_cb, "obs_spam"); |
|
} |
|
|
|
static void _str_global_timeout(void* arg) { |
|
(void)arg; |
|
if (gs) gs->phase = 2; |
|
} |
|
|
|
static int _str_cond_links_up(void) { |
|
if (!gs) return 0; |
|
int total_links = 0; |
|
for (int i = 0; i < STRESS_N; i++) { |
|
if (!gs->inst[i] || !gs->inst[i]->connections) return 0; |
|
struct ll_entry* e = gs->inst[i]->connections->head; |
|
while (e) { |
|
struct conn_queue_entry* ce = (struct conn_queue_entry*)e->data; |
|
struct ETCP_LINK* l = ce->conn->links; |
|
while (l) { if (l->initialized) total_links++; l = l->next; } |
|
e = e->next; |
|
} |
|
} |
|
/* hub should have 12 links (one per spoke), spokes have 1 each */ |
|
return total_links >= STRESS_N * 2 - 2; /* 12 hub links + 12 spoke links = 24 total */ |
|
} |
|
|
|
static int _str_wait_for(const char* desc, int (*cond)(void), int timeout_tb) { |
|
struct str_state* s = gs; |
|
uint64_t start = get_time_tb(); |
|
while (!cond() && (get_time_tb() - start) < (uint64_t)timeout_tb && s->phase == 0) |
|
uasync_poll(s->ua, 1); |
|
if (cond()) return 1; |
|
printf(" STRESS TIMEOUT: %s\n", desc); s->phase = 2; |
|
return 0; |
|
} |
|
|
|
static int _str_init(void) { |
|
struct str_state* s = u_calloc(1, sizeof(*s)); |
|
if (!s) return -1; |
|
gs = s; |
|
s->spam_active = 0; |
|
s->phase = 0; |
|
s->sync_pending = 0; |
|
|
|
/* --- temp dir --- */ |
|
char tmpld[128] = "/tmp/utun_str_XXXXXX"; |
|
if (test_mkdtemp(tmpld) != 0) { printf(" mkdtemp failed\n"); u_free(s); gs = NULL; return -1; } |
|
|
|
/* --- create configs --- */ |
|
int base_port = 44000 + (getpid() % 10000); |
|
char cfgs[STRESS_N][256]; int ports[STRESS_N]; |
|
for (int i = 0; i < STRESS_N; i++) ports[i] = base_port + i; |
|
|
|
utun_instance_set_tun_init_enabled(0); |
|
s->ua = uasync_create(); |
|
if (!s->ua) goto fail; |
|
|
|
/* hub config */ |
|
snprintf(cfgs[0], sizeof(cfgs[0]), "%s/cfg_0.conf", tmpld); |
|
{ |
|
char dbp[256]; snprintf(dbp, sizeof(dbp), "%s/db_0", tmpld); utun_mkdir(dbp, 0755); |
|
_write_cfg(cfgs[0], |
|
"[global]\nmy_node_id=0xAAAAAAAAAAAAAAAA\ntun_ip=10.240.0.1/24\ntun_ifname=tun240\nkeepalive_adaptive=0\ndb_path=%s\n\n" |
|
"[server:s0]\naddr=127.0.0.1:%d\ntype=public\n\n" |
|
"[allowed_keys]\nallow_all=1\n", dbp, ports[0]); |
|
config_ensure_keys_and_node_id(cfgs[0]); |
|
} |
|
|
|
char* hub_pub = _get_pubkey(cfgs[0]); if (!hub_pub) goto fail; |
|
/* spoke configs */ |
|
for (int i = 1; i < STRESS_N; i++) { |
|
snprintf(cfgs[i], sizeof(cfgs[i]), "%s/cfg_%d.conf", tmpld, i); |
|
char dbp[256]; snprintf(dbp, sizeof(dbp), "%s/db_%d", tmpld, i); utun_mkdir(dbp, 0755); |
|
char nid[32]; snprintf(nid, sizeof(nid), "BBBBBBBBBBBB%02d00", i); |
|
_write_cfg(cfgs[i], |
|
"[global]\nmy_node_id=0x%s\ntun_ip=10.240.0.%d/24\ntun_ifname=tun240\nkeepalive_adaptive=0\ndb_path=%s\n\n" |
|
"[server:s%d]\naddr=127.0.0.1:%d\ntype=public\n\n" |
|
"[client:to_hub]\nkeepalive=1\npeer_public_key=%s\nlink=s%d:127.0.0.1:%d\n\n" |
|
"[allowed_keys]\nallow_all=1\n", |
|
nid, i + 1, dbp, i, ports[i], hub_pub, i, ports[0]); |
|
config_ensure_keys_and_node_id(cfgs[i]); |
|
} |
|
u_free(hub_pub); |
|
|
|
/* --- create instances --- */ |
|
for (int i = 0; i < STRESS_N; i++) { |
|
s->inst[i] = utun_instance_create(s->ua, cfgs[i]); |
|
if (!s->inst[i] || utun_instance_init(s->inst[i]) != 0) { |
|
printf(" FAIL: instance %d init\n", i); |
|
goto fail; |
|
} |
|
} |
|
printf(" %d instances created, waiting for links...\n", STRESS_N); |
|
|
|
s->global_timeout = uasync_set_timeout(s->ua, STRESS_LINK_TB, NULL, _str_global_timeout, "str_timeout"); |
|
if (!_str_wait_for("links up", _str_cond_links_up, STRESS_LINK_TB)) { printf(" links never came up\n"); goto fail; } |
|
printf(" all links UP\n"); |
|
uasync_cancel_timeout(s->ua, s->global_timeout); s->global_timeout = NULL; |
|
|
|
/* --- init merkle_sync on all --- */ |
|
for (int i = 0; i < STRESS_N; i++) { |
|
memset(&s->data[i], 0, sizeof(s->data[i])); |
|
s->ctx[i].data = &s->data[i]; |
|
s->ctx[i].inst = s->inst[i]; |
|
s->ctx[i].tag = (char)('A' + i); |
|
_ms_ensure_groups(s->inst[i]); |
|
if (merkle_sync_init(s->inst[i], MS_SVC_ID, &g_test_ops, &s->ctx[i]) != 0) { |
|
printf(" FAIL: merkle_sync_init %d\n", i); goto fail; |
|
} |
|
} |
|
printf(" merkle_sync inited on all\n"); |
|
|
|
/* --- seed each instance with its own member; sync to hub --- */ |
|
for (int i = 1; i < STRESS_N; i++) { |
|
uint64_t key = ((uint64_t)i) << 60; |
|
_data_insert(&s->data[i], key, (uint32_t)i); |
|
merkle_sync_recompute_path(s->inst[i], MS_NS_STRESS, key); |
|
} |
|
/* initial sync: each spoke → hub */ |
|
s->sync_pending = STRESS_N - 1; |
|
for (int i = 1; i < STRESS_N; i++) |
|
merkle_sync_start(s->inst[i], s->inst[STRESS_HUB]->node_id, MS_NS_STRESS, _str_sync_done, s); |
|
_str_wait_for("initial sync", _str_cond_sync_done, STRESS_SYNC_TB); |
|
|
|
/* observers: sync with hub too */ |
|
|
|
return 0; |
|
|
|
fail: |
|
_str_cleanup(); |
|
return -1; |
|
} |
|
|
|
static int _str_cond_sync_done(void) { |
|
return gs ? __sync_fetch_and_add(&gs->sync_pending, 0) <= 0 : 0; |
|
} |
|
|
|
static void _str_cleanup(void) { |
|
struct str_state* s = gs; |
|
if (!s) return; |
|
s->spam_active = 0; |
|
if (s->global_timeout && s->ua) { uasync_cancel_timeout(s->ua, s->global_timeout); s->global_timeout = NULL; } |
|
if (s->safety_timer && s->ua) { uasync_cancel_timeout(s->ua, s->safety_timer); s->safety_timer = NULL; } |
|
if (s->stop_timer && s->ua) { uasync_cancel_timeout(s->ua, s->stop_timer); s->stop_timer = NULL; } |
|
if (s->drain_timer && s->ua) { uasync_cancel_timeout(s->ua, s->drain_timer); s->drain_timer = NULL; } |
|
if (s->round_timer && s->ua) { uasync_cancel_timeout(s->ua, s->round_timer); s->round_timer = NULL; } |
|
for (int i = STRESS_N - 1; i >= 0; i--) { |
|
if (!s->inst[i]) continue; |
|
merkle_sync_destroy(s->inst[i]); |
|
s->inst[i]->running = 0; |
|
utun_instance_destroy(s->inst[i]); |
|
s->inst[i] = NULL; |
|
} |
|
if (s->ua) { uasync_destroy(s->ua, 0); s->ua = NULL; } |
|
u_free(s); |
|
gs = NULL; |
|
} |
|
|
|
static void _str_stop_spam(void* arg) { |
|
(void)arg; |
|
if (!gs) return; |
|
gs->stop_timer = NULL; |
|
gs->spam_active = 0; |
|
printf(" spam stopped\n"); |
|
/* короткий дренаж — даём in-flight спам-синкам утихнуть перед финальными раундами */ |
|
gs->drain_timer = uasync_set_timeout(gs->ua, STRESS_DRAIN_TB, NULL, _str_drain_cb, "str_drain"); |
|
} |
|
|
|
static void _str_drain_cb(void* arg) { |
|
(void)arg; |
|
if (!gs) return; |
|
gs->drain_timer = NULL; |
|
_str_start_final_round(); |
|
} |
|
|
|
static void _str_start_final_round(void) { |
|
struct str_state* s = gs; |
|
if (!s) return; |
|
s->final_round++; |
|
s->final_pending = STRESS_N - 1; |
|
printf(" final round %d: syncing %d spokes -> hub\n", s->final_round, s->final_pending); |
|
for (int i = 1; i < STRESS_N; i++) |
|
merkle_sync_start(s->inst[i], s->inst[STRESS_HUB]->node_id, MS_NS_STRESS, _str_final_done, NULL); |
|
} |
|
|
|
static void _str_final_done(uint64_t peer, const char* ns, int result, void* arg) { |
|
(void)peer; (void)ns; (void)result; (void)arg; |
|
if (!gs) return; |
|
gs->final_pending--; |
|
if (gs->final_pending <= 0) _str_check_roots(); |
|
} |
|
|
|
static void _str_verify(void) { |
|
struct str_state* s = gs; |
|
int ok = 1; |
|
|
|
/* tree sizes match pairwise */ |
|
int na = _db_count_rows(s->inst[STRESS_HUB]->topo_sqlite_db, MS_NS_STRESS); |
|
for (int i = 1; i < STRESS_N && ok; i++) { |
|
int nb = _db_count_rows(s->inst[i]->topo_sqlite_db, MS_NS_STRESS); |
|
if (na != nb) { printf(" tree size mismatch: hub=%d inst[%d]=%d\n", na, i, nb); ok = 0; } |
|
} |
|
|
|
/* root (level=1, prefix=0) hashes identical */ |
|
const uint8_t* root0 = _test_hash(s->inst[STRESS_HUB], MS_NS_STRESS, 1, 0); |
|
uint8_t root_copy[MT_HASH_SIZE]; memcpy(root_copy, root0, MT_HASH_SIZE); |
|
for (int i = 1; i < STRESS_N && ok; i++) { |
|
const uint8_t* ri = _test_hash(s->inst[i], MS_NS_STRESS, 1, 0); |
|
if (memcmp(root_copy, ri, MT_HASH_SIZE) != 0) { printf(" ROOT HASH MISMATCH inst[%d]\n", i); ok = 0; } |
|
} |
|
|
|
/* recompute consistency for each instance */ |
|
for (int i = 0; i < STRESS_N && ok; i++) { |
|
sqlite3_stmt* st = NULL; |
|
sqlite3_prepare_v2(s->inst[i]->topo_sqlite_db, |
|
"SELECT level,prefix64 FROM merkle_tree_hash WHERE namespace=? ORDER BY level,prefix64", |
|
-1, &st, NULL); |
|
sqlite3_bind_text(st, 1, MS_NS_STRESS, -1, SQLITE_STATIC); |
|
while (sqlite3_step(st) == SQLITE_ROW && ok) { |
|
uint8_t lv = (uint8_t)sqlite3_column_int(st, 0); |
|
uint64_t pf = (uint64_t)sqlite3_column_int64(st, 1); |
|
uint8_t expected[MT_HASH_SIZE]; |
|
_expected_hash(&s->ctx[i], MS_NS_STRESS, lv, pf, expected); |
|
const uint8_t* stored = _test_hash(s->inst[i], MS_NS_STRESS, lv, pf); |
|
uint8_t scopy[MT_HASH_SIZE]; memcpy(scopy, stored, MT_HASH_SIZE); |
|
if (memcmp(expected, scopy, MT_HASH_SIZE) != 0) { |
|
printf(" consistency fail inst[%d] L%d P%016llx\n", i, lv, (unsigned long long)pf); |
|
ok = 0; |
|
} |
|
} |
|
sqlite3_finalize(st); |
|
} |
|
|
|
/* print summary */ |
|
printf(" tree rows: %d data items per node:", na); |
|
for (int i = 0; i < STRESS_N && i < 6; i++) printf(" %d", s->data[i].count); |
|
printf("..\n"); |
|
|
|
s->phase = ok ? 1 : 2; |
|
} |
|
|
|
static void _str_check_roots(void) { |
|
struct str_state* s = gs; |
|
if (!s) return; |
|
const uint8_t* root0 = _test_hash(s->inst[STRESS_HUB], MS_NS_STRESS, 1, 0); |
|
uint8_t root_copy[MT_HASH_SIZE]; |
|
memcpy(root_copy, root0, MT_HASH_SIZE); |
|
int converged = 1; |
|
for (int i = 1; i < STRESS_N; i++) { |
|
const uint8_t* ri = _test_hash(s->inst[i], MS_NS_STRESS, 1, 0); |
|
if (memcmp(root_copy, ri, MT_HASH_SIZE) != 0) { converged = 0; break; } |
|
} |
|
if (converged) { |
|
printf(" roots converged after %d round(s)\n", s->final_round); |
|
_str_verify(); |
|
} else if (s->final_round < STRESS_MAX_ROUNDS) { |
|
s->round_timer = uasync_set_timeout(s->ua, STRESS_ROUND_GAP_TB, NULL, _str_round_cb, "str_round"); |
|
} else { |
|
printf(" no convergence after %d rounds\n", s->final_round); |
|
s->phase = 2; |
|
} |
|
} |
|
|
|
static void _str_round_cb(void* arg) { |
|
(void)arg; |
|
if (!gs) return; |
|
gs->round_timer = NULL; |
|
_str_start_final_round(); |
|
} |
|
|
|
static void _str_fail_timeout(void* arg) { |
|
(void)arg; |
|
if (!gs) return; |
|
printf(" SAFETY TIMEOUT\n"); |
|
gs->phase = 2; |
|
} |
|
|
|
static void test_stress_spam(void) { |
|
TEST("stress: 10 spammers + 2 obs, 5s spam, verify merkle roots"); |
|
if (_str_init() != 0) { FAIL("init failed"); _str_cleanup(); return; } |
|
|
|
struct str_state* s = gs; |
|
|
|
/* --- start spam --- */ |
|
s->spam_active = 1; |
|
srand(12345); |
|
for (int i = STRESS_SPAM_BEGIN; i <= STRESS_SPAM_END; i++) { |
|
/* stagger initial delays */ |
|
int d = (rand() % 30) + 1; |
|
uasync_set_timeout(s->ua, (uint32_t)(d * 10), (void*)(intptr_t)i, _str_spam_cb, "spam"); |
|
} |
|
/* observers sync with hub aggressively */ |
|
uasync_set_timeout(s->ua, 10, (void*)(intptr_t)STRESS_OBS_A, _str_obs_spam_cb, "obsA"); |
|
uasync_set_timeout(s->ua, 15, (void*)(intptr_t)STRESS_OBS_B, _str_obs_spam_cb, "obsB"); |
|
|
|
/* --- spam stop + safety timeout: событиями, без холостых ожиданий --- */ |
|
s->stop_timer = uasync_set_timeout(s->ua, (uint32_t)(STRESS_SPAM_MS * 10), NULL, _str_stop_spam, "str_stop"); |
|
s->safety_timer = uasync_set_timeout(s->ua, STRESS_SAFETY_TB, NULL, _str_fail_timeout, "str_safety"); |
|
|
|
printf(" spamming for %dms...\n", STRESS_SPAM_MS); |
|
/* Событийный цикл: фазы переключаются таймерами и done-коллбэками. |
|
* poll с ограниченным таймаутом (10ms), т.к. uasync_poll(ua,-1) при уже |
|
* просроченном ближайшем таймере уходит в бесконечный epoll_wait и не |
|
* обрабатывает expired-таймеры. */ |
|
while (s->phase == 0) |
|
uasync_poll(s->ua, 100); |
|
|
|
if (s->phase != 1) { FAIL("convergence failed (phase=%d)", s->phase); _str_cleanup(); return; } |
|
PASS(); |
|
_str_cleanup(); |
|
} |
|
static int _cond_pair_equal(void) { |
|
if (data_a.count != data_b.count) return 0; |
|
for (int i = 0; i < data_a.count; i++) |
|
if (data_a.items[i].key != data_b.items[i].key || data_a.items[i].val != data_b.items[i].val) return 0; |
|
return _db_compare_trees(i_a->topo_sqlite_db, i_b->topo_sqlite_db, MS_NS_TEST) == 0; |
|
} |
|
|
|
static void test_large_leaf_and_automatic_update(void) { |
|
TEST("6000 records in one leaf (>64K), then automatic update without start/broadcast"); |
|
if (_intg_init_two() != 0) { FAIL("setup failed"); return; } |
|
for (uint64_t i = 0; i < 6000; i++) _data_insert(&data_b, i, (uint32_t)i); |
|
_data_sort(&data_b); |
|
if (merkle_sync_recompute_path(i_b, MS_NS_TEST, 0) < 0) { FAIL("recompute failed"); _intg_cleanup(); return; } |
|
done_sync = 0; |
|
merkle_sync_start(i_a, i_b->node_id, MS_NS_TEST, _on_sync_done, NULL); |
|
if (!_wait_for("large leaf", _cond_done, PHASE_TIMEOUT_TB) || !_cond_pair_equal()) { |
|
FAIL("large leaf did not converge"); _intg_cleanup(); return; |
|
} |
|
data_b.items[0].val = 9999; |
|
merkle_sync_recompute_path(i_b, MS_NS_TEST, 0); |
|
if (!_wait_for("automatic change", _cond_pair_equal, PHASE_TIMEOUT_TB)) { |
|
FAIL("automatic change did not propagate"); _intg_cleanup(); return; |
|
} |
|
PASS(); _intg_cleanup(); |
|
} |
|
|
|
static int run_stage6(void) { |
|
printf("--- Stage 6: protocol edge cases ---\n"); |
|
stage_failures = 0; |
|
test_cancel(); |
|
test_start_overwrite(); |
|
test_large_leaf_and_automatic_update(); |
|
return stage_failures; |
|
} |
|
|
|
static int run_stage7(void) { |
|
printf("--- Stage 7: stress test (10 spammers + 2 A↔B obs, 5s) ---\n"); |
|
stage_failures = 0; |
|
test_stress_spam(); |
|
return stage_failures; |
|
} |
|
|
|
int main(void) { |
|
printf("=== test_merkle_sync ===\n"); |
|
debug_config_init(); |
|
debug_set_level(DEBUG_LEVEL_ERROR); |
|
if (getenv("UTUN_TEST_DEBUG")) debug_set_category_level(DEBUG_CATEGORY_MEMBER_SYNC, DEBUG_LEVEL_DEBUG); |
|
|
|
if (run_stage1() != 0) { printf("=== Stage 1 FAILED ===\n"); return 1; } |
|
printf("=== Stage 1 PASSED (%d tests) ===\n\n", tests_run); |
|
|
|
if (run_stage2() != 0) { printf("=== Stage 2 FAILED ===\n"); return 1; } |
|
printf("=== Stage 2 PASSED (%d tests) ===\n\n", tests_run); |
|
|
|
if (run_stage3() != 0) { printf("=== Stage 3 FAILED ===\n"); return 1; } |
|
printf("=== Stage 3 PASSED (%d tests) ===\n\n", tests_run); |
|
|
|
if (run_stage4() != 0) { printf("=== Stage 4 FAILED ===\n"); return 1; } |
|
printf("=== Stage 4 PASSED (%d tests) ===\n\n", tests_run); |
|
|
|
if (run_stage5() != 0) { printf("=== Stage 5 FAILED ===\n"); return 1; } |
|
printf("=== Stage 5 PASSED (%d tests) ===\n\n", tests_run); |
|
|
|
if (run_stage6() != 0) { printf("=== Stage 6 FAILED ===\n"); return 1; } |
|
printf("=== Stage 6 PASSED (%d tests) ===\n\n", tests_run); |
|
|
|
if (run_stage7() != 0) { printf("=== Stage 7 FAILED ===\n"); return 1; } |
|
printf("=== Stage 7 PASSED (%d tests) ===\n\n", tests_run); |
|
|
|
printf("=== ALL PASS (%d tests) ===\n", tests_run); |
|
return 0; |
|
}
|
|
|