From b4728d7257302b6651d54ee8ae4d188a0684251c Mon Sep 17 00:00:00 2001 From: thesimplekid Date: Sat, 28 Jun 2025 11:22:55 +0100 Subject: [PATCH] feat: refund multi sig --- crates/cashu/src/nuts/nut11/mod.rs | 30 +++++++++++++++++-- .../src/sub_commands/create_request.rs | 3 ++ crates/cdk-cli/src/sub_commands/send.rs | 9 +++--- 3 files changed, 35 insertions(+), 7 deletions(-) diff --git a/crates/cashu/src/nuts/nut11/mod.rs b/crates/cashu/src/nuts/nut11/mod.rs index bc83cde7..076aac03 100644 --- a/crates/cashu/src/nuts/nut11/mod.rs +++ b/crates/cashu/src/nuts/nut11/mod.rs @@ -178,15 +178,24 @@ impl Proof { spending_conditions.locktime, spending_conditions.refund_keys, ) { + let needed_refund_sigs = spending_conditions.num_sigs_refund.unwrap_or(1) as usize; + + let mut valid_pubkeys = HashSet::new(); + // If lock time has passed check if refund witness signature is valid if locktime.lt(&unix_time()) { 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(()); + if !valid_pubkeys.insert(v) { + return Err(Error::DuplicateSignature); + } + + if valid_pubkeys.len() >= needed_refund_sigs { + return Ok(()); + } } } } @@ -443,7 +452,7 @@ pub struct Conditions { /// Refund keys #[serde(skip_serializing_if = "Option::is_none")] pub refund_keys: Option>, - /// Numbedr of signatures required + /// Number of signatures required /// /// Default is 1 #[serde(skip_serializing_if = "Option::is_none")] @@ -452,6 +461,11 @@ pub struct Conditions { /// /// Default [`SigFlag::SigInputs`] pub sig_flag: SigFlag, + /// Number of refund signatures required + /// + /// Default is 1 + #[serde(skip_serializing_if = "Option::is_none")] + pub num_sigs_refund: Option, } impl Conditions { @@ -462,6 +476,7 @@ impl Conditions { refund_keys: Option>, num_sigs: Option, sig_flag: Option, + num_sigs_refund: Option, ) -> Result { if let Some(locktime) = locktime { ensure_cdk!(locktime.ge(&unix_time()), Error::LocktimeInPast); @@ -473,6 +488,7 @@ impl Conditions { refund_keys, num_sigs, sig_flag: sig_flag.unwrap_or_default(), + num_sigs_refund, }) } } @@ -484,6 +500,7 @@ impl From for Vec> { refund_keys, num_sigs, sig_flag, + num_sigs_refund: _, } = conditions; let mut tags = Vec::new(); @@ -564,6 +581,7 @@ impl TryFrom>> for Conditions { refund_keys, num_sigs, sig_flag, + num_sigs_refund: None, }) } } @@ -583,6 +601,9 @@ pub enum TagKind { Refund, /// Pubkey Pubkeys, + /// Number signatures required + #[serde(rename = "n_sigs_refund")] + NSigsRefund, /// Custom tag kind Custom(String), } @@ -595,6 +616,7 @@ impl fmt::Display for TagKind { Self::Locktime => write!(f, "locktime"), Self::Refund => write!(f, "refund"), Self::Pubkeys => write!(f, "pubkeys"), + Self::NSigsRefund => write!(f, "n_sigs_refund"), Self::Custom(kind) => write!(f, "{kind}"), } } @@ -857,6 +879,7 @@ mod tests { .unwrap()]), num_sigs: Some(2), sig_flag: SigFlag::SigAll, + num_sigs_refund: None, }; let secret: Nut10Secret = Nut10Secret::new(Kind::P2PK, data.to_string(), Some(conditions)); @@ -891,6 +914,7 @@ mod tests { refund_keys: Some(vec![v_key]), num_sigs: Some(2), sig_flag: SigFlag::SigInputs, + num_sigs_refund: None, }; let secret: Secret = Nut10Secret::new(Kind::P2PK, v_key.to_string(), Some(conditions)) diff --git a/crates/cdk-cli/src/sub_commands/create_request.rs b/crates/cdk-cli/src/sub_commands/create_request.rs index 04779f48..cda60dd4 100644 --- a/crates/cdk-cli/src/sub_commands/create_request.rs +++ b/crates/cdk-cli/src/sub_commands/create_request.rs @@ -161,6 +161,7 @@ pub async fn create_request( refund_keys: None, num_sigs: Some(num_sigs), sig_flag: SigFlag::SigInputs, + num_sigs_refund: None, }; // Try to parse the hash @@ -186,6 +187,7 @@ pub async fn create_request( refund_keys: None, num_sigs: Some(num_sigs), sig_flag: SigFlag::SigInputs, + num_sigs_refund: None, }; // Create HTLC conditions with the hash and pubkeys in conditions @@ -203,6 +205,7 @@ pub async fn create_request( refund_keys: None, num_sigs: Some(num_sigs), sig_flag: SigFlag::SigInputs, + num_sigs_refund: None, }), )) } diff --git a/crates/cdk-cli/src/sub_commands/send.rs b/crates/cdk-cli/src/sub_commands/send.rs index 59cd1078..5d177547 100644 --- a/crates/cdk-cli/src/sub_commands/send.rs +++ b/crates/cdk-cli/src/sub_commands/send.rs @@ -118,6 +118,7 @@ pub async fn send( refund_keys, sub_command_args.required_sigs, None, + None, ) .unwrap(); @@ -155,8 +156,8 @@ pub async fn send( refund_keys, sub_command_args.required_sigs, None, - ) - .unwrap(); + None, + )?; Some(SpendingConditions::new_htlc_hash(hash, Some(conditions))?) } @@ -187,8 +188,8 @@ pub async fn send( refund_keys, sub_command_args.required_sigs, None, - ) - .unwrap(); + None, + )?; Some(SpendingConditions::P2PKConditions { data: data_pubkey,