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];
pubkeys.extend(conditions.pubkeys.clone().unwrap_or_default()); if let Some(conditions) = conditions {
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,13 +585,15 @@ 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 conditions.sig_flag.eq(&SigFlag::SigAll) { if let Ok(conditions) = Conditions::try_from(tags) {
sig_flag = SigFlag::SigAll; if conditions.sig_flag.eq(&SigFlag::SigAll) {
} sig_flag = SigFlag::SigAll;
}
if let Some(pubs) = conditions.pubkeys { if let Some(pubs) = conditions.pubkeys {
pubkeys.extend(pubs); pubkeys.extend(pubs);
}
} }
} }
} }
@@ -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,27 +56,55 @@ 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());
// Check locktime let htlc_witness = match &self.witness {
if let Some(locktime) = conditions.locktime { Some(Witness::HTLCWitness(witness)) => witness,
// If locktime is in passed and no refund keys provided anyone can spend _ => return Err(Error::IncorrectSecretKind),
if locktime.lt(&unix_time()) && conditions.refund_keys.is_none() { };
return Ok(());
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<Signature> = 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<Signature> = signatures let signatures: Vec<Signature> = signatures
.signatures()
.ok_or(Error::SignaturesNotProvided)?
.iter() .iter()
.flat_map(|s| Signature::from_str(s)) .flat_map(|s| Signature::from_str(s))
.collect(); .collect();
// If secret includes refund keys check that there is a valid signature if valid_signatures(self.secret.as_bytes(), &pubkey, &signatures).lt(&req_sigs) {
if valid_signatures(self.secret.as_bytes(), &refund_key, &signatures).ge(&1) { return Err(Error::IncorrectSecretKind);
return Ok(());
} }
} }
} }
@@ -85,11 +113,6 @@ impl Proof {
return Err(Error::IncorrectSecretKind); return Err(Error::IncorrectSecretKind);
} }
let htlc_witness = match &self.witness {
Some(Witness::HTLCWitness(witness)) => witness,
_ => return Err(Error::IncorrectSecretKind),
};
let hash_lock = let hash_lock =
Sha256Hash::from_str(&secret.secret_data.data).map_err(|_| Error::InvalidHash)?; Sha256Hash::from_str(&secret.secret_data.data).map_err(|_| Error::InvalidHash)?;
@@ -99,24 +122,6 @@ impl Proof {
return Err(Error::Preimage); 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<Signature> = 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(()) 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,21 +1549,32 @@ impl Wallet {
SpendingConditions::P2PKConditions { data, conditions } => { SpendingConditions::P2PKConditions { data, conditions } => {
let mut pubkeys = vec![data]; 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, conditions.refund_keys,
Some(pubkeys), conditions.pubkeys,
conditions.locktime, conditions.locktime,
conditions.num_sigs, conditions.num_sigs,
) ),
} None => (None, None, None, None),
SpendingConditions::HTLCConditions { conditions, .. } => ( },
conditions.refund_keys,
conditions.pubkeys,
conditions.locktime,
conditions.num_sigs,
),
}; };
if refund_keys.is_some() && locktime.is_none() { if refund_keys.is_some() && locktime.is_none() {