fix(NUT10): secret tag is optional

This commit is contained in:
thesimplekid
2024-06-21 11:44:38 +02:00
parent b066b92a8d
commit a97f64fa56
8 changed files with 122 additions and 87 deletions

View File

@@ -39,10 +39,13 @@ impl Deref for JsP2PKSpendingConditions {
#[wasm_bindgen(js_class = P2PKSpendingConditions)] #[wasm_bindgen(js_class = P2PKSpendingConditions)]
impl JsP2PKSpendingConditions { impl JsP2PKSpendingConditions {
#[wasm_bindgen(constructor)] #[wasm_bindgen(constructor)]
pub fn new(pubkey: String, conditions: JsConditions) -> Result<JsP2PKSpendingConditions> { pub fn new(
pubkey: String,
conditions: Option<JsConditions>,
) -> Result<JsP2PKSpendingConditions> {
let pubkey = PublicKey::from_str(&pubkey).map_err(into_err)?; let pubkey = PublicKey::from_str(&pubkey).map_err(into_err)?;
Ok(Self { Ok(Self {
inner: SpendingConditions::new_p2pk(pubkey, conditions.deref().clone()), inner: SpendingConditions::new_p2pk(pubkey, conditions.map(|c| c.deref().clone())),
}) })
} }
} }

View File

@@ -39,9 +39,12 @@ impl Deref for JsHTLCSpendingConditions {
#[wasm_bindgen(js_class = HTLCSpendingConditions)] #[wasm_bindgen(js_class = HTLCSpendingConditions)]
impl JsHTLCSpendingConditions { impl JsHTLCSpendingConditions {
#[wasm_bindgen(constructor)] #[wasm_bindgen(constructor)]
pub fn new(preimage: String, conditions: JsConditions) -> Result<JsHTLCSpendingConditions> { pub fn new(
preimage: String,
conditions: Option<JsConditions>,
) -> Result<JsHTLCSpendingConditions> {
Ok(Self { 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)?, .map_err(into_err)?,
}) })
} }

View File

@@ -100,7 +100,10 @@ pub async fn send(wallet: Wallet, sub_command_args: &SendSubCommand) -> Result<(
) )
.unwrap(); .unwrap();
Some(SpendingConditions::new_htlc(preimage.clone(), conditions)?) Some(SpendingConditions::new_htlc(
preimage.clone(),
Some(conditions),
)?)
} }
None => match sub_command_args.pubkey.is_empty() { None => match sub_command_args.pubkey.is_empty() {
true => None, true => None,
@@ -136,7 +139,7 @@ pub async fn send(wallet: Wallet, sub_command_args: &SendSubCommand) -> Result<(
Some(SpendingConditions::P2PKConditions { Some(SpendingConditions::P2PKConditions {
data: data_pubkey, data: data_pubkey,
conditions, conditions: Some(conditions),
}) })
} }
}, },

View File

