diff --git a/src/routing_layer/topo_node_sqlite.c b/src/routing_layer/topo_node_sqlite.c index 66261fb1..6839b270 100644 --- a/src/routing_layer/topo_node_sqlite.c +++ b/src/routing_layer/topo_node_sqlite.c @@ -724,11 +724,16 @@ int topo_node_sqlite_member_owner_put(sqlite3* db, const char* channel_id, uint6 return 0; } +/* Записывает уже проверенную вызывающим идентичность; любой отказ SQL возвращается вызывающему. */ int topo_node_sqlite_node_update_verified(sqlite3* db, uint64_t node_id, const char* name, const uint8_t* x25519, const uint8_t* ed25519, uint64_t update_ts, time_t created_at) { - if (!db || !x25519 || !ed25519) return -1; + if (!db || !x25519 || !ed25519) { + DEBUG_ERROR(DEBUG_CATEGORY_BGP, "verified node write: invalid arguments node=%016llx", (unsigned long long)node_id); + return -1; + } sqlite3_stmt* stmt = NULL; - if (sqlite3_prepare_v2(db, + const char* stage = "prepare"; + int rc = sqlite3_prepare_v2(db, "INSERT INTO nodes(node_id, name, x25519_pubkey, ed25519_pubkey, created_at, update_ts, online)" " VALUES(?,?,?,?,?,?,1)" " ON CONFLICT(node_id) DO UPDATE SET" @@ -739,15 +744,25 @@ int topo_node_sqlite_node_update_verified(sqlite3* db, uint64_t node_id, " created_at=COALESCE(nodes.created_at, excluded.created_at)," " update_ts=CASE WHEN nodes.update_ts IS NULL OR excluded.update_ts > nodes.update_ts" " THEN excluded.update_ts ELSE nodes.update_ts END", - -1, &stmt, NULL) != SQLITE_OK) return -1; - sqlite3_bind_int64(stmt, 1, (sqlite3_int64)node_id); - sqlite3_bind_text(stmt, 2, name && name[0] ? name : "", -1, SQLITE_STATIC); - sqlite3_bind_blob(stmt, 3, x25519, 32, SQLITE_STATIC); - sqlite3_bind_blob(stmt, 4, ed25519, 32, SQLITE_STATIC); - sqlite3_bind_int64(stmt, 5, (sqlite3_int64)created_at); - sqlite3_bind_int64(stmt, 6, (sqlite3_int64)update_ts); - sqlite3_step(stmt); - sqlite3_finalize(stmt); + -1, &stmt, NULL); + if (rc == SQLITE_OK) { + stage = "bind"; + rc = sqlite3_bind_int64(stmt, 1, (sqlite3_int64)node_id); + if (rc == SQLITE_OK) rc = sqlite3_bind_text(stmt, 2, name && name[0] ? name : "", -1, SQLITE_STATIC); + if (rc == SQLITE_OK) rc = sqlite3_bind_blob(stmt, 3, x25519, 32, SQLITE_STATIC); + if (rc == SQLITE_OK) rc = sqlite3_bind_blob(stmt, 4, ed25519, 32, SQLITE_STATIC); + if (rc == SQLITE_OK) rc = sqlite3_bind_int64(stmt, 5, (sqlite3_int64)created_at); + if (rc == SQLITE_OK) rc = sqlite3_bind_int64(stmt, 6, (sqlite3_int64)update_ts); + } + if (rc == SQLITE_OK) { stage = "step"; rc = sqlite3_step(stmt); } + if (rc != SQLITE_DONE) DEBUG_ERROR(DEBUG_CATEGORY_BGP, "verified node write: node=%016llx stage=%s rc=%d: %s", + (unsigned long long)node_id, stage, rc, sqlite3_errmsg(db)); + int final_rc = sqlite3_finalize(stmt); + if (final_rc != SQLITE_OK && rc == SQLITE_DONE) DEBUG_ERROR(DEBUG_CATEGORY_BGP, + "verified node write: node=%016llx stage=finalize rc=%d: %s", (unsigned long long)node_id, final_rc, sqlite3_errmsg(db)); + if (rc != SQLITE_DONE || final_rc != SQLITE_OK) return -1; + DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "verified node stored: node=%016llx timestamp=%llu changed=%d", + (unsigned long long)node_id, (unsigned long long)update_ts, sqlite3_changes(db)); return 0; } diff --git a/tests/test_node_snapshot.c b/tests/test_node_snapshot.c index 0b4afea4..c2f7652c 100644 --- a/tests/test_node_snapshot.c +++ b/tests/test_node_snapshot.c @@ -64,6 +64,32 @@ static void test_zero_port_schema(void) { } /* Новый wire и SQL сохраняют адреса, протоколы, классификацию и REALITY; смена формата оставляет bootstrap. */ +/* Отказ INSERT/UPDATE не должен выглядеть успешным сохранением уже проверенного узла. */ +static void test_verified_write(void) { + sqlite3* db = NULL; uint8_t key[32] = {1}; + assert(sqlite3_open(":memory:", &db) == SQLITE_OK && !topo_node_sqlite_init(db)); + assert(!topo_node_sqlite_node_update_verified(db, 7, "before", key, key, 100, 1)); + assert(sqlite3_exec(db, "CREATE TRIGGER reject_node BEFORE INSERT ON nodes " + "BEGIN SELECT RAISE(ABORT,'test insert rejection'); END", NULL, NULL, NULL) == SQLITE_OK); + assert(topo_node_sqlite_node_update_verified(db, 8, "rejected", key, key, 101, 1) < 0); + assert(sqlite3_exec(db, "DROP TRIGGER reject_node; CREATE TRIGGER reject_node BEFORE UPDATE ON nodes " + "BEGIN SELECT RAISE(ABORT,'test update rejection'); END", NULL, NULL, NULL) == SQLITE_OK); + assert(topo_node_sqlite_node_update_verified(db, 7, "rejected", key, key, 101, 1) < 0); + sqlite3_stmt* st = NULL; + assert(sqlite3_prepare_v2(db, "SELECT name,update_ts FROM nodes ORDER BY node_id", -1, &st, NULL) == SQLITE_OK); + assert(sqlite3_step(st) == SQLITE_ROW && !strcmp((const char*)sqlite3_column_text(st, 0), "before")); + assert(sqlite3_column_int64(st, 1) == 100 && sqlite3_step(st) == SQLITE_DONE); sqlite3_finalize(st); + assert(sqlite3_exec(db, "DROP TRIGGER reject_node", NULL, NULL, NULL) == SQLITE_OK); + assert(!topo_node_sqlite_node_update_verified(db, 7, "after", key, key, 101, 1)); + assert(sqlite3_prepare_v2(db, "SELECT name,update_ts FROM nodes WHERE node_id=7", -1, &st, NULL) == SQLITE_OK); + assert(sqlite3_step(st) == SQLITE_ROW && !strcmp((const char*)sqlite3_column_text(st, 0), "after")); + assert(sqlite3_column_int64(st, 1) == 101); sqlite3_finalize(st); + assert(sqlite3_exec(db, "DROP TABLE nodes", NULL, NULL, NULL) == SQLITE_OK); + assert(topo_node_sqlite_node_update_verified(db, 7, "no table", key, key, 102, 1) < 0); + sqlite3_close(db); + DEBUG_INFO(DEBUG_CATEGORY_BGP, "[PASS] verified node writes: insert/update, rejected writes, prepare failure and stored state"); +} + static void test_compact_addresses(struct UTUN_INSTANCE *inst) { test_zero_port_schema(); sqlite3 *saved_db = inst->topo_sqlite_db; @@ -239,6 +265,7 @@ int main(void) { inst.node_id = sc_derive_node_id_from_pubkey(inst.my_keys.public_key); assert(sqlite3_open(":memory:", &inst.topo_sqlite_db) == SQLITE_OK); assert(topo_node_sqlite_init(inst.topo_sqlite_db) == 0); + test_verified_write(); test_compact_addresses(&inst); struct TOPO_NODE* first = record(&inst, 100, "old", 1);