Bladeren bron

feat: keyset v2 configuration (#1592)

By default, the mint will use V2 (Version01) for new keysets but will preserve existing V1 (Version00) keysets to avoid unnecessary rotation. You can force a specific policy using config.toml or environment variables:
use_keyset_v2 = true (or CDK_MINTD_USE_KEYSET_V2=true): Forces V2. If the current active keyset is V1, it will be rotated to V2 on startup.
use_keyset_v2 = false (or CDK_MINTD_USE_KEYSET_V2=false): Forces V1. If the current active keyset is V2, it will be rotated to V1 on startup.
Unset (Default): Preserves the current keyset version. If no keyset exists, V2 is created.
tsk 1 maand geleden
bovenliggende
commit
121ca874c0

+ 1 - 0
crates/cdk-integration-tests/src/bin/start_regtest_mints.rs

@@ -275,6 +275,7 @@ fn create_ldk_settings(
             signatory_url: None,
             signatory_certs: None,
             input_fee_ppk: None,
+            use_keyset_v2: None,
             http_cache: cdk_axum::cache::Config::default(),
             enable_swagger_ui: None,
             logging: LoggingConfig::default(),

+ 3 - 0
crates/cdk-integration-tests/src/shared.rs

@@ -207,6 +207,7 @@ pub fn create_fake_wallet_settings(
                 .as_ref()
                 .map(|(_, certs_dir)| certs_dir.clone()),
             input_fee_ppk: None,
+            use_keyset_v2: None,
             http_cache: cache::Config::default(),
             logging: cdk_mintd::config::LoggingConfig {
                 output: cdk_mintd::config::LoggingOutput::Both,
@@ -260,6 +261,7 @@ pub fn create_cln_settings(
             signatory_url: None,
             signatory_certs: None,
             input_fee_ppk: None,
+            use_keyset_v2: None,
             http_cache: cache::Config::default(),
             logging: cdk_mintd::config::LoggingConfig {
                 output: cdk_mintd::config::LoggingOutput::Both,
@@ -308,6 +310,7 @@ pub fn create_lnd_settings(
             signatory_url: None,
             signatory_certs: None,
             input_fee_ppk: None,
+            use_keyset_v2: None,
             http_cache: cache::Config::default(),
             logging: cdk_mintd::config::LoggingConfig {
                 output: cdk_mintd::config::LoggingOutput::Both,

+ 4 - 2
crates/cdk-integration-tests/tests/integration_tests_pure.rs

@@ -566,6 +566,7 @@ async fn test_swap_overpay_underpay_fee() {
             CurrencyUnit::Sat,
             cdk_integration_tests::standard_keyset_amounts(32),
             1,
+            true,
         )
         .await
         .unwrap();
@@ -584,8 +585,7 @@ async fn test_swap_overpay_underpay_fee() {
         .await
         .expect("Could not get proofs");
 
-    let keys = mint_bob.pubkeys().keysets.first().unwrap().clone().keys;
-    let keyset_id = Id::v1_from_keys(&keys);
+    let keyset_id = mint_bob.pubkeys().keysets.first().unwrap().id;
     let fee_and_amounts = (0, ((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>())).into();
 
     let preswap = PreMintSecrets::random(
@@ -645,6 +645,7 @@ async fn test_mint_enforce_fee() {
             CurrencyUnit::Sat,
             cdk_integration_tests::standard_keyset_amounts(32),
             1,
+            true,
         )
         .await
         .unwrap();
@@ -758,6 +759,7 @@ async fn test_mint_change_with_fee_melt() {
             CurrencyUnit::Sat,
             cdk_integration_tests::standard_keyset_amounts(32),
             1,
+            true,
         )
         .await
         .unwrap();

+ 2 - 0
crates/cdk-integration-tests/tests/mint.rs

@@ -80,6 +80,7 @@ async fn test_correct_keyset() {
         CurrencyUnit::Sat,
         cdk_integration_tests::standard_keyset_amounts(32),
         0,
+        true,
     )
     .await
     .unwrap();
@@ -98,6 +99,7 @@ async fn test_correct_keyset() {
         CurrencyUnit::Sat,
         cdk_integration_tests::standard_keyset_amounts(32),
         0,
+        true,
     )
     .await
     .unwrap();

+ 5 - 0
crates/cdk-integration-tests/tests/test_swap_flow.rs

@@ -726,6 +726,7 @@ async fn test_swap_with_fees() {
         CurrencyUnit::Sat,
         cdk_integration_tests::standard_keyset_amounts(32),
         100,
+        true,
     )
     .await
     .expect("Failed to rotate keyset");
@@ -825,6 +826,7 @@ async fn test_melt_with_fees_swap_before_melt() {
         CurrencyUnit::Sat,
         cdk_integration_tests::standard_keyset_amounts(32),
         1000, // 1 sat per proof
+        true,
     )
     .await
     .expect("Failed to rotate keyset");
@@ -1072,6 +1074,7 @@ async fn test_melt_small_amount_tight_margin() {
         CurrencyUnit::Sat,
         cdk_integration_tests::standard_keyset_amounts(32),
         1000,
+        true,
     )
     .await
     .expect("Failed to rotate keyset");
@@ -1178,6 +1181,7 @@ async fn test_melt_swap_tight_margin_regression() {
         CurrencyUnit::Sat,
         cdk_integration_tests::standard_keyset_amounts(32),
         250,
+        true,
     )
     .await
     .expect("Failed to rotate keyset");
@@ -1505,6 +1509,7 @@ async fn test_wallet_multi_keyset_counter_updates() {
         CurrencyUnit::Sat,
         cdk_integration_tests::standard_keyset_amounts(32),
         0,
+        true,
     )
     .await
     .expect("Failed to rotate keyset");

+ 3 - 0
crates/cdk-integration-tests/tests/wallet_saga.rs

@@ -233,6 +233,7 @@ async fn test_melt_saga_includes_input_fees() -> Result<()> {
         CurrencyUnit::Sat,
         cdk_integration_tests::standard_keyset_amounts(32),
         1000, // 1 sat per proof input fee
+        true,
     )
     .await
     .expect("Failed to rotate keyset");
@@ -324,6 +325,7 @@ async fn test_melt_with_swap_non_optimal_proofs() -> Result<()> {
         CurrencyUnit::Sat,
         cdk_integration_tests::standard_keyset_amounts(32),
         100, // 0.1 sat per proof input fee
+        true,
     )
     .await
     .expect("Failed to rotate keyset");
@@ -420,6 +422,7 @@ async fn test_melt_swap_gap_recovery() -> Result<()> {
         CurrencyUnit::Sat,
         cdk_integration_tests::standard_keyset_amounts(32),
         1000,
+        true,
     )
     .await
     .expect("Failed to rotate keyset");

+ 4 - 0
crates/cdk-mint-rpc/src/mint_rpc_cli/subcommands/rotate_next_keyset.rs

@@ -22,6 +22,9 @@ pub struct RotateNextKeysetCommand {
     /// The input fee in parts per thousand to apply when minting with this keyset
     #[arg(short, long)]
     input_fee_ppk: Option<u64>,
+    /// Use keyset v2
+    #[arg(long)]
+    use_keyset_v2: Option<bool>,
 }
 
 /// Executes the rotate_next_keyset command against the mint server
@@ -50,6 +53,7 @@ pub async fn rotate_next_keyset(
             unit: sub_command_args.unit.clone(),
             amounts,
             input_fee_ppk: sub_command_args.input_fee_ppk,
+            use_keyset_v2: sub_command_args.use_keyset_v2,
         }))
         .await?;
 

+ 1 - 0
crates/cdk-mint-rpc/src/proto/cdk-mint-rpc.proto

@@ -124,6 +124,7 @@ message RotateNextKeysetRequest {
     string unit = 1;
     repeated uint64 amounts = 2;
     optional uint64 input_fee_ppk = 3;
+    optional bool use_keyset_v2 = 4;
 }
 
 

+ 6 - 1
crates/cdk-mint-rpc/src/proto/server.rs

@@ -749,7 +749,12 @@ impl CdkMint for MintRPCServer {
 
         let keyset_info = self
             .mint
-            .rotate_keyset(unit, amounts, request.input_fee_ppk.unwrap_or(0))
+            .rotate_keyset(
+                unit,
+                amounts,
+                request.input_fee_ppk.unwrap_or(0),
+                request.use_keyset_v2.unwrap_or(true),
+            )
             .await
             .map_err(|_| Status::invalid_argument("Could not rotate keyset".to_string()))?;
 

+ 19 - 0
crates/cdk-mintd/README.md

@@ -93,6 +93,25 @@ export CDK_MINTD_DATABASE=sqlite
 cdk-mintd
 ```
 
+### Keyset Version Management
+
+The mint supports rotating keysets to newer versions (e.g., migrating from V1 to V2).
+
+**Policy Configuration:**
+By default, the mint will use V2 (Version01) for *new* keysets but will preserve existing V1 (Version00) keysets to avoid unnecessary rotation. You can force a specific policy using `config.toml` or environment variables:
+
+- `use_keyset_v2 = true` (or `CDK_MINTD_USE_KEYSET_V2=true`): Forces V2. If the current active keyset is V1, it will be rotated to V2 on startup.
+- `use_keyset_v2 = false` (or `CDK_MINTD_USE_KEYSET_V2=false`): Forces V1. If the current active keyset is V2, it will be rotated to V1 on startup.
+- **Unset (Default)**: Preserves the current keyset version. If no keyset exists, V2 is created.
+
+**Manual Rotation:**
+You can manually trigger a rotation to a specific version using the CLI:
+
+```bash
+mint-cli rotate-next-keyset --use-keyset-v2       # Rotate to V2
+mint-cli rotate-next-keyset --use-keyset-v2=false # Rotate to V1
+```
+
 ## Production Examples
 
 ### With LDK Node (Recommended for Testing)

+ 6 - 0
crates/cdk-mintd/example.config.toml

@@ -7,6 +7,12 @@ mnemonic = ""
 # input_fee_ppk = 0
 # enable_swagger_ui = false
 
+# Set keyset version preference.
+# true = Force upgrade to V2 (Version01).
+# false = Force downgrade to V1 (Version00).
+# If unset (default), existing keysets are preserved, but new ones use V2.
+# use_keyset_v2 = true
+
 [info.quote_ttl]
 # Prefer explicit fields over inline tables for readability and ease of overrides
 mint_ttl = 600

+ 4 - 0
crates/cdk-mintd/src/config.rs

@@ -57,6 +57,8 @@ pub struct Info {
     pub signatory_url: Option<String>,
     pub signatory_certs: Option<String>,
     pub input_fee_ppk: Option<u64>,
+    /// Use keyset v2
+    pub use_keyset_v2: Option<bool>,
 
     pub http_cache: cache::Config,
 
@@ -88,6 +90,7 @@ impl Default for Info {
             signatory_url: None,
             signatory_certs: None,
             input_fee_ppk: None,
+            use_keyset_v2: None,
             http_cache: cache::Config::default(),
             enable_swagger_ui: None,
             logging: LoggingConfig::default(),
@@ -114,6 +117,7 @@ impl std::fmt::Debug for Info {
             .field("listen_port", &self.listen_port)
             .field("mnemonic", &mnemonic_display)
             .field("input_fee_ppk", &self.input_fee_ppk)
+            .field("use_keyset_v2", &self.use_keyset_v2)
             .field("http_cache", &self.http_cache)
             .field("logging", &self.logging)
             .field("enable_swagger_ui", &self.enable_swagger_ui)

+ 1 - 0
crates/cdk-mintd/src/env_vars/common.rs

@@ -16,6 +16,7 @@ pub const ENV_EXTEND_CACHE_SECONDS: &str = "CDK_MINTD_EXTEND_CACHE_SECONDS";
 pub const ENV_INPUT_FEE_PPK: &str = "CDK_MINTD_INPUT_FEE_PPK";
 pub const ENV_QUOTE_TTL_MINT: &str = "CDK_MINTD_QUOTE_TTL_MINT";
 pub const ENV_QUOTE_TTL_MELT: &str = "CDK_MINTD_QUOTE_TTL_MELT";
+pub const ENV_USE_KEYSET_V2: &str = "CDK_MINTD_USE_KEYSET_V2";
 
 pub const ENV_ENABLE_SWAGGER: &str = "CDK_MINTD_ENABLE_SWAGGER";
 pub const ENV_LOGGING_OUTPUT: &str = "CDK_MINTD_LOGGING_OUTPUT";

+ 6 - 0
crates/cdk-mintd/src/env_vars/info.rs

@@ -65,6 +65,12 @@ impl Info {
             }
         }
 
+        if let Ok(use_keyset_v2_str) = env::var(ENV_USE_KEYSET_V2) {
+            if let Ok(use_keyset_v2) = use_keyset_v2_str.parse() {
+                self.use_keyset_v2 = Some(use_keyset_v2);
+            }
+        }
+
         // Logging configuration
         if let Ok(output_str) = env::var(ENV_LOGGING_OUTPUT) {
             if let Ok(output) = LoggingOutput::from_str(&output_str) {

+ 2 - 0
crates/cdk-mintd/src/lib.rs

@@ -438,6 +438,8 @@ fn configure_basic_info(settings: &config::Settings, mint_builder: MintBuilder)
         }
     }
 
+    builder = builder.with_keyset_v2(settings.info.use_keyset_v2);
+
     builder
 }
 /// Configures Lightning Network backend based on the specified backend type

+ 30 - 79
crates/cdk-signatory/src/common.rs

@@ -6,64 +6,41 @@ use bitcoin::secp256k1::{self, All, Secp256k1};
 use cdk_common::database;
 use cdk_common::error::Error;
 use cdk_common::mint::MintKeySetInfo;
-use cdk_common::nuts::{CurrencyUnit, Id, MintKeySet};
+use cdk_common::nuts::{CurrencyUnit, MintKeySet};
 use cdk_common::util::unix_time;
 
-/// Initialize keysets and returns a [`Result`] with a tuple of the following:
-/// * a [`HashMap`] mapping each active keyset `Id` to `MintKeySet`
-/// * a [`Vec`] of `CurrencyUnit` containing active keysets units
+/// Initialize keysets
 pub async fn init_keysets(
     xpriv: Xpriv,
     secp_ctx: &Secp256k1<All>,
     localstore: &Arc<dyn database::MintKeysDatabase<Err = database::Error> + Send + Sync>,
     supported_units: &HashMap<CurrencyUnit, (u64, u8)>,
-    custom_paths: &HashMap<CurrencyUnit, DerivationPath>,
-) -> Result<(HashMap<Id, MintKeySet>, Vec<CurrencyUnit>), Error> {
-    let mut active_keysets: HashMap<Id, MintKeySet> = HashMap::new();
-    let mut active_keyset_units: Vec<CurrencyUnit> = vec![];
-
-    // Get keysets info from DB
+) -> Result<(), Error> {
     let keysets_infos = localstore.get_keyset_infos().await?;
-
     let mut tx = localstore.begin_transaction().await?;
-    if !keysets_infos.is_empty() {
-        tracing::debug!("Setting all saved keysets to inactive");
-        for keyset in keysets_infos.clone() {
-            // Set all to in active
-            let mut keyset = keyset;
-            keyset.active = false;
-            tx.add_keyset_info(keyset).await?;
-        }
 
-        let keysets_by_unit: HashMap<CurrencyUnit, Vec<MintKeySetInfo>> =
-            keysets_infos.iter().fold(HashMap::new(), |mut acc, ks| {
-                acc.entry(ks.unit.clone()).or_default().push(ks.clone());
-                acc
-            });
+    let keysets_by_unit: HashMap<CurrencyUnit, Vec<MintKeySetInfo>> =
+        keysets_infos.iter().fold(HashMap::new(), |mut acc, ks| {
+            acc.entry(ks.unit.clone()).or_default().push(ks.clone());
+            acc
+        });
 
-        for (unit, keysets) in keysets_by_unit {
+    for (unit, keysets) in keysets_by_unit {
+        // We only care about units that are supported
+        if let Some((input_fee_ppk, max_order)) = supported_units.get(&unit) {
             let mut keysets = keysets;
             keysets.sort_by(|a, b| b.derivation_path_index.cmp(&a.derivation_path_index));
 
-            // Get the keyset with the highest counter
-            let highest_index_keyset = keysets
-                .first()
-                .cloned()
-                .expect("unit will not be added to hashmap if empty");
-
-            let keysets: Vec<MintKeySetInfo> = keysets
-                .into_iter()
-                .filter(|ks| ks.derivation_path_index.is_some())
-                .collect();
-
-            if let Some((input_fee_ppk, max_order)) = supported_units.get(&unit) {
-                if !keysets.is_empty()
-                    && highest_index_keyset.input_fee_ppk == *input_fee_ppk
+            if let Some(highest_index_keyset) = keysets.first() {
+                // Check if it matches our criteria
+                if highest_index_keyset.input_fee_ppk == *input_fee_ppk
                     && highest_index_keyset.amounts.len() == (*max_order as usize)
                 {
                     tracing::debug!("Current highest index keyset matches expect fee and max order. Setting active");
                     let id = highest_index_keyset.id;
-                    let keyset = MintKeySet::generate_from_xpriv(
+
+                    // Validate we can generate it (sanity check)
+                    let _ = MintKeySet::generate_from_xpriv(
                         secp_ctx,
                         xpriv,
                         &highest_index_keyset.amounts,
@@ -71,53 +48,21 @@ pub async fn init_keysets(
                         highest_index_keyset.derivation_path.clone(),
                         highest_index_keyset.input_fee_ppk,
                         highest_index_keyset.final_expiry,
-                        cdk_common::nut02::KeySetVersion::Version00,
+                        highest_index_keyset.id.get_version(),
                     );
-                    active_keysets.insert(id, keyset);
-                    let mut keyset_info = highest_index_keyset;
-                    keyset_info.active = true;
-                    tx.add_keyset_info(keyset_info).await?;
-                    active_keyset_units.push(unit.clone());
-                    tx.set_active_keyset(unit, id).await?;
-                } else {
-                    // Check to see if there are not keysets by this unit
-                    let derivation_path_index = if keysets.is_empty() {
-                        1
-                    } else {
-                        highest_index_keyset.derivation_path_index.unwrap_or(0) + 1
-                    };
-
-                    let derivation_path = match custom_paths.get(&unit) {
-                        Some(path) => path.clone(),
-                        None => derivation_path_from_unit(unit.clone(), derivation_path_index)
-                            .ok_or(Error::UnsupportedUnit)?,
-                    };
 
-                    let (keyset, keyset_info) = create_new_keyset(
-                        secp_ctx,
-                        xpriv,
-                        derivation_path,
-                        Some(derivation_path_index),
-                        unit.clone(),
-                        &highest_index_keyset.amounts,
-                        *input_fee_ppk,
-                        // TODO: add Mint settings for a final expiry of newly generated keysets
-                        None,
-                    );
-
-                    let id = keyset_info.id;
+                    let mut keyset_info = highest_index_keyset.clone();
+                    keyset_info.active = true;
                     tx.add_keyset_info(keyset_info).await?;
                     tx.set_active_keyset(unit.clone(), id).await?;
-                    active_keysets.insert(id, keyset);
-                    active_keyset_units.push(unit.clone());
-                };
+                }
             }
         }
     }
 
     tx.commit().await?;
 
-    Ok((active_keysets, active_keyset_units))
+    Ok(())
 }
 
 /// Generate new [`MintKeySetInfo`] from path
@@ -132,7 +77,14 @@ pub fn create_new_keyset<C: secp256k1::Signing>(
     amounts: &[u64],
     input_fee_ppk: u64,
     final_expiry: Option<u64>,
+    use_keyset_v2: bool,
 ) -> (MintKeySet, MintKeySetInfo) {
+    let version = if use_keyset_v2 {
+        cdk_common::nut02::KeySetVersion::Version01
+    } else {
+        cdk_common::nut02::KeySetVersion::Version00
+    };
+
     let keyset = MintKeySet::generate(
         secp,
         xpriv
@@ -142,8 +94,7 @@ pub fn create_new_keyset<C: secp256k1::Signing>(
         amounts,
         input_fee_ppk,
         final_expiry,
-        // TODO: change this to Version01 to generate keysets v2
-        cdk_common::nut02::KeySetVersion::Version00,
+        version,
     );
     let keyset_info = MintKeySetInfo {
         id: keyset.id,

+ 2 - 44
crates/cdk-signatory/src/db_signatory.rs

@@ -48,52 +48,9 @@ impl DbSignatory {
         let secp_ctx = Secp256k1::new();
         let xpriv = Xpriv::new_master(bitcoin::Network::Bitcoin, seed).expect("RNG busted");
 
-        let (mut active_keysets, active_keyset_units) = init_keysets(
-            xpriv,
-            &secp_ctx,
-            &localstore,
-            &supported_units,
-            &custom_paths,
-        )
-        .await?;
+        init_keysets(xpriv, &secp_ctx, &localstore, &supported_units).await?;
 
         supported_units.entry(CurrencyUnit::Auth).or_insert((0, 1));
-        let mut tx = localstore.begin_transaction().await?;
-
-        // Create new keysets for supported units that aren't covered by the current keysets
-        for (unit, (fee, max_order)) in supported_units {
-            if !active_keyset_units.contains(&unit) {
-                let derivation_path = match custom_paths.get(&unit) {
-                    Some(path) => path.clone(),
-                    None => {
-                        derivation_path_from_unit(unit.clone(), 0).ok_or(Error::UnsupportedUnit)?
-                    }
-                };
-
-                let amounts = (0..max_order)
-                    .map(|i| 2_u64.pow(i as u32))
-                    .collect::<Vec<_>>();
-
-                let (keyset, keyset_info) = create_new_keyset(
-                    &secp_ctx,
-                    xpriv,
-                    derivation_path,
-                    Some(0),
-                    unit.clone(),
-                    &amounts,
-                    fee,
-                    // TODO: add and connect settings for this
-                    None,
-                );
-
-                let id = keyset_info.id;
-                tx.add_keyset_info(keyset_info).await?;
-                tx.set_active_keyset(unit, id).await?;
-                active_keysets.insert(id, keyset);
-            }
-        }
-
-        tx.commit().await?;
 
         let keys = Self {
             keysets: Default::default(),
@@ -267,6 +224,7 @@ impl Signatory for DbSignatory {
             args.input_fee_ppk,
             // TODO: add and connect settings for this
             None,
+            args.use_keyset_v2,
         );
         let id = info.id;
         let mut tx = self.localstore.begin_transaction().await?;

+ 2 - 0
crates/cdk-signatory/src/proto/convert.rs

@@ -337,6 +337,7 @@ impl From<crate::signatory::RotateKeyArguments> for RotationRequest {
             unit: Some(value.unit.into()),
             amounts: value.amounts,
             input_fee_ppk: value.input_fee_ppk,
+            use_keyset_v2: value.use_keyset_v2,
         }
     }
 }
@@ -352,6 +353,7 @@ impl TryInto<crate::signatory::RotateKeyArguments> for RotationRequest {
                 .try_into()?,
             amounts: self.amounts,
             input_fee_ppk: self.input_fee_ppk,
+            use_keyset_v2: self.use_keyset_v2,
         })
     }
 }

+ 1 - 0
crates/cdk-signatory/src/proto/signatory.proto

@@ -74,6 +74,7 @@ message RotationRequest {
   CurrencyUnit unit = 1;
   uint64 input_fee_ppk = 2;
   repeated uint64 amounts = 3;
+  bool use_keyset_v2 = 4;
 }
 
 enum CurrencyUnitType {

+ 2 - 0
crates/cdk-signatory/src/signatory.rs

@@ -46,6 +46,8 @@ pub struct RotateKeyArguments {
     pub amounts: Vec<u64>,
     /// Input fee
     pub input_fee_ppk: u64,
+    /// KeySet Version
+    pub use_keyset_v2: bool,
 }
 
 #[derive(Debug, Clone)]

+ 73 - 2
crates/cdk/src/mint/builder.rs

@@ -12,7 +12,7 @@ use cdk_common::nut05::MeltMethodOptions;
 use cdk_common::payment::DynMintPayment;
 #[cfg(feature = "auth")]
 use cdk_common::{database::DynMintAuthDatabase, nut21, nut22};
-use cdk_signatory::signatory::Signatory;
+use cdk_signatory::signatory::{RotateKeyArguments, Signatory};
 
 use super::nut17::SupportedMethods;
 use super::nut19::{self, CachedEndpoint};
@@ -37,6 +37,7 @@ pub struct MintBuilder {
     payment_processors: HashMap<PaymentProcessorKey, DynMintPayment>,
     supported_units: HashMap<CurrencyUnit, (u64, u8)>,
     custom_paths: HashMap<CurrencyUnit, DerivationPath>,
+    use_keyset_v2: Option<bool>,
 }
 
 impl std::fmt::Debug for MintBuilder {
@@ -72,9 +73,16 @@ impl MintBuilder {
             payment_processors: HashMap::new(),
             supported_units: HashMap::new(),
             custom_paths: HashMap::new(),
+            use_keyset_v2: None,
         }
     }
 
+    /// Set use keyset v2
+    pub fn with_keyset_v2(mut self, use_keyset_v2: Option<bool>) -> Self {
+        self.use_keyset_v2 = use_keyset_v2;
+        self
+    }
+
     /// Set clear auth settings
     #[cfg(feature = "auth")]
     pub fn with_auth(
@@ -381,9 +389,72 @@ impl MintBuilder {
 
     /// Build the mint with the provided signatory
     pub async fn build_with_signatory(
-        self,
+        #[allow(unused_mut)] mut self,
         signatory: Arc<dyn Signatory + Send + Sync>,
     ) -> Result<Mint, Error> {
+        // Check active keysets and rotate if necessary
+        let active_keysets = signatory.keysets().await?;
+
+        // Ensure Auth keyset is created when auth is enabled
+        #[cfg(feature = "auth")]
+        if self.auth_localstore.is_some() {
+            self.supported_units
+                .entry(CurrencyUnit::Auth)
+                .or_insert((0, 1));
+        }
+
+        for (unit, (fee, max_order)) in &self.supported_units {
+            // Check if we have an active keyset for this unit
+            let keyset = active_keysets
+                .keysets
+                .iter()
+                .find(|k| k.active && k.unit == *unit);
+
+            let mut rotate = false;
+
+            if let Some(keyset) = keyset {
+                // Check if fee matches
+                if keyset.input_fee_ppk != *fee {
+                    tracing::info!(
+                        "Rotating keyset for unit {} due to fee mismatch (current: {}, expected: {})",
+                        unit,
+                        keyset.input_fee_ppk,
+                        fee
+                    );
+                    rotate = true;
+                }
+
+                // Check if version matches explicit preference
+                if let Some(want_v2) = self.use_keyset_v2 {
+                    let is_v2 =
+                        keyset.id.get_version() == cdk_common::nut02::KeySetVersion::Version01;
+                    if want_v2 && !is_v2 {
+                        tracing::info!("Rotating keyset for unit {} due to explicit V2 preference (current is V1)", unit);
+                        rotate = true;
+                    } else if !want_v2 && is_v2 {
+                        tracing::info!("Rotating keyset for unit {} due to explicit V1 preference (current is V2)", unit);
+                        rotate = true;
+                    }
+                }
+            } else {
+                // No active keyset for this unit
+                tracing::info!("Rotating keyset for unit {} (no active keyset found)", unit);
+                rotate = true;
+            }
+
+            if rotate {
+                let amounts: Vec<u64> = (0..*max_order).map(|i| 2_u64.pow(i as u32)).collect();
+                signatory
+                    .rotate_keyset(RotateKeyArguments {
+                        unit: unit.clone(),
+                        amounts,
+                        input_fee_ppk: *fee,
+                        use_keyset_v2: self.use_keyset_v2.unwrap_or(true),
+                    })
+                    .await?;
+            }
+        }
+
         #[cfg(feature = "auth")]
         if let Some(auth_localstore) = self.auth_localstore {
             return Mint::new_with_auth(

+ 2 - 0
crates/cdk/src/mint/keysets/mod.rs

@@ -77,6 +77,7 @@ impl Mint {
         unit: CurrencyUnit,
         amounts: Vec<u64>,
         input_fee_ppk: u64,
+        use_keyset_v2: bool,
     ) -> Result<MintKeySetInfo, Error> {
         let result = self
             .signatory
@@ -84,6 +85,7 @@ impl Mint {
                 unit,
                 amounts,
                 input_fee_ppk,
+                use_keyset_v2,
             })
             .await?;
 

File diff suppressed because it is too large
+ 16 - 2
crates/cdk/src/mint/mod.rs


Some files were not shown because too many files changed in this diff