Skip to content

Commit 97b477d

Browse files
committed
Deduplicate cached Cashu proofs
Keep the in-memory proofs cache aligned with the KV store's one-proof-per-Y invariant when proofs are re-added. Replacing cached entries prevents retries or recovery replays from duplicating proofs and inflating reported balances or proof enumeration results.
1 parent dbfa033 commit 97b477d

1 file changed

Lines changed: 67 additions & 7 deletions

File tree

orange-sdk/src/trusted_wallet/cashu/cashu_store.rs

Lines changed: 67 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use std::collections::HashMap;
1+
use std::collections::{HashMap, HashSet};
22
use std::fmt::Debug;
33
use std::str::FromStr;
44
use std::sync::{Arc, RwLock};
@@ -257,6 +257,21 @@ impl CashuKvDatabase {
257257
format!("proof_{}", hex::encode(proof.y.serialize()))
258258
}
259259

260+
fn update_proofs_cache(
261+
cache: &mut Vec<ProofInfo>, added: Vec<ProofInfo>, removed_ys: Vec<PublicKey>,
262+
) {
263+
let added_ys: HashSet<_> = added.iter().map(|proof| proof.y).collect();
264+
let removed_ys: HashSet<_> = removed_ys.into_iter().collect();
265+
266+
// Match the KV store's proof_<Y> uniqueness by replacing cached entries with
267+
// the same Y before appending the updated proof data.
268+
cache.retain(|proof| !added_ys.contains(&proof.y));
269+
cache.extend(added);
270+
271+
// Remove proofs with matching Y values
272+
cache.retain(|proof| !removed_ys.contains(&proof.y));
273+
}
274+
260275
fn generate_mint_key(mint_url: &MintUrl) -> String {
261276
// Generate a deterministic hash of the mint URL for use as a key
262277
let mut hasher = DefaultHasher::new();
@@ -736,12 +751,7 @@ impl WalletDatabase<cdk::cdk_database::Error> for CashuKvDatabase {
736751
// Update cache
737752
{
738753
let mut cache = self.proofs_cache.write().unwrap();
739-
740-
// Add new proofs
741-
cache.extend(added);
742-
743-
// Remove proofs with matching Y values
744-
cache.retain(|proof| !removed_ys.contains(&proof.y));
754+
Self::update_proofs_cache(&mut cache, added, removed_ys);
745755
}
746756

747757
Ok(())
@@ -1262,3 +1272,53 @@ pub(super) async fn write_has_recovered(
12621272
.await
12631273
.map_err(TrustedError::IOError)
12641274
}
1275+
1276+
#[cfg(test)]
1277+
mod tests {
1278+
use super::*;
1279+
use cdk::Amount;
1280+
use cdk::nuts::Proof;
1281+
use cdk::secret::Secret;
1282+
1283+
fn proof_info(secret: &str, amount: u64) -> ProofInfo {
1284+
let proof = Proof::new(
1285+
Amount::from(amount),
1286+
Id::from_str("009a1f293253e41e").unwrap(),
1287+
Secret::new(secret),
1288+
PublicKey::from_str(
1289+
"024369d2d22a80ecf78f3937da9d5f30c1b9f74f0c32684d583cca0fa6a61cdcfc",
1290+
)
1291+
.unwrap(),
1292+
);
1293+
1294+
ProofInfo::new(
1295+
proof,
1296+
MintUrl::from_str("https://mint.example.com").unwrap(),
1297+
State::Unspent,
1298+
CurrencyUnit::Sat,
1299+
)
1300+
.unwrap()
1301+
}
1302+
1303+
#[test]
1304+
fn update_proofs_cache_replaces_existing_proof_with_same_y() {
1305+
let original = proof_info("same cached proof", 1);
1306+
let replacement = proof_info("same cached proof", 2);
1307+
let mut cache = vec![original.clone()];
1308+
1309+
CashuKvDatabase::update_proofs_cache(&mut cache, vec![replacement.clone()], vec![]);
1310+
1311+
assert_eq!(cache, vec![replacement]);
1312+
assert_eq!(cache.iter().filter(|proof| proof.y == original.y).count(), 1);
1313+
}
1314+
1315+
#[test]
1316+
fn update_proofs_cache_removal_wins_over_added_proof() {
1317+
let proof = proof_info("removed cached proof", 1);
1318+
let mut cache = vec![proof.clone()];
1319+
1320+
CashuKvDatabase::update_proofs_cache(&mut cache, vec![proof.clone()], vec![proof.y]);
1321+
1322+
assert!(cache.is_empty());
1323+
}
1324+
}

0 commit comments

Comments
 (0)