فهرست منبع

feat: Add migration for keyset_id as foreign key in SQLite database (#634)

thesimplekid 1 ماه پیش
والد
کامیت
467cc0a027

+ 6 - 0
crates/cdk-common/src/database/mod.rs

@@ -37,4 +37,10 @@ pub enum Error {
     /// Attempt to update state of spent proof
     #[error("Attempt to update state of spent proof")]
     AttemptUpdateSpentProof,
+    /// Proof not found
+    #[error("Proof not found")]
+    ProofNotFound,
+    /// Invalid keyset
+    #[error("Unknown or invalid keyset")]
+    InvalidKeysetId,
 }

+ 6 - 0
crates/cdk-sqlite/src/mint/error.rs

@@ -50,6 +50,12 @@ pub enum Error {
     /// Unknown quote TTL
     #[error("Unknown quote TTL")]
     UnknownQuoteTTL,
+    /// Proof not found
+    #[error("Proof not found")]
+    ProofNotFound,
+    /// Invalid keyset ID
+    #[error("Invalid keyset ID")]
+    InvalidKeysetId,
 }
 
 impl From<Error> for cdk_common::database::Error {

+ 51 - 0
crates/cdk-sqlite/src/mint/migrations/20250307213652_keyset_id_as_foreign_key.sql

@@ -0,0 +1,51 @@
+-- Add foreign key constraints for keyset_id in SQLite
+-- SQLite requires recreating tables to add foreign keys
+
+-- First, ensure we have the right schema information
+PRAGMA foreign_keys = OFF;
+
+-- Create new proof table with foreign key constraint
+CREATE TABLE proof_new (
+    y BLOB PRIMARY KEY,
+    amount INTEGER NOT NULL,
+    keyset_id TEXT NOT NULL REFERENCES keyset(id),
+    secret TEXT NOT NULL,
+    c BLOB NOT NULL,
+    witness TEXT,
+    state TEXT CHECK (state IN ('SPENT', 'PENDING', 'UNSPENT', 'RESERVED', 'UNKNOWN')) NOT NULL,
+    quote_id TEXT
+);
+
+-- Copy data from old proof table to new one
+INSERT INTO proof_new SELECT * FROM proof;
+
+-- Create new blind_signature table with foreign key constraint
+CREATE TABLE blind_signature_new (
+    y BLOB PRIMARY KEY,
+    amount INTEGER NOT NULL,
+    keyset_id TEXT NOT NULL REFERENCES keyset(id),
+    c BLOB NOT NULL,
+    dleq_e TEXT,
+    dleq_s TEXT,
+    quote_id TEXT
+);
+
+-- Copy data from old blind_signature table to new one
+INSERT INTO blind_signature_new SELECT * FROM blind_signature;
+
+-- Drop old tables
+DROP TABLE IF EXISTS proof;
+DROP TABLE IF EXISTS blind_signature;
+
+-- Rename new tables to original names
+ALTER TABLE proof_new RENAME TO proof;
+ALTER TABLE blind_signature_new RENAME TO blind_signature;
+
+-- Recreate all indexes
+CREATE INDEX IF NOT EXISTS proof_keyset_id_index ON proof(keyset_id);
+CREATE INDEX IF NOT EXISTS state_index ON proof(state);
+CREATE INDEX IF NOT EXISTS secret_index ON proof(secret);
+CREATE INDEX IF NOT EXISTS blind_signature_keyset_id_index ON blind_signature(keyset_id);
+
+-- Re-enable foreign keys
+PRAGMA foreign_keys = ON;

+ 59 - 10
crates/cdk-sqlite/src/mint/mod.rs

@@ -855,9 +855,9 @@ FROM keyset;
     async fn add_proofs(&self, proofs: Proofs, quote_id: Option<Uuid>) -> Result<(), Self::Err> {
         let mut transaction = self.pool.begin().await.map_err(Error::from)?;
         for proof in proofs {
-            if let Err(err) = sqlx::query(
+            let result = sqlx::query(
                 r#"
-INSERT INTO proof
+INSERT OR IGNORE INTO proof
 (y, amount, keyset_id, secret, c, witness, state, quote_id)
 VALUES (?, ?, ?, ?, ?, ?, ?, ?);
         "#,
@@ -871,10 +871,25 @@ VALUES (?, ?, ?, ?, ?, ?, ?, ?);
             .bind("UNSPENT")
             .bind(quote_id.map(|q| q.hyphenated()))
             .execute(&mut transaction)
-            .await
-            .map_err(Error::from)
-            {
-                tracing::debug!("Attempting to add known proof. Skipping.... {:?}", err);
+            .await;
+
+            // We still need to check for foreign key constraint errors
+            if let Err(err) = result {
+                if let sqlx::Error::Database(db_err) = &err {
+                    if db_err.message().contains("FOREIGN KEY constraint failed") {
+                        tracing::error!(
+                            "Foreign key constraint failed when adding proof: {:?}",
+                            err
+                        );
+                        transaction.rollback().await.map_err(Error::from)?;
+                        return Err(database::Error::InvalidKeysetId);
+                    }
+                }
+
+                // For any other error, roll back and return the error
+                tracing::error!("Error adding proof: {:?}", err);
+                transaction.rollback().await.map_err(Error::from)?;
+                return Err(Error::from(err).into());
             }
         }
         transaction.commit().await.map_err(Error::from)?;
@@ -1077,7 +1092,7 @@ WHERE keyset_id=?;
             "?,".repeat(ys.len()).trim_end_matches(',')
         );
 
-        let mut current_states = ys
+        let rows = ys
             .iter()
             .fold(sqlx::query(&sql), |query, y| {
                 query.bind(y.to_bytes().to_vec())
@@ -1087,7 +1102,16 @@ WHERE keyset_id=?;
             .map_err(|err| {
                 tracing::error!("SQLite could not get state of proof: {err:?}");
                 Error::SQLX(err)
-            })?
+            })?;
+
+        // Check if all proofs exist
+        if rows.len() != ys.len() {
+            transaction.rollback().await.map_err(Error::from)?;
+            tracing::warn!("Attempted to update state of non-existent proof");
+            return Err(database::Error::ProofNotFound);
+        }
+
+        let mut current_states = rows
             .into_iter()
             .map(|row| {
                 PublicKey::from_slice(row.get("y"))
@@ -1694,6 +1718,7 @@ fn sqlite_row_to_melt_request(row: SqliteRow) -> Result<(MeltBolt11Request<Uuid>
 
 #[cfg(test)]
 mod tests {
+    use cdk_common::mint::MintKeySetInfo;
     use cdk_common::Amount;
 
     use super::*;
@@ -1702,8 +1727,20 @@ mod tests {
     async fn test_remove_spent_proofs() {
         let db = memory::empty().await.unwrap();
 
-        // Create some test proofs
+        // Create a keyset and add it to the database
         let keyset_id = Id::from_str("00916bbf7ef91a36").unwrap();
+        let keyset_info = MintKeySetInfo {
+            id: keyset_id.clone(),
+            unit: CurrencyUnit::Sat,
+            active: true,
+            valid_from: 0,
+            valid_to: None,
+            derivation_path: bitcoin::bip32::DerivationPath::from_str("m/0'/0'/0'").unwrap(),
+            derivation_path_index: Some(0),
+            max_order: 32,
+            input_fee_ppk: 0,
+        };
+        db.add_keyset_info(keyset_info).await.unwrap();
 
         let proofs = vec![
             Proof {
@@ -1758,8 +1795,20 @@ mod tests {
     async fn test_update_spent_proofs() {
         let db = memory::empty().await.unwrap();
 
-        // Create some test proofs
+        // Create a keyset and add it to the database
         let keyset_id = Id::from_str("00916bbf7ef91a36").unwrap();
+        let keyset_info = MintKeySetInfo {
+            id: keyset_id.clone(),
+            unit: CurrencyUnit::Sat,
+            active: true,
+            valid_from: 0,
+            valid_to: None,
+            derivation_path: bitcoin::bip32::DerivationPath::from_str("m/0'/0'/0'").unwrap(),
+            derivation_path_index: Some(0),
+            max_order: 32,
+            input_fee_ppk: 0,
+        };
+        db.add_keyset_info(keyset_info).await.unwrap();
 
         let proofs = vec![
             Proof {