|
1 | 1 | //! SQLite Storage for CDK
|
2 | 2 |
|
3 | 3 | use std::cmp::Ordering;
|
4 |
| -use std::collections::HashMap; |
| 4 | +use std::collections::{HashMap, HashSet}; |
5 | 5 | use std::path::Path;
|
6 | 6 | use std::str::FromStr;
|
7 | 7 | use std::sync::Arc;
|
@@ -558,22 +558,36 @@ impl MintDatabase for MintRedbDatabase {
|
558 | 558 | ) -> Result<(), Self::Err> {
|
559 | 559 | let write_txn = self.db.begin_write().map_err(Error::from)?;
|
560 | 560 |
|
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(); |
568 | 562 |
|
569 | 563 | {
|
570 | 564 | let mut proof_state_table = write_txn
|
571 | 565 | .open_table(PROOFS_STATE_TABLE)
|
572 | 566 | .map_err(Error::from)?;
|
573 | 567 | for y in ys {
|
574 |
| - proof_state_table |
| 568 | + let state = proof_state_table |
575 | 569 | .remove(&y.to_bytes())
|
576 | 570 | .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)?; |
577 | 591 | }
|
578 | 592 | }
|
579 | 593 |
|
@@ -684,37 +698,44 @@ impl MintDatabase for MintRedbDatabase {
|
684 | 698 | let write_txn = self.db.begin_write().map_err(Error::from)?;
|
685 | 699 |
|
686 | 700 | let mut states = Vec::with_capacity(ys.len());
|
687 |
| - |
688 |
| - let state_str = serde_json::to_string(&proofs_state).map_err(Error::from)?; |
689 |
| - |
690 | 701 | {
|
691 |
| - let mut table = write_txn |
| 702 | + let table = write_txn |
692 | 703 | .open_table(PROOFS_STATE_TABLE)
|
693 | 704 | .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)? { |
699 | 709 | 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)?) |
702 | 711 | }
|
703 |
| - None => current_state = None, |
704 |
| - } |
| 712 | + None => None, |
| 713 | + }; |
| 714 | + states.push(current_state); |
705 | 715 | }
|
706 |
| - states.push(current_state); |
707 | 716 | }
|
| 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 | + } |
708 | 724 |
|
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 { |
711 | 733 | table
|
712 | 734 | .insert(y.to_bytes(), state_str.as_str())
|
713 | 735 | .map_err(Error::from)?;
|
714 | 736 | }
|
715 | 737 | }
|
716 | 738 | }
|
717 |
| - |
718 | 739 | write_txn.commit().map_err(Error::from)?;
|
719 | 740 |
|
720 | 741 | Ok(states)
|
@@ -924,3 +945,137 @@ impl MintDatabase for MintRedbDatabase {
|
924 | 945 | Err(Error::UnknownQuoteTTL.into())
|
925 | 946 | }
|
926 | 947 | }
|
| 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