Skip to main content

cashu/nuts/nut10/
mod.rs

1//! NUT-10: Spending conditions
2//!
3//! <https://github.com/cashubtc/nuts/blob/main/10.md>
4
5use std::str::FromStr;
6
7use serde::{Deserialize, Serialize};
8
9use super::nut01::PublicKey;
10use crate::{nut11, nut14};
11
12pub mod spending_conditions;
13pub use spending_conditions::{Conditions, SpendingConditions};
14
15pub mod secret;
16pub use secret::Secret;
17
18pub mod error;
19pub use error::Error;
20
21pub mod tag;
22pub use tag::{Tag, TagKind};
23
24/// Refund path requirements (available after locktime for HTLC)
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub(crate) struct RefundPath {
27    /// Public keys that can provide valid signatures for refund
28    pub pubkeys: Vec<PublicKey>,
29    /// Minimum number of signatures required from the refund pubkeys
30    pub required_sigs: u64,
31}
32
33/// Spending requirements for P2PK or HTLC verification
34///
35/// Returned by `get_pubkeys_and_required_sigs` to indicate what conditions
36/// must be met to spend a proof.
37#[derive(Debug, Clone, PartialEq, Eq)]
38pub(crate) struct SpendingRequirements {
39    /// Whether a preimage is required (HTLC only, for receiver path)
40    pub preimage_needed: bool,
41    /// Public keys that can provide valid signatures (receiver path)
42    pub pubkeys: Vec<PublicKey>,
43    /// Minimum number of signatures required from the pubkeys
44    pub required_sigs: u64,
45    /// Refund path (available after locktime for HTLC)
46    /// Per NUT-14: receiver path is ALWAYS available, refund path is available after locktime
47    pub refund_path: Option<RefundPath>,
48}
49
50///  NUT10 Secret Kind
51#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
52pub enum Kind {
53    /// NUT-11 P2PK
54    P2PK,
55    /// NUT-14 HTLC
56    HTLC,
57}
58
59/// Secret Date
60#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
61pub struct SecretData {
62    /// Unique random string
63    nonce: String,
64    /// Expresses the spending condition specific to each kind
65    data: String,
66    /// Additional data committed to and can be used for feature extensions
67    #[serde(skip_serializing_if = "Option::is_none")]
68    tags: Option<Vec<Vec<String>>>,
69}
70
71impl SecretData {
72    /// Create new [`SecretData`]
73    pub fn new<S, V>(data: S, tags: Option<V>) -> Self
74    where
75        S: Into<String>,
76        V: Into<Vec<Vec<String>>>,
77    {
78        let nonce = crate::secret::Secret::generate().to_string();
79
80        Self {
81            nonce,
82            data: data.into(),
83            tags: tags.map(Into::into),
84        }
85    }
86
87    /// Get the nonce
88    pub fn nonce(&self) -> &str {
89        &self.nonce
90    }
91
92    /// Get the data
93    pub fn data(&self) -> &str {
94        &self.data
95    }
96
97    /// Get the tags
98    pub fn tags(&self) -> Option<&Vec<Vec<String>>> {
99        self.tags.as_ref()
100    }
101}
102
103fn check_duplicate_pubkeys(pubkeys: &[PublicKey]) -> Result<(), Error> {
104    let mut x_coords = std::collections::HashSet::with_capacity(pubkeys.len());
105    for pk in pubkeys {
106        if !x_coords.insert(pk.x_only_public_key().serialize()) {
107            return Err(Error::NUT11(crate::nuts::nut11::Error::DuplicatePubkey));
108        }
109    }
110    Ok(())
111}
112
113/// Get the relevant public keys and required signature count for P2PK or HTLC verification
114/// This is for NUT-11(P2PK) and NUT-14(HTLC)
115///
116/// For P2PK (NUT-11):
117/// - Before locktime: only primary pubkeys path available
118/// - After locktime with refund keys: refund path available
119/// - After locktime without refund keys: anyone can spend
120///
121/// For HTLC (NUT-14):
122/// - Receiver path (preimage + pubkeys): ALWAYS available
123/// - Sender/Refund path (refund keys, no preimage): available AFTER locktime
124///
125/// From NUT-14: "This pathway is ALWAYS available to the receivers, as possession
126/// of the preimage confirms performance of the Sender's wishes."
127///
128/// Returns `SpendingRequirements` containing:
129/// - `preimage_needed`: For P2PK, always false. For HTLC, true (receiver path).
130/// - `pubkeys`: The public keys for the primary/receiver path
131/// - `required_sigs`: The minimum number of signatures required for primary path
132/// - `refund_path`: Optional refund path (available after locktime)
133pub(crate) fn get_pubkeys_and_required_sigs(
134    secret: &Secret,
135    current_time: u64,
136) -> Result<SpendingRequirements, Error> {
137    debug_assert!(
138        secret.kind() == Kind::P2PK || secret.kind() == Kind::HTLC,
139        "get_pubkeys_and_required_sigs called with invalid kind - this is a bug"
140    );
141
142    let conditions: Conditions = secret
143        .secret_data()
144        .tags()
145        .cloned()
146        .unwrap_or_default()
147        .try_into()?;
148
149    // Check if locktime has passed
150    let locktime_passed = conditions
151        .locktime
152        .map(|locktime| locktime < current_time)
153        .unwrap_or(false);
154
155    match secret.kind() {
156        Kind::P2PK => {
157            // P2PK: never needs preimage
158            // Per NUT-11: "Locktime Multisig conditions continue to apply, and the proof
159            // can continue to be spent according to Locktime Multisig rules."
160            // This means the primary path (data + pubkeys) is ALWAYS available.
161
162            // Build primary pubkeys (data + pubkeys tag)
163            let mut primary_keys = vec![];
164
165            // Add the pubkey from secret.data
166            let data_pubkey = PublicKey::from_str(secret.secret_data().data())?;
167            primary_keys.push(data_pubkey);
168
169            // Add any additional pubkeys from conditions
170            if let Some(additional_keys) = &conditions.pubkeys {
171                primary_keys.extend(additional_keys.clone());
172            }
173
174            check_duplicate_pubkeys(&primary_keys)?;
175
176            let primary_num_sigs_required = conditions.num_sigs.unwrap_or(1);
177
178            // Refund path is available after locktime
179            let refund_path = if locktime_passed {
180                if let Some(refund_keys) = &conditions.refund_keys {
181                    check_duplicate_pubkeys(refund_keys)?;
182                    Some(RefundPath {
183                        pubkeys: refund_keys.clone(),
184                        required_sigs: conditions.num_sigs_refund.unwrap_or(1),
185                    })
186                } else {
187                    // Locktime passed, no refund keys: anyone can spend via refund path
188                    Some(RefundPath {
189                        pubkeys: vec![],
190                        required_sigs: 0,
191                    })
192                }
193            } else {
194                None
195            };
196
197            Ok(SpendingRequirements {
198                preimage_needed: false,
199                pubkeys: primary_keys,
200                required_sigs: primary_num_sigs_required,
201                refund_path,
202            })
203        }
204        Kind::HTLC => {
205            // HTLC: receiver path (preimage + pubkeys) is ALWAYS available per NUT-14
206            // "This pathway is ALWAYS available to the receivers"
207            let pubkeys = conditions.pubkeys.clone().unwrap_or_default();
208
209            if !pubkeys.is_empty() {
210                check_duplicate_pubkeys(&pubkeys)?;
211            }
212
213            let required_sigs = if pubkeys.is_empty() {
214                0
215            } else {
216                conditions.num_sigs.unwrap_or(1)
217            };
218
219            // Refund path is available after locktime
220            let refund_path = if locktime_passed {
221                if let Some(refund_keys) = &conditions.refund_keys {
222                    check_duplicate_pubkeys(refund_keys)?;
223                    Some(RefundPath {
224                        pubkeys: refund_keys.clone(),
225                        required_sigs: conditions.num_sigs_refund.unwrap_or(1),
226                    })
227                } else {
228                    // Locktime passed, no refund keys: anyone can spend via refund path
229                    Some(RefundPath {
230                        pubkeys: vec![],
231                        required_sigs: 0,
232                    })
233                }
234            } else {
235                None
236            };
237
238            Ok(SpendingRequirements {
239                preimage_needed: true,
240                pubkeys,
241                required_sigs,
242                refund_path,
243            })
244        }
245    }
246}
247
248use super::Proofs;
249
250/// Trait for requests that spend proofs (SwapRequest, MeltRequest)
251pub trait SpendingConditionVerification {
252    /// Get the input proofs
253    fn inputs(&self) -> &Proofs;
254
255    /// Construct the message to sign for SIG_ALL verification
256    ///
257    /// This concatenates all relevant transaction data that must be signed.
258    /// For swap: input secrets + output blinded messages
259    /// For melt: input secrets + quote/payment request
260    fn sig_all_msg_to_sign(&self) -> String;
261
262    /// Check if at least one proof in the set has SIG_ALL flag set
263    ///
264    /// SIG_ALL requires all proofs in the transaction to be signed.
265    /// If any proof has this flag, we need to verify signatures on all proofs.
266    fn has_at_least_one_sig_all(&self) -> Result<bool, Error> {
267        for proof in self.inputs() {
268            // Try to extract spending conditions from the proof's secret
269            if let Ok(spending_conditions) = super::SpendingConditions::try_from(&proof.secret) {
270                // Check for SIG_ALL flag in either P2PK or HTLC conditions
271                let has_sig_all = match spending_conditions {
272                    super::SpendingConditions::P2PKConditions { conditions, .. } => conditions
273                        .map(|c| c.sig_flag == super::SigFlag::SigAll)
274                        .unwrap_or(false),
275                    super::SpendingConditions::HTLCConditions { conditions, .. } => conditions
276                        .map(|c| c.sig_flag == super::SigFlag::SigAll)
277                        .unwrap_or(false),
278                };
279
280                if has_sig_all {
281                    return Ok(true);
282                }
283            } else if proof.witness.is_some() {
284                return Err(Error::NUT11(nut11::Error::IncorrectWitnessKind));
285            }
286        }
287
288        Ok(false)
289    }
290
291    /// Verify all inputs meet SIG_ALL requirements per NUT-11
292    ///
293    /// When any input has SIG_ALL, all inputs must have:
294    /// 1. Same kind (P2PK or HTLC)
295    /// 2. SIG_ALL flag set
296    /// 3. Same Secret.data
297    /// 4. Same Secret.tags
298    fn verify_all_inputs_match_for_sig_all(&self) -> Result<(), Error> {
299        let inputs = self.inputs();
300
301        // Get first input's properties
302        let first_input = inputs.first().ok_or(Error::SpendConditionsNotMet)?;
303        let first_secret = Secret::try_from(&first_input.secret)?;
304        let first_kind = first_secret.kind();
305        let first_data = first_secret.secret_data().data();
306        let first_tags = first_secret.secret_data().tags();
307
308        // Get first input's conditions to check SIG_ALL flag
309        let first_conditions =
310            super::Conditions::try_from(first_tags.cloned().unwrap_or_default())?;
311
312        // Verify first input has SIG_ALL (it should, since we only call this function when SIG_ALL is detected)
313        if first_conditions.sig_flag != super::SigFlag::SigAll {
314            return Err(Error::SpendConditionsNotMet);
315        }
316
317        // Verify all remaining inputs match
318        for proof in inputs.iter().skip(1) {
319            let secret = Secret::try_from(&proof.secret)?;
320
321            // Check kind matches
322            if secret.kind() != first_kind {
323                return Err(Error::SpendConditionsNotMet);
324            }
325
326            // Check data matches
327            if secret.secret_data().data() != first_data {
328                return Err(Error::SpendConditionsNotMet);
329            }
330
331            // Check tags match (this also ensures SIG_ALL flag matches, since sig_flag is part of tags)
332            if secret.secret_data().tags() != first_tags {
333                return Err(Error::SpendConditionsNotMet);
334            }
335        }
336
337        Ok(())
338    }
339
340    /// Verify spending conditions for this transaction
341    ///
342    /// This is the main entry point for spending condition verification.
343    /// It checks if any input has SIG_ALL and dispatches to the appropriate verification path.
344    fn verify_spending_conditions(&self) -> Result<(), Error> {
345        // Check if any input has SIG_ALL flag
346        if self.has_at_least_one_sig_all()? {
347            // at least one input has SIG_ALL
348            self.verify_full_sig_all_check()
349        } else {
350            // none of the inputs are SIG_ALL, so we can simply check
351            // each independently and verify any spending conditions
352            // that may - or may not - be there.
353            self.verify_inputs_individually()
354        }
355    }
356
357    /// Verify spending conditions when SIG_ALL is present
358    ///
359    /// When SIG_ALL is set, all proofs in the transaction must be signed together.
360    fn verify_full_sig_all_check(&self) -> Result<(), Error> {
361        debug_assert!(
362            self.has_at_least_one_sig_all()?,
363            "verify_full_sig_all_check() called on proofs without SIG_ALL. This shouldn't happen"
364        );
365        // Verify all inputs meet SIG_ALL requirements per NUT-11:
366        // All inputs must have: (1) same kind, (2) SIG_ALL flag, (3) same data, (4) same tags
367        self.verify_all_inputs_match_for_sig_all()?;
368
369        // Get the first input to determine the kind
370        let first_input = self.inputs().first().ok_or(Error::SpendConditionsNotMet)?;
371        let first_secret =
372            Secret::try_from(&first_input.secret).map_err(|_| Error::IncorrectSecretKind)?;
373
374        // Dispatch based on secret kind
375        match first_secret.kind() {
376            Kind::P2PK => {
377                nut11::verify_sig_all_p2pk(first_input, self.sig_all_msg_to_sign())?;
378            }
379            Kind::HTLC => {
380                nut14::verify_sig_all_htlc(first_input, self.sig_all_msg_to_sign())?;
381            }
382        }
383
384        Ok(())
385    }
386
387    /// Verify spending conditions for each input individually
388    ///
389    /// Handles SIG_INPUTS mode, non-NUT-10 secrets, and any other case where inputs
390    /// are verified independently rather than as a group.
391    /// This function will NOT be called if any input has SIG_ALL.
392    fn verify_inputs_individually(&self) -> Result<(), Error> {
393        debug_assert!(
394            !(self.has_at_least_one_sig_all()?),
395            "verify_inputs_individually() called on SIG_ALL. This shouldn't happen"
396        );
397        for proof in self.inputs() {
398            // Check if secret is a nut10 secret with conditions
399            if let Ok(secret) = Secret::try_from(&proof.secret) {
400                // Verify this function isn't being called with SIG_ALL proofs (development check)
401                if let Ok(conditions) = super::Conditions::try_from(
402                    secret.secret_data().tags().cloned().unwrap_or_default(),
403                ) {
404                    debug_assert!(
405                        conditions.sig_flag != super::SigFlag::SigAll,
406                        "verify_inputs_individually called with SIG_ALL proof - this is a bug"
407                    );
408                }
409
410                match secret.kind() {
411                    Kind::P2PK => {
412                        proof.verify_p2pk()?;
413                    }
414                    Kind::HTLC => {
415                        proof.verify_htlc()?;
416                    }
417                }
418            }
419            // If not a nut10 secret, skip verification (plain secret)
420        }
421        Ok(())
422    }
423}
424
425#[cfg(test)]
426mod tests {
427    use std::assert_eq;
428    use std::str::FromStr;
429
430    use super::*;
431
432    #[test]
433    fn test_secret_serialize() {
434        let secret_data = SecretData::new(
435            "026562efcfadc8e86d44da6a8adf80633d974302e62c850774db1fb36ff4cc7198".to_string(),
436            Some(vec![vec![
437                "key".to_string(),
438                "value1".to_string(),
439                "value2".to_string(),
440            ]]),
441        );
442
443        let secret = Secret::new(Kind::P2PK, secret_data.clone());
444
445        let secret_str = format!(
446            r#"["P2PK",{{"nonce":"{}","data":"026562efcfadc8e86d44da6a8adf80633d974302e62c850774db1fb36ff4cc7198","tags":[["key","value1","value2"]]}}]"#,
447            secret_data.nonce(),
448        );
449
450        assert_eq!(serde_json::to_string(&secret).unwrap(), secret_str);
451    }
452
453    #[test]
454    fn test_secret_round_trip_serialization() {
455        // Create a Secret instance
456        let original_secret = Secret::new(
457            Kind::P2PK,
458            SecretData::new(
459                "026562efcfadc8e86d44da6a8adf80633d974302e62c850774db1fb36ff4cc7198".to_string(),
460                None::<Vec<Vec<String>>>,
461            ),
462        );
463
464        // Serialize the Secret to JSON string
465        let serialized = serde_json::to_string(&original_secret).unwrap();
466
467        // Deserialize directly back to Secret using serde
468        let deserialized_secret: Secret = serde_json::from_str(&serialized).unwrap();
469
470        // Verify the direct serde serialization/deserialization round trip works
471        assert_eq!(original_secret, deserialized_secret);
472
473        // Also verify that the conversion to crate::secret::Secret works
474        let cashu_secret = crate::secret::Secret::from_str(&serialized).unwrap();
475        let deserialized_from_cashu: Secret = TryFrom::try_from(&cashu_secret).unwrap();
476        assert_eq!(original_secret, deserialized_from_cashu);
477    }
478
479    #[test]
480    fn test_htlc_secret_round_trip() {
481        // The reference BOLT11 invoice is:
482        // lnbc100n1p5z3a63pp56854ytysg7e5z9fl3w5mgvrlqjfcytnjv8ff5hm5qt6gl6alxesqdqqcqzzsxqyz5vqsp5p0x0dlhn27s63j4emxnk26p7f94u0lyarnfp5yqmac9gzy4ngdss9qxpqysgqne3v0hnzt2lp0hc69xpzckk0cdcar7glvjhq60lsrfe8gejdm8c564prrnsft6ctxxyrewp4jtezrq3gxxqnfjj0f9tw2qs9y0lslmqpfu7et9
483
484        // Payment hash (typical 32 byte hash in hex format)
485        let payment_hash = "5c23fc3aec9d985bd5fc88ca8bceaccc52cf892715dd94b42b84f1b43350751e";
486
487        // Create a Secret instance with HTLC kind
488        let secret_data = SecretData::new(payment_hash.to_string(), None::<Vec<Vec<String>>>);
489
490        let original_secret = Secret::new(Kind::HTLC, secret_data.clone());
491
492        // Serialize the Secret to JSON string
493        let serialized = serde_json::to_string(&original_secret).unwrap();
494
495        // Validate serialized format
496        let expected_json = format!(
497            r#"["HTLC",{{"nonce":"{}","data":"{}"}}]"#,
498            secret_data.nonce(),
499            payment_hash
500        );
501        assert_eq!(serialized, expected_json);
502
503        // Deserialize directly back to Secret using serde
504        let deserialized_secret: Secret = serde_json::from_str(&serialized).unwrap();
505
506        // Verify the direct serde serialization/deserialization round trip works
507        assert_eq!(original_secret, deserialized_secret);
508        assert_eq!(deserialized_secret.kind(), Kind::HTLC);
509        assert_eq!(deserialized_secret.secret_data().data, payment_hash);
510    }
511}