// Copyright 2015, 2016 Ethcore (UK) Ltd. // This file is part of Parity. // Parity is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // Parity is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // You should have received a copy of the GNU General Public License // along with Parity. If not, see . use std::env; use std::fs::File; use std::time::Duration; use std::io::{BufRead, BufReader}; use std::net::{SocketAddr, IpAddr}; use std::path::PathBuf; use cli::{USAGE, Args}; use docopt::Docopt; use die::*; use util::*; use util::log::Colour::*; use ethcore::account_provider::AccountProvider; use util::network_settings::NetworkSettings; use ethcore::client::{append_path, get_db_path, Mode, ClientConfig, DatabaseCompactionProfile, Switch, VMType}; use ethcore::miner::{MinerOptions, PendingSet, GasPricer, GasPriceCalibratorOptions}; use ethcore::ethereum; use ethcore::spec::Spec; use ethsync::SyncConfig; use rpc::IpcConfiguration; pub struct Configuration { pub args: Args } pub struct Directories { pub keys: String, pub db: String, pub dapps: String, pub signer: String, } impl Configuration { pub fn parse() -> Self { Configuration { args: Docopt::new(USAGE).and_then(|d| d.decode()).unwrap_or_else(|e| e.exit()), } } pub fn mode(&self) -> Mode { match &(self.args.flag_mode[..]) { "active" => Mode::Active, "passive" => Mode::Passive(Duration::from_secs(self.args.flag_mode_timeout), Duration::from_secs(self.args.flag_mode_alarm)), "dark" => Mode::Dark(Duration::from_secs(self.args.flag_mode_timeout)), _ => die!("{}: Invalid address for --mode. Must be one of active, passive or dark.", self.args.flag_mode), } } fn net_port(&self) -> u16 { self.args.flag_port } fn chain(&self) -> String { if self.args.flag_testnet { "morden".to_owned() } else { self.args.flag_chain.clone() } } fn max_peers(&self) -> u32 { self.args.flag_maxpeers.unwrap_or(self.args.flag_peers) as u32 } fn decode_u256(d: &str, argument: &str) -> U256 { U256::from_dec_str(d).unwrap_or_else(|_| U256::from_str(clean_0x(d)).unwrap_or_else(|_| die!("{}: Invalid numeric value for {}. Must be either a decimal or a hex number.", d, argument) ) ) } fn work_notify(&self) -> Vec { self.args.flag_notify_work.as_ref().map_or_else(Vec::new, |s| s.split(',').map(|s| s.to_owned()).collect()) } pub fn miner_options(&self) -> MinerOptions { let (own, ext) = match self.args.flag_reseal_on_txs.as_str() { "none" => (false, false), "own" => (true, false), "ext" => (false, true), "all" => (true, true), x => die!("{}: Invalid value for --reseal option. Use --help for more information.", x) }; MinerOptions { new_work_notify: self.work_notify(), force_sealing: self.args.flag_force_sealing, reseal_on_external_tx: ext, reseal_on_own_tx: own, tx_gas_limit: self.args.flag_tx_gas_limit.as_ref().map_or(!U256::zero(), |d| Self::decode_u256(d, "--tx-gas-limit")), tx_queue_size: self.args.flag_tx_queue_size, pending_set: match self.args.flag_relay_set.as_str() { "cheap" => PendingSet::AlwaysQueue, "strict" => PendingSet::AlwaysSealing, "lenient" => PendingSet::SealingOrElseQueue, x => die!("{}: Invalid value for --relay-set option. Use --help for more information.", x) }, reseal_min_period: Duration::from_millis(self.args.flag_reseal_min_period), work_queue_size: self.args.flag_work_queue_size, enable_resubmission: !self.args.flag_remove_solved, } } pub fn author(&self) -> Option
{ self.args.flag_etherbase.as_ref() .or(self.args.flag_author.as_ref()) .map(|d| Address::from_str(clean_0x(d)).unwrap_or_else(|_| { die!("{}: Invalid address for --author. Must be 40 hex characters, with or without the 0x at the beginning.", d) })) } pub fn gas_floor_target(&self) -> U256 { let d = &self.args.flag_gas_floor_target; U256::from_dec_str(d).unwrap_or_else(|_| { die!("{}: Invalid target gas floor given. Must be a decimal unsigned 256-bit number.", d) }) } pub fn gas_ceil_target(&self) -> U256 { let d = &self.args.flag_gas_cap; U256::from_dec_str(d).unwrap_or_else(|_| { die!("{}: Invalid target gas ceiling given. Must be a decimal unsigned 256-bit number.", d) }) } fn to_duration(s: &str) -> Duration { let bad = |_| { die!("{}: Invalid duration given. See parity --help for more information.", s) }; Duration::from_secs(match s { "twice-daily" => 12 * 60 * 60, "half-hourly" => 30 * 60, "1second" | "1 second" | "second" => 1, "1minute" | "1 minute" | "minute" => 60, "hourly" | "1hour" | "1 hour" | "hour" => 60 * 60, "daily" | "1day" | "1 day" | "day" => 24 * 60 * 60, x if x.ends_with("seconds") => FromStr::from_str(&x[0..x.len() - 7]).unwrap_or_else(bad), x if x.ends_with("minutes") => FromStr::from_str(&x[0..x.len() - 7]).unwrap_or_else(bad) * 60, x if x.ends_with("hours") => FromStr::from_str(&x[0..x.len() - 5]).unwrap_or_else(bad) * 60 * 60, x if x.ends_with("days") => FromStr::from_str(&x[0..x.len() - 4]).unwrap_or_else(bad) * 24 * 60 * 60, x => FromStr::from_str(x).unwrap_or_else(bad), }) } pub fn gas_pricer(&self) -> GasPricer { match self.args.flag_gasprice.as_ref() { Some(d) => { GasPricer::Fixed(U256::from_dec_str(d).unwrap_or_else(|_| { die!("{}: Invalid gas price given. Must be a decimal unsigned 256-bit number.", d) })) } _ => { let usd_per_tx: f32 = FromStr::from_str(&self.args.flag_usd_per_tx).unwrap_or_else(|_| { die!("{}: Invalid basic transaction price given in USD. Must be a decimal number.", self.args.flag_usd_per_tx) }); match self.args.flag_usd_per_eth.as_str() { "auto" => { GasPricer::new_calibrated(GasPriceCalibratorOptions { usd_per_tx: usd_per_tx, recalibration_period: Self::to_duration(self.args.flag_price_update_period.as_str()), }) }, x => { let usd_per_eth: f32 = FromStr::from_str(x).unwrap_or_else(|_| die!("{}: Invalid ether price given in USD. Must be a decimal number.", x)); let wei_per_usd: f32 = 1.0e18 / usd_per_eth; let gas_per_tx: f32 = 21000.0; let wei_per_gas: f32 = wei_per_usd * usd_per_tx / gas_per_tx; info!("Using a fixed conversion rate of Ξ1 = {} ({} wei/gas)", White.bold().paint(format!("US${}", usd_per_eth)), Yellow.bold().paint(format!("{}", wei_per_gas))); GasPricer::Fixed(U256::from_dec_str(&format!("{:.0}", wei_per_gas)).unwrap()) } } } } } pub fn extra_data(&self) -> Bytes { match self.args.flag_extradata.as_ref().or(self.args.flag_extra_data.as_ref()) { Some(ref x) if x.len() <= 32 => x.as_bytes().to_owned(), None => version_data(), Some(ref x) => { die!("{}: Extra data must be at most 32 characters.", x); } } } pub fn spec(&self) -> Spec { match self.chain().as_str() { "frontier" | "homestead" | "mainnet" => ethereum::new_frontier(), "homestead-dogmatic" => ethereum::new_frontier_dogmatic(), "morden" | "testnet" => ethereum::new_morden(), "olympic" => ethereum::new_olympic(), f => Spec::load(contents(f).unwrap_or_else(|_| { die!("{}: Couldn't read chain specification file. Sure it exists?", f) }).as_ref()), } } pub fn normalize_enode(e: &str) -> Option { if is_valid_node_url(e) { Some(e.to_owned()) } else { None } } pub fn init_nodes(&self, spec: &Spec) -> Vec { match self.args.flag_bootnodes { Some(ref x) if !x.is_empty() => x.split(',').map(|s| { Self::normalize_enode(s).unwrap_or_else(|| { die!("{}: Invalid node address format given for a boot node.", s) }) }).collect(), Some(_) => Vec::new(), None => spec.nodes().to_owned(), } } pub fn init_reserved_nodes(&self) -> Vec { use std::fs::File; if let Some(ref path) = self.args.flag_reserved_peers { let mut buffer = String::new(); let mut node_file = File::open(path).unwrap_or_else(|e| { die!("Error opening reserved nodes file: {}", e); }); node_file.read_to_string(&mut buffer).expect("Error reading reserved node file"); buffer.lines().map(|s| { Self::normalize_enode(s).unwrap_or_else(|| { die!("{}: Invalid node address format given for a reserved node.", s); }) }).collect() } else { Vec::new() } } pub fn net_addresses(&self) -> (Option, Option) { let port = self.net_port(); let listen_address = Some(SocketAddr::new(IpAddr::from_str("0.0.0.0").unwrap(), port)); let public_address = if self.args.flag_nat.starts_with("extip:") { let host = &self.args.flag_nat[6..]; let host = IpAddr::from_str(host).unwrap_or_else(|_| die!("Invalid host given with `--nat extip:{}`", host)); Some(SocketAddr::new(host, port)) } else { None }; (listen_address, public_address) } pub fn net_settings(&self, spec: &Spec) -> NetworkConfiguration { let mut ret = NetworkConfiguration::new(); ret.nat_enabled = self.args.flag_nat == "any" || self.args.flag_nat == "upnp"; ret.boot_nodes = self.init_nodes(spec); let (listen, public) = self.net_addresses(); ret.listen_address = listen; ret.public_address = public; ret.use_secret = self.args.flag_node_key.as_ref().map(|s| Secret::from_str(s).unwrap_or_else(|_| s.sha3())); ret.discovery_enabled = !self.args.flag_no_discovery && !self.args.flag_nodiscover; ret.ideal_peers = self.max_peers(); let mut net_path = PathBuf::from(&self.path()); net_path.push("network"); ret.config_path = Some(net_path.to_str().unwrap().to_owned()); ret.reserved_nodes = self.init_reserved_nodes(); if self.args.flag_reserved_only { ret.non_reserved_mode = ::util::network::NonReservedPeerMode::Deny; } ret } fn find_best_db(&self, spec: &Spec) -> Option { let mut ret = None; let mut latest_era = None; let jdb_types = [journaldb::Algorithm::Archive, journaldb::Algorithm::EarlyMerge, journaldb::Algorithm::OverlayRecent, journaldb::Algorithm::RefCounted]; for i in jdb_types.into_iter() { let db = journaldb::new(&append_path(&get_db_path(Path::new(&self.path()), *i, spec.genesis_header().hash()), "state"), *i, kvdb::DatabaseConfig::default()); trace!(target: "parity", "Looking for best DB: {} at {:?}", i, db.latest_era()); match (latest_era, db.latest_era()) { (Some(best), Some(this)) if best >= this => {} (_, None) => {} (_, Some(this)) => { latest_era = Some(this); ret = Some(*i); } } } ret } pub fn pruning_algorithm(&self, spec: &Spec) -> journaldb::Algorithm { match self.args.flag_pruning.as_str() { "archive" => journaldb::Algorithm::Archive, "light" => journaldb::Algorithm::EarlyMerge, "fast" => journaldb::Algorithm::OverlayRecent, "basic" => journaldb::Algorithm::RefCounted, "auto" => self.find_best_db(spec).unwrap_or(journaldb::Algorithm::OverlayRecent), _ => { die!("Invalid pruning method given."); } } } pub fn client_config(&self, spec: &Spec) -> ClientConfig { let mut client_config = ClientConfig::default(); client_config.mode = self.mode(); match self.args.flag_cache { Some(mb) => { client_config.blockchain.max_cache_size = mb * 1024 * 1024; client_config.blockchain.pref_cache_size = client_config.blockchain.max_cache_size * 3 / 4; } None => { client_config.blockchain.pref_cache_size = self.args.flag_cache_pref_size; client_config.blockchain.max_cache_size = self.args.flag_cache_max_size; } } // forced blockchain (blocks + extras) db cache size if provided client_config.blockchain.db_cache_size = self.args.flag_db_cache_size.and_then(|cs| Some(cs / 2)); client_config.tracing.enabled = match self.args.flag_tracing.as_str() { "auto" => Switch::Auto, "on" => Switch::On, "off" => Switch::Off, _ => { die!("Invalid tracing method given!") } }; // forced trace db cache size if provided client_config.tracing.db_cache_size = self.args.flag_db_cache_size.and_then(|cs| Some(cs / 4)); client_config.pruning = self.pruning_algorithm(spec); if self.args.flag_fat_db { if let journaldb::Algorithm::Archive = client_config.pruning { client_config.trie_spec = TrieSpec::Fat; } else { die!("Fatdb is not supported. Please re-run with --pruning=archive") } } // forced state db cache size if provided client_config.db_cache_size = self.args.flag_db_cache_size.and_then(|cs| Some(cs / 4)); // compaction profile client_config.db_compaction = match self.args.flag_db_compaction.as_str() { "ssd" => DatabaseCompactionProfile::Default, "hdd" => DatabaseCompactionProfile::HDD, _ => { die!("Invalid compaction profile given (--db-compaction argument), expected hdd/ssd (default)."); } }; if self.args.flag_jitvm { client_config.vm_type = VMType::jit().unwrap_or_else(|| die!("Parity is built without the JIT EVM.")) } trace!(target: "parity", "Using pruning strategy of {}", client_config.pruning); client_config.name = self.args.flag_identity.clone(); client_config.queue.max_mem_use = self.args.flag_queue_max_size; client_config } pub fn sync_config(&self, spec: &Spec) -> SyncConfig { let mut sync_config = SyncConfig::default(); sync_config.network_id = self.args.flag_network_id.as_ref().or(self.args.flag_networkid.as_ref()).map_or(spec.network_id(), |id| { U256::from_str(id).unwrap_or_else(|_| die!("{}: Invalid index given with --network-id/--networkid", id)) }); sync_config } pub fn account_service(&self) -> AccountProvider { use ethcore::ethstore::{import_accounts, EthStore}; use ethcore::ethstore::dir::{GethDirectory, DirectoryType, DiskDirectory}; // Secret Store let passwords = self.args.flag_password.iter().flat_map(|filename| { BufReader::new(&File::open(filename).unwrap_or_else(|_| die!("{} Unable to read password file. Ensure it exists and permissions are correct.", filename))) .lines() .map(|l| l.unwrap()) .collect::>() .into_iter() }).collect::>(); if !self.args.flag_no_import_keys { let dir_type = if self.args.flag_testnet { DirectoryType::Testnet } else { DirectoryType::Main }; let from = GethDirectory::open(dir_type); let to = DiskDirectory::create(self.keys_path()).unwrap(); // ignore error, cause geth may not exist let _ = import_accounts(&from, &to); } let dir = Box::new(DiskDirectory::create(self.keys_path()).unwrap()); let iterations = self.keys_iterations(); let account_service = AccountProvider::new(Box::new(EthStore::open_with_iterations(dir, iterations).unwrap())); if let Some(ref unlocks) = self.args.flag_unlock { for d in unlocks.split(',') { let a = Address::from_str(clean_0x(d)).unwrap_or_else(|_| { die!("{}: Invalid address for --unlock. Must be 40 hex characters, without the 0x at the beginning.", d) }); if passwords.iter().find(|p| account_service.unlock_account_permanently(a, (*p).clone()).is_ok()).is_none() { die!("No password given to unlock account {}. Pass the password using `--password`.", a); } } } account_service } pub fn rpc_apis(&self) -> String { self.args.flag_rpcapi.clone().unwrap_or(self.args.flag_jsonrpc_apis.clone()) } pub fn rpc_cors(&self) -> Vec { let cors = self.args.flag_jsonrpc_cors.clone().or(self.args.flag_rpccorsdomain.clone()); cors.map_or_else(Vec::new, |c| c.split(',').map(|s| s.to_owned()).collect()) } fn geth_ipc_path(&self) -> String { if cfg!(windows) { r"\\.\pipe\geth.ipc".to_owned() } else { match self.args.flag_testnet { true => path::ethereum::with_testnet("geth.ipc"), false => path::ethereum::with_default("geth.ipc"), }.to_str().unwrap().to_owned() } } pub fn keys_iterations(&self) -> u32 { self.args.flag_keys_iterations } pub fn ipc_settings(&self) -> IpcConfiguration { IpcConfiguration { enabled: !(self.args.flag_ipcdisable || self.args.flag_ipc_off || self.args.flag_no_ipc), socket_addr: self.ipc_path(), apis: self.args.flag_ipcapi.clone().unwrap_or(self.args.flag_ipc_apis.clone()), } } pub fn network_settings(&self) -> NetworkSettings { if self.args.flag_jsonrpc { println!("WARNING: Flag -j/--json-rpc is deprecated. JSON-RPC is now on by default. Ignoring."); } NetworkSettings { name: self.args.flag_identity.clone(), chain: self.chain(), max_peers: self.max_peers(), network_port: self.net_port(), rpc_enabled: !self.args.flag_jsonrpc_off && !self.args.flag_no_jsonrpc, rpc_interface: self.args.flag_rpcaddr.clone().unwrap_or(self.args.flag_jsonrpc_interface.clone()), rpc_port: self.args.flag_rpcport.unwrap_or(self.args.flag_jsonrpc_port), } } pub fn directories(&self) -> Directories { let db_path = Configuration::replace_home( self.args.flag_datadir.as_ref().unwrap_or(&self.args.flag_db_path)); ::std::fs::create_dir_all(&db_path).unwrap_or_else(|e| die_with_io_error("main", e)); let keys_path = Configuration::replace_home( if self.args.flag_testnet { "$HOME/.parity/testnet_keys" } else { &self.args.flag_keys_path } ); ::std::fs::create_dir_all(&keys_path).unwrap_or_else(|e| die_with_io_error("main", e)); let dapps_path = Configuration::replace_home(&self.args.flag_dapps_path); ::std::fs::create_dir_all(&dapps_path).unwrap_or_else(|e| die_with_io_error("main", e)); let signer_path = Configuration::replace_home(&self.args.flag_signer_path); ::std::fs::create_dir_all(&signer_path).unwrap_or_else(|e| die_with_io_error("main", e)); if self.args.flag_geth { let geth_path = path::ethereum::default(); ::std::fs::create_dir_all(geth_path.as_path()).unwrap_or_else( |e| die!("Error while attempting to create '{}' for geth mode: {}", &geth_path.to_str().unwrap(), e)); } Directories { keys: keys_path, db: db_path, dapps: dapps_path, signer: signer_path, } } pub fn keys_path(&self) -> String { self.directories().keys } pub fn path(&self) -> String { self.directories().db } fn replace_home(arg: &str) -> String { arg.replace("$HOME", env::home_dir().unwrap().to_str().unwrap()) } fn ipc_path(&self) -> String { if self.args.flag_geth { self.geth_ipc_path() } else if cfg!(windows) { r"\\.\pipe\parity.jsonrpc".to_owned() } else { Configuration::replace_home(&self.args.flag_ipcpath.clone().unwrap_or(self.args.flag_ipc_path.clone())) } } pub fn have_color(&self) -> bool { !self.args.flag_no_color && !cfg!(windows) } pub fn signer_port(&self) -> Option { if !self.signer_enabled() { None } else { Some(self.args.flag_signer_port) } } pub fn rpc_interface(&self) -> String { match self.network_settings().rpc_interface.as_str() { "all" => "0.0.0.0", "local" => "127.0.0.1", x => x, }.into() } pub fn dapps_interface(&self) -> String { match self.args.flag_dapps_interface.as_str() { "all" => "0.0.0.0", "local" => "127.0.0.1", x => x, }.into() } pub fn dapps_enabled(&self) -> bool { !self.args.flag_dapps_off && !self.args.flag_no_dapps && cfg!(feature = "dapps") } pub fn signer_enabled(&self) -> bool { (self.args.flag_unlock.is_none() && !self.args.flag_no_signer) || self.args.flag_force_signer } } #[cfg(test)] mod tests { use super::*; use cli::USAGE; use docopt::Docopt; use util::network_settings::NetworkSettings; fn parse(args: &[&str]) -> Configuration { Configuration { args: Docopt::new(USAGE).unwrap().argv(args).decode().unwrap(), } } #[test] fn should_parse_network_settings() { // given // when let conf = parse(&["parity", "--testnet", "--identity", "testname"]); // then assert_eq!(conf.network_settings(), NetworkSettings { name: "testname".to_owned(), chain: "morden".to_owned(), max_peers: 25, network_port: 30303, rpc_enabled: true, rpc_interface: "local".to_owned(), rpc_port: 8545, }); } #[test] fn should_parse_rpc_settings_with_geth_compatiblity() { // given fn assert(conf: Configuration) { let net = conf.network_settings(); assert_eq!(net.rpc_enabled, true); assert_eq!(net.rpc_interface, "all".to_owned()); assert_eq!(net.rpc_port, 8000); assert_eq!(conf.rpc_cors(), vec!["*".to_owned()]); assert_eq!(conf.rpc_apis(), "web3,eth".to_owned()); } // when let conf1 = parse(&["parity", "-j", "--jsonrpc-port", "8000", "--jsonrpc-interface", "all", "--jsonrpc-cors", "*", "--jsonrpc-apis", "web3,eth" ]); let conf2 = parse(&["parity", "--rpc", "--rpcport", "8000", "--rpcaddr", "all", "--rpccorsdomain", "*", "--rpcapi", "web3,eth" ]); // then assert(conf1); assert(conf2); } }