diff --git a/README.md b/README.md index 31f7977f..a5bea5e9 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ CDK is a collection of rust crates for [Cashu](https://github.com/cashubtc) wall - :heavy_check_mark: [NUT-11](https://github.com/cashubtc/nuts/blob/main/11.md) - :heavy_check_mark: [NUT-12](https://github.com/cashubtc/nuts/blob/main/12.md) - :heavy_check_mark: [NUT-13](https://github.com/cashubtc/nuts/blob/main/13.md) +- :heavy_check_mark: [NUT-14](https://github.com/cashubtc/nuts/blob/main/14.md) ## License diff --git a/crates/cdk/src/client.rs b/crates/cdk/src/client.rs index 14e310c9..d793aa31 100644 --- a/crates/cdk/src/client.rs +++ b/crates/cdk/src/client.rs @@ -234,7 +234,6 @@ impl HttpClient { let value = res.json::().await?; let response: Result = serde_json::from_value(value.clone()); - match response { Ok(res) => Ok(res), Err(_) => Err(ErrorResponse::from_json(&value.to_string())?.into()), diff --git a/crates/cdk/src/mint.rs b/crates/cdk/src/mint.rs index b533e9e6..00e92d92 100644 --- a/crates/cdk/src/mint.rs +++ b/crates/cdk/src/mint.rs @@ -9,6 +9,7 @@ use thiserror::Error; use tokio::sync::RwLock; use tracing::{debug, error, info}; +use self::nut11::enforce_sig_flag; use crate::cdk_database::{self, MintDatabase}; use crate::dhke::{hash_to_curve, sign_message, verify_message}; use crate::error::ErrorResponse; @@ -49,6 +50,8 @@ pub enum Error { NUT11(#[from] crate::nuts::nut11::Error), #[error(transparent)] Nut12(#[from] crate::nuts::nut12::Error), + #[error(transparent)] + Nut14(#[from] crate::nuts::nut14::Error), /// Database Error #[error(transparent)] Database(#[from] crate::cdk_database::Error), @@ -439,6 +442,15 @@ impl Mint { return Err(Error::MultipleUnits); } + let (sig_flag, pubkeys) = enforce_sig_flag(swap_request.inputs.clone()); + + if sig_flag.eq(&SigFlag::SigAll) { + let pubkeys = pubkeys.into_iter().collect(); + for blinded_messaage in &swap_request.outputs { + blinded_messaage.verify_p2pk(&pubkeys, 1)?; + } + } + for proof in swap_request.inputs { self.localstore.add_spent_proof(proof).await?; } @@ -461,11 +473,18 @@ impl Mint { if let Ok(secret) = <&crate::secret::Secret as TryInto>::try_into(&proof.secret) { - // Verify if p2pk - if secret.kind.eq(&Kind::P2PK) { - proof.verify_p2pk()?; - } else { - return Err(Error::UnknownSecretKind); + // Checks and verifes known secret kinds. + // If it is an unknown secret kind it will be treated as a normal secret. + // Spending conditions will **not** be check. It is up to the wallet to ensure + // only supported secret kinds are used as there is no way for the mint to enforce + // only signing supported secrets as they are blinded at that point. + match secret.kind { + Kind::P2PK => { + proof.verify_p2pk()?; + } + Kind::HTLC => { + proof.verify_htlc()?; + } } } @@ -552,6 +571,15 @@ impl Mint { } if let Some(outputs) = &melt_request.outputs { + let (sig_flag, pubkeys) = enforce_sig_flag(melt_request.inputs.clone()); + + if sig_flag.eq(&SigFlag::SigAll) { + let pubkeys = pubkeys.into_iter().collect(); + for blinded_messaage in outputs { + blinded_messaage.verify_p2pk(&pubkeys, 1)?; + } + } + let output_keysets_ids: HashSet = outputs.iter().map(|b| b.keyset_id).collect(); for id in output_keysets_ids { let keyset = self diff --git a/crates/cdk/src/nuts/mod.rs b/crates/cdk/src/nuts/mod.rs index 5a546352..88661628 100644 --- a/crates/cdk/src/nuts/mod.rs +++ b/crates/cdk/src/nuts/mod.rs @@ -13,6 +13,7 @@ pub mod nut11; pub mod nut12; #[cfg(feature = "nut13")] pub mod nut13; +pub mod nut14; pub use nut00::{ BlindSignature, BlindedMessage, CurrencyUnit, PaymentMethod, PreMint, PreMintSecrets, Proof, @@ -35,5 +36,5 @@ pub use nut06::{MintInfo, MintVersion, Nuts}; pub use nut07::{CheckStateRequest, CheckStateResponse, ProofState, State}; pub use nut09::{RestoreRequest, RestoreResponse}; pub use nut10::{Kind, Secret as Nut10Secret, SecretData}; -pub use nut11::{P2PKConditions, SigFlag, Signatures, SigningKey, VerifyingKey}; +pub use nut11::{Conditions, P2PKWitness, SigFlag, SigningKey, SpendingConditions, VerifyingKey}; pub use nut12::{BlindSignatureDleq, ProofDleq}; diff --git a/crates/cdk/src/nuts/nut00.rs b/crates/cdk/src/nuts/nut00.rs index 969c4e51..6d1d1ee3 100644 --- a/crates/cdk/src/nuts/nut00.rs +++ b/crates/cdk/src/nuts/nut00.rs @@ -14,11 +14,14 @@ use serde::{Deserialize, Deserializer, Serialize}; use thiserror::Error; use url::Url; +use super::nut10; +use super::nut11::SpendingConditions; use crate::dhke::blind_message; use crate::nuts::nut01::{PublicKey, SecretKey}; -use crate::nuts::nut11::{witness_deserialize, witness_serialize, Signatures}; +use crate::nuts::nut11::{serde_p2pk_witness, P2PKWitness}; use crate::nuts::nut12::BlindSignatureDleq; -use crate::nuts::{Id, P2PKConditions, ProofDleq}; +use crate::nuts::nut14::{serde_htlc_witness, HTLCWitness}; +use crate::nuts::{Id, ProofDleq}; use crate::secret::Secret; use crate::url::UncheckedUrl; use crate::Amount; @@ -77,11 +80,8 @@ pub struct BlindedMessage { /// Witness /// /// - #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] - //#[serde(serialize_with = "witness_serialize")] - //#[serde(deserialize_with = "witness_deserialize")] - pub witness: Option, + pub witness: Option, } impl BlindedMessage { @@ -97,9 +97,9 @@ impl BlindedMessage { } /// Add witness - pub fn witness(mut self, witness: Signatures) -> Self { + #[inline] + pub fn witness(&mut self, witness: Witness) { self.witness = Some(witness); - self } } @@ -123,11 +123,44 @@ pub struct BlindSignature { /// DLEQ Proof /// /// - #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] pub dleq: Option, } +/// Witness +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[serde(untagged)] +pub enum Witness { + /// P2PK Witness + #[serde(with = "serde_p2pk_witness")] + P2PKWitness(P2PKWitness), + /// HTLC Witness + #[serde(with = "serde_htlc_witness")] + HTLCWitness(HTLCWitness), +} + +impl Witness { + pub fn add_signatures(&mut self, signatues: Vec) { + match self { + Self::P2PKWitness(p2pk_witness) => p2pk_witness.signatures.extend(signatues), + Self::HTLCWitness(htlc_witness) => { + htlc_witness.signatures = htlc_witness.signatures.clone().map(|sigs| { + let mut sigs = sigs; + sigs.extend(signatues); + sigs + }); + } + } + } + + pub fn signatures(&self) -> Option> { + match self { + Self::P2PKWitness(witness) => Some(witness.signatures.clone()), + Self::HTLCWitness(witness) => witness.signatures.clone(), + } + } +} + /// Proofs #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct Proof { @@ -142,12 +175,10 @@ pub struct Proof { #[serde(rename = "C")] pub c: PublicKey, /// Witness - #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] - #[serde(serialize_with = "witness_serialize")] - #[serde(deserialize_with = "witness_deserialize")] - pub witness: Option, + pub witness: Option, /// DLEQ Proof + #[serde(skip_serializing_if = "Option::is_none")] pub dleq: Option, } @@ -381,17 +412,19 @@ impl PreMintSecrets { Ok(PreMintSecrets { secrets: output }) } - pub fn with_p2pk_conditions( + pub fn with_conditions( keyset_id: Id, amount: Amount, - conditions: P2PKConditions, + conditions: SpendingConditions, ) -> Result { let amount_split = amount.split(); let mut output = Vec::with_capacity(amount_split.len()); for amount in amount_split { - let secret: Secret = conditions.clone().try_into()?; + let secret: nut10::Secret = conditions.clone().into(); + + let secret: Secret = secret.try_into()?; let (blinded, r) = blind_message(&secret.to_bytes(), None)?; let blinded_message = BlindedMessage::new(amount, keyset_id, blinded); diff --git a/crates/cdk/src/nuts/nut04.rs b/crates/cdk/src/nuts/nut04.rs index db23ccb2..0f61de33 100644 --- a/crates/cdk/src/nuts/nut04.rs +++ b/crates/cdk/src/nuts/nut04.rs @@ -67,20 +67,22 @@ pub struct MintBolt11Response { } /// Mint Method Settings -#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct MintMethodSettings { /// Payment Method e.g. bolt11 method: PaymentMethod, /// Currency Unit e.g. sat unit: CurrencyUnit, /// Min Amount - min_amount: Amount, + #[serde(skip_serializing_if = "Option::is_none")] + min_amount: Option, /// Max Amount - max_amount: Amount, + #[serde(skip_serializing_if = "Option::is_none")] + max_amount: Option, } /// Mint Settings -#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct Settings { methods: Vec, disabled: bool, diff --git a/crates/cdk/src/nuts/nut05.rs b/crates/cdk/src/nuts/nut05.rs index b6af2439..f0b1bcca 100644 --- a/crates/cdk/src/nuts/nut05.rs +++ b/crates/cdk/src/nuts/nut05.rs @@ -74,20 +74,23 @@ pub struct MeltBolt11Response { } /// Melt Method Settings -#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct MeltMethodSettings { /// Payment Method e.g. bolt11 method: PaymentMethod, /// Currency Unit e.g. sat unit: CurrencyUnit, /// Min Amount - min_amount: Amount, + #[serde(skip_serializing_if = "Option::is_none")] + min_amount: Option, /// Max Amount - max_amount: Amount, + #[serde(skip_serializing_if = "Option::is_none")] + max_amount: Option, } /// Melt Settings -#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct Settings { methods: Vec, + disabled: bool, } diff --git a/crates/cdk/src/nuts/nut06.rs b/crates/cdk/src/nuts/nut06.rs index 440a9b1d..e73cfa1a 100644 --- a/crates/cdk/src/nuts/nut06.rs +++ b/crates/cdk/src/nuts/nut06.rs @@ -5,10 +5,10 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer}; use super::nut01::PublicKey; -use super::{nut04, nut05, nut07, nut08}; +use super::{nut04, nut05}; /// Mint Version -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct MintVersion { pub name: String, pub version: String, @@ -42,7 +42,7 @@ impl<'de> Deserialize<'de> for MintVersion { } /// Mint Info [NIP-09] -#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Default, Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct MintInfo { /// name of the mint and should be recognizable #[serde(skip_serializing_if = "Option::is_none")] @@ -69,7 +69,8 @@ pub struct MintInfo { pub motd: Option, } -#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)] +/// Supported nuts and settings +#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct Nuts { #[serde(default)] #[serde(rename = "4")] @@ -79,21 +80,40 @@ pub struct Nuts { pub nut05: nut05::Settings, #[serde(default)] #[serde(rename = "7")] - pub nut07: nut07::Settings, + pub nut07: SupportedSettings, #[serde(default)] #[serde(rename = "8")] - pub nut08: nut08::Settings, - // TODO: Change to nut settings + pub nut08: SupportedSettings, #[serde(default)] #[serde(rename = "9")] - pub nut09: nut07::Settings, - // TODO: Change to nut settings + pub nut09: SupportedSettings, + #[serde(rename = "10")] #[serde(default)] - pub nut10: nut07::Settings, - // TODO: Change to nut settings + pub nut10: SupportedSettings, + #[serde(rename = "11")] + #[serde(default)] + pub nut11: SupportedSettings, #[serde(default)] #[serde(rename = "12")] - pub nut12: nut07::Settings, + pub nut12: SupportedSettings, + #[serde(default)] + #[serde(rename = "13")] + pub nut13: SupportedSettings, + #[serde(default)] + #[serde(rename = "14")] + pub nut14: SupportedSettings, +} + +/// Check state Settings +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct SupportedSettings { + supported: bool, +} + +impl Default for SupportedSettings { + fn default() -> Self { + Self { supported: true } + } } #[cfg(test)] @@ -150,7 +170,8 @@ mod tests { "min_amount": 0, "max_amount": 10000 } - ] + ], + "disabled": false }, "7": {"supported": true}, "8": {"supported": true}, diff --git a/crates/cdk/src/nuts/nut07.rs b/crates/cdk/src/nuts/nut07.rs index d9ca20e7..193066e1 100644 --- a/crates/cdk/src/nuts/nut07.rs +++ b/crates/cdk/src/nuts/nut07.rs @@ -39,9 +39,3 @@ pub struct ProofState { pub struct CheckStateResponse { pub states: Vec, } - -/// Spendable Settings -#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct Settings { - supported: bool, -} diff --git a/crates/cdk/src/nuts/nut08.rs b/crates/cdk/src/nuts/nut08.rs index afd31a98..fa9b7504 100644 --- a/crates/cdk/src/nuts/nut08.rs +++ b/crates/cdk/src/nuts/nut08.rs @@ -2,8 +2,6 @@ //! //! -use serde::{Deserialize, Serialize}; - use super::nut05::{MeltBolt11Request, MeltBolt11Response}; use crate::Amount; @@ -22,9 +20,3 @@ impl MeltBolt11Response { .map(|c| c.iter().map(|b| b.amount).sum()) } } - -/// Melt Settings -#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct Settings { - supported: bool, -} diff --git a/crates/cdk/src/nuts/nut10.rs b/crates/cdk/src/nuts/nut10.rs index 0b100c06..6844c191 100644 --- a/crates/cdk/src/nuts/nut10.rs +++ b/crates/cdk/src/nuts/nut10.rs @@ -2,21 +2,23 @@ //! //! -use core::str::FromStr; +use std::str::FromStr; use serde::ser::SerializeTuple; use serde::{Deserialize, Serialize, Serializer}; use crate::error::Error; -#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)] +/// NUT10 Secret Kind +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum Kind { /// NUT-11 P2PK - #[default] P2PK, + /// NUT-14 HTLC + HTLC, } -#[derive(Debug, Default, Clone, Deserialize, PartialEq, Eq, Serialize)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct SecretData { /// Unique random string pub nonce: String, @@ -27,23 +29,25 @@ pub struct SecretData { pub tags: Vec>, } -#[derive(Debug, Default, Clone, Deserialize, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize)] pub struct Secret { /// Kind of the spending condition pub kind: Kind, + /// Secret Data pub secret_data: SecretData, } impl Secret { - pub fn new(kind: Kind, data: S, tags: Vec>) -> Self + pub fn new(kind: Kind, data: S, tags: V) -> Self where S: Into, + V: Into>>, { let nonce = crate::secret::Secret::generate().to_string(); let secret_data = SecretData { nonce, data: data.into(), - tags, + tags: tags.into(), }; Self { kind, secret_data } diff --git a/crates/cdk/src/nuts/nut11.rs b/crates/cdk/src/nuts/nut11/mod.rs similarity index 71% rename from crates/cdk/src/nuts/nut11.rs rename to crates/cdk/src/nuts/nut11/mod.rs index 04ad38fd..c547a1b3 100644 --- a/crates/cdk/src/nuts/nut11.rs +++ b/crates/cdk/src/nuts/nut11/mod.rs @@ -2,7 +2,7 @@ //! //! -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::fmt; use std::ops::Deref; use std::str::FromStr; @@ -15,21 +15,26 @@ use bitcoin::secp256k1::{ }; use serde::de::Error as DeserializerError; use serde::ser::SerializeSeq; -use serde::{de, ser, Deserialize, Deserializer, Serialize, Serializer}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; use thiserror::Error; +use super::nut00::Witness; use super::nut01::PublicKey; -use super::nut10::{Secret, SecretData}; -use super::{Proof, SecretKey}; +use super::{Kind, Nut10Secret, Proof, Proofs, SecretKey}; use crate::nuts::nut00::BlindedMessage; use crate::util::{hex, unix_time}; use crate::SECP256K1; +pub mod serde_p2pk_witness; + #[derive(Debug, Error)] pub enum Error { /// Incorrect secret kind #[error("Secret is not a p2pk secret")] IncorrectSecretKind, + /// Incorrect secret kind + #[error("Witness is not a p2pk witness")] + IncorrectWitnessKind, /// P2PK locktime has already passed #[error("Locktime in past")] LocktimeInPast, @@ -39,14 +44,24 @@ pub enum Error { /// Unknown tag in P2PK secret #[error("Unknown Tag P2PK secret")] UnknownTag, + /// Unknown Sigflag + #[error("Unknown Sigflag")] + UnknownSigFlag, /// P2PK Spend conditions not meet #[error("P2PK Spend conditions are not met")] SpendConditionsNotMet, /// Pubkey must be in data field of P2PK #[error("P2PK Required in secret data")] P2PKPubkeyRequired, + /// Unknown Kind #[error("Kind not found")] KindNotFound, + /// HTLC hash invalid + #[error("Invalid Hash")] + InvalidHash, + /// Witness Signatures not provided + #[error("Witness Signatures not provided")] + SignaturesNotProvided, /// Parse Url Error #[error(transparent)] UrlParseError(#[from] url::ParseError), @@ -70,66 +85,73 @@ pub enum Error { Secret(#[from] crate::secret::Error), } +/// P2Pk Witness #[derive(Default, Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub struct Signatures { - #[serde(default)] - #[serde(skip_serializing_if = "Vec::is_empty")] +pub struct P2PKWitness { pub signatures: Vec, } -impl Signatures { +impl P2PKWitness { #[inline] pub fn is_empty(&self) -> bool { self.signatures.is_empty() } } -/// Serialize [Signatures] as stringified JSON -pub fn witness_serialize(x: &Option, s: S) -> Result -where - S: Serializer, -{ - s.serialize_str(&serde_json::to_string(&x).map_err(ser::Error::custom)?) -} - -/// Serialize [Signatures] from stringified JSON -pub fn witness_deserialize<'de, D>(deserializer: D) -> Result, D::Error> -where - D: Deserializer<'de>, -{ - let s: String = String::deserialize(deserializer)?; - serde_json::from_str(&s).map_err(de::Error::custom) -} - impl Proof { - pub fn verify_p2pk(&self) -> Result<(), Error> { - if !self.secret.is_p2pk() { - return Err(Error::IncorrectSecretKind); - } + /// Sign [Proof] + pub fn sign_p2pk(&mut self, secret_key: SigningKey) -> Result<(), Error> { + let msg: Vec = self.secret.to_bytes(); + let signature: Signature = secret_key.sign(&msg)?; - let secret: Secret = self.secret.clone().try_into()?; - let spending_conditions: P2PKConditions = secret.clone().try_into()?; + let signatures = vec![signature.to_string()]; + + match self.witness.as_mut() { + Some(witness) => { + witness.add_signatures(signatures); + } + None => { + let mut p2pk_witness = Witness::P2PKWitness(P2PKWitness::default()); + p2pk_witness.add_signatures(signatures); + self.witness = Some(p2pk_witness); + } + }; + + Ok(()) + } + + /// Verify P2PK signature on [Proof] + pub fn verify_p2pk(&self) -> Result<(), Error> { + let secret: Nut10Secret = self.secret.clone().try_into()?; + let spending_conditions: Conditions = secret.secret_data.tags.try_into()?; let msg: &[u8] = self.secret.as_bytes(); let mut valid_sigs = 0; - if let Some(witness) = &self.witness { - for signature in witness.signatures.iter() { - let mut pubkeys = spending_conditions.pubkeys.clone(); + let witness_signatures = match &self.witness { + Some(witness) => witness.signatures(), + None => None, + }; - pubkeys.push(VerifyingKey::from_str(&secret.secret_data.data)?); + let witness_signatures = witness_signatures.ok_or(Error::SignaturesNotProvided)?; - for v in &spending_conditions.pubkeys { - let sig = Signature::from_str(signature)?; + let mut pubkeys = spending_conditions.pubkeys.clone().unwrap_or_default(); - if v.verify(msg, &sig).is_ok() { - valid_sigs += 1; - } else { - tracing::debug!( - "Could not verify signature: {sig} on message: {}", - self.secret.to_string() - ) - } + if secret.kind.eq(&Kind::P2PK) { + pubkeys.push(VerifyingKey::from_str(&secret.secret_data.data)?); + } + + for signature in witness_signatures.iter() { + for v in &pubkeys { + let sig = Signature::from_str(signature)?; + + if v.verify(msg, &sig).is_ok() { + valid_sigs += 1; + } else { + tracing::debug!( + "Could not verify signature: {sig} on message: {}", + self.secret.to_string() + ) } } } @@ -144,16 +166,13 @@ impl Proof { ) { // If lock time has passed check if refund witness signature is valid if locktime.lt(&unix_time()) { - if let Some(signatures) = &self.witness { - for s in &signatures.signatures { - for v in &refund_keys { - let sig = - Signature::from_str(s).map_err(|_| Error::InvalidSignature)?; + for s in witness_signatures.iter() { + for v in &refund_keys { + let sig = Signature::from_str(s).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(()); - } + // As long as there is one valid refund signature it can be spent + if v.verify(msg, &sig).is_ok() { + return Ok(()); } } } @@ -162,35 +181,46 @@ impl Proof { Err(Error::SpendConditionsNotMet) } +} - pub fn sign_p2pk(&mut self, secret_key: SigningKey) -> Result<(), Error> { - let msg: Vec = self.secret.to_bytes(); - let signature: Signature = secret_key.sign(&msg)?; +/// Returns count of valid signatures +pub fn valid_signatures(msg: &[u8], pubkeys: &[VerifyingKey], signatures: &[Signature]) -> u64 { + let mut count = 0; - self.witness - .as_mut() - .unwrap_or(&mut Signatures::default()) - .signatures - .push(signature.to_string()); - - Ok(()) + for pubkey in pubkeys { + for signature in signatures { + if pubkey.verify(msg, signature).is_ok() { + count += 1; + } + } } + + count } impl BlindedMessage { + /// Sign [BlindedMessage] pub fn sign_p2pk(&mut self, secret_key: SigningKey) -> Result<(), Error> { let msg: [u8; 33] = self.blinded_secret.to_bytes(); let signature: Signature = secret_key.sign(&msg)?; - self.witness - .as_mut() - .unwrap_or(&mut Signatures::default()) - .signatures - .push(signature.to_string()); + let signatures = vec![signature.to_string()]; + + match self.witness.as_mut() { + Some(witness) => { + witness.add_signatures(signatures); + } + None => { + let mut p2pk_witness = Witness::P2PKWitness(P2PKWitness::default()); + p2pk_witness.add_signatures(signatures); + self.witness = Some(p2pk_witness); + } + }; Ok(()) } + /// Verify P2PK conditions on [BlindedMessage] pub fn verify_p2pk( &self, pubkeys: &Vec, @@ -198,7 +228,11 @@ impl BlindedMessage { ) -> Result<(), Error> { let mut valid_sigs = 0; if let Some(witness) = &self.witness { - for signature in &witness.signatures { + for signature in witness + .signatures() + .ok_or(Error::SignaturesNotProvided)? + .iter() + { for v in pubkeys { let msg = &self.blinded_secret.to_bytes(); let sig = Signature::from_str(signature)?; @@ -223,11 +257,87 @@ impl BlindedMessage { } } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct P2PKConditions { +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum SpendingConditions { + /// NUT11 Spending conditions + P2PKConditions { + data: VerifyingKey, + conditions: Conditions, + }, + /// NUT14 Spending conditions + HTLCConditions { + data: Sha256Hash, + conditions: Conditions, + }, +} + +impl SpendingConditions { + /// New HTLC [SpendingConditions] + pub fn new_htlc(preimage: String, conditions: Conditions) -> Result { + let htlc = Sha256Hash::hash(&hex::decode(preimage)?); + + Ok(Self::HTLCConditions { + data: htlc, + conditions, + }) + } + + /// New P2PK [SpendingConditions] + pub fn new_p2pk(pubkey: VerifyingKey, conditions: Conditions) -> Self { + Self::P2PKConditions { + data: pubkey, + conditions, + } + } + + /// Kind of [SpendingConditions] + pub fn kind(&self) -> Kind { + match self { + Self::P2PKConditions { .. } => Kind::P2PK, + Self::HTLCConditions { .. } => Kind::HTLC, + } + } +} + +impl TryFrom for SpendingConditions { + type Error = Error; + fn try_from(secret: Nut10Secret) -> Result { + match secret.kind { + Kind::P2PK => Ok(SpendingConditions::P2PKConditions { + data: VerifyingKey::from_str(&secret.secret_data.data)?, + conditions: secret.secret_data.tags.try_into()?, + }), + Kind::HTLC => Ok(Self::HTLCConditions { + data: Sha256Hash::from_str(&secret.secret_data.data) + .map_err(|_| Error::InvalidHash)?, + conditions: secret.secret_data.tags.try_into()?, + }), + } + } +} + +impl From for super::nut10::Secret { + fn from(conditions: SpendingConditions) -> super::nut10::Secret { + match conditions { + SpendingConditions::P2PKConditions { data, conditions } => super::nut10::Secret::new( + Kind::P2PK, + data.to_normalized_public_key().to_hex(), + conditions, + ), + SpendingConditions::HTLCConditions { data, conditions } => { + super::nut10::Secret::new(Kind::HTLC, data.to_string(), conditions) + } + } + } +} + +/// P2PK and HTLC spending conditions +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct Conditions { #[serde(skip_serializing_if = "Option::is_none")] pub locktime: Option, - pub pubkeys: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub pubkeys: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub refund_keys: Option>, #[serde(skip_serializing_if = "Option::is_none")] @@ -235,10 +345,10 @@ pub struct P2PKConditions { pub sig_flag: SigFlag, } -impl P2PKConditions { +impl Conditions { pub fn new( locktime: Option, - pubkeys: Vec, + pubkeys: Option>, refund_keys: Option>, num_sigs: Option, sig_flag: Option, @@ -258,11 +368,9 @@ impl P2PKConditions { }) } } - -impl TryFrom for Secret { - type Error = Error; - fn try_from(conditions: P2PKConditions) -> Result { - let P2PKConditions { +impl From for Vec> { + fn from(conditions: Conditions) -> Vec> { + let Conditions { locktime, pubkeys, refund_keys, @@ -270,17 +378,10 @@ impl TryFrom for Secret { sig_flag, } = conditions; - let data = match pubkeys.first() { - Some(data) => data.to_string(), - None => return Err(Error::P2PKPubkeyRequired), - }; - - let data = data.to_string(); - let mut tags = Vec::new(); - if pubkeys.len().gt(&1) { - tags.push(Tag::PubKeys(pubkeys.into_iter().skip(1).collect()).as_vec()); + if let Some(pubkeys) = pubkeys { + tags.push(Tag::PubKeys(pubkeys.into_iter().collect()).as_vec()); } if let Some(locktime) = locktime { @@ -295,48 +396,23 @@ impl TryFrom for Secret { tags.push(Tag::Refund(refund_keys).as_vec()) } tags.push(Tag::SigFlag(sig_flag).as_vec()); - - Ok(Secret { - kind: super::nut10::Kind::P2PK, - secret_data: SecretData { - nonce: crate::secret::Secret::default().to_string(), - data, - tags, - }, - }) + tags } } -impl TryFrom for crate::secret::Secret { +impl TryFrom>> for Conditions { type Error = Error; - fn try_from(conditions: P2PKConditions) -> Result { - let secret: Secret = conditions.try_into()?; - - secret.try_into().map_err(|_| Error::IncorrectSecretKind) - } -} - -impl TryFrom for P2PKConditions { - type Error = Error; - fn try_from(secret: Secret) -> Result { - let tags: HashMap = secret - .clone() - .secret_data - .tags + fn try_from(tags: Vec>) -> Result { + let tags: HashMap = tags .into_iter() .map(|t| Tag::try_from(t).unwrap()) .map(|t| (t.kind(), t)) .collect(); - let mut pubkeys: Vec = vec![]; - - if let Some(Tag::PubKeys(keys)) = tags.get(&TagKind::Pubkeys) { - let mut keys = keys.clone(); - pubkeys.append(&mut keys); - } - - let data_pubkey = VerifyingKey::from_str(&secret.secret_data.data)?; - pubkeys.push(data_pubkey); + let pubkeys = match tags.get(&TagKind::Pubkeys) { + Some(Tag::PubKeys(pubkeys)) => Some(pubkeys.clone()), + _ => None, + }; let locktime = if let Some(tag) = tags.get(&TagKind::Locktime) { match tag { @@ -374,7 +450,7 @@ impl TryFrom for P2PKConditions { None }; - Ok(P2PKConditions { + Ok(Conditions { locktime, pubkeys, refund_keys, @@ -384,7 +460,8 @@ impl TryFrom for P2PKConditions { } } -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)] +// P2PK and HTLC Spending condition tags +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord)] #[serde(rename_all = "lowercase")] pub enum TagKind { /// Signature flag @@ -436,7 +513,6 @@ pub enum SigFlag { #[default] SigInputs, SigAll, - Custom(String), } impl fmt::Display for SigFlag { @@ -444,25 +520,48 @@ impl fmt::Display for SigFlag { match self { Self::SigAll => write!(f, "SIG_ALL"), Self::SigInputs => write!(f, "SIG_INPUTS"), - Self::Custom(flag) => write!(f, "{}", flag), } } } -impl From for SigFlag -where - S: AsRef, -{ - fn from(tag: S) -> Self { - match tag.as_ref() { - "SIG_ALL" => Self::SigAll, - "SIG_INPUTS" => Self::SigInputs, - tag => Self::Custom(tag.to_string()), +impl FromStr for SigFlag { + type Err = Error; + fn from_str(tag: &str) -> Result { + match tag { + "SIG_ALL" => Ok(Self::SigAll), + "SIG_INPUTS" => Ok(Self::SigInputs), + _ => Err(Error::UnknownSigFlag), } } } -#[derive(Debug, Clone, PartialEq, Eq)] +pub fn enforce_sig_flag(proofs: Proofs) -> (SigFlag, HashSet) { + let mut sig_flag = SigFlag::SigInputs; + let mut pubkeys = HashSet::new(); + for proof in proofs { + if let Ok(secret) = Nut10Secret::try_from(proof.secret) { + if secret.kind.eq(&Kind::P2PK) { + if let Ok(verifying_key) = VerifyingKey::from_str(&secret.secret_data.data) { + pubkeys.insert(verifying_key); + } + } + + if let Ok(conditions) = Conditions::try_from(secret.secret_data.tags) { + if conditions.sig_flag.eq(&SigFlag::SigAll) { + sig_flag = SigFlag::SigAll; + } + + if let Some(pubs) = conditions.pubkeys { + pubkeys.extend(pubs); + } + } + } + } + + (sig_flag, pubkeys) +} + +#[derive(Debug, Clone, Hash, PartialEq, Eq)] pub enum Tag { SigFlag(SigFlag), NSigs(u64), @@ -501,7 +600,7 @@ where }; match tag_kind { - TagKind::SigFlag => Ok(Tag::SigFlag(SigFlag::from(tag[1].as_ref()))), + TagKind::SigFlag => Ok(Tag::SigFlag(SigFlag::from_str(tag[1].as_ref())?)), TagKind::NSigs => Ok(Tag::NSigs(tag[1].as_ref().parse()?)), TagKind::Locktime => Ok(Tag::LockTime(tag[1].as_ref().parse()?)), TagKind::Refund => { @@ -535,7 +634,6 @@ impl From for Vec { Tag::LockTime(locktime) => vec![TagKind::Locktime.to_string(), locktime.to_string()], Tag::PubKeys(pubkeys) => { let mut tag = vec![TagKind::Pubkeys.to_string()]; - for pubkey in pubkeys.into_iter() { let pubkey: PublicKey = pubkey.to_normalized_public_key(); tag.push(pubkey.to_string()) @@ -579,7 +677,7 @@ impl<'de> Deserialize<'de> for Tag { } } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] #[serde(transparent)] pub struct VerifyingKey(XOnlyPublicKey); @@ -706,17 +804,19 @@ mod tests { use super::*; use crate::nuts::Id; + use crate::secret::Secret; use crate::Amount; #[test] fn test_secret_ser() { - let conditions = P2PKConditions { + let data = VerifyingKey::from_str( + "033281c37677ea273eb7183b783067f5244933ef78d8c3f15b1a77cb246099c26e", + ) + .unwrap(); + + let conditions = Conditions { locktime: Some(99999), - pubkeys: vec![ - VerifyingKey::from_str( - "033281c37677ea273eb7183b783067f5244933ef78d8c3f15b1a77cb246099c26e", - ) - .unwrap(), + pubkeys: Some(vec![ VerifyingKey::from_str( "02698c4e2b5f9534cd0687d87513c759790cf829aa5739184a3e3735471fbda904", ) @@ -725,7 +825,7 @@ mod tests { "023192200a0cfd3867e48eb63b03ff599c7e46c8f4e41146b2d281173ca6c50c54", ) .unwrap(), - ], + ]), refund_keys: Some(vec![VerifyingKey::from_str( "033281c37677ea273eb7183b783067f5244933ef78d8c3f15b1a77cb246099c26e", ) @@ -734,11 +834,11 @@ mod tests { sig_flag: SigFlag::SigAll, }; - let secret: Secret = conditions.try_into().unwrap(); + let secret: Nut10Secret = Nut10Secret::new(Kind::P2PK, data.to_string(), conditions); let secret_str = serde_json::to_string(&secret).unwrap(); - let secret_der: Secret = serde_json::from_str(&secret_str).unwrap(); + let secret_der: Nut10Secret = serde_json::from_str(&secret_str).unwrap(); assert_eq!(secret_der, secret); } @@ -763,15 +863,17 @@ mod tests { let v_key_two: VerifyingKey = signing_key_two.verifying_key(); let v_key_three: VerifyingKey = signing_key_three.verifying_key(); - let conditions = P2PKConditions { + let conditions = Conditions { locktime: Some(21), - pubkeys: vec![v_key.clone(), v_key_two, v_key_three], - refund_keys: Some(vec![v_key]), + pubkeys: Some(vec![v_key_two, v_key_three]), + refund_keys: Some(vec![v_key.clone()]), num_sigs: Some(2), sig_flag: SigFlag::SigInputs, }; - let secret: super::Secret = conditions.try_into().unwrap(); + let secret: Secret = Nut10Secret::new(Kind::P2PK, v_key.to_string(), conditions) + .try_into() + .unwrap(); let mut proof = Proof { keyset_id: Id::from_str("009a1f293253e41e").unwrap(), @@ -781,7 +883,7 @@ mod tests { "02698c4e2b5f9534cd0687d87513c759790cf829aa5739184a3e3735471fbda904", ) .unwrap(), - witness: Some(Signatures { signatures: vec![] }), + witness: Some(Witness::P2PKWitness(P2PKWitness { signatures: vec![] })), dleq: None, }; diff --git a/crates/cdk/src/nuts/nut11/serde_p2pk_witness.rs b/crates/cdk/src/nuts/nut11/serde_p2pk_witness.rs new file mode 100644 index 00000000..843b0d1e --- /dev/null +++ b/crates/cdk/src/nuts/nut11/serde_p2pk_witness.rs @@ -0,0 +1,22 @@ +//! Serde utils for P2PK Witness + +use serde::{de, ser, Deserialize, Deserializer, Serializer}; + +use super::P2PKWitness; + +/// Serialize [P2PKWitness] as stringified JSON +pub fn serialize(x: &P2PKWitness, s: S) -> Result +where + S: Serializer, +{ + s.serialize_str(&serde_json::to_string(&x).map_err(ser::Error::custom)?) +} + +/// Deserialize [P2PKWitness] from stringified JSON +pub fn deserialize<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let s: String = String::deserialize(deserializer)?; + serde_json::from_str(&s).map_err(de::Error::custom) +} diff --git a/crates/cdk/src/nuts/nut14/mod.rs b/crates/cdk/src/nuts/nut14/mod.rs new file mode 100644 index 00000000..74985968 --- /dev/null +++ b/crates/cdk/src/nuts/nut14/mod.rs @@ -0,0 +1,130 @@ +//! NUT-14: Hashed Time Lock Contacts (HTLC) +//! +//! + +use std::str::FromStr; + +use bitcoin::hashes::sha256::Hash as Sha256Hash; +use bitcoin::hashes::Hash; +use bitcoin::secp256k1::schnorr::Signature; +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +use super::nut00::Witness; +use super::nut10::Secret; +use super::nut11::valid_signatures; +use super::{Conditions, Proof}; +use crate::util::unix_time; + +pub mod serde_htlc_witness; + +#[derive(Debug, Error)] +pub enum Error { + /// Incorrect secret kind + #[error("Secret is not a HTLC secret")] + IncorrectSecretKind, + /// HTLC locktime has already passed + #[error("Locktime in past")] + LocktimeInPast, + /// Hash Required + #[error("Hash Required")] + HashRequired, + /// Hash is not valid + #[error("Hash is not valid")] + InvalidHash, + /// Preimage does not match + #[error("Preimage does not match")] + Preimage, + /// Witness Signatures not provided + #[error("Witness did not provide signatures")] + SignaturesNotProvided, + /// NUT11 Error + #[error(transparent)] + NUT11(#[from] super::nut11::Error), + #[error(transparent)] + Serde(#[from] serde_json::Error), +} + +#[derive(Default, Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct HTLCWitness { + pub preimage: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub signatures: Option>, +} + +impl Proof { + /// Verify HTLC + pub fn verify_htlc(&self) -> Result<(), Error> { + let secret: Secret = self.secret.clone().try_into()?; + let conditions: Conditions = secret.secret_data.tags.try_into()?; + + // Check locktime + if let Some(locktime) = conditions.locktime { + // If locktime is in passed and no refund keys provided anyone can spend + if locktime.lt(&unix_time()) && conditions.refund_keys.is_none() { + return Ok(()); + } + + // If refund keys are provided verify p2pk signatures + if let (Some(refund_key), Some(signatures)) = (conditions.refund_keys, &self.witness) { + let signatures: Vec = signatures + .signatures() + .ok_or(Error::SignaturesNotProvided)? + .iter() + .flat_map(|s| Signature::from_str(s)) + .collect(); + + // If secret includes refund keys check that there is a valid signature + if valid_signatures(self.secret.as_bytes(), &refund_key, &signatures).ge(&1) { + return Ok(()); + } + } + } + + if secret.kind.ne(&super::Kind::HTLC) { + return Err(Error::IncorrectSecretKind); + } + + let htlc_witness = match &self.witness { + Some(Witness::HTLCWitness(witness)) => witness, + _ => return Err(Error::IncorrectSecretKind), + }; + + let hash_lock = + Sha256Hash::from_str(&secret.secret_data.data).map_err(|_| Error::InvalidHash)?; + + let preimage_hash = Sha256Hash::hash(htlc_witness.preimage.as_bytes()); + + if hash_lock.ne(&preimage_hash) { + return Err(Error::Preimage); + } + + // If pubkeys are present check there is a valid signature + if let Some(pubkey) = conditions.pubkeys { + let req_sigs = conditions.num_sigs.unwrap_or(1); + let signatures = htlc_witness + .signatures + .as_ref() + .ok_or(Error::SignaturesNotProvided)?; + + let signatures: Vec = signatures + .iter() + .flat_map(|s| Signature::from_str(s)) + .collect(); + + if valid_signatures(self.secret.as_bytes(), &pubkey, &signatures).lt(&req_sigs) { + return Err(Error::IncorrectSecretKind); + } + } + + Ok(()) + } + + #[inline] + pub fn add_preimage(&mut self, preimage: String) { + self.witness = Some(Witness::HTLCWitness(HTLCWitness { + preimage, + signatures: None, + })) + } +} diff --git a/crates/cdk/src/nuts/nut14/serde_htlc_witness.rs b/crates/cdk/src/nuts/nut14/serde_htlc_witness.rs new file mode 100644 index 00000000..1224b4cf --- /dev/null +++ b/crates/cdk/src/nuts/nut14/serde_htlc_witness.rs @@ -0,0 +1,20 @@ +use serde::{de, ser, Deserialize, Deserializer, Serializer}; + +use super::HTLCWitness; + +/// Serialize [HTLCWitness] as stringified JSON +pub fn serialize(x: &HTLCWitness, s: S) -> Result +where + S: Serializer, +{ + s.serialize_str(&serde_json::to_string(&x).map_err(ser::Error::custom)?) +} + +/// Deserialize [HTLCWitness] from stringified JSON +pub fn deserialize<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let s: String = String::deserialize(deserializer)?; + serde_json::from_str(&s).map_err(de::Error::custom) +} diff --git a/crates/cdk/src/wallet.rs b/crates/cdk/src/wallet.rs index 1485f607..98a72589 100644 --- a/crates/cdk/src/wallet.rs +++ b/crates/cdk/src/wallet.rs @@ -6,21 +6,22 @@ use std::str::FromStr; use std::sync::Arc; use bip39::Mnemonic; +use bitcoin::hashes::sha256::Hash as Sha256Hash; +use bitcoin::hashes::Hash; use thiserror::Error; -use tracing::{debug, warn}; use crate::cdk_database::wallet_memory::WalletMemoryDatabase; use crate::cdk_database::{self, WalletDatabase}; use crate::client::HttpClient; use crate::dhke::{construct_proofs, hash_to_curve, unblind_message}; use crate::nuts::{ - nut12, BlindSignature, CurrencyUnit, Id, KeySet, KeySetInfo, Keys, MintInfo, P2PKConditions, + nut12, BlindSignature, Conditions, CurrencyUnit, Id, KeySet, KeySetInfo, Keys, Kind, MintInfo, PreMintSecrets, PreSwap, Proof, ProofState, Proofs, PublicKey, RestoreRequest, SigFlag, - SigningKey, State, SwapRequest, Token, + SigningKey, SpendingConditions, State, SwapRequest, Token, VerifyingKey, }; use crate::types::{MeltQuote, Melted, MintQuote}; use crate::url::UncheckedUrl; -use crate::util::unix_time; +use crate::util::{hex, unix_time}; use crate::{Amount, Bolt11Invoice}; #[derive(Debug, Error)] @@ -42,6 +43,8 @@ pub enum Error { P2PKConditionsNotMet(String), #[error("Invalid Spending Conditions: `{0}`")] InvalidSpendConditions(String), + #[error("Preimage not provided")] + PreimageNotProvided, #[error("Unknown Key")] UnknownKey, #[error(transparent)] @@ -61,6 +64,8 @@ pub enum Error { /// Database Error #[error(transparent)] Database(#[from] crate::cdk_database::Error), + #[error(transparent)] + Serde(#[from] serde_json::Error), #[error("`{0}`")] Custom(String), } @@ -136,7 +141,7 @@ impl Wallet { { Ok(mint_info) => Some(mint_info), Err(err) => { - warn!("Could not get mint info {}", err); + tracing::warn!("Could not get mint info {}", err); None } }; @@ -410,87 +415,6 @@ impl Wallet { Ok(minted_amount) } - /// Receive - pub async fn receive(&mut self, encoded_token: &str) -> Result<(), Error> { - let token_data = Token::from_str(encoded_token)?; - - let unit = token_data.unit.unwrap_or_default(); - - // Verify the signature DLEQ is valid - // Verify that all proofs in the token have a valid DLEQ proof if one is supplied - { - for mint_proof in &token_data.token { - let mint_url = &mint_proof.mint; - let proofs = &mint_proof.proofs; - - for proof in proofs { - let keys = self.get_keyset_keys(mint_url, proof.keyset_id).await?; - let key = keys.amount_key(proof.amount).ok_or(Error::UnknownKey)?; - match proof.verify_dleq(key) { - Ok(_) | Err(nut12::Error::MissingDleqProof) => continue, - Err(_) => return Err(Error::CouldNotVerifyDleq), - } - } - } - } - - let mut proofs: HashMap = HashMap::new(); - for token in token_data.token { - if token.proofs.is_empty() { - continue; - } - - let active_keyset_id = self.active_mint_keyset(&token.mint, &unit).await?; - - // TODO: if none fetch keyset for mint - - let keys = if let Some(keys) = self.localstore.get_keys(&active_keyset_id).await? { - keys - } else { - self.get_keyset_keys(&token.mint, active_keyset_id).await?; - self.localstore.get_keys(&active_keyset_id).await?.unwrap() - }; - - // Sum amount of all proofs - let amount: Amount = token.proofs.iter().map(|p| p.amount).sum(); - - let pre_swap = self - .create_swap(&token.mint, &unit, Some(amount), token.proofs) - .await?; - - let swap_response = self - .client - .post_swap(token.mint.clone().try_into()?, pre_swap.swap_request) - .await?; - - // Proof to keep - let p = construct_proofs( - swap_response.signatures, - pre_swap.pre_mint_secrets.rs(), - pre_swap.pre_mint_secrets.secrets(), - &keys, - )?; - - #[cfg(feature = "nut13")] - if self.mnemonic.is_some() { - self.localstore - .increment_keyset_counter(&active_keyset_id, p.len() as u64) - .await?; - } - - let mint_proofs = proofs.entry(token.mint).or_default(); - - mint_proofs.extend(p); - } - - for (mint, p) in proofs { - self.add_mint(mint.clone()).await?; - self.localstore.add_proofs(mint, p).await?; - } - - Ok(()) - } - /// Create Swap Payload async fn create_swap( &mut self, @@ -498,6 +422,7 @@ impl Wallet { unit: &CurrencyUnit, amount: Option, proofs: Proofs, + spending_conditions: Option, ) -> Result { let active_keyset_id = self.active_mint_keyset(mint_url, unit).await?; @@ -505,59 +430,112 @@ impl Wallet { let proofs_total = proofs.iter().map(|p| p.amount).sum(); let desired_amount = amount.unwrap_or(proofs_total); - - let mut counter = None; + let change_amount = proofs_total - desired_amount; let mut desired_messages; + let change_messages; #[cfg(not(feature = "nut13"))] { - desired_messages = PreMintSecrets::random(active_keyset_id, desired_amount)?; + (desired_messages, change_messages) = match spendig_conditions { + Some(conditions) => ( + PreMintSecrets::with_conditions(active_keyset_id, desired_amount, conditions)?, + PreMintSecrets::random(active_keyset_id, change_amount), + ), + None => ( + PreMintSecrets::random(active_keyset_id, proofs_total)?, + PreMintSecrets::default(), + ), + }; } #[cfg(feature = "nut13")] { - desired_messages = if let Some(mnemonic) = &self.mnemonic { - let count = self - .localstore - .get_keyset_counter(&active_keyset_id) - .await?; + (desired_messages, change_messages) = match &self.mnemonic { + Some(mnemonic) => match spending_conditions { + Some(conditions) => { + let count = self + .localstore + .get_keyset_counter(&active_keyset_id) + .await?; - let count = if let Some(count) = count { - count + 1 - } else { - 0 - }; + let count = if let Some(count) = count { + count + 1 + } else { + 0 + }; - let premint_secrets = PreMintSecrets::from_seed( - active_keyset_id, - count, - mnemonic, - desired_amount, - false, - )?; + let change_premint_secrets = PreMintSecrets::from_seed( + active_keyset_id, + count, + mnemonic, + change_amount, + false, + )?; - counter = Some(count + premint_secrets.len() as u64); + ( + PreMintSecrets::with_conditions( + active_keyset_id, + desired_amount, + conditions, + )?, + change_premint_secrets, + ) + } + None => { + let count = self + .localstore + .get_keyset_counter(&active_keyset_id) + .await?; - premint_secrets - } else { - PreMintSecrets::random(active_keyset_id, desired_amount)? + let count = if let Some(count) = count { + count + 1 + } else { + 0 + }; + + let premint_secrets = PreMintSecrets::from_seed( + active_keyset_id, + count, + mnemonic, + desired_amount, + false, + )?; + + let count = count + premint_secrets.len() as u64; + + let change_premint_secrets = PreMintSecrets::from_seed( + active_keyset_id, + count, + mnemonic, + change_amount, + false, + )?; + + (premint_secrets, change_premint_secrets) + } + }, + None => match spending_conditions { + Some(conditions) => ( + PreMintSecrets::with_conditions( + active_keyset_id, + desired_amount, + conditions, + )?, + PreMintSecrets::random(active_keyset_id, change_amount)?, + ), + None => ( + PreMintSecrets::random(active_keyset_id, desired_amount)?, + PreMintSecrets::random(active_keyset_id, change_amount)?, + ), + }, }; } - if let Some(amt) = amount { - let change_amount = proofs_total - amt; - - 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)? - }; - // Combine the BlindedMessages totoalling the desired amount with change - desired_messages.combine(change_messages); - // Sort the premint secrets to avoid finger printing - desired_messages.sort_secrets(); - }; + // Combine the BlindedMessages totoalling the desired amount with change + desired_messages.combine(change_messages); + // Sort the premint secrets to avoid finger printing + desired_messages.sort_secrets(); let swap_request = SwapRequest::new(proofs, desired_messages.blinded_messages()); @@ -633,11 +611,20 @@ impl Wallet { mint_url: &UncheckedUrl, unit: &CurrencyUnit, amount: Amount, + conditions: Option, ) -> Result { - let proofs = self.select_proofs(mint_url.clone(), unit, amount).await?; + let input_proofs = self.select_proofs(mint_url.clone(), unit, amount).await?; + + let active_keyset_id = self.active_mint_keyset(mint_url, unit).await?; let pre_swap = self - .create_swap(mint_url, unit, Some(amount), proofs.clone()) + .create_swap( + mint_url, + unit, + Some(amount), + input_proofs.clone(), + conditions, + ) .await?; let swap_response = self @@ -655,12 +642,10 @@ impl Wallet { &self.active_keys(mint_url, unit).await?.unwrap(), )?; - let active_keyset = self.active_mint_keyset(mint_url, unit).await?; - #[cfg(feature = "nut13")] if self.mnemonic.is_some() { self.localstore - .increment_keyset_counter(&active_keyset, post_swap_proofs.len() as u64) + .increment_keyset_counter(&active_keyset_id, post_swap_proofs.len() as u64) .await?; } @@ -677,18 +662,19 @@ impl Wallet { let send_amount: Amount = send_proofs.iter().map(|p| p.amount).sum(); if send_amount.ne(&amount) { - warn!( + tracing::warn!( "Send amount proofs is {:?} expected {:?}", - send_amount, amount + send_amount, + amount ); } self.localstore - .remove_proofs(mint_url.clone(), &proofs) + .remove_proofs(mint_url.clone(), &input_proofs) .await?; self.localstore - .add_pending_proofs(mint_url.clone(), proofs) + .add_pending_proofs(mint_url.clone(), input_proofs) .await?; self.localstore .add_pending_proofs(mint_url.clone(), send_proofs.clone()) @@ -874,7 +860,7 @@ impl Wallet { }; if let Some(change_proofs) = change_proofs { - debug!( + tracing::debug!( "Change amount returned from melt: {}", change_proofs.iter().map(|p| p.amount).sum::() ); @@ -901,89 +887,13 @@ impl Wallet { Ok(melted) } - /// Create P2PK locked proofs - /// Uses a swap to swap proofs for locked p2pk conditions - pub async fn send_p2pk( - &mut self, - mint_url: &UncheckedUrl, - unit: &CurrencyUnit, - amount: Amount, - conditions: P2PKConditions, - ) -> Result { - let input_proofs = self.select_proofs(mint_url.clone(), unit, amount).await?; - let active_keyset_id = self.active_mint_keyset(mint_url, unit).await?; - - let input_amount: Amount = input_proofs.iter().map(|p| p.amount).sum(); - let change_amount = input_amount - amount; - - let send_premint_secrets = - PreMintSecrets::with_p2pk_conditions(active_keyset_id, amount, conditions)?; - - let change_premint_secrets = PreMintSecrets::random(active_keyset_id, change_amount)?; - let mut pre_mint_secrets = send_premint_secrets; - pre_mint_secrets.combine(change_premint_secrets); - - let swap_request = - SwapRequest::new(input_proofs.clone(), pre_mint_secrets.blinded_messages()); - - let pre_swap = PreSwap { - pre_mint_secrets, - swap_request, - }; - - let swap_response = self - .client - .post_swap(mint_url.clone().try_into()?, pre_swap.swap_request) - .await?; - - let post_swap_proofs = construct_proofs( - swap_response.signatures, - pre_swap.pre_mint_secrets.rs(), - pre_swap.pre_mint_secrets.secrets(), - &self.active_keys(mint_url, unit).await?.unwrap(), - )?; - - let mut send_proofs = vec![]; - let mut change_proofs = vec![]; - - for proof in post_swap_proofs { - let conditions: Result = (&proof.secret).try_into(); - if conditions.is_ok() { - send_proofs.push(proof); - } else { - change_proofs.push(proof); - } - } - - self.localstore - .remove_proofs(mint_url.clone(), &input_proofs) - .await?; - - self.localstore - .add_pending_proofs(mint_url.clone(), input_proofs) - .await?; - self.localstore - .add_pending_proofs(mint_url.clone(), send_proofs.clone()) - .await?; - self.localstore - .add_proofs(mint_url.clone(), change_proofs.clone()) - .await?; - - Ok(send_proofs) - } - - /// Receive p2pk - pub async fn receive_p2pk( + /// Receive + pub async fn receive( &mut self, encoded_token: &str, - signing_keys: Vec, + signing_keys: Option>, + preimages: Option>, ) -> Result<(), Error> { - let signing_key = signing_keys[0].clone(); - let pubkey_secret_key: HashMap = signing_keys - .into_iter() - .map(|s| (s.public_key().to_string(), s)) - .collect(); - let token_data = Token::from_str(encoded_token)?; let unit = token_data.unit.unwrap_or_default(); @@ -1005,7 +915,27 @@ impl Wallet { let mut proofs = token.proofs; - let mut sig_flag = None; + let mut sig_flag = SigFlag::SigInputs; + + let pubkey_secret_key = match &signing_keys { + Some(signing_keys) => signing_keys + .iter() + .map(|s| (s.verifying_key().to_string(), s)) + .collect(), + None => HashMap::new(), + }; + + // Map hash of preimage to preimage + let hashed_to_preimage = match preimages { + Some(ref preimages) => preimages + .iter() + .flat_map(|p| match hex::decode(p) { + Ok(hex_bytes) => Some((Sha256Hash::hash(&hex_bytes).to_string(), p)), + Err(_) => None, + }) + .collect(), + None => HashMap::new(), + }; for proof in &mut proofs { // Verify that proof DLEQ is valid @@ -1020,29 +950,45 @@ impl Wallet { proof.secret.clone(), ) { - let conditions: Result = secret.try_into(); + let conditions: Result = secret.secret_data.tags.try_into(); if let Ok(conditions) = conditions { - let pubkeys = conditions.pubkeys; + let mut pubkeys = conditions.pubkeys.unwrap_or_default(); + match secret.kind { + Kind::P2PK => { + let data_key = VerifyingKey::from_str(&secret.secret_data.data)?; + + pubkeys.push(data_key); + } + Kind::HTLC => { + let hashed_preimage = &secret.secret_data.data; + let preimage = hashed_to_preimage + .get(hashed_preimage) + .ok_or(Error::PreimageNotProvided)?; + proof.add_preimage(preimage.to_string()); + } + } for pubkey in pubkeys { if let Some(signing) = pubkey_secret_key.get(&pubkey.to_string()) { - proof.sign_p2pk(signing.clone())?; + proof.sign_p2pk(signing.to_owned().clone())?; } } - sig_flag = Some(conditions.sig_flag); + if conditions.sig_flag.eq(&SigFlag::SigAll) { + sig_flag = SigFlag::SigAll; + } } } } let mut pre_swap = self - .create_swap(&token.mint, &unit, Some(amount), proofs) + .create_swap(&token.mint, &unit, Some(amount), proofs, None) .await?; - if let Some(sigflag) = sig_flag { - if sigflag.eq(&SigFlag::SigAll) { - for blinded_message in &mut pre_swap.swap_request.outputs { - blinded_message.sign_p2pk(signing_key.clone()).unwrap(); + if sig_flag.eq(&SigFlag::SigAll) { + for blinded_message in &mut pre_swap.swap_request.outputs { + for signing_key in pubkey_secret_key.values() { + blinded_message.sign_p2pk(signing_key.to_owned().clone())? } } } @@ -1112,7 +1058,7 @@ impl Wallet { start_counter + 100, )?; - debug!( + tracing::debug!( "Attempting to restore counter {}-{} for mint {} keyset {}", start_counter, start_counter + 100, @@ -1158,7 +1104,7 @@ impl Wallet { &keys, )?; - debug!("Restored {} proofs", proofs.len()); + tracing::debug!("Restored {} proofs", proofs.len()); #[cfg(feature = "nut13")] self.localstore @@ -1196,12 +1142,12 @@ impl Wallet { pub fn verify_token_p2pk( &self, token: &Token, - spending_conditions: P2PKConditions, + spending_conditions: Conditions, ) -> Result<(), Error> { use crate::nuts::nut10; if spending_conditions.refund_keys.is_some() && spending_conditions.locktime.is_none() { - warn!( + tracing::warn!( "Invalid spending conditions set: Locktime must be set if refund keys are allowed" ); return Err(Error::InvalidSpendConditions( @@ -1211,14 +1157,15 @@ impl Wallet { for mint_proof in &token.token { for proof in &mint_proof.proofs { - let secret: nut10::Secret = (&proof.secret).try_into().unwrap(); + let secret: nut10::Secret = (&proof.secret).try_into()?; - let proof_conditions: P2PKConditions = secret.try_into().unwrap(); + let proof_conditions: Conditions = secret.secret_data.tags.try_into()?; if spending_conditions.num_sigs.ne(&proof_conditions.num_sigs) { - debug!( + tracing::debug!( "Spending condition requires: {:?} sigs proof secret specifies: {:?}", - spending_conditions.num_sigs, proof_conditions.num_sigs + spending_conditions.num_sigs, + proof_conditions.num_sigs ); return Err(Error::P2PKConditionsNotMet( @@ -1226,17 +1173,17 @@ impl Wallet { )); } + let spending_condition_pubkeys = + spending_conditions.pubkeys.clone().unwrap_or_default(); + let proof_pubkeys = proof_conditions.pubkeys.unwrap_or_default(); + // Check the Proof has the required pubkeys - if proof_conditions - .pubkeys - .len() - .ne(&spending_conditions.pubkeys.len()) - || !proof_conditions - .pubkeys + if proof_pubkeys.len().ne(&spending_condition_pubkeys.len()) + || !proof_pubkeys .iter() - .all(|pubkey| spending_conditions.pubkeys.contains(pubkey)) + .all(|pubkey| spending_condition_pubkeys.contains(pubkey)) { - debug!("Proof did not included Publickeys meeting condition"); + tracing::debug!("Proof did not included Publickeys meeting condition"); return Err(Error::P2PKConditionsNotMet( "Pubkeys in proof not allowed by spending condition".to_string(), ));