Explorar o código

feat: nut13 restore for active keyset

thesimplekid hai 1 ano
pai
achega
c4389079f3

+ 2 - 1
crates/cashu-sdk/Cargo.toml

@@ -14,9 +14,10 @@ default = ["mint", "wallet", "all-nuts", "redb"]
 mint = ["cashu/mint"]
 wallet = ["cashu/wallet", "dep:minreq", "dep:once_cell"]
 gloo = ["dep:gloo"]
-all-nuts = ["nut07", "nut08", "nut10", "nut11", "nut13"]
+all-nuts = ["nut07", "nut08", "nut09", "nut10", "nut11", "nut13"]
 nut07 = ["cashu/nut07"]
 nut08 = ["cashu/nut08"]
+nut09 = ["cashu/nut09"]
 nut10 = ["cashu/nut10"]
 nut11 = ["cashu/nut11"]
 nut13 = ["cashu/nut13"]

+ 30 - 0
crates/cashu-sdk/src/client/gloo_client.rs

@@ -1,6 +1,8 @@
 //! gloo wasm http Client
 
 use async_trait::async_trait;
+#[cfg(feature = "nut09")]
+use cashu::nuts::nut09::{RestoreRequest, RestoreResponse};
 use cashu::nuts::{
     BlindedMessage, MeltBolt11Request, MeltBolt11Response, MintBolt11Request, MintBolt11Response,
     MintInfo, PreMintSecrets, Proof, SwapRequest, SwapResponse, *,
@@ -254,4 +256,32 @@ impl Client for HttpClient {
             Err(_) => Err(Error::from_json(&res.to_string())?),
         }
     }
+
+    /// Restore [NUT-09]
+    #[cfg(feature = "nut09")]
+    async fn post_check_state(
+        &self,
+        mint_url: Url,
+        request: RestoreRequest,
+    ) -> Result<CheckStateResponse, Error> {
+        let url = join_url(mint_url, &["v1", "check"])?;
+
+        let res = Request::post(url.as_str())
+            .json(&request)
+            .map_err(|err| Error::Gloo(err.to_string()))?
+            .send()
+            .await
+            .map_err(|err| Error::Gloo(err.to_string()))?
+            .json::<Value>()
+            .await
+            .map_err(|err| Error::Gloo(err.to_string()))?;
+
+        let response: Result<RestoreRequest, serde_json::Error> =
+            serde_json::from_value(res.clone());
+
+        match response {
+            Ok(res) => Ok(res),
+            Err(_) => Err(Error::from_json(&res.to_string())?),
+        }
+    }
 }

+ 24 - 0
crates/cashu-sdk/src/client/minreq_client.rs

@@ -2,6 +2,8 @@
 
 use async_trait::async_trait;
 use cashu::error::ErrorResponse;
+#[cfg(feature = "nut09")]
+use cashu::nuts::nut09::{RestoreRequest, RestoreResponse};
 #[cfg(feature = "nut07")]
 use cashu::nuts::PublicKey;
 use cashu::nuts::{
@@ -221,4 +223,26 @@ impl Client for HttpClient {
             Err(_) => Err(ErrorResponse::from_json(&res.to_string())?.into()),
         }
     }
+
+    #[cfg(feature = "nut09")]
+    async fn post_restore(
+        &self,
+        mint_url: Url,
+        request: RestoreRequest,
+    ) -> Result<RestoreResponse, Error> {
+        let url = join_url(mint_url, &["v1", "restore"])?;
+
+        let res = minreq::post(url)
+            .with_json(&request)?
+            .send()?
+            .json::<Value>()?;
+
+        let response: Result<RestoreResponse, serde_json::Error> =
+            serde_json::from_value(res.clone());
+
+        match response {
+            Ok(res) => Ok(res),
+            Err(_) => Err(ErrorResponse::from_json(&res.to_string())?.into()),
+        }
+    }
 }

+ 9 - 0
crates/cashu-sdk/src/client/mod.rs

@@ -2,6 +2,8 @@
 
 use async_trait::async_trait;
 use cashu::error::ErrorResponse;
+#[cfg(feature = "nut09")]
+use cashu::nuts::nut09::{RestoreRequest, RestoreResponse};
 #[cfg(feature = "nut07")]
 use cashu::nuts::CheckStateResponse;
 #[cfg(feature = "nut07")]
@@ -111,6 +113,13 @@ pub trait Client {
     ) -> Result<CheckStateResponse, Error>;
 
     async fn get_mint_info(&self, mint_url: Url) -> Result<MintInfo, Error>;
