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)]
impl JsP2PKSpendingConditions {
#[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)?;
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)]
impl JsHTLCSpendingConditions {
#[wasm_bindgen(constructor)]
pub fn new(preimage: String, conditions: JsConditions) -> Result<JsHTLCSpendingConditions> {
pub fn new(
preimage: String,
conditions: Option<JsConditions>,
) -> Result<JsHTLCSpendingConditions> {
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)?,
})
}

View File

@@ -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),
})
}
},

View File

@@ -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(

View File

@@ -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<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tags: Option<Vec<Vec<String>>>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize)]
@@ -38,16 +38,17 @@ pub struct 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
S: Into<String>,
V: Into<Vec<Vec<String>>>,
{
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(),
]],
]]),
},
};

View File

@@ -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<Conditions>,
},
/// NUT14 Spending conditions
HTLCConditions {
data: Sha256Hash,
conditions: Conditions,
conditions: Option<Conditions>,
},
}
impl 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)?);
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<Conditions>) -> Self {
Self::P2PKConditions {
data: pubkey,
conditions,
@@ -292,8 +293,8 @@ impl SpendingConditions {
pub fn num_sigs(&self) -> Option<u64> {
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<u64> {
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<Vec<PublicKey>> {
pub fn refund_keys(&self) -> Option<Vec<PublicKey>> {
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<Nut10Secret> 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<PublicKey>) {
}
}
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();

View File

@@ -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<Conditions> =
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<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
.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<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(())
}

View File

@@ -1344,7 +1344,8 @@ impl Wallet {
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 {
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() {