Skip to content

Commit

Permalink
feat: Add migration for keyset_id as foreign key in SQLite database
Browse files Browse the repository at this point in the history
  • Loading branch information
thesimplekid committed Mar 6, 2025
1 parent 5a7362c commit 556c384
Show file tree
Hide file tree
Showing 4 changed files with 133 additions and 9 deletions.
6 changes: 6 additions & 0 deletions crates/cdk-common/src/database/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 changes: 6 additions & 0 deletions crates/cdk-sqlite/src/mint/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
75 changes: 66 additions & 9 deletions crates/cdk-sqlite/src/mint/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -855,7 +855,7 @@ 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
(y, amount, keyset_id, secret, c, witness, state, quote_id)
Expand All @@ -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)?;
Expand Down Expand Up @@ -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())
Expand All @@ -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"))
Expand Down Expand Up @@ -1694,6 +1726,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::*;
Expand All @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down

0 comments on commit 556c384

Please sign in to comment.