Skip to main content

cashu/nuts/
nut13.rs

1//! NUT-13: Deterministic Secrets
2//!
3//! <https://github.com/cashubtc/nuts/blob/main/13.md>
4
5use bitcoin::bip32::{ChildNumber, DerivationPath, Xpriv};
6use bitcoin::secp256k1::hashes::{hmac, sha256, Hash, HashEngine, HmacEngine};
7use bitcoin::{secp256k1, Network};
8use thiserror::Error;
9use tracing::instrument;
10
11use super::nut00::{BlindedMessage, PreMint, PreMintSecrets};
12use super::nut01::SecretKey;
13use super::nut02::Id;
14use crate::amount::{FeeAndAmounts, SplitTarget};
15use crate::dhke::blind_message;
16use crate::secret::Secret;
17use crate::util::hex;
18use crate::{Amount, SECP256K1};
19
20/// NUT13 Error
21#[derive(Debug, Error)]
22pub enum Error {
23    /// DHKE error
24    #[error(transparent)]
25    DHKE(#[from] crate::dhke::Error),
26    /// Amount Error
27    #[error(transparent)]
28    Amount(#[from] crate::amount::Error),
29    /// NUT00 Error
30    #[error(transparent)]
31    NUT00(#[from] crate::nuts::nut00::Error),
32    /// NUT02 Error
33    #[error(transparent)]
34    NUT02(#[from] crate::nuts::nut02::Error),
35    /// Bip32 Error
36    #[error(transparent)]
37    Bip32(#[from] bitcoin::bip32::Error),
38    /// HMAC Error
39    #[error("HMAC error: {0}")]
40    Hmac(bitcoin::secp256k1::hashes::FromSliceError),
41    /// SecretKey Error
42    #[error(transparent)]
43    SecpError(#[from] bitcoin::secp256k1::Error),
44}
45
46impl Secret {
47    /// Create new [`Secret`] from seed
48    pub fn from_seed(seed: &[u8; 64], keyset_id: Id, counter: u32) -> Result<Self, Error> {
49        match keyset_id.get_version() {
50            super::nut02::KeySetVersion::Version00 => Self::legacy_derive(seed, keyset_id, counter),
51            super::nut02::KeySetVersion::Version01 => Self::derive(seed, keyset_id, counter),
52        }
53    }
54
55    fn legacy_derive(seed: &[u8; 64], keyset_id: Id, counter: u32) -> Result<Self, Error> {
56        let xpriv = Xpriv::new_master(Network::Bitcoin, seed)?;
57        let path = derive_path_from_keyset_id(keyset_id)?
58            .child(ChildNumber::from_hardened_idx(counter)?)
59            .child(ChildNumber::from_normal_idx(0)?);
60        let derived_xpriv = xpriv.derive_priv(&SECP256K1, &path)?;
61
62        Ok(Self::new(hex::encode(
63            derived_xpriv.private_key.secret_bytes(),
64        )))
65    }
66
67    fn derive(seed: &[u8; 64], keyset_id: Id, counter: u32) -> Result<Self, Error> {
68        let mut message = Vec::new();
69        message.extend_from_slice(b"Cashu_KDF_HMAC_SHA256");
70        message.extend_from_slice(&keyset_id.to_bytes());
71        message.extend_from_slice(&(counter as u64).to_be_bytes());
72        message.extend_from_slice(b"\x00");
73
74        let mut engine = HmacEngine::<sha256::Hash>::new(seed);
75        engine.input(&message);
76        let hmac_result = hmac::Hmac::<sha256::Hash>::from_engine(engine);
77        let result_bytes = hmac_result.to_byte_array();
78
79        Ok(Self::new(hex::encode(&result_bytes[..32])))
80    }
81}
82
83impl SecretKey {
84    /// Create new [`SecretKey`] from seed
85    pub fn from_seed(seed: &[u8; 64], keyset_id: Id, counter: u32) -> Result<Self, Error> {
86        match keyset_id.get_version() {
87            super::nut02::KeySetVersion::Version00 => Self::legacy_derive(seed, keyset_id, counter),
88            super::nut02::KeySetVersion::Version01 => Self::derive(seed, keyset_id, counter),
89        }
90    }
91
92    fn legacy_derive(seed: &[u8; 64], keyset_id: Id, counter: u32) -> Result<Self, Error> {
93        let xpriv = Xpriv::new_master(Network::Bitcoin, seed)?;
94        let path = derive_path_from_keyset_id(keyset_id)?
95            .child(ChildNumber::from_hardened_idx(counter)?)
96            .child(ChildNumber::from_normal_idx(1)?);
97        let derived_xpriv = xpriv.derive_priv(&SECP256K1, &path)?;
98
99        Ok(Self::from(derived_xpriv.private_key))
100    }
101
102    fn derive(seed: &[u8; 64], keyset_id: Id, counter: u32) -> Result<Self, Error> {
103        let mut message = Vec::new();
104        message.extend_from_slice(b"Cashu_KDF_HMAC_SHA256");
105        message.extend_from_slice(&keyset_id.to_bytes());
106        message.extend_from_slice(&(counter as u64).to_be_bytes());
107        message.extend_from_slice(b"\x01");
108
109        let mut engine = HmacEngine::<sha256::Hash>::new(seed);
110        engine.input(&message);
111        let hmac_result = hmac::Hmac::<sha256::Hash>::from_engine(engine);
112        let result_bytes = hmac_result.to_byte_array();
113
114        Ok(Self::from(secp256k1::SecretKey::from_slice(
115            &result_bytes[..32],
116        )?))
117    }
118}
119
120impl PreMintSecrets {
121    /// Generate blinded messages from predetermined secrets and blindings
122    /// factor
123    #[instrument(skip(seed))]
124    pub fn from_seed(
125        keyset_id: Id,
126        counter: u32,
127        seed: &[u8; 64],
128        amount: Amount,
129        amount_split_target: &SplitTarget,
130        fee_and_amounts: &FeeAndAmounts,
131    ) -> Result<Self, Error> {
132        let mut pre_mint_secrets = PreMintSecrets::new(keyset_id);
133
134        for (counter, amount) in
135            (counter..).zip(amount.split_targeted(amount_split_target, fee_and_amounts)?)
136        {
137            let secret = Secret::from_seed(seed, keyset_id, counter)?;
138            let blinding_factor = SecretKey::from_seed(seed, keyset_id, counter)?;
139
140            let (blinded, r) = blind_message(&secret.to_bytes(), Some(blinding_factor))?;
141
142            let blinded_message = BlindedMessage::new(amount, keyset_id, blinded);
143
144            let pre_mint = PreMint {
145                blinded_message,
146                secret: secret.clone(),
147                r,
148                amount,
149            };
150
151            pre_mint_secrets.secrets.push(pre_mint);
152        }
153
154        Ok(pre_mint_secrets)
155    }
156
157    /// New [`PreMintSecrets`] from seed with a zero amount used for change
158    pub fn from_seed_blank(
159        keyset_id: Id,
160        counter: u32,
161        seed: &[u8; 64],
162        amount: Amount,
163    ) -> Result<Self, Error> {
164        if amount <= Amount::ZERO {
165            return Ok(PreMintSecrets::new(keyset_id));
166        }
167        let count = ((u64::from(amount) as f64).log2().ceil() as u64).max(1);
168        let mut pre_mint_secrets = PreMintSecrets::new(keyset_id);
169
170        for counter in counter..(counter + count as u32) {
171            let secret = Secret::from_seed(seed, keyset_id, counter)?;
172            let blinding_factor = SecretKey::from_seed(seed, keyset_id, counter)?;
173
174            let (blinded, r) = blind_message(&secret.to_bytes(), Some(blinding_factor))?;
175
176            let amount = Amount::ZERO;
177
178            let blinded_message = BlindedMessage::new(amount, keyset_id, blinded);
179
180            let pre_mint = PreMint {
181                blinded_message,
182                secret: secret.clone(),
183                r,
184                amount,
185            };
186
187            pre_mint_secrets.secrets.push(pre_mint);
188        }
189
190        Ok(pre_mint_secrets)
191    }
192
193    /// Generate blinded messages from predetermined secrets and blindings
194    /// factor
195    pub fn restore_batch(
196        keyset_id: Id,
197        seed: &[u8; 64],
198        start_count: u32,
199        end_count: u32,
200    ) -> Result<Self, Error> {
201        let mut pre_mint_secrets = PreMintSecrets::new(keyset_id);
202
203        for i in start_count..end_count {
204            let secret = Secret::from_seed(seed, keyset_id, i)?;
205            let blinding_factor = SecretKey::from_seed(seed, keyset_id, i)?;
206
207            let (blinded, r) = blind_message(&secret.to_bytes(), Some(blinding_factor))?;
208
209            let blinded_message = BlindedMessage::new(Amount::ZERO, keyset_id, blinded);
210
211            let pre_mint = PreMint {
212                blinded_message,
213                secret: secret.clone(),
214                r,
215                amount: Amount::ZERO,
216            };
217
218            pre_mint_secrets.secrets.push(pre_mint);
219        }
220
221        Ok(pre_mint_secrets)
222    }
223}
224
225fn derive_path_from_keyset_id(id: Id) -> Result<DerivationPath, Error> {
226    let index = u32::from(id);
227
228    let keyset_child_number = ChildNumber::from_hardened_idx(index)?;
229    Ok(DerivationPath::from(vec![
230        ChildNumber::from_hardened_idx(129372)?,
231        ChildNumber::from_hardened_idx(0)?,
232        keyset_child_number,
233    ]))
234}
235
236#[cfg(test)]
237mod tests {
238    use std::str::FromStr;
239
240    use bip39::Mnemonic;
241    use bitcoin::bip32::DerivationPath;
242
243    use super::*;
244
245    #[test]
246    fn test_secret_from_seed() {
247        let seed =
248            "half depart obvious quality work element tank gorilla view sugar picture humble";
249        let mnemonic = Mnemonic::from_str(seed).unwrap();
250        let seed: [u8; 64] = mnemonic.to_seed("");
251        let keyset_id = Id::from_str("009a1f293253e41e").unwrap();
252
253        let test_secrets = [
254            "485875df74771877439ac06339e284c3acfcd9be7abf3bc20b516faeadfe77ae",
255            "8f2b39e8e594a4056eb1e6dbb4b0c38ef13b1b2c751f64f810ec04ee35b77270",
256            "bc628c79accd2364fd31511216a0fab62afd4a18ff77a20deded7b858c9860c8",
257            "59284fd1650ea9fa17db2b3acf59ecd0f2d52ec3261dd4152785813ff27a33bf",
258            "576c23393a8b31cc8da6688d9c9a96394ec74b40fdaf1f693a6bb84284334ea0",
259        ];
260
261        for (i, test_secret) in test_secrets.iter().enumerate() {
262            let secret = Secret::from_seed(&seed, keyset_id, i.try_into().unwrap()).unwrap();
263            assert_eq!(secret, Secret::from_str(test_secret).unwrap())
264        }
265    }
266    #[test]
267    fn test_r_from_seed() {
268        let seed =
269            "half depart obvious quality work element tank gorilla view sugar picture humble";
270        let mnemonic = Mnemonic::from_str(seed).unwrap();
271        let seed: [u8; 64] = mnemonic.to_seed("");
272        let keyset_id = Id::from_str("009a1f293253e41e").unwrap();
273
274        let test_rs = [
275            "ad00d431add9c673e843d4c2bf9a778a5f402b985b8da2d5550bf39cda41d679",
276            "967d5232515e10b81ff226ecf5a9e2e2aff92d66ebc3edf0987eb56357fd6248",
277            "b20f47bb6ae083659f3aa986bfa0435c55c6d93f687d51a01f26862d9b9a4899",
278            "fb5fca398eb0b1deb955a2988b5ac77d32956155f1c002a373535211a2dfdc29",
279            "5f09bfbfe27c439a597719321e061e2e40aad4a36768bb2bcc3de547c9644bf9",
280        ];
281
282        for (i, test_r) in test_rs.iter().enumerate() {
283            let r = SecretKey::from_seed(&seed, keyset_id, i.try_into().unwrap()).unwrap();
284            assert_eq!(r, SecretKey::from_hex(test_r).unwrap())
285        }
286    }
287
288    #[test]
289    fn test_derive_path_from_keyset_id() {
290        let test_cases = [
291            ("009a1f293253e41e", "m/129372'/0'/864559728'"),
292            ("0000000000000000", "m/129372'/0'/0'"),
293            ("00ffffffffffffff", "m/129372'/0'/33554431'"),
294        ];
295
296        for (id_hex, expected_path) in test_cases {
297            let id = Id::from_str(id_hex).unwrap();
298            let path = derive_path_from_keyset_id(id).unwrap();
299            assert_eq!(
300                DerivationPath::from_str(expected_path).unwrap(),
301                path,
302                "Path derivation failed for ID {id_hex}"
303            );
304        }
305    }
306
307    #[test]
308    fn test_secret_derivation_keyset_v2() {
309        let seed =
310            "half depart obvious quality work element tank gorilla view sugar picture humble";
311        let mnemonic = Mnemonic::from_str(seed).unwrap();
312        let seed: [u8; 64] = mnemonic.to_seed("");
313
314        // Test with a v2 keyset ID (33 bytes, starting with "01")
315        let keyset_id =
316            Id::from_str("012e23479a0029432eaad0d2040c09be53bab592d5cbf1d55e0dd26c9495951b30")
317                .unwrap();
318
319        // Expected secrets derived using the new derivation
320        let test_secrets = [
321            "ba250bf927b1df5dd0a07c543be783a4349a7f99904acd3406548402d3484118",
322            "3a6423fe56abd5e74ec9d22a91ee110cd2ce45a7039901439d62e5534d3438c1",
323            "843484a75b78850096fac5b513e62854f11d57491cf775a6fd2edf4e583ae8c0",
324            "3600608d5cf8197374f060cfbcff134d2cd1fb57eea68cbcf2fa6917c58911b6",
325            "717fce9cc6f9ea060d20dd4e0230af4d63f3894cc49dd062fd99d033ea1ac1dd",
326        ];
327
328        for (i, test_secret) in test_secrets.iter().enumerate() {
329            let secret = Secret::from_seed(&seed, keyset_id, i.try_into().unwrap()).unwrap();
330            // Note: The actual expected values would need to be computed from a reference implementation
331            // For now, we just verify the derivation works and produces consistent results
332            assert_eq!(secret.to_string().len(), 64); // Should be 32 bytes = 64 hex chars
333
334            // Test deterministic derivation: same inputs should produce same outputs
335            let secret2 = Secret::from_str(test_secret).unwrap();
336            assert_eq!(secret, secret2);
337        }
338    }
339
340    #[test]
341    fn test_secret_key_derivation_keyset_v2() {
342        let seed =
343            "half depart obvious quality work element tank gorilla view sugar picture humble";
344        let mnemonic = Mnemonic::from_str(seed).unwrap();
345        let seed: [u8; 64] = mnemonic.to_seed("");
346
347        // Test with a v2 keyset ID (33 bytes, starting with "01")
348        let keyset_id =
349            Id::from_str("012e23479a0029432eaad0d2040c09be53bab592d5cbf1d55e0dd26c9495951b30")
350                .unwrap();
351
352        let test_secret_keys = [
353            "4f8b32a54aed811b692a665ed296b4c1fc2f37a8be4006379e95063a76693745",
354            "c4b8412ee644067007423480c9e556385b71ffdff0f340bc16a95c0534fe0e01",
355            "ceff40983441c40acaf77d2a8ddffd5c1c84391fb9fd0dc4607c186daab1c829",
356            "41ad26b840fb62d29b2318a82f1d9cd40dc0f1e58183cc57562f360a32fdfad6",
357            "fb986a9c76758593b0e2d1a5172ade977c858d87111a220e16c292a9347abf81",
358        ];
359
360        for (i, test_secret) in test_secret_keys.iter().enumerate() {
361            let secret_key = SecretKey::from_seed(&seed, keyset_id, i as u32).unwrap();
362
363            // Verify the secret key is valid (32 bytes)
364            let secret_bytes = secret_key.secret_bytes();
365            assert_eq!(secret_bytes.len(), 32);
366
367            // Test deterministic derivation
368            let secret_key2 = SecretKey::from_str(test_secret).unwrap();
369            assert_eq!(secret_key, secret_key2);
370        }
371    }
372
373    #[test]
374    fn test_v2_derivation_with_different_keysets() {
375        let seed =
376            "half depart obvious quality work element tank gorilla view sugar picture humble";
377        let mnemonic = Mnemonic::from_str(seed).unwrap();
378        let seed: [u8; 64] = mnemonic.to_seed("");
379
380        let keyset_id_1 =
381            Id::from_str("01adc013fa9d85171586660abab27579888611659d357bc86bc09cb26eee8bc035")
382                .unwrap();
383        let keyset_id_2 =
384            Id::from_str("01bef024fb9e85171586660abab27579888611659d357bc86bc09cb26eee8bc046")
385                .unwrap();
386
387        // Different keyset IDs should produce different secrets even with same counter
388        for counter in 0..3 {
389            let secret_1 = Secret::from_seed(&seed, keyset_id_1, counter).unwrap();
390            let secret_2 = Secret::from_seed(&seed, keyset_id_2, counter).unwrap();
391            assert_ne!(
392                secret_1, secret_2,
393                "Different keyset IDs should produce different secrets for counter {}",
394                counter
395            );
396
397            let secret_key_1 = SecretKey::from_seed(&seed, keyset_id_1, counter).unwrap();
398            let secret_key_2 = SecretKey::from_seed(&seed, keyset_id_2, counter).unwrap();
399            assert_ne!(
400                secret_key_1, secret_key_2,
401                "Different keyset IDs should produce different secret keys for counter {}",
402                counter
403            );
404        }
405    }
406
407    #[test]
408    fn test_v2_derivation_incremental_counters() {
409        let seed =
410            "half depart obvious quality work element tank gorilla view sugar picture humble";
411        let mnemonic = Mnemonic::from_str(seed).unwrap();
412        let seed: [u8; 64] = mnemonic.to_seed("");
413
414        let keyset_id =
415            Id::from_str("01adc013fa9d85171586660abab27579888611659d357bc86bc09cb26eee8bc035")
416                .unwrap();
417
418        let mut secrets = Vec::new();
419        let mut secret_keys = Vec::new();
420
421        // Generate secrets with incremental counters
422        for counter in 0..10 {
423            let secret = Secret::from_seed(&seed, keyset_id, counter).unwrap();
424            let secret_key = SecretKey::from_seed(&seed, keyset_id, counter).unwrap();
425
426            // Ensure no duplicates
427            assert!(
428                !secrets.contains(&secret),
429                "Duplicate secret found for counter {}",
430                counter
431            );
432            assert!(
433                !secret_keys.contains(&secret_key),
434                "Duplicate secret key found for counter {}",
435                counter
436            );
437
438            secrets.push(secret);
439            secret_keys.push(secret_key);
440        }
441    }
442
443    #[test]
444    fn test_v2_hmac_message_construction() {
445        let seed =
446            "half depart obvious quality work element tank gorilla view sugar picture humble";
447        let mnemonic = Mnemonic::from_str(seed).unwrap();
448        let seed: [u8; 64] = mnemonic.to_seed("");
449
450        let keyset_id =
451            Id::from_str("01adc013fa9d85171586660abab27579888611659d357bc86bc09cb26eee8bc035")
452                .unwrap();
453        let counter: u32 = 42;
454
455        // Test that the HMAC message is constructed correctly
456        // Message should be: b"Cashu_KDF_HMAC_SHA512" + keyset_id.to_bytes() + counter.to_be_bytes()
457        let _expected_prefix = b"Cashu_KDF_HMAC_SHA512";
458        let keyset_bytes = keyset_id.to_bytes();
459        let _counter_bytes = (counter as u64).to_be_bytes();
460
461        // Verify keyset ID v2 structure: version byte (01) + 32 bytes
462        assert_eq!(keyset_bytes.len(), 33);
463        assert_eq!(keyset_bytes[0], 0x01);
464
465        // The actual HMAC construction is internal, but we can verify the derivation works
466        let secret = Secret::from_seed(&seed, keyset_id, counter).unwrap();
467        let secret_key = SecretKey::from_seed(&seed, keyset_id, counter).unwrap();
468
469        // Verify outputs are valid hex strings of correct length
470        assert_eq!(secret.to_string().len(), 64); // 32 bytes as hex
471        assert_eq!(secret_key.secret_bytes().len(), 32);
472    }
473
474    #[test]
475    fn test_pre_mint_secrets_with_v2_keyset() {
476        let seed =
477            "half depart obvious quality work element tank gorilla view sugar picture humble";
478        let mnemonic = Mnemonic::from_str(seed).unwrap();
479        let seed: [u8; 64] = mnemonic.to_seed("");
480
481        let keyset_id =
482            Id::from_str("01adc013fa9d85171586660abab27579888611659d357bc86bc09cb26eee8bc035")
483                .unwrap();
484        let amount = Amount::from(1000u64);
485        let split_target = SplitTarget::default();
486        let fee_and_amounts = (0, (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()).into();
487
488        // Test PreMintSecrets generation with v2 keyset
489        let pre_mint_secrets =
490            PreMintSecrets::from_seed(keyset_id, 0, &seed, amount, &split_target, &fee_and_amounts)
491                .unwrap();
492
493        // Verify all secrets in the pre_mint use the new v2 derivation
494        for (i, pre_mint) in pre_mint_secrets.secrets.iter().enumerate() {
495            // Verify the secret was derived correctly
496            let expected_secret = Secret::from_seed(&seed, keyset_id, i as u32).unwrap();
497            assert_eq!(pre_mint.secret, expected_secret);
498
499            // Verify keyset ID version
500            assert_eq!(
501                pre_mint.blinded_message.keyset_id.get_version(),
502                super::super::nut02::KeySetVersion::Version01
503            );
504        }
505    }
506
507    #[test]
508    fn test_restore_batch_with_v2_keyset() {
509        let seed =
510            "half depart obvious quality work element tank gorilla view sugar picture humble";
511        let mnemonic = Mnemonic::from_str(seed).unwrap();
512        let seed: [u8; 64] = mnemonic.to_seed("");
513
514        let keyset_id =
515            Id::from_str("01adc013fa9d85171586660abab27579888611659d357bc86bc09cb26eee8bc035")
516                .unwrap();
517
518        let start_count = 5;
519        let end_count = 10;
520
521        // Test batch restoration with v2 keyset
522        let pre_mint_secrets =
523            PreMintSecrets::restore_batch(keyset_id, &seed, start_count, end_count).unwrap();
524
525        assert_eq!(
526            pre_mint_secrets.secrets.len(),
527            (end_count - start_count) as usize
528        );
529
530        // Verify each secret in the batch
531        for (i, pre_mint) in pre_mint_secrets.secrets.iter().enumerate() {
532            let counter = start_count + i as u32;
533            let expected_secret = Secret::from_seed(&seed, keyset_id, counter).unwrap();
534            assert_eq!(pre_mint.secret, expected_secret);
535        }
536    }
537}