boltz_client/util/
secrets.rs

1use crate::network::Chain;
2use crate::swaps::bitcoin::BtcSwapScript;
3use crate::swaps::liquid::LBtcSwapScript;
4use crate::util::error::{ErrorKind, S5Error};
5use bip39::Mnemonic;
6use bitcoin::bip32::{DerivationPath, Fingerprint, Xpriv};
7use bitcoin::secp256k1::{Keypair, Secp256k1};
8use elements::secp256k1_zkp::{Keypair as ZKKeyPair, Secp256k1 as ZKSecp256k1};
9
10use bitcoin::secp256k1::hashes::{hash160, ripemd160, sha256, Hash};
11use lightning_invoice::Bolt11Invoice;
12
13use bitcoin::secp256k1::rand::rngs::OsRng;
14use rand_core::RngCore;
15
16use serde::{Deserialize, Serialize};
17use std::fmt::Display;
18use std::fmt::Formatter;
19use std::str::FromStr;
20
21const SUBMARINE_SWAP_ACCOUNT: u32 = 21;
22const REVERSE_SWAP_ACCOUNT: u32 = 42;
23
24const BITCOIN_NETWORK_PATH: u32 = 0;
25const LIQUID_NETWORK_PATH: u32 = 1776;
26const TESTNET_NETWORK_PATH: u32 = 1;
27
28/// Derived Keypair for use in a script.
29/// Can be used directly with Bitcoin structures
30/// Can be converted .into() LiquidSwapKey
31#[derive(Serialize, Deserialize, Debug, Clone)]
32pub struct SwapKey {
33    pub fingerprint: Fingerprint,
34    pub path: DerivationPath,
35    pub keypair: Keypair,
36}
37impl SwapKey {
38    /// Derives keys for a submarine swap at standardized path
39    /// m/49'/<0;1777;1>/21'/0/*
40    pub fn from_submarine_account(
41        mnemonic: &str,
42        passphrase: &str,
43        network: Chain,
44        index: u64,
45    ) -> Result<SwapKey, S5Error> {
46        let secp = Secp256k1::new();
47        let mnemonic_struct = Mnemonic::from_str(&mnemonic).unwrap();
48        let seed = mnemonic_struct.to_seed(passphrase);
49        let root = match Xpriv::new_master(bitcoin::Network::Testnet, &seed) {
50            Ok(xprv) => xprv,
51            Err(_) => return Err(S5Error::new(ErrorKind::Key, "Invalid Master Key.")),
52        };
53        let fingerprint = root.fingerprint(&secp);
54        let purpose = DerivationPurpose::Compatible;
55        let network_path = match network {
56            Chain::Bitcoin => BITCOIN_NETWORK_PATH,
57            Chain::Liquid => LIQUID_NETWORK_PATH,
58            _ => TESTNET_NETWORK_PATH,
59        };
60        let derivation_path = format!(
61            "m/{}h/{}h/{}h/0/{}",
62            purpose.to_string(),
63            network_path,
64            SUBMARINE_SWAP_ACCOUNT,
65            index
66        );
67        let path = match DerivationPath::from_str(&derivation_path) {
68            Ok(hdpath) => hdpath,
69            Err(_) => {
70                return Err(S5Error::new(
71                    ErrorKind::Key,
72                    "Invalid purpose or account in derivation path.",
73                ))
74            }
75        };
76        let child_xprv = match root.derive_priv(&secp, &path) {
77            Ok(xprv) => xprv,
78            Err(e) => return Err(S5Error::new(ErrorKind::Key, &e.to_string())),
79        };
80
81        let key_pair = match Keypair::from_seckey_str(
82            &secp,
83            &hex::encode(child_xprv.private_key.secret_bytes()),
84        ) {
85            Ok(kp) => kp,
86            Err(_) => return Err(S5Error::new(ErrorKind::Key, "BAD SECKEY STRING")),
87        };
88
89        Ok(SwapKey {
90            fingerprint: fingerprint,
91            path: path,
92            keypair: key_pair,
93        })
94    }
95    /// Derives keys for a submarine swap at standardized path
96    /// m/49'/<0;1777;1>/42'/0/*
97    pub fn from_reverse_account(
98        mnemonic: &str,
99        passphrase: &str,
100        network: Chain,
101        index: u64,
102    ) -> Result<SwapKey, S5Error> {
103        let secp = Secp256k1::new();
104        let mnemonic_struct = Mnemonic::from_str(mnemonic).unwrap();
105        let seed = mnemonic_struct.to_seed(passphrase);
106        let root = match Xpriv::new_master(bitcoin::Network::Testnet, &seed) {
107            Ok(xprv) => xprv,
108            Err(_) => return Err(S5Error::new(ErrorKind::Key, "Invalid Master Key.")),
109        };
110        let fingerprint = root.fingerprint(&secp);
111        let purpose = DerivationPurpose::Native;
112        let network_path = match network {
113            Chain::Bitcoin => BITCOIN_NETWORK_PATH,
114            Chain::Liquid => LIQUID_NETWORK_PATH,
115            _ => TESTNET_NETWORK_PATH,
116        };
117        // m/84h/1h/42h/<0;1>/*  - child key for segwit wallet - xprv
118        let derivation_path = format!(
119            "m/{}h/{}h/{}h/0/{}",
120            purpose, network_path, REVERSE_SWAP_ACCOUNT, index
121        );
122        let path = match DerivationPath::from_str(&derivation_path) {
123            Ok(hdpath) => hdpath,
124            Err(_) => {
125                return Err(S5Error::new(
126                    ErrorKind::Key,
127                    "Invalid purpose or account in derivation path.",
128                ))
129            }
130        };
131        let child_xprv = match root.derive_priv(&secp, &path) {
132            Ok(xprv) => xprv,
133            Err(e) => return Err(S5Error::new(ErrorKind::Key, &e.to_string())),
134        };
135
136        let key_pair = match Keypair::from_seckey_str(
137            &secp,
138            &hex::encode(child_xprv.private_key.secret_bytes()),
139        ) {
140            Ok(kp) => kp,
141            Err(_) => return Err(S5Error::new(ErrorKind::Key, "BAD SECKEY STRING")),
142        };
143
144        Ok(SwapKey {
145            fingerprint: fingerprint,
146            path: path,
147            keypair: key_pair,
148        })
149    }
150}
151#[derive(Clone)]
152
153/// For Liquid keys, first create a SwapKey and then call .into() to get the equaivalent ZKKeypair
154/// let sk = SwapKey::from_reverse_account(&mnemonic.to_string(), "", Chain::LiquidTestnet, 1).unwrap()
155/// let lsk: LiquidSwapKey = swap_key.into();
156/// let zkkp = lsk.keypair;
157#[derive(Serialize, Deserialize, Debug)]
158pub struct LiquidSwapKey {
159    pub fingerprint: Fingerprint,
160    pub path: DerivationPath,
161    pub keypair: ZKKeyPair,
162}
163impl From<SwapKey> for LiquidSwapKey {
164    fn from(swapkey: SwapKey) -> Self {
165        let secp = ZKSecp256k1::new();
166        let liquid_keypair =
167            ZKKeyPair::from_seckey_str(&secp, &swapkey.keypair.display_secret().to_string())
168                .unwrap();
169
170        LiquidSwapKey {
171            fingerprint: swapkey.fingerprint,
172            path: swapkey.path,
173            keypair: liquid_keypair,
174        }
175    }
176}
177enum DerivationPurpose {
178    _Legacy,
179    Compatible,
180    Native,
181    _Taproot,
182}
183impl Display for DerivationPurpose {
184    fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
185        match self {
186            DerivationPurpose::_Legacy => write!(f, "44"),
187            DerivationPurpose::Compatible => write!(f, "49"),
188            DerivationPurpose::Native => write!(f, "84"),
189            DerivationPurpose::_Taproot => write!(f, "86"),
190        }
191    }
192}
193
194/// Internally used rng to generate secure 32 byte preimages
195fn rng_32b() -> [u8; 32] {
196    let mut bytes = [0u8; 32];
197    OsRng.fill_bytes(&mut bytes);
198    bytes
199}
200
201/// Helper to work with Preimage & Hashes required for swap scripts.
202#[derive(Debug, Clone)]
203pub struct Preimage {
204    pub bytes: Option<[u8; 32]>,
205    pub sha256: sha256::Hash,
206    pub hash160: hash160::Hash,
207}
208
209impl Preimage {
210    /// Creates a new random preimage
211    pub fn new() -> Preimage {
212        let preimage = rng_32b();
213        let sha256 = sha256::Hash::hash(&preimage);
214        let hash160 = hash160::Hash::hash(&preimage);
215
216        Preimage {
217            bytes: Some(preimage),
218            sha256: sha256,
219            hash160: hash160,
220        }
221    }
222
223    /// Creates a struct from a preimage string.
224    pub fn from_str(preimage: &str) -> Result<Preimage, S5Error> {
225        let decoded = match hex::decode(preimage) {
226            Ok(decoded) => decoded,
227            Err(e) => return Err(S5Error::new(ErrorKind::Input, &e.to_string())),
228        };
229        // Ensure the decoded bytes are exactly 32 bytes long
230        let preimage_bytes: [u8; 32] = match decoded.try_into() {
231            Ok(bytes) => bytes,
232            Err(_) => {
233                return Err(S5Error::new(
234                    ErrorKind::Input,
235                    "Decoded Preimage input is not 32 bytes",
236                ))
237            }
238        };
239        let sha256 = sha256::Hash::hash(&preimage_bytes);
240        let hash160 = hash160::Hash::hash(&preimage_bytes);
241        Ok(Preimage {
242            bytes: Some(preimage_bytes),
243            sha256: sha256,
244            hash160: hash160,
245        })
246    }
247
248    /// Creates a Preimage struct without a value and only a hash
249    /// Used only in submarine swaps where we do not know the preimage, only the hash
250    pub fn from_sha256_str(preimage_sha256: &str) -> Result<Preimage, S5Error> {
251        let sha256 = match sha256::Hash::from_str(preimage_sha256) {
252            Ok(result) => result,
253            Err(e) => return Err(S5Error::new(ErrorKind::Input, &e.to_string())),
254        };
255        let hash160 = hash160::Hash::from_slice(
256            ripemd160::Hash::hash(sha256.as_byte_array()).as_byte_array(),
257        )
258        .unwrap();
259        // will never fail as long as sha256 is a valid sha256::Hash
260        Ok(Preimage {
261            bytes: None,
262            sha256: sha256,
263            hash160: hash160,
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, S5Error> {
270        let invoice = match Bolt11Invoice::from_str(&invoice_str) {
271            Ok(invoice) => invoice,
272            Err(e) => {
273                println!("{:?}", e);
274                return Err(S5Error::new(
275                    ErrorKind::Input,
276                    "Could not parse invoice string.",
277                ));
278            }
279        };
280        Ok(Preimage::from_sha256_str(
281            &invoice.payment_hash().to_string(),
282        )?)
283    }
284
285    /// Converts the preimage value bytes to String
286    pub fn to_string(&self) -> Option<String> {
287        match self.bytes {
288            Some(result) => Some(hex::encode(result)),
289            None => None,
290        }
291    }
292}
293use serde_json;
294use std::fs::File;
295use std::io::{Read, Write};
296use std::path::{Path, PathBuf};
297
298/// Boltz standard JSON refund swap file. Can be used to create a file that can be uploaded to boltz.exchange
299#[derive(Serialize, Deserialize, Debug, Clone)]
300pub struct RefundSwapFile {
301    pub id: String,
302    pub currency: String,
303    pub redeem_script: String,
304    pub private_key: String,
305    pub timeout_block_height: u64,
306}
307impl RefundSwapFile {
308    pub fn file_name(&self) -> String {
309        format!("boltz-{}.json", self.id)
310    }
311    pub fn write_to_file<P: AsRef<Path>>(&self, path: P) -> Result<(), S5Error> {
312        let mut full_path = PathBuf::from(path.as_ref());
313        full_path.push(self.file_name());
314        let mut file = match File::create(&full_path) {
315            Ok(f) => f,
316            Err(e) => return Err(S5Error::new(ErrorKind::Input, &e.to_string())),
317        };
318        let json = match serde_json::to_string_pretty(self) {
319            Ok(j) => j,
320            Err(e) => return Err(S5Error::new(ErrorKind::Script, &e.to_string())),
321        };
322        if let Err(e) = writeln!(file, "{}", json) {
323            return Err(S5Error::new(ErrorKind::Input, &e.to_string()));
324        }
325        Ok(())
326    }
327    pub fn read_from_file<P: AsRef<Path>>(path: P) -> Result<Self, S5Error> {
328        let mut file = match File::open(path) {
329            Ok(f) => f,
330            Err(e) => return Err(S5Error::new(ErrorKind::Input, &e.to_string())),
331        };
332        let mut contents = String::new();
333        if let Err(e) = file.read_to_string(&mut contents) {
334            return Err(S5Error::new(ErrorKind::Input, &e.to_string()));
335        }
336        match serde_json::from_str(&contents) {
337            Ok(refund_swap_file) => Ok(refund_swap_file),
338            Err(e) => Err(S5Error::new(ErrorKind::Script, &e.to_string())),
339        }
340    }
341}
342
343/// Recovery items for storage
344#[derive(Serialize, Deserialize, Debug, Clone)]
345pub struct BtcSubmarineRecovery {
346    pub id: String,
347    pub refund_key: String,
348    pub redeem_script: String,
349}
350impl BtcSubmarineRecovery {
351    pub fn new(id: &str, refund_key: &Keypair, redeem_script: &str) -> Self {
352        BtcSubmarineRecovery {
353            id: id.to_string(),
354            refund_key: refund_key.display_secret().to_string(),
355            redeem_script: redeem_script.to_string(),
356        }
357    }
358}
359impl Into<RefundSwapFile> for BtcSubmarineRecovery {
360    fn into(self) -> RefundSwapFile {
361        let script = BtcSwapScript::submarine_from_str(&self.redeem_script).unwrap();
362        RefundSwapFile {
363            id: self.id,
364            currency: "BTC".to_string(),
365            redeem_script: self.redeem_script,
366            private_key: self.refund_key,
367            timeout_block_height: script.timelock as u64,
368        }
369    }
370}
371
372impl TryInto<BtcSwapScript> for &BtcSubmarineRecovery {
373    type Error = S5Error;
374    fn try_into(self) -> Result<BtcSwapScript, Self::Error> {
375        Ok(BtcSwapScript::submarine_from_str(&self.redeem_script).unwrap())
376    }
377}
378
379impl TryInto<Keypair> for &BtcSubmarineRecovery {
380    type Error = S5Error;
381    fn try_into(self) -> Result<Keypair, Self::Error> {
382        let secp = Secp256k1::new();
383        Ok(Keypair::from_seckey_str(&secp, &self.refund_key).unwrap())
384    }
385}
386
387/// Recovery items for storage
388#[derive(Serialize, Deserialize, Debug, Clone)]
389pub struct BtcReverseRecovery {
390    pub id: String,
391    pub preimage: String,
392    pub claim_key: String,
393    pub redeem_script: String,
394}
395impl BtcReverseRecovery {
396    pub fn new(id: &str, preimage: &Preimage, claim_key: &Keypair, redeem_script: &str) -> Self {
397        BtcReverseRecovery {
398            id: id.to_string(),
399            claim_key: claim_key.display_secret().to_string(),
400            preimage: preimage.to_string().unwrap(),
401            redeem_script: redeem_script.to_string(),
402        }
403    }
404}
405impl TryInto<BtcSwapScript> for &BtcReverseRecovery {
406    type Error = S5Error;
407    fn try_into(self) -> Result<BtcSwapScript, Self::Error> {
408        Ok(BtcSwapScript::reverse_from_str(&self.redeem_script).unwrap())
409    }
410}
411
412impl TryInto<Keypair> for &BtcReverseRecovery {
413    type Error = S5Error;
414    fn try_into(self) -> Result<Keypair, Self::Error> {
415        let secp = Secp256k1::new();
416        Ok(Keypair::from_seckey_str(&secp, &self.claim_key).unwrap())
417    }
418}
419impl TryInto<Preimage> for &BtcReverseRecovery {
420    type Error = S5Error;
421    fn try_into(self) -> Result<Preimage, Self::Error> {
422        Ok(Preimage::from_str(&self.preimage).unwrap())
423    }
424}
425
426/// Recovery items for storage
427#[derive(Serialize, Deserialize, Debug, Clone)]
428pub struct LBtcSubmarineRecovery {
429    pub id: String,
430    pub refund_key: String,
431    pub blinding_key: String,
432    pub redeem_script: String,
433}
434impl LBtcSubmarineRecovery {
435    pub fn new(
436        id: &str,
437        refund_key: &Keypair,
438        blinding_key: &ZKKeyPair,
439        redeem_script: &str,
440    ) -> Self {
441        LBtcSubmarineRecovery {
442            id: id.to_string(),
443            refund_key: refund_key.display_secret().to_string(),
444            redeem_script: redeem_script.to_string(),
445            blinding_key: blinding_key.display_secret().to_string(),
446        }
447    }
448}
449impl Into<RefundSwapFile> for LBtcSubmarineRecovery {
450    fn into(self) -> RefundSwapFile {
451        let script =
452            LBtcSwapScript::submarine_from_str(&self.redeem_script, &self.blinding_key).unwrap();
453        RefundSwapFile {
454            id: self.id,
455            currency: "L-BTC".to_string(),
456            redeem_script: self.redeem_script,
457            private_key: self.refund_key,
458            timeout_block_height: script.timelock as u64,
459        }
460    }
461}
462/// Recovery items for storage
463#[derive(Serialize, Deserialize, Debug, Clone)]
464pub struct LBtcReverseRecovery {
465    pub id: String,
466    pub preimage: String,
467    pub claim_key: String,
468    pub blinding_key: String,
469    pub redeem_script: String,
470}
471impl LBtcReverseRecovery {
472    pub fn new(
473        id: &str,
474        preimage: &Preimage,
475        claim_key: &Keypair,
476        blinding_key: &ZKKeyPair,
477        redeem_script: &str,
478    ) -> Self {
479        LBtcReverseRecovery {
480            id: id.to_string(),
481            claim_key: claim_key.display_secret().to_string(),
482            blinding_key: blinding_key.display_secret().to_string(),
483            preimage: preimage.to_string().unwrap(),
484            redeem_script: redeem_script.to_string(),
485        }
486    }
487}
488impl TryInto<LBtcSwapScript> for &LBtcReverseRecovery {
489    type Error = S5Error;
490    fn try_into(self) -> Result<LBtcSwapScript, Self::Error> {
491        Ok(LBtcSwapScript::reverse_from_str(&self.redeem_script, &self.blinding_key).unwrap())
492    }
493}
494
495impl TryInto<Keypair> for &LBtcReverseRecovery {
496    type Error = S5Error;
497    fn try_into(self) -> Result<Keypair, Self::Error> {
498        let secp = Secp256k1::new();
499        Ok(Keypair::from_seckey_str(&secp, &self.claim_key).unwrap())
500    }
501}
502impl TryInto<Preimage> for &LBtcReverseRecovery {
503    type Error = S5Error;
504    fn try_into(self) -> Result<Preimage, Self::Error> {
505        Ok(Preimage::from_str(&self.preimage).unwrap())
506    }
507}
508#[cfg(test)]
509mod tests {
510
511    use super::*;
512
513    #[test]
514    fn test_derivation() {
515        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";
516        let index = 0 as u64; // 0
517        let sk = SwapKey::from_submarine_account(mnemonic, "", Chain::Bitcoin, index).unwrap();
518        let lks: LiquidSwapKey = sk.clone().into();
519        assert!(sk.fingerprint == lks.fingerprint);
520        // println!("{:?}", derived.unwrap().Keypair.display_secret());
521        assert_eq!(&sk.fingerprint.to_string().clone(), "9a6a2580");
522        assert_eq!(
523            &sk.keypair.display_secret().to_string(),
524            "d8d26ab9ba4e2c44f1a1fb9e10dc9d78707aaaaf38b5d42cf5c8bf00306acd85"
525        );
526    }
527
528    #[test]
529    #[ignore]
530    fn test_recover() {
531        let recovery = BtcSubmarineRecovery {
532            id: "y8uGeA".to_string(),
533            refund_key: "5416f1e024c191605502017d066786e294f841e711d3d437d13e9d27e40e066e".to_string(),
534            redeem_script: "a914046fabc17989627f6ca9c1846af8e470263e712d87632102c929edb654bc1da91001ec27d74d42b5d6a8cf8aef2fab7c55f2eb728eed0d1f6703634d27b1752102c530b4583640ab3df5c75c5ce381c4b747af6bdd6c618db7e5248cb0adcf3a1868ac".to_string(),
535        };
536        let file: RefundSwapFile = recovery.into();
537        let base_path = "/tmp/boltz-rust";
538        file.write_to_file(base_path);
539        let file_path = base_path.to_owned() + "/" + &file.file_name();
540        let file_struct = RefundSwapFile::read_from_file(file_path);
541        println!("Refund File: {:?}", file_struct);
542    }
543}