Эх сурвалжийг харах

feat: add keyset u32 mapping migration (#926)

* feat: add keyset u32 mapping migration and duplicate handling

- Add new database migration (version 3) to include u32 representation for keysets
- Implement migration for both redb and SQL databases
- Add duplicate detection and handling for keyset entries
- Create unique index constraint for keyset_u32 column in SQL
- Update keyset storage to include u32 identifiers
- Handle backwards compatibility for existing databases

* chore: clippy

* refactor(cashu): simplify keyset ID verification logic

- Consolidate match expression into a single expression
- Use direct comparison with ensure_cdk macro
- Improve readability of keyset ID validation

* refactor(cdk): rename `fetch_keyset_keys` to `load_keyset_keys` for clarity

- Renamed `fetch_keyset_keys` to `load_keyset_keys` across multiple modules to better reflect its behavior of loading keys from local storage or fetching from mint when missing.
- Added debug logging to indicate when keys are being fetched from the mint.
- Simplified key loading logic in `update_mint_keysets` by removing redundant existence checks.

* chore: remove unused vec
thesimplekid 2 сар өмнө
parent
commit
3c4fce5c45

+ 9 - 10
crates/cashu/src/nuts/nut02.rs

@@ -445,18 +445,17 @@ pub struct KeySet {
 impl KeySet {
     /// Verify the keyset id matches keys
     pub fn verify_id(&self) -> Result<(), Error> {
-        match self.id.version {
-            KeySetVersion::Version00 => {
-                let keys_id: Id = Id::v1_from_keys(&self.keys);
+        let keys_id = match self.id.version {
+            KeySetVersion::Version00 => Id::v1_from_keys(&self.keys),
+            KeySetVersion::Version01 => Id::v2_from_data(&self.keys, &self.unit, self.final_expiry),
+        };
 
-                ensure_cdk!(keys_id == self.id, Error::IncorrectKeysetId);
-            }
-            KeySetVersion::Version01 => {
-                let keys_id: Id = Id::v2_from_data(&self.keys, &self.unit, self.final_expiry);
+        ensure_cdk!(
+            u32::from(keys_id) == u32::from(self.id),
+            Error::IncorrectKeysetId
+        );
 
-                ensure_cdk!(keys_id == self.id, Error::IncorrectKeysetId);
-            }
-        }
+        ensure_cdk!(keys_id == self.id, Error::IncorrectKeysetId);
 
         Ok(())
     }

+ 3 - 0
crates/cdk-redb/src/error.rs

@@ -64,6 +64,9 @@ pub enum Error {
     /// Unknown Database Version
     #[error("Unknown database version")]
     UnknownDatabaseVersion,
+    /// Duplicate
+    #[error("Duplicate")]
+    Duplicate,
 }
 
 impl From<Error> for cdk_common::database::Error {

+ 54 - 0
crates/cdk-redb/src/wallet/migrations.rs

@@ -1,14 +1,17 @@
 //! Wallet Migrations
+use std::collections::HashSet;
 use std::ops::Deref;
 use std::str::FromStr;
 use std::sync::Arc;
 
 use cdk_common::mint_url::MintUrl;
+use cdk_common::Id;
 use redb::{
     Database, MultimapTableDefinition, ReadableMultimapTable, ReadableTable, TableDefinition,
 };
 
 use super::Error;
+use crate::wallet::{KEYSETS_TABLE, KEYSET_U32_MAPPING, MINT_KEYS_TABLE};
 
 // <Mint_url, Info>
 const MINTS_TABLE: TableDefinition<&str, &str> = TableDefinition::new("mints_table");
@@ -16,6 +19,57 @@ const MINTS_TABLE: TableDefinition<&str, &str> = TableDefinition::new("mints_tab
 const MINT_KEYSETS_TABLE: MultimapTableDefinition<&str, &[u8]> =
     MultimapTableDefinition::new("mint_keysets");
 
+pub(crate) fn migrate_02_to_03(db: Arc<Database>) -> Result<u32, Error> {
+    let write_txn = db.begin_write().map_err(Error::from)?;
+
+    let mut duplicate = false;
+
+    {
+        let table = write_txn.open_table(MINT_KEYS_TABLE).map_err(Error::from)?;
+
+        let ids: Vec<Id> = table
+            .iter()
+            .map_err(Error::from)?
+            .flatten()
+            .flat_map(|(id, _)| Id::from_str(id.value()))
+            .collect();
+
+        let mut table = write_txn
+            .open_table(KEYSET_U32_MAPPING)
+            .map_err(Error::from)?;
+
+        // Also process existing keysets
+        let keysets_table = write_txn.open_table(KEYSETS_TABLE).map_err(Error::from)?;
+        let keyset_ids: Vec<Id> = keysets_table
+            .iter()
+            .map_err(Error::from)?
+            .flatten()
+            .flat_map(|(id_bytes, _)| Id::from_bytes(id_bytes.value()))
+            .collect();
+
+        let ids: HashSet<Id> = ids.into_iter().chain(keyset_ids).collect();
+
+        for id in ids {
+            let t = table.insert(u32::from(id), id.to_string().as_str())?;
+
+            tracing::info!("Adding u32 {} for keyset {}", u32::from(id), id.to_string());
+
+            if t.is_some() {
+                duplicate = true;
+            }
+        }
+    }
+
+    if duplicate {
+        write_txn.abort()?;
+        return Err(Error::Duplicate);
+    }
+
+    write_txn.commit()?;
+
+    Ok(3)
+}
+
 pub fn migrate_01_to_02(db: Arc<Database>) -> Result<u32, Error> {
     migrate_trim_mint_urls_01_to_02(db)?;
     Ok(2)

+ 96 - 8
crates/cdk-redb/src/wallet/mod.rs

@@ -21,7 +21,7 @@ use tracing::instrument;
 
 use super::error::Error;
 use crate::migrations::migrate_00_to_01;
-use crate::wallet::migrations::migrate_01_to_02;
+use crate::wallet::migrations::{migrate_01_to_02, migrate_02_to_03};
 
 mod migrations;
 
@@ -44,7 +44,9 @@ const KEYSET_COUNTER: TableDefinition<&str, u32> = TableDefinition::new("keyset_
 // <Transaction_id, Transaction>
 const TRANSACTIONS_TABLE: TableDefinition<&[u8], &str> = TableDefinition::new("transactions");
 
-const DATABASE_VERSION: u32 = 2;
+const KEYSET_U32_MAPPING: TableDefinition<u32, &str> = TableDefinition::new("keyset_u32_mapping");
+
+const DATABASE_VERSION: u32 = 3;
 
 /// Wallet Redb Database
 #[derive(Debug, Clone)]
@@ -90,6 +92,10 @@ impl WalletRedbDatabase {
                                 current_file_version = migrate_01_to_02(Arc::clone(&db))?;
                             }
 
+                            if current_file_version == 2 {
+                                current_file_version = migrate_02_to_03(Arc::clone(&db))?;
+                            }
+
                             if current_file_version != DATABASE_VERSION {
                                 tracing::warn!(
                                     "Database upgrade did not complete at {} current is {}",
@@ -136,6 +142,7 @@ impl WalletRedbDatabase {
                         let _ = write_txn.open_table(PROOFS_TABLE)?;
                         let _ = write_txn.open_table(KEYSET_COUNTER)?;
                         let _ = write_txn.open_table(TRANSACTIONS_TABLE)?;
+                        let _ = write_txn.open_table(KEYSET_U32_MAPPING)?;
                         table.insert("db_version", DATABASE_VERSION.to_string().as_str())?;
                     }
 
@@ -290,20 +297,64 @@ impl WalletDatabase for WalletRedbDatabase {
     ) -> Result<(), Self::Err> {
         let write_txn = self.db.begin_write().map_err(Error::from)?;
 
+        let mut existing_u32 = false;
+
         {
             let mut table = write_txn
                 .open_multimap_table(MINT_KEYSETS_TABLE)
                 .map_err(Error::from)?;
             let mut keysets_table = write_txn.open_table(KEYSETS_TABLE).map_err(Error::from)?;
+            let mut u32_table = write_txn
+                .open_table(KEYSET_U32_MAPPING)
+                .map_err(Error::from)?;
 
             for keyset in keysets {
-                table
-                    .insert(
-                        mint_url.to_string().as_str(),
-                        keyset.id.to_bytes().as_slice(),
-                    )
+                // Check if keyset already exists
+                let existing_keyset = {
+                    let existing_keyset = keysets_table
+                        .get(keyset.id.to_bytes().as_slice())
+                        .map_err(Error::from)?;
+
+                    existing_keyset.map(|r| r.value().to_string())
+                };
+
+                let existing = u32_table
+                    .insert(u32::from(keyset.id), keyset.id.to_string().as_str())
                     .map_err(Error::from)?;
 
+                match existing {
+                    None => existing_u32 = false,
+                    Some(id) => {
+                        let id = Id::from_str(id.value())?;
+
+                        if id == keyset.id {
+                            existing_u32 = false;
+                        } else {
+                            println!("Breaking here");
+                            existing_u32 = true;
+                            break;
+                        }
+                    }
+                }
+
+                let keyset = if let Some(existing_keyset) = existing_keyset {
+                    let mut existing_keyset: KeySetInfo = serde_json::from_str(&existing_keyset)?;
+
+                    existing_keyset.active = keyset.active;
+                    existing_keyset.input_fee_ppk = keyset.input_fee_ppk;
+
+                    existing_keyset
+                } else {
+                    table
+                        .insert(
+                            mint_url.to_string().as_str(),
+                            keyset.id.to_bytes().as_slice(),
+                        )
+                        .map_err(Error::from)?;
+
+                    keyset
+                };
+
                 keysets_table
                     .insert(
                         keyset.id.to_bytes().as_slice(),
@@ -314,6 +365,14 @@ impl WalletDatabase for WalletRedbDatabase {
                     .map_err(Error::from)?;
             }
         }
+
+        if existing_u32 {
+            tracing::warn!("Keyset already exists for keyset id");
+            write_txn.abort().map_err(Error::from)?;
+
+            return Err(database::Error::Duplicate);
+        }
+
         write_txn.commit().map_err(Error::from)?;
 
         Ok(())
@@ -514,16 +573,45 @@ impl WalletDatabase for WalletRedbDatabase {
 
         keyset.verify_id()?;
 
+        let existing_keys;
+        let existing_u32;
+
         {
             let mut table = write_txn.open_table(MINT_KEYS_TABLE).map_err(Error::from)?;
-            table
+
+            existing_keys = table
                 .insert(
                     keyset.id.to_string().as_str(),
                     serde_json::to_string(&keyset.keys)
                         .map_err(Error::from)?
                         .as_str(),
                 )
+                .map_err(Error::from)?
+                .is_some();
+
+            let mut table = write_txn
+                .open_table(KEYSET_U32_MAPPING)
+                .map_err(Error::from)?;
+
+            let existing = table
+                .insert(u32::from(keyset.id), keyset.id.to_string().as_str())
                 .map_err(Error::from)?;
+
+            match existing {
+                None => existing_u32 = false,
+                Some(id) => {
+                    let id = Id::from_str(id.value())?;
+
+                    existing_u32 = id != keyset.id;
+                }
+            }
+        }
+
+        if existing_keys || existing_u32 {
+            tracing::warn!("Keys already exist for keyset id");
+            write_txn.abort().map_err(Error::from)?;
+
+            return Err(database::Error::Duplicate);
         }
 
         write_txn.commit().map_err(Error::from)?;

+ 1 - 0
crates/cdk-sql-common/src/wallet/migrations.rs

@@ -19,4 +19,5 @@ pub static MIGRATIONS: &[(&str, &str, &str)] = &[
     ("sqlite", "20250401120000_add_transactions_table.sql", include_str!(r#"./migrations/sqlite/20250401120000_add_transactions_table.sql"#)),
     ("sqlite", "20250616144830_add_keyset_expiry.sql", include_str!(r#"./migrations/sqlite/20250616144830_add_keyset_expiry.sql"#)),
     ("sqlite", "20250707093445_bolt12.sql", include_str!(r#"./migrations/sqlite/20250707093445_bolt12.sql"#)),
+    ("sqlite", "20250729111701_keyset_v2_u32.sql", include_str!(r#"./migrations/sqlite/20250729111701_keyset_v2_u32.sql"#)),
 ];

+ 11 - 0
crates/cdk-sql-common/src/wallet/migrations/sqlite/20250729111701_keyset_v2_u32.sql

@@ -0,0 +1,11 @@
+-- Add u32 representation column to key table with unique constraint
+ALTER TABLE key ADD COLUMN keyset_u32 INTEGER;
+
+-- Add unique constraint on the new column
+CREATE UNIQUE INDEX IF NOT EXISTS keyset_u32_unique ON key(keyset_u32);
+
+-- Add u32 representation column to keyset table with unique constraint
+ALTER TABLE keyset ADD COLUMN keyset_u32 INTEGER;
+
+-- Add unique constraint on the new column
+CREATE UNIQUE INDEX IF NOT EXISTS keyset_u32_unique_keyset ON keyset(keyset_u32);

+ 75 - 10
crates/cdk-sql-common/src/wallet/mod.rs

@@ -53,6 +53,74 @@ where
     /// Migrate [`WalletSqliteDatabase`]
     async fn migrate(conn: &DB) -> Result<(), Error> {
         migrate(conn, DB::name(), migrations::MIGRATIONS).await?;
+        // Update any existing keys with missing keyset_u32 values
+        Self::add_keyset_u32(conn).await?;
+        Ok(())
+    }
+
+    async fn add_keyset_u32(conn: &DB) -> Result<(), Error> {
+        // First get the keysets where keyset_u32 on key is null
+        let keys_without_u32: Vec<Vec<Column>> = query(
+            r#"
+            SELECT
+                id
+            FROM key
+            WHERE keyset_u32 IS NULL
+            "#,
+        )?
+        .fetch_all(conn)
+        .await?;
+
+        for id in keys_without_u32 {
+            let id = column_as_string!(id.first().unwrap());
+
+            if let Ok(id) = Id::from_str(&id) {
+                query(
+                    r#"
+            UPDATE
+                key
+            SET keyset_u32 = :u32_keyset
+            WHERE id = :keyset_id
+            "#,
+                )?
+                .bind("u32_keyset", u32::from(id))
+                .bind("keyset_id", id.to_string())
+                .execute(conn)
+                .await?;
+            }
+        }
+
+        // Also update keysets where keyset_u32 is null
+        let keysets_without_u32: Vec<Vec<Column>> = query(
+            r#"
+            SELECT
+                id
+            FROM keyset
+            WHERE keyset_u32 IS NULL
+            "#,
+        )?
+        .fetch_all(conn)
+        .await?;
+
+        for id in keysets_without_u32 {
+            let id = column_as_string!(id.first().unwrap());
+
+            if let Ok(id) = Id::from_str(&id) {
+                query(
+                    r#"
+            UPDATE
+                keyset
+            SET keyset_u32 = :u32_keyset
+            WHERE id = :keyset_id
+            "#,
+                )?
+                .bind("u32_keyset", u32::from(id))
+                .bind("keyset_id", id.to_string())
+                .execute(conn)
+                .await?;
+            }
+        }
+
         Ok(())
     }
 }
@@ -301,15 +369,12 @@ ON CONFLICT(mint_url) DO UPDATE SET
             query(
                 r#"
     INSERT INTO keyset
-    (mint_url, id, unit, active, input_fee_ppk, final_expiry)
+    (mint_url, id, unit, active, input_fee_ppk, final_expiry, keyset_u32)
     VALUES
-    (:mint_url, :id, :unit, :active, :input_fee_ppk, :final_expiry)
+    (:mint_url, :id, :unit, :active, :input_fee_ppk, :final_expiry, :keyset_u32)
     ON CONFLICT(id) DO UPDATE SET
-        mint_url = excluded.mint_url,
-        unit = excluded.unit,
         active = excluded.active,
-        input_fee_ppk = excluded.input_fee_ppk,
-        final_expiry = excluded.final_expiry;
+        input_fee_ppk = excluded.input_fee_ppk
     "#,
             )?
             .bind("mint_url", mint_url.to_string())
@@ -318,6 +383,7 @@ ON CONFLICT(mint_url) DO UPDATE SET
             .bind("active", keyset.active)
             .bind("input_fee_ppk", keyset.input_fee_ppk as i64)
             .bind("final_expiry", keyset.final_expiry.map(|v| v as i64))
+            .bind("keyset_u32", u32::from(keyset.id))
             .execute(&self.db)
             .await?;
         }
@@ -554,11 +620,9 @@ ON CONFLICT(id) DO UPDATE SET
         query(
             r#"
             INSERT INTO key
-            (id, keys)
+            (id, keys, keyset_u32)
             VALUES
-            (:id, :keys)
-            ON CONFLICT(id) DO UPDATE SET
-                keys = excluded.keys
+            (:id, :keys, :keyset_u32)
         "#,
         )?
         .bind("id", keyset.id.to_string())
@@ -566,6 +630,7 @@ ON CONFLICT(id) DO UPDATE SET
             "keys",
             serde_json::to_string(&keyset.keys).map_err(Error::from)?,
         )
+        .bind("keyset_u32", u32::from(keyset.id))
         .execute(&self.db)
         .await?;
 

+ 2 - 2
crates/cdk/src/wallet/issue/issue_bolt11.rs

@@ -264,12 +264,12 @@ impl Wallet {
 
         let mint_res = self.client.post_mint(request).await?;
 
-        let keys = self.fetch_keyset_keys(active_keyset_id).await?;
+        let keys = self.load_keyset_keys(active_keyset_id).await?;
 
         // Verify the signature DLEQ is valid
         {
             for (sig, premint) in mint_res.signatures.iter().zip(&premint_secrets.secrets) {
-                let keys = self.fetch_keyset_keys(sig.keyset_id).await?;
+                let keys = self.load_keyset_keys(sig.keyset_id).await?;
                 let key = keys.amount_key(sig.amount).ok_or(Error::AmountKey)?;
                 match sig.verify_dleq(key, premint.blinded_message.blinded_secret) {
                     Ok(_) | Err(nut12::Error::MissingDleqProof) => (),

+ 2 - 2
crates/cdk/src/wallet/issue/issue_bolt12.rs

@@ -161,12 +161,12 @@ impl Wallet {
 
         let mint_res = self.client.post_mint(request).await?;
 
-        let keys = self.fetch_keyset_keys(active_keyset_id).await?;
+        let keys = self.load_keyset_keys(active_keyset_id).await?;
 
         // Verify the signature DLEQ is valid
         {
             for (sig, premint) in mint_res.signatures.iter().zip(&premint_secrets.secrets) {
-                let keys = self.fetch_keyset_keys(sig.keyset_id).await?;
+                let keys = self.load_keyset_keys(sig.keyset_id).await?;
                 let key = keys.amount_key(sig.amount).ok_or(Error::AmountKey)?;
                 match sig.verify_dleq(key, premint.blinded_message.blinded_secret) {
                     Ok(_) | Err(nut12::Error::MissingDleqProof) => (),

+ 9 - 6
crates/cdk/src/wallet/keysets.rs

@@ -7,15 +7,21 @@ use crate::nuts::{Id, KeySetInfo, Keys};
 use crate::{Error, Wallet};
 
 impl Wallet {
-    /// Fetch keys for mint keyset
+    /// Load keys for mint keyset
     ///
     /// Returns keys from local database if they are already stored.
     /// If keys are not found locally, goes online to query the mint for the keyset and stores the [`Keys`] in local database.
     #[instrument(skip(self))]
-    pub async fn fetch_keyset_keys(&self, keyset_id: Id) -> Result<Keys, Error> {
+    pub async fn load_keyset_keys(&self, keyset_id: Id) -> Result<Keys, Error> {
         let keys = if let Some(keys) = self.localstore.get_keys(&keyset_id).await? {
             keys
         } else {
+            tracing::debug!(
+                "Keyset {} not in db fetching from mint {}",
+                keyset_id,
+                self.mint_url
+            );
+
             let keys = self.client.get_mint_keyset(keyset_id).await?;
 
             keys.verify_id()?;
@@ -92,10 +98,7 @@ impl Wallet {
 
         // Ensure we have keys for all active keysets
         for keyset in &keysets {
-            if self.localstore.get_keys(&keyset.id).await?.is_none() {
-                tracing::debug!("Fetching missing keys for keyset {}", keyset.id);
-                self.fetch_keyset_keys(keyset.id).await?;
-            }
+            self.load_keyset_keys(keyset.id).await?;
         }
 
         Ok(keysets)

+ 2 - 2
crates/cdk/src/wallet/mod.rs

@@ -383,7 +383,7 @@ impl Wallet {
         let mut restored_value = Amount::ZERO;
 
         for keyset in keysets {
-            let keys = self.fetch_keyset_keys(keyset.id).await?;
+            let keys = self.load_keyset_keys(keyset.id).await?;
             let mut empty_batch = 0;
             let mut start_counter = 0;
 
@@ -632,7 +632,7 @@ impl Wallet {
             let mint_pubkey = match keys_cache.get(&proof.keyset_id) {
                 Some(keys) => keys.amount_key(proof.amount),
                 None => {
-                    let keys = self.fetch_keyset_keys(proof.keyset_id).await?;
+                    let keys = self.load_keyset_keys(proof.keyset_id).await?;
 
                     let key = keys.amount_key(proof.amount);
                     keys_cache.insert(proof.keyset_id, keys);

+ 2 - 2
crates/cdk/src/wallet/receive.rs

@@ -42,7 +42,7 @@ impl Wallet {
 
         let active_keyset_id = self.fetch_active_keyset().await?.id;
 
-        let keys = self.fetch_keyset_keys(active_keyset_id).await?;
+        let keys = self.load_keyset_keys(active_keyset_id).await?;
 
         let mut proofs = proofs;
 
@@ -70,7 +70,7 @@ impl Wallet {
         for proof in &mut proofs {
             // Verify that proof DLEQ is valid
             if proof.dleq.is_some() {
-                let keys = self.fetch_keyset_keys(proof.keyset_id).await?;
+                let keys = self.load_keyset_keys(proof.keyset_id).await?;
                 let key = keys.amount_key(proof.amount).ok_or(Error::AmountKey)?;
                 proof.verify_dleq(key)?;
             }