diff --git a/Cargo.lock b/Cargo.lock index 1ff201d4026..d49f0ccdacd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -178,6 +178,12 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" +[[package]] +name = "assertables" +version = "7.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c24e9d990669fbd16806bff449e4ac644fd9b1fca014760087732fe4102f131" + [[package]] name = "async-stream" version = "0.3.5" @@ -575,23 +581,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "clap" -version = "3.2.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" -dependencies = [ - "atty", - "bitflags 1.3.2", - "clap_derive 3.2.25", - "clap_lex 0.2.4", - "indexmap 1.9.3", - "once_cell", - "strsim", - "termcolor", - "textwrap", -] - [[package]] name = "clap" version = "4.4.11" @@ -599,7 +588,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfaff671f6b22ca62406885ece523383b9b64022e341e53e009a62ebc47a45f2" dependencies = [ "clap_builder", - "clap_derive 4.4.7", + "clap_derive", ] [[package]] @@ -610,23 +599,10 @@ checksum = "a216b506622bb1d316cd51328dce24e07bdff4a6128a47c7e7fad11878d5adbb" dependencies = [ "anstream", "anstyle", - "clap_lex 0.6.0", + "clap_lex", "strsim", ] -[[package]] -name = "clap_derive" -version = "3.2.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae6371b8bdc8b7d3959e9cf7b22d4435ef3e79e138688421ec654acf8c81b008" -dependencies = [ - "heck", - "proc-macro-error", - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "clap_derive" version = "4.4.7" @@ -639,15 +615,6 @@ dependencies = [ "syn 2.0.41", ] -[[package]] -name = "clap_lex" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" -dependencies = [ - "os_str_bytes", -] - [[package]] name = "clap_lex" version = "0.6.0" @@ -765,9 +732,9 @@ dependencies = [ [[package]] name = "const-oid" -version = "0.9.5" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28c122c3980598d243d63d9a704629a2d748d101f278052ff068be5a4423ab6f" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" [[package]] name = "core-foundation" @@ -939,7 +906,7 @@ dependencies = [ "anes", "cast", "ciborium", - "clap 4.4.11", + "clap", "criterion-plot", "is-terminal", "itertools 0.10.5", @@ -2610,8 +2577,11 @@ dependencies = [ name = "iroha" version = "2.0.0-pre-rc.20" dependencies = [ + "assertables", + "clap", "color-eyre", "eyre", + "futures", "iroha_config", "iroha_core", "iroha_crypto", @@ -2626,8 +2596,11 @@ dependencies = [ "iroha_wasm_builder", "once_cell", "owo-colors", + "path-absolutize", + "serde_json", "serial_test", "supports-color 2.1.0", + "tempfile", "thread-local-panic-hook", "tokio", "tracing", @@ -2678,11 +2651,12 @@ dependencies = [ name = "iroha_client_cli" version = "2.0.0-pre-rc.20" dependencies = [ - "clap 3.2.25", + "clap", "color-eyre", "dialoguer", "erased-serde", "iroha_client", + "iroha_config_base", "iroha_primitives", "json5", "once_cell", @@ -2702,10 +2676,10 @@ dependencies = [ "iroha_config_base", "iroha_crypto", "iroha_data_model", + "iroha_genesis", "iroha_primitives", "json5", "once_cell", - "path-absolutize", "proptest", "serde", "serde_json", @@ -2978,11 +2952,8 @@ version = "2.0.0-pre-rc.20" dependencies = [ "derive_more", "eyre", - "iroha_config", "iroha_crypto", "iroha_data_model", - "iroha_logger", - "iroha_primitives", "iroha_schema", "once_cell", "serde", @@ -3161,7 +3132,7 @@ version = "2.0.0-pre-rc.20" name = "iroha_swarm" version = "2.0.0-pre-rc.20" dependencies = [ - "clap 4.4.11", + "clap", "color-eyre", "derive_more", "expect-test", @@ -3321,7 +3292,7 @@ dependencies = [ name = "iroha_wasm_builder_cli" version = "2.0.0-pre-rc.20" dependencies = [ - "clap 4.4.11", + "clap", "color-eyre", "iroha_wasm_builder", "owo-colors", @@ -3454,7 +3425,7 @@ dependencies = [ name = "kagami" version = "2.0.0-pre-rc.20" dependencies = [ - "clap 4.4.11", + "clap", "color-eyre", "derive_more", "iroha_config", @@ -3481,7 +3452,7 @@ dependencies = [ name = "kura_inspector" version = "2.0.0-pre-rc.20" dependencies = [ - "clap 4.4.11", + "clap", "iroha_core", "iroha_data_model", "iroha_version", @@ -3924,12 +3895,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "os_str_bytes" -version = "6.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1" - [[package]] name = "overload" version = "0.1.1" @@ -3975,7 +3940,7 @@ dependencies = [ name = "parity_scale_decoder" version = "2.0.0-pre-rc.20" dependencies = [ - "clap 4.4.11", + "clap", "colored", "eyre", "iroha_crypto", @@ -5360,12 +5325,6 @@ dependencies = [ "unique_port", ] -[[package]] -name = "textwrap" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" - [[package]] name = "thiserror" version = "1.0.50" @@ -5888,9 +5847,9 @@ dependencies = [ [[package]] name = "unsafe-libyaml" -version = "0.2.9" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f28467d3e1d3c6586d8f25fa243f544f5800fec42d97032474e17222c2b75cfa" +checksum = "ab4c90930b95a82d00dc9e9ac071b4991924390d46cbd0dfe566148667605e4b" [[package]] name = "untrusted" diff --git a/README.md b/README.md index d93882849bc..98f41678038 100644 --- a/README.md +++ b/README.md @@ -157,7 +157,18 @@ A brief overview on how to configure and maintain an Iroha instance: ### Configuration -**Note:** this section is under development. You can track it in the [issue](https://github.com/hyperledger/iroha-2-docs/issues/392). +There is a set of configuration parameters that could be passed either through a configuration file or environment variables. + +```shell +# look for `config.json` or `config.json5` (won't fail if files are not found) +iroha + +# Override default config path through CLI or ENV +iroha --config /path/to/config.json +IROHA_CONFIG=/path/to/config.json iroha +``` + +**Note:** detailed configuration reference is [work in progress](https://github.com/hyperledger/iroha-2-docs/issues/392). ### Endpoints @@ -198,9 +209,9 @@ prometheus --config.file=configs/prometheus.yml ### Storage -The blocks are written to the `blocks` sub-folder, which is created automatically by Iroha in the working directory of the peer. Additionally, if specified, the logging file must also be stored in a user-specified directory. +Iroha stores blocks and snapshots in the `storage` directory, which is created automatically by Iroha in the working directory of the peer. If `kura.block_store_path` is specified in the config file, it overrides the default one and is resolved relative to the config file location. -No additional storage is necessary. +**Note:** detailed configuration reference is [work in progress](https://github.com/hyperledger/iroha-2-docs/issues/392). ### Scalability diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 0ca76872233..8507b7d77ef 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -55,6 +55,7 @@ iroha_torii = { workspace = true } iroha_genesis = { workspace = true } iroha_wasm_builder = { workspace = true } +clap = { workspace = true, features = ["derive", "env", "string"] } color-eyre = { workspace = true } eyre = { workspace = true } tracing = { workspace = true } @@ -67,6 +68,11 @@ thread-local-panic-hook = { version = "0.1.0", optional = true } [dev-dependencies] serial_test = "2.0.0" +tempfile = { workspace = true } +serde_json = { workspace = true } +futures = { workspace = true } +path-absolutize = { workspace = true } +assertables = "7" [build-dependencies] iroha_wasm_builder = { workspace = true } diff --git a/cli/src/lib.rs b/cli/src/lib.rs index 53ee9fe3306..b63e0880efd 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -6,13 +6,14 @@ //! should be constructed externally: (see `main.rs`). #[cfg(debug_assertions)] use core::sync::atomic::{AtomicBool, Ordering}; -use std::sync::Arc; +use std::{path::PathBuf, sync::Arc}; use color_eyre::eyre::{eyre, Result, WrapErr}; use iroha_config::{ base::proxy::{LoadFromDisk, LoadFromEnv, Override}, + genesis::ParsedConfiguration as ParsedGenesisConfiguration, iroha::{Configuration, ConfigurationProxy}, - path::Path as ConfigPath, + path::Path, telemetry::Configuration as TelemetryConfiguration, }; use iroha_core::{ @@ -40,58 +41,8 @@ use tokio::{ task, }; +// FIXME: move from CLI pub mod samples; -pub mod style; - -/// Arguments for Iroha2. Configuration for arguments is parsed from -/// environment variables and then the appropriate object is -/// constructed. -#[derive(Debug)] -pub struct Arguments { - /// Set this flag on the peer that should submit genesis on the network initial start. - pub submit_genesis: bool, - /// Set custom genesis file path. `None` if `submit_genesis` set to `false`. - pub genesis_path: Option, - /// Set custom config file path. - pub config_path: ConfigPath, -} - -/// Default configuration path -static CONFIGURATION_PATH: once_cell::sync::Lazy<&'static std::path::Path> = - once_cell::sync::Lazy::new(|| std::path::Path::new("config")); - -/// Default genesis path -static GENESIS_PATH: once_cell::sync::Lazy<&'static std::path::Path> = - once_cell::sync::Lazy::new(|| std::path::Path::new("genesis")); - -impl Default for Arguments { - fn default() -> Self { - Self { - submit_genesis: false, - genesis_path: Some(ConfigPath::default(&GENESIS_PATH)), - config_path: ConfigPath::default(&CONFIGURATION_PATH), - } - } -} - -/// Reflects user decision (or its absence) about ANSI colored output -#[derive(Copy, Clone, Debug)] -pub enum TerminalColorsArg { - /// Coloring should be decided automatically - Default, - /// User explicitly specified the value - UserSet(bool), -} - -impl TerminalColorsArg { - /// Transforms the enumeration into flag - pub fn evaluate(self) -> bool { - match self { - Self::Default => supports_color::on(supports_color::Stream::Stdout).is_some(), - Self::UserSet(x) => x, - } - } -} /// Iroha is an /// [Orchestrator](https://en.wikipedia.org/wiki/Orchestration_%28computing%29) @@ -99,9 +50,8 @@ impl TerminalColorsArg { /// and queries processing, work of consensus and storage. /// /// # Usage -/// Construct and then `start` or `start_as_task`. If you experience -/// an immediate shutdown after constructing Iroha, then you probably -/// forgot this step. +/// Construct and then use [`Iroha::start()`] or [`Iroha::start_as_task()`]. If you experience +/// an immediate shutdown after constructing Iroha, then you probably forgot this step. #[must_use = "run `.start().await?` to not immediately stop Iroha"] pub struct Iroha { /// Actor responsible for the configuration @@ -235,17 +185,20 @@ impl Iroha { })); } - /// Create Iroha with specified broker, config, and genesis. + /// Create new Iroha instance. /// /// # Errors /// - Reading telemetry configs - /// - telemetry setup - /// - Initialization of [`Sumeragi`] + /// - Telemetry setup + /// - Initialization of [`Sumeragi`] and [`Kura`] + /// + /// # Side Effects + /// - Sets global panic hook #[allow(clippy::too_many_lines)] #[iroha_logger::log(name = "init", skip_all)] // This is actually easier to understand as a linear sequence of init statements. - pub async fn with_genesis( - genesis: Option, + pub async fn new( config: Configuration, + genesis: Option, logger: LoggerHandle, ) -> Result { let listen_addr = config.torii.p2p_addr.clone(); @@ -255,7 +208,7 @@ impl Iroha { let (events_sender, _) = broadcast::channel(10000); let world = World::with( - [genesis_domain(config.genesis.account_public_key.clone())], + [genesis_domain(config.genesis.public_key.clone())], config.sumeragi.trusted_peers.peers.clone(), ); @@ -530,56 +483,261 @@ fn genesis_domain(public_key: PublicKey) -> Domain { domain } -/// Combine configuration proxies from several locations, preferring `ENV` vars over config file +macro_rules! mutate_nested_option { + ($obj:expr, self, $func:expr) => { + $obj.as_mut().map($func) + }; + ($obj:expr, $field:ident, $func:expr) => { + $obj.$field.as_mut().map($func) + }; + ($obj:expr, [$field:ident, $($rest:tt)+], $func:expr) => { + $obj.$field.as_mut().map(|x| { + mutate_nested_option!(x, [$($rest)+], $func) + }) + }; + ($obj:tt, [$field:tt], $func:expr) => { + mutate_nested_option!($obj, $field, $func) + }; +} + +/// Read and parse Iroha configuration and genesis block. +/// +/// The pipeline of configuration reading is as follows: +/// +/// 1. Construct a layer with default values +/// 2. If [`Path`] resolves, construct a layer from the file and merge it into the previous one +/// 3. Construct a layer from ENV vars and merge it into the previous one +/// 4. Check whether the final layer contains the complete configuration +/// +/// After reading it, this function ensures validity of genesis configuration and constructs the +/// [`GenesisNetwork`] according to it. /// /// # Errors -/// - if config fails to build -pub fn combine_configs(args: &Arguments) -> color_eyre::eyre::Result { - args.config_path - .first_existing_path() - .map_or_else( - || { - eprintln!("Configuration file not found. Using environment variables as fallback."); - ConfigurationProxy::default() - }, - |path| { - let path_proxy = ConfigurationProxy::from_path(&path.as_path()); - // Override the default to ensure that the variables - // not specified in the config file don't have to be - // explicitly specified in the env. - ConfigurationProxy::default().override_with(path_proxy) - }, - ) - .override_with( - ConfigurationProxy::from_std_env() - .wrap_err("Failed to build configuration from env")?, - ) +/// - If provided user configuration is invalid or incomplete +/// - If genesis config is invalid +pub fn read_config( + path: &Path, + submit_genesis: bool, +) -> Result<(Configuration, Option)> { + let config = ConfigurationProxy::default(); + + let config = if let Some(actual_config_path) = path + .try_resolve() + .wrap_err("Failed to resolve configuration file")? + { + let mut cfg = config.override_with(ConfigurationProxy::from_path(&*actual_config_path)); + let config_dir = actual_config_path + .parent() + .expect("If config file was read, than it should has a parent. It is a bug."); + + // careful here: `genesis.file` might be a path relative to the config file. + // we need to resolve it before proceeding + // TODO: move this logic into `iroha_config` + // https://github.com/hyperledger/iroha/issues/4161 + let join_to_config_dir = |x: &mut PathBuf| { + *x = config_dir.join(&x); + }; + mutate_nested_option!(cfg, [genesis, file, self], join_to_config_dir); + mutate_nested_option!(cfg, [snapshot, dir_path], join_to_config_dir); + mutate_nested_option!(cfg, [kura, block_store_path], join_to_config_dir); + mutate_nested_option!(cfg, [telemetry, file, self], join_to_config_dir); + + cfg + } else { + config + }; + + // it is not chained to the previous expressions so that config proxy from env is evaluated + // after reading a file + let config = config.override_with( + ConfigurationProxy::from_std_env().wrap_err("Failed to build configuration from env")?, + ); + + let config = config .build() - .map_err(Into::into) + .wrap_err("Failed to finalize configuration")?; + + // TODO: move validation logic below to `iroha_config` + + if !submit_genesis && config.sumeragi.trusted_peers.peers.len() < 2 { + return Err(eyre!("\ + The network consists from this one peer only (`sumeragi.trusted_peers` is less than 2). \ + Since `--submit-genesis` is not set, there is no way to receive the genesis block. \ + Either provide the genesis by setting `--submit-genesis` argument, `genesis.private_key`, \ + and `genesis.file` configuration parameters, or increase the number of trusted peers in \ + the network using `sumeragi.trusted_peers` configuration parameter. + ")); + } + + let genesis = if let ParsedGenesisConfiguration::Full { + key_pair, + raw_block, + } = config + .genesis + .clone() + .parse(submit_genesis) + .wrap_err("Invalid genesis configuration")? + { + Some( + GenesisNetwork::new(raw_block, &key_pair) + .wrap_err("Failed to construct the genesis")?, + ) + } else { + None + }; + + Ok((config, genesis)) } -#[cfg(not(feature = "test-network"))] #[cfg(test)] mod tests { - use std::{iter::repeat, panic, thread}; - - use futures::future::join_all; - use serial_test::serial; + use iroha_genesis::RawGenesisBlockBuilder; use super::*; - #[tokio::test] - #[serial] - async fn iroha_should_notify_on_panic() { - let notify = Arc::new(Notify::new()); - let hook = panic::take_hook(); - ::prepare_panic_hook(Arc::clone(¬ify)); - let waiters: Vec<_> = repeat(()).take(10).map(|_| Arc::clone(¬ify)).collect(); - let handles: Vec<_> = waiters.iter().map(|waiter| waiter.notified()).collect(); - thread::spawn(move || { - panic!("Test panic"); - }); - join_all(handles).await; - panic::set_hook(hook); + #[cfg(not(feature = "test-network"))] + mod no_test_network { + use std::{iter::repeat, panic, thread}; + + use futures::future::join_all; + use serial_test::serial; + + use super::*; + + #[tokio::test] + #[serial] + async fn iroha_should_notify_on_panic() { + let notify = Arc::new(Notify::new()); + let hook = panic::take_hook(); + ::prepare_panic_hook(Arc::clone(¬ify)); + let waiters: Vec<_> = repeat(()).take(10).map(|_| Arc::clone(¬ify)).collect(); + let handles: Vec<_> = waiters.iter().map(|waiter| waiter.notified()).collect(); + thread::spawn(move || { + panic!("Test panic"); + }); + join_all(handles).await; + panic::set_hook(hook); + } + } + + mod config_integration { + use assertables::{assert_contains, assert_contains_as_result}; + use iroha_crypto::KeyPair; + use iroha_genesis::{ExecutorMode, ExecutorPath}; + use iroha_primitives::addr::socket_addr; + use path_absolutize::Absolutize as _; + + use super::*; + + fn config_factory() -> Result { + let mut base = ConfigurationProxy::default(); + + let key_pair = KeyPair::generate()?; + + base.public_key = Some(key_pair.public_key().clone()); + base.private_key = Some(key_pair.private_key().clone()); + + let torii = base.torii.as_mut().unwrap(); + torii.p2p_addr = Some(socket_addr!(127.0.0.1:1337)); + torii.api_url = Some(socket_addr!(127.0.0.1:1337)); + + let genesis = base.genesis.as_mut().unwrap(); + genesis.private_key = Some(Some(key_pair.private_key().clone())); + genesis.public_key = Some(key_pair.public_key().clone()); + + Ok(base) + } + + #[test] + fn relative_file_paths_resolution() -> Result<()> { + // Given + + let genesis = RawGenesisBlockBuilder::default() + .executor(ExecutorMode::Path(ExecutorPath("./executor.wasm".into()))) + .build(); + + let config = { + let mut cfg = config_factory()?; + cfg.genesis.as_mut().unwrap().file = Some(Some("./genesis/gen.json".into())); + cfg.kura.as_mut().unwrap().block_store_path = Some("../storage".into()); + cfg.snapshot.as_mut().unwrap().dir_path = Some("../snapshots".into()); + cfg.telemetry.as_mut().unwrap().file = Some(Some("../logs/telemetry".into())); + cfg + }; + + let dir = tempfile::tempdir()?; + let genesis_path = dir.path().join("config/genesis/gen.json"); + let executor_path = dir.path().join("config/genesis/executor.wasm"); + let config_path = dir.path().join("config/config.json5"); + std::fs::create_dir(dir.path().join("config"))?; + std::fs::create_dir(dir.path().join("config/genesis"))?; + std::fs::write(config_path, serde_json::to_string(&config)?)?; + std::fs::write(genesis_path, serde_json::to_string(&genesis)?)?; + std::fs::write(executor_path, "")?; + + let config_path = Path::default(dir.path().join("config/config")); + + // When + + let (config, genesis) = read_config(&config_path, true)?; + + // Then + + // No need to check whether genesis.file is resolved - if not, genesis wouldn't be read + assert!(genesis.is_some()); + + assert_eq!( + config.kura.block_store_path.absolutize()?, + dir.path().join("storage") + ); + assert_eq!( + config.snapshot.dir_path.absolutize()?, + dir.path().join("snapshots") + ); + assert_eq!( + config.telemetry.file.expect("Should be set").absolutize()?, + dir.path().join("logs/telemetry") + ); + + Ok(()) + } + + #[test] + fn fails_with_no_trusted_peers_and_submit_role() -> Result<()> { + // Given + + let genesis = RawGenesisBlockBuilder::default() + .executor(ExecutorMode::Path(ExecutorPath("./executor.wasm".into()))) + .build(); + + let config = { + let mut cfg = config_factory()?; + cfg.genesis.as_mut().unwrap().file = Some(Some("./genesis.json".into())); + cfg + }; + + let dir = tempfile::tempdir()?; + std::fs::write( + dir.path().join("config.json"), + serde_json::to_string(&config)?, + )?; + std::fs::write( + dir.path().join("genesis.json"), + serde_json::to_string(&genesis)?, + )?; + std::fs::write(dir.path().join("executor.wasm"), "")?; + let config_path = Path::user_provided(dir.path().join("config.json"))?; + + // When & Then + + let report = read_config(&config_path, false).unwrap_err(); + + assert_contains!( + format!("{report}"), + "The network consists from this one peer only" + ); + + Ok(()) + } } } diff --git a/cli/src/main.rs b/cli/src/main.rs index 16629ea7ea7..14ef7a587d5 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -1,235 +1,159 @@ //! Iroha peer command-line interface. use std::env; -use color_eyre::eyre::WrapErr as _; -use iroha::{style::Styling, TerminalColorsArg}; -use iroha_config::path::Path as ConfigPath; -use iroha_genesis::{GenesisNetwork, RawGenesisBlock}; -use owo_colors::OwoColorize as _; - -const HELP_ARG: [&str; 2] = ["--help", "-h"]; -const SUBMIT_ARG: [&str; 2] = ["--submit-genesis", "-s"]; -const VERSION_ARG: [&str; 2] = ["--version", "-V"]; -const TERMINAL_COLORS_ARG: &str = "--terminal-colors"; -const NO_TERMINAL_COLORS_ARG: &str = "--no-terminal-colors"; - -const REQUIRED_ENV_VARS: [(&str, &str); 7] = [ - ("IROHA_TORII", "Torii (gateway) endpoint configuration"), - ( - "IROHA_SUMERAGI", - "Sumeragi (emperor) consensus configuration", - ), - ( - "IROHA_KURA", - "Kura (storage). Configuration of block storage ", - ), - ("IROHA_BLOCK_SYNC", "Block synchronisation configuration"), - ("IROHA_PUBLIC_KEY", "Peer public key"), - ("IROHA_PRIVATE_KEY", "Peer private key"), - ("IROHA_GENESIS", "Genesis block configuration"), -]; +use clap::Parser; +use color_eyre::eyre::Result; +use iroha_config::path::Path; -#[tokio::main] -/// To make `Iroha` peer work all actors should be started first. -/// After that moment it you can start it with listening to torii events. -/// -/// # Side effect -/// - Prints welcome message in the log -/// -/// # Errors -/// - Reading genesis from disk -/// - Reading config file -/// - Reading config from `env` -/// - Missing required fields in combined configuration -/// - Telemetry setup -/// - [`Sumeragi`] init -async fn main() -> Result<(), color_eyre::Report> { - let mut args = iroha::Arguments::default(); - - let terminal_colors = env::var("TERMINAL_COLORS") - .ok() - .map(|s| !s.as_str().parse().unwrap_or(true)) - .or_else(|| { - if env::args().any(|a| a == TERMINAL_COLORS_ARG) { - Some(true) - } else if env::args().any(|a| a == NO_TERMINAL_COLORS_ARG) { - Some(false) - } else { - None - } - }) - .map_or(TerminalColorsArg::Default, TerminalColorsArg::UserSet) - .evaluate(); - - if terminal_colors { - color_eyre::install()?; - } +const DEFAULT_CONFIG_PATH: &str = "config"; - let styling = Styling::new(terminal_colors); +fn is_colouring_supported() -> bool { + supports_color::on(supports_color::Stream::Stdout).is_some() +} - if env::args().any(|a| HELP_ARG.contains(&a.as_str())) { - print_help(&styling)?; - return Ok(()); - } +fn default_terminal_colors_str() -> clap::builder::OsStr { + is_colouring_supported().to_string().into() +} - if env::args().any(|a| VERSION_ARG.contains(&a.as_str())) { - print_version(&styling); - return Ok(()); - } +/// Iroha peer Command-Line Interface. +#[derive(Parser, Debug)] +#[command(name = "iroha", version = concat!("version=", env!("CARGO_PKG_VERSION"), " git_commit_sha=", env!("VERGEN_GIT_SHA")), author)] +struct Args { + /// Path to the configuration file, defaults to `config.json`/`config.json5` + /// + /// Supported extensions are `.json` and `.json5`. By default, Iroha looks for a + /// `config` file with one of the supported extensions in the current working directory. + /// If the default config file is not found, Iroha will rely on default values and environment + /// variables. However, if the config path is set explicitly with this argument and the file + /// is not found, Iroha will exit with an error. + #[arg( + long, + short, + env("IROHA_CONFIG"), + value_name("PATH"), + value_parser(Path::user_provided_str), + value_hint(clap::ValueHint::FilePath) + )] + config: Option, + /// Whether to enable ANSI colored output or not + /// + /// By default, Iroha determines whether the terminal supports colors or not. + /// + /// In order to disable this flag explicitly, pass `--terminal-colors=false`. + #[arg( + long, + env, + default_missing_value("true"), + default_value(default_terminal_colors_str()), + action(clap::ArgAction::Set), + require_equals(true), + num_args(0..=1), + )] + terminal_colors: bool, + /// Whether the current peer should submit the genesis block or not + /// + /// Only one peer in the network should submit the genesis block. + /// + /// This argument must be set alongside with `genesis.file` and `genesis.private_key` + /// configuration options. If not, Iroha will exit with an error. + /// + /// In case when the network consists only of this one peer, i.e. the amount of trusted + /// peers in the configuration (`sumeragi.trusted_peers`) is less than 2, this peer must + /// submit the genesis, since there are no other peers who can provide it. In this case, Iroha + /// will exit with an error if `--submit-genesis` is not set. + #[arg(long)] + submit_genesis: bool, +} - if env::args().any(|a| SUBMIT_ARG.contains(&a.as_str())) { - args.submit_genesis = true; - if let Ok(genesis_path) = env::var("IROHA2_GENESIS_PATH") { - args.genesis_path = Some( - ConfigPath::user_provided(&genesis_path) - .wrap_err_with(|| "Required, because `--submit-genesis` was specified.") - .wrap_err_with(|| format!("Could not read `{genesis_path}`"))?, - ); - } - } else { - args.genesis_path = None; - } +#[tokio::main] +async fn main() -> Result<()> { + let args = Args::parse(); - for arg in env::args().skip(1) { - if !arg.is_empty() - && !([HELP_ARG, SUBMIT_ARG] - .iter() - .any(|group| group.contains(&arg.as_str()))) - { - print_help(&styling)?; - eyre::bail!( - "Unrecognised command-line flag `{}`", - arg.style(styling.negative) - ); - } + if args.terminal_colors { + color_eyre::install()?; } - if let Ok(config_path) = env::var("IROHA2_CONFIG_PATH") { - args.config_path = ConfigPath::user_provided(&config_path) - .wrap_err_with(|| format!("Failed to parse `{config_path}` as configuration path"))?; - } - if !args.config_path.exists() { - // Require all the fields defined in default `config.json` - // to be specified as env vars with their respective prefixes - - // TODO: Consider moving these into the - // `iroha::combine_configs` and dependent functions. - for var_name in REQUIRED_ENV_VARS { - // Rather than short circuit and require the person to fix - // the missing env vars one by one, print out the whole - // list of missing environment variables. - let _ = env::var(var_name.0).map_err(|e| { - println!( - "{}: {}", - var_name.0.style(styling.highlight), - e.style(styling.negative) - ); - }); - } - } + let config_path = args + .config + .unwrap_or_else(|| Path::default(DEFAULT_CONFIG_PATH)); + + let (config, genesis) = iroha::read_config(&config_path, args.submit_genesis)?; + let logger = iroha_logger::init_global(&config.logger, args.terminal_colors)?; - let config = iroha::combine_configs(&args)?; - let logger = iroha_logger::init_global(&config.logger, terminal_colors)?; - if !config.disable_panic_terminal_colors { - // FIXME: it shouldn't be logged here; it is a part of configuration domain - // this message can be very simply broken by the changes in the configuration - // https://github.com/hyperledger/iroha/issues/3506 - iroha_logger::warn!("The configuration parameter `DISABLE_PANIC_TERMINAL_COLORS` is deprecated. Set `TERMINAL_COLORS=false` instead. ") - } iroha_logger::info!( - version = %env!("CARGO_PKG_VERSION"), git_commit_sha = env!("VERGEN_GIT_SHA"), + version = env!("CARGO_PKG_VERSION"), + git_commit_sha = env!("VERGEN_GIT_SHA"), "Hyperledgerいろは2にようこそ!(translation) Welcome to Hyperledger Iroha!" ); - assert!(args.submit_genesis || config.sumeragi.trusted_peers.peers.len() > 1, - "Only peer in network, yet required to receive genesis topology. This is a configuration error." - ); + if genesis.is_some() { + iroha_logger::debug!("Submitting genesis."); + } - let genesis = args - .submit_genesis - .then_some(()) - .and(args.genesis_path) - .map(|genesis_path| { - let genesis_path = genesis_path.first_existing_path().ok_or({ - color_eyre::eyre::eyre!("Genesis block file {genesis_path:?} doesn't exist") - })?; - - let genesis_block = RawGenesisBlock::from_path(genesis_path.as_ref())?; - GenesisNetwork::from_configuration(genesis_block, Some(&config.genesis)) - .wrap_err("Failed to initialize genesis.") - }) - .transpose()?; - - iroha::Iroha::with_genesis(genesis, config, logger) + iroha::Iroha::new(config, genesis, logger) .await? .start() .await?; + Ok(()) } -fn print_help(styling: &Styling) -> Result<(), std::io::Error> { - use std::io::Write; - - let stdout = std::io::stdout(); - let lock = stdout.lock(); - let mut buffer = std::io::BufWriter::with_capacity(1024 * REQUIRED_ENV_VARS.len(), lock); - writeln!(buffer, "{}", "Iroha 2".bold().green())?; - writeln!(buffer, "pass {} for this message", styling.or(&HELP_ARG))?; - writeln!( - buffer, - "pass {} to submit genesis from this peer", - styling.or(&SUBMIT_ARG) - )?; - writeln!( - buffer, - "pass {} to print version information", - styling.or(&VERSION_ARG) - )?; - writeln!(buffer)?; - writeln!(buffer, "Iroha 2 is configured via environment variables:")?; - writeln!( - buffer, - " {} is the location of your {}", - "IROHA2_CONFIG_PATH".style(styling.highlight), - styling.with_json_file_ext("config") - )?; - writeln!( - buffer, - " {} is the location of your {}", - "IROHA2_GENESIS_PATH".style(styling.highlight), - styling.with_json_file_ext("genesis") - )?; - writeln!( - buffer, - "If either of these is not provided, Iroha checks the current directory." - )?; - writeln!(buffer)?; - writeln!( - buffer, - "Additionally, in case of absence of both {} and {} -in the current directory, all the variables from {} should be set via the environment -as follows:", - "IROHA2_CONFIG_PATH".style(styling.highlight), - styling.with_json_file_ext("config"), - styling.with_json_file_ext("config") - )?; - for var in REQUIRED_ENV_VARS { - writeln!(buffer, " {}: {}", var.0.style(styling.highlight), var.1)?; +#[cfg(test)] +mod tests { + use assertables::{assert_contains, assert_contains_as_result}; + + use super::*; + + #[test] + #[allow(clippy::bool_assert_comparison)] // for expressiveness + fn default_args() -> Result<()> { + let args = Args::try_parse_from(["test"])?; + + assert_eq!(args.config, None); + assert_eq!(args.terminal_colors, is_colouring_supported()); + assert_eq!(args.submit_genesis, false); + + Ok(()) } - writeln!( - buffer, - "Examples of these variables can be found in the default `configs/peer/config.json`." - )?; - Ok(()) -} -fn print_version(styling: &Styling) { - println!( - "{} {} (git hash {}) \n {}: {}", - "Hyperledger Iroha".style(styling.positive), - env!("CARGO_PKG_VERSION").style(styling.highlight), - env!("VERGEN_GIT_SHA"), - "cargo features".style(styling.highlight), - env!("VERGEN_CARGO_FEATURES") - ); + #[test] + #[allow(clippy::bool_assert_comparison)] // for expressiveness + fn terminal_colors_works_as_expected() -> Result<()> { + fn try_with(arg: &str) -> Result { + Ok(Args::try_parse_from(["test", arg])?.terminal_colors) + } + + assert_eq!( + Args::try_parse_from(["test"])?.terminal_colors, + is_colouring_supported() + ); + assert_eq!(try_with("--terminal-colors")?, true); + assert_eq!(try_with("--terminal-colors=false")?, false); + assert_eq!(try_with("--terminal-colors=true")?, true); + assert!(try_with("--terminal-colors=random").is_err()); + + Ok(()) + } + + #[test] + fn user_provided_config_path_works() -> Result<()> { + let args = Args::try_parse_from(["test", "--config", "/home/custom/file.json"])?; + + assert_eq!( + args.config, + Some(Path::user_provided("/home/custom/file.json").unwrap()) + ); + + Ok(()) + } + + #[test] + fn user_cannot_provide_invalid_extension() { + let err = Args::try_parse_from(["test", "--config", "file.toml"]) + .expect_err("Should not allow TOML"); + + let formatted = format!("{err}"); + assert_contains!(formatted, "invalid value 'file.toml' for '--config"); + assert_contains!(formatted, "unsupported file extension `toml`"); + } } diff --git a/cli/src/samples.rs b/cli/src/samples.rs index a91c6354356..1a59f2b0a25 100644 --- a/cli/src/samples.rs +++ b/cli/src/samples.rs @@ -1,4 +1,4 @@ -//! This module contains the sample configurations used for testing and benchmarking throghout Iroha. +//! This module contains the sample configurations used for testing and benchmarking throughout Iroha. use std::{collections::HashSet, path::Path, str::FromStr}; use iroha_config::{ @@ -79,8 +79,9 @@ pub fn get_config_proxy(peers: UniqueVec, key_pair: Option) -> ..iroha_config::queue::ConfigurationProxy::default() }), genesis: Some(Box::new(iroha_config::genesis::ConfigurationProxy { - account_private_key: Some(Some(private_key)), - account_public_key: Some(public_key), + private_key: Some(Some(private_key)), + public_key: Some(public_key), + file: Some(Some("./genesis.json".into())), })), ..ConfigurationProxy::default() } diff --git a/cli/src/style.rs b/cli/src/style.rs deleted file mode 100644 index 393ae591140..00000000000 --- a/cli/src/style.rs +++ /dev/null @@ -1,67 +0,0 @@ -//! Style and colouration of Iroha CLI outputs. -use owo_colors::{OwoColorize, Style}; - -/// Styling information set at run-time for pretty-printing with colour -#[derive(Clone, Copy, Debug)] -pub struct Styling { - /// Positive highlight - pub positive: Style, - /// Negative highlight. Usually error message. - pub negative: Style, - /// Neutral highlight - pub highlight: Style, - /// Minor message - pub minor: Style, -} - -impl Default for Styling { - fn default() -> Self { - Self { - positive: Style::new().green().bold(), - negative: Style::new().red().bold(), - highlight: Style::new().bold(), - minor: Style::new().green(), - } - } -} - -impl Styling { - #[must_use] - /// Constructor - pub fn new(terminal_colors: bool) -> Self { - if terminal_colors { - Self::default() - } else { - Self::no_color() - } - } - - fn no_color() -> Self { - Self { - positive: Style::new(), - negative: Style::new(), - highlight: Style::new(), - minor: Style::new(), - } - } - - /// Produce documentation for argument group - pub fn or(&self, arg_group: &[&str; 2]) -> String { - format!( - "`{}` (short `{}`)", - arg_group[0].style(self.positive), - arg_group[1].style(self.minor) - ) - } - - /// Convenience method for ".json or .json5" pattern - pub fn with_json_file_ext(&self, name: &str) -> String { - let json = format!("{name}.json"); - let json5 = format!("{name}.json5"); - format!( - "`{}` or `{}`", - json.style(self.highlight), - json5.style(self.highlight) - ) - } -} diff --git a/client/benches/torii.rs b/client/benches/torii.rs index 4ca452b930f..088ec7eb406 100644 --- a/client/benches/torii.rs +++ b/client/benches/torii.rs @@ -17,12 +17,23 @@ use tokio::runtime::Runtime; const MINIMUM_SUCCESS_REQUEST_RATIO: f32 = 0.9; +// assumes that config is having a complete genesis key pair +fn get_genesis_key_pair(config: &iroha_config::iroha::Configuration) -> KeyPair { + if let (public_key, Some(private_key)) = + (&config.genesis.public_key, &config.genesis.private_key) + { + KeyPair::new(public_key.clone(), private_key.clone()).expect("Should be valid") + } else { + panic!("Cannot get genesis key pair from the config. Probably a bug.") + } +} + fn query_requests(criterion: &mut Criterion) { let mut peer = ::new().expect("Failed to create peer"); let configuration = get_config(unique_vec![peer.id.clone()], Some(get_key_pair())); let rt = Runtime::test(); - let genesis = GenesisNetwork::from_configuration( + let genesis = GenesisNetwork::new( RawGenesisBlockBuilder::default() .domain("wonderland".parse().expect("Valid")) .account( @@ -34,7 +45,7 @@ fn query_requests(criterion: &mut Criterion) { construct_executor("../default_executor").expect("Failed to construct executor"), ) .build(), - Some(&configuration.genesis), + &get_genesis_key_pair(&configuration), ) .expect("genesis creation failed"); @@ -120,7 +131,7 @@ fn instruction_submits(criterion: &mut Criterion) { let rt = Runtime::test(); let mut peer = ::new().expect("Failed to create peer"); let configuration = get_config(unique_vec![peer.id.clone()], Some(get_key_pair())); - let genesis = GenesisNetwork::from_configuration( + let genesis = GenesisNetwork::new( RawGenesisBlockBuilder::default() .domain("wonderland".parse().expect("Valid")) .account( @@ -132,7 +143,7 @@ fn instruction_submits(criterion: &mut Criterion) { construct_executor("../default_executor").expect("Failed to construct executor"), ) .build(), - Some(&configuration.genesis), + &get_genesis_key_pair(&configuration), ) .expect("failed to create genesis"); let builder = PeerBuilder::new() diff --git a/client/examples/million_accounts_genesis.rs b/client/examples/million_accounts_genesis.rs index 57993c1a972..a6de431c796 100644 --- a/client/examples/million_accounts_genesis.rs +++ b/client/examples/million_accounts_genesis.rs @@ -2,7 +2,7 @@ use std::{thread, time::Duration}; use iroha::samples::{construct_executor, get_config}; -use iroha_client::data_model::prelude::*; +use iroha_client::{crypto::KeyPair, data_model::prelude::*}; use iroha_data_model::isi::InstructionBox; use iroha_genesis::{GenesisNetwork, RawGenesisBlock, RawGenesisBlockBuilder}; use iroha_primitives::unique_vec; @@ -38,10 +38,18 @@ fn main_genesis() { let mut peer = ::new().expect("Failed to create peer"); let configuration = get_config(unique_vec![peer.id.clone()], Some(get_key_pair())); let rt = Runtime::test(); - let genesis = GenesisNetwork::from_configuration( - generate_genesis(1_000_000_u32), - Some(&configuration.genesis), - ) + let genesis = GenesisNetwork::new(generate_genesis(1_000_000_u32), &{ + let private_key = configuration + .genesis + .private_key + .as_ref() + .expect("Should be from get_config"); + KeyPair::new( + configuration.genesis.public_key.clone(), + private_key.clone(), + ) + .expect("Should be a valid key pair") + }) .expect("genesis creation failed"); let builder = PeerBuilder::new() diff --git a/client/tests/integration/permissions.rs b/client/tests/integration/permissions.rs index da443789009..a6250427fbd 100644 --- a/client/tests/integration/permissions.rs +++ b/client/tests/integration/permissions.rs @@ -17,20 +17,11 @@ fn genesis_transactions_are_validated() { // Setting up genesis - let mut genesis = GenesisNetwork::test(true).expect("Expected genesis"); - - let grant_invalid_token = Grant::permission_token( + let genesis = GenesisNetwork::test_with_instructions([Grant::permission_token( PermissionToken::new("InvalidToken".parse().unwrap(), &json!(null)), AccountId::from_str("alice@wonderland").unwrap(), - ); - - let tx_ref = &mut genesis.transactions.last_mut().unwrap().0; - match &mut tx_ref.payload_mut().instructions { - Executable::Instructions(instructions) => { - instructions.push(grant_invalid_token.into()); - } - Executable::Wasm(_) => panic!("Expected instructions"), - } + ) + .into()]); // Starting peer let (_rt, _peer, test_client) = ::new() diff --git a/client/tests/integration/triggers/by_call_trigger.rs b/client/tests/integration/triggers/by_call_trigger.rs index 41644169a73..7c2cf8aff41 100644 --- a/client/tests/integration/triggers/by_call_trigger.rs +++ b/client/tests/integration/triggers/by_call_trigger.rs @@ -359,14 +359,7 @@ fn trigger_in_genesis_using_base64() -> Result<()> { ); // Registering trigger in genesis - let mut genesis = GenesisNetwork::test(true).expect("Expected genesis"); - let tx_ref = &mut genesis.transactions[0].0; - match &mut tx_ref.payload_mut().instructions { - Executable::Instructions(instructions) => { - instructions.push(Register::trigger(trigger).into()); - } - Executable::Wasm(_) => panic!("Expected instructions"), - } + let genesis = GenesisNetwork::test_with_instructions([Register::trigger(trigger).into()]); let (_rt, _peer, mut test_client) = ::new() .with_genesis(genesis) diff --git a/client_cli/Cargo.toml b/client_cli/Cargo.toml index c05c17decb2..c9ac564c6de 100644 --- a/client_cli/Cargo.toml +++ b/client_cli/Cargo.toml @@ -25,10 +25,10 @@ maintenance = { status = "actively-developed" } [dependencies] iroha_client = { workspace = true } iroha_primitives = { workspace = true } +iroha_config_base = { workspace = true } color-eyre = { workspace = true } -# TODO: migrate to clap v4 (and use the workspace dependency) -clap = { version = "3.2.25", features = ["derive"] } +clap = { workspace = true, features = ["derive"] } dialoguer = { version = "0.11.0", default-features = false } json5 = { workspace = true } once_cell = { workspace = true } diff --git a/client_cli/pytests/common/consts.py b/client_cli/pytests/common/consts.py index 1bc8e500f4b..c7f4201a4c4 100644 --- a/client_cli/pytests/common/consts.py +++ b/client_cli/pytests/common/consts.py @@ -14,7 +14,7 @@ class Stderr(Enum): """ Enum for standard error messages. """ - CANNOT_BE_EMPTY = 'cannot be empty\n\nFor more information try --help\n' + CANNOT_BE_EMPTY = 'cannot be empty\n\nFor more information, try \'--help\'.\n' REPETITION = 'Repetition' TOO_LONG = 'Name length violation' FAILED_TO_FIND_DOMAIN = 'Entity missing' diff --git a/client_cli/src/main.rs b/client_cli/src/main.rs index 83f380d63a8..7f22f2dbb89 100644 --- a/client_cli/src/main.rs +++ b/client_cli/src/main.rs @@ -1,86 +1,94 @@ //! iroha client command line use std::{ - fmt, fs::{self, read as read_file}, io::{stdin, stdout}, + path::PathBuf, str::FromStr, time::Duration, }; -use clap::StructOpt; use color_eyre::{ - eyre::{ContextCompat as _, Error, WrapErr}, + eyre::{eyre, Error, WrapErr}, Result, }; +// FIXME: sync with `kagami` (it uses `inquiry`, migrate both to something single) use dialoguer::Confirm; use erased_serde::Serialize; use iroha_client::{ client::{Client, QueryResult}, - config::{path::Path as ConfigPath, Configuration as ClientConfiguration}, + config::{path::Path, Configuration as ClientConfiguration, ConfigurationProxy}, data_model::prelude::*, }; +use iroha_config_base::proxy::{LoadFromDisk, LoadFromEnv, Override}; use iroha_primitives::addr::SocketAddr; -/// Metadata wrapper, which can be captured from cli arguments (from user supplied file). -#[derive(Debug, Clone)] -pub struct Metadata(pub UnlimitedMetadata); - -impl fmt::Display for Metadata { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{self:?}") - } +/// Re-usable clap `--metadata ` (`-m`) argument. +/// Should be combined with `#[command(flatten)]` attr. +#[derive(clap::Args, Debug, Clone)] +// FIXME: `pub` is needed because Rust complains about "leaking private types" +// when this type is used inside of modules. I don't know how to fix it. +pub struct MetadataArgs { + /// The JSON/JSON5 file with key-value metadata pairs + #[arg(short, long, value_name("PATH"), value_hint(clap::ValueHint::FilePath))] + metadata: Option, } -impl FromStr for Metadata { - type Err = Error; - fn from_str(file: &str) -> Result { - if file.is_empty() { - return Ok(Self(UnlimitedMetadata::default())); - } - let err_msg = format!("Failed to open the metadata file {}.", &file); - let deser_err_msg = format!("Failed to deserialize metadata from file: {}", &file); - let content = fs::read_to_string(file).wrap_err(err_msg)?; - let metadata: UnlimitedMetadata = json5::from_str(&content).wrap_err(deser_err_msg)?; - Ok(Self(metadata)) - } -} +impl MetadataArgs { + fn load(self) -> Result { + let value: Option = self + .metadata + .map(|path| { + let content = fs::read_to_string(&path).wrap_err_with(|| { + eyre!("Failed to read the metadata file `{}`", path.display()) + })?; + let metadata: UnlimitedMetadata = + json5::from_str(&content).wrap_err_with(|| { + eyre!( + "Failed to deserialize metadata from file `{}`", + path.display() + ) + })?; + Ok::<_, color_eyre::Report>(metadata) + }) + .transpose()?; -/// Client configuration wrapper. Allows getting itself from arguments from cli (from user supplied file). -#[derive(Debug, Clone)] -struct Configuration(pub ClientConfiguration); - -impl FromStr for Configuration { - type Err = Error; - fn from_str(file: &str) -> Result { - let deser_err_msg = format!("Failed to decode config file {} ", &file); - let err_msg = format!("Failed to open config file {}", &file); - let content = fs::read_to_string(file).wrap_err(err_msg)?; - let cfg = json5::from_str(&content).wrap_err(deser_err_msg)?; - Ok(Self(cfg)) + Ok(value.unwrap_or_default()) } } /// Iroha CLI Client provides an ability to interact with Iroha Peers Web API without direct network usage. -#[derive(StructOpt, Debug)] -#[structopt(name = "iroha_client_cli", version = concat!("version=", env!("CARGO_PKG_VERSION"), " git_commit_sha=", env!("VERGEN_GIT_SHA")), author)] +#[derive(clap::Parser, Debug)] +#[command(name = "iroha_client_cli", version = concat!("version=", env!("CARGO_PKG_VERSION"), " git_commit_sha=", env!("VERGEN_GIT_SHA")), author)] struct Args { - /// Sets a config file path - #[structopt(short, long)] - config: Option, + /// Path to the configuration file, defaults to `config.json`/`config.json5` + /// + /// Supported extensions are `.json` and `.json5`. By default, Iroha Client looks for a + /// `config` file with one of the supported extensions in the current working directory. + /// If the default config file is not found, Iroha will rely on default values and environment + /// variables. However, if the config path is set explicitly with this argument and the file + /// is not found, Iroha Client will exit with an error. + #[arg( + short, + long, + value_name("PATH"), + value_hint(clap::ValueHint::FilePath), + value_parser(Path::user_provided_str) + )] + config: Option, /// More verbose output - #[structopt(short, long)] + #[arg(short, long)] verbose: bool, /// Skip MST check. By setting this flag searching similar transactions on the server can be omitted. /// Thus if you don't use multisignature transactions you should use this flag as it will increase speed of submitting transactions. /// Also setting this flag could be useful when `iroha_client_cli` is used to submit the same transaction multiple times (like mint for example) in short period of time. - #[structopt(long)] + #[arg(long)] skip_mst_check: bool, /// Subcommands of client cli - #[structopt(subcommand)] + #[command(subcommand)] subcommand: Subcommand, } -#[derive(StructOpt, Debug)] +#[derive(clap::Subcommand, Debug)] enum Subcommand { /// The subcommand related to domains #[clap(subcommand)] @@ -170,32 +178,33 @@ impl RunArgs for Subcommand { const RETRY_COUNT_MST: u32 = 1; const RETRY_IN_MST: Duration = Duration::from_millis(100); -static DEFAULT_CONFIG_PATH: once_cell::sync::Lazy<&'static std::path::Path> = - once_cell::sync::Lazy::new(|| std::path::Path::new("config")); +static DEFAULT_CONFIG_PATH: &str = "config"; fn main() -> Result<()> { color_eyre::install()?; let Args { - config: config_opt, + config: config_path, subcommand, verbose, skip_mst_check, } = clap::Parser::parse(); - let config = if let Some(config) = config_opt { - config + + let config = ConfigurationProxy::default(); + let config = if let Some(path) = config_path + .unwrap_or_else(|| Path::default(DEFAULT_CONFIG_PATH)) + .try_resolve() + .wrap_err("Failed to resolve config file")? + { + config.override_with(ConfigurationProxy::from_path(&*path)) } else { - let config_path = ConfigPath::default(&DEFAULT_CONFIG_PATH); - Configuration::from_str( - config_path - .first_existing_path() - .wrap_err("Configuration file does not exist")? - .as_ref() - .to_string_lossy() - .as_ref(), - )? + config }; - - let Configuration(config) = config; + let config = config.override_with( + ConfigurationProxy::from_std_env().wrap_err("Failed to read config from ENV")?, + ); + let config = config + .build() + .wrap_err("Failed to finalize configuration")?; if verbose { eprintln!( @@ -287,7 +296,7 @@ mod events { use super::*; /// Get event stream from iroha peer - #[derive(StructOpt, Debug, Clone, Copy)] + #[derive(clap::Subcommand, Debug, Clone, Copy)] pub enum Args { /// Gets pipeline events Pipeline, @@ -327,7 +336,7 @@ mod blocks { use super::*; /// Get block stream from iroha peer - #[derive(StructOpt, Debug, Clone, Copy)] + #[derive(clap::Args, Debug, Clone, Copy)] pub struct Args { /// Block height from which to start streaming blocks height: NonZeroU64, @@ -375,29 +384,25 @@ mod domain { } /// Add subcommand for domain - #[derive(Debug, StructOpt)] + #[derive(Debug, clap::Args)] pub struct Register { /// Domain name as double-quoted string - #[structopt(short, long)] + #[arg(short, long)] pub id: DomainId, - /// The JSON/JSON5 file with key-value metadata pairs - #[structopt(short, long, default_value = "")] - pub metadata: super::Metadata, + #[command(flatten)] + pub metadata: MetadataArgs, } impl RunArgs for Register { fn run(self, context: &mut dyn RunContext) -> Result<()> { - let Self { - id, - metadata: Metadata(metadata), - } = self; + let Self { id, metadata } = self; let create_domain = iroha_client::data_model::isi::Register::domain(Domain::new(id)); - submit([create_domain], metadata, context).wrap_err("Failed to create domain") + submit([create_domain], metadata.load()?, context).wrap_err("Failed to create domain") } } /// List domains with this command - #[derive(StructOpt, Debug, Clone)] + #[derive(clap::Subcommand, Debug, Clone)] pub enum List { /// All domains All, @@ -425,20 +430,19 @@ mod domain { } /// Transfer a domain between accounts - #[derive(Debug, StructOpt)] + #[derive(Debug, clap::Args)] pub struct Transfer { /// Domain name as double-quited string - #[structopt(short, long)] + #[arg(short, long)] pub id: DomainId, /// Account from which to transfer (in form `name@domain_name') - #[structopt(short, long)] + #[arg(short, long)] pub from: AccountId, /// Account to which to transfer (in form `name@domain_name') - #[structopt(short, long)] + #[arg(short, long)] pub to: AccountId, - /// The JSON/JSON5 file with key-value metadata pairs - #[structopt(short, long, default_value = "")] - pub metadata: super::Metadata, + #[command(flatten)] + pub metadata: MetadataArgs, } impl RunArgs for Transfer { @@ -447,10 +451,11 @@ mod domain { id, from, to, - metadata: Metadata(metadata), + metadata, } = self; let transfer_domain = iroha_client::data_model::isi::Transfer::domain(from, id, to); - submit([transfer_domain], metadata, context).wrap_err("Failed to transfer domain") + submit([transfer_domain], metadata.load()?, context) + .wrap_err("Failed to transfer domain") } } } @@ -463,15 +468,15 @@ mod account { use super::*; /// subcommands for account subcommand - #[derive(StructOpt, Debug)] + #[derive(clap::Subcommand, Debug)] pub enum Args { /// Register account Register(Register), /// Set something in account - #[clap(subcommand)] + #[command(subcommand)] Set(Set), /// List accounts - #[clap(subcommand)] + #[command(subcommand)] List(List), /// Grant a permission to the account Grant(Grant), @@ -492,34 +497,30 @@ mod account { } /// Register account - #[derive(StructOpt, Debug)] + #[derive(clap::Args, Debug)] pub struct Register { /// Id of account in form `name@domain_name' - #[structopt(short, long)] + #[arg(short, long)] pub id: AccountId, /// Its public key - #[structopt(short, long)] + #[arg(short, long)] pub key: PublicKey, - /// /// The JSON file with key-value metadata pairs - #[structopt(short, long, default_value = "")] - pub metadata: super::Metadata, + #[command(flatten)] + pub metadata: MetadataArgs, } impl RunArgs for Register { fn run(self, context: &mut dyn RunContext) -> Result<()> { - let Self { - id, - key, - metadata: Metadata(metadata), - } = self; + let Self { id, key, metadata } = self; let create_account = iroha_client::data_model::isi::Register::account(Account::new(id, [key])); - submit([create_account], metadata, context).wrap_err("Failed to register account") + submit([create_account], metadata.load()?, context) + .wrap_err("Failed to register account") } } /// Set subcommand of account - #[derive(StructOpt, Debug)] + #[derive(clap::Subcommand, Debug)] pub enum Set { /// Signature condition SignatureCondition(SignatureCondition), @@ -531,7 +532,7 @@ mod account { } } - #[derive(Debug)] + #[derive(Debug, Clone)] pub struct Signature(SignatureCheckCondition); impl FromStr for Signature { @@ -548,13 +549,12 @@ mod account { } /// Set accounts signature condition - #[derive(StructOpt, Debug)] + #[derive(clap::Args, Debug)] pub struct SignatureCondition { /// Signature condition file pub condition: Signature, - /// The JSON/JSON5 file with key-value metadata pairs - #[structopt(short, long, default_value = "")] - pub metadata: super::Metadata, + #[command(flatten)] + pub metadata: MetadataArgs, } impl RunArgs for SignatureCondition { @@ -562,15 +562,16 @@ mod account { let account_id = context.configuration().account_id.clone(); let Self { condition: Signature(condition), - metadata: Metadata(metadata), + metadata, } = self; let mint_box = Mint::account_signature_check_condition(condition, account_id); - submit([mint_box], metadata, context).wrap_err("Failed to set signature condition") + submit([mint_box], metadata.load()?, context) + .wrap_err("Failed to set signature condition") } } /// List accounts with this command - #[derive(StructOpt, Debug, Clone)] + #[derive(clap::Subcommand, Debug, Clone)] pub enum List { /// All accounts All, @@ -597,21 +598,20 @@ mod account { } } - #[derive(StructOpt, Debug)] + #[derive(clap::Args, Debug)] pub struct Grant { /// Account id - #[structopt(short, long)] + #[arg(short, long)] pub id: AccountId, /// The JSON/JSON5 file with a permission token - #[structopt(short, long)] + #[arg(short, long)] pub permission: Permission, - /// The JSON/JSON5 file with key-value metadata pairs - #[structopt(short, long, default_value = "")] - pub metadata: super::Metadata, + #[command(flatten)] + pub metadata: MetadataArgs, } /// [`PermissionToken`] wrapper implementing [`FromStr`] - #[derive(Debug)] + #[derive(Debug, Clone)] pub struct Permission(PermissionToken); impl FromStr for Permission { @@ -633,19 +633,19 @@ mod account { let Self { id, permission, - metadata: Metadata(metadata), + metadata, } = self; let grant = iroha_client::data_model::isi::Grant::permission_token(permission.0, id); - submit([grant], metadata, context) + submit([grant], metadata.load()?, context) .wrap_err("Failed to grant the permission to the account") } } /// List all account permissions - #[derive(StructOpt, Debug)] + #[derive(clap::Args, Debug)] pub struct ListPermissions { /// Account id - #[structopt(short, long)] + #[arg(short, long)] id: AccountId, } @@ -668,7 +668,7 @@ mod asset { use super::*; /// Subcommand for dealing with asset - #[derive(StructOpt, Debug)] + #[derive(clap::Subcommand, Debug)] pub enum Args { /// Register subcommand of asset Register(Register), @@ -695,20 +695,19 @@ mod asset { } /// Register subcommand of asset - #[derive(StructOpt, Debug)] + #[derive(clap::Args, Debug)] pub struct Register { /// Asset id for registering (in form of `name#domain_name') - #[structopt(short, long)] + #[arg(short, long)] pub id: AssetDefinitionId, /// Mintability of asset - #[structopt(short, long)] + #[arg(short, long)] pub unmintable: bool, /// Value type stored in asset - #[structopt(short, long)] + #[arg(short, long)] pub value_type: AssetValueType, - /// The JSON/JSON5 file with key-value metadata pairs - #[structopt(short, long, default_value = "")] - pub metadata: super::Metadata, + #[command(flatten)] + pub metadata: MetadataArgs, } impl RunArgs for Register { @@ -717,7 +716,7 @@ mod asset { id, value_type, unmintable, - metadata: Metadata(metadata), + metadata, } = self; let mut asset_definition = match value_type { AssetValueType::Quantity => AssetDefinition::quantity(id), @@ -730,26 +729,25 @@ mod asset { } let create_asset_definition = iroha_client::data_model::isi::Register::asset_definition(asset_definition); - submit([create_asset_definition], metadata, context) + submit([create_asset_definition], metadata.load()?, context) .wrap_err("Failed to register asset") } } /// Command for minting asset in existing Iroha account - #[derive(StructOpt, Debug)] + #[derive(clap::Args, Debug)] pub struct Mint { /// Account id where asset is stored (in form of `name@domain_name') - #[structopt(long)] + #[arg(long)] pub account: AccountId, /// Asset id from which to mint (in form of `name#domain_name') - #[structopt(long)] + #[arg(long)] pub asset: AssetDefinitionId, /// Quantity to mint - #[structopt(short, long)] + #[arg(short, long)] pub quantity: u32, - /// The JSON/JSON5 file with key-value metadata pairs - #[structopt(short, long, default_value = "")] - pub metadata: super::Metadata, + #[command(flatten)] + pub metadata: MetadataArgs, } impl RunArgs for Mint { @@ -758,32 +756,31 @@ mod asset { account, asset, quantity, - metadata: Metadata(metadata), + metadata, } = self; let mint_asset = iroha_client::data_model::isi::Mint::asset_quantity( quantity, AssetId::new(asset, account), ); - submit([mint_asset], metadata, context) + submit([mint_asset], metadata.load()?, context) .wrap_err("Failed to mint asset of type `NumericValue::U32`") } } /// Command for minting asset in existing Iroha account - #[derive(StructOpt, Debug)] + #[derive(clap::Args, Debug)] pub struct Burn { /// Account id where asset is stored (in form of `name@domain_name') - #[structopt(long)] + #[arg(long)] pub account: AccountId, /// Asset id from which to mint (in form of `name#domain_name') - #[structopt(long)] + #[arg(long)] pub asset: AssetDefinitionId, /// Quantity to mint - #[structopt(short, long)] + #[arg(short, long)] pub quantity: u32, - /// The JSON/JSON5 file with key-value metadata pairs - #[structopt(short, long, default_value = "")] - pub metadata: super::Metadata, + #[command(flatten)] + pub metadata: MetadataArgs, } impl RunArgs for Burn { @@ -792,35 +789,34 @@ mod asset { account, asset, quantity, - metadata: Metadata(metadata), + metadata, } = self; let burn_asset = iroha_client::data_model::isi::Burn::asset_quantity( quantity, AssetId::new(asset, account), ); - submit([burn_asset], metadata, context) + submit([burn_asset], metadata.load()?, context) .wrap_err("Failed to burn asset of type `NumericValue::U32`") } } /// Transfer asset between accounts - #[derive(StructOpt, Debug)] + #[derive(clap::Args, Debug)] pub struct Transfer { /// Account from which to transfer (in form `name@domain_name') - #[structopt(short, long)] + #[arg(short, long)] pub from: AccountId, /// Account to which to transfer (in form `name@domain_name') - #[structopt(short, long)] + #[arg(short, long)] pub to: AccountId, /// Asset id to transfer (in form like `name#domain_name') - #[structopt(short, long)] + #[arg(short, long)] pub asset_id: AssetDefinitionId, /// Quantity of asset as number - #[structopt(short, long)] + #[arg(short, long)] pub quantity: u32, - /// The JSON/JSON5 file with key-value metadata pairs - #[structopt(short, long, default_value = "")] - pub metadata: super::Metadata, + #[command(flatten)] + pub metadata: MetadataArgs, } impl RunArgs for Transfer { @@ -830,25 +826,25 @@ mod asset { to, asset_id, quantity, - metadata: Metadata(metadata), + metadata, } = self; let transfer_asset = iroha_client::data_model::isi::Transfer::asset_quantity( AssetId::new(asset_id, from), quantity, to, ); - submit([transfer_asset], metadata, context).wrap_err("Failed to transfer asset") + submit([transfer_asset], metadata.load()?, context).wrap_err("Failed to transfer asset") } } /// Get info of asset - #[derive(StructOpt, Debug)] + #[derive(clap::Args, Debug)] pub struct Get { /// Account where asset is stored (in form of `name@domain_name') - #[structopt(long)] + #[arg(long)] pub account: AccountId, /// Asset name to lookup (in form of `name#domain_name') - #[structopt(long)] + #[arg(long)] pub asset: AssetDefinitionId, } @@ -866,7 +862,7 @@ mod asset { } /// List assets with this command - #[derive(StructOpt, Debug, Clone)] + #[derive(clap::Subcommand, Debug, Clone)] pub enum List { /// All assets All, @@ -898,7 +894,7 @@ mod peer { use super::*; /// Subcommand for dealing with peer - #[derive(StructOpt, Debug)] + #[derive(clap::Subcommand, Debug)] pub enum Args { /// Register subcommand of peer Register(Register), @@ -916,17 +912,16 @@ mod peer { } /// Register subcommand of peer - #[derive(StructOpt, Debug)] + #[derive(clap::Args, Debug)] pub struct Register { /// P2P address of the peer e.g. `127.0.0.1:1337` - #[structopt(short, long)] + #[arg(short, long)] pub address: SocketAddr, /// Public key of the peer - #[structopt(short, long)] + #[arg(short, long)] pub key: PublicKey, - /// The JSON/JSON5 file with key-value metadata pairs - #[structopt(short, long, default_value = "")] - pub metadata: super::Metadata, + #[command(flatten)] + pub metadata: MetadataArgs, } impl RunArgs for Register { @@ -934,27 +929,26 @@ mod peer { let Self { address, key, - metadata: Metadata(metadata), + metadata, } = self; let register_peer = iroha_client::data_model::isi::Register::peer(Peer::new( PeerId::new(&address, &key), )); - submit([register_peer], metadata, context).wrap_err("Failed to register peer") + submit([register_peer], metadata.load()?, context).wrap_err("Failed to register peer") } } /// Unregister subcommand of peer - #[derive(StructOpt, Debug)] + #[derive(clap::Args, Debug)] pub struct Unregister { /// P2P address of the peer e.g. `127.0.0.1:1337` - #[structopt(short, long)] + #[arg(short, long)] pub address: SocketAddr, /// Public key of the peer - #[structopt(short, long)] + #[arg(short, long)] pub key: PublicKey, - /// The JSON/JSON5 file with key-value metadata pairs - #[structopt(short, long, default_value = "")] - pub metadata: super::Metadata, + #[command(flatten)] + pub metadata: MetadataArgs, } impl RunArgs for Unregister { @@ -962,11 +956,12 @@ mod peer { let Self { address, key, - metadata: Metadata(metadata), + metadata, } = self; let unregister_peer = iroha_client::data_model::isi::Unregister::peer(PeerId::new(&address, &key)); - submit([unregister_peer], metadata, context).wrap_err("Failed to unregister peer") + submit([unregister_peer], metadata.load()?, context) + .wrap_err("Failed to unregister peer") } } } @@ -977,10 +972,10 @@ mod wasm { use super::*; /// Subcommand for dealing with Wasm - #[derive(Debug, StructOpt)] + #[derive(Debug, clap::Args)] pub struct Args { /// Specify a path to the Wasm file or skip this flag to read from stdin - #[structopt(short, long)] + #[arg(short, long)] path: Option, } @@ -1012,7 +1007,7 @@ mod json { use super::*; /// Subcommand for submitting multi-instructions - #[derive(Clone, Copy, Debug, StructOpt)] + #[derive(Clone, Copy, Debug, clap::Args)] pub struct Args; impl RunArgs for Args { diff --git a/config/Cargo.toml b/config/Cargo.toml index 2f548f60946..d6df71128fa 100644 --- a/config/Cargo.toml +++ b/config/Cargo.toml @@ -15,6 +15,7 @@ iroha_config_base = { workspace = true } iroha_data_model = { workspace = true } iroha_primitives = { workspace = true } iroha_crypto = { workspace = true } +iroha_genesis = { workspace = true } eyre = { workspace = true } tracing = { workspace = true } @@ -29,7 +30,6 @@ thiserror = { workspace = true } displaydoc = { workspace = true } derive_more = { workspace = true } cfg-if = { workspace = true } -path-absolutize = { workspace = true } once_cell = { workspace = true } [dev-dependencies] diff --git a/config/iroha_test_config.json b/config/iroha_test_config.json index 6ebbf417a26..7a180598bbb 100644 --- a/config/iroha_test_config.json +++ b/config/iroha_test_config.json @@ -4,7 +4,6 @@ "digest_function": "ed25519", "payload": "282ED9F3CF92811C3818DBC4AE594ED59DC1A2F78E4241E31924E101D6B1FB831C61FAF8FE94E253B93114240394F79A607B7FA55F9E5A41EBEC74B88055768B" }, - "DISABLE_PANIC_TERMINAL_COLORS": false, "KURA": { "INIT_MODE": "strict", "BLOCK_STORE_PATH": "./storage", @@ -62,14 +61,15 @@ "TOKIO_CONSOLE_ADDR": "127.0.0.1:5555" }, "GENESIS": { - "ACCOUNT_PUBLIC_KEY": "ed01204CFFD0EE429B1BDD36B3910EC570852B8BB63F18750341772FB46BC856C5CAAF", - "ACCOUNT_PRIVATE_KEY": { + "PUBLIC_KEY": "ed01204CFFD0EE429B1BDD36B3910EC570852B8BB63F18750341772FB46BC856C5CAAF", + "PRIVATE_KEY": { "digest_function": "ed25519", "payload": "D748E18CE60CB30DEA3E73C9019B7AF45A8D465E3D71BCC9A5EF99A008205E534CFFD0EE429B1BDD36B3910EC570852B8BB63F18750341772FB46BC856C5CAAF" }, "WAIT_FOR_PEERS_RETRY_COUNT_LIMIT": 100, "WAIT_FOR_PEERS_RETRY_PERIOD_MS": 500, - "GENESIS_SUBMISSION_DELAY_MS": 1000 + "GENESIS_SUBMISSION_DELAY_MS": 1000, + "FILE": "./genesis.json" }, "WSV": { "ASSET_METADATA_LIMITS": { diff --git a/config/src/genesis.rs b/config/src/genesis.rs index 2bb9e8d892b..b6881ac4d65 100644 --- a/config/src/genesis.rs +++ b/config/src/genesis.rs @@ -1,6 +1,10 @@ //! Module with genesis configuration logic. +use std::path::PathBuf; + +use eyre::Report; use iroha_config_base::derive::{view, Proxy}; -use iroha_crypto::{PrivateKey, PublicKey}; +use iroha_crypto::{KeyPair, PrivateKey, PublicKey}; +use iroha_genesis::RawGenesisBlock; use serde::{Deserialize, Serialize}; // Generate `ConfigurationView` without the private key @@ -12,22 +16,88 @@ view! { pub struct Configuration { /// The public key of the genesis account, should be supplied to all peers. #[config(serde_as_str)] - pub account_public_key: PublicKey, + pub public_key: PublicKey, /// The private key of the genesis account, only needed for the peer that submits the genesis block. #[view(ignore)] - pub account_private_key: Option, + pub private_key: Option, + /// Path to the genesis file + #[config(serde_as_str)] + pub file: Option } } impl Default for ConfigurationProxy { fn default() -> Self { Self { - account_public_key: None, - account_private_key: Some(None), + public_key: None, + private_key: Some(None), + file: None, } } } +/// Parsed variant of the user-provided [`Configuration`] +// TODO: incorporate this struct into the final, parsed configuration +// https://github.com/hyperledger/iroha/issues/3500 +pub enum ParsedConfiguration { + /// The peer can only observe the genesis block + Partial { + /// Genesis account public key + public_key: PublicKey, + }, + /// The peer is responsible for submitting the genesis block + Full { + /// Genesis account key pair + key_pair: KeyPair, + /// Raw genesis block + raw_block: RawGenesisBlock, + }, +} + +impl Configuration { + /// Parses user configuration into a stronger-typed structure [`ParsedConfiguration`] + /// + /// # Errors + /// See [`ParseError`] + pub fn parse(self, submit: bool) -> Result { + match (self.private_key, self.file, submit) { + (None, None, false) => Ok(ParsedConfiguration::Partial { + public_key: self.public_key, + }), + (Some(private_key), Some(path), true) => { + let raw_block = RawGenesisBlock::from_path(&path) + .map_err(|report| ParseError::File { path, report })?; + + Ok(ParsedConfiguration::Full { + key_pair: KeyPair::new(self.public_key, private_key)?, + raw_block, + }) + } + (_, _, true) => Err(ParseError::SubmitIsSetButRestAreNot), + (_, _, false) => Err(ParseError::SubmitIsNotSetButRestAre), + } + } +} + +/// Error which might occur during [`Configuration::parse()`] +#[derive(Debug, displaydoc::Display, thiserror::Error)] +pub enum ParseError { + /// `--submit-genesis` was provided, but `genesis.private_key` and/or `genesis.file` are missing + SubmitIsSetButRestAreNot, + /// `--submit-genesis` was not provided, but `genesis.private_key` and/or `genesis.file` are set + SubmitIsNotSetButRestAre, + /// Genesis key pair is invalid + InvalidKeyPair(#[from] iroha_crypto::error::Error), + /// Cannot read the genesis block from file `{path}` + File { + /// Original error report + #[source] + report: Report, + /// Path to the file + path: PathBuf, + }, +} + #[cfg(test)] pub mod tests { use iroha_crypto::KeyPair; @@ -61,10 +131,11 @@ pub mod tests { prop_compose! { pub fn arb_proxy() ( - (account_public_key, account_private_key) in arb_keys(), + (public_key, private_key) in arb_keys(), + file in prop::option::of(Just(None)) ) -> ConfigurationProxy { - ConfigurationProxy { account_public_key, account_private_key } + ConfigurationProxy { public_key, private_key, file } } } } diff --git a/config/src/iroha.rs b/config/src/iroha.rs index ffa28eddc2e..1946b2571b1 100644 --- a/config/src/iroha.rs +++ b/config/src/iroha.rs @@ -20,8 +20,6 @@ view! { /// Private key of this peer #[view(ignore)] pub private_key: PrivateKey, - /// Disable coloring of the backtrace and error report on panic - pub disable_panic_terminal_colors: bool, /// `Kura` configuration #[config(inner)] pub kura: Box, @@ -68,7 +66,6 @@ impl Default for ConfigurationProxy { Self { public_key: None, private_key: None, - disable_panic_terminal_colors: Some(bool::default()), kura: Some(Box::default()), sumeragi: Some(Box::default()), torii: Some(Box::default()), @@ -169,6 +166,8 @@ impl ConfigurationProxy { #[cfg(test)] mod tests { + use std::path::PathBuf; + use proptest::prelude::*; use super::*; @@ -204,7 +203,6 @@ mod tests { prop_compose! { fn arb_proxy()( (public_key, private_key) in arb_keys(), - disable_panic_terminal_colors in prop::option::of(Just(true)), kura in prop::option::of(kura::tests::arb_proxy().prop_map(Box::new)), sumeragi in (prop::option::of(sumeragi::tests::arb_proxy().prop_map(Box::new))), torii in (prop::option::of(torii::tests::arb_proxy().prop_map(Box::new))), @@ -218,7 +216,7 @@ mod tests { snapshot in prop::option::of(snapshot::tests::arb_proxy().prop_map(Box::new)), live_query_store in prop::option::of(live_query_store::tests::arb_proxy()), ) -> ConfigurationProxy { - ConfigurationProxy { public_key, private_key, disable_panic_terminal_colors, kura, sumeragi, torii, block_sync, queue, + ConfigurationProxy { public_key, private_key, kura, sumeragi, torii, block_sync, queue, logger, genesis, wsv, network, telemetry, snapshot, live_query_store } } } @@ -244,7 +242,7 @@ mod tests { fn parse_example_json() { let cfg_proxy = ConfigurationProxy::from_path(CONFIGURATION_PATH); assert_eq!( - "./storage", + PathBuf::from("./storage"), cfg_proxy.kura.unwrap().block_store_path.unwrap() ); assert_eq!( diff --git a/config/src/kura.rs b/config/src/kura.rs index 5ce29c4ce95..8f97dbbf94b 100644 --- a/config/src/kura.rs +++ b/config/src/kura.rs @@ -1,5 +1,7 @@ //! Module for kura-related configuration and structs +use std::path::PathBuf; + use eyre::Result; use iroha_config_base::derive::Proxy; use serde::{Deserialize, Serialize}; @@ -14,7 +16,8 @@ pub struct Configuration { /// Initialization mode: `strict` or `fast`. pub init_mode: Mode, /// Path to the existing block store folder or path to create new folder. - pub block_store_path: String, + #[config(serde_as_str)] + pub block_store_path: PathBuf, /// Whether or not new blocks be outputted to a file called blocks.json. pub debug_output_new_blocks: bool, } diff --git a/config/src/path.rs b/config/src/path.rs index f6f14887c4e..488af5ded46 100644 --- a/config/src/path.rs +++ b/config/src/path.rs @@ -5,8 +5,6 @@ extern crate alloc; use alloc::borrow::Cow; use std::path::PathBuf; -// TODO: replace with `std::fs::absolute` when it's stable. -use path_absolutize::Absolutize as _; use InnerPath::*; /// Allowed configuration file extension that user can provide. @@ -14,105 +12,140 @@ pub const ALLOWED_CONFIG_EXTENSIONS: [&str; 2] = ["json", "json5"]; /// Error type for [`Path`]. #[derive(Debug, Clone, thiserror::Error, displaydoc::Display)] -pub enum ExtensionError { - /// No valid file extension found. Allowed file extensions are: {ALLOWED_CONFIG_EXTENSIONS:?} - Missing, - /// Provided config file has an unsupported file extension `{0}`, allowed extensions are: {ALLOWED_CONFIG_EXTENSIONS:?}. - Invalid(String), +pub enum Error { + /// File doesn't have an extension. Allowed file extensions are: {ALLOWED_CONFIG_EXTENSIONS:?} + MissingExtension, + /// Provided config file has an unsupported file extension `{0}`. Allowed extensions are: {ALLOWED_CONFIG_EXTENSIONS:?}. + InvalidExtension(String), + /// User-provided file `{0}` is not found. + FileNotFound(String), } /// Result type for [`Path`] constructors. -pub type Result = std::result::Result; +pub type Result = std::result::Result; /// Inner helper struct. /// /// With this struct, we force to use [`Path`]'s constructors instead of constructing it directly. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] enum InnerPath { + /// Contains path without an extension, so that it will try to resolve + /// using [`ALLOWED_CONFIG_EXTENSIONS`]. [`Path::try_resolve()`] will not fail if file isn't + /// found. Default(PathBuf), + /// Contains full path, with extension. [`Path::try_resolve()`] will fail if not found. UserProvided(PathBuf), } /// Wrapper around path to config file (e.g. `config.json`). /// /// Provides abstraction above user-provided config and default ones. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub struct Path(InnerPath); impl core::fmt::Display for Path { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match &self.0 { - Default(pth) => write!( - f, - "{:?} (default)", - pth.with_extension("json") - .absolutize() - .expect("Malformed default path") - ), - UserProvided(pth) => write!( - f, - "{:?} (user-provided)", - pth.with_extension("json") - .absolutize() - .expect("Malformed user-provided path") - ), + Default(path) => { + write!( + f, + "{}.{{{}}}", + path.display(), + ALLOWED_CONFIG_EXTENSIONS.join(",") + ) + } + UserProvided(path) => write!(f, "{}", path.display()), } } } impl Path { - /// Construct new [`Path`] from the default `path`. + /// Construct new [`Path`] which will try to resolve multiple allowed extensions and will not + /// fail resolution ([`Self::try_resolve()`]) if file is not found. /// - /// # Panics + /// The path should not have an extension. /// - /// Panics if `path` contains an extension. - pub fn default(path: &'static std::path::Path) -> Self { - assert!( - path.extension().is_none(), - "Default configuration path should have no extension" - ); - - Self(Default(path.to_owned())) + /// # Panics + /// If the path has an extension. + pub fn default(path: impl AsRef) -> Self { + let path = path.as_ref().to_path_buf(); + if path.extension().is_some() { + panic!("Default config path is not supposed to have an extension. It is a bug.") + } + Self(Default(path)) } - /// Construct new [`Path`] from user-provided `path`. + /// Construct new [`Path`] from user-provided `path` which will fail to [`Self::try_resolve()`] + /// if file is not found. /// /// # Errors - /// - /// An error will be returned if `path` contains no file extension - /// or contains unsupported one. - pub fn user_provided(path: impl Into) -> Result { - let path = path.into(); + /// If `path`'s extension is absent or unsupported. + pub fn user_provided(path: impl AsRef) -> Result { + let path = path.as_ref(); let extension = path .extension() - .ok_or(ExtensionError::Missing)? + .ok_or(Error::MissingExtension)? .to_string_lossy(); if !ALLOWED_CONFIG_EXTENSIONS.contains(&extension.as_ref()) { - return Err(ExtensionError::Invalid(extension.into_owned())); + return Err(Error::InvalidExtension(extension.into_owned())); } - Ok(Self(UserProvided(path))) + Ok(Self(UserProvided(path.to_path_buf()))) + } + + /// Same as [`Self::user_provided()`], but accepts `&str` (useful for clap) + /// + /// # Errors + /// See [`Self::user_provided()`] + pub fn user_provided_str(raw: &str) -> Result { + Self::user_provided(raw) } /// Try to get first existing path by applying possible extensions if there are any. - pub fn first_existing_path(&self) -> Option> { + /// + /// # Errors + /// If user-provided path is not found + pub fn try_resolve(&self) -> Result>> { match &self.0 { - Default(path) => ALLOWED_CONFIG_EXTENSIONS.iter().find_map(|extension| { - let path_ext = path.with_extension(extension); - path_ext.exists().then_some(Cow::Owned(path_ext)) - }), - UserProvided(path) => path.exists().then_some(Cow::Borrowed(path)), + Default(path) => { + let maybe = ALLOWED_CONFIG_EXTENSIONS.iter().find_map(|extension| { + let path_ext = path.with_extension(extension); + path_ext.exists().then_some(Cow::Owned(path_ext)) + }); + Ok(maybe) + } + UserProvided(path) => { + if path.exists() { + Ok(Some(Cow::Borrowed(path))) + } else { + Err(Error::FileNotFound(path.to_string_lossy().into_owned())) + } + } } } +} - /// Check if config path exists by applying allowed extensions if there are any. - pub fn exists(&self) -> bool { - match &self.0 { - Default(path) => ALLOWED_CONFIG_EXTENSIONS - .iter() - .any(|extension| path.with_extension(extension).exists()), - UserProvided(path) => path.exists(), - } +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn display_multi_extensions() { + let path = Path::default("config"); + + let display = format!("{path}"); + + assert_eq!(display, "config.{json,json5}") + } + + #[test] + fn display_strict_extension() { + let path = + Path::user_provided("config.json").expect("Should be valid since extension is valid"); + + let display = format!("{path}"); + + assert_eq!(display, "config.json") } } diff --git a/config/src/snapshot.rs b/config/src/snapshot.rs index ea949340767..e828e5635d0 100644 --- a/config/src/snapshot.rs +++ b/config/src/snapshot.rs @@ -1,5 +1,7 @@ //! Module for `SnapshotMaker`-related configuration and structs. +use std::path::PathBuf; + use iroha_config_base::derive::Proxy; use serde::{Deserialize, Serialize}; @@ -16,7 +18,8 @@ pub struct Configuration { /// The period of time to wait between attempts to create new snapshot. pub create_every_ms: u64, /// Path to the directory where snapshots should be stored - pub dir_path: String, + #[config(serde_as_str)] + pub dir_path: PathBuf, /// Flag to enable or disable snapshot creation pub creation_enabled: bool, } @@ -25,7 +28,7 @@ impl Default for ConfigurationProxy { fn default() -> Self { Self { create_every_ms: Some(DEFAULT_SNAPSHOT_CREATE_EVERY_MS), - dir_path: Some(DEFAULT_SNAPSHOT_PATH.to_owned()), + dir_path: Some(DEFAULT_SNAPSHOT_PATH.into()), creation_enabled: Some(DEFAULT_ENABLED), } } @@ -41,7 +44,7 @@ pub mod tests { pub fn arb_proxy() ( create_every_ms in prop::option::of(Just(DEFAULT_SNAPSHOT_CREATE_EVERY_MS)), - dir_path in prop::option::of(Just(DEFAULT_SNAPSHOT_PATH.to_owned())), + dir_path in prop::option::of(Just(DEFAULT_SNAPSHOT_PATH.into())), creation_enabled in prop::option::of(Just(DEFAULT_ENABLED)), ) -> ConfigurationProxy { diff --git a/configs/peer/config.json b/configs/peer/config.json index 11d5b354ce8..3f0dc2f87a9 100644 --- a/configs/peer/config.json +++ b/configs/peer/config.json @@ -1,7 +1,6 @@ { "PUBLIC_KEY": null, "PRIVATE_KEY": null, - "DISABLE_PANIC_TERMINAL_COLORS": false, "KURA": { "INIT_MODE": "strict", "BLOCK_STORE_PATH": "./storage", @@ -40,8 +39,9 @@ "FORMAT": "full" }, "GENESIS": { - "ACCOUNT_PUBLIC_KEY": null, - "ACCOUNT_PRIVATE_KEY": null + "PUBLIC_KEY": null, + "PRIVATE_KEY": null, + "FILE": null }, "WSV": { "ASSET_METADATA_LIMITS": { diff --git a/core/src/snapshot.rs b/core/src/snapshot.rs index e642dc01635..52aad1bd6ee 100644 --- a/core/src/snapshot.rs +++ b/core/src/snapshot.rs @@ -41,7 +41,7 @@ pub struct SnapshotMaker { /// Frequency at which snapshot is made snapshot_create_every: Duration, /// Path to the directory where snapshots are stored - snapshot_dir: String, + snapshot_dir: PathBuf, /// Flag to enable/disable snapshot creation snapshot_creation_enabled: bool, /// Flag to signal that new wsv is available for taking snapshot diff --git a/core/src/sumeragi/main_loop.rs b/core/src/sumeragi/main_loop.rs index 33e5e41515f..fcca60b867b 100644 --- a/core/src/sumeragi/main_loop.rs +++ b/core/src/sumeragi/main_loop.rs @@ -242,7 +242,7 @@ impl Sumeragi { assert_eq!(self.wsv.latest_block_hash(), None); let transactions: Vec<_> = genesis_network - .transactions + .into_transactions() .into_iter() .map(AcceptedTransaction::accept_genesis) .collect(); diff --git a/core/test_network/src/lib.rs b/core/test_network/src/lib.rs index bf61826e05c..9b217a69d59 100644 --- a/core/test_network/src/lib.rs +++ b/core/test_network/src/lib.rs @@ -68,13 +68,17 @@ pub fn get_key_pair() -> KeyPair { /// Trait used to differentiate a test instance of `genesis`. pub trait TestGenesis: Sized { - /// Construct Iroha genesis network and optionally submit genesis - /// from the given peer. - fn test(submit_genesis: bool) -> Option; + /// Construct Iroha genesis network + fn test() -> Self { + Self::test_with_instructions([]) + } + + /// Construct genesis network with additional instructions + fn test_with_instructions(extra_isi: impl IntoIterator) -> Self; } impl TestGenesis for GenesisNetwork { - fn test(submit_genesis: bool) -> Option { + fn test_with_instructions(extra_isi: impl IntoIterator) -> Self { let cfg = Configuration::test(); // TODO: Fix this somehow. Probably we need to make `kagami` a library (#3253). @@ -121,14 +125,16 @@ impl TestGenesis for GenesisNetwork { .append_instruction(Grant::permission_token(permission, alice_id.clone()).into()); } - if submit_genesis { - return Some( - GenesisNetwork::from_configuration(genesis, Some(&cfg.genesis)) - .expect("Failed to init genesis"), - ); + for isi in extra_isi.into_iter() { + first_transaction.append_instruction(isi); } - None + let key_pair = KeyPair::new( + cfg.genesis.public_key.clone(), + cfg.genesis.private_key.expect("Should be"), + ) + .expect("Genesis key pair should be valid"); + GenesisNetwork::new(genesis, &key_pair).expect("Failed to init genesis") } } @@ -215,7 +221,7 @@ impl Network { let peer = PeerBuilder::new() .with_configuration(config) - .with_into_genesis(GenesisNetwork::test(false)) + .with_genesis(GenesisNetwork::test()) .start() .await; @@ -256,7 +262,7 @@ impl Network { (n, builder) } }) - .map(|(n, builder)| builder.with_into_genesis(GenesisNetwork::test(n == 0))) + .map(|(n, builder)| builder.with_into_genesis((n == 0).then(GenesisNetwork::test))) .take(n_peers as usize) .collect::>(); let mut peers = builders @@ -408,7 +414,6 @@ impl Peer { }), public_key: self.key_pair.public_key().clone(), private_key: self.key_pair.private_key().clone(), - disable_panic_terminal_colors: true, ..configuration } } @@ -432,7 +437,7 @@ impl Peer { let handle = task::spawn( async move { - let mut iroha = Iroha::with_genesis(genesis, configuration, logger) + let mut iroha = Iroha::new(configuration, genesis, logger) .await .expect("Failed to start iroha"); let job_handle = iroha.start_as_task().unwrap(); @@ -556,8 +561,8 @@ impl PeerBuilder { /// Set the test genesis network. #[must_use] - pub fn with_test_genesis(self, submit_genesis: bool) -> Self { - self.with_into_genesis(GenesisNetwork::test(submit_genesis)) + pub fn with_test_genesis(self) -> Self { + self.with_into_genesis(GenesisNetwork::test()) } /// Set Iroha configuration @@ -603,7 +608,7 @@ impl PeerBuilder { config }); let genesis = match self.genesis { - WithGenesis::Default => GenesisNetwork::test(true), + WithGenesis::Default => Some(GenesisNetwork::test()), WithGenesis::None => None, WithGenesis::Has(genesis) => Some(genesis), }; diff --git a/docker-compose.dev.local.yml b/docker-compose.dev.local.yml index a1d0855dc04..b7e9c831877 100644 --- a/docker-compose.dev.local.yml +++ b/docker-compose.dev.local.yml @@ -7,12 +7,14 @@ services: build: ./ platform: linux/amd64 environment: + IROHA_CONFIG: /config/config.json IROHA_PUBLIC_KEY: ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB IROHA_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb"}' TORII_P2P_ADDR: iroha0:1337 TORII_API_URL: iroha0:8080 - IROHA_GENESIS_ACCOUNT_PUBLIC_KEY: ed01204164BF554923ECE1FD412D241036D863A6AE430476C898248B8237D77534CFC4 - IROHA_GENESIS_ACCOUNT_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"82b3bde54aebeca4146257da0de8d59d8e46d5fe34887dcd8072866792fcb3ad4164bf554923ece1fd412d241036d863a6ae430476c898248b8237d77534cfc4"}' + IROHA_GENESIS_PUBLIC_KEY: ed01204164BF554923ECE1FD412D241036D863A6AE430476C898248B8237D77534CFC4 + IROHA_GENESIS_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"82b3bde54aebeca4146257da0de8d59d8e46d5fe34887dcd8072866792fcb3ad4164bf554923ece1fd412d241036d863a6ae430476c898248b8237d77534cfc4"}' + IROHA_GENESIS_FILE: /config/genesis.json SUMERAGI_TRUSTED_PEERS: '[{"address":"iroha1:1338","public_key":"ed0120815BBDC9775D28C3633269B25F22D048E2AA2E36017CBE5AD85F15220BEB6F6F"},{"address":"iroha3:1340","public_key":"ed0120A66522370D60B9C09E79ADE2E9BB1EF2E78733A944B999B3A6AEE687CE476D61"},{"address":"iroha2:1339","public_key":"ed0120F417E0371E6ADB32FD66749477402B1AB67F84A8E9B082E997980CC91F327736"}]' ports: - 1337:1337 @@ -25,11 +27,12 @@ services: build: ./ platform: linux/amd64 environment: + IROHA_CONFIG: /config/config.json IROHA_PUBLIC_KEY: ed0120815BBDC9775D28C3633269B25F22D048E2AA2E36017CBE5AD85F15220BEB6F6F IROHA_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"c02ffad5e455e7ec620d74de5769681e4d8385906bce5a437eb67452a9efbbc2815bbdc9775d28c3633269b25f22d048e2aa2e36017cbe5ad85f15220beb6f6f"}' TORII_P2P_ADDR: iroha1:1338 TORII_API_URL: iroha1:8081 - IROHA_GENESIS_ACCOUNT_PUBLIC_KEY: ed01204164BF554923ECE1FD412D241036D863A6AE430476C898248B8237D77534CFC4 + IROHA_GENESIS_PUBLIC_KEY: ed01204164BF554923ECE1FD412D241036D863A6AE430476C898248B8237D77534CFC4 SUMERAGI_TRUSTED_PEERS: '[{"address":"iroha0:1337","public_key":"ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB"},{"address":"iroha3:1340","public_key":"ed0120A66522370D60B9C09E79ADE2E9BB1EF2E78733A944B999B3A6AEE687CE476D61"},{"address":"iroha2:1339","public_key":"ed0120F417E0371E6ADB32FD66749477402B1AB67F84A8E9B082E997980CC91F327736"}]' ports: - 1338:1338 @@ -41,11 +44,12 @@ services: build: ./ platform: linux/amd64 environment: + IROHA_CONFIG: /config/config.json IROHA_PUBLIC_KEY: ed0120F417E0371E6ADB32FD66749477402B1AB67F84A8E9B082E997980CC91F327736 IROHA_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"29c5ed1409cb10fd791bc4ff8a6cb5e22a5fae7e36f448ef3ea2988b1319a88bf417e0371e6adb32fd66749477402b1ab67f84a8e9b082e997980cc91f327736"}' TORII_P2P_ADDR: iroha2:1339 TORII_API_URL: iroha2:8082 - IROHA_GENESIS_ACCOUNT_PUBLIC_KEY: ed01204164BF554923ECE1FD412D241036D863A6AE430476C898248B8237D77534CFC4 + IROHA_GENESIS_PUBLIC_KEY: ed01204164BF554923ECE1FD412D241036D863A6AE430476C898248B8237D77534CFC4 SUMERAGI_TRUSTED_PEERS: '[{"address":"iroha1:1338","public_key":"ed0120815BBDC9775D28C3633269B25F22D048E2AA2E36017CBE5AD85F15220BEB6F6F"},{"address":"iroha0:1337","public_key":"ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB"},{"address":"iroha3:1340","public_key":"ed0120A66522370D60B9C09E79ADE2E9BB1EF2E78733A944B999B3A6AEE687CE476D61"}]' ports: - 1339:1339 @@ -57,11 +61,12 @@ services: build: ./ platform: linux/amd64 environment: + IROHA_CONFIG: /config/config.json IROHA_PUBLIC_KEY: ed0120A66522370D60B9C09E79ADE2E9BB1EF2E78733A944B999B3A6AEE687CE476D61 IROHA_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"5eed4855fad183c451aac39dfc50831607e4cf408c98e2b977f3ce4a2df42ce2a66522370d60b9c09e79ade2e9bb1ef2e78733a944b999b3a6aee687ce476d61"}' TORII_P2P_ADDR: iroha3:1340 TORII_API_URL: iroha3:8083 - IROHA_GENESIS_ACCOUNT_PUBLIC_KEY: ed01204164BF554923ECE1FD412D241036D863A6AE430476C898248B8237D77534CFC4 + IROHA_GENESIS_PUBLIC_KEY: ed01204164BF554923ECE1FD412D241036D863A6AE430476C898248B8237D77534CFC4 SUMERAGI_TRUSTED_PEERS: '[{"address":"iroha1:1338","public_key":"ed0120815BBDC9775D28C3633269B25F22D048E2AA2E36017CBE5AD85F15220BEB6F6F"},{"address":"iroha0:1337","public_key":"ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB"},{"address":"iroha2:1339","public_key":"ed0120F417E0371E6ADB32FD66749477402B1AB67F84A8E9B082E997980CC91F327736"}]' ports: - 1340:1340 diff --git a/docker-compose.dev.single.yml b/docker-compose.dev.single.yml index b817eece0c8..a6d0af1cdae 100644 --- a/docker-compose.dev.single.yml +++ b/docker-compose.dev.single.yml @@ -7,12 +7,14 @@ services: build: ./ platform: linux/amd64 environment: + IROHA_CONFIG: /config/config.json IROHA_PUBLIC_KEY: ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB IROHA_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb"}' TORII_P2P_ADDR: iroha0:1337 TORII_API_URL: iroha0:8080 - IROHA_GENESIS_ACCOUNT_PUBLIC_KEY: ed01204164BF554923ECE1FD412D241036D863A6AE430476C898248B8237D77534CFC4 - IROHA_GENESIS_ACCOUNT_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"82b3bde54aebeca4146257da0de8d59d8e46d5fe34887dcd8072866792fcb3ad4164bf554923ece1fd412d241036d863a6ae430476c898248b8237d77534cfc4"}' + IROHA_GENESIS_PUBLIC_KEY: ed01204164BF554923ECE1FD412D241036D863A6AE430476C898248B8237D77534CFC4 + IROHA_GENESIS_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"82b3bde54aebeca4146257da0de8d59d8e46d5fe34887dcd8072866792fcb3ad4164bf554923ece1fd412d241036d863a6ae430476c898248b8237d77534cfc4"}' + IROHA_GENESIS_FILE: /config/genesis.json ports: - 1337:1337 - 8080:8080 diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 54404425d99..a483a1c1b16 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -7,12 +7,14 @@ services: image: hyperledger/iroha2:dev platform: linux/amd64 environment: + IROHA_CONFIG: /config/config.json IROHA_PUBLIC_KEY: ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB IROHA_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb"}' TORII_P2P_ADDR: iroha0:1337 TORII_API_URL: iroha0:8080 - IROHA_GENESIS_ACCOUNT_PUBLIC_KEY: ed01204164BF554923ECE1FD412D241036D863A6AE430476C898248B8237D77534CFC4 - IROHA_GENESIS_ACCOUNT_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"82b3bde54aebeca4146257da0de8d59d8e46d5fe34887dcd8072866792fcb3ad4164bf554923ece1fd412d241036d863a6ae430476c898248b8237d77534cfc4"}' + IROHA_GENESIS_PUBLIC_KEY: ed01204164BF554923ECE1FD412D241036D863A6AE430476C898248B8237D77534CFC4 + IROHA_GENESIS_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"82b3bde54aebeca4146257da0de8d59d8e46d5fe34887dcd8072866792fcb3ad4164bf554923ece1fd412d241036d863a6ae430476c898248b8237d77534cfc4"}' + IROHA_GENESIS_FILE: /config/genesis.json SUMERAGI_TRUSTED_PEERS: '[{"address":"iroha1:1338","public_key":"ed0120815BBDC9775D28C3633269B25F22D048E2AA2E36017CBE5AD85F15220BEB6F6F"},{"address":"iroha3:1340","public_key":"ed0120A66522370D60B9C09E79ADE2E9BB1EF2E78733A944B999B3A6AEE687CE476D61"},{"address":"iroha2:1339","public_key":"ed0120F417E0371E6ADB32FD66749477402B1AB67F84A8E9B082E997980CC91F327736"}]' ports: - 1337:1337 @@ -25,11 +27,12 @@ services: image: hyperledger/iroha2:dev platform: linux/amd64 environment: + IROHA_CONFIG: /config/config.json IROHA_PUBLIC_KEY: ed0120815BBDC9775D28C3633269B25F22D048E2AA2E36017CBE5AD85F15220BEB6F6F IROHA_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"c02ffad5e455e7ec620d74de5769681e4d8385906bce5a437eb67452a9efbbc2815bbdc9775d28c3633269b25f22d048e2aa2e36017cbe5ad85f15220beb6f6f"}' TORII_P2P_ADDR: iroha1:1338 TORII_API_URL: iroha1:8081 - IROHA_GENESIS_ACCOUNT_PUBLIC_KEY: ed01204164BF554923ECE1FD412D241036D863A6AE430476C898248B8237D77534CFC4 + IROHA_GENESIS_PUBLIC_KEY: ed01204164BF554923ECE1FD412D241036D863A6AE430476C898248B8237D77534CFC4 SUMERAGI_TRUSTED_PEERS: '[{"address":"iroha0:1337","public_key":"ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB"},{"address":"iroha3:1340","public_key":"ed0120A66522370D60B9C09E79ADE2E9BB1EF2E78733A944B999B3A6AEE687CE476D61"},{"address":"iroha2:1339","public_key":"ed0120F417E0371E6ADB32FD66749477402B1AB67F84A8E9B082E997980CC91F327736"}]' ports: - 1338:1338 @@ -41,11 +44,12 @@ services: image: hyperledger/iroha2:dev platform: linux/amd64 environment: + IROHA_CONFIG: /config/config.json IROHA_PUBLIC_KEY: ed0120F417E0371E6ADB32FD66749477402B1AB67F84A8E9B082E997980CC91F327736 IROHA_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"29c5ed1409cb10fd791bc4ff8a6cb5e22a5fae7e36f448ef3ea2988b1319a88bf417e0371e6adb32fd66749477402b1ab67f84a8e9b082e997980cc91f327736"}' TORII_P2P_ADDR: iroha2:1339 TORII_API_URL: iroha2:8082 - IROHA_GENESIS_ACCOUNT_PUBLIC_KEY: ed01204164BF554923ECE1FD412D241036D863A6AE430476C898248B8237D77534CFC4 + IROHA_GENESIS_PUBLIC_KEY: ed01204164BF554923ECE1FD412D241036D863A6AE430476C898248B8237D77534CFC4 SUMERAGI_TRUSTED_PEERS: '[{"address":"iroha1:1338","public_key":"ed0120815BBDC9775D28C3633269B25F22D048E2AA2E36017CBE5AD85F15220BEB6F6F"},{"address":"iroha0:1337","public_key":"ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB"},{"address":"iroha3:1340","public_key":"ed0120A66522370D60B9C09E79ADE2E9BB1EF2E78733A944B999B3A6AEE687CE476D61"}]' ports: - 1339:1339 @@ -57,11 +61,12 @@ services: image: hyperledger/iroha2:dev platform: linux/amd64 environment: + IROHA_CONFIG: /config/config.json IROHA_PUBLIC_KEY: ed0120A66522370D60B9C09E79ADE2E9BB1EF2E78733A944B999B3A6AEE687CE476D61 IROHA_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"5eed4855fad183c451aac39dfc50831607e4cf408c98e2b977f3ce4a2df42ce2a66522370d60b9c09e79ade2e9bb1ef2e78733a944b999b3a6aee687ce476d61"}' TORII_P2P_ADDR: iroha3:1340 TORII_API_URL: iroha3:8083 - IROHA_GENESIS_ACCOUNT_PUBLIC_KEY: ed01204164BF554923ECE1FD412D241036D863A6AE430476C898248B8237D77534CFC4 + IROHA_GENESIS_PUBLIC_KEY: ed01204164BF554923ECE1FD412D241036D863A6AE430476C898248B8237D77534CFC4 SUMERAGI_TRUSTED_PEERS: '[{"address":"iroha1:1338","public_key":"ed0120815BBDC9775D28C3633269B25F22D048E2AA2E36017CBE5AD85F15220BEB6F6F"},{"address":"iroha0:1337","public_key":"ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB"},{"address":"iroha2:1339","public_key":"ed0120F417E0371E6ADB32FD66749477402B1AB67F84A8E9B082E997980CC91F327736"}]' ports: - 1340:1340 diff --git a/genesis/Cargo.toml b/genesis/Cargo.toml index 994afeb51a9..7727bc779c0 100644 --- a/genesis/Cargo.toml +++ b/genesis/Cargo.toml @@ -11,11 +11,8 @@ license.workspace = true workspace = true [dependencies] -iroha_config = { workspace = true } iroha_crypto = { workspace = true } iroha_data_model = { workspace = true, features = ["http"] } -iroha_logger = { workspace = true } -iroha_primitives = { workspace = true } iroha_schema = { workspace = true } derive_more = { workspace = true, features = ["deref"] } diff --git a/genesis/src/lib.rs b/genesis/src/lib.rs index 84cd9964e49..d32ebb22405 100644 --- a/genesis/src/lib.rs +++ b/genesis/src/lib.rs @@ -8,8 +8,7 @@ use std::{ }; use derive_more::From; -use eyre::{bail, eyre, ErrReport, Result, WrapErr}; -use iroha_config::genesis::Configuration; +use eyre::{eyre, ErrReport, Result, WrapErr}; use iroha_crypto::{KeyPair, PublicKey}; use iroha_data_model::{ asset::AssetDefinition, @@ -34,58 +33,50 @@ pub struct GenesisTransaction(pub SignedTransaction); /// [`GenesisNetwork`] contains initial transactions and genesis setup related parameters. #[derive(Debug, Clone)] pub struct GenesisNetwork { - /// transactions from `GenesisBlock`, any transacton is accepted - pub transactions: Vec, + /// Transactions from [`RawGenesisBlock`]. This vector is guaranteed to be non-empty, + /// unless [`GenesisNetwork::transactions_mut()`] is used. + transactions: Vec, } impl GenesisNetwork { /// Construct [`GenesisNetwork`] from configuration. /// /// # Errors - /// Fails if genesis block is not found or cannot be deserialized. - pub fn from_configuration( - raw_block: RawGenesisBlock, - genesis_config: Option<&Configuration>, - ) -> Result { - iroha_logger::debug!("Submitting genesis."); - let genesis_config = - genesis_config.expect("Should be `Some` when `submit_genesis` is true"); - let genesis_key_pair = KeyPair::new( - genesis_config.account_public_key.clone(), - genesis_config - .account_private_key - .clone() - .ok_or_else(|| eyre!("Genesis account private key is empty."))?, - )?; - #[cfg(not(test))] + /// - If fails to sign a transaction (which means that the `key_pair` is malformed rather + /// than anything else) + /// - If transactions set is empty + pub fn new(raw_block: RawGenesisBlock, genesis_key_pair: &KeyPair) -> Result { // First instruction should be Executor upgrade. // This makes possible to grant permissions to users in genesis. let transactions_iter = std::iter::once(GenesisTransactionBuilder { - isi: vec![Upgrade::new(Executor::try_from(raw_block.executor)?).into()], + isi: vec![Upgrade::new( + Executor::try_from(raw_block.executor) + .wrap_err("Failed to construct the executor")?, + ) + .into()], }) .chain(raw_block.transactions); - #[cfg(test)] - let transactions_iter = raw_block.transactions.into_iter(); - let transactions = transactions_iter - .map(|raw_transaction| { - raw_transaction.sign(genesis_key_pair.clone()) - }) .enumerate() - .filter_map(|(i, res)| { - res.map_err(|error| { - let error_msg = format!("{error:#}"); - iroha_logger::error!(error = %error_msg, transaction_num=i, "Genesis transaction failed") - }) - .ok() - }).map(GenesisTransaction) - .collect::>(); - if transactions.is_empty() { - bail!("Genesis transaction set contains no valid transactions"); - } + .map(|(i, raw_transaction)| { + raw_transaction + // FIXME: fix underlying chain of `.sign` so that it doesn't + // consume the key pair unnecessarily. It might be costly to clone + // the key pair for a large genesis. + .sign(genesis_key_pair.clone()) + .map(GenesisTransaction) + .wrap_err_with(|| eyre!("Failed to sign transaction at index {i}")) + }) + .collect::>>()?; + Ok(GenesisNetwork { transactions }) } + + /// Consume `self` into genesis transactions + pub fn into_transactions(self) -> Vec { + self.transactions + } } /// [`RawGenesisBlock`] is an initial block of the network @@ -106,19 +97,18 @@ impl RawGenesisBlock { /// # Errors /// If file not found or deserialization from file fails. pub fn from_path + Debug>(path: P) -> Result { - let file = File::open(&path).wrap_err(format!("Failed to open {:?}", &path))?; + let file = File::open(&path) + .wrap_err_with(|| eyre!("Failed to open {}", path.as_ref().display()))?; let size = file .metadata() .wrap_err("Unable to access genesis file metadata")? .len(); if size >= Self::WARN_ON_GENESIS_GTE { - iroha_logger::warn!(%size, threshold = %Self::WARN_ON_GENESIS_GTE, "Genesis is quite large, it will take some time to apply it"); + eprintln!("Genesis is quite large, it will take some time to apply it (size = {}, threshold = {})", size, Self::WARN_ON_GENESIS_GTE); } let reader = BufReader::new(file); - let mut raw_genesis_block: Self = serde_json::from_reader(reader).wrap_err(format!( - "Failed to deserialize raw genesis block from {:?}", - &path - ))?; + let mut raw_genesis_block: Self = serde_json::from_reader(reader) + .wrap_err_with(|| eyre!("Failed to deserialize raw genesis block from {:?}", &path))?; raw_genesis_block.executor.set_genesis_path(path); Ok(raw_genesis_block) } @@ -363,33 +353,27 @@ impl RawGenesisDomainBuilder { #[cfg(test)] mod tests { - use iroha_config::{base::proxy::Builder, genesis::ConfigurationProxy}; use super::*; fn dummy_executor() -> ExecutorMode { - ExecutorMode::Path(ExecutorPath("./executor.wasm".into())) + ExecutorMode::Inline(Executor::new(WasmSmartContract::from_compiled(vec![ + 1, 2, 3, + ]))) } #[test] fn load_new_genesis_block() -> Result<()> { - let (genesis_public_key, genesis_private_key) = KeyPair::generate()?.into(); + let genesis_key_pair = KeyPair::generate()?; let (alice_public_key, _) = KeyPair::generate()?.into(); - let _genesis_block = GenesisNetwork::from_configuration( + let _genesis_block = GenesisNetwork::new( RawGenesisBlockBuilder::default() .domain("wonderland".parse()?) .account("alice".parse()?, alice_public_key) .finish_domain() .executor(dummy_executor()) .build(), - Some( - &ConfigurationProxy { - account_public_key: Some(genesis_public_key), - account_private_key: Some(Some(genesis_private_key)), - } - .build() - .expect("Default genesis config should build when provided the `public key`"), - ), + &genesis_key_pair, )?; Ok(()) } diff --git a/scripts/test_env.py b/scripts/test_env.py index 458fc0ae6c1..cd72d89c5aa 100755 --- a/scripts/test_env.py +++ b/scripts/test_env.py @@ -29,29 +29,32 @@ def __init__(self, args: argparse.Namespace): self.out_dir = args.out_dir peers_dir = args.out_dir.joinpath("peers") os.makedirs(peers_dir, exist_ok=True) + self.shared_env = dict(os.environ) + + self.peers = [_Peer(args, i) for i in range(args.n_peers)] + try: shutil.copy2(f"{args.root_dir}/configs/peer/config.json", peers_dir) - shutil.copy2(f"{args.root_dir}/configs/peer/genesis.json", peers_dir) - shutil.copy2(f"{args.root_dir}/configs/peer/executor.wasm", peers_dir) + # genesis should be supplied only for the first peer + peer_0_dir = self.peers[0].peer_dir + shutil.copy2(f"{args.root_dir}/configs/peer/genesis.json", peer_0_dir) + # assuming that `genesis.json` contains path to the executor as `./executor.wasm` + shutil.copy2(f"{args.root_dir}/configs/peer/executor.wasm", peer_0_dir) except FileNotFoundError: logging.error(f"Some of the config files are missing. \ Please provide them in the `{args.root_dir}/configs/peer` directory") sys.exit(1) copy_or_prompt_build_bin("iroha", args.root_dir, peers_dir) - self.peers = [_Peer(args, i) for i in range(args.n_peers)] - - os.environ["IROHA2_CONFIG_PATH"] = str(peers_dir.joinpath("config.json")) - os.environ["IROHA2_GENESIS_PATH"] = str(peers_dir.joinpath("genesis.json")) - os.environ["IROHA_GENESIS_ACCOUNT_PUBLIC_KEY"] = self.peers[0].public_key - os.environ["IROHA_GENESIS_ACCOUNT_PRIVATE_KEY"] = self.peers[0].private_key + self.shared_env["IROHA_CONFIG"] = str(peers_dir.joinpath("config.json")) + self.shared_env["IROHA_GENESIS_PUBLIC_KEY"] = self.peers[0].public_key logging.info("Generating trusted peers...") self.trusted_peers = [] for peer in self.peers: peer_entry = {"address": f"{peer.host_ip}:{peer.p2p_port}", "public_key": peer.public_key} self.trusted_peers.append(json.dumps(peer_entry)) - os.environ["SUMERAGI_TRUSTED_PEERS"] = f"[{','.join(self.trusted_peers)}]" + self.shared_env["SUMERAGI_TRUSTED_PEERS"] = f"[{','.join(self.trusted_peers)}]" def wait_for_genesis(self, n_tries: int): for i in range(n_tries): @@ -73,9 +76,8 @@ def wait_for_genesis(self, n_tries: int): sys.exit(2) def run(self): - self.peers[0].run(is_genesis=True) - for peer in self.peers[1:]: - peer.run() + for i, peer in enumerate(self.peers): + peer.run(shared_env=self.shared_env, submit_genesis=(i == 0)) self.wait_for_genesis(20) class _Peer: @@ -91,6 +93,7 @@ def __init__(self, args: argparse.Namespace, nth: int): self.tokio_console_port = 5555 + nth self.out_dir = args.out_dir self.root_dir = args.root_dir + self.peer_dir = self.out_dir.joinpath(f"peers/{self.name}") self.host_ip = args.host_ip logging.info(f"Peer {self.name} generating key pair...") @@ -108,31 +111,37 @@ def __init__(self, args: argparse.Namespace, nth: int): self.public_key = json_keypair['public_key'] self.private_key = json.dumps(json_keypair['private_key']) + os.makedirs(self.peer_dir, exist_ok=True) + os.makedirs(self.peer_dir.joinpath("storage"), exist_ok=True) + logging.info(f"Peer {self.name} initialized") - def run(self, is_genesis: bool = False): + def run(self, shared_env: dict(), submit_genesis: bool = False): logging.info(f"Running peer {self.name}...") - peer_dir = self.out_dir.joinpath(f"peers/{self.name}") - os.makedirs(peer_dir, exist_ok=True) - os.makedirs(peer_dir.joinpath("storage"), exist_ok=True) - - os.environ["KURA_BLOCK_STORE_PATH"] = str(peer_dir.joinpath("storage")) - os.environ["SNAPSHOT_DIR_PATH"] = str(peer_dir.joinpath("storage")) - os.environ["LOG_LEVEL"] = "TRACE" - os.environ["LOG_FORMAT"] = "\"pretty\"" - os.environ["LOG_TOKIO_CONSOLE_ADDR"] = f"{self.host_ip}:{self.tokio_console_port}" - os.environ["IROHA_PUBLIC_KEY"] = self.public_key - os.environ["IROHA_PRIVATE_KEY"] = self.private_key - os.environ["SUMERAGI_DEBUG_FORCE_SOFT_FORK"] = "false" - os.environ["TORII_P2P_ADDR"] = f"{self.host_ip}:{self.p2p_port}" - os.environ["TORII_API_URL"] = f"{self.host_ip}:{self.api_port}" - - genesis_arg = "--submit-genesis" if is_genesis else "" + + peer_env = dict(shared_env) + peer_env["KURA_BLOCK_STORE_PATH"] = str(self.peer_dir.joinpath("storage")) + peer_env["SNAPSHOT_DIR_PATH"] = str(self.peer_dir.joinpath("storage")) + peer_env["LOG_LEVEL"] = "INFO" + peer_env["LOG_FORMAT"] = "\"pretty\"" + peer_env["LOG_TOKIO_CONSOLE_ADDR"] = f"{self.host_ip}:{self.tokio_console_port}" + peer_env["IROHA_PUBLIC_KEY"] = self.public_key + peer_env["IROHA_PRIVATE_KEY"] = self.private_key + peer_env["SUMERAGI_DEBUG_FORCE_SOFT_FORK"] = "false" + peer_env["TORII_P2P_ADDR"] = f"{self.host_ip}:{self.p2p_port}" + peer_env["TORII_API_URL"] = f"{self.host_ip}:{self.api_port}" + + if submit_genesis: + peer_env["IROHA_GENESIS_PRIVATE_KEY"] = self.private_key + # Assuming it was copied to the peer's directory + peer_env["IROHA_GENESIS_FILE"] = str(self.peer_dir.joinpath("genesis.json")) + # FD never gets closed - log_file = open(peer_dir.joinpath(".log"), "w") + stdout_file = open(self.peer_dir.joinpath(".stdout"), "w") + stderr_file = open(self.peer_dir.joinpath(".stderr"), "w") # These processes are created detached from the parent process already - subprocess.Popen([self.name, genesis_arg], executable=f"{self.out_dir}/peers/iroha", - stdout=log_file, stderr=subprocess.STDOUT) + subprocess.Popen([self.name] + (["--submit-genesis"] if submit_genesis else []), + executable=f"{self.out_dir}/peers/iroha", env=peer_env, stdout=stdout_file, stderr=stderr_file) def pos_int(arg): if int(arg) > 0: diff --git a/tools/kagami/src/config.rs b/tools/kagami/src/config.rs index 2d33d5b4a4b..e36b53fcd18 100644 --- a/tools/kagami/src/config.rs +++ b/tools/kagami/src/config.rs @@ -6,13 +6,13 @@ use iroha_primitives::small::SmallStr; use super::*; -#[derive(Parser, Debug, Clone, Copy)] +#[derive(Parser, Debug, Clone)] pub struct Args { #[clap(subcommand)] mode: Mode, } -#[derive(Subcommand, Debug, Clone, Copy)] +#[derive(Subcommand, Debug, Clone)] pub enum Mode { Client(client::Args), Peer(peer::Args), @@ -64,16 +64,30 @@ mod client { } mod peer { + use std::path::PathBuf; + use iroha_config::iroha::ConfigurationProxy as IrohaConfigurationProxy; use super::*; - #[derive(ClapArgs, Debug, Clone, Copy)] - pub struct Args; + #[derive(ClapArgs, Debug, Clone)] + pub struct Args { + /// Specifies the value of `genesis.file` configuration parameter. + /// + /// Note: relative paths are not resolved but included as-is. + #[arg(long, value_name = "PATH")] + genesis_file_in_config: Option, + } impl RunArgs for Args { fn run(self, writer: &mut BufWriter) -> Outcome { - let config = IrohaConfigurationProxy::default(); + let mut config = IrohaConfigurationProxy::default(); + + if let Some(path) = self.genesis_file_in_config { + let genesis = config.genesis.as_mut().unwrap(); + genesis.file = Some(Some(path)); + } + writeln!(writer, "{}", serde_json::to_string_pretty(&config)?) .wrap_err("Failed to write serialized peer configuration to the buffer.") } diff --git a/tools/swarm/src/cli.rs b/tools/swarm/src/cli.rs index eb7f45e9371..f7185e9519c 100644 --- a/tools/swarm/src/cli.rs +++ b/tools/swarm/src/cli.rs @@ -27,7 +27,7 @@ pub struct Cli { pub no_banner: bool, /// Path to a directory with Iroha configuration. It will be mapped as volume for containers. /// - /// The directory should contain `config.json` and `genesis.json`. + /// The directory should contain `config.json` and `genesis.json` #[arg(long, short)] pub config_dir: PathBuf, #[command(flatten)] diff --git a/tools/swarm/src/compose.rs b/tools/swarm/src/compose.rs index 36c43317750..80dc51f7ac4 100644 --- a/tools/swarm/src/compose.rs +++ b/tools/swarm/src/compose.rs @@ -20,6 +20,8 @@ use crate::{cli::SourceParsed, util::AbsolutePath}; /// Config directory inside of the docker image const DIR_CONFIG_IN_DOCKER: &str = "/config"; +const PATH_TO_CONFIG: &str = "/config/config.json"; +const PATH_TO_GENESIS: &str = "/config/genesis.json"; const GENESIS_KEYPAIR_SEED: &[u8; 7] = b"genesis"; const COMMAND_SUBMIT_GENESIS: &str = "iroha --submit-genesis"; const DOCKER_COMPOSE_VERSION: &str = "3.8"; @@ -105,7 +107,7 @@ impl DockerComposeService { source: ServiceSource, volumes: Vec<(String, String)>, trusted_peers: BTreeSet, - genesis_public_key: Option, + genesis_public_key: PublicKey, genesis_private_key: Option, ) -> Self { let ports = vec![ @@ -207,21 +209,23 @@ pub enum ServiceSource { #[derive(Serialize, Debug)] #[serde(rename_all = "UPPERCASE")] struct FullPeerEnv { + iroha_config: String, iroha_public_key: PublicKey, iroha_private_key: SerializeAsJsonStr, torii_p2p_addr: SocketAddr, torii_api_url: SocketAddr, + iroha_genesis_public_key: PublicKey, #[serde(skip_serializing_if = "Option::is_none")] - iroha_genesis_account_public_key: Option, + iroha_genesis_private_key: Option>, #[serde(skip_serializing_if = "Option::is_none")] - iroha_genesis_account_private_key: Option>, + iroha_genesis_file: Option, #[serde(skip_serializing_if = "Option::is_none")] sumeragi_trusted_peers: Option>>, } struct CompactPeerEnv { key_pair: KeyPair, - genesis_public_key: Option, + genesis_public_key: PublicKey, /// Genesis private key is only needed for a peer that is submitting the genesis block genesis_private_key: Option, p2p_addr: SocketAddr, @@ -231,11 +235,23 @@ struct CompactPeerEnv { impl From for FullPeerEnv { fn from(value: CompactPeerEnv) -> Self { + let (iroha_genesis_private_key, iroha_genesis_file) = + value + .genesis_private_key + .map_or((None, None), |private_key| { + ( + Some(private_key).map(SerializeAsJsonStr), + Some(PATH_TO_GENESIS.to_string()), + ) + }); + Self { + iroha_config: PATH_TO_CONFIG.to_string(), iroha_public_key: value.key_pair.public_key().clone(), iroha_private_key: SerializeAsJsonStr(value.key_pair.private_key().clone()), - iroha_genesis_account_public_key: value.genesis_public_key, - iroha_genesis_account_private_key: value.genesis_private_key.map(SerializeAsJsonStr), + iroha_genesis_public_key: value.genesis_public_key, + iroha_genesis_private_key, + iroha_genesis_file, torii_p2p_addr: value.p2p_addr, torii_api_url: value.api_addr, sumeragi_trusted_peers: if value.trusted_peers.is_empty() { @@ -321,7 +337,7 @@ impl DockerComposeBuilder<'_> { .filter(|trusted_peer| trusted_peer.public_key() != peer.key_pair.public_key()) .cloned() .collect(), - Some(genesis_key_pair.public_key().clone()), + genesis_key_pair.public_key().clone(), Some(genesis_key_pair.private_key().clone()), ); @@ -341,7 +357,7 @@ impl DockerComposeBuilder<'_> { }) .cloned() .collect(), - Some(genesis_key_pair.public_key().clone()), + genesis_key_pair.public_key().clone(), None, ); @@ -542,11 +558,11 @@ mod tests { } #[test] - fn default_config_with_swarm_env_are_exhaustive() { + fn default_config_with_swarm_env_is_exhaustive() { let keypair = KeyPair::generate().unwrap(); let env: TestEnv = CompactPeerEnv { key_pair: keypair.clone(), - genesis_public_key: Some(keypair.public_key().clone()), + genesis_public_key: keypair.public_key().clone(), genesis_private_key: Some(keypair.private_key().clone()), p2p_addr: SocketAddr::from_str("127.0.0.1:1337").unwrap(), api_addr: SocketAddr::from_str("127.0.0.1:1338").unwrap(), @@ -554,6 +570,8 @@ mod tests { } .into(); + // pretending like we've read `IROHA_CONFIG` env to know the config location + let _ = env.fetch("IROHA_CONFIG").expect("should be presented"); let proxy = ConfigurationProxy::default() .override_with(ConfigurationProxy::from_env(&env).expect("valid env")); @@ -591,7 +609,7 @@ mod tests { source: ServiceSource::Build(PathBuf::from(".")), environment: CompactPeerEnv { key_pair: key_pair.clone(), - genesis_public_key: Some(key_pair.public_key().clone()), + genesis_public_key: key_pair.public_key().clone(), genesis_private_key: Some(key_pair.private_key().clone()), p2p_addr: SocketAddr::from_str("iroha1:1339").unwrap(), api_addr: SocketAddr::from_str("iroha1:1338").unwrap(), @@ -624,12 +642,14 @@ mod tests { build: . platform: linux/amd64 environment: + IROHA_CONFIG: /config/config.json IROHA_PUBLIC_KEY: ed012039E5BF092186FACC358770792A493CA98A83740643A3D41389483CF334F748C8 IROHA_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"db9d90d20f969177bd5882f9fe211d14d1399d5440d04e3468783d169bbc4a8e39e5bf092186facc358770792a493ca98a83740643a3d41389483cf334f748c8"}' TORII_P2P_ADDR: iroha1:1339 TORII_API_URL: iroha1:1338 - IROHA_GENESIS_ACCOUNT_PUBLIC_KEY: ed012039E5BF092186FACC358770792A493CA98A83740643A3D41389483CF334F748C8 - IROHA_GENESIS_ACCOUNT_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"db9d90d20f969177bd5882f9fe211d14d1399d5440d04e3468783d169bbc4a8e39e5bf092186facc358770792a493ca98a83740643a3d41389483cf334f748c8"}' + IROHA_GENESIS_PUBLIC_KEY: ed012039E5BF092186FACC358770792A493CA98A83740643A3D41389483CF334F748C8 + IROHA_GENESIS_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"db9d90d20f969177bd5882f9fe211d14d1399d5440d04e3468783d169bbc4a8e39e5bf092186facc358770792a493ca98a83740643a3d41389483cf334f748c8"}' + IROHA_GENESIS_FILE: /config/genesis.json ports: - 1337:1337 - 8080:8080 @@ -650,7 +670,7 @@ mod tests { .unwrap(); let env: FullPeerEnv = CompactPeerEnv { key_pair: key_pair.clone(), - genesis_public_key: Some(key_pair.public_key().clone()), + genesis_public_key: key_pair.public_key().clone(), genesis_private_key: None, p2p_addr: SocketAddr::from_str("iroha0:1337").unwrap(), api_addr: SocketAddr::from_str("iroha0:1337").unwrap(), @@ -660,11 +680,12 @@ mod tests { let actual = serde_yaml::to_string(&env).unwrap(); let expected = expect_test::expect![[r#" + IROHA_CONFIG: /config/config.json IROHA_PUBLIC_KEY: ed0120415388A90FA238196737746A70565D041CFB32EAA0C89FF8CB244C7F832A6EBD IROHA_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"6bf163fd75192b81a78cb20c5f8cb917f591ac6635f2577e6ca305c27a456a5d415388a90fa238196737746a70565d041cfb32eaa0c89ff8cb244c7f832a6ebd"}' TORII_P2P_ADDR: iroha0:1337 TORII_API_URL: iroha0:1337 - IROHA_GENESIS_ACCOUNT_PUBLIC_KEY: ed0120415388A90FA238196737746A70565D041CFB32EAA0C89FF8CB244C7F832A6EBD + IROHA_GENESIS_PUBLIC_KEY: ed0120415388A90FA238196737746A70565D041CFB32EAA0C89FF8CB244C7F832A6EBD "#]]; expected.assert_eq(&actual); } @@ -698,12 +719,14 @@ mod tests { build: ./iroha-cloned platform: linux/amd64 environment: + IROHA_CONFIG: /config/config.json IROHA_PUBLIC_KEY: ed0120F0321EB4139163C35F88BF78520FF7071499D7F4E79854550028A196C7B49E13 IROHA_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"5f8d1291bf6b762ee748a87182345d135fd167062857aa4f20ba39f25e74c4b0f0321eb4139163c35f88bf78520ff7071499d7f4e79854550028a196c7b49e13"}' TORII_P2P_ADDR: iroha0:1337 TORII_API_URL: iroha0:8080 - IROHA_GENESIS_ACCOUNT_PUBLIC_KEY: ed01203420F48A9EEB12513B8EB7DAF71979CE80A1013F5F341C10DCDA4F6AA19F97A9 - IROHA_GENESIS_ACCOUNT_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"5a6d5f06a90d29ad906e2f6ea8b41b4ef187849d0d397081a4a15ffcbe71e7c73420f48a9eeb12513b8eb7daf71979ce80a1013f5f341c10dcda4f6aa19f97a9"}' + IROHA_GENESIS_PUBLIC_KEY: ed01203420F48A9EEB12513B8EB7DAF71979CE80A1013F5F341C10DCDA4F6AA19F97A9 + IROHA_GENESIS_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"5a6d5f06a90d29ad906e2f6ea8b41b4ef187849d0d397081a4a15ffcbe71e7c73420f48a9eeb12513b8eb7daf71979ce80a1013f5f341c10dcda4f6aa19f97a9"}' + IROHA_GENESIS_FILE: /config/genesis.json SUMERAGI_TRUSTED_PEERS: '[{"address":"iroha2:1339","public_key":"ed0120312C1B7B5DE23D366ADCF23CD6DB92CE18B2AA283C7D9F5033B969C2DC2B92F4"},{"address":"iroha3:1340","public_key":"ed0120854457B2E3D6082181DA73DC01C1E6F93A72D0C45268DC8845755287E98A5DEE"},{"address":"iroha1:1338","public_key":"ed0120A88554AA5C86D28D0EEBEC497235664433E807881CD31E12A1AF6C4D8B0F026C"}]' ports: - 1337:1337 @@ -716,11 +739,12 @@ mod tests { build: ./iroha-cloned platform: linux/amd64 environment: + IROHA_CONFIG: /config/config.json IROHA_PUBLIC_KEY: ed0120A88554AA5C86D28D0EEBEC497235664433E807881CD31E12A1AF6C4D8B0F026C IROHA_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"8d34d2c6a699c61e7a9d5aabbbd07629029dfb4f9a0800d65aa6570113edb465a88554aa5c86d28d0eebec497235664433e807881cd31e12a1af6c4d8b0f026c"}' TORII_P2P_ADDR: iroha1:1338 TORII_API_URL: iroha1:8081 - IROHA_GENESIS_ACCOUNT_PUBLIC_KEY: ed01203420F48A9EEB12513B8EB7DAF71979CE80A1013F5F341C10DCDA4F6AA19F97A9 + IROHA_GENESIS_PUBLIC_KEY: ed01203420F48A9EEB12513B8EB7DAF71979CE80A1013F5F341C10DCDA4F6AA19F97A9 SUMERAGI_TRUSTED_PEERS: '[{"address":"iroha2:1339","public_key":"ed0120312C1B7B5DE23D366ADCF23CD6DB92CE18B2AA283C7D9F5033B969C2DC2B92F4"},{"address":"iroha3:1340","public_key":"ed0120854457B2E3D6082181DA73DC01C1E6F93A72D0C45268DC8845755287E98A5DEE"},{"address":"iroha0:1337","public_key":"ed0120F0321EB4139163C35F88BF78520FF7071499D7F4E79854550028A196C7B49E13"}]' ports: - 1338:1338 @@ -732,11 +756,12 @@ mod tests { build: ./iroha-cloned platform: linux/amd64 environment: + IROHA_CONFIG: /config/config.json IROHA_PUBLIC_KEY: ed0120312C1B7B5DE23D366ADCF23CD6DB92CE18B2AA283C7D9F5033B969C2DC2B92F4 IROHA_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"cf4515a82289f312868027568c0da0ee3f0fde7fef1b69deb47b19fde7cbc169312c1b7b5de23d366adcf23cd6db92ce18b2aa283c7d9f5033b969c2dc2b92f4"}' TORII_P2P_ADDR: iroha2:1339 TORII_API_URL: iroha2:8082 - IROHA_GENESIS_ACCOUNT_PUBLIC_KEY: ed01203420F48A9EEB12513B8EB7DAF71979CE80A1013F5F341C10DCDA4F6AA19F97A9 + IROHA_GENESIS_PUBLIC_KEY: ed01203420F48A9EEB12513B8EB7DAF71979CE80A1013F5F341C10DCDA4F6AA19F97A9 SUMERAGI_TRUSTED_PEERS: '[{"address":"iroha3:1340","public_key":"ed0120854457B2E3D6082181DA73DC01C1E6F93A72D0C45268DC8845755287E98A5DEE"},{"address":"iroha1:1338","public_key":"ed0120A88554AA5C86D28D0EEBEC497235664433E807881CD31E12A1AF6C4D8B0F026C"},{"address":"iroha0:1337","public_key":"ed0120F0321EB4139163C35F88BF78520FF7071499D7F4E79854550028A196C7B49E13"}]' ports: - 1339:1339 @@ -748,11 +773,12 @@ mod tests { build: ./iroha-cloned platform: linux/amd64 environment: + IROHA_CONFIG: /config/config.json IROHA_PUBLIC_KEY: ed0120854457B2E3D6082181DA73DC01C1E6F93A72D0C45268DC8845755287E98A5DEE IROHA_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"ab0e99c2b845b4ac7b3e88d25a860793c7eb600a25c66c75cba0bae91e955aa6854457b2e3d6082181da73dc01c1e6f93a72d0c45268dc8845755287e98a5dee"}' TORII_P2P_ADDR: iroha3:1340 TORII_API_URL: iroha3:8083 - IROHA_GENESIS_ACCOUNT_PUBLIC_KEY: ed01203420F48A9EEB12513B8EB7DAF71979CE80A1013F5F341C10DCDA4F6AA19F97A9 + IROHA_GENESIS_PUBLIC_KEY: ed01203420F48A9EEB12513B8EB7DAF71979CE80A1013F5F341C10DCDA4F6AA19F97A9 SUMERAGI_TRUSTED_PEERS: '[{"address":"iroha2:1339","public_key":"ed0120312C1B7B5DE23D366ADCF23CD6DB92CE18B2AA283C7D9F5033B969C2DC2B92F4"},{"address":"iroha1:1338","public_key":"ed0120A88554AA5C86D28D0EEBEC497235664433E807881CD31E12A1AF6C4D8B0F026C"},{"address":"iroha0:1337","public_key":"ed0120F0321EB4139163C35F88BF78520FF7071499D7F4E79854550028A196C7B49E13"}]' ports: - 1340:1340 diff --git a/torii/src/lib.rs b/torii/src/lib.rs index 31245d6ffc0..83d9ce10a26 100644 --- a/torii/src/lib.rs +++ b/torii/src/lib.rs @@ -204,7 +204,7 @@ impl Torii { .with(warp::trace::request())) } - /// Start main api endpoints. + /// Start main API endpoints. /// /// # Errors /// Can fail due to listening to network or if http server fails @@ -268,19 +268,19 @@ pub enum Error { Query(#[from] iroha_data_model::ValidationFail), /// Failed to accept transaction AcceptTransaction(#[from] iroha_core::tx::AcceptTransactionFail), - /// Error while getting or setting configuration + /// Failed to get or set configuration Config(#[source] eyre::Report), /// Failed to push into queue PushIntoQueue(#[from] Box), #[cfg(feature = "telemetry")] - /// Error while getting Prometheus metrics + /// Failed to get Prometheus metrics Prometheus(#[source] eyre::Report), #[cfg(feature = "telemetry")] - /// Internal error while getting status + /// Failed to get status StatusFailure(#[source] eyre::Report), /// Failure caused by configuration subsystem ConfigurationFailure(#[from] KisoError), - /// Cannot find status segment by provided path + /// Failed to find status segment by provided path StatusSegmentNotFound(#[source] eyre::Report), }