thesimplekid 1 year ago
parent
commit
c4bff1a724
14 changed files with 733 additions and 131 deletions
  1. 2 1
      Cargo.toml
  2. 21 0
      LICENSES/cashu-rs-MIT
  3. 5 4
      integration_test/src/main.rs
  4. 87 0
      src/amount.rs
  5. 4 3
      src/cashu_wallet.rs
  6. 7 5
      src/client.rs
  7. 62 52
      src/dhke.rs
  8. 12 0
      src/error.rs
  9. 155 41
      src/keyset.rs
  10. 5 1
      src/lib.rs
  11. 285 0
      src/mint.rs
  12. 22 0
      src/serde_utils.rs
  13. 54 23
      src/types.rs
  14. 12 1
      src/utils.rs

+ 2 - 1
Cargo.toml

@@ -15,7 +15,7 @@ members = ["integration_test"]
 
 [dependencies]
 base64 = "0.21.0"
-bitcoin = { version = "0.30.0", features=["serde"] }
+bitcoin = { version = "0.30.0", features=["serde",  "rand"] }
 bitcoin_hashes = "0.12.0"
 hex = "0.4.3"
 k256 = { version = "0.13.1", features=["arithmetic"] }
@@ -27,6 +27,7 @@ serde = { version = "1.0.160", features = ["derive"]}
 serde_json = "1.0.96"
 url = "2.3.1"
 regex = "1.8.4"
+log = "0.4.19"
 
 [dev-dependencies]
 tokio = {version = "1.27.0", features = ["rt", "macros"] }

+ 21 - 0
LICENSES/cashu-rs-MIT

@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2023 Clark Moody
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 5 - 4
integration_test/src/main.rs

@@ -4,11 +4,12 @@ use std::str::FromStr;
 use std::thread;
 use std::time::Duration;
 
-use bitcoin::Amount;
 use cashu_crab::cashu_wallet::CashuWallet;
 use cashu_crab::client::Client;
 use cashu_crab::keyset::Keys;
-use cashu_crab::types::{Invoice, MintProofs, Proofs, Token};
+use cashu_crab::mint;
+use cashu_crab::types::{self, Invoice, MintProofs, Proofs, Token};
+use cashu_crab::Amount;
 
 const MINTURL: &str = "https://testnut.cashu.space";
 
