From 6a6ad5c612ec291eb49b4ab814f2d58b34a8fc73 Mon Sep 17 00:00:00 2001 From: evgeny Date: Tue, 27 Oct 2026 04:19:14 +0100 Subject: [PATCH] Store channel metadata and peer schema atomically with checked SQL operations --- src/routing_layer/topo_node_sqlite.c | 112 +++++++++++++++++---------- src/routing_layer/topo_node_sqlite.h | 3 + 2 files changed, 74 insertions(+), 41 deletions(-) diff --git a/src/routing_layer/topo_node_sqlite.c b/src/routing_layer/topo_node_sqlite.c index 15665bd4..fce210f9 100644 --- a/src/routing_layer/topo_node_sqlite.c +++ b/src/routing_layer/topo_node_sqlite.c @@ -491,9 +491,10 @@ int topo_node_sqlite_member_local_init(sqlite3* db, const char* channel_id) { const char* column = (const char*)sqlite3_column_text(st, 1); if (column && !strcmp(column, "radio_muted")) has_radio_muted = 1; } - sqlite3_finalize(st); - if (rc != SQLITE_DONE) { - DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "radio: read member schema ch=%s: %s", channel_id, sqlite3_errmsg(db)); return -1; + int final = sqlite3_finalize(st); + if (rc != SQLITE_DONE || final != SQLITE_OK) { + DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "radio: read member schema ch=%s step=%d finalize=%d: %s", + channel_id, rc, final, sqlite3_errmsg(db)); return -1; } if (!has_radio_muted) { snprintf(sql, sizeof(sql), "ALTER TABLE \"%s\" ADD COLUMN radio_muted INTEGER NOT NULL DEFAULT 0", peers_tbl); @@ -510,7 +511,15 @@ int topo_node_sqlite_channel_put(sqlite3* db, const char* channel_id, const uint8_t* x25519_pub, const uint8_t* x25519_priv, const uint8_t* ed25519_pub, const uint8_t* ed25519_priv, const uint8_t* signature) { - if (!db || !channel_id || !name || !x25519_pub || !ed25519_pub || !signature) return -1; + if (!db || !channel_id || !name || !x25519_pub || !ed25519_pub || !signature) { + DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "channel put: invalid database/description arguments"); return -1; + } + int rc = sqlite3_exec(db, "SAVEPOINT channel_put", NULL, NULL, NULL); + if (rc != SQLITE_OK) { + DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "channel put: begin channel=%s rc=%d: %s", channel_id, rc, sqlite3_errmsg(db)); return -1; + } + sqlite3_stmt* stmt = NULL; + const char* stage = "allocate peers schema"; char peers_tbl[80]; peers_table_name(channel_id, peers_tbl, sizeof(peers_tbl)); @@ -548,38 +557,36 @@ int topo_node_sqlite_channel_put(sqlite3* db, const char* channel_id, int ddl_sz = snprintf(NULL, 0, ddl_tmpl, peers_tbl); char* ddl = u_malloc(ddl_sz + 1); - if (ddl) { - snprintf(ddl, ddl_sz + 1, ddl_tmpl, peers_tbl); - char* err = NULL; - sqlite3_exec(db, ddl, NULL, NULL, &err); - if (err) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "topo_node_sqlite_channel_put peers: %s", err); sqlite3_free(err); } - u_free(ddl); - } + if (!ddl) { rc = SQLITE_NOMEM; goto error; } + snprintf(ddl, ddl_sz + 1, ddl_tmpl, peers_tbl); stage = "create peers schema"; + rc = sqlite3_exec(db, ddl, NULL, NULL, NULL); u_free(ddl); + if (rc != SQLITE_OK) goto error; - if (topo_node_sqlite_member_local_init(db, channel_id) < 0) return -1; + stage = "local member schema"; + if (topo_node_sqlite_member_local_init(db, channel_id) < 0) { rc = SQLITE_ERROR; goto error; } int idx_sz = snprintf(NULL, 0, "CREATE INDEX IF NOT EXISTS \"idx_%s_type_rtt\" ON \"%s\" (node_type, node_RTT)", peers_tbl, peers_tbl); char* idx_sql = u_malloc(idx_sz + 1); - if (idx_sql) { - snprintf(idx_sql, idx_sz + 1, - "CREATE INDEX IF NOT EXISTS \"idx_%s_type_rtt\" ON \"%s\" (node_type, node_RTT)", peers_tbl, peers_tbl); - sqlite3_exec(db, idx_sql, NULL, NULL, NULL); - u_free(idx_sql); - } + stage = "allocate type index"; + if (!idx_sql) { rc = SQLITE_NOMEM; goto error; } + snprintf(idx_sql, idx_sz + 1, + "CREATE INDEX IF NOT EXISTS \"idx_%s_type_rtt\" ON \"%s\" (node_type, node_RTT)", peers_tbl, peers_tbl); + stage = "create type index"; rc = sqlite3_exec(db, idx_sql, NULL, NULL, NULL); u_free(idx_sql); + if (rc != SQLITE_OK) goto error; int sidx_sz = snprintf(NULL, 0, "CREATE INDEX IF NOT EXISTS \"idx_%s_storage\" ON \"%s\" (storage)", peers_tbl, peers_tbl); char* sidx_sql = u_malloc(sidx_sz + 1); - if (sidx_sql) { - snprintf(sidx_sql, sidx_sz + 1, - "CREATE INDEX IF NOT EXISTS \"idx_%s_storage\" ON \"%s\" (storage)", peers_tbl, peers_tbl); - sqlite3_exec(db, sidx_sql, NULL, NULL, NULL); - u_free(sidx_sql); - } + stage = "allocate storage index"; + if (!sidx_sql) { rc = SQLITE_NOMEM; goto error; } + snprintf(sidx_sql, sidx_sz + 1, + "CREATE INDEX IF NOT EXISTS \"idx_%s_storage\" ON \"%s\" (storage)", peers_tbl, peers_tbl); + stage = "create storage index"; rc = sqlite3_exec(db, sidx_sql, NULL, NULL, NULL); u_free(sidx_sql); + if (rc != SQLITE_OK) goto error; - sqlite3_stmt* stmt = NULL; - if (sqlite3_prepare_v2(db, + stage = "prepare channel"; + rc = sqlite3_prepare_v2(db, "INSERT INTO channels(channel_id, name, owner_node_id," " x25519_pubkey, x25519_privkey, ed25519_pubkey, ed25519_privkey, signature)" " VALUES(?,?,?,?,?,?,?,?) ON CONFLICT(channel_id) DO UPDATE SET" @@ -587,22 +594,45 @@ int topo_node_sqlite_channel_put(sqlite3* db, const char* channel_id, " x25519_privkey=COALESCE(excluded.x25519_privkey,channels.x25519_privkey)," " ed25519_privkey=COALESCE(excluded.ed25519_privkey,channels.ed25519_privkey)" " WHERE channels.x25519_pubkey=excluded.x25519_pubkey AND channels.ed25519_pubkey=excluded.ed25519_pubkey", - -1, &stmt, NULL) != SQLITE_OK) return -1; - sqlite3_bind_text(stmt, 1, channel_id, -1, SQLITE_STATIC); - sqlite3_bind_text(stmt, 2, name, -1, SQLITE_STATIC); - sqlite3_bind_int64(stmt, 3, (sqlite3_int64)owner_node_id); - sqlite3_bind_blob(stmt, 4, x25519_pub, 32, SQLITE_STATIC); - if (x25519_priv) sqlite3_bind_blob(stmt, 5, x25519_priv, 32, SQLITE_STATIC); - else sqlite3_bind_null(stmt, 5); - sqlite3_bind_blob(stmt, 6, ed25519_pub, 32, SQLITE_STATIC); - if (ed25519_priv) sqlite3_bind_blob(stmt, 7, ed25519_priv, 32, SQLITE_STATIC); - else sqlite3_bind_null(stmt, 7); - sqlite3_bind_blob(stmt, 8, signature, 64, SQLITE_STATIC); - int rc = sqlite3_step(stmt); sqlite3_finalize(stmt); - if (rc == SQLITE_DONE && !sqlite3_changes(db)) { - DEBUG_WARN(DEBUG_CATEGORY_CHAT, "channel: rejected identity change channel=%s", channel_id); return -1; + -1, &stmt, NULL); + if (rc == SQLITE_OK) { stage = "bind channel"; rc = sqlite3_bind_text(stmt, 1, channel_id, -1, SQLITE_STATIC); } + if (rc == SQLITE_OK) { stage = "bind name"; rc = sqlite3_bind_text(stmt, 2, name, -1, SQLITE_STATIC); } + if (rc == SQLITE_OK) { stage = "bind owner"; rc = sqlite3_bind_int64(stmt, 3, (sqlite3_int64)owner_node_id); } + if (rc == SQLITE_OK) { stage = "bind X25519 public"; rc = sqlite3_bind_blob(stmt, 4, x25519_pub, 32, SQLITE_STATIC); } + if (rc == SQLITE_OK) { + stage = "bind X25519 private"; + rc = x25519_priv ? sqlite3_bind_blob(stmt, 5, x25519_priv, 32, SQLITE_STATIC) : sqlite3_bind_null(stmt, 5); } - return rc == SQLITE_DONE ? 0 : -1; + if (rc == SQLITE_OK) { stage = "bind Ed25519 public"; rc = sqlite3_bind_blob(stmt, 6, ed25519_pub, 32, SQLITE_STATIC); } + if (rc == SQLITE_OK) { + stage = "bind Ed25519 private"; + rc = ed25519_priv ? sqlite3_bind_blob(stmt, 7, ed25519_priv, 32, SQLITE_STATIC) : sqlite3_bind_null(stmt, 7); + } + if (rc == SQLITE_OK) { stage = "bind signature"; rc = sqlite3_bind_blob(stmt, 8, signature, 64, SQLITE_STATIC); } + if (rc == SQLITE_OK) { stage = "write channel"; rc = sqlite3_step(stmt); } + if (rc != SQLITE_DONE) goto error; + stage = "channel row count"; + if (sqlite3_changes(db) != 1) { rc = SQLITE_CONSTRAINT; goto error; } + stage = "finalize channel"; rc = sqlite3_finalize(stmt); stmt = NULL; + if (rc != SQLITE_OK) goto error; + stage = "release channel"; rc = sqlite3_exec(db, "RELEASE channel_put", NULL, NULL, NULL); + if (rc == SQLITE_OK) return 0; +error: + DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "channel put: channel=%s stage=%s rc=%d (%s) db=%s", + channel_id, stage, rc, sqlite3_errstr(rc), sqlite3_errmsg(db)); + { + int final = sqlite3_finalize(stmt); + if (final != SQLITE_OK) DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "channel put: cleanup finalize channel=%s rc=%d", channel_id, final); + } + if (sqlite3_get_autocommit(db)) return -1; + rc = sqlite3_exec(db, "ROLLBACK TO channel_put", NULL, NULL, NULL); + if (rc != SQLITE_OK) { + DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "channel put: rollback channel=%s rc=%d; transaction must be rolled back or closed", channel_id, rc); + return -1; + } + rc = sqlite3_exec(db, "RELEASE channel_put", NULL, NULL, NULL); + if (rc != SQLITE_OK) DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "channel put: rollback release channel=%s rc=%d", channel_id, rc); + return -1; } /* Удаляет канал и производные таблицы целиком; savepoint сохраняет внешнюю транзакцию. */ diff --git a/src/routing_layer/topo_node_sqlite.h b/src/routing_layer/topo_node_sqlite.h index 2b611238..e5b86dfb 100644 --- a/src/routing_layer/topo_node_sqlite.h +++ b/src/routing_layer/topo_node_sqlite.h @@ -49,6 +49,9 @@ int topo_node_sqlite_get_member_channels(sqlite3* db, uint64_t node_id, /* Создать peers_ и сохранить канал; 0/-1. Ключи — по 32, signature — 64 байта, privkey необязательны. * При обновлении публичные ключи обязаны совпасть; имя/владелец/подпись заменяются, NULL privkey сохраняет прежний. + * Свой savepoint включает peers/индексы/описание; проверяет DDL,все bind/step/changes1/finalize/release. + * Ошибка сохраняет прежние данные/схему. Внешняя транзакция допустима и не завершается. + * Отказ rollback не выполняет release:caller должен откатить/закрыть оставшуюся транзакцию. * Подпись/пары ключей не проверяет. Табличный суффикс: первые 63 символа, вне [A-Za-z0-9_] заменены на '_'. */ int topo_node_sqlite_channel_put(sqlite3* db, const char* channel_id, const char* name, uint64_t owner_node_id,