boltz_client/util/
secrets.rs

1//! # Usage Instructions
2//!
3//! ## Creating SwapMasterKey
4//! Use `SwapMasterKey::new` with your wallet mnemonic and passphrase to create a SwapMasterKey.
5//! The method will internally derive the BIP85 swap mnemonic from your wallet mnemonic.
6//! The SwapMasterKey should then be stored (it can be serialized to JSON) to use for each swap with the `derive_swapkey` method.
7//!
8//! ## Example
9//! ```no_run
10//! use boltz_client::util::secrets::SwapMasterKey;
11//! use boltz_client::network::Network;
12//!
13//! // Create SwapMasterKey from wallet mnemonic (BIP85 derivation happens internally)
14//! let wallet_mnemonic = "bacon bacon bacon bacon bacon bacon bacon bacon bacon bacon bacon bacon bacon bacon bacon bacon bacon bacon bacon bacon bacon bacon bacon bacon";
15//! let swap_master_key = SwapMasterKey::new(wallet_mnemonic, None, Network::Mainnet)?;
16//!
17//! // Store swap_master_key (can be serialized to JSON)
18//! // Later, derive keys for each swap
19//! let swap_key = swap_master_key.derive_swapkey(0)?;
20//! # Ok::<(), boltz_client::error::Error>(())
21//! ```
22
23use std::str::FromStr;
24
25use bip39::Mnemonic;
26use bip85_extended;
27use bitcoin::bip32::{DerivationPath, Fingerprint, Xpriv, Xpub};
28use bitcoin::hashes::{hash160, ripemd160, sha256, Hash};
29use bitcoin::hex::{DisplayHex, FromHex};
30use bitcoin::key::rand::{rngs::OsRng, RngCore};
31use bitcoin::secp256k1::{Keypair, Secp256k1};
32use elements::secp256k1_zkp::{Keypair as ZKKeyPair, Secp256k1 as ZKSecp256k1};
33use lightning_invoice::Bolt11Invoice;
34use serde::{Deserialize, Serialize};
35
36use crate::error::Error;
37use crate::network::Network;
38
39const MNEMONIC_BIP85_INDEX: u32 = 26589;
40const SWAP_KEY_DERIVATION_PATH: &str = "m/44/0/0/0";
41
42/// Swap master key to derive swap keys for all swaps.
43///
44/// This struct holds the BIP85-derived swap mnemonic and the master private key derived from it.
45/// It can be stored (serialized to JSON) and reused to derive individual swap keys without
46/// needing to pass the mnemonic and passphrase repeatedly.
47///
48/// The mnemonic field contains the BIP85-derived swap mnemonic, which can be used for recovery
49/// on: https://boltz.exchange/rescue/external?mode=rescue-key
50///
51/// Can also be used to get the root xpubs that can be used with the swap/restore API.
52#[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Debug)]
53pub struct SwapMasterKey {
54    /// The BIP85-derived swap mnemonic
55    pub mnemonic: Mnemonic,
56    /// The child xprv derived from the swap mnemonic at the standard derivation path
57    pub xprv: Xpriv,
58    /// The fingerprint of the root key
59    pub fingerprint: Fingerprint,
60    /// The network this key is for
61    pub network: Network,
62}
63
64impl SwapMasterKey {
65    /// Creates SwapMasterKey from a wallet mnemonic.
66    ///
67    /// This method internally derives the BIP85 swap mnemonic from the wallet mnemonic
68    /// using BIP85 index `26589`, then creates the SwapMasterKey
69    /// from that derived mnemonic.
70    ///
71    /// The swap mnemonic stored in the returned struct can be used for recovery on:
72    /// https://boltz.exchange/rescue/external?mode=rescue-key
73    ///
74    /// # Arguments
75    /// * `wallet_mnemonic` - The wallet mnemonic to derive the swap mnemonic from
76    /// * `wallet_passphrase` - Optional passphrase for the wallet mnemonic
77    /// * `network` - The network (Mainnet, Testnet, or Regtest)
78    pub fn new(
79        wallet_mnemonic: &str,
80        wallet_passphrase: Option<&str>,
81        network: Network,
82    ) -> Result<SwapMasterKey, Error> {
83        let secp = Secp256k1::new();
84        let root = Self::derive_root_xpriv(wallet_mnemonic, wallet_passphrase, network)?;
85        let swap_mnemonic =
86            bip85_extended::mnemonic::to_mnemonic(&secp, &root, 12, MNEMONIC_BIP85_INDEX)?;
87        Self::from_mnemonic(&swap_mnemonic.to_string(), None, network)
88    }
89
90    /// Creates SwapMasterKey directly from a mnemonic.
91    ///
92    /// Use this method if you already have the swap mnemonic (e.g., from a previous
93    /// SwapMasterKey that was serialized, or if you're restoring from the rescue key).
94    ///
95    /// The mnemonic will be used to derive the master key at the standard derivation path
96    /// `m/26589'/0'/0'`.
97    ///
98    /// # Arguments
99    /// * `mnemonic` - The swap mnemonic (12-word BIP39 mnemonic)
100    /// * `passphrase` - Optional passphrase for the mnemonic
101    /// * `network` - The network (Mainnet, Testnet, or Regtest)
102    pub fn from_mnemonic(
103        mnemonic: &str,
104        passphrase: Option<&str>,
105        network: Network,
106    ) -> Result<SwapMasterKey, Error> {
107        let secp = Secp256k1::new();
108        let root = Self::derive_root_xpriv(mnemonic, passphrase, network)?;
109        let fingerprint = root.fingerprint(&secp);
110
111        let master_path = DerivationPath::from_str(SWAP_KEY_DERIVATION_PATH)?;
112        let xprv = root.derive_priv(&secp, &master_path)?;
113
114        let mnemonic_struct = Mnemonic::from_str(mnemonic)?;
115
116        Ok(SwapMasterKey {
117            mnemonic: mnemonic_struct,
118            xprv,
119            fingerprint,
120            network,
121        })
122    }
123    /// Derives a KeyPair for a specific swap index.
124    ///
125    /// Use this method for each swap. The client must handle incrementing the index
126    /// themselves to ensure each swap uses a unique key.
127    ///
128    /// # Arguments
129    /// * `index` - The swap index (0, 1, 2, etc.)
130    ///
131    /// # Returns
132    /// A `KeyPair` derived at path `m/26589'/0'/0'/{index}`
133    pub fn derive_swapkey(&self, index: u64) -> Result<Keypair, Error> {
134        let secp = Secp256k1::new();
135        let child_path = DerivationPath::from_str(&format!("m/{index}"))?;
136        let child_xprv = self.xprv.derive_priv(&secp, &child_path)?;
137        let key_pair = Keypair::from_secret_key(&secp, &child_xprv.private_key);
138        Ok(key_pair)
139    }
140
141    /// Derives a ZKKeyPair for Liquid swaps at a specific swap index.
142    ///
143    /// Use this method for each Liquid swap. The client must handle incrementing the index
144    /// themselves to ensure each swap uses a unique key.
145    ///
146    /// # Arguments
147    /// * `index` - The swap index (0, 1, 2, etc.)
148    ///
149    /// # Returns
150    /// A `ZKKeyPair` derived at path `m/26589'/0'/0'/{index}`
151    pub fn derive_liquid_swapkey(&self, index: u64) -> Result<ZKKeyPair, Error> {
152        let keypair = self.derive_swapkey(index)?;
153        let secp = ZKSecp256k1::new();
154        let zk_keypair = ZKKeyPair::from_seckey_str(&secp, &keypair.display_secret().to_string())?;
155        Ok(zk_keypair)
156    }
157
158    /// Gets the master extended public key (xpub) derived from the master private key.
159    ///
160    /// This xpub can be used with the swap/restore API to enable key recovery.
161    ///
162    /// # Returns
163    /// The master extended public key
164    pub fn get_master_xpub(&self) -> Xpub {
165        let secp = Secp256k1::new();
166        Xpub::from_priv(&secp, &self.xprv)
167    }
168
169    fn derive_root_xpriv(
170        mnemonic: &str,
171        passphrase: Option<&str>,
172        network: Network,
173    ) -> Result<Xpriv, Error> {
174        let mnemonic_struct = Mnemonic::from_str(mnemonic)?;
175        let seed = mnemonic_struct.to_seed(passphrase.unwrap_or(""));
176        let root = Xpriv::new_master(bitcoin::Network::from(network), &seed)?;
177        Ok(root)
178    }
179}
180
181/// For Liquid keys, first create a KeyPair from SwapMasterKey and then convert it to ZKKeyPair
182/// let swap_master_key = SwapMasterKey::new(wallet_mnemonic, None, Network::Mainnet)?;
183/// let keypair = swap_master_key.derive_swapkey(1)?;
184/// let zk_keypair = ZKKeyPair::from_seckey_str(&ZKSecp256k1::new(), &keypair.display_secret().to_string())?;
185/// Internally used rng to generate secure 32 byte preimages
186pub(crate) fn rng_32b() -> [u8; 32] {
187    let mut bytes = [0u8; 32];
188    OsRng.fill_bytes(&mut bytes);
189    bytes
190}
191
192/// Helper to work with Preimage & Hashes required for swap scripts.
193#[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Debug)]
194pub struct Preimage {
195    pub bytes: Option<[u8; 32]>,
196    pub sha256: sha256::Hash,
197    pub hash160: hash160::Hash,
198}
199
200impl FromStr for Preimage {
201    type Err = Error;
202
203    /// Creates a struct from a preimage string.
204    fn from_str(preimage: &str) -> Result<Self, Self::Err> {
205        Self::from_vec(Vec::from_hex(preimage)?)
206    }
207}
208
209impl Default for Preimage {
210    fn default() -> Self {
211        Preimage::random()
212    }
213}
214
215impl Preimage {
216    /// Creates a new random preimage
217    /// RECOMMENDED NOT TO USE THIS FUNCTION
218    /// USE FROM_SWAP_KEY INSTEAD
219    pub fn random() -> Preimage {
220        let preimage = rng_32b();
221        let sha256 = sha256::Hash::hash(&preimage);
222        let hash160 = hash160::Hash::hash(&preimage);
223
224        Preimage {
225            sha256,
226            hash160,
227            bytes: Some(preimage),
228        }
229    }
230
231    /// Creates a struct from a preimage vector.
232    pub fn from_vec(preimage: Vec<u8>) -> Result<Preimage, Error> {
233        // Ensure the decoded bytes are exactly 32 bytes long
234        let preimage: [u8; 32] = preimage
235            .try_into()
236            .map_err(|_| Error::Protocol("Decoded Preimage input is not 32 bytes".to_string()))?;
237        let sha256 = sha256::Hash::hash(&preimage);
238        let hash160 = hash160::Hash::hash(&preimage);
239        Ok(Preimage {
240            sha256,
241            hash160,
242            bytes: Some(preimage),
243        })
244    }
245
246    /// Creates a Preimage struct without a value and only a hash
247    /// Used only in submarine swaps where we do not know the preimage, only the hash
248    pub fn from_sha256_str(preimage_sha256: &str) -> Result<Preimage, Error> {
249        Self::from_sha256_vec(Vec::from_hex(preimage_sha256)?)
250    }
251
252    /// Creates a Preimage struct without a value and only a hash
253    /// Used only in submarine swaps where we do not know the preimage, only the hash
254    pub fn from_sha256_vec(preimage_sha256: Vec<u8>) -> Result<Preimage, Error> {
255        let sha256 = sha256::Hash::from_slice(preimage_sha256.as_slice())?;
256        let hash160 = hash160::Hash::from_slice(
257            ripemd160::Hash::hash(sha256.as_byte_array()).as_byte_array(),
258        )?;
259        // will never fail as long as sha256 is a valid sha256::Hash
260        Ok(Preimage {
261            sha256,
262            hash160,
263            bytes: None,
264        })
265    }
266
267    /// Extracts the preimage sha256 hash from a lightning invoice
268    /// Creates a Preimage struct without a value and only a hash
269    pub fn from_invoice_str(invoice_str: &str) -> Result<Preimage, Error> {
270        let invoice = Bolt11Invoice::from_str(invoice_str)?;
271        Preimage::from_sha256_str(&invoice.payment_hash().to_string())
272    }
273
274    /// Converts the preimage value bytes to String
275    pub fn to_string(&self) -> Option<String> {
276        self.bytes.map(|res| res.to_lower_hex_string())
277    }
278
279    /// Creates a Preimage from a KeyPair's private key hash
280    /// sha256(privateKey(index))
281    /// RECOMMENDED TO ENSURE SWAPS CAN BE RESTORED MORE EASILY
282    pub fn from_swap_key(keypair: &Keypair) -> Preimage {
283        let private_key_bytes = keypair.secret_key().secret_bytes();
284        let preimage = sha256::Hash::hash(&private_key_bytes);
285        let preimage_bytes: [u8; 32] = *preimage.as_byte_array();
286        let sha256 = sha256::Hash::hash(&preimage_bytes);
287        let hash160 = hash160::Hash::hash(&preimage_bytes);
288
289        Preimage {
290            bytes: Some(preimage_bytes),
291            sha256,
292            hash160,
293        }
294    }
295}
296
297#[cfg(test)]
298mod tests {
299    use super::*;
300    use elements::pset::serialize::Serialize;
301
302    #[macros::test_all]
303    fn test_derivation() {
304        let mnemonic: &str = "bacon bacon bacon bacon bacon bacon bacon bacon bacon bacon bacon bacon bacon bacon bacon bacon bacon bacon bacon bacon bacon bacon bacon bacon";
305        let index = 0_u64; // 0
306        let swap_master_key =
307            SwapMasterKey::from_mnemonic(mnemonic, None, Network::Mainnet).unwrap();
308        let keypair = swap_master_key.derive_swapkey(index).unwrap();
309        let secp = ZKSecp256k1::new();
310        let zk_keypair =
311            ZKKeyPair::from_seckey_str(&secp, &keypair.display_secret().to_string()).unwrap();
312        assert_eq!(keypair.public_key(), zk_keypair.public_key());
313    }
314
315    #[macros::test_all]
316    fn test_preimage_from_str() {
317        let preimage = Preimage::random();
318        assert_eq!(
319            Preimage::from_str(&hex::encode(preimage.bytes.unwrap()).to_string()).unwrap(),
320            preimage
321        );
322    }
323
324    #[macros::test_all]
325    fn test_preimage_from_vec() {
326        let preimage = Preimage::random();
327        assert_eq!(
328            Preimage::from_vec(Vec::from(preimage.bytes.unwrap())).unwrap(),
329            preimage
330        );
331    }
332
333    #[macros::test_all]
334    fn test_preimage_from_vec_invalid_length() {
335        let mut bytes = [0u8; 33];
336        OsRng.fill_bytes(&mut bytes);
337        assert_eq!(
338            Preimage::from_vec(Vec::from(bytes))
339                .err()
340                .unwrap()
341                .message(),
342            "Decoded Preimage input is not 32 bytes".to_string()
343        );
344    }
345
346    #[macros::test_all]
347    fn test_preimage_from_sha256_str() {
348        let preimage = Preimage::random();
349        let compare = Preimage::from_sha256_str(preimage.sha256.to_string().as_str()).unwrap();
350
351        assert_eq!(compare.bytes, None);
352        assert_eq!(compare.sha256, preimage.sha256);
353        assert_eq!(compare.hash160, preimage.hash160);
354    }
355
356    #[macros::test_all]
357    fn test_preimage_from_sha256_vec() {
358        let preimage = Preimage::random();
359        let compare = Preimage::from_sha256_vec(preimage.sha256.serialize()).unwrap();
360
361        assert_eq!(compare.bytes, None);
362        assert_eq!(compare.sha256, preimage.sha256);
363        assert_eq!(compare.hash160, preimage.hash160);
364    }
365
366    #[macros::test_all]
367    fn test_swap_master_key() -> Result<(), Error> {
368        let wallet_mnemonic = "bacon bacon bacon bacon bacon bacon bacon bacon bacon bacon bacon bacon bacon bacon bacon bacon bacon bacon bacon bacon bacon bacon bacon bacon";
369        let wallet_passphrase = None;
370        let network = Network::Mainnet;
371
372        let swap_master_key = SwapMasterKey::new(wallet_mnemonic, wallet_passphrase, network)?;
373
374        assert_eq!(
375            swap_master_key.mnemonic.to_string(),
376            "velvet engage shaft effort clarify annual protect client only surround sock gain"
377                .to_string(),
378            "BIP85 extended method should produce the same mnemonic as raw derivation"
379        );
380
381        assert_eq!(
382            swap_master_key.xprv.to_string(),
383            "xprvA2Cw2wgWLdz9ppAYGVYpfVT6zjdVpQg9MPh9vuFQj3CMDguEv1reAdHiaSPyjqmk7sb7BA9X8T29snJGMtLBWKmATtdPaQijCbc5bbViDsH",
384            "xpriv should match expected value"
385        );
386
387        let master_xpub = swap_master_key.get_master_xpub();
388        assert_eq!(
389            master_xpub.to_string(),
390            "xpub6FCHSTDQB1YT3JF1NX5q2dPqYmTzDsPzicckjHf2HNjL6VEPTZAtiRcCRixiSpfKinRfWGFQ5b1yw74jzsBd1hkm25864ZpH8uND7rjKjiV",
391            "xpub should match expected value"
392        );
393
394        let swap_key_at_index_0 = swap_master_key.derive_swapkey(0)?;
395
396        let xprv_str = "xprvA44z2P7u7pXcG23soQ32XV9jUgbnQukETR1sNT2HKu681zaNU7iPEo3qQ6tMrAzNK8kQmFqEqFURCxXEZpEuVLK4KdXH8atNQpxrqYGjq17";
397        let swap_xprv = Xpriv::from_str(xprv_str)?;
398        let secp = Secp256k1::new();
399
400        let keypair_from_xprv = Keypair::from_secret_key(&secp, &swap_xprv.private_key);
401
402        assert_eq!(
403            swap_key_at_index_0.public_key(),
404            keypair_from_xprv.public_key(),
405            "Swap key at index 0 should match KeyPair derived from xprv"
406        );
407        assert_eq!(
408            swap_key_at_index_0.secret_key(),
409            keypair_from_xprv.secret_key(),
410            "Swap key secret at index 0 should match KeyPair secret derived from xprv"
411        );
412
413        let preimage = Preimage::from_swap_key(&swap_key_at_index_0);
414        assert_eq!(
415            preimage.bytes.unwrap().to_lower_hex_string(),
416            "f19d42c70bf00267b6c5dcfe6e1094386f8c72389f1ced91e0132d1502bbd244".to_string(),
417        );
418
419        Ok(())
420    }
421}