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 thiserror::Error;
7use tracing::instrument;
8
9use super::nut00::{BlindedMessage, PreMint, PreMintSecrets};
10use super::nut01::SecretKey;
11use super::nut02::Id;
12use crate::amount::SplitTarget;
13use crate::dhke::blind_message;
14use crate::secret::Secret;
15use crate::util::hex;
16use crate::{Amount, SECP256K1};
17
18/// NUT13 Error
19#[derive(Debug, Error)]
20pub enum Error {
21    /// DHKE error
22    #[error(transparent)]
23    DHKE(#[from] crate::dhke::Error),
24    /// Amount Error
25    #[error(transparent)]
26    Amount(#[from] crate::amount::Error),
27    /// NUT00 Error
28    #[error(transparent)]
29    NUT00(#[from] crate::nuts::nut00::Error),
30    /// NUT02 Error
31    #[error(transparent)]
32    NUT02(#[from] crate::nuts::nut02::Error),
33    /// Bip32 Error
34    #[error(transparent)]
35    Bip32(#[from] bitcoin::bip32::Error),
36}
37
38impl Secret {
39    /// Create new [`Secret`] from xpriv
40    pub fn from_xpriv(xpriv: Xpriv, keyset_id: Id, counter: u32) -> Result<Self, Error> {
41        let path = derive_path_from_keyset_id(keyset_id)?
42            .child(ChildNumber::from_hardened_idx(counter)?)
43            .child(ChildNumber::from_normal_idx(0)?);
44        let derived_xpriv = xpriv.derive_priv(&SECP256K1, &path)?;
45
46        Ok(Self::new(hex::encode(
47            derived_xpriv.private_key.secret_bytes(),
48        )))
49    }
50}
51
52impl SecretKey {
53    /// Create new [`SecretKey`] from xpriv
54    pub fn from_xpriv(xpriv: Xpriv, keyset_id: Id, counter: u32) -> Result<Self, Error> {
55        let path = derive_path_from_keyset_id(keyset_id)?
56            .child(ChildNumber::from_hardened_idx(counter)?)
57            .child(ChildNumber::from_normal_idx(1)?);
58        let derived_xpriv = xpriv.derive_priv(&SECP256K1, &path)?;
59
60        Ok(Self::from(derived_xpriv.private_key))
61    }
62}
63
64impl PreMintSecrets {
65    /// Generate blinded messages from predetermined secrets and blindings
66    /// factor
67    #[instrument(skip(xpriv))]
68    pub fn from_xpriv(
69        keyset_id: Id,
70        counter: u32,
71        xpriv: Xpriv,
72        amount: Amount,
73        amount_split_target: &SplitTarget,
74    ) -> Result<Self, Error> {
75        let mut pre_mint_secrets = PreMintSecrets::new(keyset_id);
76
77        let mut counter = counter;
78
79        for amount in amount.split_targeted(amount_split_target)? {
80            let secret = Secret::from_xpriv(xpriv, keyset_id, counter)?;
81            let blinding_factor = SecretKey::from_xpriv(xpriv, keyset_id, counter)?;
82
83            let (blinded, r) = blind_message(&secret.to_bytes(), Some(blinding_factor))?;
84
85            let blinded_message = BlindedMessage::new(amount, keyset_id, blinded);
86
87            let pre_mint = PreMint {
88                blinded_message,
89                secret: secret.clone(),
90                r,
91                amount,
92            };
93
94            pre_mint_secrets.secrets.push(pre_mint);
95            counter += 1;
96        }
97
98        Ok(pre_mint_secrets)
99    }
100
101    /// New [`PreMintSecrets`] from xpriv with a zero amount used for change
102    pub fn from_xpriv_blank(
103        keyset_id: Id,
104        counter: u32,
105        xpriv: Xpriv,
106        amount: Amount,
107    ) -> Result<Self, Error> {
108        if amount <= Amount::ZERO {
109            return Ok(PreMintSecrets::new(keyset_id));
110        }
111        let count = ((u64::from(amount) as f64).log2().ceil() as u64).max(1);
112        let mut pre_mint_secrets = PreMintSecrets::new(keyset_id);
113
114        let mut counter = counter;
115
116        for _ in 0..count {
117            let secret = Secret::from_xpriv(xpriv, keyset_id, counter)?;
118            let blinding_factor = SecretKey::from_xpriv(xpriv, keyset_id, counter)?;
119
120            let (blinded, r) = blind_message(&secret.to_bytes(), Some(blinding_factor))?;
121
122            let amount = Amount::ZERO;
123
124            let blinded_message = BlindedMessage::new(amount, keyset_id, blinded);
125
126            let pre_mint = PreMint {
127                blinded_message,
128                secret: secret.clone(),
129                r,
130                amount,
131            };
132
133            pre_mint_secrets.secrets.push(pre_mint);
134            counter += 1;
135        }
136
137        Ok(pre_mint_secrets)
138    }
139
140    /// Generate blinded messages from predetermined secrets and blindings
141    /// factor
142    pub fn restore_batch(
143        keyset_id: Id,
144        xpriv: Xpriv,
145        start_count: u32,
146        end_count: u32,
147    ) -> Result<Self, Error> {
148        let mut pre_mint_secrets = PreMintSecrets::new(keyset_id);
149
150        for i in start_count..=end_count {
151            let secret = Secret::from_xpriv(xpriv, keyset_id, i)?;
152            let blinding_factor = SecretKey::from_xpriv(xpriv, keyset_id, i)?;
153
154            let (blinded, r) = blind_message(&secret.to_bytes(), Some(blinding_factor))?;
155
156            let blinded_message = BlindedMessage::new(Amount::ZERO, keyset_id, blinded);
157
158            let pre_mint = PreMint {
159                blinded_message,
160                secret: secret.clone(),
161                r,
162                amount: Amount::ZERO,
163            };
164
165            pre_mint_secrets.secrets.push(pre_mint);
166        }
167
168        Ok(pre_mint_secrets)
169    }
170}
171
172fn derive_path_from_keyset_id(id: Id) -> Result<DerivationPath, Error> {
173    let index = u32::from(id);
174
175    let keyset_child_number = ChildNumber::from_hardened_idx(index)?;
176    Ok(DerivationPath::from(vec![
177        ChildNumber::from_hardened_idx(129372)?,
178        ChildNumber::from_hardened_idx(0)?,
179        keyset_child_number,
180    ]))
181}
182
183#[cfg(test)]
184mod tests {
185    use std::str::FromStr;
186
187    use bip39::Mnemonic;
188    use bitcoin::bip32::DerivationPath;
189    use bitcoin::Network;
190
191    use super::*;
192
193    #[test]
194    fn test_secret_from_seed() {
195        let seed =
196            "half depart obvious quality work element tank gorilla view sugar picture humble";
197        let mnemonic = Mnemonic::from_str(seed).unwrap();
198        let seed: [u8; 64] = mnemonic.to_seed("");
199        let xpriv = Xpriv::new_master(Network::Bitcoin, &seed).unwrap();
200        let keyset_id = Id::from_str("009a1f293253e41e").unwrap();
201
202        let test_secrets = [
203            "485875df74771877439ac06339e284c3acfcd9be7abf3bc20b516faeadfe77ae",
204            "8f2b39e8e594a4056eb1e6dbb4b0c38ef13b1b2c751f64f810ec04ee35b77270",
205            "bc628c79accd2364fd31511216a0fab62afd4a18ff77a20deded7b858c9860c8",
206            "59284fd1650ea9fa17db2b3acf59ecd0f2d52ec3261dd4152785813ff27a33bf",
207            "576c23393a8b31cc8da6688d9c9a96394ec74b40fdaf1f693a6bb84284334ea0",
208        ];
209
210        for (i, test_secret) in test_secrets.iter().enumerate() {
211            let secret = Secret::from_xpriv(xpriv, keyset_id, i.try_into().unwrap()).unwrap();
212            assert_eq!(secret, Secret::from_str(test_secret).unwrap())
213        }
214    }
215    #[test]
216    fn test_r_from_seed() {
217        let seed =
218            "half depart obvious quality work element tank gorilla view sugar picture humble";
219        let mnemonic = Mnemonic::from_str(seed).unwrap();
220        let seed: [u8; 64] = mnemonic.to_seed("");
221        let xpriv = Xpriv::new_master(Network::Bitcoin, &seed).unwrap();
222        let keyset_id = Id::from_str("009a1f293253e41e").unwrap();
223
224        let test_rs = [
225            "ad00d431add9c673e843d4c2bf9a778a5f402b985b8da2d5550bf39cda41d679",
226            "967d5232515e10b81ff226ecf5a9e2e2aff92d66ebc3edf0987eb56357fd6248",
227            "b20f47bb6ae083659f3aa986bfa0435c55c6d93f687d51a01f26862d9b9a4899",
228            "fb5fca398eb0b1deb955a2988b5ac77d32956155f1c002a373535211a2dfdc29",
229            "5f09bfbfe27c439a597719321e061e2e40aad4a36768bb2bcc3de547c9644bf9",
230        ];
231
232        for (i, test_r) in test_rs.iter().enumerate() {
233            let r = SecretKey::from_xpriv(xpriv, keyset_id, i.try_into().unwrap()).unwrap();
234            assert_eq!(r, SecretKey::from_hex(test_r).unwrap())
235        }
236    }
237
238    #[test]
239    fn test_derive_path_from_keyset_id() {
240        let test_cases = [
241            ("009a1f293253e41e", "m/129372'/0'/864559728'"),
242            ("0000000000000000", "m/129372'/0'/0'"),
243            ("00ffffffffffffff", "m/129372'/0'/33554431'"),
244        ];
245
246        for (id_hex, expected_path) in test_cases {
247            let id = Id::from_str(id_hex).unwrap();
248            let path = derive_path_from_keyset_id(id).unwrap();
249            assert_eq!(
250                DerivationPath::from_str(expected_path).unwrap(),
251                path,
252                "Path derivation failed for ID {id_hex}"
253            );
254        }
255    }
256}