lighthouse/common/eth2_wallet_manager/src/wallet_manager.rs
Akihito Nakano 4186d117af Replace OpenOptions::new with File::options to be readable (#3059)
## Issue Addressed

Closes #3049 

This PR updates widely but this replace is safe as `File::options()` is equivelent to `OpenOptions::new()`.
ref: https://doc.rust-lang.org/stable/src/std/fs.rs.html#378-380
2022-03-07 06:30:18 +00:00

387 lines
11 KiB
Rust

use crate::{
filesystem::{create, Error as FilesystemError},
LockedWallet,
};
use eth2_wallet::{bip39::Mnemonic, Error as WalletError, Uuid, Wallet, WalletBuilder};
use lockfile::LockfileError;
use std::collections::HashMap;
use std::ffi::OsString;
use std::fs::{create_dir_all, read_dir, File};
use std::io;
use std::path::{Path, PathBuf};
#[derive(Debug)]
pub enum Error {
DirectoryDoesNotExist(PathBuf),
WalletError(WalletError),
FilesystemError(FilesystemError),
UnableToReadDir(io::Error),
UnableToReadWallet(io::Error),
UnableToReadFilename(OsString),
NameAlreadyTaken(String),
WalletNameUnknown(String),
WalletDirExists(PathBuf),
IoError(io::Error),
MissingWalletDir(PathBuf),
UuidMismatch((Uuid, Uuid)),
LockfileError(LockfileError),
}
impl From<io::Error> for Error {
fn from(e: io::Error) -> Error {
Error::IoError(e)
}
}
impl From<WalletError> for Error {
fn from(e: WalletError) -> Error {
Error::WalletError(e)
}
}
impl From<FilesystemError> for Error {
fn from(e: FilesystemError) -> Error {
Error::FilesystemError(e)
}
}
impl From<LockfileError> for Error {
fn from(e: LockfileError) -> Error {
Error::LockfileError(e)
}
}
/// Defines the type of an EIP-2386 wallet.
///
/// Presently only `Hd` wallets are supported.
pub enum WalletType {
/// Hierarchical-deterministic.
Hd,
}
/// Manages a directory containing EIP-2386 wallets.
///
/// Each wallet is stored in a directory with the name of the wallet UUID. Inside each directory a
/// EIP-2386 JSON wallet is also stored using the UUID as the filename.
///
/// In each wallet directory an optional `.lock` exists to prevent concurrent reads and writes from
/// the same wallet.
///
/// Example:
///
/// ```ignore
/// wallets
/// ├── 35c07717-c6f3-45e8-976f-ef5d267e86c9
/// │   └── 35c07717-c6f3-45e8-976f-ef5d267e86c9
/// └── 747ad9dc-e1a1-4804-ada4-0dc124e46c49
/// └── .lock
/// └── 747ad9dc-e1a1-4804-ada4-0dc124e46c49
/// ```
pub struct WalletManager {
dir: PathBuf,
}
impl WalletManager {
/// Open a directory containing multiple wallets.
///
/// Pass the `wallets` directory as `dir` (see struct-level example).
pub fn open<P: AsRef<Path>>(dir: P) -> Result<Self, Error> {
let dir: PathBuf = dir.as_ref().into();
if dir.exists() {
Ok(Self { dir })
} else {
Err(Error::DirectoryDoesNotExist(dir))
}
}
/// Searches all wallets in `self.dir` and returns the wallet with this name.
///
/// ## Errors
///
/// - If there is no wallet with this name.
/// - If there is a file-system or parsing error.
pub fn wallet_by_name(&self, name: &str) -> Result<LockedWallet, Error> {
LockedWallet::open(
self.dir.clone(),
self.wallets()?
.get(name)
.ok_or_else(|| Error::WalletNameUnknown(name.into()))?,
)
}
/// Creates a new wallet with the given `name` in `self.dir` with the given `mnemonic` as a
/// seed, encrypted with `password`.
///
/// ## Errors
///
/// - If a wallet with this name already exists.
/// - If there is a file-system or parsing error.
pub fn create_wallet(
&self,
name: String,
_wallet_type: WalletType,
mnemonic: &Mnemonic,
password: &[u8],
) -> Result<LockedWallet, Error> {
if self.wallets()?.contains_key(&name) {
return Err(Error::NameAlreadyTaken(name));
}
let wallet = WalletBuilder::from_mnemonic(mnemonic, password, name)?.build()?;
let uuid = *wallet.uuid();
let wallet_dir = self.dir.join(format!("{}", uuid));
if wallet_dir.exists() {
return Err(Error::WalletDirExists(wallet_dir));
}
create_dir_all(&wallet_dir)?;
create(&wallet_dir, &wallet)?;
drop(wallet);
LockedWallet::open(&self.dir, &uuid)
}
/// Iterates all wallets in `self.dir` and returns a mapping of their name to their UUID.
///
/// Ignores any items in `self.dir` that:
///
/// - Are files.
/// - Are directories, but their file-name does not parse as a UUID.
///
/// This function is fairly strict, it will fail if any directory is found that does not obey
/// the expected structure (e.g., there is a UUID directory that does not contain a valid JSON
/// keystore with the same UUID).
pub fn wallets(&self) -> Result<HashMap<String, Uuid>, Error> {
let mut wallets = HashMap::new();
for f in read_dir(&self.dir).map_err(Error::UnableToReadDir)? {
let f = f?;
// Ignore any non-directory objects in the root wallet dir.
if f.file_type()?.is_dir() {
let file_name = f
.file_name()
.into_string()
.map_err(Error::UnableToReadFilename)?;
// Ignore any paths that don't parse as a UUID.
if let Ok(uuid) = Uuid::parse_str(&file_name) {
let wallet_path = f.path().join(format!("{}", uuid));
let wallet = File::options()
.read(true)
.create(false)
.open(wallet_path)
.map_err(Error::UnableToReadWallet)
.and_then(|f| Wallet::from_json_reader(f).map_err(Error::WalletError))?;
if *wallet.uuid() != uuid {
return Err(Error::UuidMismatch((uuid, *wallet.uuid())));
}
wallets.insert(wallet.name().into(), *wallet.uuid());
}
}
}
Ok(wallets)
}
}
#[cfg(test)]
// These tests are very slow in debug, only test in release.
#[cfg(not(debug_assertions))]
mod tests {
use super::*;
use crate::{filesystem::read, locked_wallet::LOCK_FILE};
use eth2_wallet::bip39::{Language, Mnemonic};
use tempfile::tempdir;
const MNEMONIC: &str =
"enemy fog enlist laundry nurse hungry discover turkey holiday resemble glad discover";
const WALLET_PASSWORD: &[u8] = &[43; 43];
fn get_mnemonic() -> Mnemonic {
Mnemonic::from_phrase(MNEMONIC, Language::English).unwrap()
}
fn create_wallet(mgr: &WalletManager, id: usize) -> LockedWallet {
let wallet = mgr
.create_wallet(
format!("{}", id),
WalletType::Hd,
&get_mnemonic(),
WALLET_PASSWORD,
)
.expect("should create wallet");
assert!(
wallet_dir_path(&mgr.dir, wallet.wallet().uuid()).exists(),
"should have created wallet dir"
);
assert!(
json_path(&mgr.dir, wallet.wallet().uuid()).exists(),
"should have created json file"
);
assert!(
lockfile_path(&mgr.dir, wallet.wallet().uuid()).exists(),
"should have created lockfile"
);
wallet
}
fn load_wallet_raw<P: AsRef<Path>>(base_dir: P, uuid: &Uuid) -> Wallet {
read(wallet_dir_path(base_dir, uuid), uuid).expect("should load raw json")
}
fn wallet_dir_path<P: AsRef<Path>>(base_dir: P, uuid: &Uuid) -> PathBuf {
let s = format!("{}", uuid);
base_dir.as_ref().join(&s)
}
fn lockfile_path<P: AsRef<Path>>(base_dir: P, uuid: &Uuid) -> PathBuf {
let s = format!("{}", uuid);
base_dir.as_ref().join(&s).join(LOCK_FILE)
}
fn json_path<P: AsRef<Path>>(base_dir: P, uuid: &Uuid) -> PathBuf {
let s = format!("{}", uuid);
base_dir.as_ref().join(&s).join(&s)
}
#[test]
fn duplicate_names() {
let dir = tempdir().unwrap();
let base_dir = dir.path();
let mgr = WalletManager::open(base_dir).unwrap();
let name = "cats".to_string();
mgr.create_wallet(
name.clone(),
WalletType::Hd,
&get_mnemonic(),
WALLET_PASSWORD,
)
.expect("should create first wallet");
match mgr.create_wallet(
name.clone(),
WalletType::Hd,
&get_mnemonic(),
WALLET_PASSWORD,
) {
Err(Error::NameAlreadyTaken(_)) => {}
_ => panic!("expected name error"),
}
}
#[test]
fn keystore_generation() {
let dir = tempdir().unwrap();
let base_dir = dir.path();
let mgr = WalletManager::open(base_dir).unwrap();
let name = "cats".to_string();
let mut w = mgr
.create_wallet(
name.clone(),
WalletType::Hd,
&get_mnemonic(),
WALLET_PASSWORD,
)
.expect("should create first wallet");
let uuid = w.wallet().uuid().clone();
assert_eq!(
load_wallet_raw(&base_dir, &uuid).nextaccount(),
0,
"should start wallet with nextaccount 0"
);
for i in 1..3 {
w.next_validator(WALLET_PASSWORD, &[50; 32], &[51; 32])
.expect("should create validator");
assert_eq!(
load_wallet_raw(&base_dir, &uuid).nextaccount(),
i,
"should update wallet with nextaccount {}",
i
);
}
drop(w);
// Check that we can open the wallet by name.
let by_name = mgr.wallet_by_name(&name).unwrap();
assert_eq!(by_name.wallet().name(), name);
drop(by_name);
let wallets = mgr.wallets().unwrap().into_iter().collect::<Vec<_>>();
assert_eq!(wallets, vec![(name, uuid)]);
}
#[test]
fn locked_wallet_lockfile() {
let dir = tempdir().unwrap();
let base_dir = dir.path();
let mgr = WalletManager::open(base_dir).unwrap();
let uuid_a = create_wallet(&mgr, 0).wallet().uuid().clone();
let uuid_b = create_wallet(&mgr, 1).wallet().uuid().clone();
let locked_a = LockedWallet::open(&base_dir, &uuid_a).expect("should open wallet a");
assert!(
lockfile_path(&base_dir, &uuid_a).exists(),
"lockfile should exist"
);
drop(locked_a);
assert!(
!lockfile_path(&base_dir, &uuid_a).exists(),
"lockfile have been cleaned up"
);
let locked_a = LockedWallet::open(&base_dir, &uuid_a).expect("should open wallet a");
let locked_b = LockedWallet::open(&base_dir, &uuid_b).expect("should open wallet b");
assert!(
lockfile_path(&base_dir, &uuid_a).exists(),
"lockfile a should exist"
);
assert!(
lockfile_path(&base_dir, &uuid_b).exists(),
"lockfile b should exist"
);
match LockedWallet::open(&base_dir, &uuid_a) {
Err(Error::LockfileError(_)) => {}
_ => panic!("did not get locked error"),
};
drop(locked_a);
LockedWallet::open(&base_dir, &uuid_a)
.expect("should open wallet a after previous instance is dropped");
match LockedWallet::open(&base_dir, &uuid_b) {
Err(Error::LockfileError(_)) => {}
_ => panic!("did not get locked error"),
};
drop(locked_b);
LockedWallet::open(&base_dir, &uuid_b)
.expect("should open wallet a after previous instance is dropped");
}
}