+
+    #[cfg(feature = "nut09")]
+    async fn post_restore(
+        &self,
+        mint_url: Url,
+        restore_request: RestoreRequest,
+    ) -> Result<RestoreResponse, Error>;
 }
 
 #[cfg(any(not(target_arch = "wasm32"), feature = "gloo"))]

+ 141 - 10
crates/cashu-sdk/src/wallet/mod.rs

@@ -6,6 +6,9 @@ use bip39::Mnemonic;
 use cashu::dhke::{construct_proofs, unblind_message};
 #[cfg(feature = "nut07")]
 use cashu::nuts::nut07::ProofState;
+use cashu::nuts::nut07::State;
+#[cfg(feature = "nut09")]
+use cashu::nuts::nut09::RestoreRequest;
 use cashu::nuts::nut11::SigningKey;
 #[cfg(feature = "nut07")]
 use cashu::nuts::PublicKey;
@@ -18,7 +21,7 @@ use cashu::url::UncheckedUrl;
 use cashu::{Amount, Bolt11Invoice};
 use localstore::LocalStore;
 use thiserror::Error;
-use tracing::warn;
+use tracing::{debug, warn};
 
 use crate::client::Client;
 use crate::utils::unix_time;
@@ -299,8 +302,13 @@ impl<C: Client, L: LocalStore> Wallet<C, L> {
                 let count = self
                     .localstore
                     .get_keyset_counter(&active_keyset_id)
-                    .await?
-                    .unwrap_or(0);
+                    .await?;
+
+                let count = if let Some(count) = count {
+                    count + 1
+                } else {
+                    0
+                };
 
                 counter = Some(count);
                 PreMintSecrets::from_seed(
@@ -427,8 +435,14 @@ impl<C: Client, L: LocalStore> Wallet<C, L> {
             let count = self
                 .localstore
                 .get_keyset_counter(&active_keyset_id)
-                .await?
-                .unwrap_or(0);
+                .await?;
+
+            let count = if let Some(count) = count {
+                count + 1
+            } else {
+                0
+            };
+
             let premint_secrets = PreMintSecrets::from_seed(
                 active_keyset_id,
                 count,
@@ -444,11 +458,11 @@ impl<C: Client, L: LocalStore> Wallet<C, L> {
             PreMintSecrets::random(active_keyset_id, desired_amount)?
         };
 
-        if let (Some(amt), Some(mnemonic)) = (amount, &self.mnemonic) {
+        if let Some(amt) = amount {
             let change_amount = proofs_total - amt;
 
-            let change_messages = if let Some(count) = counter {
-                PreMintSecrets::from_seed(active_keyset_id, count, mnemonic, desired_amount, false)?
+            let change_messages = if let (Some(count), Some(mnemonic)) = (counter, &self.mnemonic) {
+                PreMintSecrets::from_seed(active_keyset_id, count, mnemonic, change_amount, false)?
             } else {
                 PreMintSecrets::random(active_keyset_id, change_amount)?
             };
@@ -547,6 +561,22 @@ impl<C: Client, L: LocalStore> Wallet<C, L> {
             &self.active_keys(mint_url, unit).await?.unwrap(),
         )?;
 
+        let active_keyset = self.active_mint_keyset(mint_url, unit).await?;
+
+        if self.mnemonic.is_some() {
+            let count = self
+                .localstore
+                .get_keyset_counter(&active_keyset)
+                .await?
+                .unwrap_or(0);
+
+            let new_count = count + post_swap_proofs.len() as u64;
+
+            self.localstore
+                .add_keyset_counter(&active_keyset, new_count)
+                .await?;
+        }
+
         post_swap_proofs.reverse();
 
         for proof in post_swap_proofs {
@@ -700,8 +730,13 @@ impl<C: Client, L: LocalStore> Wallet<C, L> {
                 let count = self
                     .localstore
                     .get_keyset_counter(&active_keyset_id)
-                    .await?
-                    .unwrap_or(0);
+                    .await?;
+
+                let count = if let Some(count) = count {
+                    count + 1
+                } else {
+                    0
+                };
 
                 counter = Some(count);
                 PreMintSecrets::from_seed(active_keyset_id, count, mnemonic, proofs_amount, true)?
@@ -930,6 +965,102 @@ impl<C: Client, L: LocalStore> Wallet<C, L> {
     ) -> Result<String, Error> {
         Ok(Token::new(mint_url, proofs, memo, unit)?.to_string())
     }
+
+    pub async fn restore(&mut self, mint_url: UncheckedUrl) -> Result<Amount, Error> {
+        // Check that mint is in store of mints
+        if self.localstore.get_mint(mint_url.clone()).await?.is_none() {
+            self.add_mint(mint_url.clone()).await?;
+        }
+
+        let active_keyset_id = &self
+            .active_mint_keyset(&mint_url, &CurrencyUnit::Sat)
+            .await?;
+        let keys = if let Some(keys) = self.localstore.get_keys(&active_keyset_id).await? {
+            keys
+        } else {
+            self.get_mint_keys(&mint_url, *active_keyset_id).await?;
+            self.localstore.get_keys(&active_keyset_id).await?.unwrap()
+        };
+
+        let mut empty_batch = 0;
+        let mut start_counter = 0;
+        let mut restored_value = Amount::ZERO;
+
+        while empty_batch.lt(&3) {
+            let premint_secrets = PreMintSecrets::restore_batch(
+                *active_keyset_id,
+                &self.mnemonic.clone().unwrap(),
+                start_counter,
+                start_counter + 100,
+            )?;
+
+            debug!(
+                "Attempting to restore counter {}-{} for mint {} keyset {}",
+                start_counter,
+                start_counter + 100,
+                mint_url,
+                active_keyset_id
+            );
+
+            let restore_request = RestoreRequest {
+                outputs: premint_secrets.blinded_messages(),
+            };
+
+            let response = self
+                .client
+                .post_restore(mint_url.clone().try_into()?, restore_request)
+                .await
+                .unwrap();
+
+            if response.signatures.is_empty() {
+                empty_batch += 1;
+                continue;
+            }
+
+            let premint_secrets: Vec<_> = premint_secrets
+                .secrets
+                .iter()
+                .filter(|p| response.outputs.contains(&p.blinded_message))
+                .collect();
+
+            // the response outputs and premint secrets should be the same after filtering
+            // blinded messages the mint did not have signatures for
+            assert_eq!(response.outputs.len(), premint_secrets.len());
+
+            let proofs = construct_proofs(
+                response.signatures,
+                premint_secrets.iter().map(|p| p.r.clone()).collect(),
+                premint_secrets.iter().map(|p| p.secret.clone()).collect(),
+                &keys,
+            )?;
+
+            self.localstore
+                .add_keyset_counter(active_keyset_id, start_counter + proofs.len() as u64)
+                .await?;
+
+            let states = self
+                .check_proofs_spent(mint_url.clone(), proofs.clone())
+                .await?;
+
+            let unspent_proofs: Vec<Proof> = proofs
+                .iter()
+                .zip(states)
+                .filter(|(_, state)| !state.state.eq(&State::Spent))
+                .map(|(p, _)| p)
+                .cloned()
+                .collect();
+
+            restored_value += unspent_proofs.iter().map(|p| p.amount).sum();
+
+            self.localstore
+                .add_proofs(mint_url.clone(), unspent_proofs)
+                .await?;
+
+            empty_batch = 0;
+            start_counter += 100;
+        }
+        Ok(restored_value)
+    }
 }
 
 /*

+ 9 - 8
crates/cashu/src/nuts/nut00.rs

@@ -31,10 +31,10 @@ pub struct BlindedMessage {
     /// Witness
     #[cfg(feature = "nut11")]
     #[serde(default)]
-    #[serde(skip_serializing_if = "Signatures::is_empty")]
-    #[serde(serialize_with = "witness_serialize")]
-    #[serde(deserialize_with = "witness_deserialize")]
-    pub witness: Signatures,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    //#[serde(serialize_with = "witness_serialize")]
+    //#[serde(deserialize_with = "witness_deserialize")]
+    pub witness: Option<Signatures>,
 }
 
 impl BlindedMessage {
@@ -44,7 +44,7 @@ impl BlindedMessage {
             keyset_id,
             b,
             #[cfg(feature = "nut11")]
-            witness: Signatures::default(),
+            witness: None,
         }
     }
 }
@@ -437,11 +437,12 @@ pub struct Proof {
     pub c: PublicKey,
     #[cfg(feature = "nut11")]
     /// Witness
+    #[cfg(feature = "nut11")]
     #[serde(default)]
-    #[serde(skip_serializing_if = "Signatures::is_empty")]
+    #[serde(skip_serializing_if = "Option::is_none")]
     #[serde(serialize_with = "witness_serialize")]
     #[serde(deserialize_with = "witness_deserialize")]
-    pub witness: Signatures,
+    pub witness: Option<Signatures>,
 }
 
 impl Proof {
@@ -452,7 +453,7 @@ impl Proof {
             secret,
             c,
             #[cfg(feature = "nut11")]
-            witness: Signatures::default(),
+            witness: None,
         }
     }
 }

+ 14 - 0
crates/cashu/src/nuts/nut09.rs

@@ -17,5 +17,19 @@ pub struct RestoreResponse {
     /// Outputs
     pub outputs: Vec<BlindedMessage>,
     /// Signatures
+    #[serde(rename = "promises")]
     pub signatures: Vec<BlindedSignature>,
 }
+
+mod test {
+
+    #[test]
+    fn restore_response() {
+        use super::*;
+        let rs = r#"{"outputs":[{"B_":"0204bbffa045f28ec836117a29ea0a00d77f1d692e38cf94f72a5145bfda6d8f41","amount":0,"id":"00ffd48b8f5ecf80", "witness":null},{"B_":"025f0615ccba96f810582a6885ffdb04bd57c96dbc590f5aa560447b31258988d7","amount":0,"id":"00ffd48b8f5ecf80"}],"promises":[{"C_":"02e9701b804dc05a5294b5a580b428237a27c7ee1690a0177868016799b1761c81","amount":8,"dleq":null,"id":"00ffd48b8f5ecf80"},{"C_":"031246ee046519b15648f1b8d8ffcb8e537409c84724e148c8d6800b2e62deb795","amount":2,"dleq":null,"id":"00ffd48b8f5ecf80"}]}"#;
+
+        let res: RestoreResponse = serde_json::from_str(rs).unwrap();
+
+        println!("{:?}", res);
+    }
+}

+ 51 - 43
crates/cashu/src/nuts/nut11.rs

@@ -32,14 +32,14 @@ impl Signatures {
     }
 }
 
-pub fn witness_serialize<S>(x: &Signatures, s: S) -> Result<S::Ok, S::Error>
+pub fn witness_serialize<S>(x: &Option<Signatures>, s: S) -> Result<S::Ok, S::Error>
 where
     S: Serializer,
 {
-    s.serialize_str(&serde_json::to_string(x).map_err(ser::Error::custom)?)
+    s.serialize_str(&serde_json::to_string(&x).map_err(ser::Error::custom)?)
 }
 
-pub fn witness_deserialize<'de, D>(deserializer: D) -> Result<Signatures, D::Error>
+pub fn witness_deserialize<'de, D>(deserializer: D) -> Result<Option<Signatures>, D::Error>
 where
     D: de::Deserializer<'de>,
 {
@@ -60,22 +60,23 @@ impl Proof {
         let mut valid_sigs = 0;
 
         let msg = &self.secret.to_bytes();
-
-        for signature in &self.witness.signatures {
-            let mut pubkeys = spending_conditions.pubkeys.clone();
-            let data_key = VerifyingKey::from_str(&secret.secret_data.data)?;
-            pubkeys.push(data_key);
-            for v in &spending_conditions.pubkeys {
-                let sig = Signature::try_from(hex::decode(signature)?.as_slice())?;
-
-                if v.verify(msg, &sig).is_ok() {
-                    valid_sigs += 1;
-                } else {
-                    debug!(
-                        "Could not verify signature: {} on message: {}",
-                        hex::encode(sig.to_bytes()),
-                        self.secret.to_string()
-                    )
+        if let Some(witness) = &self.witness {
+            for signature in &witness.signatures {
+                let mut pubkeys = spending_conditions.pubkeys.clone();
+                let data_key = VerifyingKey::from_str(&secret.secret_data.data)?;
+                pubkeys.push(data_key);
+                for v in &spending_conditions.pubkeys {
+                    let sig = Signature::try_from(hex::decode(signature)?.as_slice())?;
+
+                    if v.verify(msg, &sig).is_ok() {
+                        valid_sigs += 1;
+                    } else {
+                        debug!(
+                            "Could not verify signature: {} on message: {}",
+                            hex::encode(sig.to_bytes()),
+                            self.secret.to_string()
+                        )
+                    }
                 }
             }
         }
@@ -84,19 +85,19 @@ impl Proof {
             return Ok(());
         }
 
-        println!("{:?}", spending_conditions.refund_keys);
-
         if let Some(locktime) = spending_conditions.locktime {
             // If lock time has passed check if refund witness signature is valid
             if locktime.lt(&unix_time()) && !spending_conditions.refund_keys.is_empty() {
-                for s in &self.witness.signatures {
-                    for v in &spending_conditions.refund_keys {
-                        let sig = Signature::try_from(hex::decode(s)?.as_slice())
-                            .map_err(|_| Error::InvalidSignature)?;
-
-                        // As long as there is one valid refund signature it can be spent
-                        if v.verify(msg, &sig).is_ok() {
-                            return Ok(());
+                if let Some(signatures) = &self.witness {
+                    for s in &signatures.signatures {
+                        for v in &spending_conditions.refund_keys {
+                            let sig = Signature::try_from(hex::decode(s)?.as_slice())
+                                .map_err(|_| Error::InvalidSignature)?;
+
+                            // As long as there is one valid refund signature it can be spent
+                            if v.verify(msg, &sig).is_ok() {
+                                return Ok(());
+                            }
                         }
                     }
                 }
@@ -112,6 +113,8 @@ impl Proof {
         let signature = secret_key.sign(msg_to_sign);
 
         self.witness
+            .as_mut()
+            .unwrap_or(&mut Signatures::default())
             .signatures
             .push(hex::encode(signature.to_bytes()));
 
@@ -126,8 +129,11 @@ impl BlindedMessage {
         let signature = secret_key.sign(&msg_to_sign);
 
         self.witness
+            .as_mut()
+            .unwrap_or(&mut Signatures::default())
             .signatures
             .push(hex::encode(signature.to_bytes()));
+
         Ok(())
     }
 
@@ -137,19 +143,21 @@ impl BlindedMessage {
         required_sigs: u64,
     ) -> Result<(), Error> {
         let mut valid_sigs = 0;
-        for signature in &self.witness.signatures {
-            for v in pubkeys {
-                let msg = &self.b.to_bytes();
-                let sig = Signature::try_from(hex::decode(signature)?.as_slice())?;
-
-                if v.verify(msg, &sig).is_ok() {
-                    valid_sigs += 1;
-                } else {
-                    debug!(
-                        "Could not verify signature: {} on message: {}",
-                        hex::encode(sig.to_bytes()),
-                        self.b.to_string()
-                    )
+        if let Some(witness) = &self.witness {
+            for signature in &witness.signatures {
+                for v in pubkeys {
+                    let msg = &self.b.to_bytes();
+                    let sig = Signature::try_from(hex::decode(signature)?.as_slice())?;
+
+                    if v.verify(msg, &sig).is_ok() {
+                        valid_sigs += 1;
+                    } else {
+                        debug!(
+                            "Could not verify signature: {} on message: {}",
+                            hex::encode(sig.to_bytes()),
+                            self.b.to_string()
+                        )
+                    }
                 }
             }
         }
@@ -749,7 +757,7 @@ mod tests {
                 "02698c4e2b5f9534cd0687d87513c759790cf829aa5739184a3e3735471fbda904",
             )
             .unwrap(),
-            witness: Signatures { signatures: vec![] },
+            witness: Some(Signatures { signatures: vec![] }),
         };
 
         proof.sign_p2pk(secret_key).unwrap();

+ 31 - 0
crates/cashu/src/nuts/nut13.rs

@@ -84,6 +84,37 @@ mod wallet {
 
             Ok(pre_mint_secrets)
         }
+
+        /// Generate blinded messages from predetermined secrets and blindings
+        /// factor
+        pub fn restore_batch(
+            keyset_id: Id,
+            mnemonic: &Mnemonic,
+            start_count: u64,
+            end_count: u64,
+        ) -> Result<Self, wallet::Error> {
+            let mut pre_mint_secrets = PreMintSecrets::default();
+
+            for i in start_count..end_count {
+                let secret = Secret::from_seed(mnemonic, keyset_id, i)?;
+                let blinding_factor = SecretKey::from_seed(mnemonic, keyset_id, i)?;
+
+                let (blinded, r) = blind_message(&secret.to_bytes(), Some(blinding_factor.into()))?;
+
+                let blinded_message = BlindedMessage::new(Amount::ZERO, keyset_id, blinded);
+
+                let pre_mint = PreMint {
+                    blinded_message,
+                    secret: secret.clone(),
+                    r: r.into(),
+                    amount: Amount::ZERO,
+                };
+
+                pre_mint_secrets.secrets.push(pre_mint);
+            }
+
+            Ok(pre_mint_secrets)
+        }
     }
 }