فهرست منبع

Remove max_order from keyset database schema (#1329)

* Remove max_order from keyset database schema

Fixes #1074

Remove the max_order column from the keyset table across the mint database
layer, replacing it with the amounts field that was already being used. This
simplifies the schema by eliminating redundant data.

* Extract standard keyset amounts generation to reusable function

Consolidates repeated pattern of `(0..32).map(|n| 2u64.pow(n)).collect()` into
a single reusable helper function. This reduces code duplication and makes the
keyset amount generation pattern more maintainable.
C 1 ماه پیش
والد
کامیت
24f9a508d5

+ 7 - 2
crates/cdk-common/src/database/mint/test/mod.rs

@@ -22,6 +22,12 @@ mod proofs;
 pub use self::mint::*;
 pub use self::proofs::*;
 
+/// Generate standard keyset amounts as powers of 2
+#[inline]
+fn standard_keyset_amounts(max_order: u32) -> Vec<u64> {
+    (0..max_order).map(|n| 2u64.pow(n)).collect()
+}
+
 #[inline]
 async fn setup_keyset<DB>(db: &DB) -> Id
 where
@@ -36,9 +42,8 @@ where
         final_expiry: None,
         derivation_path: DerivationPath::from_str("m/0'/0'/0'").unwrap(),
         derivation_path_index: Some(0),
-        max_order: 32,
         input_fee_ppk: 0,
-        amounts: vec![],
+        amounts: standard_keyset_amounts(32),
     };
     let mut writer = db.begin_transaction().await.expect("db.begin()");
     writer.add_keyset_info(keyset_info).await.unwrap();

+ 0 - 2
crates/cdk-common/src/mint.rs