@@ -68,7 +69,7 @@ async fn test_get_mint_keysets(client: &Client) {
 
 async fn test_request_mint(wallet: &CashuWallet) {
     let mint = wallet
-        .request_mint(Amount::from_sat(MINTAMOUNT))
+        .request_mint(Amount::from_sat(MINTAMOUNT).into())
         .await
         .unwrap();
 
@@ -120,7 +121,7 @@ async fn test_receive(wallet: &CashuWallet, token: &str) -> String {
     s.unwrap()
 }
 
-async fn test_check_spendable(client: &Client, token: &str) -> Proofs {
+async fn test_check_spendable(client: &Client, token: &str) -> mint::Proofs {
     let mint_keys = client.get_keys().await.unwrap();
 
     let wallet = CashuWallet::new(client.to_owned(), mint_keys);

+ 87 - 0
src/amount.rs

@@ -0,0 +1,87 @@
+// https://github.com/clarkmoody/cashu-rs
+use serde::{Deserialize, Serialize};
+
+/// Number of satoshis
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
+#[serde(transparent)]
+pub struct Amount(#[serde(with = "bitcoin::amount::serde::as_sat")] bitcoin::Amount);
+
+impl Amount {
+    pub const ZERO: Amount = Amount(bitcoin::Amount::ZERO);
+
+    /// Split into parts that are powers of two
+    pub fn split(&self) -> Vec<Self> {
+        let sats = self.0.to_sat();
+        (0_u64..64)
+            .into_iter()
+            .rev()
+            .filter_map(|bit| {
+                let part = 1 << bit;
+                ((sats & part) == part).then_some(Self::from(part))
+            })
+            .collect()
+    }
+
+    pub fn to_sat(&self) -> u64 {
+        self.0.to_sat()
+    }
+
+    pub fn to_msat(&self) -> u64 {
+        self.0.to_sat() * 1000
+    }
+
+    pub fn from_sat(sat: u64) -> Self {
+        Self(bitcoin::Amount::from_sat(sat))
+    }
+
+    pub fn from_msat(msat: u64) -> Self {
+        Self(bitcoin::Amount::from_sat(msat / 1000))
+    }
+}
+
+impl Default for Amount {
+    fn default() -> Self {
+        Amount::ZERO
+    }
+}
+
+impl From<u64> for Amount {
+    fn from(value: u64) -> Self {
+        Self(bitcoin::Amount::from_sat(value))
+    }
+}
+
+impl From<Amount> for u64 {
+    fn from(value: Amount) -> Self {
+        value.0.to_sat()
+    }
+}
+
+impl std::ops::Add for Amount {
+    type Output = Amount;
+
+    fn add(self, rhs: Amount) -> Self::Output {
+        Amount(self.0 + rhs.0)
+    }
+}
+
+impl std::ops::AddAssign for Amount {
+    fn add_assign(&mut self, rhs: Self) {
+        self.0 += rhs.0;
+    }
+}
+
+impl std::ops::Sub for Amount {
+    type Output = Amount;
+
+    fn sub(self, rhs: Amount) -> Self::Output {
+        Amount(self.0 - rhs.0)
+    }
+}
+
+impl core::iter::Sum for Amount {
+    fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
+        let sats: u64 = iter.map(|amt| amt.0.to_sat()).sum();
+        Amount::from(sats)
+    }
+}

+ 4 - 3
src/cashu_wallet.rs

@@ -1,20 +1,21 @@
 //! Cashu Wallet
 use std::str::FromStr;
 
-use bitcoin::Amount;
-
 pub use crate::Invoice;
 use crate::{
     client::Client,
     dhke::construct_proofs,
     error::Error,
     keyset::Keys,
+    mint,
     types::{
         BlindedMessages, Melted, Proofs, ProofsStatus, RequestMintResponse, SendProofs,
         SplitPayload, SplitRequest, Token,
     },
 };
 
+use crate::amount::Amount;
+
 #[derive(Clone, Debug)]
 pub struct CashuWallet {
     pub client: Client,
@@ -34,7 +35,7 @@ impl CashuWallet {
     // TODO: getter method for keys that if it cant get them try again
 
     /// Check if a proof is spent
-    pub async fn check_proofs_spent(&self, proofs: &Proofs) -> Result<ProofsStatus, Error> {
+    pub async fn check_proofs_spent(&self, proofs: &mint::Proofs) -> Result<ProofsStatus, Error> {
         let spendable = self.client.check_spendable(proofs).await?;
 
         // Separate proofs in spent and unspent based on mint response

+ 7 - 5
src/client.rs

@@ -1,13 +1,14 @@
 //! Client to connet to mint
 use std::fmt;
 
-use bitcoin::Amount;
 use serde_json::Value;
 use url::Url;
 
+use crate::amount::Amount;
 pub use crate::Invoice;
 use crate::{
-    keyset::{Keys, MintKeySets},
+    keyset::{self, Keys},
+    mint,
     types::{
         BlindedMessage, BlindedMessages, CheckFeesRequest, CheckFeesResponse,
         CheckSpendableRequest, CheckSpendableResponse, MeltRequest, MeltResponse, MintInfo,
@@ -139,11 +140,12 @@ impl Client {
     }
 
     /// Get Keysets [NUT-02]
-    pub async fn get_keysets(&self) -> Result<MintKeySets, Error> {
+    pub async fn get_keysets(&self) -> Result<keyset::Response, Error> {
         let url = self.mint_url.join("keysets")?;
         let res = minreq::get(url).send()?.json::<Value>()?;
 
-        let response: Result<MintKeySets, serde_json::Error> = serde_json::from_value(res.clone());
+        let response: Result<keyset::Response, serde_json::Error> =
+            serde_json::from_value(res.clone());
 
         match response {
             Ok(res) => Ok(res),
@@ -266,7 +268,7 @@ impl Client {
     /// Spendable check [NUT-07]
     pub async fn check_spendable(
         &self,
-        proofs: &Vec<Proof>,
+        proofs: &Vec<mint::Proof>,
     ) -> Result<CheckSpendableResponse, Error> {
         let url = self.mint_url.join("check")?;
         let request = CheckSpendableRequest {

+ 62 - 52
src/dhke.rs

@@ -4,18 +4,19 @@ use std::ops::Mul;
 
 use bitcoin_hashes::sha256;
 use bitcoin_hashes::Hash;
-use k256::{ProjectivePoint, PublicKey, Scalar, SecretKey};
+use k256::{ProjectivePoint, Scalar, SecretKey};
 
 use crate::error::Error;
-use crate::keyset::Keys;
+use crate::keyset;
+use crate::keyset::{Keys, PublicKey};
 use crate::types::{Promise, Proof, Proofs};
 
-fn hash_to_curve(message: &[u8]) -> PublicKey {
+fn hash_to_curve(message: &[u8]) -> k256::PublicKey {
     let mut msg_to_hash = message.to_vec();
 
     loop {
         let hash = sha256::Hash::hash(&msg_to_hash);
-        match PublicKey::from_sec1_bytes(
+        match k256::PublicKey::from_sec1_bytes(
             &[0x02u8]
                 .iter()
                 .chain(&hash.to_byte_array())
@@ -42,7 +43,7 @@ pub fn blind_message(
 
     let b = ProjectivePoint::from(y) + ProjectivePoint::from(&r.public_key());
 
-    Ok((PublicKey::try_from(b)?, r))
+    Ok((k256::PublicKey::try_from(b)?.into(), r))
 }
 
 /// Unblind Message (Alice Step 3)
@@ -55,18 +56,18 @@ pub fn unblind_message(
 ) -> Result<PublicKey, Error> {
     // C
     // Unblinded message
-    let c = ProjectivePoint::from(blinded_key.as_affine())
-        - mint_pubkey
+    let c = ProjectivePoint::from(Into::<k256::PublicKey>::into(blinded_key).as_affine())
+        - Into::<k256::PublicKey>::into(mint_pubkey)
             .as_affine()
             .mul(Scalar::from(r.as_scalar_primitive()));
 
-    Ok(PublicKey::try_from(c)?)
+    Ok(k256::PublicKey::try_from(c)?.into())
 }
 
 /// Construct Proof
 pub fn construct_proofs(
     promises: Vec<Promise>,
-    rs: Vec<SecretKey>,
+    rs: Vec<keyset::SecretKey>,
     secrets: Vec<String>,
     keys: &Keys,
 ) -> Result<Proofs, Error> {
@@ -78,7 +79,7 @@ pub fn construct_proofs(
             .ok_or(Error::CustomError("Could not get proofs".to_string()))?
             .to_owned();
 
-        let unblinded_signature = unblind_message(blinded_c, rs[i].clone(), a)?;
+        let unblinded_signature = unblind_message(blinded_c, rs[i].clone().into(), a)?;
 
         let proof = Proof {
             id: Some(promise.id),
@@ -94,6 +95,37 @@ pub fn construct_proofs(
     Ok(proofs)
 }
 
+/// Sign Blinded Message (Step2 bob)
+pub fn sign_message(
+    a: SecretKey,
+    blinded_message: k256::PublicKey,
+) -> Result<k256::PublicKey, Error> {
+    Ok(k256::PublicKey::try_from(
+        blinded_message
+            .as_affine()
+            .mul(Scalar::from(a.as_scalar_primitive())),
+    )
+    .unwrap())
+}
+
+/// Verify Message
+pub fn verify_message(
+    a: SecretKey,
+    unblinded_message: k256::PublicKey,
+    msg: &str,
+) -> Result<(), Error> {
+    // Y
+    let y = hash_to_curve(msg.as_bytes());
+
+    if unblinded_message
+        == k256::PublicKey::try_from(*y.as_affine() * Scalar::from(a.as_scalar_primitive()))?
+    {
+        return Ok(());
+    }
+
+    Err(Error::TokenNotVerifed)
+}
+
 #[cfg(test)]
 mod tests {
     use hex::decode;
@@ -109,7 +141,7 @@ mod tests {
         let sec_hex = decode(secret).unwrap();
 
         let y = hash_to_curve(&sec_hex);
-        let expected_y = PublicKey::from_sec1_bytes(
+        let expected_y = k256::PublicKey::from_sec1_bytes(
             &hex::decode("0266687aadf862bd776c8fc18b8e9f8e20089714856ee233b3902a591d0d5f2925")
                 .unwrap(),
         )
@@ -119,7 +151,7 @@ mod tests {
         let secret = "0000000000000000000000000000000000000000000000000000000000000001";
         let sec_hex = decode(secret).unwrap();
         let y = hash_to_curve(&sec_hex);
-        let expected_y = PublicKey::from_sec1_bytes(
+        let expected_y = k256::PublicKey::from_sec1_bytes(
             &hex::decode("02ec4916dd28fc4c10d78e287ca5d9cc51ee1ae73cbfde08c6b37324cbfaac8bc5")
                 .unwrap(),
         )
@@ -136,11 +168,12 @@ mod tests {
 
         assert_eq!(
             b,
-            PublicKey::from_sec1_bytes(
+            k256::PublicKey::from_sec1_bytes(
                 &hex::decode("02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2")
                     .unwrap()
             )
             .unwrap()
+            .into()
         );
 
         assert_eq!(r, sec);
@@ -157,11 +190,11 @@ mod tests {
         let bob_sec = SecretKey::new(ScalarPrimitive::ONE);
 
         // C_
-        let signed = sign_message(bob_sec, blinded_message).unwrap();
+        let signed = sign_message(bob_sec, blinded_message.into()).unwrap();
 
         assert_eq!(
             signed,
-            PublicKey::from_sec1_bytes(
+            k256::PublicKey::from_sec1_bytes(
                 &hex::decode("02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2")
                     .unwrap()
             )
@@ -171,58 +204,35 @@ mod tests {
 
     #[test]
     fn test_unblind_message() {
-        let blinded_key = PublicKey::from_sec1_bytes(
+        let blinded_key = k256::PublicKey::from_sec1_bytes(
             &hex::decode("02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2")
                 .unwrap(),
         )
         .unwrap();
 
         let r = SecretKey::new(ScalarPrimitive::ONE);
-        let a = PublicKey::from_sec1_bytes(
+        let a = k256::PublicKey::from_sec1_bytes(
             &hex::decode("020000000000000000000000000000000000000000000000000000000000000001")
                 .unwrap(),
         )
         .unwrap();
 
-        let unblinded = unblind_message(blinded_key, r, a).unwrap();
+        let unblinded = unblind_message(blinded_key.into(), r, a.into()).unwrap();
 
         assert_eq!(
-            PublicKey::from_sec1_bytes(
-                &hex::decode("03c724d7e6a5443b39ac8acf11f40420adc4f99a02e7cc1b57703d9391f6d129cd")
+            Into::<PublicKey>::into(
+                k256::PublicKey::from_sec1_bytes(
+                    &hex::decode(
+                        "03c724d7e6a5443b39ac8acf11f40420adc4f99a02e7cc1b57703d9391f6d129cd"
+                    )
                     .unwrap()
-            )
-            .unwrap(),
+                )
+                .unwrap()
+            ),
             unblinded
         );
     }
 
-    /// Sign Blinded Message (Step2 bob)
-    // Really only needed for mint
-    // Used here for testing
-    fn sign_message(a: SecretKey, blinded_message: PublicKey) -> Result<PublicKey, Error> {
-        Ok(PublicKey::try_from(
-            blinded_message
-                .as_affine()
-                .mul(Scalar::from(a.as_scalar_primitive())),
-        )
-        .unwrap())
-    }
-
-    /// Verify Message
-    // Really only needed for mint
-    // used for testing
-    fn verify_message(
-        a: SecretKey,
-        unblinded_message: PublicKey,
-        msg: &str,
-    ) -> Result<bool, Error> {
-        // Y
-        let y = hash_to_curve(msg.as_bytes());
-
-        Ok(unblinded_message
-            == PublicKey::try_from(*y.as_affine() * Scalar::from(a.as_scalar_primitive())).unwrap())
-    }
-
     #[ignore]
     #[test]
     fn test_blinded_dhke() {
@@ -243,11 +253,11 @@ mod tests {
         let blinded = blind_message(&y.to_sec1_bytes(), None).unwrap();
 
         // C_
-        let signed = sign_message(bob_sec.clone(), blinded.0).unwrap();
+        let signed = sign_message(bob_sec.clone(), blinded.0.into()).unwrap();
 
         // C
-        let c = unblind_message(signed, blinded.1, bob_pub).unwrap();
+        let c = unblind_message(signed.into(), blinded.1, bob_pub.into()).unwrap();
 
-        assert!(verify_message(bob_sec, c, &x).unwrap());
+        assert!(verify_message(bob_sec, c.into(), &x).is_ok());
     }
 }

+ 12 - 0
src/error.rs

@@ -23,6 +23,12 @@ pub enum Error {
     HexError(hex::FromHexError),
     /// From elliptic curve
     EllipticError(k256::elliptic_curve::Error),
+    AmountKey,
+    Amount,
+    TokenSpent,
+    TokenNotVerifed,
+    OutputOrdering,
+    InvoiceAmountUndefined,
     CrabMintError(crate::client::Error),
 }
 
@@ -39,7 +45,13 @@ impl fmt::Display for Error {
             Error::CustomError(err) => write!(f, "{}", err),
             Error::HexError(err) => write!(f, "{}", err),
             Error::EllipticError(err) => write!(f, "{}", err),
+            Error::AmountKey => write!(f, "No Key for amount"),
+            Error::Amount => write!(f, "Amount miss match"),
+            Error::TokenSpent => write!(f, "Token Spent"),
+            Error::TokenNotVerifed => write!(f, "Token Not Verified"),
             Error::CrabMintError(err) => write!(f, "{}", err),
+            Error::OutputOrdering => write!(f, "Output ordering"),
+            Error::InvoiceAmountUndefined => write!(f, "Invoice without amount"),
         }
     }
 }

+ 155 - 41
src/keyset.rs

@@ -2,65 +2,56 @@
 
 use std::collections::BTreeMap;
 use std::collections::HashMap;
+use std::collections::HashSet;
 
 use base64::{engine::general_purpose, Engine as _};
 use bitcoin::hashes::sha256::Hash as Sha256;
 use bitcoin::hashes::Hash;
-use k256::PublicKey;
-use serde::de;
-use serde::{Deserialize, Serialize, Serializer};
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct PublicKeyWrapper(pub PublicKey);
-
-impl Serialize for PublicKeyWrapper {
-    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
-    where
-        S: Serializer,
-    {
-        serializer.serialize_str(&hex::encode(self.0.to_sec1_bytes()))
+use serde::{Deserialize, Serialize};
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(transparent)]
+pub struct PublicKey(#[serde(with = "crate::serde_utils::serde_public_key")] k256::PublicKey);
+
+impl From<PublicKey> for k256::PublicKey {
+    fn from(value: PublicKey) -> k256::PublicKey {
+        value.0
     }
 }
 
-impl<'de> Deserialize<'de> for PublicKeyWrapper {
-    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
-    where
-        D: serde::Deserializer<'de>,
-    {
-        struct PublicKeyWrapperVisitor;
-
-        impl<'de> de::Visitor<'de> for PublicKeyWrapperVisitor {
-            type Value = PublicKeyWrapper;
+impl From<k256::PublicKey> for PublicKey {
+    fn from(value: k256::PublicKey) -> Self {
+        Self(value)
+    }
+}
 
-            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
-                formatter.write_str("a hexadecimal string representing a public key")
-            }
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(transparent)]
+pub struct SecretKey(#[serde(with = "crate::serde_utils::serde_secret_key")] k256::SecretKey);
 
-            fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
-            where
-                E: de::Error,
-            {
-                let bytes = hex::decode(value).map_err(E::custom)?;
-                let public_key = PublicKey::from_sec1_bytes(&bytes).map_err(E::custom)?;
-                Ok(PublicKeyWrapper(public_key))
-            }
-        }
+impl From<SecretKey> for k256::SecretKey {
+    fn from(value: SecretKey) -> k256::SecretKey {
+        value.0
+    }
+}
 
-        deserializer.deserialize_str(PublicKeyWrapperVisitor)
+impl From<k256::SecretKey> for SecretKey {
+    fn from(value: k256::SecretKey) -> Self {
+        Self(value)
     }
 }
 
 /// Mint Keys [NUT-01]
 #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
-pub struct Keys(BTreeMap<u64, PublicKeyWrapper>);
+pub struct Keys(BTreeMap<u64, PublicKey>);
 
 impl Keys {
-    pub fn new(keys: BTreeMap<u64, PublicKeyWrapper>) -> Self {
+    pub fn new(keys: BTreeMap<u64, PublicKey>) -> Self {
         Self(keys)
     }
 
     pub fn amount_key(&self, amount: &u64) -> Option<PublicKey> {
-        self.0.get(amount).map(|key| key.0.to_owned())
+        self.0.get(amount).cloned()
     }
 
     pub fn as_hashmap(&self) -> HashMap<u64, String> {
@@ -90,11 +81,134 @@ impl Keys {
     }
 }
 
-/// Mint Keysets [UT-02]
+/// Mint Keysets [NUT-02]
 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
-pub struct MintKeySets {
+pub struct Response {
     /// set of public keys that the mint generates
-    pub keysets: Vec<String>,
+    pub keysets: HashSet<String>,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
+pub struct KeySet {
+    pub id: String,
+    pub keys: Keys,
+}
+
+impl From<mint::Keys> for Keys {
+    fn from(keys: mint::Keys) -> Self {
+        Self(
+            keys.0
+                .iter()
+                .map(|(amount, keypair)| (*amount, keypair.public_key.clone()))
+                .collect(),
+        )
+    }
+}
+
+impl From<mint::KeySet> for KeySet {
+    fn from(keyset: mint::KeySet) -> Self {
+        Self {
+            id: keyset.id,
+            keys: Keys::from(keyset.keys),
+        }
+    }
+}
+
+pub mod mint {
+    use std::collections::BTreeMap;
+
+    use base64::{engine::general_purpose, Engine as _};
+    use bitcoin::hashes::sha256::Hash as Sha256;
+    use bitcoin_hashes::Hash;
+    use bitcoin_hashes::HashEngine;
+    use k256::SecretKey;
+    use serde::Deserialize;
+    use serde::Serialize;
+
+    use super::PublicKey;
+    use crate::serde_utils;
+
+    #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+    pub struct Keys(pub BTreeMap<u64, KeyPair>);
+
+    #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+    pub struct KeySet {
+        pub id: String,
+        pub keys: Keys,
+    }
+
+    impl KeySet {
+        pub fn generate(
+            secret: impl Into<String>,
+            derivation_path: impl Into<String>,
+            max_order: u8,
+        ) -> Self {
+            // Elliptic curve math context
+
+            /* NUT-02 § 2.1
+                for i in range(MAX_ORDER):
+                    k_i = HASH_SHA256(s + D + i)[:32]
+            */
+
+            let mut map = BTreeMap::new();
+
+            // SHA-256 midstate, for quicker hashing
+            let mut engine = Sha256::engine();
+            engine.input(secret.into().as_bytes());
+            engine.input(derivation_path.into().as_bytes());
+
+            for i in 0..max_order {
+                let amount = 2_u64.pow(i as u32);
+
+                // Reuse midstate
+                let mut e = engine.clone();
+                e.input(i.to_string().as_bytes());
+                let hash = Sha256::from_engine(e);
+                let secret_key = SecretKey::from_slice(&hash.to_byte_array()).unwrap();
+                let keypair = KeyPair::from_secret_key(secret_key);
+                map.insert(amount, keypair);
+            }
+
+            Self {
+                id: Self::id(&map),
+                keys: Keys(map),
+            }
+        }
+
+        fn id(map: &BTreeMap<u64, KeyPair>) -> String {
+            /* 1 - sort keyset by amount
+             * 2 - concatenate all (sorted) public keys to one string
+             * 3 - HASH_SHA256 the concatenated public keys
+             * 4 - take the first 12 characters of the hash
+             */
+
+            let pubkeys_concat = map
+                .values()
+                .map(|keypair| hex::encode(&keypair.public_key.0.to_sec1_bytes()))
+                .collect::<Vec<String>>()
+                .join("");
+
+            let hash = general_purpose::STANDARD.encode(Sha256::hash(pubkeys_concat.as_bytes()));
+
+            hash[0..12].to_string()
+        }
+    }
+
+    #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+    pub struct KeyPair {
+        pub public_key: PublicKey,
+        #[serde(with = "serde_utils::serde_secret_key")]
+        pub secret_key: SecretKey,
+    }
+
+    impl KeyPair {
+        fn from_secret_key(secret_key: SecretKey) -> Self {
+            Self {
+                public_key: secret_key.public_key().into(),
+                secret_key,
+            }
+        }
+    }
 }
 
 #[cfg(test)]

+ 5 - 1
src/lib.rs

@@ -1,13 +1,17 @@
+pub mod amount;
 pub mod cashu_wallet;
 pub mod client;
 pub mod dhke;
 pub mod error;
 pub mod keyset;
+pub mod mint;
 pub mod serde_utils;
 pub mod types;
 pub mod utils;
 
-pub use bitcoin::Amount;
+pub use amount::Amount;
+pub use bitcoin::hashes::sha256::Hash as Sha256;
+pub use lightning_invoice;
 pub use lightning_invoice::Invoice;
 
 pub type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;

+ 285 - 0
src/mint.rs

@@ -0,0 +1,285 @@
+use std::collections::{HashMap, HashSet};
+
+use serde::{Deserialize, Serialize};
+
+use crate::dhke::verify_message;
+use crate::error::Error;
+use crate::types::{
+    self, BlindedMessage, CheckSpendableRequest, CheckSpendableResponse, MeltRequest, MeltResponse,
+    PostMintResponse, Promise, SplitRequest, SplitResponse,
+};
+use crate::Amount;
+use crate::{
+    dhke::sign_message,
+    keyset::{
+        self,
+        mint::{self, KeySet},
+        PublicKey,
+    },
+    types::MintRequest,
+};
+
+pub struct Mint {
+    pub active_keyset: KeySet,
+    pub inactive_keysets: HashMap<String, mint::KeySet>,
+    pub spent_secrets: HashSet<String>,
+}
+
+impl Mint {
+    pub fn new(
+        secret: &str,
+        derivation_path: &str,
+        inactive_keysets: HashMap<String, mint::KeySet>,
+        spent_secrets: HashSet<String>,
+        max_order: u8,
+    ) -> Self {
+        Self {
+            active_keyset: keyset::mint::KeySet::generate(secret, derivation_path, max_order),
+            inactive_keysets,
+            spent_secrets,
+        }
+    }
+
+    /// Retrieve the public keys of the active keyset for distribution to
+    /// wallet clients
+    pub fn active_keyset_pubkeys(&self) -> keyset::KeySet {
+        keyset::KeySet::from(self.active_keyset.clone())
+    }
+
+    /// Return a list of all supported keysets
+    pub fn keysets(&self) -> keyset::Response {
+        let mut keysets: HashSet<_> = self.inactive_keysets.keys().cloned().collect();
+        keysets.insert(self.active_keyset.id.clone());
+        keyset::Response { keysets }
+    }
+
+    pub fn active_keyset(&self) -> keyset::mint::KeySet {
+        self.active_keyset.clone()
+    }
+
+    pub fn keyset(&self, id: &str) -> Option<keyset::KeySet> {
+        if &self.active_keyset.id == id {
+            return Some(self.active_keyset.clone().into());
+        }
+
+        self.inactive_keysets.get(id).map(|k| k.clone().into())
+    }
+
+    pub fn process_mint_request(
+        &mut self,
+        mint_request: MintRequest,
+    ) -> Result<PostMintResponse, Error> {
+        let mut blind_signatures = Vec::with_capacity(mint_request.outputs.len());
+
+        for blinded_message in mint_request.outputs {
+            blind_signatures.push(self.blind_sign(&blinded_message)?);
+        }
+
+        Ok(PostMintResponse {
+            promises: blind_signatures,
+        })
+    }
+
+    fn blind_sign(&self, blinded_message: &BlindedMessage) -> Result<Promise, Error> {
+        let BlindedMessage { amount, b } = blinded_message;
+
+        let Some(key_pair) = self.active_keyset.keys.0.get(&amount.to_sat()) else {
+            // No key for amount
+            return Err(Error::AmountKey);
+        };
+
+        let c = sign_message(key_pair.secret_key.clone(), b.clone().into())?;
+
+        Ok(Promise {
+            amount: amount.clone(),
+            c: c.into(),
+            id: self.active_keyset.id.clone(),
+        })
+    }
+
+    fn create_split_response(
+        &self,
+        amount: Amount,
+        outputs: &[BlindedMessage],
+    ) -> Result<SplitResponse, Error> {
+        let mut target_total = Amount::ZERO;
+        let mut change_total = Amount::ZERO;
+        let mut target = Vec::with_capacity(outputs.len());
+        let mut change = Vec::with_capacity(outputs.len());
+
+        // Create sets of target and change amounts that we're looking for
+        // in the outputs (blind messages). As we loop, take from those sets,
+        // target amount first.
+        for output in outputs {
+            let signed = self.blind_sign(&output)?;
+
+            // Accumulate outputs into the target (send) list
+            if target_total + signed.amount <= amount {
+                target_total += signed.amount;
+                target.push(signed);
+            } else {
+                change_total += signed.amount;
+                change.push(signed);
+            }
+        }
+
+        Ok(SplitResponse {
+            fst: change,
+            snd: target,
+        })
+    }
+
+    pub fn process_split_request(
+        &mut self,
+        split_request: SplitRequest,
+    ) -> Result<SplitResponse, Error> {
+        let proofs_total = split_request.proofs_amount();
+        if proofs_total < split_request.amount {
+            return Err(Error::Amount);
+        }
+
+        let output_total = split_request.output_amount();
+        if output_total < split_request.amount {
+            return Err(Error::Amount);
+        }
+
+        if proofs_total != output_total {
+            return Err(Error::Amount);
+        }
+
+        let mut secrets = Vec::with_capacity(split_request.proofs.len());
+        for proof in &split_request.proofs {
+            secrets.push(self.verify_proof(proof)?);
+        }
+
+        let mut split_response =
+            self.create_split_response(split_request.amount, &split_request.outputs)?;
+
+        if split_response.target_amount() != split_request.amount {
+            let mut outputs = split_request.outputs;
+            outputs.reverse();
+            split_response = self.create_split_response(split_request.amount, &outputs)?;
+        }
+
+        if split_response.target_amount() != split_request.amount {
+            return Err(Error::OutputOrdering);
+        }
+
+        for secret in secrets {
+            self.spent_secrets.insert(secret);
+        }
+
+        Ok(split_response)
+    }
+
+    fn verify_proof(&self, proof: &types::Proof) -> Result<String, Error> {
+        if self.spent_secrets.contains(&proof.secret) {
+            return Err(Error::TokenSpent);
+        }
+
+        let keyset = proof.id.as_ref().map_or_else(
+            || &self.active_keyset,
+            |id| {
+                if let Some(keyset) = self.inactive_keysets.get(id) {
+                    keyset
+                } else {
+                    &self.active_keyset
+                }
+            },
+        );
+
+        let Some(keypair) = keyset.keys.0.get(&proof.amount.to_sat()) else {
+            return Err(Error::AmountKey);
+        };
+
+        verify_message(
+            keypair.secret_key.to_owned(),
+            proof.c.clone().into(),
+            &proof.secret,
+        )?;
+
+        Ok(proof.secret.clone())
+    }
+
+    pub fn check_spendable(
+        &self,
+        check_spendable: &CheckSpendableRequest,
+    ) -> Result<CheckSpendableResponse, Error> {
+        let mut spendable = vec![];
+        for proof in &check_spendable.proofs {
+            spendable.push(self.spent_secrets.contains(&proof.secret))
+        }
+
+        Ok(CheckSpendableResponse { spendable })
+    }
+
+    pub fn verify_melt_request(&mut self, melt_request: &MeltRequest) -> Result<(), Error> {
+        let proofs_total = melt_request.proofs_amount();
+
+        // TODO: Fee reserve
+        if proofs_total < melt_request.invoice_amount()? {
+            return Err(Error::Amount);
+        }
+
+        let mut secrets = Vec::with_capacity(melt_request.proofs.len());
+        for proof in &melt_request.proofs {
+            secrets.push(self.verify_proof(&proof)?);
+        }
+
+        Ok(())
+    }
+
+    pub fn process_melt_request(
+        &mut self,
+        melt_request: &MeltRequest,
+        preimage: &str,
+        total_spent: Amount,
+    ) -> Result<MeltResponse, Error> {
+        let secrets = Vec::with_capacity(melt_request.proofs.len());
+        for secret in secrets {
+            self.spent_secrets.insert(secret);
+        }
+
+        let change_target = melt_request.proofs_amount() - total_spent;
+        let amounts = change_target.split();
+        let mut change = Vec::with_capacity(amounts.len());
+
+        if let Some(outputs) = &melt_request.outputs {
+            for (i, amount) in amounts.iter().enumerate() {
+                let mut message = outputs[i].clone();
+
+                message.amount = amount.clone();
+
+                let signature = self.blind_sign(&message)?;
+                change.push(signature)
+            }
+        }
+
+        Ok(MeltResponse {
+            paid: true,
+            preimage: Some(preimage.to_string()),
+            change: Some(change),
+        })
+    }
+}
+
+/// Proofs [NUT-00]
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub struct Proof {
+    /// Amount in satoshi
+    pub amount: Option<Amount>,
+    /// Secret message
+    // #[serde(with = "crate::serde_utils::bytes_base64")]
+    pub secret: String,
+    /// Unblinded signature
+    #[serde(rename = "C")]
+    pub c: Option<PublicKey>,
+    /// `Keyset id`
+    pub id: Option<String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    /// P2SHScript that specifies the spending condition for this Proof
+    pub script: Option<String>,
+}
+
+/// List of proofs
+pub type Proofs = Vec<Proof>;

+ 22 - 0
src/serde_utils.rs

@@ -100,3 +100,25 @@ pub mod serde_public_key {
         }
     }
 }
+
+pub mod serde_secret_key {
+    use k256::SecretKey;
+    use serde::Deserialize;
+
+    pub fn serialize<S>(pubkey: &SecretKey, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: serde::Serializer,
+    {
+        let encoded = hex::encode(pubkey.to_bytes());
+        serializer.serialize_str(&encoded)
+    }
+
+    pub fn deserialize<'de, D>(deserializer: D) -> Result<SecretKey, D::Error>
+    where
+        D: serde::Deserializer<'de>,
+    {
+        let encoded = String::deserialize(deserializer)?;
+        let decoded = hex::decode(encoded).map_err(serde::de::Error::custom)?;
+        SecretKey::from_slice(&decoded).map_err(serde::de::Error::custom)
+    }
+}

+ 54 - 23
src/types.rs

@@ -3,38 +3,34 @@
 use std::str::FromStr;
 
 use base64::{engine::general_purpose, Engine as _};
-use bitcoin::Amount;
-use k256::{PublicKey, SecretKey};
 use serde::{Deserialize, Deserializer, Serialize, Serializer};
 use url::Url;
 
+use crate::keyset::{self, PublicKey};
 use crate::utils::generate_secret;
+use crate::Amount;
 pub use crate::Invoice;
-use crate::{
-    dhke::blind_message, error::Error, serde_utils, serde_utils::serde_url, utils::split_amount,
-};
+use crate::{dhke::blind_message, error::Error, mint, serde_utils::serde_url, utils::split_amount};
 
 /// Blinded Message [NUT-00]
 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
 pub struct BlindedMessage {
     /// Amount in satoshi
-    #[serde(with = "bitcoin::amount::serde::as_sat")]
     pub amount: Amount,
     /// encrypted secret message (B_)
     #[serde(rename = "B_")]
-    #[serde(with = "serde_utils::serde_public_key")]
     pub b: PublicKey,
 }
 
 /// Blinded Messages [NUT-00]
-#[derive(Debug, Default, Clone, PartialEq, Eq)]
+#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
 pub struct BlindedMessages {
     /// Blinded messages
     pub blinded_messages: Vec<BlindedMessage>,
     /// Secrets
     pub secrets: Vec<String>,
     /// Rs
-    pub rs: Vec<SecretKey>,
+    pub rs: Vec<keyset::SecretKey>,
     /// Amounts
     pub amounts: Vec<Amount>,
 }
@@ -52,7 +48,7 @@ impl BlindedMessages {
 
             blinded_messages.secrets.push(secret);
             blinded_messages.blinded_messages.push(blinded_message);
-            blinded_messages.rs.push(r);
+            blinded_messages.rs.push(r.into());
             blinded_messages.amounts.push(amount);
         }
 
@@ -63,6 +59,8 @@ impl BlindedMessages {
     pub fn blank(fee_reserve: Amount) -> Result<Self, Error> {
         let mut blinded_messages = BlindedMessages::default();
 
+        let fee_reserve = bitcoin::Amount::from_sat(fee_reserve.to_sat());
+
         let count = (fee_reserve
             .to_float_in(bitcoin::Denomination::Satoshi)
             .log2()
@@ -80,7 +78,7 @@ impl BlindedMessages {
 
             blinded_messages.secrets.push(secret);
             blinded_messages.blinded_messages.push(blinded_message);
-            blinded_messages.rs.push(r);
+            blinded_messages.rs.push(r.into());
             blinded_messages.amounts.push(Amount::ZERO);
         }
 
@@ -88,7 +86,7 @@ impl BlindedMessages {
     }
 }
 
-#[derive(Debug, Clone, PartialEq, Eq)]
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
 pub struct SplitPayload {
     pub keep_blinded_messages: BlindedMessages,
     pub send_blinded_messages: BlindedMessages,
@@ -99,11 +97,9 @@ pub struct SplitPayload {
 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
 pub struct Promise {
     pub id: String,
-    #[serde(with = "bitcoin::amount::serde::as_sat")]
     pub amount: Amount,
     /// blinded signature (C_) on the secret message `B_` of [BlindedMessage]
     #[serde(rename = "C_")]
-    #[serde(with = "serde_utils::serde_public_key")]
     pub c: PublicKey,
 }
 
@@ -111,14 +107,12 @@ pub struct Promise {
 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
 pub struct Proof {
     /// Amount in satoshi
-    #[serde(with = "bitcoin::amount::serde::as_sat")]
     pub amount: Amount,
     /// Secret message
     // #[serde(with = "crate::serde_utils::bytes_base64")]
     pub secret: String,
     /// Unblinded signature
     #[serde(rename = "C")]
-    #[serde(with = "serde_utils::serde_public_key")]
     pub c: PublicKey,
     /// `Keyset id`
     pub id: Option<String>,
@@ -145,6 +139,15 @@ pub struct MintRequest {
     pub outputs: Vec<BlindedMessage>,
 }
 
+impl MintRequest {
+    pub fn total_amount(&self) -> Amount {
+        self.outputs
+            .iter()
+            .map(|BlindedMessage { amount, .. }| *amount)
+            .sum()
+    }
+}
+
 /// Post Mint Response [NUT-05]
 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
 pub struct PostMintResponse {
@@ -152,11 +155,9 @@ pub struct PostMintResponse {
 }
 
 /// Check Fees Response [NUT-05]
-
 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
 pub struct CheckFeesResponse {
     /// Expected Mac Fee in satoshis    
-    #[serde(with = "bitcoin::amount::serde::as_sat")]
     pub fee: Amount,
 }
 
@@ -178,6 +179,19 @@ pub struct MeltRequest {
     pub outputs: Option<Vec<BlindedMessage>>,
 }
 
+impl MeltRequest {
+    pub fn proofs_amount(&self) -> Amount {
+        self.proofs.iter().map(|proof| proof.amount).sum()
+    }
+
+    pub fn invoice_amount(&self) -> Result<Amount, Error> {
+        match self.pr.amount_milli_satoshis() {
+            Some(value) => Ok(Amount::from_sat(value)),
+            None => Err(Error::InvoiceAmountUndefined),
+        }
+    }
+}
+
 /// Melt Response [NUT-05]
 /// Lightning fee return [NUT-08] if change is defined
 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
@@ -197,12 +211,20 @@ pub struct Melted {
 /// Split Request [NUT-06]
 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
 pub struct SplitRequest {
-    #[serde(with = "bitcoin::amount::serde::as_sat")]
     pub amount: Amount,
     pub proofs: Proofs,
     pub outputs: Vec<BlindedMessage>,
 }
 
+impl SplitRequest {
+    pub fn proofs_amount(&self) -> Amount {
+        self.proofs.iter().map(|proof| proof.amount).sum()
+    }
+    pub fn output_amount(&self) -> Amount {
+        self.outputs.iter().map(|proof| proof.amount).sum()
+    }
+}
+
 /// Split Response [NUT-06]
 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
 pub struct SplitResponse {
@@ -212,10 +234,20 @@ pub struct SplitResponse {
     pub snd: Vec<Promise>,
 }
 
+impl SplitResponse {
+    pub fn change_amount(&self) -> Amount {
+        self.fst.iter().map(|Promise { amount, .. }| *amount).sum()
+    }
+
+    pub fn target_amount(&self) -> Amount {
+        self.snd.iter().map(|Promise { amount, .. }| *amount).sum()
+    }
+}
+
 /// Check spendabale request [NUT-07]
 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
 pub struct CheckSpendableRequest {
-    pub proofs: Proofs,
+    pub proofs: mint::Proofs,
 }
 
 /// Check Spendable Response [NUT-07]
@@ -228,8 +260,8 @@ pub struct CheckSpendableResponse {
 
 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
 pub struct ProofsStatus {
-    pub spendable: Proofs,
-    pub spent: Proofs,
+    pub spendable: mint::Proofs,
+    pub spent: mint::Proofs,
 }
 
 #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
@@ -278,7 +310,6 @@ pub struct MintInfo {
     /// name of the mint and should be recognizable
     pub name: Option<String>,
     /// hex pubkey of the mint
-    #[serde(with = "serde_utils::serde_public_key::opt")]
     pub pubkey: Option<PublicKey>,
     /// implementation name and the version running
     pub version: Option<MintVersion>,

+ 12 - 1
src/utils.rs

@@ -1,10 +1,13 @@
 //! Utils
 
 use base64::{engine::general_purpose, Engine as _};
-use bitcoin::Amount;
+use bitcoin::hashes::sha256::Hash as Sha256;
+use bitcoin::hashes::Hash;
 use rand::prelude::*;
 use regex::Regex;
 
+use crate::amount::Amount;
+
 /// Split amount into cashu denominations (powers of 2)
 pub fn split_amount(amount: Amount) -> Vec<Amount> {
     let mut chunks = Vec::new();
@@ -34,6 +37,14 @@ pub fn generate_secret() -> String {
     general_purpose::STANDARD.encode(secret)
 }
 
+pub fn random_hash() -> Vec<u8> {
+    let mut rng = rand::thread_rng();
+    let mut random_bytes = [0u8; Sha256::LEN];
+    rng.fill_bytes(&mut random_bytes);
+    let hash = Sha256::hash(&random_bytes);
+    hash.to_byte_array().to_vec()
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;