196 lines
14 KiB
C
196 lines
14 KiB
C
/* Проверенные/новые/старые версии в настоящей SQLite: конкурентные пиры, отказ и отмена. */
|
|
#include "../src/chat/member_sync.c"
|
|
#include <assert.h>
|
|
|
|
struct identity { uint64_t id; uint8_t x[32], ed[32], secret[32], join[64], tree[64]; };
|
|
static struct identity identities[5];
|
|
static uint8_t group_secret[32] = {91}, group_public[32], group_x[32] = {92};
|
|
|
|
static void public_key(const uint8_t secret[32], uint8_t public[32]) {
|
|
EVP_PKEY* key = EVP_PKEY_new_raw_private_key(EVP_PKEY_ED25519, NULL, secret, 32); assert(key);
|
|
size_t bytes = 32; assert(EVP_PKEY_get_raw_public_key(key, public, &bytes) == 1); EVP_PKEY_free(key);
|
|
}
|
|
|
|
static void init_identity(unsigned i) {
|
|
struct identity* p = &identities[i]; p->x[0] = i + 11; p->secret[0] = i + 31;
|
|
public_key(p->secret, p->ed); p->id = sc_derive_node_id_from_pubkey(p->x);
|
|
uint8_t message[112]; assert(member_sync_build_join_msg(group_x, group_public, p->id, p->x, 1, message, sizeof(message)) == 112);
|
|
assert(sc_ed25519_sign(p->secret, message, sizeof(message), p->join) == SC_OK);
|
|
assert(!member_sync_sign_join(group_secret, p->join, p->tree));
|
|
}
|
|
|
|
static void profile(struct UTUN_INSTANCE* inst, unsigned person, uint64_t version) {
|
|
struct identity* p = &identities[person]; uint8_t message[256], signature[64]; char info[64];
|
|
snprintf(info, sizeof(info), "{\"name\":\"profile-%llu\"}", (unsigned long long)version);
|
|
int bytes = member_sync_build_update_msg(p->join, version, info, message, sizeof(message)); assert(bytes > 0);
|
|
assert(sc_ed25519_sign(p->secret, message, bytes, signature) == SC_OK);
|
|
assert(member_sync_put(inst, "1001", p->id, p->x, p->ed, p->join, 1, signature, version, info,
|
|
NULL, NULL, 0, 0, p->tree) >= 0);
|
|
}
|
|
|
|
static void setup(struct UTUN_INSTANCE* inst, struct UASYNC* ua, unsigned self) {
|
|
memset(inst, 0, sizeof(*inst)); inst->node_id = identities[self].id; inst->ua = ua;
|
|
assert(sqlite3_open(":memory:", &inst->topo_sqlite_db) == SQLITE_OK);
|
|
assert(!topo_node_sqlite_init(inst->topo_sqlite_db));
|
|
assert(!topo_node_sqlite_channel_put(inst->topo_sqlite_db, "1001", "versions", identities[0].id,
|
|
group_x, NULL, group_public, NULL, (uint8_t[64]){0}));
|
|
assert(!member_sync_init(inst));
|
|
for (unsigned i = 0; i < 3; i++) profile(inst, i, 1);
|
|
}
|
|
|
|
static sqlite3_int64 scalar(struct UTUN_INSTANCE* inst, const char* sql) {
|
|
sqlite3_stmt* st = NULL; assert(sqlite3_prepare_v2(inst->topo_sqlite_db, sql, -1, &st, NULL) == SQLITE_OK);
|
|
assert(sqlite3_step(st) == SQLITE_ROW); sqlite3_int64 value = sqlite3_column_int64(st, 0);
|
|
assert(sqlite3_finalize(st) == SQLITE_OK); return value;
|
|
}
|
|
|
|
static uint64_t version(struct UTUN_INSTANCE* inst) {
|
|
struct member_saved record; assert(member_read_saved(inst->topo_sqlite_db, "1001", identities[2].id, 0, 0, &record) == 1);
|
|
return record.rec.update_ts;
|
|
}
|
|
|
|
static void stage_person(struct UTUN_INSTANCE* source, struct UTUN_INSTANCE* target, unsigned person, void* state) {
|
|
uint64_t prefix = merkle_sync_level_prefix(identities[person].id, MT_MAX_LEVEL);
|
|
uint8_t bytes[MT_PAGE_SIZE]; size_t length = sizeof(bytes); int more; merkle_key_t after = {{0}}, next;
|
|
assert(!_member_get_page(source, "1001", prefix, 0, &after, bytes, &length, &next, &more) && !more);
|
|
assert(!member_stage_page(target, state, prefix, 0, &after, &next, more, bytes, length));
|
|
}
|
|
|
|
static struct member_pull* receive(struct UTUN_INSTANCE* source, struct UTUN_INSTANCE* target, unsigned person, uint64_t peer) {
|
|
void* state = NULL; assert(!member_begin_pull(target, "1001", peer, 1, &state));
|
|
stage_person(source, target, person, state); return state;
|
|
}
|
|
|
|
static void finish_receive(struct UTUN_INSTANCE* inst, struct member_pull* pull) {
|
|
assert(!member_commit_pull(inst, pull)); member_publish_pull(inst, pull); member_abort_pull(inst, pull);
|
|
assert(sqlite3_get_autocommit(inst->topo_sqlite_db));
|
|
}
|
|
|
|
static void corrupt(struct UTUN_INSTANCE* inst) {
|
|
char sql[192]; snprintf(sql, sizeof(sql), "UPDATE peers_1001 SET update_sig=zeroblob(64)||X'01' WHERE node_id=%lld AND record_state=0",
|
|
(long long)identities[2].id);
|
|
assert(sqlite3_exec(inst->topo_sqlite_db, sql, NULL, NULL, NULL) == SQLITE_OK);
|
|
}
|
|
|
|
static void revoke_member(struct UTUN_INSTANCE* inst, unsigned person) {
|
|
struct member_saved current; uint8_t signature[64], message[128];
|
|
assert(member_read_saved(inst->topo_sqlite_db, "1001", identities[person].id, 0, 0, ¤t) == 1);
|
|
const char* tags = "{\"ver\":\"1\",\"deleted\":\"yes\"}";
|
|
int bytes = member_sync_build_owner_msg(identities[person].id, tags, message, sizeof(message)); assert(bytes > 0);
|
|
assert(sc_ed25519_sign(group_secret, message, bytes, signature) == SC_OK);
|
|
current.rec.adm_tags = tags; current.rec.adm_tags_sig = signature;
|
|
assert(member_sync_apply_record(inst, "1001", inst->node_id, ¤t.rec) >= 0);
|
|
}
|
|
|
|
int main(void) {
|
|
debug_config_init(); debug_set_level(DEBUG_LEVEL_INFO);
|
|
size_t baseline = u_get_allocated_count(); struct UASYNC* ua = uasync_create(); assert(ua);
|
|
public_key(group_secret, group_public);
|
|
for (unsigned i = 0; i < 5; i++) init_identity(i);
|
|
struct UTUN_INSTANCE source, target; setup(&source, ua, 1); setup(&target, ua, 0);
|
|
uint8_t original[32], staged[32]; assert(!merkle_sync_read_hash(&target, MT_DATA_MEMBERS, "1001", 0, 0, original));
|
|
profile(&source, 2, 2);
|
|
struct member_pull* a = receive(&source, &target, 2, identities[1].id);
|
|
assert(version(&target) == 1 && member_sync_count(&target, "1001") == 3);
|
|
assert(!merkle_sync_read_hash(&target, MT_DATA_MEMBERS, "1001", 0, 0, staged) && !memcmp(original, staged, 32));
|
|
assert(scalar(&target, "SELECT COUNT(*) FROM nodes WHERE name='profile-2'") == 0);
|
|
profile(&source, 2, 3);
|
|
struct member_pull* b = receive(&source, &target, 2, identities[3].id);
|
|
assert(a->id != b->id && scalar(&target, "SELECT COUNT(*) FROM peers_1001 WHERE record_state=1") == 4);
|
|
finish_receive(&target, b); assert(version(&target) == 3);
|
|
assert(scalar(&target, "SELECT COUNT(*) FROM peers_1001 WHERE record_state=1") == 2);
|
|
finish_receive(&target, a); assert(version(&target) == 3);
|
|
assert(!scalar(&target, "SELECT COUNT(*) FROM peers_1001 WHERE record_state<>0"));
|
|
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "PASS two peers: isolated staging, no early profile/hash change, stale completion preserves newer commit");
|
|
|
|
profile(&source, 2, 4); a = receive(&source, &target, 2, identities[1].id); b = receive(&source, &target, 2, identities[3].id);
|
|
assert(sqlite3_exec(target.topo_sqlite_db, "BEGIN", NULL, NULL, NULL) == SQLITE_OK);
|
|
assert(member_commit_pull(&target, a) == MT_ERR_IO && !sqlite3_get_autocommit(target.topo_sqlite_db));
|
|
assert(sqlite3_exec(target.topo_sqlite_db, "ROLLBACK", NULL, NULL, NULL) == SQLITE_OK);
|
|
member_abort_pull(&target, a);
|
|
assert(scalar(&target, "SELECT COUNT(*) FROM peers_1001 WHERE record_state=1") == 2);
|
|
finish_receive(&target, b); assert(version(&target) == 4);
|
|
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "PASS cancelling one peer leaves the other exchange intact");
|
|
|
|
profile(&source, 2, 5);
|
|
char sql[256]; snprintf(sql, sizeof(sql), "UPDATE peers_1001 SET update_sig=randomblob(64) WHERE node_id=%lld AND record_state=0",
|
|
(long long)identities[2].id);
|
|
assert(sqlite3_exec(source.topo_sqlite_db, sql, NULL, NULL, NULL) == SQLITE_OK);
|
|
a = receive(&source, &target, 2, identities[1].id); finish_receive(&target, a); assert(version(&target) == 4);
|
|
assert(sqlite3_exec(target.topo_sqlite_db, sql, NULL, NULL, NULL) == SQLITE_OK);
|
|
a = receive(&source, &target, 2, identities[1].id); finish_receive(&target, a);
|
|
assert(!topo_node_sqlite_member_in_channel(target.topo_sqlite_db, "1001", identities[2].id));
|
|
assert(member_sync_count(&target, "1001") == 2 && topo_node_sqlite_member_allowed(target.topo_sqlite_db, "1001", identities[0].id));
|
|
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "PASS bad new keeps good old; both invalid removes only that member");
|
|
|
|
profile(&source, 2, 6); a = receive(&source, &target, 2, identities[1].id); finish_receive(&target, a); assert(version(&target) == 6);
|
|
profile(&source, 2, 7); a = receive(&source, &target, 2, identities[1].id);
|
|
snprintf(sql, sizeof(sql), "CREATE TRIGGER fail_identity BEFORE INSERT ON nodes WHEN NEW.node_id=%lld"
|
|
" BEGIN SELECT RAISE(ABORT,'injected identity failure'); END", (long long)identities[2].id);
|
|
assert(sqlite3_exec(target.topo_sqlite_db, sql, NULL, NULL, NULL) == SQLITE_OK);
|
|
assert(member_commit_pull(&target, a) < 0 && version(&target) == 6);
|
|
member_publish_pull(&target, a); member_abort_pull(&target, a);
|
|
assert(!scalar(&target, "SELECT COUNT(*) FROM peers_1001 WHERE record_state<>0"));
|
|
assert(sqlite3_exec(target.topo_sqlite_db, "DROP TRIGGER fail_identity", NULL, NULL, NULL) == SQLITE_OK);
|
|
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "PASS SQL failure rolls back versions, identities and admission together");
|
|
|
|
a = receive(&source, &target, 2, identities[1].id); sqlite3_int64 previous = a->id;
|
|
member_sync_destroy(&target); assert(!member_sync_init(&target));
|
|
assert(version(&target) == 6 && !scalar(&target, "SELECT COUNT(*) FROM peers_1001 WHERE record_state<>0"));
|
|
b = receive(&source, &target, 2, identities[3].id); assert(b->id > previous);
|
|
member_abort_pull(&target, a); finish_receive(&target, b); assert(version(&target) == 7);
|
|
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "PASS restart removes abandoned versions without reusing exchange identifiers");
|
|
|
|
snprintf(sql, sizeof(sql), "UPDATE peers_1001 SET local_nick='local alias',radio_muted=1,node_RTT=42,connected=1"
|
|
" WHERE node_id=%lld AND record_state=0", (long long)identities[2].id);
|
|
assert(sqlite3_exec(target.topo_sqlite_db, sql, NULL, NULL, NULL) == SQLITE_OK);
|
|
corrupt(&target); profile(&source, 2, 8);
|
|
a = receive(&source, &target, 2, identities[1].id); finish_receive(&target, a); assert(version(&target) == 8);
|
|
assert(scalar(&target, "SELECT COUNT(*) FROM peers_1001 WHERE record_state=0 AND local_nick='local alias'"
|
|
" AND radio_muted=1 AND node_RTT=42 AND connected=1") == 1);
|
|
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "PASS valid new version repairs a malformed previous version and preserves local settings");
|
|
|
|
/* У старого обмена нет отзыва: позднее завершение не возвращает права. */
|
|
a = receive(&source, &target, 2, identities[1].id);
|
|
revoke_member(&source, 2); b = receive(&source, &target, 2, identities[3].id); finish_receive(&target, b);
|
|
assert(!topo_node_sqlite_member_allowed(target.topo_sqlite_db, "1001", identities[2].id));
|
|
finish_receive(&target, a);
|
|
assert(member_sync_count(&target, "1001") == 3 && !topo_node_sqlite_member_allowed(target.topo_sqlite_db, "1001", identities[2].id));
|
|
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "PASS signed revocation survives stale concurrent completion as valid data");
|
|
|
|
/* Повреждённая высокая версия профиля не блокирует исправление; отдельный отзыв сохраняется. */
|
|
struct UTUN_INSTANCE clean; setup(&clean, ua, 1); profile(&clean, 2, 2);
|
|
corrupt(&target); a = receive(&clean, &target, 2, identities[1].id); finish_receive(&target, a);
|
|
assert(version(&target) == 2 && !topo_node_sqlite_member_allowed(target.topo_sqlite_db, "1001", identities[2].id));
|
|
member_sync_destroy(&clean); assert(sqlite3_close(clean.topo_sqlite_db) == SQLITE_OK);
|
|
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "PASS valid older profile repairs invalid newer profile without discarding owner revocation");
|
|
|
|
/* Собственное исключение не удаляет проверенные записи остальных участников. */
|
|
revoke_member(&source, 0); a = receive(&source, &target, 0, identities[1].id); finish_receive(&target, a);
|
|
assert(member_sync_count(&target, "1001") == 3 && !topo_node_sqlite_member_allowed(target.topo_sqlite_db, "1001", identities[0].id));
|
|
assert(topo_node_sqlite_member_allowed(target.topo_sqlite_db, "1001", identities[1].id));
|
|
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "PASS self revocation keeps valid roster and denies local group access");
|
|
|
|
/* Ребёнок раньше родителя: до полной проверки его ключ не попадает в общие nodes. */
|
|
assert(!member_sync_sign_join(identities[3].secret, identities[4].join, identities[4].tree));
|
|
profile(&source, 4, 1);
|
|
snprintf(sql, sizeof(sql), "UPDATE peers_1001 SET signed_by=%lld WHERE node_id=%lld AND record_state=0",
|
|
(long long)identities[3].id, (long long)identities[4].id);
|
|
assert(sqlite3_exec(source.topo_sqlite_db, sql, NULL, NULL, NULL) == SQLITE_OK);
|
|
a = receive(&source, &target, 4, identities[1].id);
|
|
assert(member_commit_pull(&target, a) == MT_ERR_CONFLICT); member_abort_pull(&target, a);
|
|
uint8_t key[32]; assert(topo_node_sqlite_get_ed25519_pubkey(target.topo_sqlite_db, identities[4].id, key) == TOPO_SQLITE_LOOKUP_MISSING);
|
|
assert(member_sync_count(&target, "1001") == 3);
|
|
a = receive(&source, &target, 4, identities[1].id);
|
|
profile(&source, 3, 1); stage_person(&source, &target, 3, a); finish_receive(&target, a);
|
|
assert(member_sync_count(&target, "1001") == 5);
|
|
/* Не-админ вправе подписать данные, но не дать членство по ограничивающей политике. */
|
|
assert(!topo_node_sqlite_member_allowed(target.topo_sqlite_db, "1001", identities[4].id));
|
|
assert(topo_node_sqlite_get_ed25519_pubkey(target.topo_sqlite_db, identities[4].id, key) == TOPO_SQLITE_LOOKUP_MISSING);
|
|
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "PASS missing inviter defers without shared identity changes; page order does not affect proof verification");
|
|
member_sync_destroy(&source); member_sync_destroy(&target);
|
|
assert(sqlite3_close(source.topo_sqlite_db) == SQLITE_OK && sqlite3_close(target.topo_sqlite_db) == SQLITE_OK);
|
|
uasync_destroy(ua, 0); assert(u_get_allocated_count() == baseline);
|
|
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "ALL PASS member version isolation and recovery"); return 0;
|
|
}
|