@@ -589,8 +589,6 @@ pub struct MintKeySetInfo {
     pub derivation_path: DerivationPath,
     /// DerivationPath index of Keyset
     pub derivation_path_index: Option<u32>,
-    /// Max order of keyset
-    pub max_order: u8,
     /// Supported amounts
     pub amounts: Vec<u64>,
     /// Input Fee ppk

+ 11 - 0
crates/cdk-integration-tests/src/lib.rs

@@ -36,6 +36,17 @@ pub mod init_pure_tests;
 pub mod init_regtest;
 pub mod shared;
 
+/// Generate standard keyset amounts as powers of 2
+///
+/// Returns a vector of amounts: [1, 2, 4, 8, 16, 32, ..., 2^(n-1)]
+/// where n is the number of amounts to generate.
+///
+/// # Arguments
+/// * `max_order` - The maximum power of 2 (exclusive). For example, max_order=32 generates amounts up to 2^31
+pub fn standard_keyset_amounts(max_order: u32) -> Vec<u64> {
+    (0..max_order).map(|n| 2u64.pow(n)).collect()
+}
+
 pub async fn fund_wallet(wallet: Arc<Wallet>, amount: Amount) {
     let quote = wallet
         .mint_quote(amount, None)

+ 15 - 3
crates/cdk-integration-tests/tests/integration_tests_pure.rs

@@ -565,7 +565,11 @@ async fn test_swap_overpay_underpay_fee() {
         .expect("Failed to create test mint");
 
     mint_bob
-        .rotate_keyset(CurrencyUnit::Sat, 32, 1)
+        .rotate_keyset(
+            CurrencyUnit::Sat,
+            cdk_integration_tests::standard_keyset_amounts(32),
+            1,
+        )
         .await
         .unwrap();
 
@@ -640,7 +644,11 @@ async fn test_mint_enforce_fee() {
         .expect("Failed to create test mint");
 
     mint_bob
-        .rotate_keyset(CurrencyUnit::Sat, 32, 1)
+        .rotate_keyset(
+            CurrencyUnit::Sat,
+            cdk_integration_tests::standard_keyset_amounts(32),
+            1,
+        )
         .await
         .unwrap();
 
@@ -749,7 +757,11 @@ async fn test_mint_change_with_fee_melt() {
         .expect("Failed to create test mint");
 
     mint_bob
-        .rotate_keyset(CurrencyUnit::Sat, 32, 1)
+        .rotate_keyset(
+            CurrencyUnit::Sat,
+            cdk_integration_tests::standard_keyset_amounts(32),
+            1,
+        )
         .await
         .unwrap();
 

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

@@ -74,7 +74,13 @@ async fn test_correct_keyset() {
         .expect("There is a keyset for unit");
     let old_keyset_info = mint.get_keyset_info(active).expect("There is keyset");
 
-    mint.rotate_keyset(CurrencyUnit::Sat, 32, 0).await.unwrap();
+    mint.rotate_keyset(
+        CurrencyUnit::Sat,
+        cdk_integration_tests::standard_keyset_amounts(32),
+        0,
+    )
+    .await
+    .unwrap();
 
     let active = mint.get_active_keysets();
 
@@ -86,7 +92,13 @@ async fn test_correct_keyset() {
 
     assert_ne!(keyset_info.id, old_keyset_info.id);
 
-    mint.rotate_keyset(CurrencyUnit::Sat, 32, 0).await.unwrap();
+    mint.rotate_keyset(
+        CurrencyUnit::Sat,
+        cdk_integration_tests::standard_keyset_amounts(32),
+        0,
+    )
+    .await
+    .unwrap();
 
     let active = mint.get_active_keysets();
 

+ 14 - 6
crates/cdk-integration-tests/tests/test_swap_flow.rs

@@ -657,9 +657,13 @@ async fn test_swap_with_fees() {
         .expect("Failed to create test wallet");
 
     // Rotate to keyset with 1 sat per proof fee
-    mint.rotate_keyset(CurrencyUnit::Sat, 32, 1)
-        .await
-        .expect("Failed to rotate keyset");
+    mint.rotate_keyset(
+        CurrencyUnit::Sat,
+        cdk_integration_tests::standard_keyset_amounts(32),
+        1,
+    )
+    .await
+    .expect("Failed to rotate keyset");
 
     // Fund with 1000 sats as individual 1-sat proofs using the fee-based keyset
     // Wait a bit for keyset to be available
@@ -973,9 +977,13 @@ async fn test_wallet_multi_keyset_counter_updates() {
     let first_keyset_id = get_keyset_id(&mint).await;
 
     // Rotate to a second keyset
-    mint.rotate_keyset(CurrencyUnit::Sat, 32, 0)
-        .await
-        .expect("Failed to rotate keyset");
+    mint.rotate_keyset(
+        CurrencyUnit::Sat,
+        cdk_integration_tests::standard_keyset_amounts(32),
+        0,
+    )
+    .await
+    .expect("Failed to rotate keyset");
 
     // Wait for keyset rotation to propagate
     tokio::time::sleep(std::time::Duration::from_millis(100)).await;

+ 14 - 5
crates/cdk-mint-rpc/src/mint_rpc_cli/subcommands/rotate_next_keyset.rs

@@ -16,9 +16,9 @@ pub struct RotateNextKeysetCommand {
     #[arg(short, long)]
     #[arg(default_value = "sat")]
     unit: String,
-    /// The maximum order (power of 2) for tokens that can be minted with this keyset
+    /// The amounts that can be minted with this keyset (e.g., "1,2,4,8,16")
     #[arg(short, long)]
-    max_order: Option<u8>,
+    amounts: Option<String>,
     /// The input fee in parts per thousand to apply when minting with this keyset
     #[arg(short, long)]
     input_fee_ppk: Option<u64>,
@@ -36,10 +36,19 @@ pub async fn rotate_next_keyset(
     client: &mut CdkMintClient<Channel>,
     sub_command_args: &RotateNextKeysetCommand,
 ) -> Result<()> {
+    let amounts = if let Some(amounts_str) = &sub_command_args.amounts {
+        amounts_str
+            .split(',')
+            .map(|s| s.trim().parse::<u64>())
+            .collect::<Result<Vec<u64>, _>>()?
+    } else {
+        vec![]
+    };
+
     let response = client
         .rotate_next_keyset(Request::new(RotateNextKeysetRequest {
             unit: sub_command_args.unit.clone(),
-            max_order: sub_command_args.max_order.map(|m| m.into()),
+            amounts,
             input_fee_ppk: sub_command_args.input_fee_ppk,
         }))
         .await?;
@@ -47,8 +56,8 @@ pub async fn rotate_next_keyset(
     let response = response.into_inner();
 
     println!(
-        "Rotated to new keyset {} for unit {} with a max order of {} and fee of {}",
-        response.id, response.unit, response.max_order, response.input_fee_ppk
+        "Rotated to new keyset {} for unit {} with amounts {:?} and fee of {}",
+        response.id, response.unit, response.amounts, response.input_fee_ppk
     );
 
     Ok(())

+ 2 - 2
crates/cdk-mint-rpc/src/proto/cdk-mint-rpc.proto

@@ -122,7 +122,7 @@ message UpdateNut04QuoteRequest {
 
 message RotateNextKeysetRequest {
     string unit = 1;
-    optional uint32 max_order = 2;
+    repeated uint64 amounts = 2;
     optional uint64 input_fee_ppk = 3;
 }
 
@@ -130,6 +130,6 @@ message RotateNextKeysetRequest {
 message RotateNextKeysetResponse {
     string id = 1;
     string unit = 2;
-    uint32 max_order = 3;
+    repeated uint64 amounts = 3;
     uint64 input_fee_ppk = 4;
 }

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

@@ -730,20 +730,22 @@ impl CdkMint for MintRPCServer {
         let unit = CurrencyUnit::from_str(&request.unit)
             .map_err(|_| Status::invalid_argument("Invalid unit".to_string()))?;
 
+        let amounts = if request.amounts.is_empty() {
+            return Err(Status::invalid_argument("amounts cannot be empty"));
+        } else {
+            request.amounts
+        };
+
         let keyset_info = self
             .mint
-            .rotate_keyset(
-                unit,
-                request.max_order.map(|a| a as u8).unwrap_or(32),
-                request.input_fee_ppk.unwrap_or(0),
-            )
+            .rotate_keyset(unit, amounts, request.input_fee_ppk.unwrap_or(0))
             .await
             .map_err(|_| Status::invalid_argument("Could not rotate keyset".to_string()))?;
 
         Ok(Response::new(RotateNextKeysetResponse {
             id: keyset_info.id.to_string(),
             unit: keyset_info.unit.to_string(),
-            max_order: keyset_info.max_order.into(),
+            amounts: keyset_info.amounts,
             input_fee_ppk: keyset_info.input_fee_ppk,
         }))
     }

+ 0 - 1
crates/cdk-signatory/src/common.rs

@@ -151,7 +151,6 @@ pub fn create_new_keyset<C: secp256k1::Signing>(
         final_expiry: keyset.final_expiry,
         derivation_path,
         derivation_path_index,
-        max_order: 0,
         amounts: amounts.to_owned(),
         input_fee_ppk,
     };

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

@@ -111,7 +111,6 @@ impl From<SignatoryKeySet> for MintKeySetInfo {
             input_fee_ppk: val.input_fee_ppk,
             derivation_path: Default::default(),
             derivation_path_index: Default::default(),
-            max_order: 0,
             amounts: val.amounts,
             final_expiry: val.final_expiry,
             valid_from: 0,

+ 2 - 0
crates/cdk-sql-common/src/mint/auth/migrations/postgres/20251122000000_drop_max_order.sql

@@ -0,0 +1,2 @@
+-- Drop max_order column from keyset table
+ALTER TABLE keyset DROP COLUMN IF EXISTS max_order;

+ 28 - 0
crates/cdk-sql-common/src/mint/auth/migrations/sqlite/20251122000000_drop_max_order.sql

@@ -0,0 +1,28 @@
+-- Drop max_order column from keyset table
+-- SQLite doesn't support DROP COLUMN directly, so we need to recreate the table
+
+-- Create new table without max_order
+CREATE TABLE keyset_new (
+    id TEXT PRIMARY KEY,
+    unit TEXT NOT NULL,
+    active BOOL NOT NULL,
+    valid_from INTEGER NOT NULL,
+    valid_to INTEGER,
+    derivation_path TEXT NOT NULL,
+    derivation_path_index INTEGER NOT NULL
+);
+
+-- Copy data from old table to new table
+INSERT INTO keyset_new (id, unit, active, valid_from, valid_to, derivation_path, derivation_path_index)
+SELECT id, unit, active, valid_from, valid_to, derivation_path, derivation_path_index
+FROM keyset;
+
+-- Drop old table
+DROP TABLE keyset;
+
+-- Rename new table to original name
+ALTER TABLE keyset_new RENAME TO keyset;
+
+-- Recreate indexes
+CREATE INDEX IF NOT EXISTS unit_index ON keyset(unit);
+CREATE INDEX IF NOT EXISTS active_index ON keyset(active);

+ 6 - 6
crates/cdk-sql-common/src/mint/auth/mod.rs

@@ -88,11 +88,11 @@ where
         INSERT INTO
             keyset (
                 id, unit, active, valid_from, valid_to, derivation_path,
-                max_order, derivation_path_index
+                amounts, input_fee_ppk, derivation_path_index
             )
         VALUES (
             :id, :unit, :active, :valid_from, :valid_to, :derivation_path,
-            :max_order, :derivation_path_index
+            :amounts, :input_fee_ppk, :derivation_path_index
         )
         ON CONFLICT(id) DO UPDATE SET
             unit = excluded.unit,
@@ -100,7 +100,8 @@ where
             valid_from = excluded.valid_from,
             valid_to = excluded.valid_to,
             derivation_path = excluded.derivation_path,
-            max_order = excluded.max_order,
+            amounts = excluded.amounts,
+            input_fee_ppk = excluded.input_fee_ppk,
             derivation_path_index = excluded.derivation_path_index
         "#,
         )?
@@ -110,7 +111,8 @@ where
         .bind("valid_from", keyset.valid_from as i64)
         .bind("valid_to", keyset.final_expiry.map(|v| v as i64))
         .bind("derivation_path", keyset.derivation_path.to_string())
-        .bind("max_order", keyset.max_order)
+        .bind("amounts", serde_json::to_string(&keyset.amounts).ok())
+        .bind("input_fee_ppk", keyset.input_fee_ppk as i64)
         .bind("derivation_path_index", keyset.derivation_path_index)
         .execute(&self.inner)
         .await?;
@@ -286,7 +288,6 @@ where
                 valid_to,
                 derivation_path,
                 derivation_path_index,
-                max_order,
                 amounts,
                 input_fee_ppk
             FROM
@@ -311,7 +312,6 @@ where
                 valid_to,
                 derivation_path,
                 derivation_path_index,
-                max_order,
                 amounts,
                 input_fee_ppk
             FROM

+ 2 - 0
crates/cdk-sql-common/src/mint/migrations/postgres/20251122000000_drop_max_order.sql

@@ -0,0 +1,2 @@
+-- Drop max_order column from keyset table
+ALTER TABLE keyset DROP COLUMN IF EXISTS max_order;

+ 30 - 0
crates/cdk-sql-common/src/mint/migrations/sqlite/20251122000000_drop_max_order.sql

@@ -0,0 +1,30 @@
+-- Drop max_order column from keyset table
+-- SQLite doesn't support DROP COLUMN directly, so we need to recreate the table
+
+-- Create new table without max_order
+CREATE TABLE keyset_new (
+    id TEXT PRIMARY KEY,
+    unit TEXT NOT NULL,
+    active BOOL NOT NULL,
+    valid_from INTEGER NOT NULL,
+    valid_to INTEGER,
+    derivation_path TEXT NOT NULL,
+    input_fee_ppk INTEGER,
+    derivation_path_index INTEGER,
+    amounts TEXT
+);
+
+-- Copy data from old table to new table
+INSERT INTO keyset_new (id, unit, active, valid_from, valid_to, derivation_path, input_fee_ppk, derivation_path_index, amounts)
+SELECT id, unit, active, valid_from, valid_to, derivation_path, input_fee_ppk, derivation_path_index, amounts
+FROM keyset;
+
+-- Drop old table
+DROP TABLE keyset;
+
+-- Rename new table to original name
+ALTER TABLE keyset_new RENAME TO keyset;
+
+-- Recreate indexes
+CREATE INDEX IF NOT EXISTS unit_index ON keyset(unit);
+CREATE INDEX IF NOT EXISTS active_index ON keyset(active);

+ 9 - 82
crates/cdk-sql-common/src/mint/mod.rs

@@ -591,11 +591,11 @@ where
         INSERT INTO
             keyset (
                 id, unit, active, valid_from, valid_to, derivation_path,
-                max_order, amounts, input_fee_ppk, derivation_path_index
+                amounts, input_fee_ppk, derivation_path_index
             )
         VALUES (
             :id, :unit, :active, :valid_from, :valid_to, :derivation_path,
-            :max_order, :amounts, :input_fee_ppk, :derivation_path_index
+            :amounts, :input_fee_ppk, :derivation_path_index
         )
         ON CONFLICT(id) DO UPDATE SET
             unit = excluded.unit,
@@ -603,7 +603,6 @@ where
             valid_from = excluded.valid_from,
             valid_to = excluded.valid_to,
             derivation_path = excluded.derivation_path,
-            max_order = excluded.max_order,
             amounts = excluded.amounts,
             input_fee_ppk = excluded.input_fee_ppk,
             derivation_path_index = excluded.derivation_path_index
@@ -615,7 +614,6 @@ where
         .bind("valid_from", keyset.valid_from as i64)
         .bind("valid_to", keyset.final_expiry.map(|v| v as i64))
         .bind("derivation_path", keyset.derivation_path.to_string())
-        .bind("max_order", keyset.max_order)
         .bind("amounts", serde_json::to_string(&keyset.amounts).ok())
         .bind("input_fee_ppk", keyset.input_fee_ppk as i64)
         .bind("derivation_path_index", keyset.derivation_path_index)
@@ -707,7 +705,6 @@ where
                 valid_to,
                 derivation_path,
                 derivation_path_index,
-                max_order,
                 amounts,
                 input_fee_ppk
             FROM
@@ -732,7 +729,6 @@ where
                 valid_to,
                 derivation_path,
                 derivation_path_index,
-                max_order,
                 amounts,
                 input_fee_ppk
             FROM
@@ -2316,16 +2312,14 @@ fn sql_row_to_keyset_info(row: Vec<Column>) -> Result<MintKeySetInfo, Error> {
             valid_to,
             derivation_path,
             derivation_path_index,
-            max_order,
             amounts,
             row_keyset_ppk
         ) = row
     );
 
-    let max_order: u8 = column_as_number!(max_order);
     let amounts = column_as_nullable_string!(amounts)
         .and_then(|str| serde_json::from_str(&str).ok())
-        .unwrap_or_else(|| (0..max_order).map(|m| 2u64.pow(m.into())).collect());
+        .ok_or_else(|| Error::Database("amounts field is required".to_string().into()))?;
 
     Ok(MintKeySetInfo {
         id: column_as_string!(id, Id::from_str, Id::from_bytes),
@@ -2334,7 +2328,6 @@ fn sql_row_to_keyset_info(row: Vec<Column>) -> Result<MintKeySetInfo, Error> {
         valid_from: column_as_number!(valid_from),
         derivation_path: column_as_string!(derivation_path, DerivationPath::from_str),
         derivation_path_index: column_as_nullable_number!(derivation_path_index),
-        max_order,
         amounts,
         input_fee_ppk: column_as_number!(row_keyset_ppk),
         final_expiry: column_as_nullable_number!(valid_to),
@@ -2629,77 +2622,13 @@ fn sql_row_to_saga(row: Vec<Column>) -> Result<mint::Saga, Error> {
 mod test {
     use super::*;
 
-    mod max_order_to_amounts_migrations {
+    mod keyset_amounts_tests {
         use super::*;
 
         #[test]
-        fn legacy_payload() {
-            let result = sql_row_to_keyset_info(vec![
-                Column::Text("0083a60439303340".to_owned()),
-                Column::Text("sat".to_owned()),
-                Column::Integer(1),
-                Column::Integer(1749844864),
-                Column::Null,
-                Column::Text("0'/0'/0'".to_owned()),
-                Column::Integer(0),
-                Column::Integer(32),
-                Column::Null,
-                Column::Integer(0),
-            ]);
-            assert!(result.is_ok());
-        }
-
-        #[test]
-        fn migrated_payload() {
-            let legacy = sql_row_to_keyset_info(vec![
-                Column::Text("0083a60439303340".to_owned()),
-                Column::Text("sat".to_owned()),
-                Column::Integer(1),
-                Column::Integer(1749844864),
-                Column::Null,
-                Column::Text("0'/0'/0'".to_owned()),
-                Column::Integer(0),
-                Column::Integer(32),
-                Column::Null,
-                Column::Integer(0),
-            ]);
-            assert!(legacy.is_ok());
-
+        fn keyset_with_amounts() {
             let amounts = (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>();
-            let migrated = sql_row_to_keyset_info(vec![
-                Column::Text("0083a60439303340".to_owned()),
-                Column::Text("sat".to_owned()),
-                Column::Integer(1),
-                Column::Integer(1749844864),
-                Column::Null,
-                Column::Text("0'/0'/0'".to_owned()),
-                Column::Integer(0),
-                Column::Integer(32),
-                Column::Text(serde_json::to_string(&amounts).expect("valid json")),
-                Column::Integer(0),
-            ]);
-            assert!(migrated.is_ok());
-            assert_eq!(legacy.unwrap(), migrated.unwrap());
-        }
-
-        #[test]
-        fn amounts_over_max_order() {
-            let legacy = sql_row_to_keyset_info(vec![
-                Column::Text("0083a60439303340".to_owned()),
-                Column::Text("sat".to_owned()),
-                Column::Integer(1),
-                Column::Integer(1749844864),
-                Column::Null,
-                Column::Text("0'/0'/0'".to_owned()),
-                Column::Integer(0),
-                Column::Integer(32),
-                Column::Null,
-                Column::Integer(0),
-            ]);
-            assert!(legacy.is_ok());
-
-            let amounts = (0..16).map(|x| 2u64.pow(x)).collect::<Vec<_>>();
-            let migrated = sql_row_to_keyset_info(vec![
+            let result = sql_row_to_keyset_info(vec![
                 Column::Text("0083a60439303340".to_owned()),
                 Column::Text("sat".to_owned()),
                 Column::Integer(1),
@@ -2707,14 +2636,12 @@ mod test {
                 Column::Null,
                 Column::Text("0'/0'/0'".to_owned()),
                 Column::Integer(0),
-                Column::Integer(32),
                 Column::Text(serde_json::to_string(&amounts).expect("valid json")),
                 Column::Integer(0),
             ]);
-            assert!(migrated.is_ok());
-            let migrated = migrated.unwrap();
-            assert_ne!(legacy.unwrap(), migrated);
-            assert_eq!(migrated.amounts.len(), 16);
+            assert!(result.is_ok());
+            let keyset = result.unwrap();
+            assert_eq!(keyset.amounts.len(), 32);
         }
     }
 }

+ 1 - 1
crates/cdk/src/mint/builder.rs

@@ -302,7 +302,7 @@ impl MintBuilder {
     ///
     /// The unit **MUST** already have been added with a ln backend
     pub fn set_unit_fee(&mut self, unit: &CurrencyUnit, input_fee_ppk: u64) -> Result<(), Error> {
-        let (input_fee, _max_order) = self
+        let (input_fee, _) = self
             .supported_units
             .get_mut(unit)
             .ok_or(Error::UnsupportedUnit)?;

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

@@ -75,14 +75,14 @@ impl Mint {
     pub async fn rotate_keyset(
         &self,
         unit: CurrencyUnit,
-        max_order: u8,
+        amounts: Vec<u64>,
         input_fee_ppk: u64,
     ) -> Result<MintKeySetInfo, Error> {
         let result = self
             .signatory
             .rotate_keyset(RotateKeyArguments {
                 unit,
-                amounts: (0..max_order).map(|n| 2u64.pow(n.into())).collect(),
+                amounts,
                 input_fee_ppk,
             })
             .await?;

+ 1 - 1
crates/cdk/src/mint/mod.rs

@@ -1071,7 +1071,7 @@ mod tests {
         let first_keyset_id = keysets.keysets[0].id;
 
         // set the first keyset to inactive and generate a new keyset
-        mint.rotate_keyset(CurrencyUnit::default(), 1, 1)
+        mint.rotate_keyset(CurrencyUnit::default(), vec![1], 1)
             .await
             .expect("test");