From 68cfbbecadd3959e62e7c25bd0f8eee8a17f4cbe Mon Sep 17 00:00:00 2001 From: AntiochP <30642645+antiochp@users.noreply.github.com> Date: Tue, 24 Oct 2017 13:34:34 -0400 Subject: [PATCH] "grin wallet init" to create the initial wallet.seed file (#198) * "wallet init" to create the initial wallet.seed file * cleanup and better error msgs * add some basic wallet docs * cleanup and make passphrase optional --- doc/build.md | 21 ++++---- doc/wallet.md | 52 +++++++++++++++++++ src/bin/grin.rs | 39 ++++++++------ wallet/src/lib.rs | 2 +- wallet/src/types.rs | 120 ++++++++++++++++++++++++++++++++++++++++++-- 5 files changed, 205 insertions(+), 29 deletions(-) create mode 100644 doc/wallet.md diff --git a/doc/build.md b/doc/build.md index baf0b6b41..62fa4d3e2 100644 --- a/doc/build.md +++ b/doc/build.md @@ -17,7 +17,7 @@ The instructions below will assume a Linux system. In order to compile and run Grin on your machine, you should have installed: * Git - to clone the repository -* cmake - 3.2 or greater should be installed and on your $PATH. Used by the build to compile the mining plugins found in the included [Cuckoo Miner](https://github.com/mimblewimble/cuckoo-miner) +* cmake - 3.2 or greater should be installed and on your $PATH. Used by the build to compile the mining plugins found in the included [Cuckoo Miner](https://github.com/mimblewimble/cuckoo-miner) * Rust - via [Rustup](https://www.rustup.rs/) - Can be installed via your package manager or manually via the following commands: ``` curl https://sh.rustup.rs -sSf | sh @@ -61,7 +61,7 @@ Provided all of the prerequisites were installed and there were no issues, there * A set of mining plugins, which should be in the 'plugins' directory located next to the grin executable -* A configuration file in the root project directory named grin.toml +* A configuration file in the root project directory named grin.toml By default, executing: @@ -98,22 +98,25 @@ At present, the relevant modes of operation are 'server' and 'wallet'. When runn For a basic example simulating a single node network, create a directory called 'node1' and change your working directory to it. You'll use this directory to run a wallet and create a new blockchain via a server running in mining mode. -Before running your mining server, a wallet server needs to be set up and listening so that the mining server knows where to send mining rewards. Do this from the first node directory with the following command: +Before running your mining server, a wallet server needs to be set up and listening so that the mining server knows where to send mining rewards. Do this from the first node directory with the following commands: - node1$ grin wallet -p "password" receive + node1$ grin wallet init + node1$ grin wallet -p "password" receive + +See [wallet](wallet.md) for more info on the various Grin wallet commands and options. This will create a wallet server listening on the default port 13416 with the password "password". Next, in another terminal window in the 'node1' directory, run a full mining node with the following command: - node1$ grin server -m run + node1$ grin server -m run -This creates a new .grin database directory in the current directory, and begins mining new blocks (with no transactions, for now). Note this starts two services listening on two default ports, -port 13414 for the peer-to-peer (P2P) service which keeps all nodes synchronised, and 13413 for the Rest API service used to verify transactions and post new transactions to the pool (for example). These ports can be configured via command line switches, or via a grin.toml file in the working directory. +This creates a new .grin database directory in the current directory, and begins mining new blocks (with no transactions, for now). Note this starts two services listening on two default ports, +port 13414 for the peer-to-peer (P2P) service which keeps all nodes synchronized, and 13413 for the Rest API service used to verify transactions and post new transactions to the pool (for example). These ports can be configured via command line switches, or via a grin.toml file in the working directory. Let the mining server find a few blocks, then stop (just ctrl-c) the mining server and the wallet server. You'll notice grin has created a database directory (.grin) in which the blockchain and peer data is stored. There should also be a wallet.dat file in the current directory, which contains a few coinbase mining rewards created each time the server mines a new block. ## Advanced Example -The following outlines a more advanced example simulating a multi-server network with transactions being posted. +The following outlines a more advanced example simulating a multi-server network with transactions being posted. For the sake of example, we're going to run three nodes with varying setups. Create two more directories beside your node1 directory, called node2 and node3. If you want to clear data from your previous run (or anytime you want to reset the blockchain and all peer data) just delete the wallet.dat file in the server1 directory and run rm -rf .grin to remove grin's database. @@ -161,7 +164,7 @@ use node 2's API listener to validate our transaction inputs before sending: node1$ grin wallet -p "password" -a "http://127.0.0.1:20001" send 20000 -d "http://127.0.0.1:35000" -Your terminal windows should all light up now. Node 1 will check its inputs against node 2, and then send a partial transaction to node 3's wallet listener. Node 3 has been configured to +Your terminal windows should all light up now. Node 1 will check its inputs against node 2, and then send a partial transaction to node 3's wallet listener. Node 3 has been configured to send signed and finalised transactions to the api listener on node 1, which should then add the transaction to the next block and validate it via mining. You can feel free to try any number of permutations or combinations of the above, just note that grin is very new and under active development, so your mileage may vary. You can also use a separate 'grin.toml' file in each server directory to simplify command line switches. diff --git a/doc/wallet.md b/doc/wallet.md new file mode 100644 index 000000000..3c52daa70 --- /dev/null +++ b/doc/wallet.md @@ -0,0 +1,52 @@ +# Grin - Basic Wallet + +## Wallet Files + +A Grin wallet maintains its state in the following files - + +``` +wallet.seed # *** passphrase protected seed file (keep this private) *** +wallet.dat # wallet outputs (both spent and unspent) +wallet.lock # lock file, prevents multiple processes writing to wallet.dat +``` + +By default Grin will look for these in the current working directory. + +## Basic Wallet Commands + +`grin wallet --help` will display usage info about the following. + +### grin wallet init + +Before using a wallet a new seed file `wallet.seed` needs to be generated via `grin wallet init` - + +``` +grin wallet init +Generating wallet seed file at: ./wallet.seed +``` + +### grin wallet info + +Some (very) basic information about current wallet outputs can be displayed with `grin wallet info` - + +``` +grin wallet -p "password" info +Using wallet seed file at: ./wallet.seed +Outputs - +key_id, height, lock_height, status, spendable?, coinbase?, value +---------------------------------- +96805837571719c692b6, 21, 24, Spent, false, true, 50000000000 +... +``` + +### grin wallet receive + +(tbd) + +### grin wallet send + +(tbd) + +### grin wallet burn + +[tbd] diff --git a/src/bin/grin.rs b/src/bin/grin.rs index 8c90811b5..75cd4cc7a 100644 --- a/src/bin/grin.rs +++ b/src/bin/grin.rs @@ -41,7 +41,6 @@ use daemonize::Daemonize; use config::GlobalConfig; use wallet::WalletConfig; use core::global; -use keychain::Keychain; use util::{LoggingConfig, LOGGER, init_logger}; fn start_from_config_file(mut global_config: GlobalConfig) { @@ -164,7 +163,8 @@ fn main() { .short("p") .long("pass") .help("Wallet passphrase used to generate the private key seed") - .takes_value(true)) + .takes_value(true) + .default_value("mimblewimble")) .arg(Arg::with_name("data_dir") .short("dd") .long("data_dir") @@ -227,7 +227,10 @@ fn main() { .takes_value(true))) .subcommand(SubCommand::with_name("info") - .about("basic wallet info (outputs)"))) + .about("basic wallet info (outputs)")) + + .subcommand(SubCommand::with_name("init") + .about("Initialize a new wallet seed file."))) .get_matches(); @@ -333,17 +336,8 @@ fn server_command(server_args: &ArgMatches, global_config: GlobalConfig) { } fn wallet_command(wallet_args: &ArgMatches) { - let hd_seed = wallet_args.value_of("pass").expect( - "Wallet passphrase required.", - ); - - // TODO do something closer to BIP39, eazy solution right now - let seed = blake2::blake2b::blake2b(32, &[], hd_seed.as_bytes()); - let keychain = Keychain::from_seed(seed.as_bytes()).expect( - "Failed to initialize keychain from the provided seed.", - ); - let mut wallet_config = WalletConfig::default(); + if let Some(port) = wallet_args.value_of("port") { let default_ip = "0.0.0.0"; wallet_config.api_http_addr = format!("{}:{}", default_ip, port); @@ -357,8 +351,25 @@ fn wallet_command(wallet_args: &ArgMatches) { wallet_config.check_node_api_http_addr = sa.to_string().clone(); } - match wallet_args.subcommand() { + // Derive the keychain based on seed from seed file and specified passphrase. + // Generate the initial wallet seed if we are running "wallet init". + if let ("init", Some(_)) = wallet_args.subcommand() { + wallet::WalletSeed::init_file(&wallet_config) + .expect("Failed to init wallet seed file."); + // we are done here with creating the wallet, so just return + return; + } + + let wallet_seed = wallet::WalletSeed::from_file(&wallet_config) + .expect("Failed to read wallet seed file."); + let passphrase = wallet_args + .value_of("pass") + .expect("Failed to read passphrase."); + let keychain = wallet_seed.derive_keychain(&passphrase) + .expect("Failed to derive keychain from seed file and passphrase."); + + match wallet_args.subcommand() { ("receive", Some(receive_args)) => { if let Some(f) = receive_args.value_of("input") { let mut file = File::open(f).expect("Unable to open transaction file."); diff --git a/wallet/src/lib.rs b/wallet/src/lib.rs index 619c8773b..24e3ae208 100644 --- a/wallet/src/lib.rs +++ b/wallet/src/lib.rs @@ -39,4 +39,4 @@ mod types; pub use info::show_info; pub use receiver::{WalletReceiver, receive_json_tx}; pub use sender::{issue_send_tx, issue_burn_tx}; -pub use types::{WalletConfig, WalletReceiveRequest, BlockFees, CbData}; +pub use types::{BlockFees, CbData, WalletConfig, WalletReceiveRequest, WalletSeed}; diff --git a/wallet/src/types.rs b/wallet/src/types.rs index e6db0fd3d..a5dcfd1ae 100644 --- a/wallet/src/types.rs +++ b/wallet/src/types.rs @@ -12,13 +12,16 @@ // See the License for the specific language governing permissions and // limitations under the License. +use blake2; +use rand::{thread_rng, Rng}; use std::{fmt, num, thread, time}; use std::convert::From; use std::fs::{self, File, OpenOptions}; -use std::io::Write; +use std::io::{self, Read, Write}; use std::path::Path; use std::path::MAIN_SEPARATOR; use std::collections::HashMap; +use std::cmp::min; use serde_json; use secp; @@ -32,6 +35,7 @@ use util::LOGGER; const DAT_FILE: &'static str = "wallet.dat"; const LOCK_FILE: &'static str = "wallet.lock"; +const SEED_FILE: &'static str = "wallet.seed"; const DEFAULT_BASE_FEE: u64 = 10; @@ -60,6 +64,8 @@ pub enum Error { WalletData(String), /// An error in the format of the JSON structures exchanged by the wallet Format(String), + /// An IO Error + IOError(io::Error), /// Error when contacting a node through its API Node(api::Error), } @@ -100,6 +106,12 @@ impl From for Error { } } +impl From for Error { + fn from(e: io::Error) -> Error { + Error::IOError(e) + } +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct WalletConfig { // Whether to run a wallet @@ -200,6 +212,98 @@ impl OutputData { } } +#[derive(Clone, PartialEq)] +pub struct WalletSeed([u8; 32]); + +impl WalletSeed { + pub fn from_bytes(bytes: &[u8]) -> WalletSeed { + let mut seed = [0; 32]; + for i in 0..min(32, bytes.len()) { + seed[i] = bytes[i]; + } + WalletSeed(seed) + } + + fn from_hex(hex: &str) -> Result { + let bytes = util::from_hex(hex.to_string())?; + Ok(WalletSeed::from_bytes(&bytes)) + } + + pub fn to_hex(&self) -> String { + util::to_hex(self.0.to_vec()) + } + + pub fn derive_keychain(&self, password: &str) -> Result { + let seed = blake2::blake2b::blake2b(64, &password.as_bytes(), &self.0); + let result = keychain::Keychain::from_seed(seed.as_bytes())?; + Ok(result) + } + + pub fn init_new() -> WalletSeed { + let seed: [u8; 32] = thread_rng().gen(); + WalletSeed(seed) + } + + pub fn init_file(wallet_config: &WalletConfig) -> Result { + // create directory if it doesn't exist + fs::create_dir_all(&wallet_config.data_file_dir)?; + + let seed_file_path = &format!( + "{}{}{}", + wallet_config.data_file_dir, + MAIN_SEPARATOR, + SEED_FILE, + ); + + debug!( + LOGGER, + "Generating wallet seed file at: {}", + seed_file_path, + ); + + if Path::new(seed_file_path).exists() { + panic!("wallet seed file already exists"); + } else { + let seed = WalletSeed::init_new(); + let mut file = File::create(seed_file_path)?; + file.write_all(&seed.to_hex().as_bytes())?; + Ok(seed) + } + } + + pub fn from_file(wallet_config: &WalletConfig) -> Result { + // create directory if it doesn't exist + fs::create_dir_all(&wallet_config.data_file_dir)?; + + let seed_file_path = &format!( + "{}{}{}", + wallet_config.data_file_dir, + MAIN_SEPARATOR, + SEED_FILE, + ); + + debug!( + LOGGER, + "Using wallet seed file at: {}", + seed_file_path, + ); + + if Path::new(seed_file_path).exists() { + let mut file = File::open(seed_file_path)?; + let mut buffer = String::new(); + file.read_to_string(&mut buffer)?; + let wallet_seed = WalletSeed::from_hex(&buffer)?; + Ok(wallet_seed) + } else { + error!( + LOGGER, + "Run: \"grin wallet init\" to initialize a new wallet.", + ); + panic!("wallet seed file does not yet exist (grin wallet init)"); + } + } +} + /// Wallet information tracking all our outputs. Based on HD derivation and /// avoids storing any key data, only storing output amounts and child index. /// This data structure is directly based on the JSON representation stored @@ -242,7 +346,7 @@ impl WalletData { .map_err(|_| { Error::WalletData(format!( "Could not create wallet lock file. Either \ - some other process is using the wallet or there's a write access issue." + some other process is using the wallet or there is a write access issue." )) }); match result { @@ -250,16 +354,22 @@ impl WalletData { break; } Err(e) => { - if retries >= 3 { + if retries >= 6 { + info!( + LOGGER, + "failed to obtain wallet.lock after {} retries, \ + unable to successfully open the wallet", + retries + ); return Err(e); } debug!( LOGGER, - "failed to obtain wallet.lock, retries - {}, sleeping", + "failed to obtain wallet.lock, retries - {}, sleeping and retrying", retries ); retries += 1; - thread::sleep(time::Duration::from_millis(500)); + thread::sleep(time::Duration::from_millis(1000)); } } }