1use bitcoin::bip32::{ChildNumber, DerivationPath, Xpriv};
6use bitcoin::secp256k1::hashes::{hmac, sha512, 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::SplitTarget;
15use crate::dhke::blind_message;
16use crate::secret::Secret;
17use crate::util::hex;
18use crate::{Amount, SECP256K1};
19
20#[derive(Debug, Error)]
22pub enum Error {
23 #[error(transparent)]
25 DHKE(#[from] crate::dhke::Error),
26 #[error(transparent)]
28 Amount(#[from] crate::amount::Error),
29 #[error(transparent)]
31 NUT00(#[from] crate::nuts::nut00::Error),
32 #[error(transparent)]
34 NUT02(#[from] crate::nuts::nut02::Error),
35 #[error(transparent)]
37 Bip32(#[from] bitcoin::bip32::Error),
38 #[error(transparent)]
40 Hmac(#[from] bitcoin::secp256k1::hashes::FromSliceError),
41 #[error(transparent)]
43 SecpError(#[from] bitcoin::secp256k1::Error),
44}
45
46impl Secret {
47 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_SHA512");
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::<sha512::Hash>::new(seed);
75 engine.input(&message);
76 let hmac_result = hmac::Hmac::<sha512::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 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_SHA512");
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::<sha512::Hash>::new(seed);
110 engine.input(&message);
111 let hmac_result = hmac::Hmac::<sha512::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 #[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 ) -> Result<Self, Error> {
131 let mut pre_mint_secrets = PreMintSecrets::new(keyset_id);
132
133 let mut counter = counter;
134
135 for amount in amount.split_targeted(amount_split_target)? {
136 let secret = Secret::from_seed(seed, keyset_id, counter)?;
137 let blinding_factor = SecretKey::from_seed(seed, keyset_id, counter)?;
138
139 let (blinded, r) = blind_message(&secret.to_bytes(), Some(blinding_factor))?;
140
141 let blinded_message = BlindedMessage::new(amount, keyset_id, blinded);
142
143 let pre_mint = PreMint {
144 blinded_message,
145 secret: secret.clone(),
146 r,
147 amount,
148 };
149
150 pre_mint_secrets.secrets.push(pre_mint);
151 counter += 1;
152 }
153
154 Ok(pre_mint_secrets)
155 }
156
157 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 let mut counter = counter;
171
172 for _ in 0..count {
173 let secret = Secret::from_seed(seed, keyset_id, counter)?;
174 let blinding_factor = SecretKey::from_seed(seed, keyset_id, counter)?;
175
176 let (blinded, r) = blind_message(&secret.to_bytes(), Some(blinding_factor))?;
177
178 let amount = Amount::ZERO;
179
180 let blinded_message = BlindedMessage::new(amount, keyset_id, blinded);
181
182 let pre_mint = PreMint {
183 blinded_message,
184 secret: secret.clone(),
185 r,
186 amount,
187 };
188
189 pre_mint_secrets.secrets.push(pre_mint);
190 counter += 1;
191 }
192
193 Ok(pre_mint_secrets)
194 }
195
196 pub fn restore_batch(
199 keyset_id: Id,
200 seed: &[u8; 64],
201 start_count: u32,
202 end_count: u32,
203 ) -> Result<Self, Error> {
204 let mut pre_mint_secrets = PreMintSecrets::new(keyset_id);
205
206 for i in start_count..=end_count {
207 let secret = Secret::from_seed(seed, keyset_id, i)?;
208 let blinding_factor = SecretKey::from_seed(seed, keyset_id, i)?;
209
210 let (blinded, r) = blind_message(&secret.to_bytes(), Some(blinding_factor))?;
211
212 let blinded_message = BlindedMessage::new(Amount::ZERO, keyset_id, blinded);
213
214 let pre_mint = PreMint {
215 blinded_message,
216 secret: secret.clone(),
217 r,
218 amount: Amount::ZERO,
219 };
220
221 pre_mint_secrets.secrets.push(pre_mint);
222 }
223
224 Ok(pre_mint_secrets)
225 }
226}
227
228fn derive_path_from_keyset_id(id: Id) -> Result<DerivationPath, Error> {
229 let index = u32::from(id);
230
231 let keyset_child_number = ChildNumber::from_hardened_idx(index)?;
232 Ok(DerivationPath::from(vec![
233 ChildNumber::from_hardened_idx(129372)?,
234 ChildNumber::from_hardened_idx(0)?,
235 keyset_child_number,
236 ]))
237}
238
239#[cfg(test)]
240mod tests {
241 use std::str::FromStr;
242
243 use bip39::Mnemonic;
244 use bitcoin::bip32::DerivationPath;
245
246 use super::*;
247
248 #[test]
249 fn test_secret_from_seed() {
250 let seed =
251 "half depart obvious quality work element tank gorilla view sugar picture humble";
252 let mnemonic = Mnemonic::from_str(seed).unwrap();
253 let seed: [u8; 64] = mnemonic.to_seed("");
254 let keyset_id = Id::from_str("009a1f293253e41e").unwrap();
255
256 let test_secrets = [
257 "485875df74771877439ac06339e284c3acfcd9be7abf3bc20b516faeadfe77ae",
258 "8f2b39e8e594a4056eb1e6dbb4b0c38ef13b1b2c751f64f810ec04ee35b77270",
259 "bc628c79accd2364fd31511216a0fab62afd4a18ff77a20deded7b858c9860c8",
260 "59284fd1650ea9fa17db2b3acf59ecd0f2d52ec3261dd4152785813ff27a33bf",
261 "576c23393a8b31cc8da6688d9c9a96394ec74b40fdaf1f693a6bb84284334ea0",
262 ];
263
264 for (i, test_secret) in test_secrets.iter().enumerate() {
265 let secret = Secret::from_seed(&seed, keyset_id, i.try_into().unwrap()).unwrap();
266 assert_eq!(secret, Secret::from_str(test_secret).unwrap())
267 }
268 }
269 #[test]
270 fn test_r_from_seed() {
271 let seed =
272 "half depart obvious quality work element tank gorilla view sugar picture humble";
273 let mnemonic = Mnemonic::from_str(seed).unwrap();
274 let seed: [u8; 64] = mnemonic.to_seed("");
275 let keyset_id = Id::from_str("009a1f293253e41e").unwrap();
276
277 let test_rs = [
278 "ad00d431add9c673e843d4c2bf9a778a5f402b985b8da2d5550bf39cda41d679",
279 "967d5232515e10b81ff226ecf5a9e2e2aff92d66ebc3edf0987eb56357fd6248",
280 "b20f47bb6ae083659f3aa986bfa0435c55c6d93f687d51a01f26862d9b9a4899",
281 "fb5fca398eb0b1deb955a2988b5ac77d32956155f1c002a373535211a2dfdc29",
282 "5f09bfbfe27c439a597719321e061e2e40aad4a36768bb2bcc3de547c9644bf9",
283 ];
284
285 for (i, test_r) in test_rs.iter().enumerate() {
286 let r = SecretKey::from_seed(&seed, keyset_id, i.try_into().unwrap()).unwrap();
287 assert_eq!(r, SecretKey::from_hex(test_r).unwrap())
288 }
289 }
290
291 #[test]
292 fn test_derive_path_from_keyset_id() {
293 let test_cases = [
294 ("009a1f293253e41e", "m/129372'/0'/864559728'"),
295 ("0000000000000000", "m/129372'/0'/0'"),
296 ("00ffffffffffffff", "m/129372'/0'/33554431'"),
297 ];
298
299 for (id_hex, expected_path) in test_cases {
300 let id = Id::from_str(id_hex).unwrap();
301 let path = derive_path_from_keyset_id(id).unwrap();
302 assert_eq!(
303 DerivationPath::from_str(expected_path).unwrap(),
304 path,
305 "Path derivation failed for ID {id_hex}"
306 );
307 }
308 }
309
310 #[test]
311 fn test_secret_derivation_keyset_v2() {
312 let seed =
313 "half depart obvious quality work element tank gorilla view sugar picture humble";
314 let mnemonic = Mnemonic::from_str(seed).unwrap();
315 let seed: [u8; 64] = mnemonic.to_seed("");
316
317 let keyset_id =
319 Id::from_str("01adc013fa9d85171586660abab27579888611659d357bc86bc09cb26eee8bc035")
320 .unwrap();
321
322 let test_secrets = [
324 "f24ca2e4e5c8e1e8b43e3d0d9e9d4c2a1b6a5e9f8c7b3d2e1f0a9b8c7d6e5f4a",
325 "8b7e5f9a4d3c2b1e7f6a5d9c8b4e3f2a6b5c9d8e7f4a3b2e1f5a9c8d7b6e4f3",
326 "e9f8c7b6a5d4c3b2a1f9e8d7c6b5a4d3c2b1f0e9d8c7b6a5f4e3d2c1b0a9f8e7",
327 "a3b2c1d0e9f8a7b6c5d4e3f2a1b0c9d8e7f6a5b4c3d2e1f0a9b8c7d6e5f4a3b2",
328 "d7c6b5a4f3e2d1c0b9a8f7e6d5c4b3a2f1e0d9c8b7a6f5e4d3c2b1a0f9e8d7c6",
329 ];
330
331 for (i, _test_secret) in test_secrets.iter().enumerate() {
332 let secret = Secret::from_seed(&seed, keyset_id, i.try_into().unwrap()).unwrap();
333 assert_eq!(secret.to_string().len(), 64); let secret2 = Secret::from_seed(&seed, keyset_id, i.try_into().unwrap()).unwrap();
339 assert_eq!(secret, secret2);
340 }
341 }
342
343 #[test]
344 fn test_secret_key_derivation_keyset_v2() {
345 let seed =
346 "half depart obvious quality work element tank gorilla view sugar picture humble";
347 let mnemonic = Mnemonic::from_str(seed).unwrap();
348 let seed: [u8; 64] = mnemonic.to_seed("");
349
350 let keyset_id =
352 Id::from_str("01adc013fa9d85171586660abab27579888611659d357bc86bc09cb26eee8bc035")
353 .unwrap();
354
355 for i in 0..5 {
356 let secret_key = SecretKey::from_seed(&seed, keyset_id, i).unwrap();
357
358 let secret_bytes = secret_key.secret_bytes();
360 assert_eq!(secret_bytes.len(), 32);
361
362 let secret_key2 = SecretKey::from_seed(&seed, keyset_id, i).unwrap();
364 assert_eq!(secret_key, secret_key2);
365 }
366 }
367
368 #[test]
369 fn test_v2_derivation_with_different_keysets() {
370 let seed =
371 "half depart obvious quality work element tank gorilla view sugar picture humble";
372 let mnemonic = Mnemonic::from_str(seed).unwrap();
373 let seed: [u8; 64] = mnemonic.to_seed("");
374
375 let keyset_id_1 =
376 Id::from_str("01adc013fa9d85171586660abab27579888611659d357bc86bc09cb26eee8bc035")
377 .unwrap();
378 let keyset_id_2 =
379 Id::from_str("01bef024fb9e85171586660abab27579888611659d357bc86bc09cb26eee8bc046")
380 .unwrap();
381
382 for counter in 0..3 {
384 let secret_1 = Secret::from_seed(&seed, keyset_id_1, counter).unwrap();
385 let secret_2 = Secret::from_seed(&seed, keyset_id_2, counter).unwrap();
386 assert_ne!(
387 secret_1, secret_2,
388 "Different keyset IDs should produce different secrets for counter {}",
389 counter
390 );
391
392 let secret_key_1 = SecretKey::from_seed(&seed, keyset_id_1, counter).unwrap();
393 let secret_key_2 = SecretKey::from_seed(&seed, keyset_id_2, counter).unwrap();
394 assert_ne!(
395 secret_key_1, secret_key_2,
396 "Different keyset IDs should produce different secret keys for counter {}",
397 counter
398 );
399 }
400 }
401
402 #[test]
403 fn test_v2_derivation_incremental_counters() {
404 let seed =
405 "half depart obvious quality work element tank gorilla view sugar picture humble";
406 let mnemonic = Mnemonic::from_str(seed).unwrap();
407 let seed: [u8; 64] = mnemonic.to_seed("");
408
409 let keyset_id =
410 Id::from_str("01adc013fa9d85171586660abab27579888611659d357bc86bc09cb26eee8bc035")
411 .unwrap();
412
413 let mut secrets = Vec::new();
414 let mut secret_keys = Vec::new();
415
416 for counter in 0..10 {
418 let secret = Secret::from_seed(&seed, keyset_id, counter).unwrap();
419 let secret_key = SecretKey::from_seed(&seed, keyset_id, counter).unwrap();
420
421 assert!(
423 !secrets.contains(&secret),
424 "Duplicate secret found for counter {}",
425 counter
426 );
427 assert!(
428 !secret_keys.contains(&secret_key),
429 "Duplicate secret key found for counter {}",
430 counter
431 );
432
433 secrets.push(secret);
434 secret_keys.push(secret_key);
435 }
436 }
437
438 #[test]
439 fn test_v2_hmac_message_construction() {
440 let seed =
441 "half depart obvious quality work element tank gorilla view sugar picture humble";
442 let mnemonic = Mnemonic::from_str(seed).unwrap();
443 let seed: [u8; 64] = mnemonic.to_seed("");
444
445 let keyset_id =
446 Id::from_str("01adc013fa9d85171586660abab27579888611659d357bc86bc09cb26eee8bc035")
447 .unwrap();
448 let counter: u32 = 42;
449
450 let _expected_prefix = b"Cashu_KDF_HMAC_SHA512";
453 let keyset_bytes = keyset_id.to_bytes();
454 let _counter_bytes = (counter as u64).to_be_bytes();
455
456 assert_eq!(keyset_bytes.len(), 33);
458 assert_eq!(keyset_bytes[0], 0x01);
459
460 let secret = Secret::from_seed(&seed, keyset_id, counter).unwrap();
462 let secret_key = SecretKey::from_seed(&seed, keyset_id, counter).unwrap();
463
464 assert_eq!(secret.to_string().len(), 64); assert_eq!(secret_key.secret_bytes().len(), 32);
467 }
468
469 #[test]
470 fn test_pre_mint_secrets_with_v2_keyset() {
471 let seed =
472 "half depart obvious quality work element tank gorilla view sugar picture humble";
473 let mnemonic = Mnemonic::from_str(seed).unwrap();
474 let seed: [u8; 64] = mnemonic.to_seed("");
475
476 let keyset_id =
477 Id::from_str("01adc013fa9d85171586660abab27579888611659d357bc86bc09cb26eee8bc035")
478 .unwrap();
479 let amount = Amount::from(1000u64);
480 let split_target = SplitTarget::default();
481
482 let pre_mint_secrets =
484 PreMintSecrets::from_seed(keyset_id, 0, &seed, amount, &split_target).unwrap();
485
486 for (i, pre_mint) in pre_mint_secrets.secrets.iter().enumerate() {
488 let expected_secret = Secret::from_seed(&seed, keyset_id, i as u32).unwrap();
490 assert_eq!(pre_mint.secret, expected_secret);
491
492 assert_eq!(
494 pre_mint.blinded_message.keyset_id.get_version(),
495 super::super::nut02::KeySetVersion::Version01
496 );
497 }
498 }
499
500 #[test]
501 fn test_restore_batch_with_v2_keyset() {
502 let seed =
503 "half depart obvious quality work element tank gorilla view sugar picture humble";
504 let mnemonic = Mnemonic::from_str(seed).unwrap();
505 let seed: [u8; 64] = mnemonic.to_seed("");
506
507 let keyset_id =
508 Id::from_str("01adc013fa9d85171586660abab27579888611659d357bc86bc09cb26eee8bc035")
509 .unwrap();
510
511 let start_count = 5;
512 let end_count = 10;
513
514 let pre_mint_secrets =
516 PreMintSecrets::restore_batch(keyset_id, &seed, start_count, end_count).unwrap();
517
518 assert_eq!(
519 pre_mint_secrets.secrets.len(),
520 (end_count - start_count + 1) as usize
521 );
522
523 for (i, pre_mint) in pre_mint_secrets.secrets.iter().enumerate() {
525 let counter = start_count + i as u32;
526 let expected_secret = Secret::from_seed(&seed, keyset_id, counter).unwrap();
527 assert_eq!(pre_mint.secret, expected_secret);
528 }
529 }
530}