Просмотр исходного кода

Keyset ID V2 Implementation (NUT-02) (#1505)

---------

Co-authored-by: thesimplekid <tsk@thesimplekid.com>
callebtc 1 неделя назад
Родитель
Сommit
fac0610537

+ 56 - 27
crates/cashu/src/nuts/nut02.rs

@@ -162,27 +162,35 @@ impl Id {
     ///
     /// This function will not panic under normal circumstances as the hash output
     /// is always valid hex and the correct length.
-    pub fn v2_from_data(map: &Keys, unit: &CurrencyUnit, expiry: Option<u64>) -> Self {
+    pub fn v2_from_data(
+        map: &Keys,
+        unit: &CurrencyUnit,
+        input_fee_ppk: u64,
+        expiry: Option<u64>,
+    ) -> Self {
         let mut keys: Vec<(&Amount, &super::PublicKey)> = map.iter().collect();
         keys.sort_by_key(|(amt, _v)| *amt);
 
-        let mut pubkeys_concat: Vec<u8> = keys
+        let keys_string = keys
             .iter()
-            .map(|(_, pubkey)| pubkey.to_bytes())
-            .collect::<Vec<[u8; 33]>>()
-            .concat();
+            .map(|(amt, pubkey)| format!("{}:{}", amt, hex::encode(pubkey.to_bytes())))
+            .collect::<Vec<String>>()
+            .join(",");
+
+        let mut data = keys_string;
+        data.push_str(&format!("|unit:{}", unit));
 
-        // Add the unit
-        pubkeys_concat.extend(b"unit:");
-        pubkeys_concat.extend(unit.to_string().to_lowercase().as_bytes());
+        if input_fee_ppk > 0 {
+            data.push_str(&format!("|input_fee_ppk:{}", input_fee_ppk));
+        }
 
-        // Add the expiration
         if let Some(expiry) = expiry {
-            pubkeys_concat.extend(b"final_expiry:");
-            pubkeys_concat.extend(expiry.to_string().as_bytes());
+            if expiry > 0 {
+                data.push_str(&format!("|final_expiry:{}", expiry));
+            }
         }
 
-        let hash = Sha256::hash(&pubkeys_concat);
+        let hash = Sha256::hash(data.as_bytes());
         let hex_of_hash = hex::encode(hash.to_byte_array());
 
         Self {
@@ -472,14 +480,11 @@ pub struct KeySet {
     /// Keyset state - indicates whether the mint will sign new outputs with this keyset
     #[serde(skip_serializing_if = "Option::is_none")]
     pub active: Option<bool>,
-    /// Input fee in parts per thousand (ppk) per input spent from this keyset
-    #[serde(
-        deserialize_with = "deserialize_input_fee_ppk",
-        default = "default_input_fee_ppk"
-    )]
-    pub input_fee_ppk: u64,
     /// Keyset [`Keys`]
     pub keys: Keys,
+    /// Input Fee PPK
+    #[serde(default)]
+    pub input_fee_ppk: u64,
     /// Expiry
     #[serde(skip_serializing_if = "Option::is_none")]
     pub final_expiry: Option<u64>,
@@ -490,7 +495,12 @@ impl KeySet {
     pub fn verify_id(&self) -> Result<(), Error> {
         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),
+            KeySetVersion::Version01 => Id::v2_from_data(
+                &self.keys,
+                &self.unit,
+                self.input_fee_ppk,
+                self.final_expiry,
+            ),
         };
 
         ensure_cdk!(
@@ -572,6 +582,9 @@ pub struct MintKeySet {
     pub unit: CurrencyUnit,
     /// Keyset [`MintKeys`]
     pub keys: MintKeys,
+    /// Input Fee PPK
+    #[serde(default)]
+    pub input_fee_ppk: u64,
     #[serde(skip_serializing_if = "Option::is_none")]
     /// Expiry [`Option<u64>`]
     pub final_expiry: Option<u64>,
@@ -590,6 +603,7 @@ impl MintKeySet {
         xpriv: Xpriv,
         unit: CurrencyUnit,
         amounts: &[u64],
+        input_fee_ppk: u64,
         final_expiry: Option<u64>,
         version: KeySetVersion,
     ) -> Self {
@@ -615,12 +629,15 @@ impl MintKeySet {
         let keys = MintKeys::new(map);
         let id = match version {
             KeySetVersion::Version00 => Id::v1_from_keys(&keys.clone().into()),
-            KeySetVersion::Version01 => Id::v2_from_data(&keys.clone().into(), &unit, final_expiry),
+            KeySetVersion::Version01 => {
+                Id::v2_from_data(&keys.clone().into(), &unit, input_fee_ppk, final_expiry)
+            }
         };
         Self {
             id,
             unit,
             keys,
+            input_fee_ppk,
             final_expiry,
         }
     }
@@ -631,12 +648,14 @@ impl MintKeySet {
     ///
     /// This function will panic if the RNG fails or if key derivation fails,
     /// which should not happen under normal circumstances.
+    #[allow(clippy::too_many_arguments)]
     pub fn generate_from_seed<C: secp256k1::Signing>(
         secp: &Secp256k1<C>,
         seed: &[u8],
         amounts: &[u64],
         currency_unit: CurrencyUnit,
         derivation_path: DerivationPath,
+        input_fee_ppk: u64,
         final_expiry: Option<u64>,
         version: KeySetVersion,
     ) -> Self {
@@ -648,6 +667,7 @@ impl MintKeySet {
                 .expect("RNG busted"),
             currency_unit,
             amounts,
+            input_fee_ppk,
             final_expiry,
             version,
         )
@@ -659,12 +679,14 @@ impl MintKeySet {
     ///
     /// This function will panic if the RNG fails or if key derivation fails,
     /// which should not happen under normal circumstances.
+    #[allow(clippy::too_many_arguments)]
     pub fn generate_from_xpriv<C: secp256k1::Signing>(
         secp: &Secp256k1<C>,
         xpriv: Xpriv,
         amounts: &[u64],
         currency_unit: CurrencyUnit,
         derivation_path: DerivationPath,
+        input_fee_ppk: u64,
         final_expiry: Option<u64>,
         version: KeySetVersion,
     ) -> Self {
@@ -675,6 +697,7 @@ impl MintKeySet {
                 .expect("RNG busted"),
             currency_unit,
             amounts,
+            input_fee_ppk,
             final_expiry,
             version,
         )
@@ -687,7 +710,12 @@ impl From<MintKeySet> for Id {
         let keys: Keys = keyset.keys.into();
         match keyset.id.version {
             KeySetVersion::Version00 => Id::v1_from_keys(&keys),
-            KeySetVersion::Version01 => Id::v2_from_data(&keys, &keyset.unit, keyset.final_expiry),
+            KeySetVersion::Version01 => Id::v2_from_data(
+                &keys,
+                &keyset.unit,
+                keyset.input_fee_ppk,
+                keyset.final_expiry,
+            ),
         }
     }
 }
@@ -814,24 +842,25 @@ mod test {
     fn test_v2_deserialization_and_id_generation() {
         let unit: CurrencyUnit = CurrencyUnit::from_str("sat").unwrap();
         let expiry: u64 = 2059210353; // +10 years from now
+        let input_fee_ppk = 100;
 
         let keys: Keys = serde_json::from_str(SHORT_KEYSET).unwrap();
         let id_from_str =
-            Id::from_str("01adc013fa9d85171586660abab27579888611659d357bc86bc09cb26eee8bc035")
+            Id::from_str("015ba18a8adcd02e715a58358eb618da4a4b3791151a4bee5e968bb88406ccf76a")
                 .unwrap();
-        let id = Id::v2_from_data(&keys, &unit, Some(expiry));
+        let id = Id::v2_from_data(&keys, &unit, input_fee_ppk, Some(expiry));
         assert_eq!(id, id_from_str);
 
         let keys: Keys = serde_json::from_str(KEYSET).unwrap();
         let id_from_str =
-            Id::from_str("0125bc634e270ad7e937af5b957f8396bb627d73f6e1fd2ffe4294c26b57daf9e0")
+            Id::from_str("01ab6aa4ff30390da34986d84be5274b48ad7a74265d791095bfc39f4098d9764f")
                 .unwrap();
-        let id = Id::v2_from_data(&keys, &unit, Some(expiry));
+        let id = Id::v2_from_data(&keys, &unit, 0, Some(expiry));
         assert_eq!(id, id_from_str);
 
-        let id = Id::v2_from_data(&keys, &unit, None);
+        let id = Id::v2_from_data(&keys, &unit, 0, None);
         let id_from_str =
-            Id::from_str("016d72f27c8d22808ad66d1959b3dab83af17e2510db7ffd57d2365d9eec3ced75")
+            Id::from_str("012fbb01a4e200c76df911eeba3b8fe1831202914b24664f4bccbd25852a6708f8")
                 .unwrap();
         assert_eq!(id, id_from_str);
     }

+ 3 - 3
crates/cdk-common/src/database/wallet/test/mod.rs

@@ -286,8 +286,8 @@ where
         id: keyset_id,
         unit: CurrencyUnit::Sat,
         active: None,
-        input_fee_ppk: 0,
         keys: keys.clone(),
+        input_fee_ppk: 0,
         final_expiry: None,
     };
 
@@ -312,8 +312,8 @@ where
         id: keyset_id,
         unit: CurrencyUnit::Sat,
         active: None,
-        input_fee_ppk: 0,
         keys: keys.clone(),
+        input_fee_ppk: 0,
         final_expiry: None,
     };
 
@@ -338,8 +338,8 @@ where
         id: keyset_id,
         unit: CurrencyUnit::Sat,
         active: None,
-        input_fee_ppk: 0,
         keys,
+        input_fee_ppk: 0,
         final_expiry: None,
     };
 

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

@@ -58,7 +58,7 @@ pub async fn init_keysets(
 
             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
+                    && 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");
@@ -69,6 +69,7 @@ pub async fn init_keysets(
                         &highest_index_keyset.amounts,
                         highest_index_keyset.unit.clone(),
                         highest_index_keyset.derivation_path.clone(),
+                        highest_index_keyset.input_fee_ppk,
                         highest_index_keyset.final_expiry,
                         cdk_common::nut02::KeySetVersion::Version00,
                     );
@@ -139,6 +140,7 @@ pub fn create_new_keyset<C: secp256k1::Signing>(
             .expect("RNG busted"),
         unit,
         amounts,
+        input_fee_ppk,
         final_expiry,
         // TODO: change this to Version01 to generate keysets v2
         cdk_common::nut02::KeySetVersion::Version00,

+ 3 - 0
crates/cdk-signatory/src/db_signatory.rs

@@ -144,6 +144,7 @@ impl DbSignatory {
             &keyset_info.amounts,
             keyset_info.unit.clone(),
             keyset_info.derivation_path.clone(),
+            keyset_info.input_fee_ppk,
             keyset_info.final_expiry,
             keyset_info.id.get_version(),
         )
@@ -286,6 +287,7 @@ mod test {
             &[1, 2],
             CurrencyUnit::Sat,
             derivation_path_from_unit(CurrencyUnit::Sat, 0).unwrap(),
+            0,
             None,
             cdk_common::nut02::KeySetVersion::Version00,
         );
@@ -332,6 +334,7 @@ mod test {
             &[1, 2],
             CurrencyUnit::Sat,
             derivation_path_from_unit(CurrencyUnit::Sat, 0).unwrap(),
+            0,
             None,
             cdk_common::nut02::KeySetVersion::Version00,
         );

+ 1 - 1
crates/cdk-signatory/src/proto/convert.rs

@@ -317,7 +317,6 @@ impl TryInto<cdk_common::KeySet> for KeySet {
                 .try_into()
                 .map_err(|_| cdk_common::Error::Custom("Invalid unit encoding".to_owned()))?,
             active: Some(self.active),
-            input_fee_ppk: self.input_fee_ppk,
             keys: cdk_common::Keys::new(
                 self.keys
                     .ok_or(cdk_common::error::Error::Custom(INTERNAL_ERROR.to_owned()))?
@@ -326,6 +325,7 @@ impl TryInto<cdk_common::KeySet> for KeySet {
                     .map(|(k, v)| cdk_common::PublicKey::from_slice(&v).map(|pk| (k.into(), pk)))
                     .collect::<Result<BTreeMap<cdk_common::Amount, cdk_common::PublicKey>, _>>()?,
             ),
+            input_fee_ppk: self.input_fee_ppk,
             final_expiry: self.final_expiry,
         })
     }

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

@@ -73,7 +73,7 @@ pub struct SignatoryKeySet {
     pub keys: Keys,
     /// Amounts supported by the keyset
     pub amounts: Vec<u64>,
-    /// Information about the fee per public key
+    /// Input fee for the keyset (parts per thousand)
     pub input_fee_ppk: u64,
     /// Final expiry of the keyset (unix timestamp in the future)
     pub final_expiry: Option<u64>,
@@ -91,8 +91,8 @@ impl From<SignatoryKeySet> for KeySet {
             id: val.id,
             unit: val.unit,
             active: Some(val.active),
-            input_fee_ppk: val.input_fee_ppk,
             keys: val.keys,
+            input_fee_ppk: val.input_fee_ppk,
             final_expiry: val.final_expiry,
         }
     }

+ 1 - 1
crates/cdk-sql-common/src/mint/keys.rs

@@ -45,7 +45,7 @@ pub(crate) fn sql_row_to_keyset_info(row: Vec<Column>) -> Result<MintKeySetInfo,
         derivation_path: column_as_string!(derivation_path, DerivationPath::from_str),
         derivation_path_index: column_as_nullable_number!(derivation_path_index),
         amounts,
-        input_fee_ppk: column_as_number!(row_keyset_ppk),
+        input_fee_ppk: column_as_nullable_number!(row_keyset_ppk).unwrap_or(0),
         final_expiry: column_as_nullable_number!(valid_to),
     })
 }

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

@@ -1368,7 +1368,7 @@ fn sql_row_to_keyset(row: Vec<Column>) -> Result<KeySetInfo, Error> {
         id: column_as_string!(id, Id::from_str, Id::from_bytes),
         unit: column_as_string!(unit, CurrencyUnit::from_str),
         active: matches!(active, Column::Integer(1)),
-        input_fee_ppk: column_as_nullable_number!(input_fee_ppk).unwrap_or_default(),
+        input_fee_ppk: column_as_nullable_number!(input_fee_ppk).unwrap_or(0),
         final_expiry: column_as_nullable_number!(final_expiry),
     })
 }

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
crates/cdk/src/mint/mod.rs


+ 1 - 1
crates/cdk/src/wallet/auth/auth_wallet.rs

@@ -390,7 +390,7 @@ impl AuthWallet {
             keysets
                 .get(&active_keyset_id)
                 .map(|x| x.input_fee_ppk)
-                .unwrap_or_default(),
+                .unwrap_or(0),
             self.load_keyset_keys(active_keyset_id)
                 .await?
                 .iter()

Некоторые файлы не были показаны из-за большого количества измененных файлов