Skip to content

Commit 393c95e

Browse files
authored
Merge pull request #630 from thesimplekid/db_check_on_delete_proofs
Db check on delete proofs
2 parents a394145 + d41d3a7 commit 393c95e

File tree

6 files changed

+375
-39
lines changed

6 files changed

+375
-39
lines changed

Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ lightning-invoice = { version = "0.32.0", features = ["serde", "std"] }
3636
serde = { version = "1", features = ["derive"] }
3737
serde_json = "1"
3838
thiserror = { version = "1" }
39-
tokio = { version = "1", default-features = false }
39+
tokio = { version = "1", default-features = false, features = ["rt", "macros", "test-util"] }
4040
tokio-util = { version = "0.7.11", default-features = false }
4141
tower-http = { version = "0.6.1", features = ["compression-full", "decompression-full", "cors", "trace"] }
4242
tokio-tungstenite = { version = "0.26.0", default-features = false }

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

+6
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,10 @@ pub enum Error {
3131
/// Unknown Quote
3232
#[error("Unknown Quote")]
3333
UnknownQuote,
34+
/// Attempt to remove spent proof
35+
#[error("Attempt to remove spent proof")]
36+
AttemptRemoveSpentProof,
37+
/// Attempt to update state of spent proof
38+
#[error("Attempt to update state of spent proof")]
39+
AttemptUpdateSpentProof,
3440
}

crates/cdk-redb/Cargo.toml

+4
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,7 @@ serde.workspace = true
2525
serde_json.workspace = true
2626
lightning-invoice.workspace = true
2727
uuid.workspace = true
28+
29+
[dev-dependencies]
30+
tempfile = "3.17.1"
31+
tokio.workspace = true

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

