فهرست منبع

Fixed race conditions

The first time keys are fetched from the mint, they do not yet exist in the
wallet database; therefore, the counter is not updated correctly, because the
code performs an update that expects the keyset row to exist.

To solve this issue, I introduced a new keyset_counter table that will upsert
the data, even if the proper keyset info is pulled later.
Cesar Rodas 1 هفته پیش
والد
کامیت
162b630aa7

+ 12 - 0
crates/cdk-sql-common/src/stmt.rs

@@ -104,6 +104,18 @@ pub fn split_sql_parts(input: &str) -> Result<Vec<SqlPart>, SqlParseError> {
                 }
             }
 
+            '-' => {
+                current.push(chars.next().unwrap());
+                if chars.peek() == Some(&'-') {
+                    while let Some(&next) = chars.peek() {
+                        current.push(chars.next().unwrap());
+                        if next == '\n' {
+                            break;
+                        }
+                    }
+                }
+            }
+
             ':' => {
                 // Flush current raw SQL
                 if !current.is_empty() {

+ 15 - 0
crates/cdk-sql-common/src/wallet/migrations/postgres/20251111000000_keyset_counter_table.sql

@@ -0,0 +1,15 @@
+-- Create dedicated keyset_counter table without foreign keys
+-- This table tracks the counter for each keyset independently
+CREATE TABLE IF NOT EXISTS keyset_counter (
+    keyset_id TEXT PRIMARY KEY,
+    counter INTEGER NOT NULL DEFAULT 0
+);
+
+-- Migrate existing counter values from keyset table
+INSERT INTO keyset_counter (keyset_id, counter)
+SELECT id, counter
+FROM keyset
+WHERE counter > 0;
+
+-- Drop the counter column from keyset table
+ALTER TABLE keyset DROP COLUMN counter;

+ 36 - 0
crates/cdk-sql-common/src/wallet/migrations/sqlite/20251111000000_keyset_counter_table.sql

@@ -0,0 +1,36 @@
+-- Create dedicated keyset_counter table without foreign keys
+-- This table tracks the counter for each keyset independently
+CREATE TABLE IF NOT EXISTS keyset_counter (
+    keyset_id TEXT PRIMARY KEY,
+    counter INTEGER NOT NULL DEFAULT 0
+);
+
+-- Migrate existing counter values from keyset table
+INSERT INTO keyset_counter (keyset_id, counter)
+SELECT id, counter
+FROM keyset
+WHERE counter > 0;
+
+-- Drop the counter column from keyset table (SQLite requires table recreation)
+-- Step 1: Create new keyset table without counter column
+CREATE TABLE keyset_new (
+    id TEXT PRIMARY KEY,
+    mint_url TEXT NOT NULL,
+    keyset_u32 INTEGER,
+    unit TEXT NOT NULL,
+    active BOOL NOT NULL,
+    input_fee_ppk INTEGER,
+    final_expiry INTEGER DEFAULT NULL,
+    FOREIGN KEY(mint_url) REFERENCES mint(mint_url) ON UPDATE CASCADE ON DELETE CASCADE
+);
+
+-- Step 2: Copy data from old keyset table (excluding counter)
+INSERT INTO keyset_new (id, keyset_u32, mint_url, unit, active, input_fee_ppk, final_expiry)
+SELECT id, keyset_u32, mint_url, unit, active, input_fee_ppk, final_expiry
+FROM keyset;
+
+-- Step 3: Drop old keyset table
+DROP TABLE keyset;
+
+-- Step 4: Rename new table to keyset
+ALTER TABLE keyset_new RENAME TO keyset;

+ 10 - 9
crates/cdk-sql-common/src/wallet/mod.rs

@@ -917,16 +917,16 @@ ON CONFLICT(id) DO UPDATE SET
         let conn = self.pool.get().map_err(|e| Error::Database(Box::new(e)))?;
         let tx = ConnectionWithTransaction::new(conn).await?;
 
-        // Lock the row and get current counter
+        // Lock the row and get current counter from keyset_counter table
         let current_counter = query(
             r#"
             SELECT counter
-            FROM keyset
-            WHERE id=:id
+            FROM keyset_counter
+            WHERE keyset_id=:keyset_id
             FOR UPDATE
             "#,
         )?
-        .bind("id", keyset_id.to_string())
+        .bind("keyset_id", keyset_id.to_string())
         .pluck(&tx)
         .await?
         .map(|n| Ok::<_, Error>(column_as_number!(n)))
@@ -935,16 +935,17 @@ ON CONFLICT(id) DO UPDATE SET
 
         let new_counter = current_counter + count;
 
-        // Update with the new counter value
+        // Upsert the new counter value
         query(
             r#"
-            UPDATE keyset
-            SET counter=:new_counter
-            WHERE id=:id
+            INSERT INTO keyset_counter (keyset_id, counter)
+            VALUES (:keyset_id, :new_counter)
+            ON CONFLICT(keyset_id) DO UPDATE SET
+                counter = excluded.counter
             "#,
         )?
+        .bind("keyset_id", keyset_id.to_string())
         .bind("new_counter", new_counter)
-        .bind("id", keyset_id.to_string())
         .execute(&tx)
         .await?;