diff --git a/bindings/cdk-js/src/nuts/nut11.rs b/bindings/cdk-js/src/nuts/nut11.rs index ab08843f..ca2cd3df 100644 --- a/bindings/cdk-js/src/nuts/nut11.rs +++ b/bindings/cdk-js/src/nuts/nut11.rs @@ -39,10 +39,13 @@ impl Deref for JsP2PKSpendingConditions { #[wasm_bindgen(js_class = P2PKSpendingConditions)] impl JsP2PKSpendingConditions { #[wasm_bindgen(constructor)] - pub fn new(pubkey: String, conditions: JsConditions) -> Result { + pub fn new( + pubkey: String, + conditions: Option, + ) -> Result { let pubkey = PublicKey::from_str(&pubkey).map_err(into_err)?; Ok(Self { - inner: SpendingConditions::new_p2pk(pubkey, conditions.deref().clone()), + inner: SpendingConditions::new_p2pk(pubkey, conditions.map(|c| c.deref().clone())), }) } } diff --git a/bindings/cdk-js/src/nuts/nut14.rs b/bindings/cdk-js/src/nuts/nut14.rs index 7e4de1f9..7ff17b47 100644 --- a/bindings/cdk-js/src/nuts/nut14.rs +++ b/bindings/cdk-js/src/nuts/nut14.rs @@ -39,9 +39,12 @@ impl Deref for JsHTLCSpendingConditions { #[wasm_bindgen(js_class = HTLCSpendingConditions)] impl JsHTLCSpendingConditions { #[wasm_bindgen(constructor)] - pub fn new(preimage: String, conditions: JsConditions) -> Result { + pub fn new( + preimage: String, + conditions: Option, + ) -> Result { Ok(Self { - inner: SpendingConditions::new_htlc(preimage, conditions.deref().clone()) + inner: SpendingConditions::new_htlc(preimage, conditions.map(|c| c.deref().clone())) .map_err(into_err)?, }) } diff --git a/crates/cdk-cli/src/sub_commands/send.rs b/crates/cdk-cli/src/sub_commands/send.rs index b840fec9..78bd1745 100644 --- a/crates/cdk-cli/src/sub_commands/send.rs +++ b/crates/cdk-cli/src/sub_commands/send.rs @@ -100,7 +100,10 @@ pub async fn send(wallet: Wallet, sub_command_args: &SendSubCommand) -> Result<( ) .unwrap(); - Some(SpendingConditions::new_htlc(preimage.clone(), conditions)?) + Some(SpendingConditions::new_htlc( + preimage.clone(), + Some(conditions), + )?) } None => match sub_command_args.pubkey.is_empty() { true => None, @@ -136,7 +139,7 @@ pub async fn send(wallet: Wallet, sub_command_args: &SendSubCommand) -> Result<( Some(SpendingConditions::P2PKConditions { data: data_pubkey, - conditions, + conditions: Some(conditions), }) } }, diff --git a/crates/cdk/examples/p2pk.rs b/crates/cdk/examples/p2pk.rs index 8e765eb4..a1db0165 100644 --- a/crates/cdk/examples/p2pk.rs +++ b/crates/cdk/examples/p2pk.rs @@ -5,7 +5,7 @@ use std::time::Duration; use cdk::amount::SplitTarget; use cdk::cdk_database::WalletMemoryDatabase; use cdk::error::Error; -use cdk::nuts::{Conditions, CurrencyUnit, SecretKey, SpendingConditions}; +use cdk::nuts::{CurrencyUnit, SecretKey, SpendingConditions}; use cdk::wallet::Wallet; use cdk::{Amount, UncheckedUrl}; use rand::Rng; @@ -51,8 +51,7 @@ async fn main() -> Result<(), Error> { let secret = SecretKey::generate(); - let spending_conditions = - SpendingConditions::new_p2pk(secret.public_key(), Conditions::default()); + let spending_conditions = SpendingConditions::new_p2pk(secret.public_key(), None); let token = wallet .send( diff --git a/crates/cdk/src/nuts/nut10.rs b/crates/cdk/src/nuts/nut10.rs index 6844c191..3033af36 100644 --- a/crates/cdk/src/nuts/nut10.rs +++ b/crates/cdk/src/nuts/nut10.rs @@ -25,8 +25,8 @@ pub struct SecretData { /// Expresses the spending condition specific to each kind pub data: String, /// Additional data committed to and can be used for feature extensions - #[serde(skip_serializing_if = "Vec::is_empty")] - pub tags: Vec>, + #[serde(skip_serializing_if = "Option::is_none")] + pub tags: Option>>, } #[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize)] @@ -38,16 +38,17 @@ pub struct Secret { } impl Secret { - pub fn new(kind: Kind, data: S, tags: V) -> Self + pub fn new(kind: Kind, data: S, tags: Option) -> Self where S: Into, V: Into>>, { let nonce = crate::secret::Secret::generate().to_string(); + let secret_data = SecretData { nonce, data: data.into(), - tags: tags.into(), + tags: tags.map(|v| v.into()), }; Self { kind, secret_data } @@ -94,11 +95,11 @@ mod tests { nonce: "5d11913ee0f92fefdc82a6764fd2457a".to_string(), data: "026562efcfadc8e86d44da6a8adf80633d974302e62c850774db1fb36ff4cc7198" .to_string(), - tags: vec![vec![ + tags: Some(vec![vec![ "key".to_string(), "value1".to_string(), "value2".to_string(), - ]], + ]]), }, }; diff --git a/crates/cdk/src/nuts/nut11/mod.rs b/crates/cdk/src/nuts/nut11/mod.rs index 3c47acc5..f2e24407 100644 --- a/crates/cdk/src/nuts/nut11/mod.rs +++ b/crates/cdk/src/nuts/nut11/mod.rs @@ -119,7 +119,8 @@ impl Proof { /// 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 spending_conditions: Conditions = + secret.secret_data.tags.unwrap_or_default().try_into()?; let msg: &[u8] = self.secret.as_bytes(); let mut valid_sigs = 0; @@ -254,18 +255,18 @@ pub enum SpendingConditions { /// NUT11 Spending conditions P2PKConditions { data: PublicKey, - conditions: Conditions, + conditions: Option, }, /// NUT14 Spending conditions HTLCConditions { data: Sha256Hash, - conditions: Conditions, + conditions: Option, }, } impl SpendingConditions { /// New HTLC [SpendingConditions] - pub fn new_htlc(preimage: String, conditions: Conditions) -> Result { + pub fn new_htlc(preimage: String, conditions: Option) -> Result { let htlc = Sha256Hash::hash(&hex::decode(preimage)?); Ok(Self::HTLCConditions { @@ -275,7 +276,7 @@ impl SpendingConditions { } /// New P2PK [SpendingConditions] - pub fn new_p2pk(pubkey: PublicKey, conditions: Conditions) -> Self { + pub fn new_p2pk(pubkey: PublicKey, conditions: Option) -> Self { Self::P2PKConditions { data: pubkey, conditions, @@ -292,8 +293,8 @@ impl SpendingConditions { pub fn num_sigs(&self) -> Option { match self { - Self::P2PKConditions { conditions, .. } => conditions.num_sigs, - Self::HTLCConditions { conditions, .. } => conditions.num_sigs, + Self::P2PKConditions { conditions, .. } => conditions.as_ref().and_then(|c| c.num_sigs), + Self::HTLCConditions { conditions, .. } => conditions.as_ref().and_then(|c| c.num_sigs), } } @@ -301,25 +302,31 @@ impl SpendingConditions { match self { Self::P2PKConditions { data, conditions } => { let mut pubkeys = vec![*data]; - pubkeys.extend(conditions.pubkeys.clone().unwrap_or_default()); + if let Some(conditions) = conditions { + pubkeys.extend(conditions.pubkeys.clone().unwrap_or_default()); + } Some(pubkeys) } - Self::HTLCConditions { conditions, .. } => conditions.pubkeys.clone(), + Self::HTLCConditions { conditions, .. } => conditions.clone().and_then(|c| c.pubkeys), } } pub fn locktime(&self) -> Option { match self { - Self::P2PKConditions { conditions, .. } => conditions.locktime, - Self::HTLCConditions { conditions, .. } => conditions.locktime, + Self::P2PKConditions { conditions, .. } => conditions.as_ref().and_then(|c| c.locktime), + Self::HTLCConditions { conditions, .. } => conditions.as_ref().and_then(|c| c.locktime), } } - pub fn refund_keys(&self) -> &Option> { + pub fn refund_keys(&self) -> Option> { match self { - Self::P2PKConditions { conditions, .. } => &conditions.refund_keys, - Self::HTLCConditions { conditions, .. } => &conditions.refund_keys, + Self::P2PKConditions { conditions, .. } => { + conditions.clone().and_then(|c| c.refund_keys) + } + Self::HTLCConditions { conditions, .. } => { + conditions.clone().and_then(|c| c.refund_keys) + } } } } @@ -339,12 +346,12 @@ impl TryFrom for SpendingConditions { match secret.kind { Kind::P2PK => Ok(SpendingConditions::P2PKConditions { data: PublicKey::from_str(&secret.secret_data.data)?, - conditions: secret.secret_data.tags.try_into()?, + conditions: secret.secret_data.tags.and_then(|t| t.try_into().ok()), }), Kind::HTLC => Ok(Self::HTLCConditions { data: Sha256Hash::from_str(&secret.secret_data.data) .map_err(|_| Error::InvalidHash)?, - conditions: secret.secret_data.tags.try_into()?, + conditions: secret.secret_data.tags.and_then(|t| t.try_into().ok()), }), } } @@ -578,13 +585,15 @@ pub fn enforce_sig_flag(proofs: Proofs) -> (SigFlag, HashSet) { } } - 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(tags) = secret.secret_data.tags { + if let Ok(conditions) = Conditions::try_from(tags) { + if conditions.sig_flag.eq(&SigFlag::SigAll) { + sig_flag = SigFlag::SigAll; + } - if let Some(pubs) = conditions.pubkeys { - pubkeys.extend(pubs); + if let Some(pubs) = conditions.pubkeys { + pubkeys.extend(pubs); + } } } } @@ -744,7 +753,7 @@ mod tests { sig_flag: SigFlag::SigAll, }; - let secret: Nut10Secret = Nut10Secret::new(Kind::P2PK, data.to_string(), conditions); + let secret: Nut10Secret = Nut10Secret::new(Kind::P2PK, data.to_string(), Some(conditions)); let secret_str = serde_json::to_string(&secret).unwrap(); @@ -778,7 +787,7 @@ mod tests { sig_flag: SigFlag::SigInputs, }; - let secret: Secret = Nut10Secret::new(Kind::P2PK, v_key.to_string(), conditions) + let secret: Secret = Nut10Secret::new(Kind::P2PK, v_key.to_string(), Some(conditions)) .try_into() .unwrap(); diff --git a/crates/cdk/src/nuts/nut14/mod.rs b/crates/cdk/src/nuts/nut14/mod.rs index 74985968..15244251 100644 --- a/crates/cdk/src/nuts/nut14/mod.rs +++ b/crates/cdk/src/nuts/nut14/mod.rs @@ -56,27 +56,55 @@ 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()?; + let conditions: Option = + secret.secret_data.tags.and_then(|c| c.try_into().ok()); - // 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(()); + let htlc_witness = match &self.witness { + Some(Witness::HTLCWitness(witness)) => witness, + _ => return Err(Error::IncorrectSecretKind), + }; + + if let Some(conditions) = conditions { + // 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 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)?; - // 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 valid_signatures(self.secret.as_bytes(), &pubkey, &signatures).lt(&req_sigs) { + return Err(Error::IncorrectSecretKind); } } } @@ -85,11 +113,6 @@ impl Proof { 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)?; @@ -99,24 +122,6 @@ impl Proof { 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(()) } diff --git a/crates/cdk/src/wallet/mod.rs b/crates/cdk/src/wallet/mod.rs index 07b10ba4..2403a1de 100644 --- a/crates/cdk/src/wallet/mod.rs +++ b/crates/cdk/src/wallet/mod.rs @@ -1344,7 +1344,8 @@ impl Wallet { proof.secret.clone(), ) { - let conditions: Result = secret.secret_data.tags.try_into(); + let conditions: Result = + secret.secret_data.tags.unwrap_or_default().try_into(); if let Ok(conditions) = conditions { let mut pubkeys = conditions.pubkeys.unwrap_or_default(); @@ -1548,21 +1549,32 @@ impl Wallet { SpendingConditions::P2PKConditions { data, conditions } => { let mut pubkeys = vec![data]; - pubkeys.extend(conditions.pubkeys.unwrap_or_default()); + match conditions { + Some(conditions) => { + pubkeys.extend(conditions.pubkeys.unwrap_or_default()); - ( + ( + conditions.refund_keys, + Some(pubkeys), + conditions.locktime, + conditions.num_sigs, + ) + } + None => (None, Some(pubkeys), None, None), + } + } + SpendingConditions::HTLCConditions { + conditions, + data: _, + } => match conditions { + Some(conditions) => ( conditions.refund_keys, - Some(pubkeys), + conditions.pubkeys, conditions.locktime, conditions.num_sigs, - ) - } - SpendingConditions::HTLCConditions { conditions, .. } => ( - conditions.refund_keys, - conditions.pubkeys, - conditions.locktime, - conditions.num_sigs, - ), + ), + None => (None, None, None, None), + }, }; if refund_keys.is_some() && locktime.is_none() {