+181-26
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
//! SQLite Storage for CDK
22
33
use std::cmp::Ordering;
4-
use std::collections::HashMap;
4+
use std::collections::{HashMap, HashSet};
55
use std::path::Path;
66
use std::str::FromStr;
77
use std::sync::Arc;
@@ -558,22 +558,36 @@ impl MintDatabase for MintRedbDatabase {
558558
) -> Result<(), Self::Err> {
559559
let write_txn = self.db.begin_write().map_err(Error::from)?;
560560

561-
{
562-
let mut proofs_table = write_txn.open_table(PROOFS_TABLE).map_err(Error::from)?;
563-
564-
for y in ys {
565-
proofs_table.remove(&y.to_bytes()).map_err(Error::from)?;
566-
}
567-
}
561+
let mut states: HashSet<State> = HashSet::new();
568562

569563
{
570564
let mut proof_state_table = write_txn
571565
.open_table(PROOFS_STATE_TABLE)
572566
.map_err(Error::from)?;
573567
for y in ys {
574-
proof_state_table
568+
let state = proof_state_table
575569
.remove(&y.to_bytes())
576570
.map_err(Error::from)?;
571+
572+
if let Some(state) = state {
573+
let state: State = serde_json::from_str(state.value()).map_err(Error::from)?;
574+
575+
states.insert(state);
576+
}
577+
}
578+
}
579+
580+
if states.contains(&State::Spent) {
581+
tracing::warn!("Db attempted to remove spent proof");
582+
write_txn.abort().map_err(Error::from)?;
583+
return Err(Self::Err::AttemptRemoveSpentProof);
584+
}
585+
586+
{
587+
let mut proofs_table = write_txn.open_table(PROOFS_TABLE).map_err(Error::from)?;
588+
589+
for y in ys {
590+
proofs_table.remove(&y.to_bytes()).map_err(Error::from)?;
577591
}
578592
}
579593

@@ -684,37 +698,44 @@ impl MintDatabase for MintRedbDatabase {
684698
let write_txn = self.db.begin_write().map_err(Error::from)?;
685699

686700
let mut states = Vec::with_capacity(ys.len());
687-
688-
let state_str = serde_json::to_string(&proofs_state).map_err(Error::from)?;
689-
690701
{
691-
let mut table = write_txn
702+
let table = write_txn
692703
.open_table(PROOFS_STATE_TABLE)
693704
.map_err(Error::from)?;
694-
695-
for y in ys {
696-
let current_state;
697-
{
698-
match table.get(y.to_bytes()).map_err(Error::from)? {
705+
{
706+
// First collect current states
707+
for y in ys {
708+
let current_state = match table.get(y.to_bytes()).map_err(Error::from)? {
699709
Some(state) => {
700-
current_state =
701-
Some(serde_json::from_str(state.value()).map_err(Error::from)?)
710+
Some(serde_json::from_str(state.value()).map_err(Error::from)?)
702711
}
703-
None => current_state = None,
704-
}
712+
None => None,
713+
};
714+
states.push(current_state);
705715
}
706-
states.push(current_state);
707716
}
717+
}
718+
719+
// Check if any proofs are spent
720+
if states.iter().any(|state| *state == Some(State::Spent)) {
721+
write_txn.abort().map_err(Error::from)?;
722+
return Err(database::Error::AttemptUpdateSpentProof);
723+
}
708724

709-
for (y, current_state) in ys.iter().zip(&states) {
710-
if current_state != &Some(State::Spent) {
725+
{
726+
let mut table = write_txn
727+
.open_table(PROOFS_STATE_TABLE)
728+
.map_err(Error::from)?;
729+
{
730+
// If no proofs are spent, proceed with update
731+
let state_str = serde_json::to_string(&proofs_state).map_err(Error::from)?;
732+
for y in ys {
711733
table
712734
.insert(y.to_bytes(), state_str.as_str())
713735
.map_err(Error::from)?;
714736
}
715737
}
716738
}
717-
718739
write_txn.commit().map_err(Error::from)?;
719740

720741
Ok(states)
@@ -924,3 +945,137 @@ impl MintDatabase for MintRedbDatabase {
924945
Err(Error::UnknownQuoteTTL.into())
925946
}
926947
}
948+
949+
#[cfg(test)]
950+
mod tests {
951+
use cdk_common::secret::Secret;
952+
use cdk_common::{Amount, SecretKey};
953+
use tempfile::tempdir;
954+
955+
use super::*;
956+
957+
#[tokio::test]
958+
async fn test_remove_spent_proofs() {
959+
let tmp_dir = tempdir().unwrap();
960+
961+
let db = MintRedbDatabase::new(&tmp_dir.path().join("mint.redb")).unwrap();
962+
// Create some test proofs
963+
let keyset_id = Id::from_str("00916bbf7ef91a36").unwrap();
964+
965+
let proofs = vec![
966+
Proof {
967+
amount: Amount::from(100),
968+
keyset_id: keyset_id.clone(),
969+
secret: Secret::generate(),
970+
c: SecretKey::generate().public_key(),
971+
witness: None,
972+
dleq: None,
973+
},
974+
Proof {
975+
amount: Amount::from(200),
976+
keyset_id: keyset_id.clone(),
977+
secret: Secret::generate(),
978+
c: SecretKey::generate().public_key(),
979+
witness: None,
980+
dleq: None,
981+
},
982+
];
983+
984+
// Add proofs to database
985+
db.add_proofs(proofs.clone(), None).await.unwrap();
986+
987+
// Mark one proof as spent
988+
db.update_proofs_states(&[proofs[0].y().unwrap()], State::Spent)
989+
.await
990+
.unwrap();
991+
992+
db.update_proofs_states(&[proofs[1].y().unwrap()], State::Unspent)
993+
.await
994+
.unwrap();
995+
996+
// Try to remove both proofs - should fail because one is spent
997+
let result = db
998+
.remove_proofs(&[proofs[0].y().unwrap(), proofs[1].y().unwrap()], None)
999+
.await;
1000+
1001+
assert!(result.is_err());
1002+
assert!(matches!(
1003+
result.unwrap_err(),
1004+
database::Error::AttemptRemoveSpentProof
1005+
));
1006+
1007+
// Verify both proofs still exist
1008+
let states = db
1009+
.get_proofs_states(&[proofs[0].y().unwrap(), proofs[1].y().unwrap()])
1010+
.await
1011+
.unwrap();
1012+
1013+
assert_eq!(states.len(), 2);
1014+
assert_eq!(states[0], Some(State::Spent));
1015+
assert_eq!(states[1], Some(State::Unspent));
1016+
}
1017+
1018+
#[tokio::test]
1019+
async fn test_update_spent_proofs() {
1020+
let tmp_dir = tempdir().unwrap();
1021+
1022+
let db = MintRedbDatabase::new(&tmp_dir.path().join("mint.redb")).unwrap();
1023+
// Create some test proofs
1024+
let keyset_id = Id::from_str("00916bbf7ef91a36").unwrap();
1025+
1026+
let proofs = vec![
1027+
Proof {
1028+
amount: Amount::from(100),
1029+
keyset_id: keyset_id.clone(),
1030+
secret: Secret::generate(),
1031+
c: SecretKey::generate().public_key(),
1032+
witness: None,
1033+
dleq: None,
1034+
},
1035+
Proof {
1036+
amount: Amount::from(200),
1037+
keyset_id: keyset_id.clone(),
1038+
secret: Secret::generate(),
1039+
c: SecretKey::generate().public_key(),
1040+
witness: None,
1041+
dleq: None,
1042+
},
1043+
];
1044+
1045+
// Add proofs to database
1046+
db.add_proofs(proofs.clone(), None).await.unwrap();
1047+
1048+
// Mark one proof as spent
1049+
db.update_proofs_states(&[proofs[0].y().unwrap()], State::Spent)
1050+
.await
1051+
.unwrap();
1052+
1053+
db.update_proofs_states(&[proofs[1].y().unwrap()], State::Unspent)
1054+
.await
1055+
.unwrap();
1056+
1057+
// Mark one proof as spent
1058+
let result = db
1059+
.update_proofs_states(
1060+
&[proofs[0].y().unwrap(), proofs[1].y().unwrap()],
1061+
State::Unspent,
1062+
)
1063+
.await;
1064+
1065+
assert!(result.is_err());
1066+
assert!(matches!(
1067+
result.unwrap_err(),
1068+
database::Error::AttemptUpdateSpentProof
1069+
));
1070+
1071+
// Verify both proofs still exist
1072+
let states = db
1073+
.get_proofs_states(&[proofs[0].y().unwrap(), proofs[1].y().unwrap()])
1074+
.await
1075+
.unwrap();
1076+
1077+
assert_eq!(states.len(), 2);
1078+
assert_eq!(states[0], Some(State::Spent));
1079+
assert_eq!(states[1], Some(State::Unspent));
1080+
}
1081+
}

0 commit comments

Comments
 (0)