Skip to content

Commit

Permalink
Merge pull request #630 from thesimplekid/db_check_on_delete_proofs
Browse files Browse the repository at this point in the history
Db check on delete proofs
  • Loading branch information
thesimplekid authored Mar 6, 2025
2 parents a394145 + d41d3a7 commit 393c95e
Show file tree
Hide file tree
Showing 6 changed files with 375 additions and 39 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ lightning-invoice = { version = "0.32.0", features = ["serde", "std"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
thiserror = { version = "1" }
tokio = { version = "1", default-features = false }
tokio = { version = "1", default-features = false, features = ["rt", "macros", "test-util"] }
tokio-util = { version = "0.7.11", default-features = false }
tower-http = { version = "0.6.1", features = ["compression-full", "decompression-full", "cors", "trace"] }
tokio-tungstenite = { version = "0.26.0", default-features = false }
Expand Down
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 @@ -31,4 +31,10 @@ pub enum Error {
/// Unknown Quote
#[error("Unknown Quote")]
UnknownQuote,
/// Attempt to remove spent proof
#[error("Attempt to remove spent proof")]
AttemptRemoveSpentProof,
/// Attempt to update state of spent proof
#[error("Attempt to update state of spent proof")]
AttemptUpdateSpentProof,
}
4 changes: 4 additions & 0 deletions crates/cdk-redb/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,7 @@ serde.workspace = true
serde_json.workspace = true
lightning-invoice.workspace = true
uuid.workspace = true

[dev-dependencies]
tempfile = "3.17.1"
tokio.workspace = true
207 changes: 181 additions & 26 deletions crates/cdk-redb/src/mint/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! SQLite Storage for CDK
use std::cmp::Ordering;
use std::collections::HashMap;
use std::collections::{HashMap, HashSet};
use std::path::Path;
use std::str::FromStr;
use std::sync::Arc;
Expand Down Expand Up @@ -558,22 +558,36 @@ impl MintDatabase for MintRedbDatabase {
) -> Result<(), Self::Err> {
let write_txn = self.db.begin_write().map_err(Error::from)?;

{
let mut proofs_table = write_txn.open_table(PROOFS_TABLE).map_err(Error::from)?;

for y in ys {
proofs_table.remove(&y.to_bytes()).map_err(Error::from)?;
}
}
let mut states: HashSet<State> = HashSet::new();

{
let mut proof_state_table = write_txn
.open_table(PROOFS_STATE_TABLE)
.map_err(Error::from)?;
for y in ys {
proof_state_table
let state = proof_state_table
.remove(&y.to_bytes())
.map_err(Error::from)?;

if let Some(state) = state {
let state: State = serde_json::from_str(state.value()).map_err(Error::from)?;

states.insert(state);
}
}
}

if states.contains(&State::Spent) {
tracing::warn!("Db attempted to remove spent proof");
write_txn.abort().map_err(Error::from)?;
return Err(Self::Err::AttemptRemoveSpentProof);
}

{
let mut proofs_table = write_txn.open_table(PROOFS_TABLE).map_err(Error::from)?;

for y in ys {
proofs_table.remove(&y.to_bytes()).map_err(Error::from)?;
}
}

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

let mut states = Vec::with_capacity(ys.len());

let state_str = serde_json::to_string(&proofs_state).map_err(Error::from)?;

{
let mut table = write_txn
let table = write_txn
.open_table(PROOFS_STATE_TABLE)
.map_err(Error::from)?;

for y in ys {
let current_state;
{
match table.get(y.to_bytes()).map_err(Error::from)? {
{
// First collect current states
for y in ys {
let current_state = match table.get(y.to_bytes()).map_err(Error::from)? {
Some(state) => {
current_state =
Some(serde_json::from_str(state.value()).map_err(Error::from)?)
Some(serde_json::from_str(state.value()).map_err(Error::from)?)
}
None => current_state = None,
}
None => None,
};
states.push(current_state);
}
states.push(current_state);
}
}

// Check if any proofs are spent
if states.iter().any(|state| *state == Some(State::Spent)) {
write_txn.abort().map_err(Error::from)?;
return Err(database::Error::AttemptUpdateSpentProof);
}

for (y, current_state) in ys.iter().zip(&states) {
if current_state != &Some(State::Spent) {
{
let mut table = write_txn
.open_table(PROOFS_STATE_TABLE)
.map_err(Error::from)?;
{
// If no proofs are spent, proceed with update
let state_str = serde_json::to_string(&proofs_state).map_err(Error::from)?;
for y in ys {
table
.insert(y.to_bytes(), state_str.as_str())
.map_err(Error::from)?;
}
}
}

write_txn.commit().map_err(Error::from)?;

Ok(states)
Expand Down Expand Up @@ -924,3 +945,137 @@ impl MintDatabase for MintRedbDatabase {
Err(Error::UnknownQuoteTTL.into())
}
}

#[cfg(test)]
mod tests {
use cdk_common::secret::Secret;
use cdk_common::{Amount, SecretKey};
use tempfile::tempdir;

use super::*;

#[tokio::test]
async fn test_remove_spent_proofs() {
let tmp_dir = tempdir().unwrap();

let db = MintRedbDatabase::new(&tmp_dir.path().join("mint.redb")).unwrap();
// Create some test proofs
let keyset_id = Id::from_str("00916bbf7ef91a36").unwrap();

let proofs = vec![
Proof {
amount: Amount::from(100),
keyset_id: keyset_id.clone(),
secret: Secret::generate(),
c: SecretKey::generate().public_key(),
witness: None,
dleq: None,
},
Proof {
amount: Amount::from(200),
keyset_id: keyset_id.clone(),
secret: Secret::generate(),
c: SecretKey::generate().public_key(),
witness: None,
dleq: None,
},
];

// Add proofs to database
db.add_proofs(proofs.clone(), None).await.unwrap();

// Mark one proof as spent
db.update_proofs_states(&[proofs[0].y().unwrap()], State::Spent)
.await
.unwrap();

db.update_proofs_states(&[proofs[1].y().unwrap()], State::Unspent)
.await
.unwrap();

// Try to remove both proofs - should fail because one is spent
let result = db
.remove_proofs(&[proofs[0].y().unwrap(), proofs[1].y().unwrap()], None)
.await;

assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
database::Error::AttemptRemoveSpentProof
));

// Verify both proofs still exist
let states = db
.get_proofs_states(&[proofs[0].y().unwrap(), proofs[1].y().unwrap()])
.await
.unwrap();

assert_eq!(states.len(), 2);
assert_eq!(states[0], Some(State::Spent));
assert_eq!(states[1], Some(State::Unspent));
}

#[tokio::test]
async fn test_update_spent_proofs() {
let tmp_dir = tempdir().unwrap();

let db = MintRedbDatabase::new(&tmp_dir.path().join("mint.redb")).unwrap();
// Create some test proofs
let keyset_id = Id::from_str("00916bbf7ef91a36").unwrap();

let proofs = vec![
Proof {
amount: Amount::from(100),
keyset_id: keyset_id.clone(),
secret: Secret::generate(),
c: SecretKey::generate().public_key(),
witness: None,
dleq: None,
},
Proof {
amount: Amount::from(200),
keyset_id: keyset_id.clone(),
secret: Secret::generate(),
c: SecretKey::generate().public_key(),
witness: None,
dleq: None,
},
];

// Add proofs to database
db.add_proofs(proofs.clone(), None).await.unwrap();

// Mark one proof as spent
db.update_proofs_states(&[proofs[0].y().unwrap()], State::Spent)
.await
.unwrap();

db.update_proofs_states(&[proofs[1].y().unwrap()], State::Unspent)
.await
.unwrap();

// Mark one proof as spent
let result = db
.update_proofs_states(
&[proofs[0].y().unwrap(), proofs[1].y().unwrap()],
State::Unspent,
)
.await;

assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
database::Error::AttemptUpdateSpentProof
));

// Verify both proofs still exist
let states = db
.get_proofs_states(&[proofs[0].y().unwrap(), proofs[1].y().unwrap()])
.await
.unwrap();

assert_eq!(states.len(), 2);
assert_eq!(states[0], Some(State::Spent));
assert_eq!(states[1], Some(State::Unspent));
}
}
Loading

0 comments on commit 393c95e

Please sign in to comment.