diff --git a/crates/cashu-sdk/src/mint/mod.rs b/crates/cashu-sdk/src/mint/mod.rs index 4ca65d10..ebaaab43 100644 --- a/crates/cashu-sdk/src/mint/mod.rs +++ b/crates/cashu-sdk/src/mint/mod.rs @@ -421,7 +421,7 @@ impl Mint { } async fn verify_proof(&self, proof: &Proof) -> Result<(), Error> { - let y = hash_to_curve(&proof.secret.to_bytes()?).unwrap(); + let y = hash_to_curve(&proof.secret.to_bytes().unwrap()).unwrap(); if self.localstore.get_spent_proof_by_hash(&y).await?.is_some() { return Err(Error::TokenSpent); } diff --git a/crates/cashu-sdk/src/wallet/mod.rs b/crates/cashu-sdk/src/wallet/mod.rs index 903b3712..f54e7aed 100644 --- a/crates/cashu-sdk/src/wallet/mod.rs +++ b/crates/cashu-sdk/src/wallet/mod.rs @@ -443,12 +443,12 @@ impl Wallet { let blinded_c = promise.c.clone(); let unblinded_sig = unblind_message(blinded_c, premint.r.into(), a).unwrap(); - let proof = Proof { - keyset_id: promise.keyset_id, - amount: promise.amount, - secret: premint.secret, - c: unblinded_sig, - }; + let proof = Proof::new( + promise.amount, + promise.keyset_id, + premint.secret, + unblinded_sig, + ); proofs.push(proof); } diff --git a/crates/cashu/src/dhke.rs b/crates/cashu/src/dhke.rs index dc196300..7d047e61 100644 --- a/crates/cashu/src/dhke.rs +++ b/crates/cashu/src/dhke.rs @@ -49,6 +49,7 @@ mod wallet { use crate::error; use crate::nuts::{BlindedSignature, Keys, Proof, Proofs, PublicKey, *}; use crate::secret::Secret; + /// Blind Message Alice Step one pub fn blind_message( secret: &[u8], @@ -103,12 +104,12 @@ mod wallet { let unblinded_signature = unblind_message(blinded_c, rs[i].clone().into(), a)?; - let proof = Proof { - keyset_id: promise.keyset_id, - amount: promise.amount, - secret: secrets[i].clone(), - c: unblinded_signature, - }; + let proof = Proof::new( + promise.amount, + promise.keyset_id, + secrets[i].clone().try_into().unwrap(), + unblinded_signature, + ); proofs.push(proof); } @@ -119,6 +120,7 @@ mod wallet { #[cfg(feature = "mint")] mod mint { + use std::fmt::Debug; use std::ops::Mul; use k256::{Scalar, SecretKey}; @@ -139,13 +141,17 @@ mod mint { } /// Verify Message - pub fn verify_message( + pub fn verify_message( a: SecretKey, unblinded_message: k256::PublicKey, - msg: &Secret, - ) -> Result<(), error::mint::Error> { + msg: V, + ) -> Result<(), error::mint::Error> + where + V: TryInto>, + >>::Error: Debug, + { // Y - let y = hash_to_curve(&msg.to_bytes()?)?; + let y = hash_to_curve(&msg.try_into().unwrap())?; if unblinded_message == k256::PublicKey::try_from(*y.as_affine() * Scalar::from(a.as_scalar_primitive()))? diff --git a/crates/cashu/src/nuts/mod.rs b/crates/cashu/src/nuts/mod.rs index 191ecb23..cd4ddf48 100644 --- a/crates/cashu/src/nuts/mod.rs +++ b/crates/cashu/src/nuts/mod.rs @@ -16,7 +16,9 @@ pub mod nut11; #[cfg(feature = "wallet")] pub use nut00::wallet::{PreMint, PreMintSecrets, Token}; -pub use nut00::{BlindedMessage, BlindedSignature, CurrencyUnit, PaymentMethod, Proof}; +#[cfg(not(feature = "nut11"))] +pub use nut00::Proof; +pub use nut00::{BlindedMessage, BlindedSignature, CurrencyUnit, PaymentMethod}; pub use nut01::{Keys, KeysResponse, PublicKey, SecretKey}; pub use nut02::mint::KeySet as MintKeySet; pub use nut02::{Id, KeySet, KeySetInfo, KeysetResponse}; @@ -35,5 +37,9 @@ pub use nut06::{MintInfo, MintVersion, Nuts}; pub use nut07::{CheckStateRequest, CheckStateResponse}; #[cfg(feature = "nut08")] pub use nut08::{MeltBolt11Request, MeltBolt11Response}; +#[cfg(feature = "nut10")] +pub use nut10::{Kind, Secret as Nut10Secret, SecretData}; +#[cfg(feature = "nut11")] +pub use nut11::{P2PKConditions, Proof, SigFlag, Signatures}; pub type Proofs = Vec; diff --git a/crates/cashu/src/nuts/nut00.rs b/crates/cashu/src/nuts/nut00.rs index 813dcdc8..df98b264 100644 --- a/crates/cashu/src/nuts/nut00.rs +++ b/crates/cashu/src/nuts/nut00.rs @@ -438,6 +438,17 @@ pub struct Proof { pub c: PublicKey, } +impl Proof { + pub fn new(amount: Amount, keyset_id: Id, secret: Secret, c: PublicKey) -> Self { + Proof { + amount, + keyset_id, + secret, + c, + } + } +} + impl Hash for Proof { fn hash(&self, state: &mut H) { self.secret.hash(state); diff --git a/crates/cashu/src/nuts/nut10.rs b/crates/cashu/src/nuts/nut10.rs index 5953ada7..a17b8c61 100644 --- a/crates/cashu/src/nuts/nut10.rs +++ b/crates/cashu/src/nuts/nut10.rs @@ -1,4 +1,3 @@ -use std::fmt; use std::str::FromStr; use serde::ser::SerializeTuple; @@ -64,59 +63,10 @@ impl Serialize for Secret { } } -#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct UncheckedSecret(String); - -impl UncheckedSecret { - pub fn as_bytes(&self) -> &[u8] { - self.0.as_bytes() - } -} - -impl fmt::Display for UncheckedSecret { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } -} - -impl From for UncheckedSecret -where - S: Into, -{ - fn from(inner: S) -> Self { - Self(inner.into()) - } -} - -impl TryFrom for UncheckedSecret { - type Error = serde_json::Error; - - fn try_from(secret: Secret) -> Result { - Ok(UncheckedSecret(serde_json::to_string(&secret)?)) - } -} - -impl FromStr for UncheckedSecret { - type Err = Error; - - fn from_str(value: &str) -> Result { - Ok(Self::from(value)) - } -} - -impl TryFrom for Secret { - type Error = serde_json::Error; - - fn try_from(unchecked_secret: UncheckedSecret) -> Result { - serde_json::from_str(&unchecked_secret.0) - } -} - -impl TryFrom<&UncheckedSecret> for Secret { - type Error = serde_json::Error; - - fn try_from(unchecked_secret: &UncheckedSecret) -> Result { - serde_json::from_str(&unchecked_secret.0) +impl TryFrom for crate::secret::Secret { + type Error = Error; + fn try_from(secret: Secret) -> Result { + Ok(crate::secret::Secret::from_str(&serde_json::to_string(&secret).unwrap()).unwrap()) } } diff --git a/crates/cashu/src/nuts/nut11.rs b/crates/cashu/src/nuts/nut11.rs index 49bc1810..2bd01c12 100644 --- a/crates/cashu/src/nuts/nut11.rs +++ b/crates/cashu/src/nuts/nut11.rs @@ -3,10 +3,10 @@ use std::collections::HashMap; use std::fmt; +use std::hash::{self, Hasher}; use std::str::FromStr; -use bitcoin::hashes::sha256::Hash as Sha256; -use bitcoin::hashes::Hash; +use bitcoin::hashes::{sha256, Hash}; use k256::schnorr::signature::{Signer, Verifier}; use k256::schnorr::{Signature, SigningKey, VerifyingKey}; use serde::de::Error as DeserializerError; @@ -15,32 +15,69 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer}; use super::nut01::PublicKey; use super::nut02::Id; -use super::nut10::{Secret, SecretData, UncheckedSecret}; +use super::nut10::{Secret, SecretData}; use crate::error::Error; use crate::utils::unix_time; use crate::Amount; -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct Signatures { signatures: Vec, } +fn no_signatures(signatures: &Signatures) -> bool { + signatures.signatures.is_empty() +} + /// Proofs [NUT-11] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct Proof { /// Amount in satoshi pub amount: Amount, /// NUT-10 Secret - pub secret: UncheckedSecret, + pub secret: crate::secret::Secret, /// Unblinded signature #[serde(rename = "C")] pub c: PublicKey, /// `Keyset id` - pub id: Option, + #[serde(rename = "id")] + pub keyset_id: Id, /// Witness + #[serde(default)] + #[serde(skip_serializing_if = "no_signatures")] pub witness: Signatures, } +impl Proof { + pub fn new(amount: Amount, keyset_id: Id, secret: crate::secret::Secret, c: PublicKey) -> Self { + Proof { + amount, + keyset_id, + secret: secret.try_into().unwrap(), + c, + witness: Signatures::default(), + } + } +} + +impl hash::Hash for Proof { + fn hash(&self, state: &mut H) { + self.secret.hash(state); + } +} + +impl Ord for Proof { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.amount.cmp(&other.amount) + } +} + +impl PartialOrd for Proof { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct P2PKConditions { pub locktime: Option, @@ -185,7 +222,7 @@ impl Proof { let mut valid_sigs = 0; - let msg = Sha256::hash(self.secret.as_bytes()); + let msg = sha256::Hash::hash(&self.secret.to_bytes().unwrap()); for signature in &self.witness.signatures { let mut pubkeys = spending_conditions.pubkeys.clone(); @@ -235,7 +272,7 @@ impl Proof { } pub fn sign_p2pk_proof(&mut self, secret_key: SigningKey) -> Result<(), Error> { - let msg_to_sign = Sha256::hash(&self.secret.as_bytes()); + let msg_to_sign = sha256::Hash::hash(&self.secret.to_bytes().unwrap()); let signature = secret_key.sign(msg_to_sign.as_byte_array()); @@ -495,7 +532,7 @@ mod tests { #[test] fn test_verify() { - let proof_str = r#"{"amount":0,"secret":"[\"P2PK\",{\"nonce\":\"190badde56afcbf67937e228744ea896bb3e48bcb60efa412799e1518618c287\",\"data\":\"0249098aa8b9d2fbec49ff8598feb17b592b986e62319a4fa488a3dc36387157a7\",\"tags\":[[\"sigflag\",\"SIG_INPUTS\"]]}]","C":"02698c4e2b5f9534cd0687d87513c759790cf829aa5739184a3e3735471fbda904","id":null,"witness":{"signatures":["2b117c29a0e405fcbcac4c632b5862eb3ace0d67c681e8209d3aa2f52d5198471629b1ec6bce75d3879c47725be89d28938e31236307b40bc6c89491fa540e35"]}}"#; + let proof_str = r#"{"amount":0,"secret":"[\"P2PK\",{\"nonce\":\"190badde56afcbf67937e228744ea896bb3e48bcb60efa412799e1518618c287\",\"data\":\"0249098aa8b9d2fbec49ff8598feb17b592b986e62319a4fa488a3dc36387157a7\",\"tags\":[[\"sigflag\",\"SIG_INPUTS\"]]}]","C":"02698c4e2b5f9534cd0687d87513c759790cf829aa5739184a3e3735471fbda904","id": "009a1f293253e41e","witness":{"signatures":["2b117c29a0e405fcbcac4c632b5862eb3ace0d67c681e8209d3aa2f52d5198471629b1ec6bce75d3879c47725be89d28938e31236307b40bc6c89491fa540e35"]}}"#; let proof: Proof = serde_json::from_str(proof_str).unwrap(); @@ -523,7 +560,7 @@ mod tests { let secret: super::Secret = conditions.try_into().unwrap(); let mut proof = Proof { - id: None, + keyset_id: Id::from_str("009a1f293253e41e").unwrap(), amount: Amount::ZERO, secret: secret.try_into().unwrap(), c: PublicKey::from_str( diff --git a/crates/cashu/src/secret.rs b/crates/cashu/src/secret.rs index f8ed169a..208c5d0e 100644 --- a/crates/cashu/src/secret.rs +++ b/crates/cashu/src/secret.rs @@ -58,9 +58,21 @@ impl Secret { Self(hex::encode(xpriv.private_key().to_bytes())) } + #[cfg(not(feature = "nut10"))] pub fn to_bytes(&self) -> Result, Error> { Ok(hex::decode(&self.0)?) } + + #[cfg(feature = "nut10")] + pub fn to_bytes(&self) -> Result, Error> { + let secret: Result = + serde_json::from_str(&self.0); + + match secret { + Ok(_) => Ok(self.0.clone().into_bytes()), + Err(_) => Ok(hex::decode(&self.0)?), + } + } } impl FromStr for Secret { @@ -77,6 +89,36 @@ impl ToString for Secret { } } +impl TryFrom for Vec { + type Error = Error; + fn try_from(value: Secret) -> Result, Error> { + value.to_bytes() + } +} + +impl TryFrom<&Secret> for Vec { + type Error = Error; + fn try_from(value: &Secret) -> Result, Error> { + value.to_bytes() + } +} + +impl TryFrom for crate::nuts::nut10::Secret { + type Error = serde_json::Error; + + fn try_from(unchecked_secret: Secret) -> Result { + serde_json::from_str(&unchecked_secret.0) + } +} + +impl TryFrom<&Secret> for crate::nuts::nut10::Secret { + type Error = serde_json::Error; + + fn try_from(unchecked_secret: &Secret) -> Result { + serde_json::from_str(&unchecked_secret.0) + } +} + #[cfg(test)] mod tests { use std::assert_eq;