@@ -5,7 +5,7 @@ use std::time::Duration;
use cdk::amount::SplitTarget; use cdk::amount::SplitTarget;
use cdk::cdk_database::WalletMemoryDatabase; use cdk::cdk_database::WalletMemoryDatabase;
use cdk::error::Error; use cdk::error::Error;
use cdk::nuts::{Conditions, CurrencyUnit, SecretKey, SpendingConditions}; use cdk::nuts::{CurrencyUnit, SecretKey, SpendingConditions};
use cdk::wallet::Wallet; use cdk::wallet::Wallet;
use cdk::{Amount, UncheckedUrl}; use cdk::{Amount, UncheckedUrl};
use rand::Rng; use rand::Rng;
@@ -51,8 +51,7 @@ async fn main() -> Result<(), Error> {
let secret = SecretKey::generate(); let secret = SecretKey::generate();
let spending_conditions = let spending_conditions = SpendingConditions::new_p2pk(secret.public_key(), None);
SpendingConditions::new_p2pk(secret.public_key(), Conditions::default());
let token = wallet let token = wallet
.send( .send(

View File

@@ -25,8 +25,8 @@ pub struct SecretData {
/// Expresses the spending condition specific to each kind /// Expresses the spending condition specific to each kind
pub data: String, pub data: String,
/// Additional data committed to and can be used for feature extensions /// Additional data committed to and can be used for feature extensions
#[serde(skip_serializing_if = "Vec::is_empty")] #[serde(skip_serializing_if = "Option::is_none")]
pub tags: Vec<Vec<String>>, pub tags: Option<Vec<Vec<String>>>,
} }
#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize)]
@@ -38,16 +38,17 @@ pub struct Secret {
} }
impl Secret { impl Secret {
pub fn new<S, V>(kind: Kind, data: S, tags: V) -> Self pub fn new<S, V>(kind: Kind, data: S, tags: Option<V>) -> Self
where where
S: Into<String>, S: Into<String>,
V: Into<Vec<Vec<String>>>, V: Into<Vec<Vec<String>>>,
{ {
let nonce = crate::secret::Secret::generate().to_string(); let nonce = crate::secret::Secret::generate().to_string();
let secret_data = SecretData { let secret_data = SecretData {
nonce, nonce,
data: data.into(), data: data.into(),
tags: tags.into(), tags: tags.map(|v| v.into()),
}; };
Self { kind, secret_data } Self { kind, secret_data }
@@ -94,11 +95,11 @@ mod tests {
nonce: "5d11913ee0f92fefdc82a6764fd2457a".to_string(), nonce: "5d11913ee0f92fefdc82a6764fd2457a".to_string(),
data: "026562efcfadc8e86d44da6a8adf80633d974302e62c850774db1fb36ff4cc7198" data: "026562efcfadc8e86d44da6a8adf80633d974302e62c850774db1fb36ff4cc7198"
.to_string(), .to_string(),
tags: vec![vec![ tags: Some(vec![vec![
"key".to_string(), "key".to_string(),
"value1".to_string(), "value1".to_string(),
"value2".to_string(), "value2".to_string(),
]], ]]),
}, },
}; };

View File

@@ -119,7 +119,8 @@ impl Proof {
/// Verify P2PK signature on [Proof] /// Verify P2PK signature on [Proof]
pub fn verify_p2pk(&self) -> Result<(), Error> { pub fn verify_p2pk(&self) -> Result<(), Error> {
let secret: Nut10Secret = self.secret.clone().try_into()?; 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 msg: &[u8] = self.secret.as_bytes();
let mut valid_sigs = 0; let mut valid_sigs = 0;
@@ -254,18 +255,18 @@ pub enum SpendingConditions {
/// NUT11 Spending conditions /// NUT11 Spending conditions
P2PKConditions { P2PKConditions {
data: PublicKey, data: PublicKey,
conditions: Conditions, conditions: Option<Conditions>,
}, },
/// NUT14 Spending conditions /// NUT14 Spending conditions
HTLCConditions { HTLCConditions {
data: Sha256Hash, data: Sha256Hash,
conditions: Conditions, conditions: Option<Conditions>,
}, },
} }
impl SpendingConditions { impl SpendingConditions {
/// New HTLC [SpendingConditions] /// New HTLC [SpendingConditions]
pub fn new_htlc(preimage: String, conditions: Conditions) -> Result<Self, Error> { pub fn new_htlc(preimage: String, conditions: Option<Conditions>) -> Result<Self, Error> {
let htlc = Sha256Hash::hash(&hex::decode(preimage)?); let htlc = Sha256Hash::hash(&hex::decode(preimage)?);
Ok(Self::HTLCConditions { Ok(Self::HTLCConditions {
@@ -275,7 +276,7 @@ impl SpendingConditions {
} }
/// New P2PK [SpendingConditions] /// New P2PK [SpendingConditions]
pub fn new_p2pk(pubkey: PublicKey, conditions: Conditions) -> Self { pub fn new_p2pk(pubkey: PublicKey, conditions: Option<Conditions>) -> Self {
Self::P2PKConditions { Self::P2PKConditions {
data: pubkey, data: pubkey,
conditions, conditions,
@@ -292,8 +293,8 @@ impl SpendingConditions {
pub fn num_sigs(&self) -> Option<u64> { pub fn num_sigs(&self) -> Option<u64> {
match self { match self {
Self::P2PKConditions { conditions, .. } => conditions.num_sigs, Self::P2PKConditions { conditions, .. } => conditions.as_ref().and_then(|c| c.num_sigs),
Self::HTLCConditions { conditions, .. } => conditions.num_sigs, Self::HTLCConditions { conditions, .. } => conditions.as_ref().and_then(|c| c.num_sigs),
} }
} }
@@ -301,25 +302,31 @@ impl SpendingConditions {
match self { match self {
Self::P2PKConditions { data, conditions } => { Self::P2PKConditions { data, conditions } => {
let mut pubkeys = vec![*data]; let mut pubkeys = vec![*data];
if let Some(conditions) = conditions {
pubkeys.extend(conditions.pubkeys.clone().unwrap_or_default()); pubkeys.extend(conditions.pubkeys.clone().unwrap_or_default());
}
Some(pubkeys) Some(pubkeys)
} }
Self::HTLCConditions { conditions, .. } => conditions.pubkeys.clone(), Self::HTLCConditions { conditions, .. } => conditions.clone().and_then(|c| c.pubkeys),
} }
} }
pub fn locktime(&self) -> Option<u64> { pub fn locktime(&self) -> Option<u64> {
match self { match self {
Self::P2PKConditions { conditions, .. } => conditions.locktime, Self::P2PKConditions { conditions, .. } => conditions.as_ref().and_then(|c| c.locktime),
Self::HTLCConditions { conditions, .. } => conditions.locktime, Self::HTLCConditions { conditions, .. } => conditions.as_ref().and_then(|c| c.locktime),
} }
} }
pub fn refund_keys(&self) -> &Option<Vec<PublicKey>> { pub fn refund_keys(&self) -> Option<Vec<PublicKey>> {
match self { match self {
Self::P2PKConditions { conditions, .. } => &conditions.refund_keys, Self::P2PKConditions { conditions, .. } => {
Self::HTLCConditions { conditions, .. } => &conditions.refund_keys, 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<Nut10Secret> for SpendingConditions {
match secret.kind { match secret.kind {
Kind::P2PK => Ok(SpendingConditions::P2PKConditions { Kind::P2PK => Ok(SpendingConditions::P2PKConditions {
data: PublicKey::from_str(&secret.secret_data.data)?, 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 { Kind::HTLC => Ok(Self::HTLCConditions {
data: Sha256Hash::from_str(&secret.secret_data.data) data: Sha256Hash::from_str(&secret.secret_data.data)
.map_err(|_| Error::InvalidHash)?, .map_err(|_| Error::InvalidHash)?,
conditions: secret.secret_data.tags.try_into()?, conditions: secret.secret_data.tags.and_then(|t| t.try_into().ok()),
}), }),
} }
} }
@@ -578,7 +585,8 @@ pub fn enforce_sig_flag(proofs: Proofs) -> (SigFlag, HashSet<PublicKey>) {
} }
} }
if let Ok(conditions) = Conditions::try_from(secret.secret_data.tags) { if let Some(tags) = secret.secret_data.tags {
if let Ok(conditions) = Conditions::try_from(tags) {
if conditions.sig_flag.eq(&SigFlag::SigAll) { if conditions.sig_flag.eq(&SigFlag::SigAll) {
sig_flag = SigFlag::SigAll; sig_flag = SigFlag::SigAll;
} }
@@ -589,6 +597,7 @@ pub fn enforce_sig_flag(proofs: Proofs) -> (SigFlag, HashSet<PublicKey>) {
} }
} }
} }
}
(sig_flag, pubkeys) (sig_flag, pubkeys)
} }
@@ -744,7 +753,7 @@ mod tests {
sig_flag: SigFlag::SigAll, 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(); let secret_str = serde_json::to_string(&secret).unwrap();
@@ -778,7 +787,7 @@ mod tests {
sig_flag: SigFlag::SigInputs, 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() .try_into()
.unwrap(); .unwrap();

View File

@@ -56,8 +56,15 @@ impl Proof {
/// Verify HTLC /// Verify HTLC
pub fn verify_htlc(&self) -> Result<(), Error> { pub fn verify_htlc(&self) -> Result<(), Error> {
let secret: Secret = self.secret.clone().try_into()?; let secret: Secret = self.secret.clone().try_into()?;
let conditions: Conditions = secret.secret_data.tags.try_into()?; let conditions: Option<Conditions> =
secret.secret_data.tags.and_then(|c| c.try_into().ok());
let htlc_witness = match &self.witness {
Some(Witness::HTLCWitness(witness)) => witness,
_ => return Err(Error::IncorrectSecretKind),
};
if let Some(conditions) = conditions {
// Check locktime // Check locktime
if let Some(locktime) = conditions.locktime { if let Some(locktime) = conditions.locktime {
// If locktime is in passed and no refund keys provided anyone can spend // If locktime is in passed and no refund keys provided anyone can spend
@@ -66,7 +73,9 @@ impl Proof {
} }
// If refund keys are provided verify p2pk signatures // If refund keys are provided verify p2pk signatures
if let (Some(refund_key), Some(signatures)) = (conditions.refund_keys, &self.witness) { if let (Some(refund_key), Some(signatures)) =
(conditions.refund_keys, &self.witness)
{
let signatures: Vec<Signature> = signatures let signatures: Vec<Signature> = signatures
.signatures() .signatures()
.ok_or(Error::SignaturesNotProvided)? .ok_or(Error::SignaturesNotProvided)?
@@ -80,28 +89,10 @@ impl Proof {
} }
} }
} }
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 pubkeys are present check there is a valid signature
if let Some(pubkey) = conditions.pubkeys { if let Some(pubkey) = conditions.pubkeys {
let req_sigs = conditions.num_sigs.unwrap_or(1); let req_sigs = conditions.num_sigs.unwrap_or(1);
let signatures = htlc_witness let signatures = htlc_witness
.signatures .signatures
.as_ref() .as_ref()
@@ -116,6 +107,20 @@ impl Proof {
return Err(Error::IncorrectSecretKind); return Err(Error::IncorrectSecretKind);
} }
} }
}
if secret.kind.ne(&super::Kind::HTLC) {
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);
}
Ok(()) Ok(())
} }

View File

@@ -1344,7 +1344,8 @@ impl Wallet {
proof.secret.clone(), proof.secret.clone(),
) )
{ {
let conditions: Result<Conditions, _> = secret.secret_data.tags.try_into(); let conditions: Result<Conditions, _> =
secret.secret_data.tags.unwrap_or_default().try_into();
if let Ok(conditions) = conditions { if let Ok(conditions) = conditions {
let mut pubkeys = conditions.pubkeys.unwrap_or_default(); let mut pubkeys = conditions.pubkeys.unwrap_or_default();
@@ -1548,6 +1549,8 @@ impl Wallet {
SpendingConditions::P2PKConditions { data, conditions } => { SpendingConditions::P2PKConditions { data, conditions } => {
let mut pubkeys = vec![data]; let mut pubkeys = vec![data];
match conditions {
Some(conditions) => {
pubkeys.extend(conditions.pubkeys.unwrap_or_default()); pubkeys.extend(conditions.pubkeys.unwrap_or_default());
( (
@@ -1557,12 +1560,21 @@ impl Wallet {
conditions.num_sigs, conditions.num_sigs,
) )
} }
SpendingConditions::HTLCConditions { conditions, .. } => ( None => (None, Some(pubkeys), None, None),
}
}
SpendingConditions::HTLCConditions {
conditions,
data: _,
} => match conditions {
Some(conditions) => (
conditions.refund_keys, conditions.refund_keys,
conditions.pubkeys, conditions.pubkeys,
conditions.locktime, conditions.locktime,
conditions.num_sigs, conditions.num_sigs,
), ),
None => (None, None, None, None),
},
}; };
if refund_keys.is_some() && locktime.is_none() { if refund_keys.is_some() && locktime.is_none() {