Skip to content

Commit 467cc0a

Browse files
authored
feat: Add migration for keyset_id as foreign key in SQLite database (cashubtc#634)
1 parent 39a7b15 commit 467cc0a

File tree

4 files changed

+122
-10
lines changed

4 files changed

+122
-10
lines changed

crates/cdk-common/src/database/mod.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,10 @@ pub enum Error {
3737
/// Attempt to update state of spent proof
3838
#[error("Attempt to update state of spent proof")]
3939
AttemptUpdateSpentProof,
40+
/// Proof not found
41+
#[error("Proof not found")]
42+
ProofNotFound,
43+
/// Invalid keyset
44+
#[error("Unknown or invalid keyset")]
45+
InvalidKeysetId,
4046
}

crates/cdk-sqlite/src/mint/error.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,12 @@ pub enum Error {
5050
/// Unknown quote TTL
5151
#[error("Unknown quote TTL")]
5252
UnknownQuoteTTL,
53+
/// Proof not found
54+
#[error("Proof not found")]
55+
ProofNotFound,
56+
/// Invalid keyset ID
57+
#[error("Invalid keyset ID")]
58+
InvalidKeysetId,
5359
}
5460

5561
impl From<Error> for cdk_common::database::Error {
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
-- Add foreign key constraints for keyset_id in SQLite
2+
-- SQLite requires recreating tables to add foreign keys
3+
4+
-- First, ensure we have the right schema information
5+
PRAGMA foreign_keys = OFF;
6+
7+
-- Create new proof table with foreign key constraint
8+
CREATE TABLE proof_new (
9+
y BLOB PRIMARY KEY,
10+
amount INTEGER NOT NULL,
11+
keyset_id TEXT NOT NULL REFERENCES keyset(id),
12+
secret TEXT NOT NULL,
13+
c BLOB NOT NULL,
14+
witness TEXT,
15+
state TEXT CHECK (state IN ('SPENT', 'PENDING', 'UNSPENT', 'RESERVED', 'UNKNOWN')) NOT NULL,
16+
quote_id TEXT
17+
);
18+
19+
-- Copy data from old proof table to new one
20+
INSERT INTO proof_new SELECT * FROM proof;
21+
22+
-- Create new blind_signature table with foreign key constraint
23+
CREATE TABLE blind_signature_new (
24+
y BLOB PRIMARY KEY,
25+
amount INTEGER NOT NULL,
26+
keyset_id TEXT NOT NULL REFERENCES keyset(id),
27+
c BLOB NOT NULL,
28+
dleq_e TEXT,
29+
dleq_s TEXT,
30+
quote_id TEXT
31+
);
32+
33+
-- Copy data from old blind_signature table to new one
34+
INSERT INTO blind_signature_new SELECT * FROM blind_signature;
35+
36+
-- Drop old tables
37+
DROP TABLE IF EXISTS proof;
38+
DROP TABLE IF EXISTS blind_signature;
39+
40+
-- Rename new tables to original names
41+
ALTER TABLE proof_new RENAME TO proof;
42+
ALTER TABLE blind_signature_new RENAME TO blind_signature;
43+
44+
-- Recreate all indexes
45+
CREATE INDEX IF NOT EXISTS proof_keyset_id_index ON proof(keyset_id);
46+
CREATE INDEX IF NOT EXISTS state_index ON proof(state);
47+
CREATE INDEX IF NOT EXISTS secret_index ON proof(secret);
48+
CREATE INDEX IF NOT EXISTS blind_signature_keyset_id_index ON blind_signature(keyset_id);
49+
50+
-- Re-enable foreign keys
51+
PRAGMA foreign_keys = ON;

crates/cdk-sqlite/src/mint/mod.rs

Lines changed: 59 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -855,9 +855,9 @@ FROM keyset;
855855
async fn add_proofs(&self, proofs: Proofs, quote_id: Option<Uuid>) -> Result<(), Self::Err> {
856856
let mut transaction = self.pool.begin().await.map_err(Error::from)?;
857857
for proof in proofs {
858-
if let Err(err) = sqlx::query(
858+
let result = sqlx::query(
859859
r#"
860-
INSERT INTO proof
860+
INSERT OR IGNORE INTO proof
861861
(y, amount, keyset_id, secret, c, witness, state, quote_id)
862862
VALUES (?, ?, ?, ?, ?, ?, ?, ?);
863863
"#,
@@ -871,10 +871,25 @@ VALUES (?, ?, ?, ?, ?, ?, ?, ?);
871871
.bind("UNSPENT")
872872
.bind(quote_id.map(|q| q.hyphenated()))
873873
.execute(&mut transaction)
874-
.await
875-
.map_err(Error::from)
876-
{
877-
tracing::debug!("Attempting to add known proof. Skipping.... {:?}", err);
874+
.await;
875+
876+
// We still need to check for foreign key constraint errors
877+
if let Err(err) = result {
878+
if let sqlx::Error::Database(db_err) = &err {
879+
if db_err.message().contains("FOREIGN KEY constraint failed") {
880+
tracing::error!(
881+
"Foreign key constraint failed when adding proof: {:?}",
882+
err
883+
);
884+
transaction.rollback().await.map_err(Error::from)?;
885+
return Err(database::Error::InvalidKeysetId);
886+
}
887+
}
888+
889+
// For any other error, roll back and return the error
890+
tracing::error!("Error adding proof: {:?}", err);
891+
transaction.rollback().await.map_err(Error::from)?;
892+
return Err(Error::from(err).into());
878893
}
879894
}
880895
transaction.commit().await.map_err(Error::from)?;
@@ -1077,7 +1092,7 @@ WHERE keyset_id=?;
10771092
"?,".repeat(ys.len()).trim_end_matches(',')
10781093
);
10791094

1080-
let mut current_states = ys
1095+
let rows = ys
10811096
.iter()
10821097
.fold(sqlx::query(&sql), |query, y| {
10831098
query.bind(y.to_bytes().to_vec())
@@ -1087,7 +1102,16 @@ WHERE keyset_id=?;
10871102
.map_err(|err| {
10881103
tracing::error!("SQLite could not get state of proof: {err:?}");
10891104
Error::SQLX(err)
1090-
})?
1105+
})?;
1106+
1107+
// Check if all proofs exist
1108+
if rows.len() != ys.len() {
1109+
transaction.rollback().await.map_err(Error::from)?;
1110+
tracing::warn!("Attempted to update state of non-existent proof");
1111+
return Err(database::Error::ProofNotFound);
1112+
}
1113+
1114+
let mut current_states = rows
10911115
.into_iter()
10921116
.map(|row| {
10931117
PublicKey::from_slice(row.get("y"))
@@ -1694,6 +1718,7 @@ fn sqlite_row_to_melt_request(row: SqliteRow) -> Result<(MeltBolt11Request<Uuid>
16941718

16951719
#[cfg(test)]
16961720
mod tests {
1721+
use cdk_common::mint::MintKeySetInfo;
16971722
use cdk_common::Amount;
16981723

16991724
use super::*;
@@ -1702,8 +1727,20 @@ mod tests {
17021727
async fn test_remove_spent_proofs() {
17031728
let db = memory::empty().await.unwrap();
17041729

1705-
// Create some test proofs
1730+
// Create a keyset and add it to the database
17061731
let keyset_id = Id::from_str("00916bbf7ef91a36").unwrap();
1732+
let keyset_info = MintKeySetInfo {
1733+
id: keyset_id.clone(),
1734+
unit: CurrencyUnit::Sat,
1735+
active: true,
1736+
valid_from: 0,
1737+
valid_to: None,
1738+
derivation_path: bitcoin::bip32::DerivationPath::from_str("m/0'/0'/0'").unwrap(),
1739+
derivation_path_index: Some(0),
1740+
max_order: 32,
1741+
input_fee_ppk: 0,
1742+
};
1743+
db.add_keyset_info(keyset_info).await.unwrap();
17071744

17081745
let proofs = vec![
17091746
Proof {
@@ -1758,8 +1795,20 @@ mod tests {
17581795
async fn test_update_spent_proofs() {
17591796
let db = memory::empty().await.unwrap();
17601797

1761-
// Create some test proofs
1798+
// Create a keyset and add it to the database
17621799
let keyset_id = Id::from_str("00916bbf7ef91a36").unwrap();
1800+
let keyset_info = MintKeySetInfo {
1801+
id: keyset_id.clone(),
1802+
unit: CurrencyUnit::Sat,
1803+
active: true,
1804+
valid_from: 0,
1805+
valid_to: None,
1806+
derivation_path: bitcoin::bip32::DerivationPath::from_str("m/0'/0'/0'").unwrap(),
1807+
derivation_path_index: Some(0),
1808+
max_order: 32,
1809+
input_fee_ppk: 0,
1810+
};
1811+
db.add_keyset_info(keyset_info).await.unwrap();
17631812

17641813
let proofs = vec![
17651814
Proof {

0 commit comments

Comments
 (0)