From 556c384ecc0209d7037dd058fda941442697b8a3 Mon Sep 17 00:00:00 2001 From: thesimplekid Date: Thu, 6 Mar 2025 15:49:46 +0000 Subject: [PATCH] feat: Add migration for keyset_id as foreign key in SQLite database --- crates/cdk-common/src/database/mod.rs | 6 ++ crates/cdk-sqlite/src/mint/error.rs | 6 ++ ...20250306154853_keyset_id_as_forign_key.sql | 55 ++++++++++++++ crates/cdk-sqlite/src/mint/mod.rs | 75 ++++++++++++++++--- 4 files changed, 133 insertions(+), 9 deletions(-) create mode 100644 crates/cdk-sqlite/src/mint/migrations/20250306154853_keyset_id_as_forign_key.sql diff --git a/crates/cdk-common/src/database/mod.rs b/crates/cdk-common/src/database/mod.rs index 06d865249..e12b0484e 100644 --- a/crates/cdk-common/src/database/mod.rs +++ b/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, } diff --git a/crates/cdk-sqlite/src/mint/error.rs b/crates/cdk-sqlite/src/mint/error.rs index d52922cfe..21d1bc65e 100644 --- a/crates/cdk-sqlite/src/mint/error.rs +++ b/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 for cdk_common::database::Error { diff --git a/crates/cdk-sqlite/src/mint/migrations/20250306154853_keyset_id_as_forign_key.sql b/crates/cdk-sqlite/src/mint/migrations/20250306154853_keyset_id_as_forign_key.sql new file mode 100644 index 000000000..31e58debf --- /dev/null +++ b/crates/cdk-sqlite/src/mint/migrations/20250306154853_keyset_id_as_forign_key.sql @@ -0,0 +1,55 @@ +-- 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 indexes for the foreign keys if they don't exist +CREATE INDEX IF NOT EXISTS proof_keyset_id_index ON proof(keyset_id); +CREATE INDEX IF NOT EXISTS blind_signature_keyset_id_index ON blind_signature(keyset_id); + +-- 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')) 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 proof; +DROP TABLE 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; diff --git a/crates/cdk-sqlite/src/mint/mod.rs b/crates/cdk-sqlite/src/mint/mod.rs index fb272d011..6998e7284 100644 --- a/crates/cdk-sqlite/src/mint/mod.rs +++ b/crates/cdk-sqlite/src/mint/mod.rs @@ -855,7 +855,7 @@ FROM keyset; async fn add_proofs(&self, proofs: Proofs, quote_id: Option) -> 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 (y, amount, keyset_id, secret, c, witness, state, quote_id) @@ -871,10 +871,33 @@ 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; + + if let Err(err) = result { + // Check if this is a foreign key constraint error (keyset_id not found) + 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); + } + } + + // If it's a unique constraint violation, it's likely a duplicate proof + if let sqlx::Error::Database(db_err) = &err { + if db_err.message().contains("UNIQUE constraint failed") { + tracing::debug!("Attempting to add known proof. Skipping.... {:?}", err); + continue; + } + } + + // 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 +1100,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 +1110,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 +1726,7 @@ fn sqlite_row_to_melt_request(row: SqliteRow) -> Result<(MeltBolt11Request #[cfg(test)] mod tests { + use cdk_common::mint::MintKeySetInfo; use cdk_common::Amount; use super::*; @@ -1702,8 +1735,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 +1803,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 {