Skip to content
This repository was archived by the owner on Aug 31, 2023. It is now read-only.

Commit 73f36a4

Browse files
Merge #157
157: feat: Persist wallet seed on disk r=klochowicz a=klochowicz The same seed is persisted across multiple runs of the application. XXX: This seed is not encrypted on disk, and should as such not be used in production. Co-authored-by: Mariusz Klochowicz <mariusz@klochowicz.com>
2 parents 4967c23 + 59982fa commit 73f36a4

File tree

2 files changed

+77
-18
lines changed

2 files changed

+77
-18
lines changed

rust/src/seed.rs

Lines changed: 72 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
use std::path::Path;
2+
3+
use anyhow::bail;
14
use anyhow::Result;
25
use bdk::bitcoin;
36
use bdk::bitcoin::util::bip32::ExtendedPrivKey;
@@ -9,47 +12,84 @@ use sha2::Sha256;
912

1013
#[derive(Clone)]
1114
pub struct Bip39Seed {
12-
pub seed: [u8; 64],
13-
pub mnemonic: Mnemonic,
15+
mnemonic: Mnemonic,
1416
}
1517

1618
impl Bip39Seed {
17-
pub fn new() -> Result<Bip39Seed> {
19+
pub fn new() -> Result<Self> {
1820
let mut rng = rand::thread_rng();
1921
let mnemonic = Mnemonic::generate_in_with(&mut rng, Language::English, 12)?;
22+
Ok(Self { mnemonic })
23+
}
2024

25+
/// Initialise a [`Seed`] from a path.
26+
/// Generates new seed if there was no seed found in the given path
27+
pub fn initialize(seed_file: &Path) -> Result<Self> {
28+
let seed = if !seed_file.exists() {
29+
tracing::info!("No seed found. Generating new seed");
30+
let seed = Self::new()?;
31+
seed.write_to(seed_file)?;
32+
seed
33+
} else {
34+
Bip39Seed::read_from(seed_file)?
35+
};
36+
Ok(seed)
37+
}
38+
39+
pub fn seed(&self) -> [u8; 64] {
2140
// passing an empty string here is the expected argument if the seed should not be
2241
// additionally password protected (according to https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki#from-mnemonic-to-seed)
23-
let seed = mnemonic.to_seed_normalized("");
24-
25-
Ok(Bip39Seed { seed, mnemonic })
42+
self.mnemonic.to_seed_normalized("")
2643
}
2744

2845
pub fn derive_extended_priv_key(&self, network: Network) -> Result<ExtendedPrivKey> {
2946
let mut ext_priv_key_seed = [0u8; 64];
3047

31-
Hkdf::<Sha256>::new(None, &self.seed)
48+
Hkdf::<Sha256>::new(None, &self.seed())
3249
.expand(b"BITCOIN_WALLET_SEED", &mut ext_priv_key_seed)
3350
.expect("array is of correct length");
3451

3552
let ext_priv_key = ExtendedPrivKey::new_master(network, &ext_priv_key_seed)?;
36-
3753
Ok(ext_priv_key)
3854
}
3955

4056
pub fn get_seed_phrase(&self) -> Vec<String> {
41-
let phrase = self
42-
.mnemonic
43-
.to_string()
44-
.split(' ')
45-
.map(|word| word.into())
46-
.collect();
47-
phrase
57+
self.mnemonic.word_iter().map(|word| word.into()).collect()
58+
}
59+
60+
// Read the entropy used to generate Mnemonic from disk
61+
fn read_from(path: &Path) -> Result<Self> {
62+
let bytes = std::fs::read(path)?;
63+
64+
let seed: Bip39Seed = TryInto::try_into(bytes)
65+
.map_err(|_| anyhow::anyhow!("Cannot read the stored entropy"))?;
66+
Ok(seed)
67+
}
68+
69+
// Store the entropy used to generate Mnemonic on disk
70+
fn write_to(&self, path: &Path) -> Result<()> {
71+
if path.exists() {
72+
let path = path.display();
73+
bail!("Refusing to overwrite file at {path}")
74+
}
75+
std::fs::write(path, &self.mnemonic.to_entropy())?;
76+
77+
Ok(())
78+
}
79+
}
80+
81+
impl TryFrom<Vec<u8>> for Bip39Seed {
82+
type Error = anyhow::Error;
83+
fn try_from(bytes: Vec<u8>) -> Result<Self, Self::Error> {
84+
let mnemonic = Mnemonic::from_entropy(&bytes)?;
85+
Ok(Bip39Seed { mnemonic })
4886
}
4987
}
5088

5189
#[cfg(test)]
5290
mod tests {
91+
use std::env::temp_dir;
92+
5393
use crate::seed::Bip39Seed;
5494

5595
#[test]
@@ -58,4 +98,21 @@ mod tests {
5898
let phrase = seed.get_seed_phrase();
5999
assert_eq!(12, phrase.len());
60100
}
101+
102+
#[test]
103+
fn reinitialised_seed_is_the_same() {
104+
let mut path = temp_dir();
105+
path.push("seed");
106+
let seed_1 = Bip39Seed::initialize(&path).unwrap();
107+
let seed_2 = Bip39Seed::initialize(&path).unwrap();
108+
assert_eq!(
109+
seed_1.mnemonic, seed_2.mnemonic,
110+
"Reinitialised wallet should contain the same mnemonic"
111+
);
112+
assert_eq!(
113+
seed_1.seed(),
114+
seed_2.seed(),
115+
"Seed derived from mnemonic should be the same"
116+
);
117+
}
61118
}

rust/src/wallet.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,14 +43,16 @@ impl From<Network> for bitcoin::Network {
4343
}
4444

4545
impl Wallet {
46-
pub fn new(network: Network) -> Result<Wallet> {
46+
pub fn new(network: Network, data_dir: &Path) -> Result<Wallet> {
4747
let electrum_url = match network {
4848
Network::Mainnet => MAINNET_ELECTRUM,
4949
Network::Testnet => TESTNET_ELECTRUM,
5050
_ => bail!("Only public networks are supported"),
5151
};
5252

53-
let seed = Bip39Seed::new()?;
53+
let mut path = data_dir.to_owned();
54+
path.push("seed");
55+
let seed = Bip39Seed::initialize(&path)?;
5456
let ext_priv_key = seed.derive_extended_priv_key(network.into())?;
5557

5658
let client = Client::new(electrum_url)?;
@@ -91,7 +93,7 @@ fn get_wallet() -> Result<MutexGuard<'static, Wallet>> {
9193
9294
pub fn init_wallet(network: Network, data_dir: &Path) -> Result<()> {
9395
tracing::debug!(?data_dir, "Wallet will be stored on disk");
94-
WALLET.set(Mutex::new(Wallet::new(network)?));
96+
WALLET.set(Mutex::new(Wallet::new(network, data_dir)?));
9597
Ok(())
9698
}
9799

0 commit comments

Comments
 (0)