From 485d63914f6f8682a0ee32ca0b944470c15f35db Mon Sep 17 00:00:00 2001 From: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> Date: Tue, 9 Jan 2024 12:57:33 +0900 Subject: [PATCH 01/94] [refactor]: wip Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- Cargo.lock | 78 ++- Cargo.toml | 5 +- client/src/lib.rs | 2 +- client_config/Cargo.toml | 19 + .../src/client/mod.rs | 0 client_config/src/lib.rs | 14 + config/Cargo.toml | 6 + config/base/Cargo.toml | 1 - config/base/derive/Cargo.toml | 24 - config/base/derive/src/lib.rs | 51 -- config/base/derive/src/proxy.rs | 324 ------------ config/base/derive/src/utils.rs | 367 ------------- config/base/derive/src/view.rs | 183 ------- config/base/src/lib.rs | 494 +----------------- config/src/block_sync.rs | 50 -- config/src/client_api.rs | 4 +- config/src/genesis.rs | 141 ----- config/src/iroha.rs | 282 ---------- config/src/kura.rs | 63 --- config/src/lib.rs | 314 ++++++++++- config/src/live_query_store.rs | 44 -- config/src/network.rs | 39 -- config/src/parameters/chain_wide.rs | 110 ++++ config/src/parameters/genesis.rs | 118 +++++ config/src/parameters/iroha.rs | 224 ++++++++ config/src/parameters/kura.rs | 144 +++++ config/src/{ => parameters}/logger.rs | 68 ++- config/src/parameters/mod.rs | 194 +++++++ config/src/parameters/queue.rs | 66 +++ config/src/parameters/snapshot.rs | 82 +++ config/src/parameters/sumeragi.rs | 90 ++++ config/src/parameters/telemetry.rs | 119 +++++ config/src/parameters/torii.rs | 67 +++ config/src/path.rs | 152 ------ config/src/queue.rs | 55 -- config/src/snapshot.rs | 54 -- config/src/sumeragi.rs | 186 ------- config/src/telemetry.rs | 117 ----- config/src/torii.rs | 99 ---- config/src/wasm.rs | 34 -- config/src/wsv.rs | 86 --- config/test/config.toml | 7 + config/tests/fixtures/.full_config_in.env | 6 + config/tests/fixtures/empty_ok_genesis.json | 4 + config/tests/fixtures/extra_fields.toml | 3 + .../genesis_with_unexisting_executor.json | 4 + .../tests/fixtures/inconsistent_genesis.toml | 18 + config/tests/fixtures/minimal_config.toml | 13 + config/tests/fixtures/missing_fields.toml | 0 config/tests/fixtures/with_genesis.toml | 18 + config/tests/ui.rs | 284 ++++++++++ core/test_network/src/lib.rs | 2 +- genesis/src/lib.rs | 11 +- macro/utils/Cargo.toml | 2 +- tools/kagami/src/config.rs | 2 +- torii/src/lib.rs | 39 +- 56 files changed, 2052 insertions(+), 2931 deletions(-) create mode 100644 client_config/Cargo.toml rename config/src/client.rs => client_config/src/client/mod.rs (100%) create mode 100644 client_config/src/lib.rs delete mode 100644 config/base/derive/Cargo.toml delete mode 100644 config/base/derive/src/lib.rs delete mode 100644 config/base/derive/src/proxy.rs delete mode 100644 config/base/derive/src/utils.rs delete mode 100644 config/base/derive/src/view.rs delete mode 100644 config/src/block_sync.rs delete mode 100644 config/src/genesis.rs delete mode 100644 config/src/iroha.rs delete mode 100644 config/src/kura.rs delete mode 100644 config/src/live_query_store.rs delete mode 100644 config/src/network.rs create mode 100644 config/src/parameters/chain_wide.rs create mode 100644 config/src/parameters/genesis.rs create mode 100644 config/src/parameters/iroha.rs create mode 100644 config/src/parameters/kura.rs rename config/src/{ => parameters}/logger.rs (60%) create mode 100644 config/src/parameters/mod.rs create mode 100644 config/src/parameters/queue.rs create mode 100644 config/src/parameters/snapshot.rs create mode 100644 config/src/parameters/sumeragi.rs create mode 100644 config/src/parameters/telemetry.rs create mode 100644 config/src/parameters/torii.rs delete mode 100644 config/src/path.rs delete mode 100644 config/src/queue.rs delete mode 100644 config/src/snapshot.rs delete mode 100644 config/src/sumeragi.rs delete mode 100644 config/src/telemetry.rs delete mode 100644 config/src/torii.rs delete mode 100644 config/src/wsv.rs create mode 100644 config/test/config.toml create mode 100644 config/tests/fixtures/.full_config_in.env create mode 100644 config/tests/fixtures/empty_ok_genesis.json create mode 100644 config/tests/fixtures/extra_fields.toml create mode 100644 config/tests/fixtures/genesis_with_unexisting_executor.json create mode 100644 config/tests/fixtures/inconsistent_genesis.toml create mode 100644 config/tests/fixtures/minimal_config.toml create mode 100644 config/tests/fixtures/missing_fields.toml create mode 100644 config/tests/fixtures/with_genesis.toml create mode 100644 config/tests/ui.rs diff --git a/Cargo.lock b/Cargo.lock index 4b76a0de150..8799a6ab89b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2755,6 +2755,10 @@ dependencies = [ "vergen", ] +[[package]] +name = "iroha_client_config" +version = "2.0.0-pre-rc.20" + [[package]] name = "iroha_config" version = "2.0.0-pre-rc.20" @@ -2762,23 +2766,29 @@ dependencies = [ "cfg-if", "derive_more", "displaydoc", + "drop_bomb", "expect-test", "eyre", + "hex", "iroha_config_base", "iroha_crypto", "iroha_data_model", "iroha_genesis", "iroha_primitives", "json5", + "nonzero_ext", "once_cell", + "parse-display", "proptest", "serde", "serde_json", "stacker", "strum 0.25.0", "thiserror", + "toml 0.8.8", "tracing", "tracing-subscriber", + "trybuild", "url", ] @@ -2789,7 +2799,6 @@ dependencies = [ "crossbeam", "displaydoc", "eyre", - "iroha_config_derive", "iroha_crypto", "json5", "parking_lot", @@ -2798,17 +2807,6 @@ dependencies = [ "thiserror", ] -[[package]] -name = "iroha_config_derive" -version = "2.0.0-pre-rc.20" -dependencies = [ - "iroha_macro_utils", - "proc-macro-error", - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "iroha_core" version = "2.0.0-pre-rc.20" @@ -3862,6 +3860,12 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nonzero_ext" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21" + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -4337,12 +4341,11 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro-crate" -version = "2.0.1" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97dc5fea232fc28d2f597b37c4876b348a40e33f3b02cc975c8d006d78d94b1a" +checksum = "7e8366a6159044a37876a2b9817124296703c586a5c92e2c53751fa06d8d43e8" dependencies = [ - "toml_datetime", - "toml_edit", + "toml_edit 0.20.2", ] [[package]] @@ -4929,6 +4932,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -5658,11 +5670,26 @@ dependencies = [ "serde", ] +[[package]] +name = "toml" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a195ec8c9da26928f773888e0742ca3ca1040c6cd859c919c9f59c1954ab35" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit 0.21.0", +] + [[package]] name = "toml_datetime" -version = "0.6.3" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +dependencies = [ + "serde", +] [[package]] name = "toml_edit" @@ -5675,6 +5702,19 @@ dependencies = [ "winnow", ] +[[package]] +name = "toml_edit" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34d383cd00a163b4a5b85053df514d45bc330f6de7737edfe0a93311d1eaa03" +dependencies = [ + "indexmap 2.1.0", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + [[package]] name = "tonic" version = "0.10.2" @@ -6331,7 +6371,7 @@ dependencies = [ "serde", "serde_derive", "sha2", - "toml", + "toml 0.5.11", "windows-sys 0.48.0", "zstd", ] diff --git a/Cargo.toml b/Cargo.toml index b961c3d7966..4dad979b674 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,9 +28,9 @@ iroha_primitives_derive = { version = "=2.0.0-pre-rc.20", path = "primitives/der iroha_data_model = { version = "=2.0.0-pre-rc.20", path = "data_model", default-features = false } iroha_data_model_derive = { version = "=2.0.0-pre-rc.20", path = "data_model/derive" } iroha_client = { version = "=2.0.0-pre-rc.20", path = "client" } +iroha_client_config = { version = "=2.0.0-pre-rc.20", path = "client_config" } iroha_config = { version = "=2.0.0-pre-rc.20", path = "config" } iroha_config_base = { version = "=2.0.0-pre-rc.20", path = "config/base" } -iroha_config_derive = { version = "=2.0.0-pre-rc.20", path = "config/base/derive" } iroha_schema_gen = { version = "=2.0.0-pre-rc.20", path = "schema/gen" } iroha_schema = { version = "=2.0.0-pre-rc.20", path = "schema", default-features = false } iroha_schema_derive = { version = "=2.0.0-pre-rc.20", path = "schema/derive" } @@ -65,6 +65,7 @@ syn2 = { package = "syn", version = "2.0.38", default-features = false } quote = "1.0.33" manyhow = { version = "0.8.1", features = ["darling"] } darling = "0.20.3" +drop_bomb = "0.1.5" futures = { version = "0.3.28", default-features = false } tokio = "1.33.0" @@ -204,9 +205,9 @@ members = [ "cli", "client", "client_cli", + "client_config", "config", "config/base", - "config/base/derive", "core", "core/test_network", "crypto", diff --git a/client/src/lib.rs b/client/src/lib.rs index 239a6eb5a7f..6fb2e9094a7 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -42,7 +42,7 @@ pub mod samples { pub mod config { //! Module for client-related configuration and structs - pub use iroha_config::{client::*, client_api as api, path, torii::uri as torii}; + pub use iroha_config::{client_api as api, path, r#mod::*, torii::uri as torii}; } pub use iroha_crypto as crypto; diff --git a/client_config/Cargo.toml b/client_config/Cargo.toml new file mode 100644 index 00000000000..a1891b4817e --- /dev/null +++ b/client_config/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "iroha_client_config" +edition.workspace = true +version.workspace = true +authors.workspace = true +description.workspace = true +repository.workspace = true +documentation.workspace = true +homepage.workspace = true +license.workspace = true +keywords.workspace = true +categories.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] + +[lints] +workspace = true diff --git a/config/src/client.rs b/client_config/src/client/mod.rs similarity index 100% rename from config/src/client.rs rename to client_config/src/client/mod.rs diff --git a/client_config/src/lib.rs b/client_config/src/lib.rs new file mode 100644 index 00000000000..7d12d9af819 --- /dev/null +++ b/client_config/src/lib.rs @@ -0,0 +1,14 @@ +pub fn add(left: usize, right: usize) -> usize { + left + right +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + let result = add(2, 2); + assert_eq!(result, 4); + } +} diff --git a/config/Cargo.toml b/config/Cargo.toml index d6df71128fa..7865b53d01e 100644 --- a/config/Cargo.toml +++ b/config/Cargo.toml @@ -21,6 +21,7 @@ eyre = { workspace = true } tracing = { workspace = true } tracing-subscriber = { workspace = true, features = ["fmt", "ansi"] } url = { workspace = true, features = ["serde"] } +drop_bomb = { workspace = true } serde = { workspace = true, features = ["derive"] } strum = { workspace = true, features = ["derive"] } @@ -31,11 +32,16 @@ displaydoc = { workspace = true } derive_more = { workspace = true } cfg-if = { workspace = true } once_cell = { workspace = true } +nonzero_ext = "0.3.0" +toml = "0.8.8" +parse-display = "0.8.2" [dev-dependencies] proptest = "1.3.1" stacker = "0.1.15" expect-test = { workspace = true } +trybuild = { workspace = true } +hex = { workspace = true } [features] tokio-console = [] diff --git a/config/base/Cargo.toml b/config/base/Cargo.toml index b11b28ea577..763bcf58162 100644 --- a/config/base/Cargo.toml +++ b/config/base/Cargo.toml @@ -11,7 +11,6 @@ license.workspace = true workspace = true [dependencies] -iroha_config_derive = { workspace = true } iroha_crypto = { workspace = true, features = ["std"] } serde = { workspace = true, default-features = false, features = ["derive"] } diff --git a/config/base/derive/Cargo.toml b/config/base/derive/Cargo.toml deleted file mode 100644 index 8aa95845755..00000000000 --- a/config/base/derive/Cargo.toml +++ /dev/null @@ -1,24 +0,0 @@ -[package] -name = "iroha_config_derive" - -edition.workspace = true -version.workspace = true -authors.workspace = true - -license.workspace = true - -[lints] -workspace = true - -[lib] -proc-macro = true - -[dependencies] -iroha_macro_utils = { workspace = true } - -syn = { workspace = true, features = ["derive", "parsing", "proc-macro", "clone-impls", "printing"] } -# This is the maximally compressed set of features. Yes we also need "printing". -quote = { workspace = true } -proc-macro2 = { workspace = true } -proc-macro-error = { workspace = true } - diff --git a/config/base/derive/src/lib.rs b/config/base/derive/src/lib.rs deleted file mode 100644 index 0cd24e4e345..00000000000 --- a/config/base/derive/src/lib.rs +++ /dev/null @@ -1,51 +0,0 @@ -//! Contains various configuration related macro definitions. - -use proc_macro::TokenStream; - -pub(crate) mod proxy; -pub(crate) mod utils; -pub(crate) mod view; - -/// Derive for config loading. More details in `iroha_config_base` reexport -#[proc_macro_derive(Override, attributes(config))] -pub fn override_derive(input: TokenStream) -> TokenStream { - let ast = syn::parse_macro_input!(input as utils::StructWithFields); - proxy::impl_override(&ast) -} - -/// Derive for config querying and setting. More details in `iroha_config_base` reexport -#[proc_macro_derive(Builder, attributes(builder))] -pub fn builder_derive(input: TokenStream) -> TokenStream { - let ast = syn::parse_macro_input!(input as utils::StructWithFields); - proxy::impl_build(&ast) -} - -/// Derive for config querying and setting. More details in `iroha_config_base` reexport -#[proc_macro_error::proc_macro_error] -#[proc_macro_derive(LoadFromEnv, attributes(config))] -pub fn load_from_env_derive(input: TokenStream) -> TokenStream { - let ast = syn::parse_macro_input!(input as utils::StructWithFields); - proxy::impl_load_from_env(&ast) -} - -/// Derive for config querying and setting. More details in `iroha_config_base` reexport -#[proc_macro_derive(LoadFromDisk)] -pub fn load_from_disk_derive(input: TokenStream) -> TokenStream { - let ast = syn::parse_macro_input!(input as utils::StructWithFields); - proxy::impl_load_from_disk(&ast) -} - -/// Derive for config querying and setting. More details in `iroha_config_base` reexport -#[proc_macro_derive(Proxy, attributes(config))] -pub fn proxy_derive(input: TokenStream) -> TokenStream { - let ast = syn::parse_macro_input!(input as utils::StructWithFields); - proxy::impl_proxy(ast) -} - -/// Generate view for given struct and convert from type to its view. -/// More details in `iroha_config_base` reexport. -#[proc_macro] -pub fn view(input: TokenStream) -> TokenStream { - let ast = syn::parse_macro_input!(input as utils::StructWithFields); - view::impl_view(ast) -} diff --git a/config/base/derive/src/proxy.rs b/config/base/derive/src/proxy.rs deleted file mode 100644 index dafef4c6145..00000000000 --- a/config/base/derive/src/proxy.rs +++ /dev/null @@ -1,324 +0,0 @@ -use proc_macro::TokenStream; -use proc_macro_error::abort; -use quote::{format_ident, quote}; -use syn::{parse_quote, Type, TypePath}; - -use super::utils::{get_inner_type, StructWithFields}; -use crate::utils; - -pub fn impl_proxy(ast: StructWithFields) -> TokenStream { - let parent_name = &ast.ident; - let parent_ty: Type = parse_quote! { #parent_name }; - let proxy_struct = gen_proxy_struct(ast); - let loadenv_derive = quote! { ::iroha_config_base::derive::LoadFromEnv }; - let disk_derive = quote! { ::iroha_config_base::derive::LoadFromDisk }; - let builder_derive = quote! { ::iroha_config_base::derive::Builder }; - let override_derive = quote! { ::iroha_config_base::derive::Override }; - quote! { - /// Proxy configuration structure to be used as an intermediate - /// for configuration loading. Both loading from disk and - /// from env should only be done via this struct, which then - /// builds into its parent [`struct@Configuration`]. - #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, - #builder_derive, - #loadenv_derive, - #disk_derive, - #override_derive - )] - #[builder(parent = #parent_ty)] - #proxy_struct - - } - .into() -} - -pub fn impl_override(ast: &StructWithFields) -> TokenStream { - let override_trait = quote! { ::iroha_config_base::proxy::Override }; - let name = &ast.ident; - let clauses = ast.fields.iter().map(|field| { - let field_name = &field.ident; - if field.has_inner { - let inner_ty = get_inner_type("Option", &field.ty); - quote! { - self.#field_name = match (self.#field_name, other.#field_name) { - (Some(this_field), Some(other_field)) => Some(<#inner_ty as #override_trait>::override_with(this_field, other_field)), - (this_field, None) => this_field, - (None, other_field) => other_field, - }; - } - } else { - quote! { - if let Some(other_field) = other.#field_name { - self.#field_name = Some(other_field) - } - } - } - }); - - quote! { - impl #override_trait for #name { - fn override_with(mut self, other: Self) -> Self { - #(#clauses)* - self - } - } - } - .into() -} - -pub fn impl_load_from_env(ast: &StructWithFields) -> TokenStream { - let env_fetcher_ident = quote! { env_fetcher }; - let fetch_env_trait = quote! { ::iroha_config_base::proxy::FetchEnv }; - let env_trait = quote! { ::iroha_config_base::proxy::LoadFromEnv }; - - let set_field = ast.fields - .iter() - .map(|field| { - let ty = &field.ty; - let as_str_attr = field.has_as_str; - let ident = &field.ident; - let field_env = &field.env_str; - - let inner_ty = if field.has_option { - get_inner_type("Option", ty) - } else { - abort!(ast, "This macro should only be used on `ConfigurationProxy` types, \ - i.e. the types which represent a partially finalised configuration \ - (with some required fields omitted and to be read from other sources). \ - These types' fields have the `Option` type wrapped around each of them.") - }; - let is_string = if let Type::Path(TypePath { path, .. }) = inner_ty { - path.is_ident("String") - } else { - false - }; - let inner = if is_string { - quote! { Ok(var) } - } else if as_str_attr { - quote! {{ - let value: ::serde_json::Value = var.into(); - ::json5::from_str(&value.to_string()) - }} - } else { - quote! { ::json5::from_str(&var) } - }; - let mut set_field = quote! { - let #ident = #env_fetcher_ident.fetch(#field_env) - // treating unicode errors the same as variable absence - .ok() - .map(|var| { - #inner.map_err(|err| { - ::iroha_config_base::derive::Error::field_deserialization_from_json5( - // FIXME: specify location precisely - // https://github.com/hyperledger/iroha/issues/3470 - #field_env, - &err - ) - }) - }) - .transpose()?; - }; - if field.has_inner { - let maybe_map_box = gen_maybe_map_box(inner_ty); - set_field.extend(quote! { - let inner_proxy = <#inner_ty as #env_trait>::from_env(#env_fetcher_ident) - #maybe_map_box - ?; - let #ident = if let Some(old_inner) = #ident { - Some(<#inner_ty as ::iroha_config_base::proxy::Override>::override_with(old_inner, inner_proxy)) - } else { - Some(inner_proxy) - }; - }); - } - set_field - }); - - let name = &ast.ident; - let fields = ast - .fields - .iter() - .map(|field| { - let ident = &field.ident; - quote! { #ident } - }) - .collect::>(); - quote! { - impl #env_trait for #name { - type ReturnValue = Result; - fn from_env(#env_fetcher_ident: &F) -> Self::ReturnValue { - #(#set_field)* - let proxy = #name { - #(#fields),* - }; - Ok(proxy) - } - } - } - .into() -} - -pub fn impl_load_from_disk(ast: &StructWithFields) -> TokenStream { - let proxy_name = &ast.ident; - let disk_trait = quote! { ::iroha_config_base::proxy::LoadFromDisk }; - let error_ty = quote! { ::iroha_config_base::derive::Error }; - let disk_err_variant = quote! { ::iroha_config_base::derive::Error::Disk }; - let serde_err_variant = quote! { ::iroha_config_base::derive::Error::Json5 }; - let none_proxy = gen_none_fields_proxy(ast); - quote! { - impl #disk_trait for #proxy_name { - type ReturnValue = Self; - fn from_path + ::std::fmt::Debug + Clone>(path: P) -> Self::ReturnValue { - let mut file = ::std::fs::File::open(path).map_err(#disk_err_variant); - // String has better parsing speed, see [issue](https://github.com/serde-rs/json/issues/160#issuecomment-253446892) - let mut s = String::new(); - let res = file - .and_then(|mut f| { - ::std::io::Read::read_to_string(&mut f, &mut s).map(move |_| s).map_err(#disk_err_variant) - }) - .and_then( - |s| -> ::core::result::Result { - json5::from_str(&s).map_err(#serde_err_variant) - }, - ) - .map_or(#none_proxy, ::std::convert::identity); - res - } - } - }.into() -} - -fn gen_proxy_struct(mut ast: StructWithFields) -> StructWithFields { - // As this changes the field types of the AST, `lvalue_read` - // and `lvalue_write` of its `StructField`s may get desynchronized - ast.fields.iter_mut().for_each(|field| { - // For fields of `Configuration` that have an inner config, the corresponding - // proxy field should have a `..Proxy` type there as well - if field.has_inner { - proxify_field_type(&mut field.ty); - } - let ty = &field.ty; - field.ty = parse_quote! { - Option<#ty> - }; - // - field - .attrs - .retain(|attr| attr.path.is_ident("doc") || attr.path.is_ident("config")); - // Fields that already wrap an option should have a - // custom deserializer so that json `null` becomes - // `Some(None)` and not just `None` - if field.has_option { - let de_helper = stringify! { ::iroha_config_base::proxy::some_option }; - let serde_attr: syn::Attribute = - parse_quote! { #[serde(default, deserialize_with = #de_helper)] }; - field.attrs.push(serde_attr); - } - field.has_option = true; - }); - ast.ident = format_ident!("{}Proxy", ast.ident); - // The only needed struct-level attributes are these - ast.attrs.retain(|attr| { - attr.path.is_ident("config") || attr.path.is_ident("serde") || attr.path.is_ident("cfg") - }); - ast -} - -#[allow(clippy::expect_used)] -pub fn proxify_field_type(field_ty: &mut syn::Type) { - if let Type::Path(path) = field_ty { - let last_segment = path.path.segments.last_mut().expect("Can't be empty"); - if last_segment.ident == "Box" { - let box_generic = utils::extract_box_generic(last_segment); - // Recursion - proxify_field_type(box_generic) - } else { - // TODO: Wouldn't it be better to get it as an associated type? - let new_ident = format_ident!("{}Proxy", last_segment.ident); - last_segment.ident = new_ident; - } - } -} - -pub fn impl_build(ast: &StructWithFields) -> TokenStream { - let checked_fields = gen_none_fields_check(ast); - let proxy_name = &ast.ident; - let parent_ty = utils::get_parent_ty(ast); - let builder_trait = quote! { ::iroha_config_base::proxy::Builder }; - let error_ty = quote! { ::iroha_config_base::derive::Error }; - - quote! { - impl #builder_trait for #proxy_name { - type ReturnValue = Result<#parent_ty, #error_ty>; - fn build(self) -> Self::ReturnValue { - Ok(#parent_ty { - #checked_fields - }) - } - } - } - .into() -} - -/// Helper function to be used in [`impl Builder`]. Verifies that all fields have -/// been initialized. -fn gen_none_fields_check(ast: &StructWithFields) -> proc_macro2::TokenStream { - let checked_fields = ast.fields.iter().map(|field| { - let ident = &field.ident; - let missing_field = quote! { ::iroha_config_base::derive::Error::MissingField }; - if field.has_inner { - let inner_ty = get_inner_type("Option", &field.ty); - let builder_trait = quote! { ::iroha_config_base::proxy::Builder }; - - let maybe_map_box = gen_maybe_map_box(inner_ty); - - quote! { - #ident: <#inner_ty as #builder_trait>::build( - self.#ident.ok_or( - #missing_field{field: stringify!(#ident), message: ""} - )? - ) - #maybe_map_box - ? - } - } else { - quote! { - #ident: self.#ident.ok_or( - #missing_field{field: stringify!(#ident), message: ""} - )? - } - } - }); - quote! { - #(#checked_fields),* - } -} - -fn gen_maybe_map_box(inner_ty: &syn::Type) -> proc_macro2::TokenStream { - if let Type::Path(path) = &inner_ty { - let last_segment = path.path.segments.last().expect("Can't be empty"); - if last_segment.ident == "Box" { - return quote! { - .map(Box::new) - }; - } - } - quote! {} -} - -/// Helper function to be used as an empty fallback for [`impl LoadFromEnv`] or [`impl LoadFromDisk`]. -/// Only meant for proxy types usage. -fn gen_none_fields_proxy(ast: &StructWithFields) -> proc_macro2::TokenStream { - let proxy_name = &ast.ident; - let none_fields = ast.fields.iter().map(|field| { - let ident = &field.ident; - quote! { - #ident: None - } - }); - quote! { - #proxy_name { - #(#none_fields),* - } - } -} diff --git a/config/base/derive/src/utils.rs b/config/base/derive/src/utils.rs deleted file mode 100644 index 36f79a76384..00000000000 --- a/config/base/derive/src/utils.rs +++ /dev/null @@ -1,367 +0,0 @@ -pub use iroha_macro_utils::{attr_struct, AttrParser}; -use proc_macro2::TokenStream; -use quote::{quote, ToTokens}; -use syn::{ - parse::{Parse, ParseStream}, - Attribute, GenericArgument, Ident, LitStr, Meta, NestedMeta, PathArguments, Token, Type, -}; - -/// Keywords used inside `#[view(...)]` and `#[config(...)]` -mod kw { - // config keywords - syn::custom_keyword!(serde_as_str); - syn::custom_keyword!(inner); - syn::custom_keyword!(env_prefix); - // view keywords - syn::custom_keyword!(ignore); - syn::custom_keyword!(into); - // builder keywords - syn::custom_keyword!(parent); -} - -/// Structure to parse `#[view(...)]` attributes. -/// [`Inner`] is responsible for parsing attribute arguments. -pub struct View(std::marker::PhantomData); - -/// Structure to parse `#[config(...)]` attributes. -/// [`Inner`] is responsible for parsing attribute arguments. -struct Config(std::marker::PhantomData); - -/// Structure to parse `#[builder(...)]` attributes. -/// [`Inner`] is responsible for parsing attribute arguments. -struct Builder(std::marker::PhantomData); - -impl AttrParser for View { - const IDENT: &'static str = "view"; -} - -impl AttrParser for Config { - const IDENT: &'static str = "config"; -} - -impl AttrParser for Builder { - const IDENT: &'static str = "builder"; -} - -attr_struct! { - pub struct ViewIgnore { - _kw: kw::ignore, - } -} - -attr_struct! { - pub struct ViewFieldType { - _kw: kw::into, - _eq: Token![=], - ty: Type, - } -} - -attr_struct! { - pub struct ConfigInner { - _kw: kw::inner, - } -} - -attr_struct! { - pub struct ConfigAsStr { - _kw: kw::serde_as_str, - } -} - -attr_struct! { - pub struct ConfigEnvPrefix { - _kw: kw::env_prefix, - _eq: Token![=], - pub prefix: LitStr, - } -} - -attr_struct! { - pub struct BuilderParent { - _kw: kw::parent, - _eq: Token![=], - pub parent: Type, - } -} - -impl From for Type { - fn from(value: ViewFieldType) -> Self { - value.ty - } -} - -#[derive(Clone)] -pub struct StructField { - pub ident: Ident, - pub ty: Type, - pub vis: syn::Visibility, - pub attrs: Vec, - pub env_str: String, - pub has_inner: bool, - pub has_option: bool, - pub has_as_str: bool, - pub lvalue_read: TokenStream, - pub lvalue_write: TokenStream, -} - -impl StructField { - fn from_ast(field: syn::Field, env_prefix: &str) -> Self { - let field_ident = field - .ident - .expect("Already checked for named fields at parsing"); - let (lvalue_read, lvalue_write) = gen_lvalue(&field.ty, &field_ident); - StructField { - has_inner: field - .attrs - .iter() - .any(|attr| Config::::parse(attr).is_ok()), - has_as_str: field - .attrs - .iter() - .any(|attr| Config::::parse(attr).is_ok()), - has_option: is_option_type(&field.ty), - env_str: env_prefix.to_owned() + &field_ident.to_string().to_uppercase(), - attrs: field.attrs, - ident: field_ident, - ty: field.ty, - vis: field.vis, - lvalue_read, - lvalue_write, - } - } -} - -impl ToTokens for StructField { - fn to_tokens(&self, tokens: &mut TokenStream) { - let StructField { - attrs, - ty, - ident, - vis, - .. - } = self; - let stream = quote! { - #(#attrs)* - #vis #ident: #ty - }; - tokens.extend(stream); - } -} - -/// Parsed struct with named fields used in proc macros of this crate -#[derive(Clone)] -pub struct StructWithFields { - pub attrs: Vec, - pub env_prefix: String, - pub vis: syn::Visibility, - _struct_token: Token![struct], - pub ident: Ident, - pub generics: syn::Generics, - pub fields: Vec, - _semi_token: Option, -} - -impl Parse for StructWithFields { - fn parse(input: ParseStream) -> syn::Result { - let attrs = input.call(Attribute::parse_outer)?; - let env_prefix = attrs - .iter() - .map(Config::::parse) - .find_map(Result::ok) - .map(|pref| pref.prefix.value()) - .unwrap_or_default(); - Ok(Self { - attrs, - vis: input.parse()?, - _struct_token: input.parse()?, - ident: input.parse()?, - generics: input.parse()?, - fields: input - .parse::()? - .named - .into_iter() - .map(|field| StructField::from_ast(field, &env_prefix)) - .collect(), - env_prefix, - _semi_token: input.parse()?, - }) - } -} - -impl ToTokens for StructWithFields { - fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { - let StructWithFields { - attrs, - vis, - ident, - generics, - fields, - .. - } = self; - let stream = quote! { - #(#attrs)* - #vis struct #ident #generics { - #(#fields),* - } - }; - tokens.extend(stream); - } -} - -/// Remove attributes with ident [`attr_ident`] from attributes -pub fn remove_attr(attrs: &mut Vec, attr_ident: &str) { - attrs.retain(|attr| !attr.path.is_ident(attr_ident)); -} - -pub fn extract_field_idents(fields: &[StructField]) -> Vec<&Ident> { - fields.iter().map(|field| &field.ident).collect::>() -} - -pub fn extract_field_types(fields: &[StructField]) -> Vec { - fields - .iter() - .map(|field| field.ty.clone()) - .collect::>() -} - -pub fn get_type_argument<'tl>(s: &str, ty: &'tl Type) -> Option<&'tl GenericArgument> { - let Type::Path(path) = ty else { - return None; - }; - let segments = &path.path.segments; - if segments.len() != 1 || segments[0].ident != s { - return None; - } - - if let PathArguments::AngleBracketed(bracketed_arguments) = &segments[0].arguments { - if bracketed_arguments.args.len() == 1 { - return Some(&bracketed_arguments.args[0]); - } - } - None -} - -pub fn get_inner_type<'tl>(outer_ty_ident: &str, ty: &'tl Type) -> &'tl Type { - #[allow(clippy::shadow_unrelated)] - get_type_argument(outer_ty_ident, ty) - .and_then(|ty| { - if let GenericArgument::Type(r#type) = ty { - Some(r#type) - } else { - None - } - }) - .unwrap_or(ty) -} - -pub fn is_arc_rwlock(ty: &Type) -> bool { - let dearced_ty = get_inner_type("Arc", ty); - get_type_argument("RwLock", dearced_ty).is_some() -} - -/// Check if the provided type is of the form [`Option<..>`] -pub fn is_option_type(ty: &Type) -> bool { - get_type_argument("Option", ty).is_some() -} - -/// Remove attributes with ident [`attr_ident`] from struct attributes and field attributes -pub fn remove_attr_from_struct(ast: &mut StructWithFields, attr_ident: &str) { - let StructWithFields { attrs, fields, .. } = ast; - for field in fields { - remove_attr(&mut field.attrs, attr_ident); - } - remove_attr(attrs, attr_ident); -} - -/// Keep only derive attributes passed as a second argument in struct attributes and field attributes -pub fn keep_derive_attr(ast: &mut StructWithFields, kept_attrs: &[&str]) { - ast.attrs - .iter_mut() - .filter(|attr| attr.path.is_ident("derive")) - .for_each(|attr| { - let meta = attr - .parse_meta() - .expect("derive macro must be in one of the meta forms"); - if let Meta::List(list) = meta { - let items: Vec = list - .nested - .into_iter() - .filter(|nested| { - if let NestedMeta::Meta(Meta::Path(path)) = nested { - return kept_attrs.iter().any(|kept_attr| path.is_ident(kept_attr)); - } - // Non-nested all kept by default - true - }) - .collect(); - *attr = syn::parse_quote!( - #[derive(#(#items),*)] - ); - } - }); -} - -/// Keep only attributes passed as a second argument in struct attributes and field attributes -pub fn keep_attrs_in_struct(ast: &mut StructWithFields, kept_attrs: &[&str]) { - let StructWithFields { attrs, fields, .. } = ast; - for field in fields { - field.attrs.retain(|attr| { - kept_attrs - .iter() - .any(|kept_attr| attr.path.is_ident(kept_attr)) - }); - } - attrs.retain(|attr| { - kept_attrs - .iter() - .any(|kept_attr| attr.path.is_ident(kept_attr)) - }); -} - -/// Generate lvalue forms for a struct field, taking [`Arc>`] types -/// into account as well. Returns a 2-tuple of read and write forms. -pub fn gen_lvalue(field_ty: &Type, field_ident: &Ident) -> (TokenStream, TokenStream) { - let is_lvalue = is_arc_rwlock(field_ty); - - let lvalue_read = if is_lvalue { - quote! { self.#field_ident.read().await } - } else { - quote! { self.#field_ident } - }; - - let lvalue_write = if is_lvalue { - quote! { self.#field_ident.write().await } - } else { - quote! { self.#field_ident } - }; - - (lvalue_read, lvalue_write) -} - -/// Check if [`StructWithFields`] has `#[builder(parent = ..)]` -pub fn get_parent_ty(ast: &StructWithFields) -> Type { - ast.attrs - .iter() - .find_map(|attr| Builder::::parse(attr).ok()) - .map(|builder| builder.parent) - .expect("Should not be called on structs with no `#[builder(..)]` attribute") -} - -pub fn extract_box_generic(box_seg: &mut syn::PathSegment) -> &mut syn::Type { - let syn::PathArguments::AngleBracketed(generics) = &mut box_seg.arguments else { - panic!("`Box` should have explicit generic"); - }; - - assert!( - generics.args.len() == 1, - "`Box` should have exactly one generic argument" - ); - let syn::GenericArgument::Type(generic_type) = - generics.args.first_mut().expect("Can't be empty") - else { - panic!("`Box` should have type as a generic argument") - }; - - generic_type -} diff --git a/config/base/derive/src/view.rs b/config/base/derive/src/view.rs deleted file mode 100644 index a020c7edc13..00000000000 --- a/config/base/derive/src/view.rs +++ /dev/null @@ -1,183 +0,0 @@ -use gen::*; -use proc_macro::TokenStream; -use quote::{format_ident, quote}; - -use super::utils::{ - extract_field_idents, extract_field_types, remove_attr, remove_attr_from_struct, AttrParser, - StructField, StructWithFields, View, ViewFieldType, ViewIgnore, -}; - -pub fn impl_view(ast: StructWithFields) -> TokenStream { - let original = original_struct(ast.clone()); - let view = view_struct(ast); - let impl_from = impl_from(&original, &view); - let impl_has_view = impl_has_view(&original); - let assertions = assertions(&view); - let out = quote! { - #original - #impl_has_view - #view - #impl_from - #assertions - }; - out.into() -} - -mod gen { - use super::*; - use crate::utils::{keep_attrs_in_struct, keep_derive_attr}; - - pub fn original_struct(mut ast: StructWithFields) -> StructWithFields { - remove_attr_from_struct(&mut ast, "view"); - ast - } - - pub fn view_struct(mut ast: StructWithFields) -> StructWithFields { - // Remove fields with #[view(ignore)] - ast.fields.retain(is_view_field_ignored); - // Change field type to `Type` if it has attribute #[view(into = Type)] - ast.fields.iter_mut().for_each(view_field_change_type); - // Replace doc-string for view - remove_attr(&mut ast.attrs, "doc"); - let view_doc = format!("View for {}", ast.ident); - ast.attrs.push(syn::parse_quote!( - #[doc = #view_doc] - )); - keep_derive_attr( - &mut ast, - &[ - "Clone", - "Debug", - "Deserialize", - "Serialize", - "PartialEq", - "Eq", - ], - ); - keep_attrs_in_struct(&mut ast, &["serde", "doc", "derive", "cfg"]); - ast.ident = format_ident!("{}View", ast.ident); - ast - } - - pub fn impl_from( - original: &StructWithFields, - view: &StructWithFields, - ) -> proc_macro2::TokenStream { - let StructWithFields { - ident: original_ident, - .. - } = original; - let StructWithFields { - generics, - ident: view_ident, - fields, - .. - } = view; - let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); - let field_idents = extract_field_idents(fields); - let field_cfg_attrs = fields - .iter() - .map(|field| { - field - .attrs - .iter() - .filter(|attr| attr.path.is_ident("cfg")) - .collect::>() - }) - .collect::>(); - - let field_froms: Vec<_> = fields - .iter() - .map(|field| { - let field_ident = &field.ident; - if let syn::Type::Path(syn::TypePath { path, .. }) = &field.ty { - let last_segment = path.segments.last().expect("Not empty"); - if last_segment.ident == "Box" { - return quote! { - #field_ident: Box::new(core::convert::From::<_>::from(*#field_ident)), - }; - } - } - quote! { - #field_ident: core::convert::From::<_>::from(#field_ident), - } - }) - .collect(); - - quote! { - impl #impl_generics core::convert::From<#original_ident> for #view_ident #ty_generics #where_clause { - fn from(config: #original_ident) -> Self { - let #original_ident { - #( - #(#field_cfg_attrs)* - #field_idents, - )* - .. - } = config; - Self { - #( - #(#field_cfg_attrs)* - #field_froms - )* - } - } - } - } - } - - pub fn impl_has_view(original: &StructWithFields) -> proc_macro2::TokenStream { - let StructWithFields { - generics, - ident: view_ident, - .. - } = original; - let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); - - quote! { - impl #impl_generics iroha_config_base::view::HasView for #view_ident #ty_generics #where_clause {} - } - } - - pub fn assertions(view: &StructWithFields) -> proc_macro2::TokenStream { - let StructWithFields { fields, .. } = view; - let field_types = extract_field_types(fields); - let messages: Vec = extract_field_idents(fields) - .iter() - .map(|ident| { - format!("Field `{ident}` has it's own view, consider adding attribute #[view(into = ViewType)]") - }) - .collect(); - quote! { - /// Assert that every field of 'View' doesn't implement `HasView` trait - const _: () = { - use iroha_config_base::view::NoView; - #( - const _: () = assert!(!iroha_config_base::view::IsInstanceHasView::<#field_types>::IS_HAS_VIEW, #messages); - )* - }; - } - } -} - -/// Check if [`Field`] has `#[view(ignore)]` -fn is_view_field_ignored(field: &StructField) -> bool { - field - .attrs - .iter() - .map(View::::parse) - .find_map(Result::ok) - .is_none() -} - -/// Change [`Field`] type to `Type` if `#[view(type = Type)]` is present -fn view_field_change_type(field: &mut StructField) { - if let Some(ty) = field - .attrs - .iter() - .map(View::::parse) - .find_map(Result::ok) - .map(ViewFieldType::into) - { - field.ty = ty; - } -} diff --git a/config/base/src/lib.rs b/config/base/src/lib.rs index 7ea61d35ddb..7721c4ceaa2 100644 --- a/config/base/src/lib.rs +++ b/config/base/src/lib.rs @@ -1,493 +1 @@ -//! Package for managing iroha configuration -use std::{fmt::Debug, path::Path}; - -use serde::{de::DeserializeOwned, Deserialize, Deserializer, Serialize}; - -pub mod derive { - //! Derives for configuration entities - /// Generate view for the type and implement conversion `Type -> View`. - /// View contains a subset of the fields that the type has. - /// - /// Works only with structs. - /// - /// ## Container attributes - /// - /// ## Field attributes - /// ### `#[view(ignore)]` - /// Marks fields to ignore when converting to view type. - /// - /// ### `#[view(into = Ty)]` - /// Sets view's field type to Ty. - /// - /// ## Examples - /// - /// ```rust - /// use iroha_config_base::derive::view; - /// - /// view! { - /// #[derive(Default)] - /// struct Structure { - /// #[view(into = u64)] - /// a: u32, - /// // `View` shouldn't have field `b` so we must exclude it. - /// #[view(ignore)] - /// b: u32, - /// } - /// } - /// - /// // Will generate something like - /// // --//-- original struct - /// // struct StructureView { - /// // a: u64, - /// // } - /// // - /// // impl From for StructureView { - /// // fn from(value: Structure) -> Self { - /// // let Structure { - /// // a, - /// // .. - /// // } = value; - /// // Self { - /// // a: From::<_>::from(a), - /// // } - /// // } - /// // } - /// - /// - /// let structure = Structure { a: 13, b: 37 }; - /// let view: StructureView = structure.into(); - /// assert_eq!(view.a, 13); - /// ``` - pub use iroha_config_derive::view; - /// Derive macro for implementing the trait - /// [`iroha_config::base::proxy::Builder`](`crate::proxy::Builder`) - /// for config structures. Meant to be used on proxy types only, for - /// details see [`iroha_config::base::derive::Proxy`](`crate::derive::Proxy`). - /// - /// # Container attributes - /// - /// ## `#[builder(parent = ..)]` - /// Takes a target type to build into, e.g. for a `ConfigurationProxy` - /// it would be `Configuration`. - /// - /// # Examples - /// - /// ```rust - /// use iroha_config_base::derive::{Builder, Override, LoadFromEnv}; - /// use iroha_config_base::proxy::Builder as _; - /// - /// // Also need `LoadFromEnv` as it owns the `#[config]` attribute - /// #[derive(Debug, PartialEq, serde::Deserialize, serde::Serialize, LoadFromEnv, Builder)] - /// #[builder(parent = Outer)] - /// struct OuterProxy { #[config(inner)] inner: Option } - /// - /// #[derive(Debug, PartialEq, serde::Deserialize, serde::Serialize, LoadFromEnv, Builder, Override)] - /// #[builder(parent = Inner)] - /// struct InnerProxy { b: Option } - /// - /// #[derive(Debug, PartialEq)] - /// struct Outer { inner: Inner } - /// - /// #[derive(Debug, PartialEq)] - /// struct Inner { b: String } - /// - /// let outer_proxy = OuterProxy { inner: Some(InnerProxy { b: Some("a".to_owned()) })}; - /// - /// let outer = Outer { inner: Inner { b: "a".to_owned() } }; - /// - /// assert_eq!(outer, outer_proxy.build().unwrap()); - /// ``` - pub use iroha_config_derive::Builder; - /// Derive macro for implementing the trait - /// [`iroha_config::base::proxy::LoadFromDisk`](`crate::proxy::LoadFromDisk`) - /// trait for config structures. - /// - /// Meant to be used on proxy types only, for - /// details see [`iroha_config::base::derive::Proxy`](`crate::derive::Proxy`). - /// - /// The trait's only method, `from_path`, - /// deserializes a JSON config at the provided path into the parent proxy structure, - /// leaving it empty in case of any error. - /// - /// The `ReturnValue` associated type can be - /// swapped for anything suitable. Currently, the proxy structure is returned - /// by default. - pub use iroha_config_derive::LoadFromDisk; - /// Derive macro for implementing the - /// [`iroha_config::base::proxy::LoadFromDisk`](`crate::proxy::LoadFromDisk`) - /// trait for config structures. - /// - /// Meant to be used on proxy types only, for - /// details see [`iroha_config::base::derive::Proxy`](`crate::derive::Proxy`). - /// - /// The `ReturnValue` associated type can be - /// swapped for anything suitable. Currently, the proxy structure is returned - /// by default. - /// - /// # Container attributes - /// ## `[config(env_prefix)]` - /// Sets prefix for all the env variables derived from fields in the - /// corresponding structure. - /// - /// ### Example - /// - /// ``` rust - /// use iroha_config_base::derive::LoadFromEnv; - /// use iroha_config_base::proxy::LoadFromEnv as _; - /// - /// #[derive(serde::Deserialize, serde::Serialize, LoadFromEnv)] - /// #[config(env_prefix = "PREFIXED_")] - /// struct PrefixedProxy { a: Option } - /// - /// std::env::set_var("PREFIXED_A", "B"); - /// let prefixed = PrefixedProxy::from_std_env().unwrap(); - /// assert_eq!(prefixed.a.unwrap(), "B"); - /// ``` - /// - /// # Field attributes - /// ## `#[config(inner)]` - /// Tells macro that the structure stores another config inside, - /// allowing to load it recursively. Moreover, the types that - /// have this attributes on them should also implement or - /// derive the [`iroha_config::base::proxy::Override`](`crate::proxy::Override`) - /// trait. - /// - /// ### Example - /// - /// ```rust - /// use iroha_config_base::derive::{Override, LoadFromEnv}; - /// use iroha_config_base::proxy::LoadFromEnv as _; - /// - /// #[derive(Debug, PartialEq, serde::Deserialize, serde::Serialize, LoadFromEnv)] - /// struct OuterProxy { #[config(inner)] inner: Option } - /// - /// #[derive(Debug, PartialEq, serde::Deserialize, serde::Serialize, Override, LoadFromEnv)] - /// struct InnerProxy { b: Option } - /// - /// let mut outer = OuterProxy { inner: Some(InnerProxy { b: Some("a".to_owned()) })}; - /// - /// std::env::set_var("B", "a"); - /// let env_outer = OuterProxy::from_std_env().unwrap(); - /// - /// assert_eq!(env_outer, outer); - /// ``` - /// - /// ## `#[config(serde_as_str)]` - /// Tells macro to deserialize from env variable as a bare string. - /// - /// ### Example - /// - /// ``` - /// use iroha_config_base::derive::LoadFromEnv; - /// use iroha_config_base::proxy::LoadFromEnv; - /// use std::net::Ipv4Addr; - /// - /// #[derive(serde::Deserialize, serde::Serialize, LoadFromEnv)] - /// struct IpAddrProxy { #[config(serde_as_str)] ip: Option } - /// - /// std::env::set_var("IP", "127.0.0.1"); - /// let ip = IpAddrProxy::from_std_env().unwrap(); - /// assert_eq!(ip.ip.unwrap(), Ipv4Addr::new(127, 0, 0, 1)); - /// ``` - pub use iroha_config_derive::LoadFromEnv; - /// Derive macro for implementing the trait - /// [`iroha_config::base::proxy::Override`](`crate::proxy::Override`) - /// for config structures. Given two proxies, consumes them by recursively overloading - /// fields of [`self`] with fields of [`other`]. Order matters here, - /// i.e. `self.combine(other)` could yield different results than `other.combine(self)`. - /// - /// Meant to be used on proxy types only, for - /// details see [`iroha_config::base::derive::Proxy`](`crate::derive::Proxy`). - /// - /// # Examples - /// - /// ```rust - /// use iroha_config_base::derive::{Override, LoadFromEnv}; - /// use iroha_config_base::proxy::Override as _; - /// - /// #[derive(Debug, PartialEq, serde::Deserialize, serde::Serialize, Override, LoadFromEnv)] - /// struct OuterProxy { - /// #[config(inner)] - /// inner: Option, - /// a: Option - /// } - /// - /// #[derive(Debug, PartialEq, serde::Deserialize, serde::Serialize, Override, LoadFromEnv)] - /// struct InnerProxy { b: Option } - /// - /// let left_outer = OuterProxy { - /// inner: Some(InnerProxy { b: Some("a".to_owned()) }), - /// a: None - /// }; - /// - /// let right_outer = OuterProxy { - /// inner: None, - /// a: Some("b".to_owned()) - /// }; - /// - /// let res_outer = OuterProxy { - /// inner: Some(InnerProxy { b: Some("a".to_owned()) }), - /// a: Some("b".to_owned()) - /// }; - /// - /// assert_eq!(left_outer.override_with(right_outer), res_outer); - /// ``` - pub use iroha_config_derive::Override; - /// Derive macro for implementing the corresponding proxy type - /// for config structures. Most of the other traits in the - /// [`iroha_config_base::proxy`](`crate::proxy`) module are - /// best derived indirectly via this macro. Proxy types serve - /// as a stand-in for flexible configuration loading either - /// from environment variables or configuration files. Proxy types also - /// provide methods to build the initial parent type from them - /// (via [`iroha_config_base::proxy::Builder`](`crate::proxy::Builder`) - /// trait) and ways to combine two proxies together (via - /// [`iroha_config_base::proxy::Override`](`crate::proxy::Override`)). - pub use iroha_config_derive::Proxy; - use serde::Deserialize; - use thiserror::Error; - - /// Represents a path to a nested field in a config structure - #[derive(Debug, Deserialize)] - #[serde(transparent)] - pub struct Field(pub Vec); - - impl std::fmt::Display for Field { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - // separate fields with dots - std::fmt::Display::fmt(&self.0.join("."), f) - } - } - - // TODO: deal with `#[serde(skip)]` - /// Derive `Configurable` and `Proxy` error - #[derive(Debug, Error, Deserialize, displaydoc::Display)] - #[ignore_extra_doc_attributes] - #[allow(clippy::enum_variant_names)] - pub enum Error { - /// Failed to deserialize the field `{field}` - /// - /// Used in [`super::proxy::LoadFromEnv`] trait for deserialization - /// errors - #[serde(skip)] - FieldDeserialization { - /// Field name (known at compile time) - field: &'static str, - /// Unified error - #[source] - error: eyre::Report, - }, - - /// Please add `{field}` to the configuration - #[serde(skip)] - MissingField { - /// Field name - field: &'static str, - /// Additional message to be added as `color_eyre::suggestion` - message: &'static str, - }, - - /// Key pair creation failed, most likely because the keys don't form a pair - Crypto(#[from] iroha_crypto::error::Error), - - // IMO this variant should not exist. If the value is inferred, we should only warn people if the inferred value is different from the provided one. - /// You should remove the field `{field}` as its value is determined by other configuration parameters - #[serde(skip)] - ProvidedInferredField { - /// Field name - field: &'static str, - /// Additional message to be added as `color_eyre::suggestion` - message: &'static str, - }, - - /// The value {value} of `{field}` is wrong. Please change the value - #[serde(skip)] - InsaneValue { - /// The value of the field that's incorrect - value: String, - /// Field name that contains invalid value - field: &'static str, - /// Additional message to be added as `color_eyre::suggestion` - message: String, - // docstring: &'static str, // TODO: Inline the docstring for easy access - }, - - /// Reading file from disk failed - /// - /// Used in the [`LoadFromDisk`](`crate::proxy::LoadFromDisk`) trait for file read errors - #[serde(skip)] - Disk(#[from] std::io::Error), - - /// Deserializing JSON failed - /// - /// Used in [`LoadFromDisk`](`crate::proxy::LoadFromDisk`) trait for deserialization errors - #[serde(skip)] - Json5(#[from] json5::Error), - } - - impl Error { - /// This method is needed because a call of [`eyre::eyre!`] cannot be compiled when - /// generated in a proc macro. So, this shorthand is needed for proc macros. - pub fn field_deserialization_from_json( - field: &'static str, - error: &serde_json::Error, - ) -> Self { - Self::FieldDeserialization { - field, - error: eyre::eyre!("JSON: {}", error), - } - } - - /// See [`Self::field_deserialization_from_json`] - pub fn field_deserialization_from_json5(field: &'static str, error: &json5::Error) -> Self { - Self::FieldDeserialization { - field, - error: eyre::eyre!("JSON5: {}", error), - } - } - } -} - -pub mod view { - //! Module for view related traits and structs - - /// Marker trait to set default value [`IsInstanceHasView::IS_INSTANCE_HAS_VIEW`] to `false` - pub trait NoView { - /// [`Self`] doesn't implement [`HasView`] - const IS_HAS_VIEW: bool = false; - } - - impl NoView for T {} - - /// Marker traits for types for which views are implemented - pub trait HasView {} - - /// Wrapper structure used to check if type implements `[HasView]` - /// If `T` doesn't implement [`HasView`] then - /// [`NoView::IS_INSTANCE_HAS_VIEW`] (`false`) will be used. - /// Otherwise [`IsInstanceHasView::IS_INSTANCE_HAS_VIEW`] (`true`) - /// from `impl` block will shadow `NoView::IS_INSTANCE_HAS_VIEW` - pub struct IsInstanceHasView(core::marker::PhantomData); - - impl IsInstanceHasView { - /// `T` implements trait [`HasView`] - pub const IS_INSTANCE_HAS_VIEW: bool = true; - } -} - -pub mod proxy { - //! Module with traits for configuration proxies - - use super::*; - - /// Trait for combining two configuration instances - pub trait Override: Serialize + DeserializeOwned + Sized { - /// If any of the fields in `other` are filled, they - /// override the values of the fields in [`self`]. - #[must_use] - fn override_with(self, other: Self) -> Self; - } - - impl Override for Box { - fn override_with(self, other: Self) -> Self { - Box::new(T::override_with(*self, *other)) - } - } - - /// Trait for configuration loading and deserialization from - /// the environment - pub trait LoadFromEnv: Sized { - /// The return type. Could be target `Configuration`, - /// some `Result`, `Option`, or any other type that - /// wraps a `..Proxy` or `Configuration` type. - type ReturnValue; - - /// Load configuration from the environment - /// - /// # Errors - /// - Fails if the deserialization of any field fails. - fn from_env(fetcher: &F) -> Self::ReturnValue; - - /// Implementation of [`Self::from_env`] using [`std::env::var`]. - fn from_std_env() -> Self::ReturnValue { - struct FetchStdEnv; - - impl FetchEnv for FetchStdEnv { - fn fetch>( - &self, - key: K, - ) -> Result { - std::env::var(key) - } - } - - Self::from_env(&FetchStdEnv) - } - } - - impl LoadFromEnv for Box { - type ReturnValue = T::ReturnValue; - - fn from_env(fetcher: &F) -> Self::ReturnValue { - T::from_env(fetcher) - } - } - - /// Abstraction over the actual implementation of how env variables are gotten - /// from the environment. Necessary for mocking in tests. - pub trait FetchEnv { - /// The signature of [`std::env::var`]. - /// - /// # Errors - /// - /// See errors of [`std::env::var`]. - fn fetch>(&self, key: K) -> Result; - } - - /// Trait for configuration loading and deserialization from disk - pub trait LoadFromDisk: Sized { - /// The return type. Could be target `Configuration`, - /// some `Result`, `Option`, or any other type that - /// wraps a `..Proxy` or `Configuration` type. - type ReturnValue; - - /// Construct [`Self`] from a path-like object. - /// - /// # Errors - /// - File not found. - /// - File found, but peer configuration parsing failed. - fn from_path + Debug + Clone>(path: P) -> Self::ReturnValue; - } - - /// Trait for building the final config from a proxy one - pub trait Builder { - /// The return type. Could be target `Configuration`, - /// some `Result`, `Option` as users see fit. - type ReturnValue; - - /// Construct [`Self::ReturnValue`] from a proxy object. - fn build(self) -> Self::ReturnValue; - } - - impl Builder for Box { - type ReturnValue = T::ReturnValue; - - fn build(self) -> Self::ReturnValue { - T::build(*self) - } - } - - /// Deserialization helper for proxy fields that wrap an `Option` - /// - /// # Errors - /// When deserialization of the field fails, e.g. it doesn't have - /// the `Option>` - #[allow(clippy::option_option)] - pub fn some_option<'de, T, D>(deserializer: D) -> Result>, D::Error> - where - T: Deserialize<'de>, - D: Deserializer<'de>, - { - Option::::deserialize(deserializer).map(Some) - } -} +//! Base configuration utilities diff --git a/config/src/block_sync.rs b/config/src/block_sync.rs deleted file mode 100644 index dd927df3ece..00000000000 --- a/config/src/block_sync.rs +++ /dev/null @@ -1,50 +0,0 @@ -//! Module for `BlockSynchronizer`-related configuration and structs. -use iroha_config_base::derive::Proxy; -use serde::{Deserialize, Serialize}; - -const DEFAULT_BLOCK_BATCH_SIZE: u32 = 4; -const DEFAULT_GOSSIP_PERIOD_MS: u64 = 10000; -const DEFAULT_ACTOR_CHANNEL_CAPACITY: u32 = 100; - -/// Configuration for `BlockSynchronizer`. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, Proxy)] -#[serde(rename_all = "UPPERCASE")] -#[config(env_prefix = "BLOCK_SYNC_")] -pub struct Configuration { - /// The period of time to wait between sending requests for the latest block. - pub gossip_period_ms: u64, - /// The number of blocks that can be sent in one message. - /// Underlying network (`iroha_network`) should support transferring messages this large. - pub block_batch_size: u32, - /// Buffer capacity of actor's MPSC channel - pub actor_channel_capacity: u32, -} - -impl Default for ConfigurationProxy { - fn default() -> Self { - Self { - gossip_period_ms: Some(DEFAULT_GOSSIP_PERIOD_MS), - block_batch_size: Some(DEFAULT_BLOCK_BATCH_SIZE), - actor_channel_capacity: Some(DEFAULT_ACTOR_CHANNEL_CAPACITY), - } - } -} - -#[cfg(test)] -pub mod tests { - use proptest::prelude::*; - - use super::*; - - prop_compose! { - pub fn arb_proxy() - ( - gossip_period_ms in prop::option::of(Just(DEFAULT_GOSSIP_PERIOD_MS)), - block_batch_size in prop::option::of(Just(DEFAULT_BLOCK_BATCH_SIZE)), - actor_channel_capacity in prop::option::of(Just(DEFAULT_ACTOR_CHANNEL_CAPACITY)), - ) - -> ConfigurationProxy { - ConfigurationProxy { gossip_period_ms, block_batch_size, actor_channel_capacity } - } - } -} diff --git a/config/src/client_api.rs b/config/src/client_api.rs index 030edb8523a..1e1db9bd7e7 100644 --- a/config/src/client_api.rs +++ b/config/src/client_api.rs @@ -12,7 +12,7 @@ use iroha_data_model::Level; use serde::{Deserialize, Serialize}; -use super::{iroha::Configuration as BaseConfiguration, logger::Configuration as BaseLogger}; +use super::parameters::{logger::Config as BaseLogger, Config as BaseConfiguration}; /// Subset of [`super::iroha`] configuration. #[derive(Debug, Serialize, Deserialize, Clone, Copy)] @@ -24,7 +24,7 @@ pub struct ConfigurationDTO { impl From<&'_ BaseConfiguration> for ConfigurationDTO { fn from(value: &'_ BaseConfiguration) -> Self { Self { - logger: value.logger.as_ref().into(), + logger: (&value.logger).into(), } } } diff --git a/config/src/genesis.rs b/config/src/genesis.rs deleted file mode 100644 index b6881ac4d65..00000000000 --- a/config/src/genesis.rs +++ /dev/null @@ -1,141 +0,0 @@ -//! Module with genesis configuration logic. -use std::path::PathBuf; - -use eyre::Report; -use iroha_config_base::derive::{view, Proxy}; -use iroha_crypto::{KeyPair, PrivateKey, PublicKey}; -use iroha_genesis::RawGenesisBlock; -use serde::{Deserialize, Serialize}; - -// Generate `ConfigurationView` without the private key -view! { - /// Configuration of the genesis block and the process of its submission. - #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Proxy)] - #[serde(rename_all = "UPPERCASE")] - #[config(env_prefix = "IROHA_GENESIS_")] - pub struct Configuration { - /// The public key of the genesis account, should be supplied to all peers. - #[config(serde_as_str)] - pub public_key: PublicKey, - /// The private key of the genesis account, only needed for the peer that submits the genesis block. - #[view(ignore)] - pub private_key: Option, - /// Path to the genesis file - #[config(serde_as_str)] - pub file: Option - } -} - -impl Default for ConfigurationProxy { - fn default() -> Self { - Self { - 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; - use proptest::prelude::*; - - use super::*; - - /// Key-pair used by default for test purposes - fn placeholder_keypair() -> KeyPair { - let public_key = "ed01204CFFD0EE429B1BDD36B3910EC570852B8BB63F18750341772FB46BC856C5CAAF" - .parse() - .expect("Public key not in multihash format"); - let private_key = PrivateKey::from_hex( - iroha_crypto::Algorithm::Ed25519, - "D748E18CE60CB30DEA3E73C9019B7AF45A8D465E3D71BCC9A5EF99A008205E534CFFD0EE429B1BDD36B3910EC570852B8BB63F18750341772FB46BC856C5CAAF" - ).expect("Private key not hex encoded"); - - KeyPair::new(public_key, private_key).expect("Key pair mismatch") - } - - #[allow(clippy::option_option)] - fn arb_keys() -> BoxedStrategy<(Option, Option>)> { - let (pub_key, _) = placeholder_keypair().into(); - ( - prop::option::of(Just(pub_key)), - prop::option::of(Just(None)), - ) - .boxed() - } - - prop_compose! { - pub fn arb_proxy() - ( - (public_key, private_key) in arb_keys(), - file in prop::option::of(Just(None)) - ) - -> ConfigurationProxy { - ConfigurationProxy { public_key, private_key, file } - } - } -} diff --git a/config/src/iroha.rs b/config/src/iroha.rs deleted file mode 100644 index ac33c9a2f32..00000000000 --- a/config/src/iroha.rs +++ /dev/null @@ -1,282 +0,0 @@ -//! This module contains [`struct@Configuration`] structure and related implementation. -use std::fmt::Debug; - -use iroha_config_base::derive::{view, Error as ConfigError, Proxy}; -use iroha_crypto::prelude::*; -use iroha_data_model::ChainId; -use serde::{Deserialize, Serialize}; - -use super::*; - -// Generate `ConfigurationView` without the private key -view! { - /// Configuration parameters for a peer - #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Proxy)] - #[serde(rename_all = "UPPERCASE")] - #[config(env_prefix = "IROHA_")] - pub struct Configuration { - /// Unique id of the blockchain. Used for simple replay attack protection. - #[config(serde_as_str)] - pub chain_id: ChainId, - /// Public key of this peer - #[config(serde_as_str)] - pub public_key: PublicKey, - /// Private key of this peer - #[view(ignore)] - pub private_key: PrivateKey, - /// `Kura` configuration - #[config(inner)] - pub kura: Box, - /// `Sumeragi` configuration - #[config(inner)] - #[view(into = Box)] - pub sumeragi: Box, - /// `Torii` configuration - #[config(inner)] - pub torii: Box, - /// `BlockSynchronizer` configuration - #[config(inner)] - pub block_sync: block_sync::Configuration, - /// `Queue` configuration - #[config(inner)] - pub queue: queue::Configuration, - /// `Logger` configuration - #[config(inner)] - pub logger: Box, - /// `GenesisBlock` configuration - #[config(inner)] - #[view(into = Box)] - pub genesis: Box, - /// `WorldStateView` configuration - #[config(inner)] - pub wsv: Box, - /// Network configuration - #[config(inner)] - pub network: network::Configuration, - /// Telemetry configuration - #[config(inner)] - pub telemetry: Box, - /// SnapshotMaker configuration - #[config(inner)] - pub snapshot: Box, - /// LiveQueryStore configuration - #[config(inner)] - pub live_query_store: live_query_store::Configuration, - } -} - -impl Default for ConfigurationProxy { - fn default() -> Self { - Self { - chain_id: None, - public_key: None, - private_key: None, - kura: Some(Box::default()), - sumeragi: Some(Box::default()), - torii: Some(Box::default()), - block_sync: Some(block_sync::ConfigurationProxy::default()), - queue: Some(queue::ConfigurationProxy::default()), - logger: Some(Box::default()), - genesis: Some(Box::default()), - wsv: Some(Box::default()), - network: Some(network::ConfigurationProxy::default()), - telemetry: Some(Box::default()), - snapshot: Some(Box::default()), - live_query_store: Some(live_query_store::ConfigurationProxy::default()), - } - } -} - -impl ConfigurationProxy { - /// Finalise Iroha config proxy by instantiating mutually equivalent fields - /// via the uppermost Iroha config fields. Configuration fields provided in the - /// Iroha config always overwrite those in sumeragi even in case of discrepancy, - /// so proper care is advised. - /// - /// # Errors - /// - If the relevant uppermost Iroha config fields were not provided. - pub fn finish(&mut self) -> Result<(), ConfigError> { - if let Some(sumeragi_proxy) = &mut self.sumeragi { - // First, iroha public/private key and sumeragi keypair are interchangeable, but - // the user is allowed to provide only the former, and keypair is generated automatically, - // bailing out if key_pair provided in sumeragi no matter its value - if sumeragi_proxy.key_pair.is_some() { - return Err(ConfigError::ProvidedInferredField { - field: "key_pair", - message: "Sumeragi should not be provided with `KEY_PAIR` directly. That value is computed from the other config parameters. Please set the `KEY_PAIR` to `null` or omit entirely." - }); - } - if let (Some(public_key), Some(private_key)) = (&self.public_key, &self.private_key) { - sumeragi_proxy.key_pair = - Some(KeyPair::new(public_key.clone(), private_key.clone())?); - } else { - return Err(ConfigError::MissingField { - field: "PUBLIC_KEY and PRIVATE_KEY", - message: "The sumeragi keypair is not provided in the example configuration. It's done this way to ensure you don't re-use the example keys in production, and know how to generate new keys. Please have a look at \n\nhttps://hyperledger.github.io/iroha-2-docs/guide/configure/keys.html\n\nto learn more.\n\n-----", - }); - } - // Second, torii gateway and sumeragi peer id are interchangeable too; the latter is derived from the - // former and overwritten silently in case of difference - if let Some(torii_proxy) = &mut self.torii { - if sumeragi_proxy.peer_id.is_none() { - sumeragi_proxy.peer_id = Some(iroha_data_model::prelude::PeerId::new( - torii_proxy - .p2p_addr - .clone() - .ok_or(ConfigError::MissingField { - field: "p2p_addr", - message: - "`p2p_addr` should not be set to `null` or `None` explicitly.", - })?, - self.public_key.clone().expect( - "Iroha `public_key` should have been initialized above at the latest", - ), - )); - } else { - // TODO: should we just warn the user that this value will be ignored? - // TODO: Consider eliminating this value from the public API. - return Err(ConfigError::ProvidedInferredField { - field: "PEER_ID", - message: "The `peer_id` is computed from the key and address. You should remove it from the config.", - }); - } - } else { - return Err(ConfigError::MissingField{ - field: "p2p_addr", - message: "Torii config should have at least `p2p_addr` provided for sumeragi finalisation", - }); - } - - sumeragi_proxy.insert_self_as_trusted_peers() - } - - Ok(()) - } - - /// The wrapper around the topmost Iroha `ConfigurationProxy` - /// that performs finalisation prior to building. For the uppermost - /// Iroha config, its `::build()` - /// method should never be used directly, as only this wrapper ensures final - /// coherence. - /// - /// # Errors - /// - Finalisation fails - /// - Building fails, e.g. any of the inner fields had a `None` value when that - /// is not allowed by the defaults. - pub fn build(mut self) -> Result { - self.finish()?; - ::build(self) - } -} - -#[cfg(test)] -pub mod tests { - use std::path::PathBuf; - - use proptest::prelude::*; - - use super::*; - use crate::{base::proxy::LoadFromDisk, sumeragi::TrustedPeers}; - - const CONFIGURATION_PATH: &str = "./iroha_test_config.json"; - - /// Key-pair used for proptests generation - pub fn placeholder_keypair() -> KeyPair { - let private_key = PrivateKey::from_hex( - Algorithm::Ed25519, - "282ED9F3CF92811C3818DBC4AE594ED59DC1A2F78E4241E31924E101D6B1FB831C61FAF8FE94E253B93114240394F79A607B7FA55F9E5A41EBEC74B88055768B" - ).expect("Private key not hex encoded"); - - KeyPair::new( - "ed01201C61FAF8FE94E253B93114240394F79A607B7FA55F9E5A41EBEC74B88055768B" - .parse() - .expect("Public key not in mulithash format"), - private_key, - ) - .expect("Key pair mismatch") - } - - fn arb_keys() -> BoxedStrategy<(Option, Option)> { - let (pub_key, priv_key) = placeholder_keypair().into(); - ( - prop::option::of(Just(pub_key)), - prop::option::of(Just(priv_key)), - ) - .boxed() - } - - pub fn placeholder_chain_id() -> ChainId { - ChainId::new("0") - } - - prop_compose! { - fn arb_proxy()( - chain_id in prop::option::of(Just(placeholder_chain_id())), - (public_key, private_key) in arb_keys(), - 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))), - block_sync in prop::option::of(block_sync::tests::arb_proxy()), - queue in prop::option::of(queue::tests::arb_proxy()), - logger in prop::option::of(logger::tests::arb_proxy().prop_map(Box::new)), - genesis in prop::option::of(genesis::tests::arb_proxy().prop_map(Box::new)), - wsv in prop::option::of(wsv::tests::arb_proxy().prop_map(Box::new)), - network in prop::option::of(network::tests::arb_proxy()), - telemetry in prop::option::of(telemetry::tests::arb_proxy().prop_map(Box::new)), - 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 { chain_id, public_key, private_key, kura, sumeragi, torii, block_sync, queue, - logger, genesis, wsv, network, telemetry, snapshot, live_query_store } - } - } - - proptest! { - fn __iroha_proxy_build_fails_on_none(proxy in arb_proxy()) { - let cfg = proxy.build(); - let example_cfg = ConfigurationProxy::from_path(CONFIGURATION_PATH).build().expect("Failed to build example Iroha config"); - if cfg.is_ok() { - assert_eq!(cfg.unwrap(), example_cfg) - } - } - } - - #[test] - fn iroha_proxy_build_fails_on_none() { - // Using `stacker` because test generated by `proptest!` takes too much stack space. - // Allocating 3MB. - stacker::grow(3 * 1024 * 1024, __iroha_proxy_build_fails_on_none) - } - - #[test] - fn parse_example_json() { - let cfg_proxy = ConfigurationProxy::from_path(CONFIGURATION_PATH); - assert_eq!( - PathBuf::from("./storage"), - cfg_proxy.kura.unwrap().block_store_path.unwrap() - ); - assert_eq!( - 10000, - cfg_proxy - .block_sync - .expect("Block sync configuration was None") - .gossip_period_ms - .expect("Gossip period was None") - ); - } - - #[test] - fn example_json_proxy_builds() { - ConfigurationProxy::from_path(CONFIGURATION_PATH).build().unwrap_or_else(|err| panic!("`ConfigurationProxy` specified in {CONFIGURATION_PATH} \ - failed to build. This probably means that some of the fields there were not updated \ - properly with new changes. Error: {err}")); - } - - #[test] - #[should_panic(expected = "Failed to parse Trusted Peers")] - fn parse_trusted_peers_fail_duplicate_peer_id() { - let trusted_peers_string = r#"[{"address":"127.0.0.1:1337", "public_key": "ed0120954C83A4220FAFFB2C1D23FC5225B3E7952D53ACBB2A065FF30C631E5E1D6B10"}, {"address":"127.0.0.1:1337", "public_key": "ed0120954C83A4220FAFFB2C1D23FC5225B3E7952D53ACBB2A065FF30C631E5E1D6B10"}, {"address":"localhost:1338", "public_key": "ed0120954C83A4220FAFFB2C1D23FC5225B3E7952D53ACBB2A065FF30C631E5E1D6B10"}, {"address": "195.162.0.1:23", "public_key": "ed0120954C83A4220FAFFB2C1D23FC5225B3E7952D53ACBB2A065FF30C631E5E1D6B10"}]"#; - let _result: TrustedPeers = - serde_json::from_str(trusted_peers_string).expect("Failed to parse Trusted Peers"); - } -} diff --git a/config/src/kura.rs b/config/src/kura.rs deleted file mode 100644 index 8f97dbbf94b..00000000000 --- a/config/src/kura.rs +++ /dev/null @@ -1,63 +0,0 @@ -//! Module for kura-related configuration and structs - -use std::path::PathBuf; - -use eyre::Result; -use iroha_config_base::derive::Proxy; -use serde::{Deserialize, Serialize}; - -const DEFAULT_BLOCK_STORE_PATH: &str = "./storage"; - -/// `Kura` configuration. -#[derive(Clone, Deserialize, Serialize, Debug, Proxy, PartialEq, Eq)] -#[serde(rename_all = "UPPERCASE")] -#[config(env_prefix = "KURA_")] -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. - #[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, -} - -impl Default for ConfigurationProxy { - fn default() -> Self { - Self { - init_mode: Some(Mode::default()), - block_store_path: Some(DEFAULT_BLOCK_STORE_PATH.into()), - debug_output_new_blocks: Some(false), - } - } -} - -/// Kura initialization mode. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)] -#[serde(rename_all = "snake_case")] -pub enum Mode { - /// Strict validation of all blocks. - #[default] - Strict, - /// Fast initialization with basic checks. - Fast, -} - -#[cfg(test)] -pub mod tests { - use proptest::prelude::*; - - use super::*; - - prop_compose! { - pub fn arb_proxy() - ( - init_mode in prop::option::of(Just(Mode::default())), - block_store_path in prop::option::of(Just(DEFAULT_BLOCK_STORE_PATH.into())), - debug_output_new_blocks in prop::option::of(Just(false)) - ) - -> ConfigurationProxy { - ConfigurationProxy { init_mode, block_store_path, debug_output_new_blocks } - } - } -} diff --git a/config/src/lib.rs b/config/src/lib.rs index 423e5a8dd19..b969ce51319 100644 --- a/config/src/lib.rs +++ b/config/src/lib.rs @@ -1,20 +1,298 @@ -//! Aggregate configuration for different Iroha modules. -pub use iroha_config_base as base; +//! Iroha configuration and related utilities. + +// FIXME +#![allow(unused, missing_docs, missing_copy_implementations)] + +use std::{ + cell::RefCell, + collections::{HashMap, HashSet}, + error::Error, + fmt::{Debug, Display, Formatter}, + io::Read, + ops::Sub, + str::FromStr, + time::Duration, +}; + +use eyre::{eyre, Report, Result, WrapErr}; +use serde::{Deserialize, Serialize}; -pub mod block_sync; -pub mod client; pub mod client_api; -pub mod genesis; -pub mod iroha; -pub mod kura; -pub mod live_query_store; -pub mod logger; -pub mod network; -pub mod path; -pub mod queue; -pub mod snapshot; -pub mod sumeragi; -pub mod telemetry; -pub mod torii; -pub mod wasm; -pub mod wsv; +pub mod parameters; + +/// User-provided duration +#[derive(Debug, Copy, Clone, Deserialize, Serialize)] +pub struct UserDuration(Duration); + +impl UserDuration { + pub fn get(self) -> Duration { + self.0 + } +} + +/// Byte size +#[derive(Debug, Copy, Clone, Deserialize, Serialize)] +pub struct ByteSize(u64); + +pub trait Complete { + type Output; + + fn complete(self) -> CompleteResult; +} + +// struct StandardEnv; + +pub trait ReadEnv { + fn get(&self, key: impl AsRef) -> Option<&str>; +} + +pub trait FromEnv { + fn from_env(env: &impl ReadEnv) -> FromEnvResult + where + Self: Sized; +} + +pub type FromEnvResult = Result>; + +pub trait FromEnvDefaultFallback {} + +impl FromEnv for T +where + T: FromEnvDefaultFallback + Default, +{ + fn from_env(env: &impl ReadEnv) -> FromEnvResult + where + Self: Sized, + { + Ok(Self::default()) + } +} + +pub struct Emitter { + errors: Vec, + bomb: drop_bomb::DropBomb, +} + +impl Emitter { + fn new() -> Self { + Self { + errors: Vec::new(), + bomb: drop_bomb::DropBomb::new( + "Errors emitter is dropped without consuming collected errors", + ), + } + } + + fn emit(&mut self, error: T) { + self.errors.push(error); + } + + fn emit_collection(&mut self, mut errors: ErrorsCollection) { + self.errors.append(&mut errors.0); + } + + fn finish(mut self) -> Result<(), ErrorsCollection> { + self.bomb.defuse(); + if !self.errors.is_empty() { + Err(ErrorsCollection(self.errors)) + } else { + Ok(()) + } + } +} + +#[derive(thiserror::Error, Debug)] +pub enum CompleteError { + #[error("Missing field: {path}")] + MissingField { path: String }, + #[error(transparent)] + Custom(#[from] Report), +} + +pub type CompleteResult = Result>; + +impl CompleteError { + pub fn missing_field(field_name: impl AsRef) -> Self { + Self::MissingField { + path: field_name.as_ref().to_string(), + } + } +} + +impl Emitter { + fn emit_missing_field(&mut self, field_name: impl AsRef) { + self.emit(CompleteError::MissingField { + path: field_name.as_ref().to_string(), + }) + } +} + +pub struct ErrorsCollection(Vec); + +impl Error for ErrorsCollection {} + +impl Display for ErrorsCollection +where + T: Display, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + for (i, item) in self.0.iter().enumerate() { + if i > 0 { + write!(f, "\n")?; + } + write!(f, "{item}")?; + } + Ok(()) + } +} + +impl Debug for ErrorsCollection +where + T: Debug, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + for (i, item) in self.0.iter().enumerate() { + if i > 0 { + write!(f, "\n")?; + } + write!(f, "{item:?}")?; + } + Ok(()) + } +} + +impl From for ErrorsCollection { + fn from(value: T) -> Self { + Self(vec![value]) + } +} + +impl IntoIterator for ErrorsCollection { + type Item = T; + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + +#[derive(Default)] +pub struct TestEnv { + map: HashMap, + visited: RefCell>, +} + +impl TestEnv { + pub fn new() -> Self { + Self::default() + } + + pub fn with_map(map: HashMap) -> Self { + Self { map, ..Self::new() } + } + + pub fn set(mut self, key: impl AsRef, value: impl AsRef) -> Self { + self.map + .insert(key.as_ref().to_string(), value.as_ref().to_string()); + self + } + + pub fn unvisited(&self) -> HashSet { + let all_keys: HashSet<_> = self.map.keys().map(ToOwned::to_owned).collect(); + let visited: HashSet<_> = self.visited.borrow().clone(); + all_keys.sub(&visited) + } +} + +impl ReadEnv for TestEnv { + fn get(&self, key: impl AsRef) -> Option<&str> { + self.visited.borrow_mut().insert(key.as_ref().to_string()); + self.map.get(key.as_ref()).map(|x| x.as_str()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn single_missing_field() { + let mut emitter = Emitter::new(); + + emitter.emit(CompleteError::missing_field("foo".to_string())); + + let err = emitter.finish().unwrap_err(); + + assert_eq!(format!("{err}"), "Missing field: foo") + } + + #[test] + fn multiple_missing_fields() { + let mut emitter = Emitter::new(); + + emitter.emit(CompleteError::missing_field("foo".to_string())); + emitter.emit(CompleteError::missing_field("bar".to_string())); + + let err = emitter.finish().unwrap_err(); + + assert_eq!(format!("{err}"), "Missing field: foo\nMissing field: bar") + } +} + +enum ParseEnvResult { + Value(T), + ParseError, + None, +} + +impl ParseEnvResult +where + T: FromStr, + ::Err: Error + Send + Sync + 'static, +{ + fn parse_simple( + emitter: &mut Emitter, + env: &impl ReadEnv, + env_key: impl AsRef, + field_name: impl AsRef, + ) -> Self { + match env + .get(env_key.as_ref()) + .map(FromStr::from_str) + .transpose() + .wrap_err_with(|| { + eyre!( + "failed to parse `{}` field from `{}` env variable", + field_name.as_ref(), + env_key.as_ref() + ) + }) { + Ok(Some(x)) => Self::Value(x), + Ok(None) => Self::None, + Err(report) => { + emitter.emit(report); + Self::ParseError + } + } + } +} + +impl From> for Option { + fn from(value: ParseEnvResult) -> Self { + match value { + ParseEnvResult::None => None, + ParseEnvResult::ParseError => None, + ParseEnvResult::Value(x) => Some(x), + } + } +} + +// impl From>> for ParseEnvResult { +// fn from(value: Result>) -> Self { +// match value { +// Ok(Some(value)) => Self::Value(value), +// Ok(None) => Self::None, +// Err(report) => +// } +// } +// } diff --git a/config/src/live_query_store.rs b/config/src/live_query_store.rs deleted file mode 100644 index de8b2a31ec2..00000000000 --- a/config/src/live_query_store.rs +++ /dev/null @@ -1,44 +0,0 @@ -//! Module for `LiveQueryStore`-related configuration and structs. - -use std::num::NonZeroU64; - -use iroha_config_base::derive::Proxy; -use serde::{Deserialize, Serialize}; - -/// Default max time a query can remain in the store unaccessed -pub static DEFAULT_QUERY_IDLE_TIME_MS: once_cell::sync::Lazy = - once_cell::sync::Lazy::new(|| NonZeroU64::new(30_000).unwrap()); - -/// Configuration for `QueryService`. -#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize, Serialize, Proxy)] -#[serde(rename_all = "UPPERCASE")] -#[config(env_prefix = "LIVE_QUERY_STORE_")] -pub struct Configuration { - /// Time query can remain in the store if unaccessed - pub query_idle_time_ms: NonZeroU64, -} - -impl Default for ConfigurationProxy { - fn default() -> Self { - Self { - query_idle_time_ms: Some(*DEFAULT_QUERY_IDLE_TIME_MS), - } - } -} - -#[cfg(test)] -pub mod tests { - use proptest::prelude::*; - - use super::*; - - prop_compose! { - pub fn arb_proxy() - ( - query_idle_time_ms in prop::option::of(Just(*DEFAULT_QUERY_IDLE_TIME_MS)), - ) - -> ConfigurationProxy { - ConfigurationProxy { query_idle_time_ms } - } - } -} diff --git a/config/src/network.rs b/config/src/network.rs deleted file mode 100644 index 845743fac42..00000000000 --- a/config/src/network.rs +++ /dev/null @@ -1,39 +0,0 @@ -//! Module for network-related configuration and structs -use iroha_config_base::derive::Proxy; -use serde::{Deserialize, Serialize}; - -const DEFAULT_ACTOR_CHANNEL_CAPACITY: u32 = 100; - -/// Network Configuration parameters -#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, Proxy)] -#[serde(rename_all = "UPPERCASE")] -#[config(env_prefix = "IROHA_NETWORK_")] -pub struct Configuration { - /// Buffer capacity of actor's MPSC channel - pub actor_channel_capacity: u32, -} - -impl Default for ConfigurationProxy { - fn default() -> Self { - Self { - actor_channel_capacity: Some(DEFAULT_ACTOR_CHANNEL_CAPACITY), - } - } -} - -#[cfg(test)] -pub mod tests { - use proptest::prelude::*; - - use super::*; - - prop_compose! { - pub fn arb_proxy() - ( - actor_channel_capacity in prop::option::of(Just(DEFAULT_ACTOR_CHANNEL_CAPACITY)), - ) - -> ConfigurationProxy { - ConfigurationProxy { actor_channel_capacity } - } - } -} diff --git a/config/src/parameters/chain_wide.rs b/config/src/parameters/chain_wide.rs new file mode 100644 index 00000000000..e3d47f60d8f --- /dev/null +++ b/config/src/parameters/chain_wide.rs @@ -0,0 +1,110 @@ +//! Chain-wide configuration parameters. +//! +//! They are supposed to be moved out of the configuration file: +//! [iroha#4028](https://github.com/hyperledger/iroha/issues/4028) + +use std::{ + num::{NonZeroU32, NonZeroU64}, + time::Duration, +}; + +use iroha_data_model::{prelude::MetadataLimits, transaction::TransactionLimits, LengthLimits}; +use nonzero_ext::nonzero; +use serde::{Deserialize, Serialize}; + +use crate::{ + ByteSize, Complete, CompleteError, CompleteResult, FromEnv, FromEnvDefaultFallback, + FromEnvResult, ReadEnv, UserDuration, +}; + +const DEFAULT_MAX_TXS: NonZeroU32 = nonzero!(2_u32.pow(9)); +const DEFAULT_BLOCK_TIME: Duration = Duration::from_secs(2); +const DEFAULT_COMMIT_TIME_LIMIT: Duration = Duration::from_secs(4); +const DEFAULT_WASM_FUEL_LIMIT: NonZeroU64 = nonzero!(30_000_000u64); +const DEFAULT_WASM_MAX_MEMORY: u64 = 500 * 2_u64.pow(20); // 500 MiB + +/// Default limits for metadata +const DEFAULT_METADATA_LIMITS: MetadataLimits = MetadataLimits::new(2_u32.pow(20), 2_u32.pow(12)); +/// Default limits for ident length +const DEFAULT_IDENT_LENGTH_LIMITS: LengthLimits = LengthLimits::new(1, 2_u32.pow(7)); +/// Default maximum number of instructions and expressions per transaction +const DEFAULT_MAX_INSTRUCTION_NUMBER: u64 = 2_u64.pow(12); +/// Default maximum number of instructions and expressions per transaction +const DEFAULT_MAX_WASM_SIZE_BYTES: u64 = 4 * 2_u64.pow(20); // 4 MiB + +/// Default transaction limits +const DEFAULT_TRANSACTION_LIMITS: TransactionLimits = + TransactionLimits::new(DEFAULT_MAX_INSTRUCTION_NUMBER, DEFAULT_MAX_WASM_SIZE_BYTES); + +#[derive(Deserialize, Serialize, Debug, Default)] +#[serde(deny_unknown_fields)] +pub struct UserLayer { + pub max_transactions_in_block: Option, + pub block_time: Option, + pub commit_time: Option, + pub transactions_limits: Option, + pub asset_metadata_limits: Option, + pub asset_definition_metadata_limits: Option, + pub account_metadata_limits: Option, + pub domain_metadata_limits: Option, + pub identifier_length_limits: Option, + pub wasm_fuel_limit: Option, + pub wasm_max_memory: Option, +} + +#[derive(Debug)] +pub struct Config { + pub max_transactions_in_block: NonZeroU32, + pub block_time: Duration, + pub commit_time: Duration, + pub transactions_limits: TransactionLimits, + pub asset_metadata_limits: MetadataLimits, + pub asset_definition_metadata_limits: MetadataLimits, + pub account_metadata_limits: MetadataLimits, + pub domain_metadata_limits: MetadataLimits, + pub identifier_length_limits: LengthLimits, + pub wasm_fuel_limit: NonZeroU64, + pub wasm_max_memory: ByteSize, +} + +impl Complete for UserLayer { + type Output = Config; + + fn complete(self) -> CompleteResult { + Ok(Config { + max_transactions_in_block: self.max_transactions_in_block.unwrap_or(DEFAULT_MAX_TXS), + block_time: self + .block_time + .map(UserDuration::get) + .unwrap_or(DEFAULT_BLOCK_TIME), + commit_time: self + .commit_time + .map(UserDuration::get) + .unwrap_or(DEFAULT_COMMIT_TIME_LIMIT), + transactions_limits: self + .transactions_limits + .unwrap_or(DEFAULT_TRANSACTION_LIMITS), + asset_metadata_limits: self + .asset_metadata_limits + .unwrap_or(DEFAULT_METADATA_LIMITS), + asset_definition_metadata_limits: self + .asset_definition_metadata_limits + .unwrap_or(DEFAULT_METADATA_LIMITS), + account_metadata_limits: self + .account_metadata_limits + .unwrap_or(DEFAULT_METADATA_LIMITS), + domain_metadata_limits: self + .domain_metadata_limits + .unwrap_or(DEFAULT_METADATA_LIMITS), + identifier_length_limits: self + .identifier_length_limits + .unwrap_or(DEFAULT_IDENT_LENGTH_LIMITS), + wasm_fuel_limit: self.wasm_fuel_limit.unwrap_or(DEFAULT_WASM_FUEL_LIMIT), + wasm_max_memory: self + .wasm_max_memory + .unwrap_or(ByteSize(DEFAULT_WASM_MAX_MEMORY)), + }) + } +} + +impl FromEnvDefaultFallback for UserLayer {} diff --git a/config/src/parameters/genesis.rs b/config/src/parameters/genesis.rs new file mode 100644 index 00000000000..3e6eb9d1695 --- /dev/null +++ b/config/src/parameters/genesis.rs @@ -0,0 +1,118 @@ +//! Module with genesis configuration logic. +use std::path::PathBuf; + +use eyre::{eyre, Context, Report}; +use iroha_crypto::{KeyPair, PrivateKey, PublicKey}; +use iroha_genesis::RawGenesisBlock; +use serde::{Deserialize, Serialize}; + +use crate::{ + Complete, CompleteError, CompleteResult, Emitter, FromEnv, FromEnvDefaultFallback, + FromEnvResult, ParseEnvResult, ReadEnv, +}; + +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Default)] +#[serde(deny_unknown_fields)] +pub struct UserLayer { + pub public_key: Option, + pub private_key: Option, + pub file: Option, +} + +#[derive(Debug)] +pub enum Config { + /// 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 Complete for UserLayer { + type Output = Config; + + fn complete(self) -> CompleteResult { + let public_key = self + .public_key + .ok_or_else(|| CompleteError::missing_field("public_key"))?; + + match (self.private_key, self.file) { + (None, None) => Ok(Config::Partial { public_key }), + (Some(private_key), Some(path)) => { + let raw_block = RawGenesisBlock::from_path(&path) + .map_err(|report| GenesisConfigError::File { path, report }) + .wrap_err("FIXME don't know how to wrap error here") + .map_err(CompleteError::Custom)?; + + Ok(Config::Full { + key_pair: KeyPair::new(public_key, private_key) + .map_err(GenesisConfigError::from) + .wrap_err("FIXME") + .map_err(CompleteError::Custom)?, + raw_block, + }) + } + _ => Err(GenesisConfigError::Inconsistent) + .wrap_err("FIXME") + .map_err(CompleteError::Custom)?, + } + } +} + +impl FromEnv for UserLayer { + fn from_env(env: &impl ReadEnv) -> FromEnvResult + where + Self: Sized, + { + let mut emitter = Emitter::new(); + + let public_key = ParseEnvResult::parse_simple( + &mut emitter, + env, + "GENESIS_PUBLIC_KEY", + "genesis.public_key", + ) + .into(); + let private_key = super::iroha::private_key_from_env( + &mut emitter, + env, + "GENESIS_PRIVATE_KEY", + "genesis.private_key", + ) + .into(); + let file = + ParseEnvResult::parse_simple(&mut emitter, env, "GENESIS_FILE", "genesis.file").into(); + + emitter.finish()?; + + Ok(Self { + public_key, + private_key, + file, + }) + } +} + +/// Error which might occur during [`Configuration::parse()`] +#[derive(Debug, displaydoc::Display, thiserror::Error)] +pub enum GenesisConfigError { + /// `genesis.file` and `genesis.private_key` should be set together + Inconsistent, + /// invalid genesis key pair + KeyPair(#[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, + }, +} diff --git a/config/src/parameters/iroha.rs b/config/src/parameters/iroha.rs new file mode 100644 index 00000000000..d0dde18376d --- /dev/null +++ b/config/src/parameters/iroha.rs @@ -0,0 +1,224 @@ +//! Basic parameters like the key pair and p2p address + +use std::{error::Error, str::FromStr}; + +use eyre::{eyre, Context, Report}; +use iroha_crypto::{Algorithm, PrivateKey, PublicKey}; +use iroha_data_model::ChainId; +use iroha_primitives::addr::SocketAddr; +use serde::{Deserialize, Serialize}; + +use crate::{ + Complete, CompleteError, CompleteResult, Emitter, FromEnv, FromEnvResult, ParseEnvResult, + ReadEnv, +}; + +#[derive(Deserialize, Serialize, Debug, Default)] +#[serde(deny_unknown_fields)] +pub struct UserLayer { + pub chain_id: Option, + pub public_key: Option, + pub private_key: Option, + pub p2p_address: Option, +} + +impl Complete for UserLayer { + type Output = Config; + + fn complete(self) -> CompleteResult { + let mut emitter = super::Emitter::::new(); + + if let None = self.public_key { + emitter.emit_missing_field("public_key"); + } + + if let None = self.private_key { + emitter.emit_missing_field("private_key"); + } + + if let None = self.p2p_address { + emitter.emit_missing_field("p2p_address"); + } + + emitter.finish()?; + + Ok(Config { + public_key: self.public_key.unwrap(), + private_key: self.private_key.unwrap(), + p2p_address: self.p2p_address.unwrap(), + }) + } +} + +pub(crate) fn private_key_from_env( + emitter: &mut Emitter, + env: &impl ReadEnv, + env_key_base: impl AsRef, + name_base: impl AsRef, +) -> ParseEnvResult { + let digest_env = format!("{}_DIGEST", env_key_base.as_ref()); + let digest_name = format!("{}.digest_function", name_base.as_ref()); + let payload_env = format!("{}_PAYLOAD", env_key_base.as_ref()); + let payload_name = format!("{}.payload", name_base.as_ref()); + + let digest_function = ParseEnvResult::parse_simple(emitter, env, &digest_env, &digest_name); + + let payload = env.get(&payload_env).map(ToOwned::to_owned); + + match (digest_function, payload) { + (ParseEnvResult::Value(digest_function), Some(payload)) => { + PrivateKey::from_hex(digest_function, &payload) + .wrap_err_with(|| { + eyre!( + "failed to construct `{}` from `{}` and `{}` environment variables", + name_base.as_ref(), + &digest_env, + &payload_env + ) + }) + .map(ParseEnvResult::Value) + .unwrap_or_else(|report| { + emitter.emit(report); + ParseEnvResult::ParseError + }) + } + (ParseEnvResult::None, None) | (ParseEnvResult::ParseError, _) => ParseEnvResult::None, + (ParseEnvResult::Value(_), None) => { + emitter.emit(eyre!( + "`{}` env was provided, but `{}` was not", + &digest_env, + &payload_env + )); + ParseEnvResult::ParseError + } + (ParseEnvResult::None, Some(_)) => { + emitter.emit(eyre!( + "`{}` env was provided, but `{}` was not", + &payload_env, + &digest_env + )); + ParseEnvResult::ParseError + } + } +} + +impl FromEnv for UserLayer { + fn from_env(env: &impl ReadEnv) -> FromEnvResult + where + Self: Sized, + { + let mut emitter = Emitter::new(); + + let chain_id = + ParseEnvResult::parse_simple(&mut emitter, env, "CHAIN_ID", "iroha.chain_id").into(); + let public_key = + ParseEnvResult::parse_simple(&mut emitter, env, "PUBLIC_KEY", "iroha.public_key") + .into(); + let private_key = + private_key_from_env(&mut emitter, env, "PRIVATE_KEY", "iroha.private_key").into(); + let p2p_address = + ParseEnvResult::parse_simple(&mut emitter, env, "P2P_ADDRESS", "iroha.p2p_address") + .into(); + + emitter.finish()?; + + Ok(Self { + chain_id, + public_key, + private_key, + p2p_address, + }) + } +} + +#[derive(Debug)] +pub struct Config { + pub chain_id: ChainId, + pub public_key: PublicKey, + pub private_key: PrivateKey, + pub p2p_address: SocketAddr, +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::TestEnv; + + #[test] + fn parses_private_key_from_env() { + let env = TestEnv::new() + .set("PRIVATE_KEY_DIGEST", "ed25519") + .set("PRIVATE_KEY_PAYLOAD", "8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb"); + + let private_key = UserLayer::from_env(&env) + .expect("input is valid, should not fail") + .private_key + .expect("private key is provided, should not fail"); + + assert_eq!(private_key.digest_function(), "ed25519".parse().unwrap()); + assert_eq!(hex::encode( private_key.payload()), "8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb"); + } + + #[test] + fn fails_to_parse_private_key_in_env_without_digest() { + let env = TestEnv::new().set("PRIVATE_KEY_DIGEST", "ed25519"); + let error = UserLayer::from_env(&env).expect_err("private key is incomplete, should fail"); + let expected = expect_test::expect![[r#" + `PRIVATE_KEY_DIGEST` env was provided, but `PRIVATE_KEY_PAYLOAD` was not + + Location: + config/src/parameters/iroha.rs:125:65"#]]; + expected.assert_eq(&format!("{error:?}")); + } + + #[test] + fn fails_to_parse_private_key_in_env_without_payload() { + let env = TestEnv::new().set("PRIVATE_KEY_PAYLOAD", "8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb"); + let error = UserLayer::from_env(&env).expect_err("private key is incomplete, should fail"); + let expected = expect_test::expect![[r#" + `PRIVATE_KEY_PAYLOAD` env was provided, but `PRIVATE_KEY_DIGEST` was not + + Location: + config/src/parameters/iroha.rs:126:64"#]]; + expected.assert_eq(&format!("{error:?}")); + } + + #[test] + fn fails_to_parse_private_key_from_env_with_invalid_payload() { + let env = TestEnv::new() + .set("PRIVATE_KEY_DIGEST", "ed25519") + .set("PRIVATE_KEY_PAYLOAD", "foo"); + + let error = UserLayer::from_env(&env).expect_err("input is invalid, should fail"); + + let expected = expect_test::expect![[r#" + failed to construct `iroha.private_key` from `PRIVATE_KEY_DIGEST` and `PRIVATE_KEY_PAYLOAD` environment variables + + Caused by: + Key could not be parsed. Odd number of digits + + Location: + config/src/parameters/iroha.rs:118:26"#]]; + expected.assert_eq(&format!("{error:?}")); + } + + #[test] + fn when_payload_provided_but_digest_is_invalid() { + let env = TestEnv::new() + .set("PRIVATE_KEY_DIGEST", "foo") + .set("PRIVATE_KEY_PAYLOAD", "8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb"); + + let error = UserLayer::from_env(&env).expect_err("input is invalid, should fail"); + + // TODO: print the bad value and supported ones + let expected = expect_test::expect![[r#" + failed to parse `iroha.private_key.digest_function` field from `PRIVATE_KEY_DIGEST` env variable + + Caused by: + Algorithm not supported + + Location: + config/src/parameters/iroha.rs:95:25"#]]; + expected.assert_eq(&format!("{error:?}")); + } +} diff --git a/config/src/parameters/kura.rs b/config/src/parameters/kura.rs new file mode 100644 index 00000000000..1dcde592eb1 --- /dev/null +++ b/config/src/parameters/kura.rs @@ -0,0 +1,144 @@ +//! Module for kura-related configuration and structs + +use std::{path::PathBuf, str::FromStr}; + +use derive_more::FromStr; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; + +use crate::{ + Complete, CompleteError, CompleteResult, Emitter, FromEnv, FromEnvResult, ParseEnvResult, + ReadEnv, +}; + +const DEFAULT_BLOCK_STORE_PATH: &str = "./storage"; + +/// `Kura` configuration. +#[derive(Clone, Deserialize, Serialize, Debug, Default)] +#[serde(deny_unknown_fields)] +pub struct UserLayer { + pub init_mode: Option, + pub block_store_path: Option, + pub debug: DebugUserConfig, +} + +#[derive(Clone, Deserialize, Serialize, Debug, Default)] +pub struct DebugUserConfig { + output_new_blocks: Option, +} + +#[derive(Debug)] +pub struct Config { + pub init_mode: Mode, + pub block_store_path: PathBuf, + pub debug_output_new_blocks: bool, +} + +impl Complete for UserLayer { + type Output = Config; + + fn complete(self) -> CompleteResult { + Ok(Config { + init_mode: self.init_mode.unwrap_or_default(), + block_store_path: self + .block_store_path + .unwrap_or_else(|| PathBuf::from(DEFAULT_BLOCK_STORE_PATH)), + debug_output_new_blocks: self.debug.output_new_blocks.unwrap_or(false), + }) + } +} + +impl FromEnv for UserLayer { + fn from_env(env: &impl ReadEnv) -> FromEnvResult + where + Self: Sized, + { + let mut emitter = Emitter::new(); + + let init_mode = + ParseEnvResult::parse_simple(&mut emitter, env, "KURA_INIT_MODE", "kura.init_mode") + .into(); + let block_store_path = ParseEnvResult::parse_simple( + &mut emitter, + env, + "KURA_BLOCK_STORE", + "kura.block_store_path", + ) + .into(); + let debug_output_new_blocks = ParseEnvResult::parse_simple( + &mut emitter, + env, + "KURA_DEBUG_OUTPUT_NEW_BLOCKS", + "kura.debug.output_new_blocks", + ) + .into(); + + emitter.finish()?; + + Ok(Self { + init_mode, + block_store_path, + debug: DebugUserConfig { + output_new_blocks: debug_output_new_blocks, + }, + }) + } +} + +/// Kura initialization mode. +#[derive( + Debug, Clone, Copy, PartialEq, Eq, Default, parse_display::Display, parse_display::FromStr, +)] +#[display(style = "snake_case")] +pub enum Mode { + /// Strict validation of all blocks. + #[default] + Strict, + /// Fast initialization with basic checks. + Fast, +} + +impl Serialize for Mode { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.collect_str(&self) + } +} + +impl<'de> Deserialize<'de> for Mode { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + String::deserialize(deserializer)? + .parse() + .map_err(serde::de::Error::custom) + } +} + +#[cfg(test)] +mod tests { + use serde_json::{from_value, json}; + + use super::*; + + #[test] + fn init_mode_display_reprs() { + assert_eq!(format!("{}", Mode::Strict), "strict"); + assert_eq!(format!("{}", Mode::Fast), "fast"); + assert_eq!("strict".parse::().unwrap(), Mode::Strict); + assert_eq!("fast".parse::().unwrap(), Mode::Fast); + } + + #[test] + fn init_mode_serde_uses_display() { + let sample = [Mode::Strict, Mode::Fast]; + let json = json!(["strict", "fast"]); + + assert_eq!(serde_json::to_string(&sample).unwrap(), json.to_string()); + + let encoded: [Mode; 2] = from_value(json).expect("should parse"); + assert_eq!(encoded, sample); + } +} diff --git a/config/src/logger.rs b/config/src/parameters/logger.rs similarity index 60% rename from config/src/logger.rs rename to config/src/parameters/logger.rs index 6d5e4e9d5e6..e85c90b598e 100644 --- a/config/src/logger.rs +++ b/config/src/parameters/logger.rs @@ -2,12 +2,13 @@ //! configuration, as well as run-time reloading of the log-level. use core::fmt::Debug; -use iroha_config_base::derive::Proxy; pub use iroha_data_model::Level; #[cfg(feature = "tokio-console")] use iroha_primitives::addr::{socket_addr, SocketAddr}; use serde::{Deserialize, Serialize}; +use crate::{Complete, CompleteError, CompleteResult, FromEnvDefaultFallback}; + #[cfg(feature = "tokio-console")] const DEFAULT_TOKIO_CONSOLE_ADDR: SocketAddr = socket_addr!(127.0.0.1:5555); @@ -23,14 +24,23 @@ pub fn into_tracing_level(level: Level) -> tracing::Level { } /// 'Logger' configuration. -#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Proxy)] -#[serde(rename_all = "UPPERCASE")] -#[config(env_prefix = "LOG_")] +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Default)] // `tokio_console_addr` is not `Copy`, but warning appears without `tokio-console` feature #[allow(missing_copy_implementations)] -pub struct Configuration { +#[serde(deny_unknown_fields)] +pub struct UserLayer { + /// Level of logging verbosity + pub level: Option, + /// Output format + pub format: Option, + #[cfg(feature = "tokio-console")] + /// Address of tokio console (only available under "tokio-console" feature) + pub tokio_console_addr: Option, +} + +#[derive(Debug)] +pub struct Config { /// Level of logging verbosity - #[config(serde_as_str)] pub level: Level, /// Output format pub format: Format, @@ -40,10 +50,11 @@ pub struct Configuration { } /// Reflects formatters in [`tracing_subscriber::fmt::format`] -#[derive(Debug, Copy, Clone, Eq, PartialEq, Deserialize, Serialize)] -#[serde(rename_all = "lowercase")] +#[derive(Debug, Copy, Clone, Eq, PartialEq, Deserialize, Serialize, Default)] +#[serde(rename_all = "snake_case")] pub enum Format { /// See [`tracing_subscriber::fmt::format::Full`] + #[default] Full, /// See [`tracing_subscriber::fmt::format::Compact`] Compact, @@ -53,45 +64,28 @@ pub enum Format { Json, } -impl Default for Format { - fn default() -> Self { - Self::Full - } -} +impl Complete for UserLayer { + type Output = Config; -impl Default for ConfigurationProxy { - fn default() -> Self { - Self { - level: Some(Level::default()), - format: Some(Format::default()), + fn complete(self) -> CompleteResult { + Ok(Config { + level: self.level.unwrap_or_default(), + format: self.format.unwrap_or_default(), #[cfg(feature = "tokio-console")] - tokio_console_addr: Some(DEFAULT_TOKIO_CONSOLE_ADDR), - } + tokio_console_addr: self + .tokio_console_addr + .unwrap_or_else(|| DEFAULT_TOKIO_CONSOLE_ADDR.clone()), + }) } } +impl FromEnvDefaultFallback for UserLayer {} + #[cfg(test)] pub mod tests { - use proptest::prelude::*; use super::*; - #[must_use = "strategies do nothing unless used"] - pub fn arb_proxy() -> impl proptest::strategy::Strategy { - let strat = ( - (prop::option::of(Just(Level::default()))), - (prop::option::of(Just(Format::default()))), - #[cfg(feature = "tokio-console")] - (prop::option::of(Just(DEFAULT_TOKIO_CONSOLE_ADDR))), - ); - proptest::strategy::Strategy::prop_map(strat, move |strat| ConfigurationProxy { - level: strat.0, - format: strat.1, - #[cfg(feature = "tokio-console")] - tokio_console_addr: strat.2, - }) - } - #[test] fn serialize_pretty_format_in_lowercase() { let value = Format::Pretty; diff --git a/config/src/parameters/mod.rs b/config/src/parameters/mod.rs new file mode 100644 index 00000000000..60863e9671d --- /dev/null +++ b/config/src/parameters/mod.rs @@ -0,0 +1,194 @@ +use std::{ + fmt::Debug, + fs::File, + io::{BufReader, Read}, + iter, + path::{Path, PathBuf}, +}; + +use eyre::{eyre, Context, Report, Result}; +use serde::{Deserialize, Serialize}; + +use crate::{Complete, CompleteError, CompleteResult, Emitter, FromEnv, FromEnvResult, ReadEnv}; + +pub mod chain_wide; +pub mod genesis; +pub mod iroha; +pub mod kura; +pub mod logger; +pub mod queue; +pub mod snapshot; +pub mod sumeragi; +pub mod telemetry; +pub mod torii; + +#[derive(Deserialize, Serialize, Debug, Default)] +#[serde(deny_unknown_fields)] +pub struct UserLayer { + #[serde(default)] + iroha: iroha::UserLayer, + #[serde(default)] + genesis: genesis::UserLayer, + #[serde(default)] + kura: kura::UserLayer, + #[serde(default)] + sumeragi: sumeragi::UserLayer, + #[serde(default)] + logger: logger::UserLayer, + #[serde(default)] + queue: queue::UserLayer, + #[serde(default)] + snapshot: snapshot::UserLayer, + #[serde(default)] + telemetry: telemetry::UserLayer, + #[serde(default)] + torii: torii::UserLayer, + #[serde(default)] + chain_wide: chain_wide::UserLayer, +} + +impl UserLayer { + pub fn from_toml(path: impl AsRef) -> Result { + let contents = { + let mut file = File::open(path.as_ref()).wrap_err_with(|| { + eyre!("cannot open file at location `{}`", path.as_ref().display()) + })?; + let mut contents = String::new(); + file.read_to_string(&mut contents)?; + contents + }; + let mut parsed: Self = toml::from_str(&contents).wrap_err("failed to parse toml")?; + parsed.normalise_paths( + path.as_ref() + .parent() + .expect("the config file path could not be empty or root"), + ); + Ok(parsed) + } + + fn normalise_paths(&mut self, relative_to: impl AsRef) { + let path = relative_to.as_ref(); + + macro_rules! patch { + ($value:expr) => { + $value.as_mut().map(|x| { + *x = path.join(&x); + }) + }; + } + + patch!(self.genesis.file); + patch!(self.snapshot.store_path); + patch!(self.kura.block_store_path); + patch!(self.telemetry.dev.file); + } + + pub fn merge(self, other: Self) -> Self { + todo!() + } +} + +impl Complete for UserLayer { + type Output = Config; + + fn complete(self) -> CompleteResult { + let mut emitter = Emitter::new(); + + macro_rules! complete_nested { + ($item:expr) => { + match $crate::Complete::complete($item) { + Ok(value) => Some(value), + Err(error) => { + emitter.emit_collection(error); + None + } + } + }; + } + + let iroha = complete_nested!(self.iroha); + let genesis = complete_nested!(self.genesis); + let kura = complete_nested!(self.kura); + let sumeragi = complete_nested!(self.sumeragi); + let logger = complete_nested!(self.logger); + let queue = complete_nested!(self.queue); + let snapshot = complete_nested!(self.snapshot); + let telemetry = complete_nested!(self.telemetry); + let torii = complete_nested!(self.torii); + let chain_wide = complete_nested!(self.chain_wide); + + emitter.finish()?; + + Ok(Config { + iroha: iroha.unwrap(), + genesis: genesis.unwrap(), + kura: kura.unwrap(), + sumeragi: sumeragi.unwrap(), + logger: logger.unwrap(), + queue: queue.unwrap(), + snapshot: snapshot.unwrap(), + telemetry: telemetry.unwrap(), + torii: torii.unwrap(), + chain_wide: chain_wide.unwrap(), + }) + } +} + +impl FromEnv for UserLayer { + fn from_env(env: &impl ReadEnv) -> FromEnvResult { + let mut emitter = Emitter::new(); + + fn from_env_nested( + env: &impl ReadEnv, + emitter: &mut Emitter, + ) -> Option { + match FromEnv::from_env(env) { + Ok(parsed) => Some(parsed), + Err(errors) => { + emitter.emit_collection(errors); + None + } + } + } + + let iroha = from_env_nested(env, &mut emitter); + let genesis = from_env_nested(env, &mut emitter); + let kura = from_env_nested(env, &mut emitter); + let sumeragi = from_env_nested(env, &mut emitter); + let logger = from_env_nested(env, &mut emitter); + let queue = from_env_nested(env, &mut emitter); + let snapshot = from_env_nested(env, &mut emitter); + let telemetry = from_env_nested(env, &mut emitter); + let torii = from_env_nested(env, &mut emitter); + let chain_wide = from_env_nested(env, &mut emitter); + + emitter.finish()?; + + Ok(Self { + iroha: iroha.unwrap(), + genesis: genesis.unwrap(), + kura: kura.unwrap(), + sumeragi: sumeragi.unwrap(), + logger: logger.unwrap(), + queue: queue.unwrap(), + snapshot: snapshot.unwrap(), + telemetry: telemetry.unwrap(), + torii: torii.unwrap(), + chain_wide: chain_wide.unwrap(), + }) + } +} + +#[derive(Debug)] +pub struct Config { + pub iroha: iroha::Config, + pub genesis: genesis::Config, + pub kura: kura::Config, + pub sumeragi: sumeragi::Config, + pub logger: logger::Config, + pub queue: queue::Config, + pub snapshot: snapshot::Config, + pub telemetry: telemetry::Config, + pub torii: torii::Config, + pub chain_wide: chain_wide::Config, +} diff --git a/config/src/parameters/queue.rs b/config/src/parameters/queue.rs new file mode 100644 index 00000000000..bd371a570d7 --- /dev/null +++ b/config/src/parameters/queue.rs @@ -0,0 +1,66 @@ +//! Module for `Queue`-related configuration and structs. +use std::{ + num::{NonZeroU32, NonZeroU64}, + time::Duration, +}; + +use nonzero_ext::nonzero; +use serde::{Deserialize, Serialize}; + +use crate::{ + Complete, CompleteError, CompleteResult, FromEnv, FromEnvDefaultFallback, FromEnvResult, + ReadEnv, UserDuration, +}; + +const DEFAULT_MAX_TRANSACTIONS_IN_QUEUE: NonZeroU32 = nonzero!(2_u32.pow(16)); +const DEFAULT_MAX_TRANSACTIONS_IN_QUEUE_PER_USER: NonZeroU32 = nonzero!(2_u32.pow(16)); +const DEFAULT_TRANSACTION_TIME_TO_LIVE: Duration = Duration::from_secs(24 * 60 * 60); // 24 hours +const DEFAULT_FUTURE_THRESHOLD: Duration = Duration::from_secs(1); + +#[derive(Deserialize, Serialize, Debug, Default)] +#[serde(deny_unknown_fields)] +pub struct UserLayer { + /// The upper limit of the number of transactions waiting in the queue. + pub max_transactions_in_queue: Option, + /// The upper limit of the number of transactions waiting in the queue for single user. + /// Use this option to apply throttling. + pub max_transactions_in_queue_per_user: Option, + /// The transaction will be dropped after this time if it is still in the queue. + pub transaction_time_to_live_ms: Option, + /// The threshold to determine if a transaction has been tampered to have a future timestamp. + pub future_threshold_ms: Option, +} + +/// `Queue` configuration. +#[derive(Copy, Clone, Deserialize, Serialize, Debug)] +pub struct Config { + pub max_transactions_in_queue: NonZeroU32, + pub max_transactions_in_queue_per_user: NonZeroU32, + pub transaction_time_to_live_ms: Duration, + pub future_threshold_ms: Duration, +} + +impl Complete for UserLayer { + type Output = Config; + + fn complete(self) -> CompleteResult { + Ok(Config { + max_transactions_in_queue: self + .max_transactions_in_queue + .unwrap_or_else(|| DEFAULT_MAX_TRANSACTIONS_IN_QUEUE), + max_transactions_in_queue_per_user: self + .max_transactions_in_queue_per_user + .unwrap_or_else(|| DEFAULT_MAX_TRANSACTIONS_IN_QUEUE), + transaction_time_to_live_ms: self + .transaction_time_to_live_ms + .map(UserDuration::get) + .unwrap_or(DEFAULT_TRANSACTION_TIME_TO_LIVE), + future_threshold_ms: self + .future_threshold_ms + .map(UserDuration::get) + .unwrap_or(DEFAULT_FUTURE_THRESHOLD), + }) + } +} + +impl FromEnvDefaultFallback for UserLayer {} diff --git a/config/src/parameters/snapshot.rs b/config/src/parameters/snapshot.rs new file mode 100644 index 00000000000..3544bb693d9 --- /dev/null +++ b/config/src/parameters/snapshot.rs @@ -0,0 +1,82 @@ +//! Module for `SnapshotMaker`-related configuration and structs. + +use std::{path::PathBuf, time::Duration}; + +use serde::{Deserialize, Serialize}; + +use crate::{ + Complete, CompleteError, CompleteResult, Emitter, FromEnv, FromEnvResult, ParseEnvResult, + ReadEnv, UserDuration, +}; + +const DEFAULT_SNAPSHOT_PATH: &str = "./storage"; +// Default frequency of making snapshots is 1 minute, need to be adjusted for larger world state view size +const DEFAULT_SNAPSHOT_CREATE_EVERY_MS: Duration = Duration::from_secs(60); +const DEFAULT_ENABLED: bool = true; + +#[derive(Debug, Clone, Deserialize, Serialize, Default)] +#[serde(deny_unknown_fields)] +pub struct UserLayer { + /// The period of time to wait between attempts to create new snapshot. + pub create_every_ms: Option, + /// Path to the directory where snapshots should be stored + pub store_path: Option, + /// Flag to enable or disable snapshot creation + pub creation_enabled: Option, +} + +#[derive(Debug)] +pub struct Config { + pub create_every_ms: Duration, + pub store_path: PathBuf, + pub creation_enabled: bool, +} + +impl Complete for UserLayer { + type Output = Config; + + fn complete(self) -> CompleteResult { + Ok(Config { + creation_enabled: self.creation_enabled.unwrap_or(DEFAULT_ENABLED), + create_every_ms: self + .create_every_ms + .map(UserDuration::get) + .unwrap_or(DEFAULT_SNAPSHOT_CREATE_EVERY_MS), + store_path: self + .store_path + .unwrap_or_else(|| PathBuf::from(DEFAULT_SNAPSHOT_PATH)), + }) + } +} + +impl FromEnv for UserLayer { + fn from_env(env: &impl ReadEnv) -> FromEnvResult + where + Self: Sized, + { + let mut emitter = Emitter::new(); + + let store_path = ParseEnvResult::parse_simple( + &mut emitter, + env, + "SNAPSHOT_STORE", + "snapshot.store_path", + ) + .into(); + let creation_enabled = ParseEnvResult::parse_simple( + &mut emitter, + env, + "SNAPSHOT_CREATION_ENABLED", + "snapshot.creation_enabled", + ) + .into(); + + emitter.finish()?; + + Ok(Self { + store_path, + creation_enabled, + ..Self::default() + }) + } +} diff --git a/config/src/parameters/sumeragi.rs b/config/src/parameters/sumeragi.rs new file mode 100644 index 00000000000..f5d4c39a05b --- /dev/null +++ b/config/src/parameters/sumeragi.rs @@ -0,0 +1,90 @@ +//! `Sumeragi` configuration. Contains both block commit and Gossip-related configuration. +use std::{fmt::Debug, fs::File, io::BufReader, num::NonZeroU32, path::Path, time::Duration}; + +use iroha_data_model::prelude::*; +use iroha_primitives::unique_vec::UniqueVec; +use serde::{Deserialize, Serialize}; + +use self::default::*; +use crate::{Complete, CompleteError, CompleteResult, FromEnvDefaultFallback, UserDuration}; + +/// Module with a set of default values. +pub mod default { + use std::{ + num::{NonZeroU32, NonZeroU64}, + time::Duration, + }; + + use nonzero_ext::nonzero; + + pub const DEFAULT_TRANSACTION_GOSSIP_PERIOD: Duration = Duration::from_secs(1); + pub const DEFAULT_MAX_TRANSACTIONS_IN_BLOCK: u32 = 2_u32.pow(9); + + pub const DEFAULT_BLOCK_GOSSIP_PERIOD: Duration = Duration::from_secs(10); + + pub const DEFAULT_MAX_TRANSACTIONS_PER_GOSSIP: NonZeroU32 = nonzero!(500u32); + pub const DEFAULT_MAX_BLOCKS_PER_GOSSIP: NonZeroU32 = nonzero!(4u32); + + // /// Default estimation of consensus duration. + // #[allow(clippy::integer_division)] + // pub const DEFAULT_CONSENSUS_ESTIMATION_MS: u64 = + // DEFAULT_BLOCK_TIME_MS + (DEFAULT_COMMIT_TIME_LIMIT_MS / 2); +} + +#[derive(Deserialize, Serialize, Debug, Default)] +#[serde(deny_unknown_fields)] +pub struct UserLayer { + pub block_gossip_period: Option, + pub max_blocks_per_gossip: Option, + pub max_transactions_per_gossip: Option, + pub transaction_gossip_period: Option, + pub trusted_peers: Option, +} + +impl Complete for UserLayer { + type Output = Config; + + fn complete(self) -> CompleteResult { + Ok(Config { + block_gossip_period: self + .block_gossip_period + .map(UserDuration::get) + .unwrap_or(DEFAULT_BLOCK_GOSSIP_PERIOD), + max_blocks_per_gossip: self + .max_blocks_per_gossip + .unwrap_or_else(|| DEFAULT_MAX_BLOCKS_PER_GOSSIP.into()), + max_transactions_per_gossip: self + .max_transactions_per_gossip + .unwrap_or_else(|| DEFAULT_MAX_TRANSACTIONS_PER_GOSSIP.into()), + transaction_gossip_period: self + .transaction_gossip_period + .map(UserDuration::get) + .unwrap_or(DEFAULT_TRANSACTION_GOSSIP_PERIOD), + trusted_peers: self.trusted_peers.unwrap_or_default(), + }) + } +} + +#[derive(Debug)] +pub struct Config { + pub block_gossip_period: Duration, + pub max_blocks_per_gossip: NonZeroU32, + pub max_transactions_per_gossip: NonZeroU32, + pub transaction_gossip_period: Duration, + pub trusted_peers: TrustedPeers, +} + +/// Part of the [`Configuration`]. It is separated from the main structure in order to be able +/// to load it from a separate file (see [`TrustedPeers::from_path`]). +#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize, Serialize)] +#[serde(rename_all = "UPPERCASE")] +#[serde(transparent)] +#[repr(transparent)] +pub struct TrustedPeers { + /// Optional list of predefined trusted peers. Must contain unique + /// entries. Custom deserializer raises error if duplicates found. + #[serde(deserialize_with = "UniqueVec::display_deserialize_failing_on_duplicates")] + pub peers: UniqueVec, +} + +impl FromEnvDefaultFallback for UserLayer {} diff --git a/config/src/parameters/telemetry.rs b/config/src/parameters/telemetry.rs new file mode 100644 index 00000000000..cfc95118963 --- /dev/null +++ b/config/src/parameters/telemetry.rs @@ -0,0 +1,119 @@ +//! Module for telemetry-related configuration and structs. +use std::{ + num::{NonZeroU64, NonZeroU8}, + path::PathBuf, + time::Duration, +}; + +use eyre::eyre; +use serde::{Deserialize, Serialize}; +use url::Url; + +use crate::{ + parameters::telemetry::retry_period::{ + DEFAULT_MAX_RETRY_DELAY_EXPONENT, DEFAULT_MIN_RETRY_PERIOD, + }, + Complete, CompleteError, CompleteResult, FromEnv, FromEnvDefaultFallback, FromEnvResult, + ReadEnv, UserDuration, +}; + +#[derive(Clone, Deserialize, Serialize, Debug, Default)] +#[serde(deny_unknown_fields)] +pub struct UserLayer { + /// The node's name to be seen on the telemetry + pub name: Option, + /// The url of the telemetry, e.g., ws://127.0.0.1:8001/submit + pub url: Option, + /// The minimum period of time in seconds to wait before reconnecting + pub min_retry_period: Option, + /// The maximum exponent of 2 that is used for increasing delay between reconnections + pub max_retry_delay_exponent: Option, + /// Dev telemetry configuration + #[serde(default)] + pub dev: UserDevConfig, +} + +#[derive(Clone, Deserialize, Serialize, Debug, Default)] +pub struct UserDevConfig { + /// The filepath that to write dev-telemetry to + pub file: Option, +} + +#[derive(Debug)] +pub struct Config { + regular: Option, + dev: Option, +} + +/// Complete configuration needed to start regular telemetry. +#[derive(Debug)] +pub struct RegularTelemetryConfig { + #[allow(missing_docs)] + pub name: String, + #[allow(missing_docs)] + pub url: Url, + #[allow(missing_docs)] + pub min_retry_period: Duration, + #[allow(missing_docs)] + pub max_retry_delay_exponent: NonZeroU8, +} + +/// Complete configuration needed to start dev telemetry. +#[derive(Debug)] +pub struct DevTelemetryConfig { + #[allow(missing_docs)] + pub file: PathBuf, +} + +impl Complete for UserLayer { + type Output = Config; + + fn complete(self) -> CompleteResult { + let Self { + name, + url, + max_retry_delay_exponent, + min_retry_period, + dev: UserDevConfig { file }, + } = self; + + let regular = match (name, url) { + (Some(name), Some(url)) => Some(RegularTelemetryConfig { + name: name.clone(), + url: url.clone(), + max_retry_delay_exponent: max_retry_delay_exponent + .unwrap_or(DEFAULT_MAX_RETRY_DELAY_EXPONENT), + min_retry_period: min_retry_period + .map(UserDuration::get) + .unwrap_or(DEFAULT_MIN_RETRY_PERIOD), + }), + (None, None) => None, + // TODO improve error detail + _ => Err(eyre!( + "telemetry.name and telemetry.file should be set together" + )) + .map_err(CompleteError::Custom)?, + }; + + let dev = file + .as_ref() + .map(|file| DevTelemetryConfig { file: file.clone() }); + + Ok(Config { regular, dev }) + } +} + +impl FromEnvDefaultFallback for UserLayer {} + +/// `RetryPeriod` configuration +pub mod retry_period { + use std::{num::NonZeroU8, time::Duration}; + + use nonzero_ext::nonzero; + + /// Default minimal retry period + // FIXME: it was `1`. Was it secs of millisecs? + pub const DEFAULT_MIN_RETRY_PERIOD: Duration = Duration::from_secs(1); + /// Default maximum exponent for the retry delay + pub const DEFAULT_MAX_RETRY_DELAY_EXPONENT: NonZeroU8 = nonzero!(4u8); +} diff --git a/config/src/parameters/torii.rs b/config/src/parameters/torii.rs new file mode 100644 index 00000000000..aa8003a0fbe --- /dev/null +++ b/config/src/parameters/torii.rs @@ -0,0 +1,67 @@ +//! `Torii` configuration as well as the default values for the URLs used for the main endpoints: `p2p`, `telemetry`, but not `api`. + +use std::time::Duration; + +use iroha_primitives::addr::{socket_addr, SocketAddr}; +use serde::{Deserialize, Serialize}; + +use crate::{ + ByteSize, Complete, CompleteError, CompleteResult, Emitter, FromEnv, FromEnvResult, + ParseEnvResult, ReadEnv, UserDuration, +}; + +const DEFAULT_MAX_CONTENT_LENGTH: u64 = 2_u64.pow(20) * 16; +const DEFAULT_QUERY_IDLE_TIME: Duration = Duration::from_secs(30); + +#[derive(Debug, Clone, Deserialize, Serialize, Default)] +#[serde(deny_unknown_fields)] +pub struct UserLayer { + pub address: Option, + pub max_content_len: Option, + pub query_idle_time: Option, +} + +#[derive(Debug)] +pub struct Config { + pub address: SocketAddr, + pub max_content_len: ByteSize, + pub query_idle_time: Duration, +} + +impl Complete for UserLayer { + type Output = Config; + + fn complete(self) -> CompleteResult { + Ok(Config { + address: self.address.ok_or_else(|| CompleteError::MissingField { + path: "address".to_string(), + })?, + max_content_len: self + .max_content_len + .unwrap_or_else(|| ByteSize(DEFAULT_MAX_CONTENT_LENGTH)), + query_idle_time: self + .query_idle_time + .map(UserDuration::get) + .unwrap_or(DEFAULT_QUERY_IDLE_TIME), + }) + } +} + +impl FromEnv for UserLayer { + fn from_env(env: &impl ReadEnv) -> FromEnvResult + where + Self: Sized, + { + let mut emitter = Emitter::new(); + + let address = + ParseEnvResult::parse_simple(&mut emitter, env, "API_ADDRESS", "torii.address").into(); + + emitter.finish()?; + + Ok(Self { + address, + ..Self::default() + }) + } +} diff --git a/config/src/path.rs b/config/src/path.rs deleted file mode 100644 index 23f1bd80b57..00000000000 --- a/config/src/path.rs +++ /dev/null @@ -1,152 +0,0 @@ -//! Module with configuration path related structures. - -extern crate alloc; - -use alloc::borrow::Cow; -use std::path::PathBuf; - -use InnerPath::*; - -/// Allowed configuration file extension that user can provide. -pub const ALLOWED_CONFIG_EXTENSIONS: [&str; 2] = ["json", "json5"]; - -/// Error type for [`Path`]. -#[derive(Debug, Clone, thiserror::Error, displaydoc::Display)] -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; - -/// Inner helper struct. -/// -/// With this struct, we force to use [`Path`]'s constructors instead of constructing it directly. -#[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, 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(path) => { - write!( - f, - "{}.{{{}}}", - path.display(), - ALLOWED_CONFIG_EXTENSIONS.join(",") - ) - } - UserProvided(path) => write!(f, "{}", path.display()), - } - } -} - -impl 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. - /// - /// The path should not have an extension. - /// - /// # Panics - /// If the path has an extension. - pub fn default(path: impl AsRef) -> Self { - let path = path.as_ref().to_path_buf(); - assert!( - path.extension().is_none(), - "Default config path is not supposed to have an extension. It is a bug." - ); - Self(Default(path)) - } - - /// Construct new [`Path`] from user-provided `path` which will fail to [`Self::try_resolve()`] - /// if file is not found. - /// - /// # Errors - /// 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(Error::MissingExtension)? - .to_string_lossy(); - if !ALLOWED_CONFIG_EXTENSIONS.contains(&extension.as_ref()) { - return Err(Error::InvalidExtension(extension.into_owned())); - } - - 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. - /// - /// # Errors - /// If user-provided path is not found - pub fn try_resolve(&self) -> Result>> { - match &self.0 { - 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())) - } - } - } - } -} - -#[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/queue.rs b/config/src/queue.rs deleted file mode 100644 index 5803e90ed7c..00000000000 --- a/config/src/queue.rs +++ /dev/null @@ -1,55 +0,0 @@ -//! Module for `Queue`-related configuration and structs. -use iroha_config_base::derive::Proxy; -use serde::{Deserialize, Serialize}; - -const DEFAULT_MAX_TRANSACTIONS_IN_QUEUE: u32 = 2_u32.pow(16); -const DEFAULT_MAX_TRANSACTIONS_IN_QUEUE_PER_USER: u32 = 2_u32.pow(16); -const DEFAULT_TRANSACTION_TIME_TO_LIVE_MS: u64 = 24 * 60 * 60 * 1000; // 24 hours -const DEFAULT_FUTURE_THRESHOLD_MS: u64 = 1000; - -/// `Queue` configuration. -#[derive(Copy, Clone, Deserialize, Serialize, Debug, Proxy, PartialEq, Eq)] -#[serde(rename_all = "UPPERCASE")] -#[config(env_prefix = "QUEUE_")] -pub struct Configuration { - /// The upper limit of the number of transactions waiting in the queue. - pub max_transactions_in_queue: u32, - /// The upper limit of the number of transactions waiting in the queue for single user. - /// Use this option to apply throttling. - pub max_transactions_in_queue_per_user: u32, - /// The transaction will be dropped after this time if it is still in the queue. - pub transaction_time_to_live_ms: u64, - /// The threshold to determine if a transaction has been tampered to have a future timestamp. - pub future_threshold_ms: u64, -} - -impl Default for ConfigurationProxy { - fn default() -> Self { - Self { - max_transactions_in_queue: Some(DEFAULT_MAX_TRANSACTIONS_IN_QUEUE), - max_transactions_in_queue_per_user: Some(DEFAULT_MAX_TRANSACTIONS_IN_QUEUE_PER_USER), - transaction_time_to_live_ms: Some(DEFAULT_TRANSACTION_TIME_TO_LIVE_MS), - future_threshold_ms: Some(DEFAULT_FUTURE_THRESHOLD_MS), - } - } -} - -#[cfg(test)] -pub mod tests { - use proptest::prelude::*; - - use super::*; - - prop_compose! { - pub fn arb_proxy() - ( - max_transactions_in_queue in prop::option::of(Just(DEFAULT_MAX_TRANSACTIONS_IN_QUEUE)), - max_transactions_in_queue_per_user in prop::option::of(Just(DEFAULT_MAX_TRANSACTIONS_IN_QUEUE_PER_USER)), - transaction_time_to_live_ms in prop::option::of(Just(DEFAULT_TRANSACTION_TIME_TO_LIVE_MS)), - future_threshold_ms in prop::option::of(Just(DEFAULT_FUTURE_THRESHOLD_MS)), - ) - -> ConfigurationProxy { - ConfigurationProxy { max_transactions_in_queue, max_transactions_in_queue_per_user, transaction_time_to_live_ms, future_threshold_ms } - } - } -} diff --git a/config/src/snapshot.rs b/config/src/snapshot.rs deleted file mode 100644 index e828e5635d0..00000000000 --- a/config/src/snapshot.rs +++ /dev/null @@ -1,54 +0,0 @@ -//! Module for `SnapshotMaker`-related configuration and structs. - -use std::path::PathBuf; - -use iroha_config_base::derive::Proxy; -use serde::{Deserialize, Serialize}; - -const DEFAULT_SNAPSHOT_PATH: &str = "./storage"; -// Default frequency of making snapshots is 1 minute, need to be adjusted for larger world state view size -const DEFAULT_SNAPSHOT_CREATE_EVERY_MS: u64 = 1000 * 60; -const DEFAULT_ENABLED: bool = true; - -/// Configuration for `SnapshotMaker`. -#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Proxy)] -#[serde(rename_all = "UPPERCASE")] -#[config(env_prefix = "SNAPSHOT_")] -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 - #[config(serde_as_str)] - pub dir_path: PathBuf, - /// Flag to enable or disable snapshot creation - pub creation_enabled: bool, -} - -impl Default for ConfigurationProxy { - fn default() -> Self { - Self { - create_every_ms: Some(DEFAULT_SNAPSHOT_CREATE_EVERY_MS), - dir_path: Some(DEFAULT_SNAPSHOT_PATH.into()), - creation_enabled: Some(DEFAULT_ENABLED), - } - } -} - -#[cfg(test)] -pub mod tests { - use proptest::prelude::*; - - use super::*; - - prop_compose! { - 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.into())), - creation_enabled in prop::option::of(Just(DEFAULT_ENABLED)), - ) - -> ConfigurationProxy { - ConfigurationProxy { create_every_ms, dir_path, creation_enabled } - } - } -} diff --git a/config/src/sumeragi.rs b/config/src/sumeragi.rs deleted file mode 100644 index a4eb7760069..00000000000 --- a/config/src/sumeragi.rs +++ /dev/null @@ -1,186 +0,0 @@ -//! `Sumeragi` configuration. Contains both block commit and Gossip-related configuration. -use std::{fmt::Debug, fs::File, io::BufReader, path::Path}; - -use eyre::{Result, WrapErr}; -use iroha_config_base::derive::{view, Proxy}; -use iroha_crypto::prelude::*; -use iroha_data_model::prelude::*; -use iroha_primitives::{unique_vec, unique_vec::UniqueVec}; -use serde::{Deserialize, Serialize}; - -use self::default::*; - -/// Module with a set of default values. -pub mod default { - /// Default number of miliseconds the peer waits for transactions before creating a block. - pub const DEFAULT_BLOCK_TIME_MS: u64 = 2000; - /// Default amount of time allocated for voting on a block before a peer can ask for a view change. - pub const DEFAULT_COMMIT_TIME_LIMIT_MS: u64 = 4000; - /// Unused const. Should be removed in the future. - pub const DEFAULT_ACTOR_CHANNEL_CAPACITY: u32 = 100; - /// Default duration in ms between every transaction gossip. - pub const DEFAULT_GOSSIP_PERIOD_MS: u64 = 1000; - /// Default maximum number of transactions sent in single gossip message. - pub const DEFAULT_GOSSIP_BATCH_SIZE: u32 = 500; - /// Default maximum number of transactions in block. - pub const DEFAULT_MAX_TRANSACTIONS_IN_BLOCK: u32 = 2_u32.pow(9); - - /// Default estimation of consensus duration. - #[allow(clippy::integer_division)] - pub const DEFAULT_CONSENSUS_ESTIMATION_MS: u64 = - DEFAULT_BLOCK_TIME_MS + (DEFAULT_COMMIT_TIME_LIMIT_MS / 2); -} - -// Generate `ConfigurationView` without keys -view! { - /// `Sumeragi` configuration. - /// [`struct@Configuration`] provides an ability to define parameters such as `BLOCK_TIME_MS` - /// and a list of `TRUSTED_PEERS`. - #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Proxy)] - #[serde(rename_all = "UPPERCASE")] - #[config(env_prefix = "SUMERAGI_")] - pub struct Configuration { - /// The key pair consisting of a private and a public key. - //TODO: consider putting a `#[serde(skip)]` on the proxy struct here - #[view(ignore)] - pub key_pair: KeyPair, - /// Current Peer Identification. - pub peer_id: PeerId, - /// The period of time a peer waits for the `CreatedBlock` message after getting a `TransactionReceipt` - pub block_time_ms: u64, - /// Optional list of predefined trusted peers. - pub trusted_peers: TrustedPeers, - /// The period of time a peer waits for `CommitMessage` from the proxy tail. - pub commit_time_limit_ms: u64, - /// The upper limit of the number of transactions per block. - pub max_transactions_in_block: u32, - /// Buffer capacity of actor's MPSC channel - pub actor_channel_capacity: u32, - /// max number of transactions in tx gossip batch message. While configuring this, pay attention to `p2p` max message size. - pub gossip_batch_size: u32, - /// Period in milliseconds for pending transaction gossiping between peers. - pub gossip_period_ms: u64, - #[cfg(debug_assertions)] - /// Only used in testing. Causes the genesis peer to withhold blocks when it - /// is the proxy tail. - pub debug_force_soft_fork: bool, - } -} - -impl Default for ConfigurationProxy { - fn default() -> Self { - Self { - key_pair: None, - peer_id: None, - trusted_peers: None, - block_time_ms: Some(DEFAULT_BLOCK_TIME_MS), - commit_time_limit_ms: Some(DEFAULT_COMMIT_TIME_LIMIT_MS), - actor_channel_capacity: Some(DEFAULT_ACTOR_CHANNEL_CAPACITY), - gossip_batch_size: Some(DEFAULT_GOSSIP_BATCH_SIZE), - gossip_period_ms: Some(DEFAULT_GOSSIP_PERIOD_MS), - max_transactions_in_block: Some(DEFAULT_MAX_TRANSACTIONS_IN_BLOCK), - #[cfg(debug_assertions)] - debug_force_soft_fork: Some(false), - } - } -} -impl ConfigurationProxy { - /// To be used for proxy finalisation. Should only be - /// used if no peers are present. - /// - /// # Panics - /// The [`peer_id`] field of [`Self`] - /// has not been initialized prior to calling this method. - pub fn insert_self_as_trusted_peers(&mut self) { - let peer_id = self - .peer_id - .as_ref() - .expect("Insertion of `self` as `trusted_peers` implies that `peer_id` field should be initialized"); - self.trusted_peers = if let Some(mut trusted_peers) = self.trusted_peers.take() { - trusted_peers.peers.push(peer_id.clone()); - Some(trusted_peers) - } else { - Some(TrustedPeers { - peers: unique_vec![peer_id.clone()], - }) - }; - } -} - -impl Configuration { - /// Time estimation from receiving a transaction to storing it in - /// a block on all peers for the "sunny day" scenario. - #[inline] - #[must_use] - pub const fn pipeline_time_ms(&self) -> u64 { - self.block_time_ms + self.commit_time_limit_ms - } -} - -/// Part of the [`Configuration`]. It is separated from the main structure in order to be able -/// to load it from a separate file (see [`TrustedPeers::from_path`]). -#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize, Serialize)] -#[serde(rename_all = "UPPERCASE")] -#[serde(transparent)] -#[repr(transparent)] -pub struct TrustedPeers { - /// Optional list of predefined trusted peers. Must contain unique - /// entries. Custom deserializer raises error if duplicates found. - #[serde(deserialize_with = "UniqueVec::display_deserialize_failing_on_duplicates")] - pub peers: UniqueVec, -} - -impl TrustedPeers { - /// Load trusted peers variables from JSON. - /// - /// # Errors - /// - File not found - /// - File is not Valid JSON. - /// - File is valid JSON, but configuration options don't match. - pub fn from_path + Debug>(path: P) -> Result { - let file = File::open(&path) - .wrap_err_with(|| format!("Failed to open trusted peers file {:?}", &path))?; - let reader = BufReader::new(file); - serde_json::from_reader(reader) - .wrap_err("Failed to deserialize json from reader") - .map_err(Into::into) - } -} - -#[cfg(test)] -pub mod tests { - use proptest::prelude::*; - - use super::*; - - prop_compose! { - #[allow(unused_variables)] - pub fn arb_proxy() - (key_pair in Just(None), - peer_id in Just(None), - block_time_ms in prop::option::of(Just(DEFAULT_BLOCK_TIME_MS)), - trusted_peers in Just(None), - commit_time_limit_ms in prop::option::of(Just(DEFAULT_COMMIT_TIME_LIMIT_MS)), - actor_channel_capacity in prop::option::of(Just(DEFAULT_ACTOR_CHANNEL_CAPACITY)), - gossip_batch_size in prop::option::of(Just(DEFAULT_GOSSIP_BATCH_SIZE)), - gossip_period_ms in prop::option::of(Just(DEFAULT_GOSSIP_PERIOD_MS)), - max_transactions_in_block in prop::option::of(Just(DEFAULT_MAX_TRANSACTIONS_IN_BLOCK)), - debug_force_soft_fork in prop::option::of(Just(false)), - ) - -> ConfigurationProxy { - ConfigurationProxy { - key_pair, - peer_id, - block_time_ms, - trusted_peers, - commit_time_limit_ms, - max_transactions_in_block, - actor_channel_capacity, - gossip_batch_size, - gossip_period_ms, - #[cfg(debug_assertions)] - debug_force_soft_fork - } - } - } -} diff --git a/config/src/telemetry.rs b/config/src/telemetry.rs deleted file mode 100644 index b7ce10f9ee4..00000000000 --- a/config/src/telemetry.rs +++ /dev/null @@ -1,117 +0,0 @@ -//! Module for telemetry-related configuration and structs. -use std::path::PathBuf; - -use iroha_config_base::derive::Proxy; -use serde::{Deserialize, Serialize}; -use url::Url; - -/// Configuration parameters container -#[derive(Clone, Deserialize, Serialize, Debug, Proxy, PartialEq, Eq)] -#[serde(rename_all = "UPPERCASE")] -#[config(env_prefix = "TELEMETRY_")] -pub struct Configuration { - /// The node's name to be seen on the telemetry - #[config(serde_as_str)] - pub name: Option, - /// The url of the telemetry, e.g., ws://127.0.0.1:8001/submit - #[config(serde_as_str)] - pub url: Option, - /// The minimum period of time in seconds to wait before reconnecting - pub min_retry_period: u64, - /// The maximum exponent of 2 that is used for increasing delay between reconnections - pub max_retry_delay_exponent: u8, - /// The filepath that to write dev-telemetry to - #[config(serde_as_str)] - pub file: Option, -} - -/// Complete configuration needed to start regular telemetry. -pub struct RegularTelemetryConfig { - #[allow(missing_docs)] - pub name: String, - #[allow(missing_docs)] - pub url: Url, - #[allow(missing_docs)] - pub min_retry_period: u64, - #[allow(missing_docs)] - pub max_retry_delay_exponent: u8, -} - -/// Complete configuration needed to start dev telemetry. -pub struct DevTelemetryConfig { - #[allow(missing_docs)] - pub file: PathBuf, -} - -impl Configuration { - /// Parses user-provided configuration into stronger typed structures - /// - /// Should be refactored with [#3500](https://github.com/hyperledger/iroha/issues/3500) - pub fn parse(&self) -> (Option, Option) { - let Self { - ref name, - ref url, - max_retry_delay_exponent, - min_retry_period, - ref file, - } = *self; - - let regular = if let (Some(name), Some(url)) = (name, url) { - Some(RegularTelemetryConfig { - name: name.clone(), - url: url.clone(), - max_retry_delay_exponent, - min_retry_period, - }) - } else { - None - }; - - let dev = file - .as_ref() - .map(|file| DevTelemetryConfig { file: file.clone() }); - - (regular, dev) - } -} - -impl Default for ConfigurationProxy { - fn default() -> Self { - Self { - name: Some(None), - url: Some(None), - min_retry_period: Some(retry_period::DEFAULT_MIN_RETRY_PERIOD), - max_retry_delay_exponent: Some(retry_period::DEFAULT_MAX_RETRY_DELAY_EXPONENT), - file: Some(None), - } - } -} - -/// `RetryPeriod` configuration -pub mod retry_period { - /// Default minimal retry period - pub const DEFAULT_MIN_RETRY_PERIOD: u64 = 1; - /// Default maximum exponent for the retry delay - pub const DEFAULT_MAX_RETRY_DELAY_EXPONENT: u8 = 4; -} - -#[cfg(test)] -pub mod tests { - use proptest::prelude::*; - - use super::*; - - prop_compose! { - pub fn arb_proxy() - ( - name in prop::option::of(Just(None)), - url in prop::option::of(Just(None)), - min_retry_period in prop::option::of(Just(retry_period::DEFAULT_MIN_RETRY_PERIOD)), - max_retry_delay_exponent in prop::option::of(Just(retry_period::DEFAULT_MAX_RETRY_DELAY_EXPONENT)), - file in prop::option::of(Just(None)), - ) - -> ConfigurationProxy { - ConfigurationProxy { name, url, min_retry_period, max_retry_delay_exponent, file } - } - } -} diff --git a/config/src/torii.rs b/config/src/torii.rs deleted file mode 100644 index d77457f0ddb..00000000000 --- a/config/src/torii.rs +++ /dev/null @@ -1,99 +0,0 @@ -//! `Torii` configuration as well as the default values for the URLs used for the main endpoints: `p2p`, `telemetry`, but not `api`. - -use iroha_config_base::derive::Proxy; -use iroha_primitives::addr::{socket_addr, SocketAddr}; -use serde::{Deserialize, Serialize}; - -/// Default socket for p2p communication -pub const DEFAULT_TORII_P2P_ADDR: SocketAddr = socket_addr!(127.0.0.1:1337); -/// Default maximum size of single transaction -pub const DEFAULT_TORII_MAX_TRANSACTION_SIZE: u32 = 2_u32.pow(15); -/// Default upper bound on `content-length` specified in the HTTP request header -pub const DEFAULT_TORII_MAX_CONTENT_LENGTH: u32 = 2_u32.pow(12) * 4000; - -/// Structure that defines the configuration parameters of `Torii` which is the routing module. -/// For example the `p2p_addr`, which is used for consensus and block-synchronisation purposes, -/// as well as `max_transaction_size`. -#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Proxy)] -#[serde(rename_all = "UPPERCASE")] -#[config(env_prefix = "TORII_")] -pub struct Configuration { - /// Torii address for p2p communication for consensus and block synchronization purposes. - #[config(serde_as_str)] - pub p2p_addr: SocketAddr, - /// Torii address for client API. - #[config(serde_as_str)] - pub api_url: SocketAddr, - /// Maximum number of bytes in raw transaction. Used to prevent from DOS attacks. - pub max_transaction_size: u32, - /// Maximum number of bytes in raw message. Used to prevent from DOS attacks. - pub max_content_len: u32, -} - -impl Default for ConfigurationProxy { - fn default() -> Self { - Self { - p2p_addr: None, - api_url: None, - max_transaction_size: Some(DEFAULT_TORII_MAX_TRANSACTION_SIZE), - max_content_len: Some(DEFAULT_TORII_MAX_CONTENT_LENGTH), - } - } -} - -pub mod uri { - //! URI that `Torii` uses to route incoming requests. - - /// Default socket for listening on external requests - pub const DEFAULT_API_ADDR: iroha_primitives::addr::SocketAddr = - iroha_primitives::addr::socket_addr!(127.0.0.1:8080); - /// Query URI is used to handle incoming Query requests. - pub const QUERY: &str = "query"; - /// Transaction URI is used to handle incoming ISI requests. - pub const TRANSACTION: &str = "transaction"; - /// Block URI is used to handle incoming Block requests. - pub const CONSENSUS: &str = "consensus"; - /// Health URI is used to handle incoming Healthcheck requests. - pub const HEALTH: &str = "health"; - /// The URI used for block synchronization. - pub const BLOCK_SYNC: &str = "block/sync"; - /// The web socket uri used to subscribe to block and transactions statuses. - pub const SUBSCRIPTION: &str = "events"; - /// The web socket uri used to subscribe to blocks stream. - pub const BLOCKS_STREAM: &str = "block/stream"; - /// Get pending transactions. - pub const MATCHING_PENDING_TRANSACTIONS: &str = "matching_pending_transactions"; - /// The URI for local config changing inspecting - pub const CONFIGURATION: &str = "configuration"; - /// URI to report status for administration - pub const STATUS: &str = "status"; - /// Metrics URI is used to export metrics according to [Prometheus - /// Guidance](https://prometheus.io/docs/instrumenting/writing_exporters/). - pub const METRICS: &str = "metrics"; - /// URI for retrieving the schema with which Iroha was built. - pub const SCHEMA: &str = "schema"; - /// URI for getting the API version currently used - pub const API_VERSION: &str = "api_version"; - /// URI for getting cpu profile - pub const PROFILE: &str = "debug/pprof/profile"; -} - -#[cfg(test)] -pub mod tests { - use proptest::prelude::*; - - use super::*; - - prop_compose! { - pub fn arb_proxy() - ( - p2p_addr in prop::option::of(Just(DEFAULT_TORII_P2P_ADDR)), - api_url in prop::option::of(Just(uri::DEFAULT_API_ADDR)), - max_transaction_size in prop::option::of(Just(DEFAULT_TORII_MAX_TRANSACTION_SIZE)), - max_content_len in prop::option::of(Just(DEFAULT_TORII_MAX_CONTENT_LENGTH)), - ) - -> ConfigurationProxy { - ConfigurationProxy { p2p_addr, api_url, max_transaction_size, max_content_len } - } - } -} diff --git a/config/src/wasm.rs b/config/src/wasm.rs index cd55fd989af..8b137891791 100644 --- a/config/src/wasm.rs +++ b/config/src/wasm.rs @@ -1,35 +1 @@ -//! Module for wasm-related configuration and structs. -use iroha_config_base::derive::Proxy; -use serde::{Deserialize, Serialize}; -use self::default::*; - -/// Module with a set of default values. -pub mod default { - /// Default amount of fuel provided for execution - pub const DEFAULT_FUEL_LIMIT: u64 = 55_000_000; - /// Default amount of memory given for smart contract - pub const DEFAULT_MAX_MEMORY: u32 = 500 * 2_u32.pow(20); // 500 MiB -} - -/// `WebAssembly Runtime` configuration. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, Proxy)] -#[config(env_prefix = "WASM_")] -#[serde(rename_all = "UPPERCASE")] -pub struct Configuration { - /// The fuel limit determines the maximum number of instructions that can be executed within a smart contract. - /// Every WASM instruction costs approximately 1 unit of fuel. See - /// [`wasmtime` reference](https://docs.rs/wasmtime/0.29.0/wasmtime/struct.Store.html#method.add_fuel) - pub fuel_limit: u64, - /// Maximum amount of linear memory a given smart contract can allocate. - pub max_memory: u32, -} - -impl Default for ConfigurationProxy { - fn default() -> Self { - Self { - fuel_limit: Some(DEFAULT_FUEL_LIMIT), - max_memory: Some(DEFAULT_MAX_MEMORY), - } - } -} diff --git a/config/src/wsv.rs b/config/src/wsv.rs deleted file mode 100644 index dcb23b23d85..00000000000 --- a/config/src/wsv.rs +++ /dev/null @@ -1,86 +0,0 @@ -//! Module for `WorldStateView`-related configuration and structs. -use default::*; -use iroha_config_base::derive::Proxy; -use iroha_data_model::{prelude::*, transaction::TransactionLimits}; -use serde::{Deserialize, Serialize}; - -use crate::wasm; - -/// Module with a set of default values. -pub mod default { - use super::*; - - /// Default limits for metadata - pub const DEFAULT_METADATA_LIMITS: MetadataLimits = - MetadataLimits::new(2_u32.pow(20), 2_u32.pow(12)); - /// Default limits for ident length - pub const DEFAULT_IDENT_LENGTH_LIMITS: LengthLimits = LengthLimits::new(1, 2_u32.pow(7)); - /// Default maximum number of instructions and expressions per transaction - pub const DEFAULT_MAX_INSTRUCTION_NUMBER: u64 = 2_u64.pow(12); - /// Default maximum number of instructions and expressions per transaction - pub const DEFAULT_MAX_WASM_SIZE_BYTES: u64 = 2_u64.pow(22); // 4 MiB - - /// Default transaction limits - pub const DEFAULT_TRANSACTION_LIMITS: TransactionLimits = - TransactionLimits::new(DEFAULT_MAX_INSTRUCTION_NUMBER, DEFAULT_MAX_WASM_SIZE_BYTES); -} - -/// `WorldStateView` configuration. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, Proxy)] -#[config(env_prefix = "WSV_")] -#[serde(rename_all = "UPPERCASE")] -pub struct Configuration { - /// [`MetadataLimits`] for every asset with store. - pub asset_metadata_limits: MetadataLimits, - /// [`MetadataLimits`] of any asset definition metadata. - pub asset_definition_metadata_limits: MetadataLimits, - /// [`MetadataLimits`] of any account metadata. - pub account_metadata_limits: MetadataLimits, - /// [`MetadataLimits`] of any domain metadata. - pub domain_metadata_limits: MetadataLimits, - /// [`LengthLimits`] for the number of chars in identifiers that can be stored in the WSV. - pub ident_length_limits: LengthLimits, - /// Limits that all transactions need to obey, in terms of size - /// of WASM blob and number of instructions. - pub transaction_limits: TransactionLimits, - /// WASM runtime configuration - #[config(inner)] - pub wasm_runtime_config: wasm::Configuration, -} - -impl Default for ConfigurationProxy { - fn default() -> Self { - Self { - asset_metadata_limits: Some(DEFAULT_METADATA_LIMITS), - asset_definition_metadata_limits: Some(DEFAULT_METADATA_LIMITS), - account_metadata_limits: Some(DEFAULT_METADATA_LIMITS), - domain_metadata_limits: Some(DEFAULT_METADATA_LIMITS), - ident_length_limits: Some(DEFAULT_IDENT_LENGTH_LIMITS), - transaction_limits: Some(DEFAULT_TRANSACTION_LIMITS), - wasm_runtime_config: Some(wasm::ConfigurationProxy::default()), - } - } -} - -#[cfg(test)] -pub mod tests { - use proptest::prelude::*; - - use super::*; - - prop_compose! { - pub fn arb_proxy() - ( - asset_metadata_limits in prop::option::of(Just(DEFAULT_METADATA_LIMITS)), - asset_definition_metadata_limits in prop::option::of(Just(DEFAULT_METADATA_LIMITS)), - account_metadata_limits in prop::option::of(Just(DEFAULT_METADATA_LIMITS)), - domain_metadata_limits in prop::option::of(Just(DEFAULT_METADATA_LIMITS)), - ident_length_limits in prop::option::of(Just(DEFAULT_IDENT_LENGTH_LIMITS)), - transaction_limits in prop::option::of(Just(DEFAULT_TRANSACTION_LIMITS)), - wasm_runtime_config in prop::option::of(Just(wasm::ConfigurationProxy::default())), - ) - -> ConfigurationProxy { - ConfigurationProxy { asset_metadata_limits, asset_definition_metadata_limits, account_metadata_limits, domain_metadata_limits, ident_length_limits, transaction_limits, wasm_runtime_config } - } - } -} diff --git a/config/test/config.toml b/config/test/config.toml new file mode 100644 index 00000000000..d88f7ec119e --- /dev/null +++ b/config/test/config.toml @@ -0,0 +1,7 @@ +[iroha] +public_key = "ed0120FAFCB2B27444221717F6FCBF900D5BE95273B1B0904B08C736B32A19F16AC1F9" +private_key = { digest = "ed25519", payload = "82886B5A2BB3785F3CA8F8A78F60EA9DB62F939937B1CFA8407316EF07909A8D236808A6D4C12C91CA19E54686C2B8F5F3A786278E3824B4571EF234DEC8683B" } +p2p_address = "localhost:1337" + +[torii] +api_address = "localhost:8080" \ No newline at end of file diff --git a/config/tests/fixtures/.full_config_in.env b/config/tests/fixtures/.full_config_in.env new file mode 100644 index 00000000000..a0531603a68 --- /dev/null +++ b/config/tests/fixtures/.full_config_in.env @@ -0,0 +1,6 @@ +PUBLIC_KEY=ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB +PRIVATE_KEY_DIGEST=ed25519 +PRIVATE_KEY_PAYLOAD=8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb +P2P_ADDRESS=127.0.0.1:5432 +GENESIS_PUBLIC_KEY=ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB +API_ADDRESS=127.0.0.1:8080 diff --git a/config/tests/fixtures/empty_ok_genesis.json b/config/tests/fixtures/empty_ok_genesis.json new file mode 100644 index 00000000000..21bcda658eb --- /dev/null +++ b/config/tests/fixtures/empty_ok_genesis.json @@ -0,0 +1,4 @@ +{ + "transactions": [], + "executor": "./executor.wasm" +} \ No newline at end of file diff --git a/config/tests/fixtures/extra_fields.toml b/config/tests/fixtures/extra_fields.toml new file mode 100644 index 00000000000..faf88829e30 --- /dev/null +++ b/config/tests/fixtures/extra_fields.toml @@ -0,0 +1,3 @@ +i_am_unknown = true +foo = false +bar = 0.5 \ No newline at end of file diff --git a/config/tests/fixtures/genesis_with_unexisting_executor.json b/config/tests/fixtures/genesis_with_unexisting_executor.json new file mode 100644 index 00000000000..7802fc241ae --- /dev/null +++ b/config/tests/fixtures/genesis_with_unexisting_executor.json @@ -0,0 +1,4 @@ +{ + "transactions": [], + "executor": "non_existing.wasm" +} \ No newline at end of file diff --git a/config/tests/fixtures/inconsistent_genesis.toml b/config/tests/fixtures/inconsistent_genesis.toml new file mode 100644 index 00000000000..740c2a82b74 --- /dev/null +++ b/config/tests/fixtures/inconsistent_genesis.toml @@ -0,0 +1,18 @@ +[iroha] +public_key = "ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB" +p2p_address = "127.0.0.1:1337" + +[iroha.private_key] +digest_function = "ed25519" +payload = "8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb" + +[genesis] +public_key = "ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB" + +# it should be also provided with a file +[genesis.private_key] +digest_function = "ed25519" +payload = "8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb" + +[torii] +address = "127.0.0.1:8080" \ No newline at end of file diff --git a/config/tests/fixtures/minimal_config.toml b/config/tests/fixtures/minimal_config.toml new file mode 100644 index 00000000000..20783f81be9 --- /dev/null +++ b/config/tests/fixtures/minimal_config.toml @@ -0,0 +1,13 @@ +[iroha] +public_key = "ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB" +p2p_address = "127.0.0.1:1337" + +[iroha.private_key] +digest_function = "ed25519" +payload = "8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb" + +[genesis] +public_key = "ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB" + +[torii] +address = "127.0.0.1:8080" \ No newline at end of file diff --git a/config/tests/fixtures/missing_fields.toml b/config/tests/fixtures/missing_fields.toml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/config/tests/fixtures/with_genesis.toml b/config/tests/fixtures/with_genesis.toml new file mode 100644 index 00000000000..3df699d8f79 --- /dev/null +++ b/config/tests/fixtures/with_genesis.toml @@ -0,0 +1,18 @@ +[iroha] +public_key = "ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB" +p2p_address = "127.0.0.1:1337" + +[iroha.private_key] +digest_function = "ed25519" +payload = "8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb" + +[genesis] +public_key = "ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB" +file = "./empty_ok_genesis.json" + +[genesis.private_key] +digest_function = "ed25519" +payload = "8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb" + +[torii] +address = "127.0.0.1:8080" \ No newline at end of file diff --git a/config/tests/ui.rs b/config/tests/ui.rs new file mode 100644 index 00000000000..fca4cc5bca5 --- /dev/null +++ b/config/tests/ui.rs @@ -0,0 +1,284 @@ +use std::{ + collections::{HashMap, HashSet}, + fs, + path::PathBuf, +}; + +use eyre::Result; +use iroha_config::{parameters::UserLayer, Complete as _, FromEnv, TestEnv}; + +fn fixtures_dir() -> PathBuf { + // CWD is the crate's root + PathBuf::from("tests/fixtures") +} + +fn parse_env(raw: impl AsRef) -> HashMap { + raw.as_ref() + .lines() + .map(|line| { + let mut items = line.split("="); + let key = items + .next() + .expect("line should be in {key}={value} format"); + let value = items + .next() + .expect("line should be in {key}={value} format"); + (key.to_string(), value.to_string()) + }) + .collect() +} + +/// This test not only asserts that the minimal set of fields is enough; +/// it also gives an insight into every single default value +#[test] +fn minimal_config_snapshot() -> Result<()> { + let config = UserLayer::from_toml(fixtures_dir().join("minimal_config.toml"))?.complete()?; + + let expected = expect_test::expect![[r#" + Config { + iroha: Config { + public_key: {digest: ed25519, payload: ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB}, + private_key: {digest: ed25519, payload: 8F4C15E5D664DA3F13778801D23D4E89B76E94C1B94B389544168B6CB894F84F8BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB}, + p2p_address: 127.0.0.1:1337, + }, + genesis: Partial { + public_key: {digest: ed25519, payload: ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB}, + }, + kura: Config { + init_mode: Strict, + block_store_path: "./storage", + debug_output_new_blocks: false, + }, + sumeragi: Config { + block_gossip_period: 10s, + max_blocks_per_gossip: 4, + max_transactions_per_gossip: 500, + transaction_gossip_period: 1s, + trusted_peers: TrustedPeers { + peers: UniqueVec( + [], + ), + }, + }, + logger: Config { + level: INFO, + format: Full, + }, + queue: Config { + max_transactions_in_queue: 65536, + max_transactions_in_queue_per_user: 65536, + transaction_time_to_live_ms: 86400s, + future_threshold_ms: 1s, + }, + snapshot: Config { + create_every_ms: 60s, + store_path: "./storage", + creation_enabled: true, + }, + telemetry: Config { + regular: None, + dev: None, + }, + torii: Config { + address: 127.0.0.1:8080, + max_content_len: ByteSize( + 16777216, + ), + query_idle_time: 30s, + }, + chain_wide: Config { + max_transactions_in_block: 512, + block_time: 2s, + commit_time: 4s, + transactions_limits: TransactionLimits { + max_instruction_number: 4096, + max_wasm_size_bytes: 4194304, + }, + asset_metadata_limits: Limits { + max_len: 1048576, + max_entry_byte_size: 4096, + }, + asset_definition_metadata_limits: Limits { + max_len: 1048576, + max_entry_byte_size: 4096, + }, + account_metadata_limits: Limits { + max_len: 1048576, + max_entry_byte_size: 4096, + }, + domain_metadata_limits: Limits { + max_len: 1048576, + max_entry_byte_size: 4096, + }, + identifier_length_limits: LengthLimits { + min: 1, + max: 128, + }, + wasm_fuel_limit: 23000000, + wasm_max_memory: ByteSize( + 524288000, + ), + }, + }"#]]; + expected.assert_eq(&format!("{config:#?}")); + + Ok(()) +} + +#[test] +fn config_with_genesis() -> Result<()> { + let _config = UserLayer::from_toml(fixtures_dir().join("with_genesis.toml"))?.complete()?; + Ok(()) +} + +#[test] +fn missing_fields() -> Result<()> { + let error = UserLayer::from_toml(fixtures_dir().join("missing_fields.toml"))? + .complete() + .expect_err("should fail with missing fields"); + + let expected = expect_test::expect![[r#" + Missing field: public_key + Missing field: private_key + Missing field: p2p_address + Missing field: public_key + Missing field: address"#]]; + expected.assert_eq(&format!("{error:#}")); + + Ok(()) +} + +#[test] +fn extra_fields() -> Result<()> { + let error = UserLayer::from_toml(fixtures_dir().join("extra_fields.toml")) + .expect_err("should fail with extra fields"); + + let expected = expect_test::expect![[r#" + failed to parse toml: TOML parse error at line 1, column 1 + | + 1 | i_am_unknown = true + | ^^^^^^^^^^^^ + unknown field `i_am_unknown`, expected one of `iroha`, `genesis`, `kura`, `sumeragi`, `logger`, `queue`, `snapshot`, `telemetry`, `torii`, `chain_wide` + "#]]; + expected.assert_eq(&format!("{error:#}")); + + Ok(()) +} + +#[test] +fn inconsistent_genesis_config() -> Result<()> { + let error = UserLayer::from_toml(fixtures_dir().join("inconsistent_genesis.toml"))? + .complete() + .expect_err("should fail with bad genesis config"); + + let expected = expect_test::expect!["FIXME"]; + expected.assert_eq(&format!("{error:#}")); + + Ok(()) +} + +/// Aims the purpose of checking that every single provided env variable is consumed and parsed +/// into a valid config. +/// +/// TODO: define all variables +#[test] +fn full_env_config() -> Result<()> { + let env = { + let path = fixtures_dir().join(".full_config_in.env"); + let contents = fs::read_to_string(path)?; + let map = parse_env(&contents); + TestEnv::with_map(map) + }; + + let layer = UserLayer::from_env(&env)?; + + assert_eq!(env.unvisited(), HashSet::new()); + + let expected = expect_test::expect![[r#" + UserLayer { + iroha: UserLayer { + public_key: Some( + {digest: ed25519, payload: ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB}, + ), + private_key: Some( + {digest: ed25519, payload: 8F4C15E5D664DA3F13778801D23D4E89B76E94C1B94B389544168B6CB894F84F8BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB}, + ), + p2p_address: Some( + 127.0.0.1:5432, + ), + }, + genesis: UserLayer { + public_key: Some( + {digest: ed25519, payload: ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB}, + ), + private_key: None, + file: None, + }, + kura: UserLayer { + init_mode: None, + block_store_path: None, + debug: DebugUserConfig { + output_new_blocks: None, + }, + }, + sumeragi: UserLayer { + block_gossip_period: None, + max_blocks_per_gossip: None, + max_transactions_per_gossip: None, + transaction_gossip_period: None, + trusted_peers: None, + }, + logger: UserLayer { + level: None, + format: None, + }, + queue: UserLayer { + max_transactions_in_queue: None, + max_transactions_in_queue_per_user: None, + transaction_time_to_live_ms: None, + future_threshold_ms: None, + }, + snapshot: UserLayer { + create_every_ms: None, + store_path: None, + creation_enabled: None, + }, + telemetry: UserLayer { + name: None, + url: None, + min_retry_period: None, + max_retry_delay_exponent: None, + dev: UserDevConfig { + file: None, + }, + }, + torii: UserLayer { + address: Some( + 127.0.0.1:8080, + ), + max_content_len: None, + query_idle_time: None, + }, + chain_wide: UserLayer { + max_transactions_in_block: None, + block_time: None, + commit_time: None, + transactions_limits: None, + asset_metadata_limits: None, + asset_definition_metadata_limits: None, + account_metadata_limits: None, + domain_metadata_limits: None, + identifier_length_limits: None, + wasm_fuel_limit: None, + wasm_max_memory: None, + }, + }"#]]; + expected.assert_eq(&format!("{layer:#?}")); + + Ok(()) +} + +#[test] +fn multiple_env_parsing_errors() { + todo!("put invalid data into multiple ENV variables in different modules and check the error report") +} diff --git a/core/test_network/src/lib.rs b/core/test_network/src/lib.rs index f283d63c292..120447c2aff 100644 --- a/core/test_network/src/lib.rs +++ b/core/test_network/src/lib.rs @@ -13,8 +13,8 @@ use iroha_client::{ }; use iroha_config::{ base::proxy::{LoadFromEnv, Override}, - client::Configuration as ClientConfiguration, iroha::{Configuration, ConfigurationProxy}, + r#mod::Configuration as ClientConfiguration, sumeragi::Configuration as SumeragiConfiguration, torii::Configuration as ToriiConfiguration, }; diff --git a/genesis/src/lib.rs b/genesis/src/lib.rs index c23d7c3b900..409947056de 100644 --- a/genesis/src/lib.rs +++ b/genesis/src/lib.rs @@ -82,6 +82,7 @@ pub struct RawGenesisBlock { /// Transactions transactions: Vec, /// Runtime Executor + // TODO `RawGenesisBlock` should have evaluated executor, i.e. loaded executor: ExecutorMode, } @@ -93,7 +94,7 @@ impl RawGenesisBlock { /// /// # Errors /// If file not found or deserialization from file fails. - pub fn from_path + Debug>(path: P) -> Result { + pub fn from_path>(path: P) -> Result { let file = File::open(&path) .wrap_err_with(|| eyre!("Failed to open {}", path.as_ref().display()))?; let size = file @@ -104,8 +105,12 @@ impl RawGenesisBlock { 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_with(|| eyre!("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.as_ref().display() + ) + })?; raw_genesis_block.executor.set_genesis_path(path); Ok(raw_genesis_block) } diff --git a/macro/utils/Cargo.toml b/macro/utils/Cargo.toml index 08c1dce1270..6ee6c66a572 100644 --- a/macro/utils/Cargo.toml +++ b/macro/utils/Cargo.toml @@ -19,4 +19,4 @@ darling = { workspace = true } quote = { workspace = true } proc-macro2 = { workspace = true } manyhow = { workspace = true } -drop_bomb = "0.1.5" +drop_bomb = { workspace = true } diff --git a/tools/kagami/src/config.rs b/tools/kagami/src/config.rs index 10c9aab9255..d194fca3336 100644 --- a/tools/kagami/src/config.rs +++ b/tools/kagami/src/config.rs @@ -29,7 +29,7 @@ impl RunArgs for Args { mod client { use iroha_config::{ - client::{BasicAuth, ConfigurationProxy, WebLogin}, + r#mod::{BasicAuth, ConfigurationProxy, WebLogin}, torii::uri::DEFAULT_API_ADDR, }; diff --git a/torii/src/lib.rs b/torii/src/lib.rs index 68507798239..749fd7ad9b8 100644 --- a/torii/src/lib.rs +++ b/torii/src/lib.rs @@ -13,7 +13,7 @@ use std::{ }; use futures::{stream::FuturesUnordered, StreamExt}; -use iroha_config::torii::{uri, Configuration as ToriiConfiguration}; +use iroha_config::torii::Configuration as ToriiConfiguration; use iroha_core::{ kiso::{Error as KisoError, KisoHandle}, kura::Kura, @@ -40,6 +40,43 @@ mod event; mod routing; mod stream; +pub mod uri { + //! URI that [`Torii`](super::Torii) uses to route incoming requests. + + /// Default socket for listening on external requests + pub const DEFAULT_API_ADDR: iroha_primitives::addr::SocketAddr = + iroha_primitives::addr::socket_addr!(127.0.0.1:8080); + /// Query URI is used to handle incoming Query requests. + pub const QUERY: &str = "query"; + /// Transaction URI is used to handle incoming ISI requests. + pub const TRANSACTION: &str = "transaction"; + /// Block URI is used to handle incoming Block requests. + pub const CONSENSUS: &str = "consensus"; + /// Health URI is used to handle incoming Healthcheck requests. + pub const HEALTH: &str = "health"; + /// The URI used for block synchronization. + pub const BLOCK_SYNC: &str = "block/sync"; + /// The web socket uri used to subscribe to block and transactions statuses. + pub const SUBSCRIPTION: &str = "events"; + /// The web socket uri used to subscribe to blocks stream. + pub const BLOCKS_STREAM: &str = "block/stream"; + /// Get pending transactions. + pub const PENDING_TRANSACTIONS: &str = "pending_transactions"; + /// The URI for local config changing inspecting + pub const CONFIGURATION: &str = "configuration"; + /// URI to report status for administration + pub const STATUS: &str = "status"; + /// Metrics URI is used to export metrics according to [Prometheus + /// Guidance](https://prometheus.io/docs/instrumenting/writing_exporters/). + pub const METRICS: &str = "metrics"; + /// URI for retrieving the schema with which Iroha was built. + pub const SCHEMA: &str = "schema"; + /// URI for getting the API version currently used + pub const API_VERSION: &str = "api_version"; + /// URI for getting cpu profile + pub const PROFILE: &str = "debug/pprof/profile"; +} + /// Main network handler and the only entrypoint of the Iroha. pub struct Torii { chain_id: Arc, From dfcbe6d93eefac014ecf88db7c23d28f903eb79b Mon Sep 17 00:00:00 2001 From: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> Date: Wed, 10 Jan 2024 11:42:26 +0900 Subject: [PATCH 02/94] [refactor]: update structure - exclude genesis block loading from config - construct `KeyPair` on `iroha` completion - use full field names in `complete()`s Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- config/src/parameters/genesis.rs | 38 ++++++++------------------- config/src/parameters/iroha.rs | 45 ++++++++++++++++++++------------ config/src/parameters/torii.rs | 6 ++--- config/tests/ui.rs | 17 +++++++----- genesis/src/lib.rs | 6 ++--- 5 files changed, 55 insertions(+), 57 deletions(-) diff --git a/config/src/parameters/genesis.rs b/config/src/parameters/genesis.rs index 3e6eb9d1695..998c4ee92be 100644 --- a/config/src/parameters/genesis.rs +++ b/config/src/parameters/genesis.rs @@ -30,8 +30,8 @@ pub enum Config { Full { /// Genesis account key pair key_pair: KeyPair, - /// Raw genesis block - raw_block: RawGenesisBlock, + /// Path to the [`RawGenesisBlock`] + file: PathBuf, }, } @@ -41,24 +41,17 @@ impl Complete for UserLayer { fn complete(self) -> CompleteResult { let public_key = self .public_key - .ok_or_else(|| CompleteError::missing_field("public_key"))?; + .ok_or_else(|| CompleteError::missing_field("genesis.public_key"))?; match (self.private_key, self.file) { (None, None) => Ok(Config::Partial { public_key }), - (Some(private_key), Some(path)) => { - let raw_block = RawGenesisBlock::from_path(&path) - .map_err(|report| GenesisConfigError::File { path, report }) - .wrap_err("FIXME don't know how to wrap error here") - .map_err(CompleteError::Custom)?; - - Ok(Config::Full { - key_pair: KeyPair::new(public_key, private_key) - .map_err(GenesisConfigError::from) - .wrap_err("FIXME") - .map_err(CompleteError::Custom)?, - raw_block, - }) - } + (Some(private_key), Some(file)) => Ok(Config::Full { + key_pair: KeyPair::new(public_key, private_key) + .map_err(GenesisConfigError::from) + .wrap_err("FIXME") + .map_err(CompleteError::Custom)?, + file, + }), _ => Err(GenesisConfigError::Inconsistent) .wrap_err("FIXME") .map_err(CompleteError::Custom)?, @@ -100,19 +93,10 @@ impl FromEnv for UserLayer { } } -/// Error which might occur during [`Configuration::parse()`] #[derive(Debug, displaydoc::Display, thiserror::Error)] pub enum GenesisConfigError { /// `genesis.file` and `genesis.private_key` should be set together Inconsistent, - /// invalid genesis key pair + /// failed to construct the genesis's keypair using `genesis.public_key` and `genesis.private_key` configuration parameters KeyPair(#[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, - }, } diff --git a/config/src/parameters/iroha.rs b/config/src/parameters/iroha.rs index d0dde18376d..fa7d800688d 100644 --- a/config/src/parameters/iroha.rs +++ b/config/src/parameters/iroha.rs @@ -3,7 +3,7 @@ use std::{error::Error, str::FromStr}; use eyre::{eyre, Context, Report}; -use iroha_crypto::{Algorithm, PrivateKey, PublicKey}; +use iroha_crypto::{Algorithm, KeyPair, PrivateKey, PublicKey}; use iroha_data_model::ChainId; use iroha_primitives::addr::SocketAddr; use serde::{Deserialize, Serialize}; @@ -28,23 +28,35 @@ impl Complete for UserLayer { fn complete(self) -> CompleteResult { let mut emitter = super::Emitter::::new(); - if let None = self.public_key { - emitter.emit_missing_field("public_key"); - } - - if let None = self.private_key { - emitter.emit_missing_field("private_key"); - } + let key_pair = match (self.public_key, self.private_key) { + (Some(public_key), Some(private_key)) => { + KeyPair::new(public_key, private_key) + .map(Some) + .wrap_err("failed to construct a key pair from `iroha.public_key` and `iroha.private_key` configuration parameters") + .unwrap_or_else(|report| { + emitter.emit(CompleteError::Custom(report)); + None + }) + }, + (public_key, private_key) => { + if public_key.is_none() { + emitter.emit_missing_field("iroha.public_key"); + } + if private_key.is_none() { + emitter.emit_missing_field("iroha.private_key"); + } + None + } + }; if let None = self.p2p_address { - emitter.emit_missing_field("p2p_address"); + emitter.emit_missing_field("iroha.p2p_address"); } emitter.finish()?; Ok(Config { - public_key: self.public_key.unwrap(), - private_key: self.private_key.unwrap(), + key_pair: key_pair.unwrap(), p2p_address: self.p2p_address.unwrap(), }) } @@ -134,8 +146,7 @@ impl FromEnv for UserLayer { #[derive(Debug)] pub struct Config { pub chain_id: ChainId, - pub public_key: PublicKey, - pub private_key: PrivateKey, + pub key_pair: KeyPair, pub p2p_address: SocketAddr, } @@ -167,7 +178,7 @@ mod tests { `PRIVATE_KEY_DIGEST` env was provided, but `PRIVATE_KEY_PAYLOAD` was not Location: - config/src/parameters/iroha.rs:125:65"#]]; + config/src/parameters/iroha.rs:97:26"#]]; expected.assert_eq(&format!("{error:?}")); } @@ -179,7 +190,7 @@ mod tests { `PRIVATE_KEY_PAYLOAD` env was provided, but `PRIVATE_KEY_DIGEST` was not Location: - config/src/parameters/iroha.rs:126:64"#]]; + config/src/parameters/iroha.rs:105:26"#]]; expected.assert_eq(&format!("{error:?}")); } @@ -198,7 +209,7 @@ mod tests { Key could not be parsed. Odd number of digits Location: - config/src/parameters/iroha.rs:118:26"#]]; + config/src/parameters/iroha.rs:81:18"#]]; expected.assert_eq(&format!("{error:?}")); } @@ -218,7 +229,7 @@ mod tests { Algorithm not supported Location: - config/src/parameters/iroha.rs:95:25"#]]; + config/src/lib.rs:263:14"#]]; expected.assert_eq(&format!("{error:?}")); } } diff --git a/config/src/parameters/torii.rs b/config/src/parameters/torii.rs index aa8003a0fbe..710d18b9bbf 100644 --- a/config/src/parameters/torii.rs +++ b/config/src/parameters/torii.rs @@ -33,9 +33,9 @@ impl Complete for UserLayer { fn complete(self) -> CompleteResult { Ok(Config { - address: self.address.ok_or_else(|| CompleteError::MissingField { - path: "address".to_string(), - })?, + address: self + .address + .ok_or_else(|| CompleteError::missing_field("torii.address"))?, max_content_len: self .max_content_len .unwrap_or_else(|| ByteSize(DEFAULT_MAX_CONTENT_LENGTH)), diff --git a/config/tests/ui.rs b/config/tests/ui.rs index fca4cc5bca5..dc53fb00014 100644 --- a/config/tests/ui.rs +++ b/config/tests/ui.rs @@ -37,8 +37,10 @@ fn minimal_config_snapshot() -> Result<()> { let expected = expect_test::expect![[r#" Config { iroha: Config { - public_key: {digest: ed25519, payload: ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB}, - private_key: {digest: ed25519, payload: 8F4C15E5D664DA3F13778801D23D4E89B76E94C1B94B389544168B6CB894F84F8BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB}, + key_pair: KeyPair { + public_key: {digest: ed25519, payload: ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB}, + private_key: {digest: ed25519, payload: 8F4C15E5D664DA3F13778801D23D4E89B76E94C1B94B389544168B6CB894F84F8BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB}, + }, p2p_address: 127.0.0.1:1337, }, genesis: Partial { @@ -138,11 +140,11 @@ fn missing_fields() -> Result<()> { .expect_err("should fail with missing fields"); let expected = expect_test::expect![[r#" - Missing field: public_key - Missing field: private_key - Missing field: p2p_address - Missing field: public_key - Missing field: address"#]]; + Missing field: iroha.public_key + Missing field: iroha.private_key + Missing field: iroha.p2p_address + Missing field: genesis.public_key + Missing field: torii.address"#]]; expected.assert_eq(&format!("{error:#}")); Ok(()) @@ -279,6 +281,7 @@ fn full_env_config() -> Result<()> { } #[test] +#[ignore] fn multiple_env_parsing_errors() { todo!("put invalid data into multiple ENV variables in different modules and check the error report") } diff --git a/genesis/src/lib.rs b/genesis/src/lib.rs index 409947056de..7f039b88922 100644 --- a/genesis/src/lib.rs +++ b/genesis/src/lib.rs @@ -43,9 +43,8 @@ impl GenesisNetwork { /// Construct [`GenesisNetwork`] from configuration. /// /// # Errors - /// - If fails to sign a transaction (which means that the `key_pair` is malformed rather - /// than anything else) - /// - If transactions set is empty + /// If fails to sign a transaction (which means that the `key_pair` is malformed rather + /// than anything else) pub fn new( raw_block: RawGenesisBlock, chain_id: &ChainId, @@ -140,6 +139,7 @@ impl ExecutorMode { } } +/// Loads the executor from the path or uses the inline blob for conversion impl TryFrom for Executor { type Error = ErrReport; From 934920b403252e589bed8d255e760ee5e31538c3 Mon Sep 17 00:00:00 2001 From: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> Date: Wed, 10 Jan 2024 11:49:03 +0900 Subject: [PATCH 03/94] [refactor]: apply lints Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- config/src/lib.rs | 81 +++++++++++++---------------- config/src/parameters/chain_wide.rs | 6 +-- config/src/parameters/iroha.rs | 14 ++--- config/src/parameters/mod.rs | 7 +-- config/src/parameters/queue.rs | 10 ++-- config/src/parameters/snapshot.rs | 3 +- config/src/parameters/sumeragi.rs | 10 ++-- config/src/parameters/telemetry.rs | 7 ++- config/src/parameters/torii.rs | 5 +- config/tests/ui.rs | 8 ++- 10 files changed, 67 insertions(+), 84 deletions(-) diff --git a/config/src/lib.rs b/config/src/lib.rs index b969ce51319..eb8d174a72b 100644 --- a/config/src/lib.rs +++ b/config/src/lib.rs @@ -93,10 +93,11 @@ impl Emitter { fn finish(mut self) -> Result<(), ErrorsCollection> { self.bomb.defuse(); - if !self.errors.is_empty() { - Err(ErrorsCollection(self.errors)) - } else { + + if self.errors.is_empty() { Ok(()) + } else { + Err(ErrorsCollection(self.errors)) } } } @@ -138,7 +139,7 @@ where fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { for (i, item) in self.0.iter().enumerate() { if i > 0 { - write!(f, "\n")?; + writeln!(f)?; } write!(f, "{item}")?; } @@ -153,7 +154,7 @@ where fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { for (i, item) in self.0.iter().enumerate() { if i > 0 { - write!(f, "\n")?; + writeln!(f)?; } write!(f, "{item:?}")?; } @@ -191,6 +192,7 @@ impl TestEnv { Self { map, ..Self::new() } } + #[must_use] pub fn set(mut self, key: impl AsRef, value: impl AsRef) -> Self { self.map .insert(key.as_ref().to_string(), value.as_ref().to_string()); @@ -207,35 +209,7 @@ impl TestEnv { impl ReadEnv for TestEnv { fn get(&self, key: impl AsRef) -> Option<&str> { self.visited.borrow_mut().insert(key.as_ref().to_string()); - self.map.get(key.as_ref()).map(|x| x.as_str()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn single_missing_field() { - let mut emitter = Emitter::new(); - - emitter.emit(CompleteError::missing_field("foo".to_string())); - - let err = emitter.finish().unwrap_err(); - - assert_eq!(format!("{err}"), "Missing field: foo") - } - - #[test] - fn multiple_missing_fields() { - let mut emitter = Emitter::new(); - - emitter.emit(CompleteError::missing_field("foo".to_string())); - emitter.emit(CompleteError::missing_field("bar".to_string())); - - let err = emitter.finish().unwrap_err(); - - assert_eq!(format!("{err}"), "Missing field: foo\nMissing field: bar") + self.map.get(key.as_ref()).map(std::string::String::as_str) } } @@ -280,19 +254,36 @@ where impl From> for Option { fn from(value: ParseEnvResult) -> Self { match value { - ParseEnvResult::None => None, - ParseEnvResult::ParseError => None, + ParseEnvResult::None | ParseEnvResult::ParseError => None, ParseEnvResult::Value(x) => Some(x), } } } -// impl From>> for ParseEnvResult { -// fn from(value: Result>) -> Self { -// match value { -// Ok(Some(value)) => Self::Value(value), -// Ok(None) => Self::None, -// Err(report) => -// } -// } -// } +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn single_missing_field() { + let mut emitter = Emitter::new(); + + emitter.emit(CompleteError::missing_field("foo")); + + let err = emitter.finish().unwrap_err(); + + assert_eq!(format!("{err}"), "Missing field: foo") + } + + #[test] + fn multiple_missing_fields() { + let mut emitter = Emitter::new(); + + emitter.emit(CompleteError::missing_field("foo")); + emitter.emit(CompleteError::missing_field("bar")); + + let err = emitter.finish().unwrap_err(); + + assert_eq!(format!("{err}"), "Missing field: foo\nMissing field: bar") + } +} diff --git a/config/src/parameters/chain_wide.rs b/config/src/parameters/chain_wide.rs index e3d47f60d8f..b5e327e0dde 100644 --- a/config/src/parameters/chain_wide.rs +++ b/config/src/parameters/chain_wide.rs @@ -75,12 +75,10 @@ impl Complete for UserLayer { max_transactions_in_block: self.max_transactions_in_block.unwrap_or(DEFAULT_MAX_TXS), block_time: self .block_time - .map(UserDuration::get) - .unwrap_or(DEFAULT_BLOCK_TIME), + .map_or(DEFAULT_BLOCK_TIME, UserDuration::get), commit_time: self .commit_time - .map(UserDuration::get) - .unwrap_or(DEFAULT_COMMIT_TIME_LIMIT), + .map_or(DEFAULT_COMMIT_TIME_LIMIT, UserDuration::get), transactions_limits: self .transactions_limits .unwrap_or(DEFAULT_TRANSACTION_LIMITS), diff --git a/config/src/parameters/iroha.rs b/config/src/parameters/iroha.rs index fa7d800688d..2b80efca5b4 100644 --- a/config/src/parameters/iroha.rs +++ b/config/src/parameters/iroha.rs @@ -49,7 +49,7 @@ impl Complete for UserLayer { } }; - if let None = self.p2p_address { + if self.p2p_address.is_none() { emitter.emit_missing_field("iroha.p2p_address"); } @@ -88,11 +88,13 @@ pub(crate) fn private_key_from_env( &payload_env ) }) - .map(ParseEnvResult::Value) - .unwrap_or_else(|report| { - emitter.emit(report); - ParseEnvResult::ParseError - }) + .map_or_else( + |report| { + emitter.emit(report); + ParseEnvResult::ParseError + }, + ParseEnvResult::Value, + ) } (ParseEnvResult::None, None) | (ParseEnvResult::ParseError, _) => ParseEnvResult::None, (ParseEnvResult::Value(_), None) => { diff --git a/config/src/parameters/mod.rs b/config/src/parameters/mod.rs index 60863e9671d..ec9a7aa1a9e 100644 --- a/config/src/parameters/mod.rs +++ b/config/src/parameters/mod.rs @@ -83,7 +83,8 @@ impl UserLayer { patch!(self.telemetry.dev.file); } - pub fn merge(self, other: Self) -> Self { + #[must_use] + pub fn merge(self, _other: Self) -> Self { todo!() } } @@ -136,8 +137,6 @@ impl Complete for UserLayer { impl FromEnv for UserLayer { fn from_env(env: &impl ReadEnv) -> FromEnvResult { - let mut emitter = Emitter::new(); - fn from_env_nested( env: &impl ReadEnv, emitter: &mut Emitter, @@ -151,6 +150,8 @@ impl FromEnv for UserLayer { } } + let mut emitter = Emitter::new(); + let iroha = from_env_nested(env, &mut emitter); let genesis = from_env_nested(env, &mut emitter); let kura = from_env_nested(env, &mut emitter); diff --git a/config/src/parameters/queue.rs b/config/src/parameters/queue.rs index bd371a570d7..010602b0d1b 100644 --- a/config/src/parameters/queue.rs +++ b/config/src/parameters/queue.rs @@ -47,18 +47,16 @@ impl Complete for UserLayer { Ok(Config { max_transactions_in_queue: self .max_transactions_in_queue - .unwrap_or_else(|| DEFAULT_MAX_TRANSACTIONS_IN_QUEUE), + .unwrap_or(DEFAULT_MAX_TRANSACTIONS_IN_QUEUE), max_transactions_in_queue_per_user: self .max_transactions_in_queue_per_user - .unwrap_or_else(|| DEFAULT_MAX_TRANSACTIONS_IN_QUEUE), + .unwrap_or(DEFAULT_MAX_TRANSACTIONS_IN_QUEUE), transaction_time_to_live_ms: self .transaction_time_to_live_ms - .map(UserDuration::get) - .unwrap_or(DEFAULT_TRANSACTION_TIME_TO_LIVE), + .map_or(DEFAULT_TRANSACTION_TIME_TO_LIVE, UserDuration::get), future_threshold_ms: self .future_threshold_ms - .map(UserDuration::get) - .unwrap_or(DEFAULT_FUTURE_THRESHOLD), + .map_or(DEFAULT_FUTURE_THRESHOLD, UserDuration::get), }) } } diff --git a/config/src/parameters/snapshot.rs b/config/src/parameters/snapshot.rs index 3544bb693d9..195fdd3c2e0 100644 --- a/config/src/parameters/snapshot.rs +++ b/config/src/parameters/snapshot.rs @@ -40,8 +40,7 @@ impl Complete for UserLayer { creation_enabled: self.creation_enabled.unwrap_or(DEFAULT_ENABLED), create_every_ms: self .create_every_ms - .map(UserDuration::get) - .unwrap_or(DEFAULT_SNAPSHOT_CREATE_EVERY_MS), + .map_or(DEFAULT_SNAPSHOT_CREATE_EVERY_MS, UserDuration::get), store_path: self .store_path .unwrap_or_else(|| PathBuf::from(DEFAULT_SNAPSHOT_PATH)), diff --git a/config/src/parameters/sumeragi.rs b/config/src/parameters/sumeragi.rs index f5d4c39a05b..4ba2a9d039f 100644 --- a/config/src/parameters/sumeragi.rs +++ b/config/src/parameters/sumeragi.rs @@ -48,18 +48,16 @@ impl Complete for UserLayer { Ok(Config { block_gossip_period: self .block_gossip_period - .map(UserDuration::get) - .unwrap_or(DEFAULT_BLOCK_GOSSIP_PERIOD), + .map_or(DEFAULT_BLOCK_GOSSIP_PERIOD, UserDuration::get), max_blocks_per_gossip: self .max_blocks_per_gossip - .unwrap_or_else(|| DEFAULT_MAX_BLOCKS_PER_GOSSIP.into()), + .unwrap_or(DEFAULT_MAX_BLOCKS_PER_GOSSIP), max_transactions_per_gossip: self .max_transactions_per_gossip - .unwrap_or_else(|| DEFAULT_MAX_TRANSACTIONS_PER_GOSSIP.into()), + .unwrap_or(DEFAULT_MAX_TRANSACTIONS_PER_GOSSIP), transaction_gossip_period: self .transaction_gossip_period - .map(UserDuration::get) - .unwrap_or(DEFAULT_TRANSACTION_GOSSIP_PERIOD), + .map_or(DEFAULT_TRANSACTION_GOSSIP_PERIOD, UserDuration::get), trusted_peers: self.trusted_peers.unwrap_or_default(), }) } diff --git a/config/src/parameters/telemetry.rs b/config/src/parameters/telemetry.rs index cfc95118963..c870dedfdc4 100644 --- a/config/src/parameters/telemetry.rs +++ b/config/src/parameters/telemetry.rs @@ -79,13 +79,12 @@ impl Complete for UserLayer { let regular = match (name, url) { (Some(name), Some(url)) => Some(RegularTelemetryConfig { - name: name.clone(), - url: url.clone(), + name, + url, max_retry_delay_exponent: max_retry_delay_exponent .unwrap_or(DEFAULT_MAX_RETRY_DELAY_EXPONENT), min_retry_period: min_retry_period - .map(UserDuration::get) - .unwrap_or(DEFAULT_MIN_RETRY_PERIOD), + .map_or(DEFAULT_MIN_RETRY_PERIOD, UserDuration::get), }), (None, None) => None, // TODO improve error detail diff --git a/config/src/parameters/torii.rs b/config/src/parameters/torii.rs index 710d18b9bbf..d6baa7232d8 100644 --- a/config/src/parameters/torii.rs +++ b/config/src/parameters/torii.rs @@ -38,11 +38,10 @@ impl Complete for UserLayer { .ok_or_else(|| CompleteError::missing_field("torii.address"))?, max_content_len: self .max_content_len - .unwrap_or_else(|| ByteSize(DEFAULT_MAX_CONTENT_LENGTH)), + .unwrap_or(ByteSize(DEFAULT_MAX_CONTENT_LENGTH)), query_idle_time: self .query_idle_time - .map(UserDuration::get) - .unwrap_or(DEFAULT_QUERY_IDLE_TIME), + .map_or(DEFAULT_QUERY_IDLE_TIME, UserDuration::get), }) } } diff --git a/config/tests/ui.rs b/config/tests/ui.rs index dc53fb00014..6dce72f623c 100644 --- a/config/tests/ui.rs +++ b/config/tests/ui.rs @@ -16,7 +16,7 @@ fn parse_env(raw: impl AsRef) -> HashMap { raw.as_ref() .lines() .map(|line| { - let mut items = line.split("="); + let mut items = line.split('='); let key = items .next() .expect("line should be in {key}={value} format"); @@ -151,7 +151,7 @@ fn missing_fields() -> Result<()> { } #[test] -fn extra_fields() -> Result<()> { +fn extra_fields() { let error = UserLayer::from_toml(fixtures_dir().join("extra_fields.toml")) .expect_err("should fail with extra fields"); @@ -163,8 +163,6 @@ fn extra_fields() -> Result<()> { unknown field `i_am_unknown`, expected one of `iroha`, `genesis`, `kura`, `sumeragi`, `logger`, `queue`, `snapshot`, `telemetry`, `torii`, `chain_wide` "#]]; expected.assert_eq(&format!("{error:#}")); - - Ok(()) } #[test] @@ -188,7 +186,7 @@ fn full_env_config() -> Result<()> { let env = { let path = fixtures_dir().join(".full_config_in.env"); let contents = fs::read_to_string(path)?; - let map = parse_env(&contents); + let map = parse_env(contents); TestEnv::with_map(map) }; From 3ace9847ea49faedff3e9a05a4070d2706e8e77f Mon Sep 17 00:00:00 2001 From: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> Date: Wed, 10 Jan 2024 15:39:35 +0900 Subject: [PATCH 04/94] [feat]: include more ENV vars, refactor Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- Cargo.lock | 1 + Cargo.toml | 1 + config/Cargo.toml | 2 +- config/src/lib.rs | 4 ++- config/src/parameters/iroha.rs | 6 ++-- config/src/parameters/kura.rs | 42 +++-------------------- config/src/parameters/logger.rs | 39 ++++++++++++++++++--- config/src/util.rs | 30 ++++++++++++++++ config/tests/{ui.rs => fixtures.rs} | 34 ++++++++++++------ config/tests/fixtures/.full_config_in.env | 9 +++++ data_model/Cargo.toml | 1 + data_model/src/lib.rs | 14 ++++++++ 12 files changed, 125 insertions(+), 58 deletions(-) create mode 100644 config/src/util.rs rename config/tests/{ui.rs => fixtures.rs} (91%) diff --git a/Cargo.lock b/Cargo.lock index 8799a6ab89b..abaf0203931 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2918,6 +2918,7 @@ dependencies = [ "iroha_version", "once_cell", "parity-scale-codec", + "parse-display", "serde", "serde_json", "serde_with", diff --git a/Cargo.toml b/Cargo.toml index 4dad979b674..2468ec4f6d2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -112,6 +112,7 @@ displaydoc = { version = "0.2.4", default-features = false } cfg-if = "1.0.0" derive_more = { version = "0.99.17", default-features = false } +parse-display = "0.8.2" async-trait = "0.1.73" strum = { version = "0.25.0", default-features = false } getset = "0.1.2" diff --git a/config/Cargo.toml b/config/Cargo.toml index 7865b53d01e..09075b14d2c 100644 --- a/config/Cargo.toml +++ b/config/Cargo.toml @@ -34,7 +34,7 @@ cfg-if = { workspace = true } once_cell = { workspace = true } nonzero_ext = "0.3.0" toml = "0.8.8" -parse-display = "0.8.2" +parse-display = { workspace = true } [dev-dependencies] proptest = "1.3.1" diff --git a/config/src/lib.rs b/config/src/lib.rs index eb8d174a72b..203e07bf1b5 100644 --- a/config/src/lib.rs +++ b/config/src/lib.rs @@ -10,15 +10,17 @@ use std::{ fmt::{Debug, Display, Formatter}, io::Read, ops::Sub, + result, str::FromStr, time::Duration, }; use eyre::{eyre, Report, Result, WrapErr}; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; pub mod client_api; pub mod parameters; +mod util; /// User-provided duration #[derive(Debug, Copy, Clone, Deserialize, Serialize)] diff --git a/config/src/parameters/iroha.rs b/config/src/parameters/iroha.rs index 2b80efca5b4..78eed202d90 100644 --- a/config/src/parameters/iroha.rs +++ b/config/src/parameters/iroha.rs @@ -180,7 +180,7 @@ mod tests { `PRIVATE_KEY_DIGEST` env was provided, but `PRIVATE_KEY_PAYLOAD` was not Location: - config/src/parameters/iroha.rs:97:26"#]]; + config/src/parameters/iroha.rs:99:26"#]]; expected.assert_eq(&format!("{error:?}")); } @@ -192,7 +192,7 @@ mod tests { `PRIVATE_KEY_PAYLOAD` env was provided, but `PRIVATE_KEY_DIGEST` was not Location: - config/src/parameters/iroha.rs:105:26"#]]; + config/src/parameters/iroha.rs:107:26"#]]; expected.assert_eq(&format!("{error:?}")); } @@ -231,7 +231,7 @@ mod tests { Algorithm not supported Location: - config/src/lib.rs:263:14"#]]; + config/src/lib.rs:239:14"#]]; expected.assert_eq(&format!("{error:?}")); } } diff --git a/config/src/parameters/kura.rs b/config/src/parameters/kura.rs index 1dcde592eb1..5af12d91aa6 100644 --- a/config/src/parameters/kura.rs +++ b/config/src/parameters/kura.rs @@ -1,14 +1,10 @@ //! Module for kura-related configuration and structs -use std::{path::PathBuf, str::FromStr}; +use std::{fmt::Display, path::PathBuf, str::FromStr}; -use derive_more::FromStr; use serde::{Deserialize, Deserializer, Serialize, Serializer}; -use crate::{ - Complete, CompleteError, CompleteResult, Emitter, FromEnv, FromEnvResult, ParseEnvResult, - ReadEnv, -}; +use crate::{Complete, CompleteResult, Emitter, FromEnv, FromEnvResult, ParseEnvResult, ReadEnv}; const DEFAULT_BLOCK_STORE_PATH: &str = "./storage"; @@ -97,30 +93,11 @@ pub enum Mode { Fast, } -impl Serialize for Mode { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - serializer.collect_str(&self) - } -} - -impl<'de> Deserialize<'de> for Mode { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - String::deserialize(deserializer)? - .parse() - .map_err(serde::de::Error::custom) - } -} +crate::util::impl_serialize_display!(Mode); +crate::util::impl_deserialize_from_str!(Mode); #[cfg(test)] mod tests { - use serde_json::{from_value, json}; - use super::*; #[test] @@ -130,15 +107,4 @@ mod tests { assert_eq!("strict".parse::().unwrap(), Mode::Strict); assert_eq!("fast".parse::().unwrap(), Mode::Fast); } - - #[test] - fn init_mode_serde_uses_display() { - let sample = [Mode::Strict, Mode::Fast]; - let json = json!(["strict", "fast"]); - - assert_eq!(serde_json::to_string(&sample).unwrap(), json.to_string()); - - let encoded: [Mode; 2] = from_value(json).expect("should parse"); - assert_eq!(encoded, sample); - } } diff --git a/config/src/parameters/logger.rs b/config/src/parameters/logger.rs index e85c90b598e..c999a651508 100644 --- a/config/src/parameters/logger.rs +++ b/config/src/parameters/logger.rs @@ -5,9 +5,13 @@ use core::fmt::Debug; pub use iroha_data_model::Level; #[cfg(feature = "tokio-console")] use iroha_primitives::addr::{socket_addr, SocketAddr}; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; -use crate::{Complete, CompleteError, CompleteResult, FromEnvDefaultFallback}; +use crate::{ + util::{impl_deserialize_from_str, impl_serialize_display}, + Complete, CompleteError, CompleteResult, Emitter, FromEnv, FromEnvResult, ParseEnvResult, + ReadEnv, +}; #[cfg(feature = "tokio-console")] const DEFAULT_TOKIO_CONSOLE_ADDR: SocketAddr = socket_addr!(127.0.0.1:5555); @@ -50,8 +54,10 @@ pub struct Config { } /// Reflects formatters in [`tracing_subscriber::fmt::format`] -#[derive(Debug, Copy, Clone, Eq, PartialEq, Deserialize, Serialize, Default)] -#[serde(rename_all = "snake_case")] +#[derive( + Debug, Copy, Clone, Eq, PartialEq, parse_display::Display, parse_display::FromStr, Default, +)] +#[display(style = "snake_case")] pub enum Format { /// See [`tracing_subscriber::fmt::format::Full`] #[default] @@ -64,6 +70,9 @@ pub enum Format { Json, } +impl_serialize_display!(Format); +impl_deserialize_from_str!(Format); + impl Complete for UserLayer { type Output = Config; @@ -79,7 +88,27 @@ impl Complete for UserLayer { } } -impl FromEnvDefaultFallback for UserLayer {} +impl FromEnv for UserLayer { + fn from_env(env: &impl ReadEnv) -> FromEnvResult + where + Self: Sized, + { + let mut emitter = Emitter::new(); + + let level = + ParseEnvResult::parse_simple(&mut emitter, env, "LOG_LEVEL", "logger.level").into(); + let format = + ParseEnvResult::parse_simple(&mut emitter, env, "LOG_FORMAT", "logger.format").into(); + + emitter.finish()?; + + Ok(Self { + level, + format, + ..Self::default() + }) + } +} #[cfg(test)] pub mod tests { diff --git a/config/src/util.rs b/config/src/util.rs new file mode 100644 index 00000000000..d502a1daeec --- /dev/null +++ b/config/src/util.rs @@ -0,0 +1,30 @@ +macro_rules! impl_serialize_display { + ($ty:ty) => { + impl serde::Serialize for $ty { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: Serializer, + { + serializer.collect_str(self) + } + } + }; +} + +macro_rules! impl_deserialize_from_str { + ($ty:ty) => { + impl<'de> serde::Deserialize<'de> for $ty { + fn deserialize(deserializer: D) -> std::result::Result + where + D: Deserializer<'de>, + { + String::deserialize(deserializer)? + .parse() + .map_err(serde::de::Error::custom) + } + } + }; +} + +pub(crate) use impl_deserialize_from_str; +pub(crate) use impl_serialize_display; diff --git a/config/tests/ui.rs b/config/tests/fixtures.rs similarity index 91% rename from config/tests/ui.rs rename to config/tests/fixtures.rs index 6dce72f623c..e3c8b9335bf 100644 --- a/config/tests/ui.rs +++ b/config/tests/fixtures.rs @@ -179,8 +179,6 @@ fn inconsistent_genesis_config() -> Result<()> { /// Aims the purpose of checking that every single provided env variable is consumed and parsed /// into a valid config. -/// -/// TODO: define all variables #[test] fn full_env_config() -> Result<()> { let env = { @@ -211,14 +209,22 @@ fn full_env_config() -> Result<()> { public_key: Some( {digest: ed25519, payload: ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB}, ), - private_key: None, + private_key: Some( + {digest: ed25519, payload: 8F4C15E5D664DA3F13778801D23D4E89B76E94C1B94B389544168B6CB894F84F8BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB}, + ), file: None, }, kura: UserLayer { - init_mode: None, - block_store_path: None, + init_mode: Some( + Strict, + ), + block_store_path: Some( + "/store/path/from/env", + ), debug: DebugUserConfig { - output_new_blocks: None, + output_new_blocks: Some( + false, + ), }, }, sumeragi: UserLayer { @@ -229,8 +235,12 @@ fn full_env_config() -> Result<()> { trusted_peers: None, }, logger: UserLayer { - level: None, - format: None, + level: Some( + DEBUG, + ), + format: Some( + Pretty, + ), }, queue: UserLayer { max_transactions_in_queue: None, @@ -240,8 +250,12 @@ fn full_env_config() -> Result<()> { }, snapshot: UserLayer { create_every_ms: None, - store_path: None, - creation_enabled: None, + store_path: Some( + "/snapshot/path/from/env", + ), + creation_enabled: Some( + false, + ), }, telemetry: UserLayer { name: None, diff --git a/config/tests/fixtures/.full_config_in.env b/config/tests/fixtures/.full_config_in.env index a0531603a68..6f81e8cdd8a 100644 --- a/config/tests/fixtures/.full_config_in.env +++ b/config/tests/fixtures/.full_config_in.env @@ -3,4 +3,13 @@ PRIVATE_KEY_DIGEST=ed25519 PRIVATE_KEY_PAYLOAD=8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb P2P_ADDRESS=127.0.0.1:5432 GENESIS_PUBLIC_KEY=ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB +GENESIS_PRIVATE_KEY_DIGEST=ed25519 +GENESIS_PRIVATE_KEY_PAYLOAD=8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb API_ADDRESS=127.0.0.1:8080 +KURA_INIT_MODE=strict +KURA_BLOCK_STORE=/store/path/from/env +KURA_DEBUG_OUTPUT_NEW_BLOCKS=false +LOG_LEVEL=DEBUG +LOG_FORMAT=pretty +SNAPSHOT_STORE=/snapshot/path/from/env +SNAPSHOT_CREATION_ENABLED=false diff --git a/data_model/Cargo.toml b/data_model/Cargo.toml index 9b80ee2b6fc..6a21cf6d6bd 100644 --- a/data_model/Cargo.toml +++ b/data_model/Cargo.toml @@ -43,6 +43,7 @@ iroha_ffi = { workspace = true, optional = true } parity-scale-codec = { workspace = true, features = ["derive"] } derive_more = { workspace = true, features = ["as_ref", "display", "constructor", "from_str", "from", "into"] } +parse-display = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_with = { workspace = true, features = ["macros"] } serde_json = { workspace = true } diff --git a/data_model/src/lib.rs b/data_model/src/lib.rs index 3c7c6fcee4f..b5692ff242b 100644 --- a/data_model/src/lib.rs +++ b/data_model/src/lib.rs @@ -979,6 +979,7 @@ pub mod model { Decode, FromRepr, IntoSchema, + parse_display::FromStr, )] #[allow(clippy::upper_case_acronyms)] #[repr(u8)] @@ -1028,6 +1029,19 @@ impl Decode for ChainId { } } +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn parse_level_from_str() { + assert_eq!( + "INFO".parse::().unwrap(), + crate::model::Level::INFO + ); + } +} + // TODO: think of a way to `impl Identifiable for IdentifiableBox`. // The main problem is lifetimes and conversion cost. From 6c159d561aca8f2fdc66091c777c5a80c4f94010 Mon Sep 17 00:00:00 2001 From: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> Date: Thu, 11 Jan 2024 12:25:22 +0900 Subject: [PATCH 05/94] [feat]: impl merging - `UserField` wrap instead of `Option` - move trusted peers uniqueness check Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- Cargo.lock | 23 +++++++ config/Cargo.toml | 1 + config/src/lib.rs | 66 ++++++++++++++++++- config/src/parameters/chain_wide.rs | 29 ++++---- config/src/parameters/genesis.rs | 19 +++--- config/src/parameters/iroha.rs | 28 ++++---- config/src/parameters/kura.rs | 7 +- config/src/parameters/logger.rs | 13 ++-- config/src/parameters/mod.rs | 33 ++++++++-- config/src/parameters/queue.rs | 15 +++-- config/src/parameters/snapshot.rs | 15 +++-- config/src/parameters/sumeragi.rs | 62 ++++++++++++----- config/src/parameters/telemetry.rs | 29 ++++---- config/src/parameters/torii.rs | 15 +++-- config/tests/fixtures.rs | 34 +++++++--- config/tests/fixtures/config_and_env.env | 1 + config/tests/fixtures/config_and_env.toml | 12 ++++ .../{.full_config_in.env => full.env} | 0 18 files changed, 290 insertions(+), 112 deletions(-) create mode 100644 config/tests/fixtures/config_and_env.env create mode 100644 config/tests/fixtures/config_and_env.toml rename config/tests/fixtures/{.full_config_in.env => full.env} (100%) diff --git a/Cargo.lock b/Cargo.lock index abaf0203931..27926862e2a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2776,6 +2776,7 @@ dependencies = [ "iroha_genesis", "iroha_primitives", "json5", + "merge", "nonzero_ext", "once_cell", "parse-display", @@ -3746,6 +3747,28 @@ dependencies = [ "autocfg", ] +[[package]] +name = "merge" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10bbef93abb1da61525bbc45eeaff6473a41907d19f8f9aa5168d214e10693e9" +dependencies = [ + "merge_derive", + "num-traits", +] + +[[package]] +name = "merge_derive" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "209d075476da2e63b4b29e72a2ef627b840589588e71400a25e3565c4f849d07" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "mime" version = "0.3.17" diff --git a/config/Cargo.toml b/config/Cargo.toml index 09075b14d2c..b2949669162 100644 --- a/config/Cargo.toml +++ b/config/Cargo.toml @@ -35,6 +35,7 @@ once_cell = { workspace = true } nonzero_ext = "0.3.0" toml = "0.8.8" parse-display = { workspace = true } +merge = "0.1.0" [dev-dependencies] proptest = "1.3.1" diff --git a/config/src/lib.rs b/config/src/lib.rs index 203e07bf1b5..d3266289ad0 100644 --- a/config/src/lib.rs +++ b/config/src/lib.rs @@ -10,12 +10,12 @@ use std::{ fmt::{Debug, Display, Formatter}, io::Read, ops::Sub, - result, str::FromStr, time::Duration, }; use eyre::{eyre, Report, Result, WrapErr}; +use merge::Merge; use serde::{Deserialize, Deserializer, Serialize, Serializer}; pub mod client_api; @@ -42,8 +42,6 @@ pub trait Complete { fn complete(self) -> CompleteResult; } -// struct StandardEnv; - pub trait ReadEnv { fn get(&self, key: impl AsRef) -> Option<&str>; } @@ -262,6 +260,53 @@ impl From> for Option { } } +#[derive( + Serialize, + Deserialize, + Ord, + PartialOrd, + Eq, + PartialEq, + derive_more::From, + Clone, + derive_more::Deref, + derive_more::DerefMut, +)] +pub struct UserField(Option); + +impl Debug for UserField { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +impl Default for UserField { + fn default() -> Self { + Self(None) + } +} + +impl Merge for UserField { + fn merge(&mut self, mut other: Self) { + if let Some(value) = other.0 { + self.0 = Some(value) + } + } +} + +impl UserField { + pub fn get(self) -> Option { + self.0 + } +} + +impl From> for UserField { + fn from(value: ParseEnvResult) -> Self { + let option: Option = value.into(); + option.into() + } +} + #[cfg(test)] mod tests { use super::*; @@ -288,4 +333,19 @@ mod tests { assert_eq!(format!("{err}"), "Missing field: foo\nMissing field: bar") } + + #[test] + fn merging_user_fields_overrides_old_value() { + let mut field = UserField(None); + field.merge(UserField(Some(4))); + assert_eq!(field, UserField(Some(4))); + + let mut field = UserField(Some(4)); + field.merge(UserField(Some(5))); + assert_eq!(field, UserField(Some(5))); + + let mut field = UserField(Some(4)); + field.merge(UserField(None)); + assert_eq!(field, UserField(Some(4))); + } } diff --git a/config/src/parameters/chain_wide.rs b/config/src/parameters/chain_wide.rs index b5e327e0dde..7b7cc29fbcf 100644 --- a/config/src/parameters/chain_wide.rs +++ b/config/src/parameters/chain_wide.rs @@ -9,12 +9,13 @@ use std::{ }; use iroha_data_model::{prelude::MetadataLimits, transaction::TransactionLimits, LengthLimits}; +use merge::Merge; use nonzero_ext::nonzero; use serde::{Deserialize, Serialize}; use crate::{ ByteSize, Complete, CompleteError, CompleteResult, FromEnv, FromEnvDefaultFallback, - FromEnvResult, ReadEnv, UserDuration, + FromEnvResult, ReadEnv, UserDuration, UserField, }; const DEFAULT_MAX_TXS: NonZeroU32 = nonzero!(2_u32.pow(9)); @@ -36,20 +37,20 @@ const DEFAULT_MAX_WASM_SIZE_BYTES: u64 = 4 * 2_u64.pow(20); // 4 MiB const DEFAULT_TRANSACTION_LIMITS: TransactionLimits = TransactionLimits::new(DEFAULT_MAX_INSTRUCTION_NUMBER, DEFAULT_MAX_WASM_SIZE_BYTES); -#[derive(Deserialize, Serialize, Debug, Default)] -#[serde(deny_unknown_fields)] +#[derive(Deserialize, Serialize, Debug, Default, Merge)] +#[serde(deny_unknown_fields, default)] pub struct UserLayer { - pub max_transactions_in_block: Option, - pub block_time: Option, - pub commit_time: Option, - pub transactions_limits: Option, - pub asset_metadata_limits: Option, - pub asset_definition_metadata_limits: Option, - pub account_metadata_limits: Option, - pub domain_metadata_limits: Option, - pub identifier_length_limits: Option, - pub wasm_fuel_limit: Option, - pub wasm_max_memory: Option, + pub max_transactions_in_block: UserField, + pub block_time: UserField, + pub commit_time: UserField, + pub transactions_limits: UserField, + pub asset_metadata_limits: UserField, + pub asset_definition_metadata_limits: UserField, + pub account_metadata_limits: UserField, + pub domain_metadata_limits: UserField, + pub identifier_length_limits: UserField, + pub wasm_fuel_limit: UserField, + pub wasm_max_memory: UserField, } #[derive(Debug)] diff --git a/config/src/parameters/genesis.rs b/config/src/parameters/genesis.rs index 998c4ee92be..9ab5e0689a6 100644 --- a/config/src/parameters/genesis.rs +++ b/config/src/parameters/genesis.rs @@ -1,22 +1,24 @@ //! Module with genesis configuration logic. -use std::path::PathBuf; +use std::{ops::Deref, path::PathBuf}; use eyre::{eyre, Context, Report}; use iroha_crypto::{KeyPair, PrivateKey, PublicKey}; use iroha_genesis::RawGenesisBlock; +use merge::Merge; use serde::{Deserialize, Serialize}; use crate::{ Complete, CompleteError, CompleteResult, Emitter, FromEnv, FromEnvDefaultFallback, - FromEnvResult, ParseEnvResult, ReadEnv, + FromEnvResult, ParseEnvResult, ReadEnv, UserField, }; -#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Default)] -#[serde(deny_unknown_fields)] +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Default, Merge)] +#[serde(deny_unknown_fields, default)] pub struct UserLayer { - pub public_key: Option, - pub private_key: Option, - pub file: Option, + pub public_key: UserField, + pub private_key: UserField, + #[serde(default)] + pub file: UserField, } #[derive(Debug)] @@ -41,9 +43,10 @@ impl Complete for UserLayer { fn complete(self) -> CompleteResult { let public_key = self .public_key + .get() .ok_or_else(|| CompleteError::missing_field("genesis.public_key"))?; - match (self.private_key, self.file) { + match (self.private_key.get(), self.file.get()) { (None, None) => Ok(Config::Partial { public_key }), (Some(private_key), Some(file)) => Ok(Config::Full { key_pair: KeyPair::new(public_key, private_key) diff --git a/config/src/parameters/iroha.rs b/config/src/parameters/iroha.rs index 78eed202d90..6458cb28fd2 100644 --- a/config/src/parameters/iroha.rs +++ b/config/src/parameters/iroha.rs @@ -6,29 +6,30 @@ use eyre::{eyre, Context, Report}; use iroha_crypto::{Algorithm, KeyPair, PrivateKey, PublicKey}; use iroha_data_model::ChainId; use iroha_primitives::addr::SocketAddr; +use merge::Merge; use serde::{Deserialize, Serialize}; use crate::{ Complete, CompleteError, CompleteResult, Emitter, FromEnv, FromEnvResult, ParseEnvResult, - ReadEnv, + ReadEnv, UserField, }; -#[derive(Deserialize, Serialize, Debug, Default)] -#[serde(deny_unknown_fields)] +#[derive(Deserialize, Serialize, Debug, Default, Merge)] +#[serde(deny_unknown_fields, default)] pub struct UserLayer { pub chain_id: Option, - pub public_key: Option, - pub private_key: Option, - pub p2p_address: Option, + pub public_key: UserField, + pub private_key: UserField, + pub p2p_address: UserField, } impl Complete for UserLayer { type Output = Config; fn complete(self) -> CompleteResult { - let mut emitter = super::Emitter::::new(); + let mut emitter = Emitter::::new(); - let key_pair = match (self.public_key, self.private_key) { + let key_pair = match (self.public_key.get(), self.private_key.get()) { (Some(public_key), Some(private_key)) => { KeyPair::new(public_key, private_key) .map(Some) @@ -57,7 +58,7 @@ impl Complete for UserLayer { Ok(Config { key_pair: key_pair.unwrap(), - p2p_address: self.p2p_address.unwrap(), + p2p_address: self.p2p_address.get().unwrap(), }) } } @@ -166,6 +167,7 @@ mod tests { let private_key = UserLayer::from_env(&env) .expect("input is valid, should not fail") .private_key + .get() .expect("private key is provided, should not fail"); assert_eq!(private_key.digest_function(), "ed25519".parse().unwrap()); @@ -180,7 +182,7 @@ mod tests { `PRIVATE_KEY_DIGEST` env was provided, but `PRIVATE_KEY_PAYLOAD` was not Location: - config/src/parameters/iroha.rs:99:26"#]]; + config/src/parameters/iroha.rs:100:26"#]]; expected.assert_eq(&format!("{error:?}")); } @@ -192,7 +194,7 @@ mod tests { `PRIVATE_KEY_PAYLOAD` env was provided, but `PRIVATE_KEY_DIGEST` was not Location: - config/src/parameters/iroha.rs:107:26"#]]; + config/src/parameters/iroha.rs:108:26"#]]; expected.assert_eq(&format!("{error:?}")); } @@ -211,7 +213,7 @@ mod tests { Key could not be parsed. Odd number of digits Location: - config/src/parameters/iroha.rs:81:18"#]]; + config/src/parameters/iroha.rs:82:18"#]]; expected.assert_eq(&format!("{error:?}")); } @@ -231,7 +233,7 @@ mod tests { Algorithm not supported Location: - config/src/lib.rs:239:14"#]]; + config/src/lib.rs:237:14"#]]; expected.assert_eq(&format!("{error:?}")); } } diff --git a/config/src/parameters/kura.rs b/config/src/parameters/kura.rs index 5af12d91aa6..c71a0521bbf 100644 --- a/config/src/parameters/kura.rs +++ b/config/src/parameters/kura.rs @@ -2,6 +2,7 @@ use std::{fmt::Display, path::PathBuf, str::FromStr}; +use merge::Merge; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use crate::{Complete, CompleteResult, Emitter, FromEnv, FromEnvResult, ParseEnvResult, ReadEnv}; @@ -9,15 +10,15 @@ use crate::{Complete, CompleteResult, Emitter, FromEnv, FromEnvResult, ParseEnvR const DEFAULT_BLOCK_STORE_PATH: &str = "./storage"; /// `Kura` configuration. -#[derive(Clone, Deserialize, Serialize, Debug, Default)] -#[serde(deny_unknown_fields)] +#[derive(Clone, Deserialize, Serialize, Debug, Default, Merge)] +#[serde(deny_unknown_fields, default)] pub struct UserLayer { pub init_mode: Option, pub block_store_path: Option, pub debug: DebugUserConfig, } -#[derive(Clone, Deserialize, Serialize, Debug, Default)] +#[derive(Clone, Deserialize, Serialize, Debug, Default, Merge)] pub struct DebugUserConfig { output_new_blocks: Option, } diff --git a/config/src/parameters/logger.rs b/config/src/parameters/logger.rs index c999a651508..fcf5411d14c 100644 --- a/config/src/parameters/logger.rs +++ b/config/src/parameters/logger.rs @@ -5,12 +5,13 @@ use core::fmt::Debug; pub use iroha_data_model::Level; #[cfg(feature = "tokio-console")] use iroha_primitives::addr::{socket_addr, SocketAddr}; +use merge::Merge; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use crate::{ util::{impl_deserialize_from_str, impl_serialize_display}, Complete, CompleteError, CompleteResult, Emitter, FromEnv, FromEnvResult, ParseEnvResult, - ReadEnv, + ReadEnv, UserField, }; #[cfg(feature = "tokio-console")] @@ -28,18 +29,18 @@ pub fn into_tracing_level(level: Level) -> tracing::Level { } /// 'Logger' configuration. -#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Default)] +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Default, Merge)] // `tokio_console_addr` is not `Copy`, but warning appears without `tokio-console` feature #[allow(missing_copy_implementations)] -#[serde(deny_unknown_fields)] +#[serde(deny_unknown_fields, default)] pub struct UserLayer { /// Level of logging verbosity - pub level: Option, + pub level: UserField, /// Output format - pub format: Option, + pub format: UserField, #[cfg(feature = "tokio-console")] /// Address of tokio console (only available under "tokio-console" feature) - pub tokio_console_addr: Option, + pub tokio_console_addr: UserField, } #[derive(Debug)] diff --git a/config/src/parameters/mod.rs b/config/src/parameters/mod.rs index ec9a7aa1a9e..46e72b3b619 100644 --- a/config/src/parameters/mod.rs +++ b/config/src/parameters/mod.rs @@ -7,6 +7,7 @@ use std::{ }; use eyre::{eyre, Context, Report, Result}; +use merge::Merge; use serde::{Deserialize, Serialize}; use crate::{Complete, CompleteError, CompleteResult, Emitter, FromEnv, FromEnvResult, ReadEnv}; @@ -22,8 +23,8 @@ pub mod sumeragi; pub mod telemetry; pub mod torii; -#[derive(Deserialize, Serialize, Debug, Default)] -#[serde(deny_unknown_fields)] +#[derive(Deserialize, Serialize, Debug, Default, Merge)] +#[serde(deny_unknown_fields, default)] pub struct UserLayer { #[serde(default)] iroha: iroha::UserLayer, @@ -83,9 +84,10 @@ impl UserLayer { patch!(self.telemetry.dev.file); } - #[must_use] - pub fn merge(self, _other: Self) -> Self { - todo!() + // FIXME workaround the inconvenient way `Merge::merge` works + pub fn merge_chain(mut self, other: Self) -> Self { + self.merge(other); + self } } @@ -193,3 +195,24 @@ pub struct Config { pub torii: torii::Config, pub chain_wide: chain_wide::Config, } + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn deserialize_empty_input_works() { + let _layer: UserLayer = toml::from_str("").unwrap(); + } + + #[test] + fn deserialize_iroha_namespace_with_not_all_fields_works() { + let _layer: UserLayer = toml::from_str( + r#" + [iroha] + p2p_address = "127.0.0.1:8080" + "#, + ) + .unwrap(); + } +} diff --git a/config/src/parameters/queue.rs b/config/src/parameters/queue.rs index 010602b0d1b..73f5f274543 100644 --- a/config/src/parameters/queue.rs +++ b/config/src/parameters/queue.rs @@ -4,12 +4,13 @@ use std::{ time::Duration, }; +use merge::Merge; use nonzero_ext::nonzero; use serde::{Deserialize, Serialize}; use crate::{ Complete, CompleteError, CompleteResult, FromEnv, FromEnvDefaultFallback, FromEnvResult, - ReadEnv, UserDuration, + ReadEnv, UserDuration, UserField, }; const DEFAULT_MAX_TRANSACTIONS_IN_QUEUE: NonZeroU32 = nonzero!(2_u32.pow(16)); @@ -17,18 +18,18 @@ const DEFAULT_MAX_TRANSACTIONS_IN_QUEUE_PER_USER: NonZeroU32 = nonzero!(2_u32.po const DEFAULT_TRANSACTION_TIME_TO_LIVE: Duration = Duration::from_secs(24 * 60 * 60); // 24 hours const DEFAULT_FUTURE_THRESHOLD: Duration = Duration::from_secs(1); -#[derive(Deserialize, Serialize, Debug, Default)] -#[serde(deny_unknown_fields)] +#[derive(Deserialize, Serialize, Debug, Default, Merge)] +#[serde(deny_unknown_fields, default)] pub struct UserLayer { /// The upper limit of the number of transactions waiting in the queue. - pub max_transactions_in_queue: Option, + pub max_transactions_in_queue: UserField, /// The upper limit of the number of transactions waiting in the queue for single user. /// Use this option to apply throttling. - pub max_transactions_in_queue_per_user: Option, + pub max_transactions_in_queue_per_user: UserField, /// The transaction will be dropped after this time if it is still in the queue. - pub transaction_time_to_live_ms: Option, + pub transaction_time_to_live_ms: UserField, /// The threshold to determine if a transaction has been tampered to have a future timestamp. - pub future_threshold_ms: Option, + pub future_threshold_ms: UserField, } /// `Queue` configuration. diff --git a/config/src/parameters/snapshot.rs b/config/src/parameters/snapshot.rs index 195fdd3c2e0..cfd871c354c 100644 --- a/config/src/parameters/snapshot.rs +++ b/config/src/parameters/snapshot.rs @@ -2,11 +2,12 @@ use std::{path::PathBuf, time::Duration}; +use merge::Merge; use serde::{Deserialize, Serialize}; use crate::{ Complete, CompleteError, CompleteResult, Emitter, FromEnv, FromEnvResult, ParseEnvResult, - ReadEnv, UserDuration, + ReadEnv, UserDuration, UserField, }; const DEFAULT_SNAPSHOT_PATH: &str = "./storage"; @@ -14,15 +15,15 @@ const DEFAULT_SNAPSHOT_PATH: &str = "./storage"; const DEFAULT_SNAPSHOT_CREATE_EVERY_MS: Duration = Duration::from_secs(60); const DEFAULT_ENABLED: bool = true; -#[derive(Debug, Clone, Deserialize, Serialize, Default)] -#[serde(deny_unknown_fields)] +#[derive(Debug, Clone, Deserialize, Serialize, Default, Merge)] +#[serde(deny_unknown_fields, default)] pub struct UserLayer { /// The period of time to wait between attempts to create new snapshot. - pub create_every_ms: Option, + pub create_every_ms: UserField, /// Path to the directory where snapshots should be stored - pub store_path: Option, + pub store_path: UserField, /// Flag to enable or disable snapshot creation - pub creation_enabled: Option, + pub creation_enabled: UserField, } #[derive(Debug)] @@ -40,9 +41,11 @@ impl Complete for UserLayer { creation_enabled: self.creation_enabled.unwrap_or(DEFAULT_ENABLED), create_every_ms: self .create_every_ms + .get() .map_or(DEFAULT_SNAPSHOT_CREATE_EVERY_MS, UserDuration::get), store_path: self .store_path + .get() .unwrap_or_else(|| PathBuf::from(DEFAULT_SNAPSHOT_PATH)), }) } diff --git a/config/src/parameters/sumeragi.rs b/config/src/parameters/sumeragi.rs index 4ba2a9d039f..223527c336f 100644 --- a/config/src/parameters/sumeragi.rs +++ b/config/src/parameters/sumeragi.rs @@ -1,12 +1,16 @@ //! `Sumeragi` configuration. Contains both block commit and Gossip-related configuration. use std::{fmt::Debug, fs::File, io::BufReader, num::NonZeroU32, path::Path, time::Duration}; +use eyre::eyre; use iroha_data_model::prelude::*; use iroha_primitives::unique_vec::UniqueVec; +use merge::Merge; use serde::{Deserialize, Serialize}; use self::default::*; -use crate::{Complete, CompleteError, CompleteResult, FromEnvDefaultFallback, UserDuration}; +use crate::{ + Complete, CompleteError, CompleteResult, FromEnvDefaultFallback, UserDuration, UserField, +}; /// Module with a set of default values. pub mod default { @@ -31,14 +35,14 @@ pub mod default { // DEFAULT_BLOCK_TIME_MS + (DEFAULT_COMMIT_TIME_LIMIT_MS / 2); } -#[derive(Deserialize, Serialize, Debug, Default)] -#[serde(deny_unknown_fields)] +#[derive(Deserialize, Serialize, Debug, Default, Merge)] +#[serde(deny_unknown_fields, default)] pub struct UserLayer { - pub block_gossip_period: Option, - pub max_blocks_per_gossip: Option, - pub max_transactions_per_gossip: Option, - pub transaction_gossip_period: Option, - pub trusted_peers: Option, + pub block_gossip_period: UserField, + pub max_blocks_per_gossip: UserField, + pub max_transactions_per_gossip: UserField, + pub transaction_gossip_period: UserField, + pub trusted_peers: UserTrustedPeers, } impl Complete for UserLayer { @@ -58,7 +62,10 @@ impl Complete for UserLayer { transaction_gossip_period: self .transaction_gossip_period .map_or(DEFAULT_TRANSACTION_GOSSIP_PERIOD, UserDuration::get), - trusted_peers: self.trusted_peers.unwrap_or_default(), + trusted_peers: TrustedPeers { + peers: construct_unique_vec(self.trusted_peers.peers) + .map_err(CompleteError::Custom)?, + }, }) } } @@ -72,17 +79,36 @@ pub struct Config { pub trusted_peers: TrustedPeers, } -/// Part of the [`Configuration`]. It is separated from the main structure in order to be able -/// to load it from a separate file (see [`TrustedPeers::from_path`]). -#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize, Serialize)] -#[serde(rename_all = "UPPERCASE")] -#[serde(transparent)] -#[repr(transparent)] +#[derive(Debug)] pub struct TrustedPeers { - /// Optional list of predefined trusted peers. Must contain unique - /// entries. Custom deserializer raises error if duplicates found. - #[serde(deserialize_with = "UniqueVec::display_deserialize_failing_on_duplicates")] pub peers: UniqueVec, } +#[derive(Deserialize, Serialize, Default, PartialEq, Eq, Debug, Clone)] +#[serde(transparent)] +pub struct UserTrustedPeers { + // FIXME: doesn't raise an error on finding duplicates during deserialization + pub peers: Vec, +} + +impl Merge for UserTrustedPeers { + fn merge(&mut self, mut other: Self) { + self.peers.append(other.peers.as_mut()) + } +} + impl FromEnvDefaultFallback for UserLayer {} + +// FIXME: handle duplicates properly, not here, and with details +fn construct_unique_vec( + unchecked: Vec, +) -> Result, eyre::Report> { + let mut unique = UniqueVec::new(); + for x in unchecked.into_iter() { + let pushed = unique.push(x); + if !pushed { + Err(eyre!("found duplicate"))? + } + } + Ok(unique) +} diff --git a/config/src/parameters/telemetry.rs b/config/src/parameters/telemetry.rs index c870dedfdc4..6d5a1a49b67 100644 --- a/config/src/parameters/telemetry.rs +++ b/config/src/parameters/telemetry.rs @@ -6,6 +6,7 @@ use std::{ }; use eyre::eyre; +use merge::Merge; use serde::{Deserialize, Serialize}; use url::Url; @@ -14,29 +15,29 @@ use crate::{ DEFAULT_MAX_RETRY_DELAY_EXPONENT, DEFAULT_MIN_RETRY_PERIOD, }, Complete, CompleteError, CompleteResult, FromEnv, FromEnvDefaultFallback, FromEnvResult, - ReadEnv, UserDuration, + ReadEnv, UserDuration, UserField, }; -#[derive(Clone, Deserialize, Serialize, Debug, Default)] -#[serde(deny_unknown_fields)] +#[derive(Clone, Deserialize, Serialize, Debug, Default, Merge)] +#[serde(deny_unknown_fields, default)] pub struct UserLayer { /// The node's name to be seen on the telemetry - pub name: Option, + pub name: UserField, /// The url of the telemetry, e.g., ws://127.0.0.1:8001/submit - pub url: Option, + pub url: UserField, /// The minimum period of time in seconds to wait before reconnecting - pub min_retry_period: Option, + pub min_retry_period: UserField, /// The maximum exponent of 2 that is used for increasing delay between reconnections - pub max_retry_delay_exponent: Option, + pub max_retry_delay_exponent: UserField, /// Dev telemetry configuration #[serde(default)] - pub dev: UserDevConfig, + pub dev: DevUserLayer, } -#[derive(Clone, Deserialize, Serialize, Debug, Default)] -pub struct UserDevConfig { +#[derive(Clone, Deserialize, Serialize, Debug, Default, Merge)] +pub struct DevUserLayer { /// The filepath that to write dev-telemetry to - pub file: Option, + pub file: UserField, } #[derive(Debug)] @@ -74,16 +75,18 @@ impl Complete for UserLayer { url, max_retry_delay_exponent, min_retry_period, - dev: UserDevConfig { file }, + dev: DevUserLayer { file }, } = self; - let regular = match (name, url) { + let regular = match (name.get(), url.get()) { (Some(name), Some(url)) => Some(RegularTelemetryConfig { name, url, max_retry_delay_exponent: max_retry_delay_exponent + .get() .unwrap_or(DEFAULT_MAX_RETRY_DELAY_EXPONENT), min_retry_period: min_retry_period + .get() .map_or(DEFAULT_MIN_RETRY_PERIOD, UserDuration::get), }), (None, None) => None, diff --git a/config/src/parameters/torii.rs b/config/src/parameters/torii.rs index d6baa7232d8..e9c2bc41ff4 100644 --- a/config/src/parameters/torii.rs +++ b/config/src/parameters/torii.rs @@ -3,22 +3,23 @@ use std::time::Duration; use iroha_primitives::addr::{socket_addr, SocketAddr}; +use merge::Merge; use serde::{Deserialize, Serialize}; use crate::{ ByteSize, Complete, CompleteError, CompleteResult, Emitter, FromEnv, FromEnvResult, - ParseEnvResult, ReadEnv, UserDuration, + ParseEnvResult, ReadEnv, UserDuration, UserField, }; const DEFAULT_MAX_CONTENT_LENGTH: u64 = 2_u64.pow(20) * 16; const DEFAULT_QUERY_IDLE_TIME: Duration = Duration::from_secs(30); -#[derive(Debug, Clone, Deserialize, Serialize, Default)] -#[serde(deny_unknown_fields)] +#[derive(Debug, Clone, Deserialize, Serialize, Default, Merge)] +#[serde(deny_unknown_fields, default)] pub struct UserLayer { - pub address: Option, - pub max_content_len: Option, - pub query_idle_time: Option, + pub address: UserField, + pub max_content_len: UserField, + pub query_idle_time: UserField, } #[derive(Debug)] @@ -35,9 +36,11 @@ impl Complete for UserLayer { Ok(Config { address: self .address + .get() .ok_or_else(|| CompleteError::missing_field("torii.address"))?, max_content_len: self .max_content_len + .get() .unwrap_or(ByteSize(DEFAULT_MAX_CONTENT_LENGTH)), query_idle_time: self .query_idle_time diff --git a/config/tests/fixtures.rs b/config/tests/fixtures.rs index e3c8b9335bf..3bded6eebf0 100644 --- a/config/tests/fixtures.rs +++ b/config/tests/fixtures.rs @@ -1,7 +1,7 @@ use std::{ collections::{HashMap, HashSet}, fs, - path::PathBuf, + path::{Path, PathBuf}, }; use eyre::Result; @@ -28,6 +28,12 @@ fn parse_env(raw: impl AsRef) -> HashMap { .collect() } +fn test_env_from_file(p: impl AsRef) -> TestEnv { + let contents = fs::read_to_string(p).expect("the path should be valid"); + let map = parse_env(contents); + TestEnv::with_map(map) +} + /// This test not only asserts that the minimal set of fields is enough; /// it also gives an insight into every single default value #[test] @@ -180,13 +186,8 @@ fn inconsistent_genesis_config() -> Result<()> { /// Aims the purpose of checking that every single provided env variable is consumed and parsed /// into a valid config. #[test] -fn full_env_config() -> Result<()> { - let env = { - let path = fixtures_dir().join(".full_config_in.env"); - let contents = fs::read_to_string(path)?; - let map = parse_env(contents); - TestEnv::with_map(map) - }; +fn full_envs_set_is_consumed() -> Result<()> { + let env = test_env_from_file(fixtures_dir().join("full.env")); let layer = UserLayer::from_env(&env)?; @@ -232,7 +233,9 @@ fn full_env_config() -> Result<()> { max_blocks_per_gossip: None, max_transactions_per_gossip: None, transaction_gossip_period: None, - trusted_peers: None, + trusted_peers: UserTrustedPeers { + peers: [], + }, }, logger: UserLayer { level: Some( @@ -262,7 +265,7 @@ fn full_env_config() -> Result<()> { url: None, min_retry_period: None, max_retry_delay_exponent: None, - dev: UserDevConfig { + dev: DevUserLayer { file: None, }, }, @@ -297,3 +300,14 @@ fn full_env_config() -> Result<()> { fn multiple_env_parsing_errors() { todo!("put invalid data into multiple ENV variables in different modules and check the error report") } + +#[test] +fn config_from_file_and_env() -> Result<()> { + let env = test_env_from_file(fixtures_dir().join("config_and_env.env")); + + let _config = UserLayer::from_toml(fixtures_dir().join("config_and_env.toml"))? + .merge_chain(UserLayer::from_env(&env)?) + .complete()?; + + Ok(()) +} diff --git a/config/tests/fixtures/config_and_env.env b/config/tests/fixtures/config_and_env.env new file mode 100644 index 00000000000..7ee9d329ee5 --- /dev/null +++ b/config/tests/fixtures/config_and_env.env @@ -0,0 +1 @@ +API_ADDRESS=127.0.0.1:8080 \ No newline at end of file diff --git a/config/tests/fixtures/config_and_env.toml b/config/tests/fixtures/config_and_env.toml new file mode 100644 index 00000000000..0fcc69fa88e --- /dev/null +++ b/config/tests/fixtures/config_and_env.toml @@ -0,0 +1,12 @@ +[iroha] +public_key = "ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB" +p2p_address = "127.0.0.1:1337" + +[iroha.private_key] +digest_function = "ed25519" +payload = "8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb" + +[genesis] +public_key = "ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB" + +# `torii.address` should be in ENV \ No newline at end of file diff --git a/config/tests/fixtures/.full_config_in.env b/config/tests/fixtures/full.env similarity index 100% rename from config/tests/fixtures/.full_config_in.env rename to config/tests/fixtures/full.env From df36c7b62b48df5b4c129bc58cf125acd9456184 Mon Sep 17 00:00:00 2001 From: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> Date: Mon, 15 Jan 2024 18:22:54 +0900 Subject: [PATCH 06/94] [refactor]: update Kagami - Remove `config` subcommand - Update default `genesis` building Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- .github/workflows/iroha2-dev-pr.yml | 6 -- scripts/tests/consistency.sh | 10 --- tools/kagami/src/config.rs | 95 ----------------------------- tools/kagami/src/genesis.rs | 12 ++-- tools/kagami/src/main.rs | 4 -- 5 files changed, 6 insertions(+), 121 deletions(-) diff --git a/.github/workflows/iroha2-dev-pr.yml b/.github/workflows/iroha2-dev-pr.yml index 1d5526cff59..bdaddbf774a 100644 --- a/.github/workflows/iroha2-dev-pr.yml +++ b/.github/workflows/iroha2-dev-pr.yml @@ -28,12 +28,6 @@ jobs: - name: Check genesis.json if: always() run: ./scripts/tests/consistency.sh genesis - - name: Check client/config.json - if: always() - run: ./scripts/tests/consistency.sh client - - name: Check peer/config.json - if: always() - run: ./scripts/tests/consistency.sh peer - name: Check schema.json if: always() run: ./scripts/tests/consistency.sh schema diff --git a/scripts/tests/consistency.sh b/scripts/tests/consistency.sh index 190024d2135..64305cbbb3e 100755 --- a/scripts/tests/consistency.sh +++ b/scripts/tests/consistency.sh @@ -7,16 +7,6 @@ case $1 in echo 'Please re-generate the genesis with `cargo run --release --bin kagami -- genesis --executor-path-in-genesis ./executor.wasm > configs/peer/genesis.json`' exit 1 };; - "client") - cargo run --release --bin kagami -- config client | diff - configs/client/config.json || { - echo 'Please re-generate client config with `cargo run --release --bin kagami -- config client > configs/client/config.json`' - exit 1 - };; - "peer") - cargo run --release --bin kagami -- config peer | diff - configs/peer/config.json || { - echo 'Please re-generate peer config with `cargo run --release --bin kagami -- config peer > configs/peer/config.json`' - exit 1 - };; "schema") cargo run --release --bin kagami -- schema | diff - docs/source/references/schema.json || { echo 'Please re-generate schema with `cargo run --release --bin kagami -- schema > docs/source/references/schema.json`' diff --git a/tools/kagami/src/config.rs b/tools/kagami/src/config.rs index d194fca3336..8b137891791 100644 --- a/tools/kagami/src/config.rs +++ b/tools/kagami/src/config.rs @@ -1,96 +1 @@ -use std::str::FromStr as _; -use clap::{Parser, Subcommand}; -use iroha_crypto::{Algorithm, PrivateKey, PublicKey}; -use iroha_primitives::small::SmallStr; - -use super::*; - -#[derive(Parser, Debug, Clone)] -pub struct Args { - #[clap(subcommand)] - mode: Mode, -} - -#[derive(Subcommand, Debug, Clone)] -pub enum Mode { - Client(client::Args), - Peer(peer::Args), -} - -impl RunArgs for Args { - fn run(self, writer: &mut BufWriter) -> Outcome { - match self.mode { - Mode::Client(args) => args.run(writer), - Mode::Peer(args) => args.run(writer), - } - } -} - -mod client { - use iroha_config::{ - r#mod::{BasicAuth, ConfigurationProxy, WebLogin}, - torii::uri::DEFAULT_API_ADDR, - }; - - use super::*; - - #[derive(ClapArgs, Debug, Clone, Copy)] - pub struct Args; - - impl RunArgs for Args { - fn run(self, writer: &mut BufWriter) -> Outcome { - let config = ConfigurationProxy { - chain_id: Some(ChainId::new("00000000-0000-0000-0000-000000000000")), - torii_api_url: Some(format!("http://{DEFAULT_API_ADDR}").parse()?), - account_id: Some("alice@wonderland".parse()?), - basic_auth: Some(Some(BasicAuth { - web_login: WebLogin::new("mad_hatter")?, - password: SmallStr::from_str("ilovetea"), - })), - public_key: Some(PublicKey::from_str( - "ed01207233BFC89DCBD68C19FDE6CE6158225298EC1131B6A130D1AEB454C1AB5183C0", - )?), - private_key: Some(PrivateKey::from_hex( - Algorithm::Ed25519, - "9AC47ABF59B356E0BD7DCBBBB4DEC080E302156A48CA907E47CB6AEA1D32719E7233BFC89DCBD68C19FDE6CE6158225298EC1131B6A130D1AEB454C1AB5183C0" - )?), - ..ConfigurationProxy::default() - } - .build()?; - writeln!(writer, "{}", serde_json::to_string_pretty(&config)?) - .wrap_err("Failed to write serialized client configuration to the buffer.") - } - } -} - -mod peer { - use std::path::PathBuf; - - use iroha_config::iroha::ConfigurationProxy as IrohaConfigurationProxy; - - use super::*; - - #[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 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/kagami/src/genesis.rs b/tools/kagami/src/genesis.rs index b873dd85c4c..f98d772f0ff 100644 --- a/tools/kagami/src/genesis.rs +++ b/tools/kagami/src/genesis.rs @@ -1,7 +1,7 @@ use std::path::PathBuf; use clap::{ArgGroup, Parser, Subcommand}; -use iroha_config::{sumeragi::default::*, wasm::default::*, wsv::default::*}; +use iroha_config::parameters::chain_wide::*; use iroha_data_model::{ asset::AssetValueType, metadata::Limits, @@ -175,9 +175,9 @@ pub fn generate_default(executor: ExecutorMode) -> color_eyre::Result color_eyre::Result RunArgs for Args { @@ -63,7 +60,6 @@ impl RunArgs for Args { Crypto(args) => args.run(writer), Schema(args) => args.run(writer), Genesis(args) => args.run(writer), - Config(args) => args.run(writer), } } } From 22cef0d3a4d4a3434b48438fb9179d1f8a5d187f Mon Sep 17 00:00:00 2001 From: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> Date: Mon, 15 Jan 2024 18:25:15 +0900 Subject: [PATCH 07/94] [refactor]: restructure code - move generic tools to `iroha_config_base` - remove `iroha_client_config` crate - define client config in `iroha_client::config` - add client config sample TOML Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- Cargo.lock | 28 +- Cargo.toml | 2 - .../src/client/mod.rs => client/src/config.rs | 161 ++++---- client/src/lib.rs | 7 +- client_config/Cargo.toml | 19 - client_config/src/lib.rs | 14 - config/Cargo.toml | 1 - config/base/Cargo.toml | 15 +- config/base/src/lib.rs | 378 +++++++++++++++++- config/src/lib.rs | 345 +--------------- config/src/parameters/chain_wide.rs | 31 +- config/src/parameters/genesis.rs | 10 +- config/src/parameters/iroha.rs | 13 +- config/src/parameters/kura.rs | 11 +- config/src/parameters/logger.rs | 12 +- config/src/parameters/mod.rs | 8 +- config/src/parameters/queue.rs | 10 +- config/src/parameters/snapshot.rs | 10 +- config/src/parameters/sumeragi.rs | 7 +- config/src/parameters/telemetry.rs | 20 +- config/src/parameters/torii.rs | 10 +- config/src/util.rs | 30 -- config/tests/fixtures.rs | 3 +- configs/client/config.example.toml | 20 + 24 files changed, 559 insertions(+), 606 deletions(-) rename client_config/src/client/mod.rs => client/src/config.rs (63%) delete mode 100644 client_config/Cargo.toml delete mode 100644 client_config/src/lib.rs delete mode 100644 config/src/util.rs create mode 100644 configs/client/config.example.toml diff --git a/Cargo.lock b/Cargo.lock index 27926862e2a..55df87cc21f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1031,20 +1031,6 @@ dependencies = [ "itertools 0.10.5", ] -[[package]] -name = "crossbeam" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2801af0d36612ae591caa9568261fddce32ce6e08a7275ea334a06a4ad021a2c" -dependencies = [ - "cfg-if", - "crossbeam-channel", - "crossbeam-deque", - "crossbeam-epoch", - "crossbeam-queue", - "crossbeam-utils", -] - [[package]] name = "crossbeam-channel" version = "0.5.9" @@ -2755,10 +2741,6 @@ dependencies = [ "vergen", ] -[[package]] -name = "iroha_client_config" -version = "2.0.0-pre-rc.20" - [[package]] name = "iroha_config" version = "2.0.0-pre-rc.20" @@ -2766,7 +2748,6 @@ dependencies = [ "cfg-if", "derive_more", "displaydoc", - "drop_bomb", "expect-test", "eyre", "hex", @@ -2797,14 +2778,11 @@ dependencies = [ name = "iroha_config_base" version = "2.0.0-pre-rc.20" dependencies = [ - "crossbeam", - "displaydoc", + "derive_more", + "drop_bomb", "eyre", - "iroha_crypto", - "json5", - "parking_lot", + "merge", "serde", - "serde_json", "thiserror", ] diff --git a/Cargo.toml b/Cargo.toml index 2468ec4f6d2..42bf1e42def 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,7 +28,6 @@ iroha_primitives_derive = { version = "=2.0.0-pre-rc.20", path = "primitives/der iroha_data_model = { version = "=2.0.0-pre-rc.20", path = "data_model", default-features = false } iroha_data_model_derive = { version = "=2.0.0-pre-rc.20", path = "data_model/derive" } iroha_client = { version = "=2.0.0-pre-rc.20", path = "client" } -iroha_client_config = { version = "=2.0.0-pre-rc.20", path = "client_config" } iroha_config = { version = "=2.0.0-pre-rc.20", path = "config" } iroha_config_base = { version = "=2.0.0-pre-rc.20", path = "config/base" } iroha_schema_gen = { version = "=2.0.0-pre-rc.20", path = "schema/gen" } @@ -206,7 +205,6 @@ members = [ "cli", "client", "client_cli", - "client_config", "config", "config/base", "core", diff --git a/client_config/src/client/mod.rs b/client/src/config.rs similarity index 63% rename from client_config/src/client/mod.rs rename to client/src/config.rs index bdf559ddcda..27b15ee27ad 100644 --- a/client_config/src/client/mod.rs +++ b/client/src/config.rs @@ -1,10 +1,10 @@ //! Module for client-related configuration and structs use core::str::FromStr; -use std::num::NonZeroU64; +use std::{num::NonZeroU64, time::Duration}; use derive_more::Display; use eyre::{Result, WrapErr}; -use iroha_config_base::derive::{Error as ConfigError, Proxy}; +use iroha_config::base::{UserDuration, UserField}; use iroha_crypto::prelude::*; use iroha_data_model::{prelude::*, ChainId}; use iroha_primitives::small::SmallStr; @@ -63,9 +63,7 @@ pub struct BasicAuth { } /// `Configuration` provides an ability to define client parameters such as `TORII_URL`. -#[derive(Debug, Clone, Deserialize, Serialize, Proxy, PartialEq, Eq)] -#[serde(rename_all = "UPPERCASE")] -#[config(env_prefix = "IROHA_")] +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] pub struct Configuration { /// Unique id of the blockchain. Used for simple replay attack protection. pub chain_id: ChainId, @@ -88,6 +86,89 @@ pub struct Configuration { pub add_transaction_nonce: bool, } +mod user_layers { + use iroha_config::base::{Complete, CompleteResult, Merge, UserDuration, UserField}; + use iroha_crypto::{PrivateKey, PublicKey}; + use iroha_data_model::account::AccountId; + use serde::{Deserialize, Deserializer}; + use url::Url; + + use crate::config::BasicAuth; + + #[derive(Debug, Clone, Deserialize, Eq, PartialEq)] + #[serde(deny_unknown_fields, default)] + pub struct Root { + pub account: Account, + pub api: Api, + pub transaction: Transaction, + } + + impl Complete for Root { + type Output = super::Config; + + fn complete(self) -> CompleteResult { + // TODO: tx timeout should be smaller than ttl + todo!() + } + } + + impl Merge for Root { + fn merge(&mut self, other: Self) { + todo!() + } + } + + #[derive(Debug, Clone, Deserialize, Eq, PartialEq)] + #[serde(deny_unknown_fields, default)] + pub struct Api { + pub torii_url: UserField, + pub basic_auth: UserField, + } + + #[derive(Debug, Clone, Deserialize, Eq, PartialEq)] + #[serde(deny_unknown_fields, default)] + pub struct Account { + pub id: UserField, + pub public_key: UserField, + pub private_key: UserField, + } + + #[derive(Debug, Clone, Deserialize, Eq, PartialEq)] + #[serde(deny_unknown_fields, default)] + pub struct Transaction { + pub time_to_live: UserField, + pub status_timeout: UserField, + pub add_nonce: UserField, + } + + #[derive(Debug, Clone, Eq, PartialEq)] + pub struct OnlyHttpUrl(Url); + + impl<'de> Deserialize<'de> for OnlyHttpUrl { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let url = Url::deserialize(deserializer)?; + if url.scheme() == "http" { + Err(serde::de::Error::custom("only HTTP is supported")) + } else { + Ok(Self(url)) + } + } + } +} + +pub struct Config { + pub account_id: AccountId, + pub key_pair: KeyPair, + pub basic_auth: Option, + pub torii_api_url: Url, + pub transaction_ttl: Duration, + pub transaction_status_timeout: Duration, + pub transaction_add_nonce: bool, +} + impl Default for ConfigurationProxy { fn default() -> Self { Self { @@ -164,73 +245,3 @@ impl ConfigurationProxy { .wrap_err("Failed to build `Configuration` from `ConfigurationProxy`") } } - -#[cfg(test)] -mod tests { - use iroha_config_base::proxy::LoadFromDisk; - use iroha_crypto::KeyGenConfiguration; - use proptest::prelude::*; - - use super::*; - use crate::torii::uri::DEFAULT_API_ADDR; - - const CONFIGURATION_PATH: &str = "../configs/client/config.json"; - - prop_compose! { - // TODO: make tests to check generated key validity - fn arb_keys_from_seed() - (seed in prop::collection::vec(any::(), 33..64)) -> (PublicKey, PrivateKey) { - let (public_key, private_key) = KeyPair::generate_with_configuration(KeyGenConfiguration::from_seed(seed)).expect("Seed was invalid").into(); - (public_key, private_key) - } - } - - prop_compose! { - fn arb_keys_with_option() - (keys in arb_keys_from_seed()) - ((a, b) in (prop::option::of(Just(keys.0)), prop::option::of(Just(keys.1)))) - -> (Option, Option) { - (a, b) - } - } - - fn placeholder_account() -> AccountId { - AccountId::from_str("alice@wonderland").expect("Invalid account Id ") - } - - prop_compose! { - fn arb_proxy() - ( - chain_id in prop::option::of(Just(crate::iroha::tests::placeholder_chain_id())), - (public_key, private_key) in arb_keys_with_option(), - account_id in prop::option::of(Just(placeholder_account())), - basic_auth in prop::option::of(Just(None)), - torii_api_url in prop::option::of(Just(format!("http://{DEFAULT_API_ADDR}").parse().unwrap())), - transaction_time_to_live_ms in prop::option::of(Just(Some(DEFAULT_TRANSACTION_TIME_TO_LIVE_MS))), - transaction_status_timeout_ms in prop::option::of(Just(DEFAULT_TRANSACTION_STATUS_TIMEOUT_MS)), - add_transaction_nonce in prop::option::of(Just(DEFAULT_ADD_TRANSACTION_NONCE)), - ) - -> ConfigurationProxy { - ConfigurationProxy { chain_id, public_key, private_key, account_id, basic_auth, torii_api_url, transaction_time_to_live_ms, transaction_status_timeout_ms, add_transaction_nonce } - } - } - - proptest! { - #[test] - fn client_proxy_build_fails_on_none(proxy in arb_proxy()) { - let cfg = proxy.build(); - if cfg.is_ok() { - let example_cfg = ConfigurationProxy::from_path(CONFIGURATION_PATH).build().expect("Failed to build example Iroha config. \ - This probably means that some of the fields of the `CONFIGURATION PATH` \ - JSON were not updated properly with new changes."); - let arb_cfg = cfg.expect("Config generated by proptest was checked to be ok by the surrounding if clause"); - // Skipping keys and `basic_auth` check as they're different from the file - assert_eq!(arb_cfg.torii_api_url, example_cfg.torii_api_url); - assert_eq!(arb_cfg.account_id, example_cfg.account_id); - assert_eq!(arb_cfg.transaction_time_to_live_ms, example_cfg.transaction_time_to_live_ms); - assert_eq!(arb_cfg.transaction_status_timeout_ms, example_cfg.transaction_status_timeout_ms); - assert_eq!(arb_cfg.add_transaction_nonce, example_cfg.add_transaction_nonce); - } - } - } -} diff --git a/client/src/lib.rs b/client/src/lib.rs index 6fb2e9094a7..50f8bc8f245 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -2,6 +2,7 @@ /// Module with iroha client itself pub mod client; +pub mod config; /// Module with general communication primitives like an HTTP request builder. pub mod http; mod http_default; @@ -39,11 +40,5 @@ pub mod samples { } } -pub mod config { - //! Module for client-related configuration and structs - - pub use iroha_config::{client_api as api, path, r#mod::*, torii::uri as torii}; -} - pub use iroha_crypto as crypto; pub use iroha_data_model as data_model; diff --git a/client_config/Cargo.toml b/client_config/Cargo.toml deleted file mode 100644 index a1891b4817e..00000000000 --- a/client_config/Cargo.toml +++ /dev/null @@ -1,19 +0,0 @@ -[package] -name = "iroha_client_config" -edition.workspace = true -version.workspace = true -authors.workspace = true -description.workspace = true -repository.workspace = true -documentation.workspace = true -homepage.workspace = true -license.workspace = true -keywords.workspace = true -categories.workspace = true - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] - -[lints] -workspace = true diff --git a/client_config/src/lib.rs b/client_config/src/lib.rs deleted file mode 100644 index 7d12d9af819..00000000000 --- a/client_config/src/lib.rs +++ /dev/null @@ -1,14 +0,0 @@ -pub fn add(left: usize, right: usize) -> usize { - left + right -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); - } -} diff --git a/config/Cargo.toml b/config/Cargo.toml index b2949669162..896ace64ace 100644 --- a/config/Cargo.toml +++ b/config/Cargo.toml @@ -21,7 +21,6 @@ eyre = { workspace = true } tracing = { workspace = true } tracing-subscriber = { workspace = true, features = ["fmt", "ansi"] } url = { workspace = true, features = ["serde"] } -drop_bomb = { workspace = true } serde = { workspace = true, features = ["derive"] } strum = { workspace = true, features = ["derive"] } diff --git a/config/base/Cargo.toml b/config/base/Cargo.toml index 763bcf58162..ae2fda8ccc7 100644 --- a/config/base/Cargo.toml +++ b/config/base/Cargo.toml @@ -11,14 +11,9 @@ license.workspace = true workspace = true [dependencies] -iroha_crypto = { workspace = true, features = ["std"] } - -serde = { workspace = true, default-features = false, features = ["derive"] } -serde_json = { workspace = true, features = ["alloc"] } -parking_lot = { workspace = true } -json5 = { workspace = true } -thiserror = { workspace = true } -displaydoc = { workspace = true } -crossbeam = { workspace = true } +merge = "0.1.0" +drop_bomb = { workspace = true } +derive_more = { workspace = true } eyre = { workspace = true } - +serde = { workspace = true } +thiserror = { workspace = true } diff --git a/config/base/src/lib.rs b/config/base/src/lib.rs index 7721c4ceaa2..0945608d059 100644 --- a/config/base/src/lib.rs +++ b/config/base/src/lib.rs @@ -1 +1,377 @@ -//! Base configuration utilities +//! Utilities behind Iroha configurations + +// FIXME +#![allow(missing_docs)] + +use std::{ + cell::RefCell, + collections::{HashMap, HashSet}, + error::Error, + fmt::{Debug, Display, Formatter}, + ops::Sub, + str::FromStr, + time::Duration, +}; + +use eyre::{eyre, Report, WrapErr}; +pub use merge::Merge; +pub use serde; +use serde::{Deserialize, Serialize}; + +#[macro_export] +macro_rules! impl_serialize_display { + ($ty:ty) => { + impl $crate::serde::Serialize for $ty { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: Serializer, + { + serializer.collect_str(self) + } + } + }; +} + +#[macro_export] +macro_rules! impl_deserialize_from_str { + ($ty:ty) => { + impl<'de> $crate::serde::Deserialize<'de> for $ty { + fn deserialize(deserializer: D) -> std::result::Result + where + D: Deserializer<'de>, + { + String::deserialize(deserializer)? + .parse() + .map_err($crate::serde::de::Error::custom) + } + } + }; +} + +/// User-provided duration +#[derive(Debug, Copy, Clone, Deserialize, Serialize)] +pub struct UserDuration(Duration); + +impl UserDuration { + pub fn get(self) -> Duration { + self.0 + } +} + +/// Byte size +#[derive(Debug, Copy, Clone, Deserialize, Serialize)] +pub struct ByteSize(pub u64); + +pub trait Complete { + type Output; + + fn complete(self) -> CompleteResult; +} + +pub trait ReadEnv { + fn get(&self, key: impl AsRef) -> Option<&str>; +} + +pub trait FromEnv { + fn from_env(env: &impl ReadEnv) -> FromEnvResult + where + Self: Sized; +} + +pub type FromEnvResult = eyre::Result>; + +pub trait FromEnvDefaultFallback {} + +impl FromEnv for T +where + T: FromEnvDefaultFallback + Default, +{ + fn from_env(_env: &impl ReadEnv) -> FromEnvResult + where + Self: Sized, + { + Ok(Self::default()) + } +} + +pub struct Emitter { + errors: Vec, + bomb: drop_bomb::DropBomb, +} + +impl Emitter { + pub fn new() -> Self { + Self { + errors: Vec::new(), + bomb: drop_bomb::DropBomb::new( + "Errors emitter is dropped without consuming collected errors", + ), + } + } + + pub fn emit(&mut self, error: T) { + self.errors.push(error); + } + + pub fn emit_collection(&mut self, mut errors: ErrorsCollection) { + self.errors.append(&mut errors.0); + } + + pub fn finish(mut self) -> eyre::Result<(), ErrorsCollection> { + self.bomb.defuse(); + + if self.errors.is_empty() { + Ok(()) + } else { + Err(ErrorsCollection(self.errors)) + } + } +} + +#[derive(thiserror::Error, Debug)] +pub enum CompleteError { + #[error("Missing field: {path}")] + MissingField { path: String }, + #[error(transparent)] + Custom(#[from] Report), +} + +pub type CompleteResult = eyre::Result>; + +impl CompleteError { + pub fn missing_field(field_name: impl AsRef) -> Self { + Self::MissingField { + path: field_name.as_ref().to_string(), + } + } +} + +impl Emitter { + pub fn emit_missing_field(&mut self, field_name: impl AsRef) { + self.emit(CompleteError::MissingField { + path: field_name.as_ref().to_string(), + }) + } +} + +pub struct ErrorsCollection(Vec); + +impl Error for ErrorsCollection {} + +impl Display for ErrorsCollection +where + T: Display, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + for (i, item) in self.0.iter().enumerate() { + if i > 0 { + writeln!(f)?; + } + write!(f, "{item}")?; + } + Ok(()) + } +} + +impl Debug for ErrorsCollection +where + T: Debug, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + for (i, item) in self.0.iter().enumerate() { + if i > 0 { + writeln!(f)?; + } + write!(f, "{item:?}")?; + } + Ok(()) + } +} + +impl From for ErrorsCollection { + fn from(value: T) -> Self { + Self(vec![value]) + } +} + +impl IntoIterator for ErrorsCollection { + type Item = T; + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + +#[derive(Default)] +pub struct TestEnv { + map: HashMap, + visited: RefCell>, +} + +impl TestEnv { + pub fn new() -> Self { + Self::default() + } + + pub fn with_map(map: HashMap) -> Self { + Self { map, ..Self::new() } + } + + #[must_use] + pub fn set(mut self, key: impl AsRef, value: impl AsRef) -> Self { + self.map + .insert(key.as_ref().to_string(), value.as_ref().to_string()); + self + } + + pub fn unvisited(&self) -> HashSet { + let all_keys: HashSet<_> = self.map.keys().map(ToOwned::to_owned).collect(); + let visited: HashSet<_> = self.visited.borrow().clone(); + all_keys.sub(&visited) + } +} + +impl ReadEnv for TestEnv { + fn get(&self, key: impl AsRef) -> Option<&str> { + self.visited.borrow_mut().insert(key.as_ref().to_string()); + self.map.get(key.as_ref()).map(std::string::String::as_str) + } +} + +pub enum ParseEnvResult { + Value(T), + ParseError, + None, +} + +impl ParseEnvResult +where + T: FromStr, + ::Err: Error + Send + Sync + 'static, +{ + pub fn parse_simple( + emitter: &mut Emitter, + env: &impl ReadEnv, + env_key: impl AsRef, + field_name: impl AsRef, + ) -> Self { + match env + .get(env_key.as_ref()) + .map(FromStr::from_str) + .transpose() + .wrap_err_with(|| { + eyre!( + "failed to parse `{}` field from `{}` env variable", + field_name.as_ref(), + env_key.as_ref() + ) + }) { + Ok(Some(x)) => Self::Value(x), + Ok(None) => Self::None, + Err(report) => { + emitter.emit(report); + Self::ParseError + } + } + } +} + +impl From> for Option { + fn from(value: ParseEnvResult) -> Self { + match value { + ParseEnvResult::None | ParseEnvResult::ParseError => None, + ParseEnvResult::Value(x) => Some(x), + } + } +} + +#[derive( + Serialize, + Deserialize, + Ord, + PartialOrd, + Eq, + PartialEq, + derive_more::From, + Clone, + derive_more::Deref, + derive_more::DerefMut, +)] +pub struct UserField(Option); + +impl Debug for UserField { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +impl Default for UserField { + fn default() -> Self { + Self(None) + } +} + +impl Merge for UserField { + fn merge(&mut self, other: Self) { + if let Some(value) = other.0 { + self.0 = Some(value) + } + } +} + +impl UserField { + pub fn get(self) -> Option { + self.0 + } +} + +impl From> for UserField { + fn from(value: ParseEnvResult) -> Self { + let option: Option = value.into(); + option.into() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn single_missing_field() { + let mut emitter = Emitter::new(); + + emitter.emit(CompleteError::missing_field("foo")); + + let err = emitter.finish().unwrap_err(); + + assert_eq!(format!("{err}"), "Missing field: foo") + } + + #[test] + fn multiple_missing_fields() { + let mut emitter = Emitter::new(); + + emitter.emit(CompleteError::missing_field("foo")); + emitter.emit(CompleteError::missing_field("bar")); + + let err = emitter.finish().unwrap_err(); + + assert_eq!(format!("{err}"), "Missing field: foo\nMissing field: bar") + } + + #[test] + fn merging_user_fields_overrides_old_value() { + let mut field = UserField(None); + field.merge(UserField(Some(4))); + assert_eq!(field, UserField(Some(4))); + + let mut field = UserField(Some(4)); + field.merge(UserField(Some(5))); + assert_eq!(field, UserField(Some(5))); + + let mut field = UserField(Some(4)); + field.merge(UserField(None)); + assert_eq!(field, UserField(Some(4))); + } +} diff --git a/config/src/lib.rs b/config/src/lib.rs index d3266289ad0..5b56eaecbc3 100644 --- a/config/src/lib.rs +++ b/config/src/lib.rs @@ -3,349 +3,6 @@ // FIXME #![allow(unused, missing_docs, missing_copy_implementations)] -use std::{ - cell::RefCell, - collections::{HashMap, HashSet}, - error::Error, - fmt::{Debug, Display, Formatter}, - io::Read, - ops::Sub, - str::FromStr, - time::Duration, -}; - -use eyre::{eyre, Report, Result, WrapErr}; -use merge::Merge; -use serde::{Deserialize, Deserializer, Serialize, Serializer}; - pub mod client_api; pub mod parameters; -mod util; - -/// User-provided duration -#[derive(Debug, Copy, Clone, Deserialize, Serialize)] -pub struct UserDuration(Duration); - -impl UserDuration { - pub fn get(self) -> Duration { - self.0 - } -} - -/// Byte size -#[derive(Debug, Copy, Clone, Deserialize, Serialize)] -pub struct ByteSize(u64); - -pub trait Complete { - type Output; - - fn complete(self) -> CompleteResult; -} - -pub trait ReadEnv { - fn get(&self, key: impl AsRef) -> Option<&str>; -} - -pub trait FromEnv { - fn from_env(env: &impl ReadEnv) -> FromEnvResult - where - Self: Sized; -} - -pub type FromEnvResult = Result>; - -pub trait FromEnvDefaultFallback {} - -impl FromEnv for T -where - T: FromEnvDefaultFallback + Default, -{ - fn from_env(env: &impl ReadEnv) -> FromEnvResult - where - Self: Sized, - { - Ok(Self::default()) - } -} - -pub struct Emitter { - errors: Vec, - bomb: drop_bomb::DropBomb, -} - -impl Emitter { - fn new() -> Self { - Self { - errors: Vec::new(), - bomb: drop_bomb::DropBomb::new( - "Errors emitter is dropped without consuming collected errors", - ), - } - } - - fn emit(&mut self, error: T) { - self.errors.push(error); - } - - fn emit_collection(&mut self, mut errors: ErrorsCollection) { - self.errors.append(&mut errors.0); - } - - fn finish(mut self) -> Result<(), ErrorsCollection> { - self.bomb.defuse(); - - if self.errors.is_empty() { - Ok(()) - } else { - Err(ErrorsCollection(self.errors)) - } - } -} - -#[derive(thiserror::Error, Debug)] -pub enum CompleteError { - #[error("Missing field: {path}")] - MissingField { path: String }, - #[error(transparent)] - Custom(#[from] Report), -} - -pub type CompleteResult = Result>; - -impl CompleteError { - pub fn missing_field(field_name: impl AsRef) -> Self { - Self::MissingField { - path: field_name.as_ref().to_string(), - } - } -} - -impl Emitter { - fn emit_missing_field(&mut self, field_name: impl AsRef) { - self.emit(CompleteError::MissingField { - path: field_name.as_ref().to_string(), - }) - } -} - -pub struct ErrorsCollection(Vec); - -impl Error for ErrorsCollection {} - -impl Display for ErrorsCollection -where - T: Display, -{ - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - for (i, item) in self.0.iter().enumerate() { - if i > 0 { - writeln!(f)?; - } - write!(f, "{item}")?; - } - Ok(()) - } -} - -impl Debug for ErrorsCollection -where - T: Debug, -{ - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - for (i, item) in self.0.iter().enumerate() { - if i > 0 { - writeln!(f)?; - } - write!(f, "{item:?}")?; - } - Ok(()) - } -} - -impl From for ErrorsCollection { - fn from(value: T) -> Self { - Self(vec![value]) - } -} - -impl IntoIterator for ErrorsCollection { - type Item = T; - type IntoIter = std::vec::IntoIter; - - fn into_iter(self) -> Self::IntoIter { - self.0.into_iter() - } -} - -#[derive(Default)] -pub struct TestEnv { - map: HashMap, - visited: RefCell>, -} - -impl TestEnv { - pub fn new() -> Self { - Self::default() - } - - pub fn with_map(map: HashMap) -> Self { - Self { map, ..Self::new() } - } - - #[must_use] - pub fn set(mut self, key: impl AsRef, value: impl AsRef) -> Self { - self.map - .insert(key.as_ref().to_string(), value.as_ref().to_string()); - self - } - - pub fn unvisited(&self) -> HashSet { - let all_keys: HashSet<_> = self.map.keys().map(ToOwned::to_owned).collect(); - let visited: HashSet<_> = self.visited.borrow().clone(); - all_keys.sub(&visited) - } -} - -impl ReadEnv for TestEnv { - fn get(&self, key: impl AsRef) -> Option<&str> { - self.visited.borrow_mut().insert(key.as_ref().to_string()); - self.map.get(key.as_ref()).map(std::string::String::as_str) - } -} - -enum ParseEnvResult { - Value(T), - ParseError, - None, -} - -impl ParseEnvResult -where - T: FromStr, - ::Err: Error + Send + Sync + 'static, -{ - fn parse_simple( - emitter: &mut Emitter, - env: &impl ReadEnv, - env_key: impl AsRef, - field_name: impl AsRef, - ) -> Self { - match env - .get(env_key.as_ref()) - .map(FromStr::from_str) - .transpose() - .wrap_err_with(|| { - eyre!( - "failed to parse `{}` field from `{}` env variable", - field_name.as_ref(), - env_key.as_ref() - ) - }) { - Ok(Some(x)) => Self::Value(x), - Ok(None) => Self::None, - Err(report) => { - emitter.emit(report); - Self::ParseError - } - } - } -} - -impl From> for Option { - fn from(value: ParseEnvResult) -> Self { - match value { - ParseEnvResult::None | ParseEnvResult::ParseError => None, - ParseEnvResult::Value(x) => Some(x), - } - } -} - -#[derive( - Serialize, - Deserialize, - Ord, - PartialOrd, - Eq, - PartialEq, - derive_more::From, - Clone, - derive_more::Deref, - derive_more::DerefMut, -)] -pub struct UserField(Option); - -impl Debug for UserField { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - self.0.fmt(f) - } -} - -impl Default for UserField { - fn default() -> Self { - Self(None) - } -} - -impl Merge for UserField { - fn merge(&mut self, mut other: Self) { - if let Some(value) = other.0 { - self.0 = Some(value) - } - } -} - -impl UserField { - pub fn get(self) -> Option { - self.0 - } -} - -impl From> for UserField { - fn from(value: ParseEnvResult) -> Self { - let option: Option = value.into(); - option.into() - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn single_missing_field() { - let mut emitter = Emitter::new(); - - emitter.emit(CompleteError::missing_field("foo")); - - let err = emitter.finish().unwrap_err(); - - assert_eq!(format!("{err}"), "Missing field: foo") - } - - #[test] - fn multiple_missing_fields() { - let mut emitter = Emitter::new(); - - emitter.emit(CompleteError::missing_field("foo")); - emitter.emit(CompleteError::missing_field("bar")); - - let err = emitter.finish().unwrap_err(); - - assert_eq!(format!("{err}"), "Missing field: foo\nMissing field: bar") - } - - #[test] - fn merging_user_fields_overrides_old_value() { - let mut field = UserField(None); - field.merge(UserField(Some(4))); - assert_eq!(field, UserField(Some(4))); - - let mut field = UserField(Some(4)); - field.merge(UserField(Some(5))); - assert_eq!(field, UserField(Some(5))); - - let mut field = UserField(Some(4)); - field.merge(UserField(None)); - assert_eq!(field, UserField(Some(4))); - } -} +pub use iroha_config_base as base; diff --git a/config/src/parameters/chain_wide.rs b/config/src/parameters/chain_wide.rs index 7b7cc29fbcf..6c9804a692d 100644 --- a/config/src/parameters/chain_wide.rs +++ b/config/src/parameters/chain_wide.rs @@ -8,33 +8,32 @@ use std::{ time::Duration, }; +use iroha_config_base::{ + ByteSize, Complete, CompleteError, CompleteResult, FromEnv, FromEnvDefaultFallback, + FromEnvResult, Merge, ReadEnv, UserDuration, UserField, +}; use iroha_data_model::{prelude::MetadataLimits, transaction::TransactionLimits, LengthLimits}; -use merge::Merge; use nonzero_ext::nonzero; use serde::{Deserialize, Serialize}; -use crate::{ - ByteSize, Complete, CompleteError, CompleteResult, FromEnv, FromEnvDefaultFallback, - FromEnvResult, ReadEnv, UserDuration, UserField, -}; - -const DEFAULT_MAX_TXS: NonZeroU32 = nonzero!(2_u32.pow(9)); -const DEFAULT_BLOCK_TIME: Duration = Duration::from_secs(2); -const DEFAULT_COMMIT_TIME_LIMIT: Duration = Duration::from_secs(4); -const DEFAULT_WASM_FUEL_LIMIT: NonZeroU64 = nonzero!(30_000_000u64); -const DEFAULT_WASM_MAX_MEMORY: u64 = 500 * 2_u64.pow(20); // 500 MiB +pub const DEFAULT_MAX_TXS: NonZeroU32 = nonzero!(2_u32.pow(9)); +pub const DEFAULT_BLOCK_TIME: Duration = Duration::from_secs(2); +pub const DEFAULT_COMMIT_TIME_LIMIT: Duration = Duration::from_secs(4); +pub const DEFAULT_WASM_FUEL_LIMIT: NonZeroU64 = nonzero!(30_000_000u64); +pub const DEFAULT_WASM_MAX_MEMORY: u64 = 500 * 2_u64.pow(20); // 500 MiB /// Default limits for metadata -const DEFAULT_METADATA_LIMITS: MetadataLimits = MetadataLimits::new(2_u32.pow(20), 2_u32.pow(12)); +pub const DEFAULT_METADATA_LIMITS: MetadataLimits = + MetadataLimits::new(2_u32.pow(20), 2_u32.pow(12)); /// Default limits for ident length -const DEFAULT_IDENT_LENGTH_LIMITS: LengthLimits = LengthLimits::new(1, 2_u32.pow(7)); +pub const DEFAULT_IDENT_LENGTH_LIMITS: LengthLimits = LengthLimits::new(1, 2_u32.pow(7)); /// Default maximum number of instructions and expressions per transaction -const DEFAULT_MAX_INSTRUCTION_NUMBER: u64 = 2_u64.pow(12); +pub const DEFAULT_MAX_INSTRUCTION_NUMBER: u64 = 2_u64.pow(12); /// Default maximum number of instructions and expressions per transaction -const DEFAULT_MAX_WASM_SIZE_BYTES: u64 = 4 * 2_u64.pow(20); // 4 MiB +pub const DEFAULT_MAX_WASM_SIZE_BYTES: u64 = 4 * 2_u64.pow(20); // 4 MiB /// Default transaction limits -const DEFAULT_TRANSACTION_LIMITS: TransactionLimits = +pub const DEFAULT_TRANSACTION_LIMITS: TransactionLimits = TransactionLimits::new(DEFAULT_MAX_INSTRUCTION_NUMBER, DEFAULT_MAX_WASM_SIZE_BYTES); #[derive(Deserialize, Serialize, Debug, Default, Merge)] diff --git a/config/src/parameters/genesis.rs b/config/src/parameters/genesis.rs index 9ab5e0689a6..a69fc0d50f9 100644 --- a/config/src/parameters/genesis.rs +++ b/config/src/parameters/genesis.rs @@ -2,16 +2,14 @@ use std::{ops::Deref, path::PathBuf}; use eyre::{eyre, Context, Report}; +use iroha_config_base::{ + Complete, CompleteError, CompleteResult, Emitter, FromEnv, FromEnvDefaultFallback, + FromEnvResult, Merge, ParseEnvResult, ReadEnv, UserField, +}; use iroha_crypto::{KeyPair, PrivateKey, PublicKey}; use iroha_genesis::RawGenesisBlock; -use merge::Merge; use serde::{Deserialize, Serialize}; -use crate::{ - Complete, CompleteError, CompleteResult, Emitter, FromEnv, FromEnvDefaultFallback, - FromEnvResult, ParseEnvResult, ReadEnv, UserField, -}; - #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Default, Merge)] #[serde(deny_unknown_fields, default)] pub struct UserLayer { diff --git a/config/src/parameters/iroha.rs b/config/src/parameters/iroha.rs index 6458cb28fd2..581997e4bca 100644 --- a/config/src/parameters/iroha.rs +++ b/config/src/parameters/iroha.rs @@ -3,17 +3,15 @@ use std::{error::Error, str::FromStr}; use eyre::{eyre, Context, Report}; +use iroha_config_base::{ + Complete, CompleteError, CompleteResult, Emitter, FromEnv, FromEnvResult, Merge, + ParseEnvResult, ReadEnv, UserField, +}; use iroha_crypto::{Algorithm, KeyPair, PrivateKey, PublicKey}; use iroha_data_model::ChainId; use iroha_primitives::addr::SocketAddr; -use merge::Merge; use serde::{Deserialize, Serialize}; -use crate::{ - Complete, CompleteError, CompleteResult, Emitter, FromEnv, FromEnvResult, ParseEnvResult, - ReadEnv, UserField, -}; - #[derive(Deserialize, Serialize, Debug, Default, Merge)] #[serde(deny_unknown_fields, default)] pub struct UserLayer { @@ -155,8 +153,9 @@ pub struct Config { #[cfg(test)] mod tests { + use iroha_config_base::TestEnv; + use super::*; - use crate::TestEnv; #[test] fn parses_private_key_from_env() { diff --git a/config/src/parameters/kura.rs b/config/src/parameters/kura.rs index c71a0521bbf..bad0903dfb2 100644 --- a/config/src/parameters/kura.rs +++ b/config/src/parameters/kura.rs @@ -2,11 +2,12 @@ use std::{fmt::Display, path::PathBuf, str::FromStr}; -use merge::Merge; +use iroha_config_base::{ + impl_deserialize_from_str, impl_serialize_display, Complete, CompleteResult, Emitter, FromEnv, + FromEnvResult, Merge, ParseEnvResult, ReadEnv, +}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; -use crate::{Complete, CompleteResult, Emitter, FromEnv, FromEnvResult, ParseEnvResult, ReadEnv}; - const DEFAULT_BLOCK_STORE_PATH: &str = "./storage"; /// `Kura` configuration. @@ -94,8 +95,8 @@ pub enum Mode { Fast, } -crate::util::impl_serialize_display!(Mode); -crate::util::impl_deserialize_from_str!(Mode); +impl_serialize_display!(Mode); +impl_deserialize_from_str!(Mode); #[cfg(test)] mod tests { diff --git a/config/src/parameters/logger.rs b/config/src/parameters/logger.rs index fcf5411d14c..142f22768c9 100644 --- a/config/src/parameters/logger.rs +++ b/config/src/parameters/logger.rs @@ -2,18 +2,15 @@ //! configuration, as well as run-time reloading of the log-level. use core::fmt::Debug; +use iroha_config_base::{ + impl_deserialize_from_str, impl_serialize_display, Complete, CompleteError, CompleteResult, + Emitter, FromEnv, FromEnvResult, Merge, ParseEnvResult, ReadEnv, UserField, +}; pub use iroha_data_model::Level; #[cfg(feature = "tokio-console")] use iroha_primitives::addr::{socket_addr, SocketAddr}; -use merge::Merge; use serde::{Deserialize, Deserializer, Serialize, Serializer}; -use crate::{ - util::{impl_deserialize_from_str, impl_serialize_display}, - Complete, CompleteError, CompleteResult, Emitter, FromEnv, FromEnvResult, ParseEnvResult, - ReadEnv, UserField, -}; - #[cfg(feature = "tokio-console")] const DEFAULT_TOKIO_CONSOLE_ADDR: SocketAddr = socket_addr!(127.0.0.1:5555); @@ -84,6 +81,7 @@ impl Complete for UserLayer { #[cfg(feature = "tokio-console")] tokio_console_addr: self .tokio_console_addr + .get() .unwrap_or_else(|| DEFAULT_TOKIO_CONSOLE_ADDR.clone()), }) } diff --git a/config/src/parameters/mod.rs b/config/src/parameters/mod.rs index 46e72b3b619..aa8ef646d0d 100644 --- a/config/src/parameters/mod.rs +++ b/config/src/parameters/mod.rs @@ -7,11 +7,11 @@ use std::{ }; use eyre::{eyre, Context, Report, Result}; -use merge::Merge; +use iroha_config_base::{ + Complete, CompleteError, CompleteResult, Emitter, FromEnv, FromEnvResult, Merge, ReadEnv, +}; use serde::{Deserialize, Serialize}; -use crate::{Complete, CompleteError, CompleteResult, Emitter, FromEnv, FromEnvResult, ReadEnv}; - pub mod chain_wide; pub mod genesis; pub mod iroha; @@ -99,7 +99,7 @@ impl Complete for UserLayer { macro_rules! complete_nested { ($item:expr) => { - match $crate::Complete::complete($item) { + match iroha_config_base::Complete::complete($item) { Ok(value) => Some(value), Err(error) => { emitter.emit_collection(error); diff --git a/config/src/parameters/queue.rs b/config/src/parameters/queue.rs index 73f5f274543..c83b952b81d 100644 --- a/config/src/parameters/queue.rs +++ b/config/src/parameters/queue.rs @@ -4,14 +4,12 @@ use std::{ time::Duration, }; -use merge::Merge; -use nonzero_ext::nonzero; -use serde::{Deserialize, Serialize}; - -use crate::{ - Complete, CompleteError, CompleteResult, FromEnv, FromEnvDefaultFallback, FromEnvResult, +use iroha_config_base::{ + Complete, CompleteError, CompleteResult, FromEnv, FromEnvDefaultFallback, FromEnvResult, Merge, ReadEnv, UserDuration, UserField, }; +use nonzero_ext::nonzero; +use serde::{Deserialize, Serialize}; const DEFAULT_MAX_TRANSACTIONS_IN_QUEUE: NonZeroU32 = nonzero!(2_u32.pow(16)); const DEFAULT_MAX_TRANSACTIONS_IN_QUEUE_PER_USER: NonZeroU32 = nonzero!(2_u32.pow(16)); diff --git a/config/src/parameters/snapshot.rs b/config/src/parameters/snapshot.rs index cfd871c354c..aa5c55472cf 100644 --- a/config/src/parameters/snapshot.rs +++ b/config/src/parameters/snapshot.rs @@ -2,13 +2,11 @@ use std::{path::PathBuf, time::Duration}; -use merge::Merge; -use serde::{Deserialize, Serialize}; - -use crate::{ - Complete, CompleteError, CompleteResult, Emitter, FromEnv, FromEnvResult, ParseEnvResult, - ReadEnv, UserDuration, UserField, +use iroha_config_base::{ + Complete, CompleteError, CompleteResult, Emitter, FromEnv, FromEnvResult, Merge, + ParseEnvResult, ReadEnv, UserDuration, UserField, }; +use serde::{Deserialize, Serialize}; const DEFAULT_SNAPSHOT_PATH: &str = "./storage"; // Default frequency of making snapshots is 1 minute, need to be adjusted for larger world state view size diff --git a/config/src/parameters/sumeragi.rs b/config/src/parameters/sumeragi.rs index 223527c336f..62d9ea9ce42 100644 --- a/config/src/parameters/sumeragi.rs +++ b/config/src/parameters/sumeragi.rs @@ -2,15 +2,14 @@ use std::{fmt::Debug, fs::File, io::BufReader, num::NonZeroU32, path::Path, time::Duration}; use eyre::eyre; +use iroha_config_base::{ + Complete, CompleteError, CompleteResult, FromEnvDefaultFallback, Merge, UserDuration, UserField, +}; use iroha_data_model::prelude::*; use iroha_primitives::unique_vec::UniqueVec; -use merge::Merge; use serde::{Deserialize, Serialize}; use self::default::*; -use crate::{ - Complete, CompleteError, CompleteResult, FromEnvDefaultFallback, UserDuration, UserField, -}; /// Module with a set of default values. pub mod default { diff --git a/config/src/parameters/telemetry.rs b/config/src/parameters/telemetry.rs index 6d5a1a49b67..8f06e623b89 100644 --- a/config/src/parameters/telemetry.rs +++ b/config/src/parameters/telemetry.rs @@ -6,16 +6,15 @@ use std::{ }; use eyre::eyre; -use merge::Merge; +use iroha_config_base::{ + Complete, CompleteError, CompleteResult, FromEnv, FromEnvDefaultFallback, FromEnvResult, Merge, + ReadEnv, UserDuration, UserField, +}; use serde::{Deserialize, Serialize}; use url::Url; -use crate::{ - parameters::telemetry::retry_period::{ - DEFAULT_MAX_RETRY_DELAY_EXPONENT, DEFAULT_MIN_RETRY_PERIOD, - }, - Complete, CompleteError, CompleteResult, FromEnv, FromEnvDefaultFallback, FromEnvResult, - ReadEnv, UserDuration, UserField, +use crate::parameters::telemetry::retry_period::{ + DEFAULT_MAX_RETRY_DELAY_EXPONENT, DEFAULT_MIN_RETRY_PERIOD, }; #[derive(Clone, Deserialize, Serialize, Debug, Default, Merge)] @@ -28,7 +27,7 @@ pub struct UserLayer { /// The minimum period of time in seconds to wait before reconnecting pub min_retry_period: UserField, /// The maximum exponent of 2 that is used for increasing delay between reconnections - pub max_retry_delay_exponent: UserField, + pub max_retry_delay_exponent: UserField, /// Dev telemetry configuration #[serde(default)] pub dev: DevUserLayer, @@ -56,7 +55,7 @@ pub struct RegularTelemetryConfig { #[allow(missing_docs)] pub min_retry_period: Duration, #[allow(missing_docs)] - pub max_retry_delay_exponent: NonZeroU8, + pub max_retry_delay_exponent: u8, } /// Complete configuration needed to start dev telemetry. @@ -114,8 +113,7 @@ pub mod retry_period { use nonzero_ext::nonzero; /// Default minimal retry period - // FIXME: it was `1`. Was it secs of millisecs? pub const DEFAULT_MIN_RETRY_PERIOD: Duration = Duration::from_secs(1); /// Default maximum exponent for the retry delay - pub const DEFAULT_MAX_RETRY_DELAY_EXPONENT: NonZeroU8 = nonzero!(4u8); + pub const DEFAULT_MAX_RETRY_DELAY_EXPONENT: u8 = 4; } diff --git a/config/src/parameters/torii.rs b/config/src/parameters/torii.rs index e9c2bc41ff4..018dd145714 100644 --- a/config/src/parameters/torii.rs +++ b/config/src/parameters/torii.rs @@ -2,14 +2,12 @@ use std::time::Duration; -use iroha_primitives::addr::{socket_addr, SocketAddr}; -use merge::Merge; -use serde::{Deserialize, Serialize}; - -use crate::{ - ByteSize, Complete, CompleteError, CompleteResult, Emitter, FromEnv, FromEnvResult, +use iroha_config_base::{ + ByteSize, Complete, CompleteError, CompleteResult, Emitter, FromEnv, FromEnvResult, Merge, ParseEnvResult, ReadEnv, UserDuration, UserField, }; +use iroha_primitives::addr::{socket_addr, SocketAddr}; +use serde::{Deserialize, Serialize}; const DEFAULT_MAX_CONTENT_LENGTH: u64 = 2_u64.pow(20) * 16; const DEFAULT_QUERY_IDLE_TIME: Duration = Duration::from_secs(30); diff --git a/config/src/util.rs b/config/src/util.rs deleted file mode 100644 index d502a1daeec..00000000000 --- a/config/src/util.rs +++ /dev/null @@ -1,30 +0,0 @@ -macro_rules! impl_serialize_display { - ($ty:ty) => { - impl serde::Serialize for $ty { - fn serialize(&self, serializer: S) -> std::result::Result - where - S: Serializer, - { - serializer.collect_str(self) - } - } - }; -} - -macro_rules! impl_deserialize_from_str { - ($ty:ty) => { - impl<'de> serde::Deserialize<'de> for $ty { - fn deserialize(deserializer: D) -> std::result::Result - where - D: Deserializer<'de>, - { - String::deserialize(deserializer)? - .parse() - .map_err(serde::de::Error::custom) - } - } - }; -} - -pub(crate) use impl_deserialize_from_str; -pub(crate) use impl_serialize_display; diff --git a/config/tests/fixtures.rs b/config/tests/fixtures.rs index 3bded6eebf0..b15dc5f72e6 100644 --- a/config/tests/fixtures.rs +++ b/config/tests/fixtures.rs @@ -5,7 +5,8 @@ use std::{ }; use eyre::Result; -use iroha_config::{parameters::UserLayer, Complete as _, FromEnv, TestEnv}; +use iroha_config::parameters::UserLayer; +use iroha_config_base::{Complete as _, FromEnv, TestEnv}; fn fixtures_dir() -> PathBuf { // CWD is the crate's root diff --git a/configs/client/config.example.toml b/configs/client/config.example.toml new file mode 100644 index 00000000000..90eb2430226 --- /dev/null +++ b/configs/client/config.example.toml @@ -0,0 +1,20 @@ +[account] +id = "alice@wonderland" +public_key = "ed01207233BFC89DCBD68C19FDE6CE6158225298EC1131B6A130D1AEB454C1AB5183C0" + +[account.private_key] +digest_function = "ed25519" +payload = "9ac47abf59b356e0bd7dcbbbb4dec080e302156a48ca907e47cb6aea1d32719e7233bfc89dcbd68c19fde6ce6158225298ec1131b6a130d1aeb454c1ab5183c0" + +[api] +torii_url = "http://127.0.0.1:8080/" + +[api.basic_auth] +login = "mad_hatter" +password = "ilovetea" + +[transaction] +time_to_live = 100_000 +status_timeout = 100_000 +add_nonce = false + From 2ad01aa7d2e7e555b3c90a7478fe72df44268b3c Mon Sep 17 00:00:00 2001 From: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> Date: Mon, 15 Jan 2024 18:26:13 +0900 Subject: [PATCH 08/94] [refactor]: update `iroha_logger` Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- logger/src/actor.rs | 2 +- logger/src/lib.rs | 23 +++++++++++++++-------- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/logger/src/actor.rs b/logger/src/actor.rs index e9e2d91280e..2c908641c26 100644 --- a/logger/src/actor.rs +++ b/logger/src/actor.rs @@ -1,6 +1,6 @@ //! Actor encapsulating interaction with logger & telemetry subsystems. -use iroha_config::logger::into_tracing_level; +use iroha_config::parameters::logger::into_tracing_level; use iroha_data_model::Level; use tokio::sync::{broadcast, mpsc, oneshot}; use tracing_core::Subscriber; diff --git a/logger/src/lib.rs b/logger/src/lib.rs index f84ddc6a7d8..17b0a6ff5d2 100644 --- a/logger/src/lib.rs +++ b/logger/src/lib.rs @@ -13,8 +13,11 @@ use std::{ use actor::LoggerHandle; use color_eyre::{eyre::eyre, Report, Result}; -pub use iroha_config::logger::{Configuration, ConfigurationProxy, Format, Level}; -use iroha_config::{base::proxy::Builder, logger::into_tracing_level}; +use iroha_config::parameters::logger::into_tracing_level; +pub use iroha_config::{ + base::Complete as _, + parameters::logger::{Config, Format, Level, UserLayer as UserConfigLayer}, +}; use tracing::subscriber::set_global_default; pub use tracing::{ debug, debug_span, error, error_span, info, info_span, instrument as log, trace, trace_span, @@ -50,7 +53,7 @@ fn try_set_logger() -> Result<()> { /// If the logger is already set, raises a generic error. // TODO: refactor configuration in a way that `terminal_colors` is part of it // https://github.com/hyperledger/iroha/issues/3500 -pub fn init_global(configuration: &Configuration, terminal_colors: bool) -> Result { +pub fn init_global(configuration: &Config, terminal_colors: bool) -> Result { try_set_logger()?; let layer = tracing_subscriber::fmt::layer() @@ -75,15 +78,19 @@ pub fn test_logger() -> LoggerHandle { LOGGER .get_or_init(|| { + // let mut config = // NOTE: if this config should be changed for some specific tests, consider // isolating those tests into a separate process and controlling default logger config // with ENV vars rather than by extending `test_logger` signature. This will both remain // `test_logger` simple and also will emphasise isolation which is necessary anyway in // case of singleton mocking (where the logger is the singleton). - let config = Configuration { - level: Level::DEBUG, - format: Format::Pretty, - ..ConfigurationProxy::default().build().unwrap() + let config = { + let mut layer = UserConfigLayer::default(); + let _ = layer.level.insert(Level::DEBUG); + let _ = layer.format.insert(Format::Pretty); + layer + .complete() + .expect("should not fail because other fields have defaults") }; init_global(&config, true).expect( @@ -103,7 +110,7 @@ pub fn disable_global() -> Result<()> { try_set_logger() } -fn step2(configuration: &Configuration, layer: L) -> Result +fn step2(configuration: &Config, layer: L) -> Result where L: tracing_subscriber::Layer + Debug + Send + Sync + 'static, { From ec65f389fff5ed29a625fc9830a32a7999bf3cff Mon Sep 17 00:00:00 2001 From: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> Date: Mon, 15 Jan 2024 18:27:06 +0900 Subject: [PATCH 09/94] [refactor]: update `iroha_telemetry` - update usage of `iroha_config` - use `Duration` in `RetryPeriod` Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- telemetry/src/dev.rs | 2 +- telemetry/src/lib.rs | 2 +- telemetry/src/retry_period.rs | 34 +++++++++++++++++----------------- telemetry/src/ws.rs | 12 +++++++----- 4 files changed, 26 insertions(+), 24 deletions(-) diff --git a/telemetry/src/dev.rs b/telemetry/src/dev.rs index 674b7bca748..1f99fa23f08 100644 --- a/telemetry/src/dev.rs +++ b/telemetry/src/dev.rs @@ -1,7 +1,7 @@ //! Module with development telemetry use eyre::{Result, WrapErr}; -use iroha_config::telemetry::DevTelemetryConfig; +use iroha_config::parameters::telemetry::DevTelemetryConfig; use iroha_logger::telemetry::Event as Telemetry; use tokio::{ fs::OpenOptions, diff --git a/telemetry/src/lib.rs b/telemetry/src/lib.rs index 8ba2ec2e2fb..3d6b97346bd 100644 --- a/telemetry/src/lib.rs +++ b/telemetry/src/lib.rs @@ -7,7 +7,7 @@ pub mod metrics; mod retry_period; pub mod ws; -pub use iroha_config::telemetry::Configuration; +pub use iroha_config::parameters::telemetry::Config; pub use iroha_telemetry_derive::metrics; pub mod msg { diff --git a/telemetry/src/retry_period.rs b/telemetry/src/retry_period.rs index b27d1b7d7fa..9f0f1d5eb24 100644 --- a/telemetry/src/retry_period.rs +++ b/telemetry/src/retry_period.rs @@ -1,10 +1,12 @@ -//! Retry period that is calculated as `min_period * 2 ^ min(exponent, max_exponent)` +//! Period for re-entrant polling + +use std::time::Duration; /// Period for re-entrant polling #[derive(Clone, Copy, Debug)] pub struct RetryPeriod { /// The minimum period - min_period: u64, + min_period: Duration, /// The maximum exponent max_exponent: u8, /// The current exponent @@ -13,7 +15,7 @@ pub struct RetryPeriod { impl RetryPeriod { /// Constructs a new object - pub const fn new(min_period: u64, max_exponent: u8) -> Self { + pub const fn new(min_period: Duration, max_exponent: u8) -> Self { Self { min_period, max_exponent, @@ -30,27 +32,25 @@ impl RetryPeriod { } } - /// Returns the period - pub fn period(&mut self) -> u64 { - let mult = 2_u64.saturating_pow(self.exponent.into()); + /// Retry period that is calculated as `min_period * 2 ^ min(exponent, max_exponent)` + pub fn period(&mut self) -> Duration { + let mult = 2_u32.saturating_pow(self.exponent.into()); self.min_period.saturating_mul(mult) } } #[cfg(test)] mod tests { + use super::*; + #[test] fn increase_exponent_saturates() { - let mut period = super::RetryPeriod { - min_period: 32000_u64, - max_exponent: u8::MAX, - exponent: (u8::MAX - 1), - }; - println!("testing {period:?}"); - let old = period.period(); - period.increase_exponent(); - assert_eq!(period.period(), 2_u64.saturating_mul(old)); - period.increase_exponent(); - assert_eq!(period.period(), 2_u64.saturating_mul(old)); + let mut value = RetryPeriod::new(Duration::from_secs(42), 10); + println!("testing {value:?}"); + let initial_period = value.period(); + value.increase_exponent(); + assert_eq!(value.period(), initial_period.saturating_mul(2)); + value.increase_exponent(); + assert_eq!(value.period(), initial_period.saturating_mul(4)); } } diff --git a/telemetry/src/ws.rs b/telemetry/src/ws.rs index c8f1486e76c..f2cf86ca661 100644 --- a/telemetry/src/ws.rs +++ b/telemetry/src/ws.rs @@ -1,10 +1,9 @@ //! Telemetry sent to a server -use std::time::Duration; use chrono::Local; use eyre::{eyre, Result}; use futures::{stream::SplitSink, Sink, SinkExt, StreamExt}; -use iroha_config::telemetry::RegularTelemetryConfig; +use iroha_config::parameters::telemetry::RegularTelemetryConfig; use iroha_logger::telemetry::Event as Telemetry; use serde_json::Map; use tokio::{ @@ -164,10 +163,13 @@ where fn schedule_reconnect(&mut self) { self.retry_period.increase_exponent(); let period = self.retry_period.period(); - iroha_logger::debug!("Scheduled reconnecting to telemetry in {} seconds", period); + iroha_logger::debug!( + "Scheduled reconnecting to telemetry in {} seconds", + period.as_secs() + ); let sender = self.internal_sender.clone(); tokio::task::spawn(async move { - tokio::time::sleep(Duration::from_secs(period)).await; + tokio::time::sleep(period).await; let _ = sender.send(InternalMessage::Reconnect).await; }); } @@ -393,7 +395,7 @@ mod tests { fail: Arc::clone(&fail_factory_create), sender: message_sender, }, - RetryPeriod::new(1, 0), + RetryPeriod::new(Duration::from_secs(1), 0), internal_sender, ); tokio::task::spawn(async move { From 0b7f10539a21338fb95650d8e3f88ec8e3f100e1 Mon Sep 17 00:00:00 2001 From: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> Date: Tue, 16 Jan 2024 14:20:35 +0900 Subject: [PATCH 10/94] [fix]: fix util macro Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- config/base/src/lib.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/config/base/src/lib.rs b/config/base/src/lib.rs index 0945608d059..3b20b424ab6 100644 --- a/config/base/src/lib.rs +++ b/config/base/src/lib.rs @@ -24,7 +24,7 @@ macro_rules! impl_serialize_display { impl $crate::serde::Serialize for $ty { fn serialize(&self, serializer: S) -> std::result::Result where - S: Serializer, + S: serde::Serializer, { serializer.collect_str(self) } @@ -38,7 +38,7 @@ macro_rules! impl_deserialize_from_str { impl<'de> $crate::serde::Deserialize<'de> for $ty { fn deserialize(deserializer: D) -> std::result::Result where - D: Deserializer<'de>, + D: serde::Deserializer<'de>, { String::deserialize(deserializer)? .parse() @@ -60,7 +60,13 @@ impl UserDuration { /// Byte size #[derive(Debug, Copy, Clone, Deserialize, Serialize)] -pub struct ByteSize(pub u64); +pub struct ByteSize(pub T); + +impl ByteSize { + pub fn get(&self) -> T { + self.0 + } +} pub trait Complete { type Output; From 2abf6b91da75f6faad6209fcfb5cdbfaf17d31c8 Mon Sep 17 00:00:00 2001 From: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> Date: Tue, 16 Jan 2024 14:22:45 +0900 Subject: [PATCH 11/94] [refactor]: re-struct config, update `iroha_core` - split "user-layer" and "actual" config modules - update logger, telemetry, and kagami (genesis) Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- config/src/client_api.rs | 2 +- config/src/kura.rs | 31 + config/src/lib.rs | 3 + config/src/logger.rs | 45 ++ config/src/parameters/actual.rs | 185 +++++ config/src/parameters/chain_wide.rs | 108 --- config/src/parameters/defaults.rs | 102 +++ config/src/parameters/genesis.rs | 103 --- config/src/parameters/iroha.rs | 238 ------- config/src/parameters/kura.rs | 112 --- config/src/parameters/logger.rs | 123 ---- config/src/parameters/mod.rs | 221 +----- config/src/parameters/queue.rs | 63 -- config/src/parameters/snapshot.rs | 82 --- config/src/parameters/sumeragi.rs | 113 --- config/src/parameters/telemetry.rs | 119 ---- config/src/parameters/torii.rs | 67 -- config/src/parameters/user_layer.rs | 964 ++++++++++++++++++++++++++ config/tests/fixtures.rs | 18 +- core/src/block.rs | 12 +- core/src/block_sync.rs | 23 +- core/src/executor.rs | 8 +- core/src/gossiper.rs | 14 +- core/src/kiso.rs | 24 +- core/src/kura.rs | 6 +- core/src/query/store.rs | 14 +- core/src/queue.rs | 147 ++-- core/src/smartcontracts/isi/domain.rs | 4 +- core/src/smartcontracts/isi/world.rs | 2 +- core/src/smartcontracts/wasm.rs | 27 +- core/src/snapshot.rs | 8 +- core/src/sumeragi/mod.rs | 27 +- core/src/wsv.rs | 41 +- logger/src/actor.rs | 2 +- logger/src/lib.rs | 18 +- telemetry/src/dev.rs | 2 +- telemetry/src/lib.rs | 4 +- telemetry/src/ws.rs | 6 +- tools/kagami/src/config.rs | 1 - tools/kagami/src/genesis.rs | 6 +- 40 files changed, 1510 insertions(+), 1585 deletions(-) create mode 100644 config/src/kura.rs create mode 100644 config/src/logger.rs create mode 100644 config/src/parameters/actual.rs delete mode 100644 config/src/parameters/chain_wide.rs create mode 100644 config/src/parameters/defaults.rs delete mode 100644 config/src/parameters/genesis.rs delete mode 100644 config/src/parameters/iroha.rs delete mode 100644 config/src/parameters/kura.rs delete mode 100644 config/src/parameters/logger.rs delete mode 100644 config/src/parameters/queue.rs delete mode 100644 config/src/parameters/snapshot.rs delete mode 100644 config/src/parameters/sumeragi.rs delete mode 100644 config/src/parameters/telemetry.rs delete mode 100644 config/src/parameters/torii.rs create mode 100644 config/src/parameters/user_layer.rs delete mode 100644 tools/kagami/src/config.rs diff --git a/config/src/client_api.rs b/config/src/client_api.rs index 1e1db9bd7e7..096c4b4b8d2 100644 --- a/config/src/client_api.rs +++ b/config/src/client_api.rs @@ -12,7 +12,7 @@ use iroha_data_model::Level; use serde::{Deserialize, Serialize}; -use super::parameters::{logger::Config as BaseLogger, Config as BaseConfiguration}; +use crate::parameters::actual::{Logger as BaseLogger, Root as BaseConfiguration}; /// Subset of [`super::iroha`] configuration. #[derive(Debug, Serialize, Deserialize, Clone, Copy)] diff --git a/config/src/kura.rs b/config/src/kura.rs new file mode 100644 index 00000000000..a7d4f63e68a --- /dev/null +++ b/config/src/kura.rs @@ -0,0 +1,31 @@ +use iroha_config_base::{impl_deserialize_from_str, impl_serialize_display}; +use serde::Serializer; + +/// Kura initialization mode. +#[derive( + Debug, Clone, Copy, PartialEq, Eq, Default, parse_display::Display, parse_display::FromStr, +)] +#[display(style = "snake_case")] +pub enum Mode { + /// Strict validation of all blocks. + #[default] + Strict, + /// Fast initialization with basic checks. + Fast, +} + +impl_serialize_display!(Mode); +impl_deserialize_from_str!(Mode); + +#[cfg(test)] +mod tests { + use crate::kura::Mode; + + #[test] + fn init_mode_display_reprs() { + assert_eq!(format!("{}", Mode::Strict), "strict"); + assert_eq!(format!("{}", Mode::Fast), "fast"); + assert_eq!("strict".parse::().unwrap(), Mode::Strict); + assert_eq!("fast".parse::().unwrap(), Mode::Fast); + } +} diff --git a/config/src/lib.rs b/config/src/lib.rs index 5b56eaecbc3..c7c6a680e5f 100644 --- a/config/src/lib.rs +++ b/config/src/lib.rs @@ -4,5 +4,8 @@ #![allow(unused, missing_docs, missing_copy_implementations)] pub mod client_api; +pub mod kura; +pub mod logger; pub mod parameters; + pub use iroha_config_base as base; diff --git a/config/src/logger.rs b/config/src/logger.rs new file mode 100644 index 00000000000..1a15d445504 --- /dev/null +++ b/config/src/logger.rs @@ -0,0 +1,45 @@ +use iroha_config_base::{impl_deserialize_from_str, impl_serialize_display}; +pub use iroha_data_model::Level; + +/// Convert [`Level`] into [`tracing::Level`] +pub fn into_tracing_level(level: Level) -> tracing::Level { + match level { + Level::TRACE => tracing::Level::TRACE, + Level::DEBUG => tracing::Level::DEBUG, + Level::INFO => tracing::Level::INFO, + Level::WARN => tracing::Level::WARN, + Level::ERROR => tracing::Level::ERROR, + } +} + +/// Reflects formatters in [`tracing_subscriber::fmt::format`] +#[derive( + Debug, Copy, Clone, Eq, PartialEq, parse_display::Display, parse_display::FromStr, Default, +)] +#[display(style = "snake_case")] +pub enum Format { + /// See [`tracing_subscriber::fmt::format::Full`] + #[default] + Full, + /// See [`tracing_subscriber::fmt::format::Compact`] + Compact, + /// See [`tracing_subscriber::fmt::format::Pretty`] + Pretty, + /// See [`tracing_subscriber::fmt::format::Json`] + Json, +} + +impl_serialize_display!(Format); +impl_deserialize_from_str!(Format); + +#[cfg(test)] +pub mod tests { + use crate::logger::Format; + + #[test] + fn serialize_pretty_format_in_lowercase() { + let value = Format::Pretty; + let actual = serde_json::to_string(&value).unwrap(); + assert_eq!("\"pretty\"", actual); + } +} diff --git a/config/src/parameters/actual.rs b/config/src/parameters/actual.rs new file mode 100644 index 00000000000..c3441c95507 --- /dev/null +++ b/config/src/parameters/actual.rs @@ -0,0 +1,185 @@ +use std::{ + num::{NonZeroU32, NonZeroU64, NonZeroUsize}, + path::PathBuf, + time::Duration, +}; + +use iroha_config_base::ByteSize; +use iroha_crypto::{KeyPair, PublicKey}; +use iroha_data_model::{ + metadata::Limits as MetadataLimits, peer::PeerId, transaction::TransactionLimits, LengthLimits, + Level, +}; +use iroha_genesis::RawGenesisBlock; +use iroha_primitives::{addr::SocketAddr, unique_vec::UniqueVec}; +use serde::{Deserialize, Serialize}; +use url::Url; + +use crate::{kura::Mode, logger::Format}; + +#[derive(Debug)] +pub struct Root { + pub iroha: Iroha, + pub genesis: Genesis, + pub torii: Torii, + pub kura: Kura, + pub sumeragi: Sumeragi, + pub block_sync: BlockSync, + pub transaction_gossiper: TransactionGossiper, + pub live_query_store: LiveQueryStore, + pub logger: Logger, + pub queue: Queue, + pub snapshot: Snapshot, + pub regular_telemetry: Option, + pub dev_telemetry: Option, + pub chain_wide: ChainWide, +} + +#[derive(Debug)] +pub struct Iroha { + pub key_pair: KeyPair, + pub p2p_address: SocketAddr, +} + +#[derive(Debug)] +pub enum Genesis { + /// 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, + /// Path to the [`RawGenesisBlock`] + file: PathBuf, + }, +} + +#[derive(Debug)] +pub struct Kura { + pub init_mode: Mode, + pub block_store_path: PathBuf, + pub debug_output_new_blocks: bool, +} + +/// `Queue` configuration. +#[derive(Copy, Clone, Deserialize, Serialize, Debug)] +pub struct Queue { + pub max_transactions_in_queue: NonZeroUsize, + pub max_transactions_in_queue_per_user: NonZeroUsize, + pub transaction_time_to_live: Duration, + pub future_threshold: Duration, +} + +impl Default for Queue { + fn default() -> Self { + todo!() + } +} + +#[derive(Debug)] +pub struct Logger { + /// Level of logging verbosity + pub level: Level, + /// Output format + pub format: Format, + #[cfg(feature = "tokio-console")] + /// Address of tokio console (only available under "tokio-console" feature) + pub tokio_console_addr: SocketAddr, +} + +#[derive(Debug)] +pub struct Snapshot { + pub create_every: Duration, + pub store_path: PathBuf, + pub creation_enabled: bool, +} + +#[derive(Debug)] +pub struct Sumeragi { + pub trusted_peers: UniqueVec, + pub debug_force_soft_fork: bool, +} + +#[derive(Debug, Clone, Copy)] +pub struct LiveQueryStore { + pub query_idle_time: Duration, +} + +impl Default for LiveQueryStore { + fn default() -> Self { + todo!() + } +} + +#[derive(Debug)] +pub struct BlockSync { + pub gossip_period: Duration, + pub batch_size: NonZeroU32, +} + +#[derive(Debug)] +pub struct TransactionGossiper { + pub gossip_period: Duration, + pub batch_size: NonZeroU32, +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +pub struct ChainWide { + pub max_transactions_in_block: NonZeroU32, + pub block_time: Duration, + pub commit_time: Duration, + pub transaction_limits: TransactionLimits, + pub asset_metadata_limits: MetadataLimits, + pub asset_definition_metadata_limits: MetadataLimits, + pub account_metadata_limits: MetadataLimits, + pub domain_metadata_limits: MetadataLimits, + pub identifier_length_limits: LengthLimits, + pub wasm_runtime: WasmRuntime, +} + +impl Default for ChainWide { + fn default() -> Self { + todo!() + } +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +pub struct WasmRuntime { + pub fuel_limit: u64, + pub max_memory: ByteSize, +} + +impl Default for WasmRuntime { + fn default() -> Self { + todo!() + } +} + +#[derive(Debug)] +pub struct Torii { + pub address: SocketAddr, + pub max_content_len: ByteSize, +} + +/// Complete configuration needed to start regular telemetry. +#[derive(Debug)] +pub struct RegularTelemetry { + #[allow(missing_docs)] + pub name: String, + #[allow(missing_docs)] + pub url: Url, + #[allow(missing_docs)] + pub min_retry_period: Duration, + #[allow(missing_docs)] + pub max_retry_delay_exponent: u8, +} + +/// Complete configuration needed to start dev telemetry. +#[derive(Debug)] +pub struct DevTelemetry { + #[allow(missing_docs)] + pub file: PathBuf, +} diff --git a/config/src/parameters/chain_wide.rs b/config/src/parameters/chain_wide.rs deleted file mode 100644 index 6c9804a692d..00000000000 --- a/config/src/parameters/chain_wide.rs +++ /dev/null @@ -1,108 +0,0 @@ -//! Chain-wide configuration parameters. -//! -//! They are supposed to be moved out of the configuration file: -//! [iroha#4028](https://github.com/hyperledger/iroha/issues/4028) - -use std::{ - num::{NonZeroU32, NonZeroU64}, - time::Duration, -}; - -use iroha_config_base::{ - ByteSize, Complete, CompleteError, CompleteResult, FromEnv, FromEnvDefaultFallback, - FromEnvResult, Merge, ReadEnv, UserDuration, UserField, -}; -use iroha_data_model::{prelude::MetadataLimits, transaction::TransactionLimits, LengthLimits}; -use nonzero_ext::nonzero; -use serde::{Deserialize, Serialize}; - -pub const DEFAULT_MAX_TXS: NonZeroU32 = nonzero!(2_u32.pow(9)); -pub const DEFAULT_BLOCK_TIME: Duration = Duration::from_secs(2); -pub const DEFAULT_COMMIT_TIME_LIMIT: Duration = Duration::from_secs(4); -pub const DEFAULT_WASM_FUEL_LIMIT: NonZeroU64 = nonzero!(30_000_000u64); -pub const DEFAULT_WASM_MAX_MEMORY: u64 = 500 * 2_u64.pow(20); // 500 MiB - -/// Default limits for metadata -pub const DEFAULT_METADATA_LIMITS: MetadataLimits = - MetadataLimits::new(2_u32.pow(20), 2_u32.pow(12)); -/// Default limits for ident length -pub const DEFAULT_IDENT_LENGTH_LIMITS: LengthLimits = LengthLimits::new(1, 2_u32.pow(7)); -/// Default maximum number of instructions and expressions per transaction -pub const DEFAULT_MAX_INSTRUCTION_NUMBER: u64 = 2_u64.pow(12); -/// Default maximum number of instructions and expressions per transaction -pub const DEFAULT_MAX_WASM_SIZE_BYTES: u64 = 4 * 2_u64.pow(20); // 4 MiB - -/// Default transaction limits -pub const DEFAULT_TRANSACTION_LIMITS: TransactionLimits = - TransactionLimits::new(DEFAULT_MAX_INSTRUCTION_NUMBER, DEFAULT_MAX_WASM_SIZE_BYTES); - -#[derive(Deserialize, Serialize, Debug, Default, Merge)] -#[serde(deny_unknown_fields, default)] -pub struct UserLayer { - pub max_transactions_in_block: UserField, - pub block_time: UserField, - pub commit_time: UserField, - pub transactions_limits: UserField, - pub asset_metadata_limits: UserField, - pub asset_definition_metadata_limits: UserField, - pub account_metadata_limits: UserField, - pub domain_metadata_limits: UserField, - pub identifier_length_limits: UserField, - pub wasm_fuel_limit: UserField, - pub wasm_max_memory: UserField, -} - -#[derive(Debug)] -pub struct Config { - pub max_transactions_in_block: NonZeroU32, - pub block_time: Duration, - pub commit_time: Duration, - pub transactions_limits: TransactionLimits, - pub asset_metadata_limits: MetadataLimits, - pub asset_definition_metadata_limits: MetadataLimits, - pub account_metadata_limits: MetadataLimits, - pub domain_metadata_limits: MetadataLimits, - pub identifier_length_limits: LengthLimits, - pub wasm_fuel_limit: NonZeroU64, - pub wasm_max_memory: ByteSize, -} - -impl Complete for UserLayer { - type Output = Config; - - fn complete(self) -> CompleteResult { - Ok(Config { - max_transactions_in_block: self.max_transactions_in_block.unwrap_or(DEFAULT_MAX_TXS), - block_time: self - .block_time - .map_or(DEFAULT_BLOCK_TIME, UserDuration::get), - commit_time: self - .commit_time - .map_or(DEFAULT_COMMIT_TIME_LIMIT, UserDuration::get), - transactions_limits: self - .transactions_limits - .unwrap_or(DEFAULT_TRANSACTION_LIMITS), - asset_metadata_limits: self - .asset_metadata_limits - .unwrap_or(DEFAULT_METADATA_LIMITS), - asset_definition_metadata_limits: self - .asset_definition_metadata_limits - .unwrap_or(DEFAULT_METADATA_LIMITS), - account_metadata_limits: self - .account_metadata_limits - .unwrap_or(DEFAULT_METADATA_LIMITS), - domain_metadata_limits: self - .domain_metadata_limits - .unwrap_or(DEFAULT_METADATA_LIMITS), - identifier_length_limits: self - .identifier_length_limits - .unwrap_or(DEFAULT_IDENT_LENGTH_LIMITS), - wasm_fuel_limit: self.wasm_fuel_limit.unwrap_or(DEFAULT_WASM_FUEL_LIMIT), - wasm_max_memory: self - .wasm_max_memory - .unwrap_or(ByteSize(DEFAULT_WASM_MAX_MEMORY)), - }) - } -} - -impl FromEnvDefaultFallback for UserLayer {} diff --git a/config/src/parameters/defaults.rs b/config/src/parameters/defaults.rs new file mode 100644 index 00000000000..90fd84ff1c4 --- /dev/null +++ b/config/src/parameters/defaults.rs @@ -0,0 +1,102 @@ +//! Module with a set of default values. + +use std::{ + num::{NonZeroU32, NonZeroU64, NonZeroUsize}, + ops::{Add, Div}, + time::Duration, +}; + +use iroha_data_model::{prelude::MetadataLimits, transaction::TransactionLimits, LengthLimits}; +use iroha_primitives::addr::{socket_addr, SocketAddr}; +use nonzero_ext::nonzero; + +pub mod queue { + use super::*; + + pub const DEFAULT_MAX_TRANSACTIONS_IN_QUEUE: NonZeroUsize = nonzero!(2_usize.pow(16)); + pub const DEFAULT_MAX_TRANSACTIONS_IN_QUEUE_PER_USER: NonZeroUsize = nonzero!(2_usize.pow(16)); + pub const DEFAULT_TRANSACTION_TIME_TO_LIVE: Duration = Duration::from_secs(24 * 60 * 60); + // 24 hours + pub const DEFAULT_FUTURE_THRESHOLD: Duration = Duration::from_secs(1); +} +pub mod kura { + use super::*; + + pub const DEFAULT_BLOCK_STORE_PATH: &str = "./storage"; +} +pub mod logger { + use super::*; + + #[cfg(feature = "tokio-console")] + pub const DEFAULT_TOKIO_CONSOLE_ADDR: SocketAddr = socket_addr!(127.0.0.1:5555); +} + +pub mod network { + use super::*; + + pub const DEFAULT_TRANSACTION_GOSSIP_PERIOD: Duration = Duration::from_secs(1); + + pub const DEFAULT_BLOCK_GOSSIP_PERIOD: Duration = Duration::from_secs(10); + + pub const DEFAULT_MAX_TRANSACTIONS_PER_GOSSIP: NonZeroU32 = nonzero!(500u32); + pub const DEFAULT_MAX_BLOCKS_PER_GOSSIP: NonZeroU32 = nonzero!(4u32); +} + +pub mod snapshot { + use super::*; + + pub const DEFAULT_SNAPSHOT_PATH: &str = "./storage"; + // Default frequency of making snapshots is 1 minute, need to be adjusted for larger world state view size + pub const DEFAULT_SNAPSHOT_CREATE_EVERY_MS: Duration = Duration::from_secs(60); + pub const DEFAULT_ENABLED: bool = true; +} + +pub mod chain_wide { + use super::*; + + pub const DEFAULT_MAX_TXS: NonZeroU32 = nonzero!(2_u32.pow(9)); + pub const DEFAULT_BLOCK_TIME: Duration = Duration::from_secs(2); + pub const DEFAULT_COMMIT_TIME_LIMIT: Duration = Duration::from_secs(4); + pub const DEFAULT_WASM_FUEL_LIMIT: u64 = 30_000_000; + pub const DEFAULT_WASM_MAX_MEMORY: u32 = 500 * 2_u32.pow(20); + + /// Default estimation of consensus duration. + pub const DEFAULT_CONSENSUS_ESTIMATION: Duration = + match DEFAULT_BLOCK_TIME.checked_add(match DEFAULT_COMMIT_TIME_LIMIT.checked_div(2) { + Some(x) => x, + None => unreachable!(), + }) { + Some(x) => x, + None => unreachable!(), + }; + + /// Default limits for metadata + pub const DEFAULT_METADATA_LIMITS: MetadataLimits = + MetadataLimits::new(2_u32.pow(20), 2_u32.pow(12)); + /// Default limits for ident length + pub const DEFAULT_IDENT_LENGTH_LIMITS: LengthLimits = LengthLimits::new(1, 2_u32.pow(7)); + /// Default maximum number of instructions and expressions per transaction + pub const DEFAULT_MAX_INSTRUCTION_NUMBER: u64 = 2_u64.pow(12); + /// Default maximum number of instructions and expressions per transaction + pub const DEFAULT_MAX_WASM_SIZE_BYTES: u64 = 4 * 2_u64.pow(20); + + /// Default transaction limits + pub const DEFAULT_TRANSACTION_LIMITS: TransactionLimits = + TransactionLimits::new(DEFAULT_MAX_INSTRUCTION_NUMBER, DEFAULT_MAX_WASM_SIZE_BYTES); +} + +pub mod torii { + use std::time::Duration; + + pub const DEFAULT_MAX_CONTENT_LENGTH: u32 = 2_u32.pow(20) * 16; + pub const DEFAULT_QUERY_IDLE_TIME: Duration = Duration::from_secs(30); +} + +pub mod telemetry { + use std::time::Duration; + + /// Default minimal retry period + pub const DEFAULT_MIN_RETRY_PERIOD: Duration = Duration::from_secs(1); + /// Default maximum exponent for the retry delay + pub const DEFAULT_MAX_RETRY_DELAY_EXPONENT: u8 = 4; +} diff --git a/config/src/parameters/genesis.rs b/config/src/parameters/genesis.rs deleted file mode 100644 index a69fc0d50f9..00000000000 --- a/config/src/parameters/genesis.rs +++ /dev/null @@ -1,103 +0,0 @@ -//! Module with genesis configuration logic. -use std::{ops::Deref, path::PathBuf}; - -use eyre::{eyre, Context, Report}; -use iroha_config_base::{ - Complete, CompleteError, CompleteResult, Emitter, FromEnv, FromEnvDefaultFallback, - FromEnvResult, Merge, ParseEnvResult, ReadEnv, UserField, -}; -use iroha_crypto::{KeyPair, PrivateKey, PublicKey}; -use iroha_genesis::RawGenesisBlock; -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Default, Merge)] -#[serde(deny_unknown_fields, default)] -pub struct UserLayer { - pub public_key: UserField, - pub private_key: UserField, - #[serde(default)] - pub file: UserField, -} - -#[derive(Debug)] -pub enum Config { - /// 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, - /// Path to the [`RawGenesisBlock`] - file: PathBuf, - }, -} - -impl Complete for UserLayer { - type Output = Config; - - fn complete(self) -> CompleteResult { - let public_key = self - .public_key - .get() - .ok_or_else(|| CompleteError::missing_field("genesis.public_key"))?; - - match (self.private_key.get(), self.file.get()) { - (None, None) => Ok(Config::Partial { public_key }), - (Some(private_key), Some(file)) => Ok(Config::Full { - key_pair: KeyPair::new(public_key, private_key) - .map_err(GenesisConfigError::from) - .wrap_err("FIXME") - .map_err(CompleteError::Custom)?, - file, - }), - _ => Err(GenesisConfigError::Inconsistent) - .wrap_err("FIXME") - .map_err(CompleteError::Custom)?, - } - } -} - -impl FromEnv for UserLayer { - fn from_env(env: &impl ReadEnv) -> FromEnvResult - where - Self: Sized, - { - let mut emitter = Emitter::new(); - - let public_key = ParseEnvResult::parse_simple( - &mut emitter, - env, - "GENESIS_PUBLIC_KEY", - "genesis.public_key", - ) - .into(); - let private_key = super::iroha::private_key_from_env( - &mut emitter, - env, - "GENESIS_PRIVATE_KEY", - "genesis.private_key", - ) - .into(); - let file = - ParseEnvResult::parse_simple(&mut emitter, env, "GENESIS_FILE", "genesis.file").into(); - - emitter.finish()?; - - Ok(Self { - public_key, - private_key, - file, - }) - } -} - -#[derive(Debug, displaydoc::Display, thiserror::Error)] -pub enum GenesisConfigError { - /// `genesis.file` and `genesis.private_key` should be set together - Inconsistent, - /// failed to construct the genesis's keypair using `genesis.public_key` and `genesis.private_key` configuration parameters - KeyPair(#[from] iroha_crypto::error::Error), -} diff --git a/config/src/parameters/iroha.rs b/config/src/parameters/iroha.rs deleted file mode 100644 index 581997e4bca..00000000000 --- a/config/src/parameters/iroha.rs +++ /dev/null @@ -1,238 +0,0 @@ -//! Basic parameters like the key pair and p2p address - -use std::{error::Error, str::FromStr}; - -use eyre::{eyre, Context, Report}; -use iroha_config_base::{ - Complete, CompleteError, CompleteResult, Emitter, FromEnv, FromEnvResult, Merge, - ParseEnvResult, ReadEnv, UserField, -}; -use iroha_crypto::{Algorithm, KeyPair, PrivateKey, PublicKey}; -use iroha_data_model::ChainId; -use iroha_primitives::addr::SocketAddr; -use serde::{Deserialize, Serialize}; - -#[derive(Deserialize, Serialize, Debug, Default, Merge)] -#[serde(deny_unknown_fields, default)] -pub struct UserLayer { - pub chain_id: Option, - pub public_key: UserField, - pub private_key: UserField, - pub p2p_address: UserField, -} - -impl Complete for UserLayer { - type Output = Config; - - fn complete(self) -> CompleteResult { - let mut emitter = Emitter::::new(); - - let key_pair = match (self.public_key.get(), self.private_key.get()) { - (Some(public_key), Some(private_key)) => { - KeyPair::new(public_key, private_key) - .map(Some) - .wrap_err("failed to construct a key pair from `iroha.public_key` and `iroha.private_key` configuration parameters") - .unwrap_or_else(|report| { - emitter.emit(CompleteError::Custom(report)); - None - }) - }, - (public_key, private_key) => { - if public_key.is_none() { - emitter.emit_missing_field("iroha.public_key"); - } - if private_key.is_none() { - emitter.emit_missing_field("iroha.private_key"); - } - None - } - }; - - if self.p2p_address.is_none() { - emitter.emit_missing_field("iroha.p2p_address"); - } - - emitter.finish()?; - - Ok(Config { - key_pair: key_pair.unwrap(), - p2p_address: self.p2p_address.get().unwrap(), - }) - } -} - -pub(crate) fn private_key_from_env( - emitter: &mut Emitter, - env: &impl ReadEnv, - env_key_base: impl AsRef, - name_base: impl AsRef, -) -> ParseEnvResult { - let digest_env = format!("{}_DIGEST", env_key_base.as_ref()); - let digest_name = format!("{}.digest_function", name_base.as_ref()); - let payload_env = format!("{}_PAYLOAD", env_key_base.as_ref()); - let payload_name = format!("{}.payload", name_base.as_ref()); - - let digest_function = ParseEnvResult::parse_simple(emitter, env, &digest_env, &digest_name); - - let payload = env.get(&payload_env).map(ToOwned::to_owned); - - match (digest_function, payload) { - (ParseEnvResult::Value(digest_function), Some(payload)) => { - PrivateKey::from_hex(digest_function, &payload) - .wrap_err_with(|| { - eyre!( - "failed to construct `{}` from `{}` and `{}` environment variables", - name_base.as_ref(), - &digest_env, - &payload_env - ) - }) - .map_or_else( - |report| { - emitter.emit(report); - ParseEnvResult::ParseError - }, - ParseEnvResult::Value, - ) - } - (ParseEnvResult::None, None) | (ParseEnvResult::ParseError, _) => ParseEnvResult::None, - (ParseEnvResult::Value(_), None) => { - emitter.emit(eyre!( - "`{}` env was provided, but `{}` was not", - &digest_env, - &payload_env - )); - ParseEnvResult::ParseError - } - (ParseEnvResult::None, Some(_)) => { - emitter.emit(eyre!( - "`{}` env was provided, but `{}` was not", - &payload_env, - &digest_env - )); - ParseEnvResult::ParseError - } - } -} - -impl FromEnv for UserLayer { - fn from_env(env: &impl ReadEnv) -> FromEnvResult - where - Self: Sized, - { - let mut emitter = Emitter::new(); - - let chain_id = - ParseEnvResult::parse_simple(&mut emitter, env, "CHAIN_ID", "iroha.chain_id").into(); - let public_key = - ParseEnvResult::parse_simple(&mut emitter, env, "PUBLIC_KEY", "iroha.public_key") - .into(); - let private_key = - private_key_from_env(&mut emitter, env, "PRIVATE_KEY", "iroha.private_key").into(); - let p2p_address = - ParseEnvResult::parse_simple(&mut emitter, env, "P2P_ADDRESS", "iroha.p2p_address") - .into(); - - emitter.finish()?; - - Ok(Self { - chain_id, - public_key, - private_key, - p2p_address, - }) - } -} - -#[derive(Debug)] -pub struct Config { - pub chain_id: ChainId, - pub key_pair: KeyPair, - pub p2p_address: SocketAddr, -} - -#[cfg(test)] -mod tests { - use iroha_config_base::TestEnv; - - use super::*; - - #[test] - fn parses_private_key_from_env() { - let env = TestEnv::new() - .set("PRIVATE_KEY_DIGEST", "ed25519") - .set("PRIVATE_KEY_PAYLOAD", "8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb"); - - let private_key = UserLayer::from_env(&env) - .expect("input is valid, should not fail") - .private_key - .get() - .expect("private key is provided, should not fail"); - - assert_eq!(private_key.digest_function(), "ed25519".parse().unwrap()); - assert_eq!(hex::encode( private_key.payload()), "8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb"); - } - - #[test] - fn fails_to_parse_private_key_in_env_without_digest() { - let env = TestEnv::new().set("PRIVATE_KEY_DIGEST", "ed25519"); - let error = UserLayer::from_env(&env).expect_err("private key is incomplete, should fail"); - let expected = expect_test::expect![[r#" - `PRIVATE_KEY_DIGEST` env was provided, but `PRIVATE_KEY_PAYLOAD` was not - - Location: - config/src/parameters/iroha.rs:100:26"#]]; - expected.assert_eq(&format!("{error:?}")); - } - - #[test] - fn fails_to_parse_private_key_in_env_without_payload() { - let env = TestEnv::new().set("PRIVATE_KEY_PAYLOAD", "8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb"); - let error = UserLayer::from_env(&env).expect_err("private key is incomplete, should fail"); - let expected = expect_test::expect![[r#" - `PRIVATE_KEY_PAYLOAD` env was provided, but `PRIVATE_KEY_DIGEST` was not - - Location: - config/src/parameters/iroha.rs:108:26"#]]; - expected.assert_eq(&format!("{error:?}")); - } - - #[test] - fn fails_to_parse_private_key_from_env_with_invalid_payload() { - let env = TestEnv::new() - .set("PRIVATE_KEY_DIGEST", "ed25519") - .set("PRIVATE_KEY_PAYLOAD", "foo"); - - let error = UserLayer::from_env(&env).expect_err("input is invalid, should fail"); - - let expected = expect_test::expect![[r#" - failed to construct `iroha.private_key` from `PRIVATE_KEY_DIGEST` and `PRIVATE_KEY_PAYLOAD` environment variables - - Caused by: - Key could not be parsed. Odd number of digits - - Location: - config/src/parameters/iroha.rs:82:18"#]]; - expected.assert_eq(&format!("{error:?}")); - } - - #[test] - fn when_payload_provided_but_digest_is_invalid() { - let env = TestEnv::new() - .set("PRIVATE_KEY_DIGEST", "foo") - .set("PRIVATE_KEY_PAYLOAD", "8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb"); - - let error = UserLayer::from_env(&env).expect_err("input is invalid, should fail"); - - // TODO: print the bad value and supported ones - let expected = expect_test::expect![[r#" - failed to parse `iroha.private_key.digest_function` field from `PRIVATE_KEY_DIGEST` env variable - - Caused by: - Algorithm not supported - - Location: - config/src/lib.rs:237:14"#]]; - expected.assert_eq(&format!("{error:?}")); - } -} diff --git a/config/src/parameters/kura.rs b/config/src/parameters/kura.rs deleted file mode 100644 index bad0903dfb2..00000000000 --- a/config/src/parameters/kura.rs +++ /dev/null @@ -1,112 +0,0 @@ -//! Module for kura-related configuration and structs - -use std::{fmt::Display, path::PathBuf, str::FromStr}; - -use iroha_config_base::{ - impl_deserialize_from_str, impl_serialize_display, Complete, CompleteResult, Emitter, FromEnv, - FromEnvResult, Merge, ParseEnvResult, ReadEnv, -}; -use serde::{Deserialize, Deserializer, Serialize, Serializer}; - -const DEFAULT_BLOCK_STORE_PATH: &str = "./storage"; - -/// `Kura` configuration. -#[derive(Clone, Deserialize, Serialize, Debug, Default, Merge)] -#[serde(deny_unknown_fields, default)] -pub struct UserLayer { - pub init_mode: Option, - pub block_store_path: Option, - pub debug: DebugUserConfig, -} - -#[derive(Clone, Deserialize, Serialize, Debug, Default, Merge)] -pub struct DebugUserConfig { - output_new_blocks: Option, -} - -#[derive(Debug)] -pub struct Config { - pub init_mode: Mode, - pub block_store_path: PathBuf, - pub debug_output_new_blocks: bool, -} - -impl Complete for UserLayer { - type Output = Config; - - fn complete(self) -> CompleteResult { - Ok(Config { - init_mode: self.init_mode.unwrap_or_default(), - block_store_path: self - .block_store_path - .unwrap_or_else(|| PathBuf::from(DEFAULT_BLOCK_STORE_PATH)), - debug_output_new_blocks: self.debug.output_new_blocks.unwrap_or(false), - }) - } -} - -impl FromEnv for UserLayer { - fn from_env(env: &impl ReadEnv) -> FromEnvResult - where - Self: Sized, - { - let mut emitter = Emitter::new(); - - let init_mode = - ParseEnvResult::parse_simple(&mut emitter, env, "KURA_INIT_MODE", "kura.init_mode") - .into(); - let block_store_path = ParseEnvResult::parse_simple( - &mut emitter, - env, - "KURA_BLOCK_STORE", - "kura.block_store_path", - ) - .into(); - let debug_output_new_blocks = ParseEnvResult::parse_simple( - &mut emitter, - env, - "KURA_DEBUG_OUTPUT_NEW_BLOCKS", - "kura.debug.output_new_blocks", - ) - .into(); - - emitter.finish()?; - - Ok(Self { - init_mode, - block_store_path, - debug: DebugUserConfig { - output_new_blocks: debug_output_new_blocks, - }, - }) - } -} - -/// Kura initialization mode. -#[derive( - Debug, Clone, Copy, PartialEq, Eq, Default, parse_display::Display, parse_display::FromStr, -)] -#[display(style = "snake_case")] -pub enum Mode { - /// Strict validation of all blocks. - #[default] - Strict, - /// Fast initialization with basic checks. - Fast, -} - -impl_serialize_display!(Mode); -impl_deserialize_from_str!(Mode); - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn init_mode_display_reprs() { - assert_eq!(format!("{}", Mode::Strict), "strict"); - assert_eq!(format!("{}", Mode::Fast), "fast"); - assert_eq!("strict".parse::().unwrap(), Mode::Strict); - assert_eq!("fast".parse::().unwrap(), Mode::Fast); - } -} diff --git a/config/src/parameters/logger.rs b/config/src/parameters/logger.rs deleted file mode 100644 index 142f22768c9..00000000000 --- a/config/src/parameters/logger.rs +++ /dev/null @@ -1,123 +0,0 @@ -//! Module containing logic related to spawning a logger from the -//! configuration, as well as run-time reloading of the log-level. -use core::fmt::Debug; - -use iroha_config_base::{ - impl_deserialize_from_str, impl_serialize_display, Complete, CompleteError, CompleteResult, - Emitter, FromEnv, FromEnvResult, Merge, ParseEnvResult, ReadEnv, UserField, -}; -pub use iroha_data_model::Level; -#[cfg(feature = "tokio-console")] -use iroha_primitives::addr::{socket_addr, SocketAddr}; -use serde::{Deserialize, Deserializer, Serialize, Serializer}; - -#[cfg(feature = "tokio-console")] -const DEFAULT_TOKIO_CONSOLE_ADDR: SocketAddr = socket_addr!(127.0.0.1:5555); - -/// Convert [`Level`] into [`tracing::Level`] -pub fn into_tracing_level(level: Level) -> tracing::Level { - match level { - Level::TRACE => tracing::Level::TRACE, - Level::DEBUG => tracing::Level::DEBUG, - Level::INFO => tracing::Level::INFO, - Level::WARN => tracing::Level::WARN, - Level::ERROR => tracing::Level::ERROR, - } -} - -/// 'Logger' configuration. -#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Default, Merge)] -// `tokio_console_addr` is not `Copy`, but warning appears without `tokio-console` feature -#[allow(missing_copy_implementations)] -#[serde(deny_unknown_fields, default)] -pub struct UserLayer { - /// Level of logging verbosity - pub level: UserField, - /// Output format - pub format: UserField, - #[cfg(feature = "tokio-console")] - /// Address of tokio console (only available under "tokio-console" feature) - pub tokio_console_addr: UserField, -} - -#[derive(Debug)] -pub struct Config { - /// Level of logging verbosity - pub level: Level, - /// Output format - pub format: Format, - #[cfg(feature = "tokio-console")] - /// Address of tokio console (only available under "tokio-console" feature) - pub tokio_console_addr: SocketAddr, -} - -/// Reflects formatters in [`tracing_subscriber::fmt::format`] -#[derive( - Debug, Copy, Clone, Eq, PartialEq, parse_display::Display, parse_display::FromStr, Default, -)] -#[display(style = "snake_case")] -pub enum Format { - /// See [`tracing_subscriber::fmt::format::Full`] - #[default] - Full, - /// See [`tracing_subscriber::fmt::format::Compact`] - Compact, - /// See [`tracing_subscriber::fmt::format::Pretty`] - Pretty, - /// See [`tracing_subscriber::fmt::format::Json`] - Json, -} - -impl_serialize_display!(Format); -impl_deserialize_from_str!(Format); - -impl Complete for UserLayer { - type Output = Config; - - fn complete(self) -> CompleteResult { - Ok(Config { - level: self.level.unwrap_or_default(), - format: self.format.unwrap_or_default(), - #[cfg(feature = "tokio-console")] - tokio_console_addr: self - .tokio_console_addr - .get() - .unwrap_or_else(|| DEFAULT_TOKIO_CONSOLE_ADDR.clone()), - }) - } -} - -impl FromEnv for UserLayer { - fn from_env(env: &impl ReadEnv) -> FromEnvResult - where - Self: Sized, - { - let mut emitter = Emitter::new(); - - let level = - ParseEnvResult::parse_simple(&mut emitter, env, "LOG_LEVEL", "logger.level").into(); - let format = - ParseEnvResult::parse_simple(&mut emitter, env, "LOG_FORMAT", "logger.format").into(); - - emitter.finish()?; - - Ok(Self { - level, - format, - ..Self::default() - }) - } -} - -#[cfg(test)] -pub mod tests { - - use super::*; - - #[test] - fn serialize_pretty_format_in_lowercase() { - let value = Format::Pretty; - let actual = serde_json::to_string(&value).unwrap(); - assert_eq!("\"pretty\"", actual); - } -} diff --git a/config/src/parameters/mod.rs b/config/src/parameters/mod.rs index aa8ef646d0d..be1e61bd1d2 100644 --- a/config/src/parameters/mod.rs +++ b/config/src/parameters/mod.rs @@ -1,218 +1,3 @@ -use std::{ - fmt::Debug, - fs::File, - io::{BufReader, Read}, - iter, - path::{Path, PathBuf}, -}; - -use eyre::{eyre, Context, Report, Result}; -use iroha_config_base::{ - Complete, CompleteError, CompleteResult, Emitter, FromEnv, FromEnvResult, Merge, ReadEnv, -}; -use serde::{Deserialize, Serialize}; - -pub mod chain_wide; -pub mod genesis; -pub mod iroha; -pub mod kura; -pub mod logger; -pub mod queue; -pub mod snapshot; -pub mod sumeragi; -pub mod telemetry; -pub mod torii; - -#[derive(Deserialize, Serialize, Debug, Default, Merge)] -#[serde(deny_unknown_fields, default)] -pub struct UserLayer { - #[serde(default)] - iroha: iroha::UserLayer, - #[serde(default)] - genesis: genesis::UserLayer, - #[serde(default)] - kura: kura::UserLayer, - #[serde(default)] - sumeragi: sumeragi::UserLayer, - #[serde(default)] - logger: logger::UserLayer, - #[serde(default)] - queue: queue::UserLayer, - #[serde(default)] - snapshot: snapshot::UserLayer, - #[serde(default)] - telemetry: telemetry::UserLayer, - #[serde(default)] - torii: torii::UserLayer, - #[serde(default)] - chain_wide: chain_wide::UserLayer, -} - -impl UserLayer { - pub fn from_toml(path: impl AsRef) -> Result { - let contents = { - let mut file = File::open(path.as_ref()).wrap_err_with(|| { - eyre!("cannot open file at location `{}`", path.as_ref().display()) - })?; - let mut contents = String::new(); - file.read_to_string(&mut contents)?; - contents - }; - let mut parsed: Self = toml::from_str(&contents).wrap_err("failed to parse toml")?; - parsed.normalise_paths( - path.as_ref() - .parent() - .expect("the config file path could not be empty or root"), - ); - Ok(parsed) - } - - fn normalise_paths(&mut self, relative_to: impl AsRef) { - let path = relative_to.as_ref(); - - macro_rules! patch { - ($value:expr) => { - $value.as_mut().map(|x| { - *x = path.join(&x); - }) - }; - } - - patch!(self.genesis.file); - patch!(self.snapshot.store_path); - patch!(self.kura.block_store_path); - patch!(self.telemetry.dev.file); - } - - // FIXME workaround the inconvenient way `Merge::merge` works - pub fn merge_chain(mut self, other: Self) -> Self { - self.merge(other); - self - } -} - -impl Complete for UserLayer { - type Output = Config; - - fn complete(self) -> CompleteResult { - let mut emitter = Emitter::new(); - - macro_rules! complete_nested { - ($item:expr) => { - match iroha_config_base::Complete::complete($item) { - Ok(value) => Some(value), - Err(error) => { - emitter.emit_collection(error); - None - } - } - }; - } - - let iroha = complete_nested!(self.iroha); - let genesis = complete_nested!(self.genesis); - let kura = complete_nested!(self.kura); - let sumeragi = complete_nested!(self.sumeragi); - let logger = complete_nested!(self.logger); - let queue = complete_nested!(self.queue); - let snapshot = complete_nested!(self.snapshot); - let telemetry = complete_nested!(self.telemetry); - let torii = complete_nested!(self.torii); - let chain_wide = complete_nested!(self.chain_wide); - - emitter.finish()?; - - Ok(Config { - iroha: iroha.unwrap(), - genesis: genesis.unwrap(), - kura: kura.unwrap(), - sumeragi: sumeragi.unwrap(), - logger: logger.unwrap(), - queue: queue.unwrap(), - snapshot: snapshot.unwrap(), - telemetry: telemetry.unwrap(), - torii: torii.unwrap(), - chain_wide: chain_wide.unwrap(), - }) - } -} - -impl FromEnv for UserLayer { - fn from_env(env: &impl ReadEnv) -> FromEnvResult { - fn from_env_nested( - env: &impl ReadEnv, - emitter: &mut Emitter, - ) -> Option { - match FromEnv::from_env(env) { - Ok(parsed) => Some(parsed), - Err(errors) => { - emitter.emit_collection(errors); - None - } - } - } - - let mut emitter = Emitter::new(); - - let iroha = from_env_nested(env, &mut emitter); - let genesis = from_env_nested(env, &mut emitter); - let kura = from_env_nested(env, &mut emitter); - let sumeragi = from_env_nested(env, &mut emitter); - let logger = from_env_nested(env, &mut emitter); - let queue = from_env_nested(env, &mut emitter); - let snapshot = from_env_nested(env, &mut emitter); - let telemetry = from_env_nested(env, &mut emitter); - let torii = from_env_nested(env, &mut emitter); - let chain_wide = from_env_nested(env, &mut emitter); - - emitter.finish()?; - - Ok(Self { - iroha: iroha.unwrap(), - genesis: genesis.unwrap(), - kura: kura.unwrap(), - sumeragi: sumeragi.unwrap(), - logger: logger.unwrap(), - queue: queue.unwrap(), - snapshot: snapshot.unwrap(), - telemetry: telemetry.unwrap(), - torii: torii.unwrap(), - chain_wide: chain_wide.unwrap(), - }) - } -} - -#[derive(Debug)] -pub struct Config { - pub iroha: iroha::Config, - pub genesis: genesis::Config, - pub kura: kura::Config, - pub sumeragi: sumeragi::Config, - pub logger: logger::Config, - pub queue: queue::Config, - pub snapshot: snapshot::Config, - pub telemetry: telemetry::Config, - pub torii: torii::Config, - pub chain_wide: chain_wide::Config, -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn deserialize_empty_input_works() { - let _layer: UserLayer = toml::from_str("").unwrap(); - } - - #[test] - fn deserialize_iroha_namespace_with_not_all_fields_works() { - let _layer: UserLayer = toml::from_str( - r#" - [iroha] - p2p_address = "127.0.0.1:8080" - "#, - ) - .unwrap(); - } -} +pub mod actual; +pub mod defaults; +pub mod user_layer; diff --git a/config/src/parameters/queue.rs b/config/src/parameters/queue.rs deleted file mode 100644 index c83b952b81d..00000000000 --- a/config/src/parameters/queue.rs +++ /dev/null @@ -1,63 +0,0 @@ -//! Module for `Queue`-related configuration and structs. -use std::{ - num::{NonZeroU32, NonZeroU64}, - time::Duration, -}; - -use iroha_config_base::{ - Complete, CompleteError, CompleteResult, FromEnv, FromEnvDefaultFallback, FromEnvResult, Merge, - ReadEnv, UserDuration, UserField, -}; -use nonzero_ext::nonzero; -use serde::{Deserialize, Serialize}; - -const DEFAULT_MAX_TRANSACTIONS_IN_QUEUE: NonZeroU32 = nonzero!(2_u32.pow(16)); -const DEFAULT_MAX_TRANSACTIONS_IN_QUEUE_PER_USER: NonZeroU32 = nonzero!(2_u32.pow(16)); -const DEFAULT_TRANSACTION_TIME_TO_LIVE: Duration = Duration::from_secs(24 * 60 * 60); // 24 hours -const DEFAULT_FUTURE_THRESHOLD: Duration = Duration::from_secs(1); - -#[derive(Deserialize, Serialize, Debug, Default, Merge)] -#[serde(deny_unknown_fields, default)] -pub struct UserLayer { - /// The upper limit of the number of transactions waiting in the queue. - pub max_transactions_in_queue: UserField, - /// The upper limit of the number of transactions waiting in the queue for single user. - /// Use this option to apply throttling. - pub max_transactions_in_queue_per_user: UserField, - /// The transaction will be dropped after this time if it is still in the queue. - pub transaction_time_to_live_ms: UserField, - /// The threshold to determine if a transaction has been tampered to have a future timestamp. - pub future_threshold_ms: UserField, -} - -/// `Queue` configuration. -#[derive(Copy, Clone, Deserialize, Serialize, Debug)] -pub struct Config { - pub max_transactions_in_queue: NonZeroU32, - pub max_transactions_in_queue_per_user: NonZeroU32, - pub transaction_time_to_live_ms: Duration, - pub future_threshold_ms: Duration, -} - -impl Complete for UserLayer { - type Output = Config; - - fn complete(self) -> CompleteResult { - Ok(Config { - max_transactions_in_queue: self - .max_transactions_in_queue - .unwrap_or(DEFAULT_MAX_TRANSACTIONS_IN_QUEUE), - max_transactions_in_queue_per_user: self - .max_transactions_in_queue_per_user - .unwrap_or(DEFAULT_MAX_TRANSACTIONS_IN_QUEUE), - transaction_time_to_live_ms: self - .transaction_time_to_live_ms - .map_or(DEFAULT_TRANSACTION_TIME_TO_LIVE, UserDuration::get), - future_threshold_ms: self - .future_threshold_ms - .map_or(DEFAULT_FUTURE_THRESHOLD, UserDuration::get), - }) - } -} - -impl FromEnvDefaultFallback for UserLayer {} diff --git a/config/src/parameters/snapshot.rs b/config/src/parameters/snapshot.rs deleted file mode 100644 index aa5c55472cf..00000000000 --- a/config/src/parameters/snapshot.rs +++ /dev/null @@ -1,82 +0,0 @@ -//! Module for `SnapshotMaker`-related configuration and structs. - -use std::{path::PathBuf, time::Duration}; - -use iroha_config_base::{ - Complete, CompleteError, CompleteResult, Emitter, FromEnv, FromEnvResult, Merge, - ParseEnvResult, ReadEnv, UserDuration, UserField, -}; -use serde::{Deserialize, Serialize}; - -const DEFAULT_SNAPSHOT_PATH: &str = "./storage"; -// Default frequency of making snapshots is 1 minute, need to be adjusted for larger world state view size -const DEFAULT_SNAPSHOT_CREATE_EVERY_MS: Duration = Duration::from_secs(60); -const DEFAULT_ENABLED: bool = true; - -#[derive(Debug, Clone, Deserialize, Serialize, Default, Merge)] -#[serde(deny_unknown_fields, default)] -pub struct UserLayer { - /// The period of time to wait between attempts to create new snapshot. - pub create_every_ms: UserField, - /// Path to the directory where snapshots should be stored - pub store_path: UserField, - /// Flag to enable or disable snapshot creation - pub creation_enabled: UserField, -} - -#[derive(Debug)] -pub struct Config { - pub create_every_ms: Duration, - pub store_path: PathBuf, - pub creation_enabled: bool, -} - -impl Complete for UserLayer { - type Output = Config; - - fn complete(self) -> CompleteResult { - Ok(Config { - creation_enabled: self.creation_enabled.unwrap_or(DEFAULT_ENABLED), - create_every_ms: self - .create_every_ms - .get() - .map_or(DEFAULT_SNAPSHOT_CREATE_EVERY_MS, UserDuration::get), - store_path: self - .store_path - .get() - .unwrap_or_else(|| PathBuf::from(DEFAULT_SNAPSHOT_PATH)), - }) - } -} - -impl FromEnv for UserLayer { - fn from_env(env: &impl ReadEnv) -> FromEnvResult - where - Self: Sized, - { - let mut emitter = Emitter::new(); - - let store_path = ParseEnvResult::parse_simple( - &mut emitter, - env, - "SNAPSHOT_STORE", - "snapshot.store_path", - ) - .into(); - let creation_enabled = ParseEnvResult::parse_simple( - &mut emitter, - env, - "SNAPSHOT_CREATION_ENABLED", - "snapshot.creation_enabled", - ) - .into(); - - emitter.finish()?; - - Ok(Self { - store_path, - creation_enabled, - ..Self::default() - }) - } -} diff --git a/config/src/parameters/sumeragi.rs b/config/src/parameters/sumeragi.rs deleted file mode 100644 index 62d9ea9ce42..00000000000 --- a/config/src/parameters/sumeragi.rs +++ /dev/null @@ -1,113 +0,0 @@ -//! `Sumeragi` configuration. Contains both block commit and Gossip-related configuration. -use std::{fmt::Debug, fs::File, io::BufReader, num::NonZeroU32, path::Path, time::Duration}; - -use eyre::eyre; -use iroha_config_base::{ - Complete, CompleteError, CompleteResult, FromEnvDefaultFallback, Merge, UserDuration, UserField, -}; -use iroha_data_model::prelude::*; -use iroha_primitives::unique_vec::UniqueVec; -use serde::{Deserialize, Serialize}; - -use self::default::*; - -/// Module with a set of default values. -pub mod default { - use std::{ - num::{NonZeroU32, NonZeroU64}, - time::Duration, - }; - - use nonzero_ext::nonzero; - - pub const DEFAULT_TRANSACTION_GOSSIP_PERIOD: Duration = Duration::from_secs(1); - pub const DEFAULT_MAX_TRANSACTIONS_IN_BLOCK: u32 = 2_u32.pow(9); - - pub const DEFAULT_BLOCK_GOSSIP_PERIOD: Duration = Duration::from_secs(10); - - pub const DEFAULT_MAX_TRANSACTIONS_PER_GOSSIP: NonZeroU32 = nonzero!(500u32); - pub const DEFAULT_MAX_BLOCKS_PER_GOSSIP: NonZeroU32 = nonzero!(4u32); - - // /// Default estimation of consensus duration. - // #[allow(clippy::integer_division)] - // pub const DEFAULT_CONSENSUS_ESTIMATION_MS: u64 = - // DEFAULT_BLOCK_TIME_MS + (DEFAULT_COMMIT_TIME_LIMIT_MS / 2); -} - -#[derive(Deserialize, Serialize, Debug, Default, Merge)] -#[serde(deny_unknown_fields, default)] -pub struct UserLayer { - pub block_gossip_period: UserField, - pub max_blocks_per_gossip: UserField, - pub max_transactions_per_gossip: UserField, - pub transaction_gossip_period: UserField, - pub trusted_peers: UserTrustedPeers, -} - -impl Complete for UserLayer { - type Output = Config; - - fn complete(self) -> CompleteResult { - Ok(Config { - block_gossip_period: self - .block_gossip_period - .map_or(DEFAULT_BLOCK_GOSSIP_PERIOD, UserDuration::get), - max_blocks_per_gossip: self - .max_blocks_per_gossip - .unwrap_or(DEFAULT_MAX_BLOCKS_PER_GOSSIP), - max_transactions_per_gossip: self - .max_transactions_per_gossip - .unwrap_or(DEFAULT_MAX_TRANSACTIONS_PER_GOSSIP), - transaction_gossip_period: self - .transaction_gossip_period - .map_or(DEFAULT_TRANSACTION_GOSSIP_PERIOD, UserDuration::get), - trusted_peers: TrustedPeers { - peers: construct_unique_vec(self.trusted_peers.peers) - .map_err(CompleteError::Custom)?, - }, - }) - } -} - -#[derive(Debug)] -pub struct Config { - pub block_gossip_period: Duration, - pub max_blocks_per_gossip: NonZeroU32, - pub max_transactions_per_gossip: NonZeroU32, - pub transaction_gossip_period: Duration, - pub trusted_peers: TrustedPeers, -} - -#[derive(Debug)] -pub struct TrustedPeers { - pub peers: UniqueVec, -} - -#[derive(Deserialize, Serialize, Default, PartialEq, Eq, Debug, Clone)] -#[serde(transparent)] -pub struct UserTrustedPeers { - // FIXME: doesn't raise an error on finding duplicates during deserialization - pub peers: Vec, -} - -impl Merge for UserTrustedPeers { - fn merge(&mut self, mut other: Self) { - self.peers.append(other.peers.as_mut()) - } -} - -impl FromEnvDefaultFallback for UserLayer {} - -// FIXME: handle duplicates properly, not here, and with details -fn construct_unique_vec( - unchecked: Vec, -) -> Result, eyre::Report> { - let mut unique = UniqueVec::new(); - for x in unchecked.into_iter() { - let pushed = unique.push(x); - if !pushed { - Err(eyre!("found duplicate"))? - } - } - Ok(unique) -} diff --git a/config/src/parameters/telemetry.rs b/config/src/parameters/telemetry.rs deleted file mode 100644 index 8f06e623b89..00000000000 --- a/config/src/parameters/telemetry.rs +++ /dev/null @@ -1,119 +0,0 @@ -//! Module for telemetry-related configuration and structs. -use std::{ - num::{NonZeroU64, NonZeroU8}, - path::PathBuf, - time::Duration, -}; - -use eyre::eyre; -use iroha_config_base::{ - Complete, CompleteError, CompleteResult, FromEnv, FromEnvDefaultFallback, FromEnvResult, Merge, - ReadEnv, UserDuration, UserField, -}; -use serde::{Deserialize, Serialize}; -use url::Url; - -use crate::parameters::telemetry::retry_period::{ - DEFAULT_MAX_RETRY_DELAY_EXPONENT, DEFAULT_MIN_RETRY_PERIOD, -}; - -#[derive(Clone, Deserialize, Serialize, Debug, Default, Merge)] -#[serde(deny_unknown_fields, default)] -pub struct UserLayer { - /// The node's name to be seen on the telemetry - pub name: UserField, - /// The url of the telemetry, e.g., ws://127.0.0.1:8001/submit - pub url: UserField, - /// The minimum period of time in seconds to wait before reconnecting - pub min_retry_period: UserField, - /// The maximum exponent of 2 that is used for increasing delay between reconnections - pub max_retry_delay_exponent: UserField, - /// Dev telemetry configuration - #[serde(default)] - pub dev: DevUserLayer, -} - -#[derive(Clone, Deserialize, Serialize, Debug, Default, Merge)] -pub struct DevUserLayer { - /// The filepath that to write dev-telemetry to - pub file: UserField, -} - -#[derive(Debug)] -pub struct Config { - regular: Option, - dev: Option, -} - -/// Complete configuration needed to start regular telemetry. -#[derive(Debug)] -pub struct RegularTelemetryConfig { - #[allow(missing_docs)] - pub name: String, - #[allow(missing_docs)] - pub url: Url, - #[allow(missing_docs)] - pub min_retry_period: Duration, - #[allow(missing_docs)] - pub max_retry_delay_exponent: u8, -} - -/// Complete configuration needed to start dev telemetry. -#[derive(Debug)] -pub struct DevTelemetryConfig { - #[allow(missing_docs)] - pub file: PathBuf, -} - -impl Complete for UserLayer { - type Output = Config; - - fn complete(self) -> CompleteResult { - let Self { - name, - url, - max_retry_delay_exponent, - min_retry_period, - dev: DevUserLayer { file }, - } = self; - - let regular = match (name.get(), url.get()) { - (Some(name), Some(url)) => Some(RegularTelemetryConfig { - name, - url, - max_retry_delay_exponent: max_retry_delay_exponent - .get() - .unwrap_or(DEFAULT_MAX_RETRY_DELAY_EXPONENT), - min_retry_period: min_retry_period - .get() - .map_or(DEFAULT_MIN_RETRY_PERIOD, UserDuration::get), - }), - (None, None) => None, - // TODO improve error detail - _ => Err(eyre!( - "telemetry.name and telemetry.file should be set together" - )) - .map_err(CompleteError::Custom)?, - }; - - let dev = file - .as_ref() - .map(|file| DevTelemetryConfig { file: file.clone() }); - - Ok(Config { regular, dev }) - } -} - -impl FromEnvDefaultFallback for UserLayer {} - -/// `RetryPeriod` configuration -pub mod retry_period { - use std::{num::NonZeroU8, time::Duration}; - - use nonzero_ext::nonzero; - - /// Default minimal retry period - pub const DEFAULT_MIN_RETRY_PERIOD: Duration = Duration::from_secs(1); - /// Default maximum exponent for the retry delay - pub const DEFAULT_MAX_RETRY_DELAY_EXPONENT: u8 = 4; -} diff --git a/config/src/parameters/torii.rs b/config/src/parameters/torii.rs deleted file mode 100644 index 018dd145714..00000000000 --- a/config/src/parameters/torii.rs +++ /dev/null @@ -1,67 +0,0 @@ -//! `Torii` configuration as well as the default values for the URLs used for the main endpoints: `p2p`, `telemetry`, but not `api`. - -use std::time::Duration; - -use iroha_config_base::{ - ByteSize, Complete, CompleteError, CompleteResult, Emitter, FromEnv, FromEnvResult, Merge, - ParseEnvResult, ReadEnv, UserDuration, UserField, -}; -use iroha_primitives::addr::{socket_addr, SocketAddr}; -use serde::{Deserialize, Serialize}; - -const DEFAULT_MAX_CONTENT_LENGTH: u64 = 2_u64.pow(20) * 16; -const DEFAULT_QUERY_IDLE_TIME: Duration = Duration::from_secs(30); - -#[derive(Debug, Clone, Deserialize, Serialize, Default, Merge)] -#[serde(deny_unknown_fields, default)] -pub struct UserLayer { - pub address: UserField, - pub max_content_len: UserField, - pub query_idle_time: UserField, -} - -#[derive(Debug)] -pub struct Config { - pub address: SocketAddr, - pub max_content_len: ByteSize, - pub query_idle_time: Duration, -} - -impl Complete for UserLayer { - type Output = Config; - - fn complete(self) -> CompleteResult { - Ok(Config { - address: self - .address - .get() - .ok_or_else(|| CompleteError::missing_field("torii.address"))?, - max_content_len: self - .max_content_len - .get() - .unwrap_or(ByteSize(DEFAULT_MAX_CONTENT_LENGTH)), - query_idle_time: self - .query_idle_time - .map_or(DEFAULT_QUERY_IDLE_TIME, UserDuration::get), - }) - } -} - -impl FromEnv for UserLayer { - fn from_env(env: &impl ReadEnv) -> FromEnvResult - where - Self: Sized, - { - let mut emitter = Emitter::new(); - - let address = - ParseEnvResult::parse_simple(&mut emitter, env, "API_ADDRESS", "torii.address").into(); - - emitter.finish()?; - - Ok(Self { - address, - ..Self::default() - }) - } -} diff --git a/config/src/parameters/user_layer.rs b/config/src/parameters/user_layer.rs new file mode 100644 index 00000000000..c9300e75fd2 --- /dev/null +++ b/config/src/parameters/user_layer.rs @@ -0,0 +1,964 @@ +use std::{ + fmt::Debug, + fs::File, + io::Read, + num::{NonZeroU32, NonZeroU64, NonZeroUsize}, + ops::{Add, Div}, + path::{Path, PathBuf}, +}; + +use eyre::{eyre, Report, WrapErr}; +use iroha_config_base::{ + ByteSize, Complete, CompleteError, CompleteResult, Emitter, FromEnv, FromEnvDefaultFallback, + FromEnvResult, Merge, ParseEnvResult, ReadEnv, UserDuration, UserField, +}; +use iroha_crypto::{KeyPair, PrivateKey, PublicKey}; +use iroha_data_model::{ + metadata::Limits as MetadataLimits, peer::PeerId, transaction::TransactionLimits, LengthLimits, + Level, +}; +use iroha_primitives::{addr::SocketAddr, unique_vec::UniqueVec}; +use serde::{Deserialize, Serialize}; +use url::Url; + +use super::defaults::{ + chain_wide::*, kura::*, logger::*, queue::*, snapshot::*, telemetry::*, torii::*, +}; +use crate::{kura::Mode, logger::Format, parameters::actual}; + +#[derive(Deserialize, Serialize, Debug, Default, Merge)] +#[serde(deny_unknown_fields, default)] +pub struct Root { + iroha: Iroha, + genesis: Genesis, + kura: Kura, + sumeragi: Sumeragi, + network: Network, + logger: Logger, + queue: Queue, + snapshot: Snapshot, + telemetry: Telemetry, + torii: Torii, + chain_wide: ChainWide, +} + +impl Root { + pub fn from_toml(path: impl AsRef) -> eyre::Result { + let contents = { + let mut file = File::open(path.as_ref()).wrap_err_with(|| { + eyre!("cannot open file at location `{}`", path.as_ref().display()) + })?; + let mut contents = String::new(); + file.read_to_string(&mut contents)?; + contents + }; + let mut parsed: Self = toml::from_str(&contents).wrap_err("failed to parse toml")?; + parsed.normalise_paths( + path.as_ref() + .parent() + .expect("the config file path could not be empty or root"), + ); + Ok(parsed) + } + + fn normalise_paths(&mut self, relative_to: impl AsRef) { + let path = relative_to.as_ref(); + + macro_rules! patch { + ($value:expr) => { + $value.as_mut().map(|x| { + *x = path.join(&x); + }) + }; + } + + patch!(self.genesis.file); + patch!(self.snapshot.store_path); + patch!(self.kura.block_store_path); + patch!(self.telemetry.dev.file); + } + + // FIXME workaround the inconvenient way `Merge::merge` works + pub fn merge_chain(mut self, other: Self) -> Self { + self.merge(other); + self + } +} + +impl Complete for Root { + type Output = actual::Root; + + fn complete(self) -> CompleteResult { + let mut emitter = Emitter::new(); + + macro_rules! complete_nested { + ($item:expr) => { + match iroha_config_base::Complete::complete($item) { + Ok(value) => Some(value), + Err(error) => { + emitter.emit_collection(error); + None + } + } + }; + } + + let iroha = complete_nested!(self.iroha); + let genesis = complete_nested!(self.genesis); + let kura = complete_nested!(self.kura); + let sumeragi = complete_nested!(self.sumeragi); + let network = complete_nested!(self.network); + let logger = complete_nested!(self.logger); + let queue = complete_nested!(self.queue); + let snapshot = complete_nested!(self.snapshot); + let telemetries = complete_nested!(self.telemetry); + let torii_and_query = complete_nested!(self.torii); + let chain_wide = complete_nested!(self.chain_wide); + + emitter.finish()?; + + let (regular_telemetry, dev_telemetry) = telemetries.unwrap(); + let (torii, live_query_store) = torii_and_query.unwrap(); + let (block_sync, transaction_gossiper) = network.unwrap(); + + Ok(actual::Root { + iroha: iroha.unwrap(), + genesis: genesis.unwrap(), + kura: kura.unwrap(), + sumeragi: sumeragi.unwrap(), + block_sync, + transaction_gossiper, + logger: logger.unwrap(), + queue: queue.unwrap(), + snapshot: snapshot.unwrap(), + regular_telemetry, + dev_telemetry, + torii, + live_query_store, + chain_wide: chain_wide.unwrap(), + }) + } +} + +impl FromEnv for Root { + fn from_env(env: &impl ReadEnv) -> FromEnvResult { + fn from_env_nested( + env: &impl ReadEnv, + emitter: &mut Emitter, + ) -> Option { + match FromEnv::from_env(env) { + Ok(parsed) => Some(parsed), + Err(errors) => { + emitter.emit_collection(errors); + None + } + } + } + + let mut emitter = Emitter::new(); + + let iroha = from_env_nested(env, &mut emitter); + let genesis = from_env_nested(env, &mut emitter); + let kura = from_env_nested(env, &mut emitter); + let sumeragi = from_env_nested(env, &mut emitter); + let network = from_env_nested(env, &mut emitter); + let logger = from_env_nested(env, &mut emitter); + let queue = from_env_nested(env, &mut emitter); + let snapshot = from_env_nested(env, &mut emitter); + let telemetry = from_env_nested(env, &mut emitter); + let torii = from_env_nested(env, &mut emitter); + let chain_wide = from_env_nested(env, &mut emitter); + + emitter.finish()?; + + Ok(Self { + iroha: iroha.unwrap(), + genesis: genesis.unwrap(), + kura: kura.unwrap(), + sumeragi: sumeragi.unwrap(), + network: network.unwrap(), + logger: logger.unwrap(), + queue: queue.unwrap(), + snapshot: snapshot.unwrap(), + telemetry: telemetry.unwrap(), + torii: torii.unwrap(), + chain_wide: chain_wide.unwrap(), + }) + } +} + +#[derive(Deserialize, Serialize, Debug, Default, Merge)] +#[serde(deny_unknown_fields, default)] +pub struct Iroha { + pub public_key: UserField, + pub private_key: UserField, + pub p2p_address: UserField, +} + +impl Complete for Iroha { + type Output = actual::Iroha; + + fn complete(self) -> CompleteResult { + let mut emitter = Emitter::::new(); + + let key_pair = match (self.public_key.get(), self.private_key.get()) { + (Some(public_key), Some(private_key)) => { + KeyPair::new(public_key, private_key) + .map(Some) + .wrap_err("failed to construct a key pair from `iroha.public_key` and `iroha.private_key` configuration parameters") + .unwrap_or_else(|report| { + emitter.emit(CompleteError::Custom(report)); + None + }) + }, + (public_key, private_key) => { + if public_key.is_none() { + emitter.emit_missing_field("iroha.public_key"); + } + if private_key.is_none() { + emitter.emit_missing_field("iroha.private_key"); + } + None + } + }; + + if self.p2p_address.is_none() { + emitter.emit_missing_field("iroha.p2p_address"); + } + + emitter.finish()?; + + Ok(actual::Iroha { + key_pair: key_pair.unwrap(), + p2p_address: self.p2p_address.get().unwrap(), + }) + } +} + +pub(crate) fn private_key_from_env( + emitter: &mut Emitter, + env: &impl ReadEnv, + env_key_base: impl AsRef, + name_base: impl AsRef, +) -> ParseEnvResult { + let digest_env = format!("{}_DIGEST", env_key_base.as_ref()); + let digest_name = format!("{}.digest_function", name_base.as_ref()); + let payload_env = format!("{}_PAYLOAD", env_key_base.as_ref()); + let payload_name = format!("{}.payload", name_base.as_ref()); + + let digest_function = ParseEnvResult::parse_simple(emitter, env, &digest_env, &digest_name); + + let payload = env.get(&payload_env).map(ToOwned::to_owned); + + match (digest_function, payload) { + (ParseEnvResult::Value(digest_function), Some(payload)) => { + PrivateKey::from_hex(digest_function, &payload) + .wrap_err_with(|| { + eyre!( + "failed to construct `{}` from `{}` and `{}` environment variables", + name_base.as_ref(), + &digest_env, + &payload_env + ) + }) + .map_or_else( + |report| { + emitter.emit(report); + ParseEnvResult::ParseError + }, + ParseEnvResult::Value, + ) + } + (ParseEnvResult::None, None) | (ParseEnvResult::ParseError, _) => ParseEnvResult::None, + (ParseEnvResult::Value(_), None) => { + emitter.emit(eyre!( + "`{}` env was provided, but `{}` was not", + &digest_env, + &payload_env + )); + ParseEnvResult::ParseError + } + (ParseEnvResult::None, Some(_)) => { + emitter.emit(eyre!( + "`{}` env was provided, but `{}` was not", + &payload_env, + &digest_env + )); + ParseEnvResult::ParseError + } + } +} + +impl FromEnv for Iroha { + fn from_env(env: &impl ReadEnv) -> FromEnvResult + where + Self: Sized, + { + let mut emitter = Emitter::new(); + + let public_key = + ParseEnvResult::parse_simple(&mut emitter, env, "PUBLIC_KEY", "iroha.public_key") + .into(); + let private_key = + private_key_from_env(&mut emitter, env, "PRIVATE_KEY", "iroha.private_key").into(); + let p2p_address = + ParseEnvResult::parse_simple(&mut emitter, env, "P2P_ADDRESS", "iroha.p2p_address") + .into(); + + emitter.finish()?; + + Ok(Self { + public_key, + private_key, + p2p_address, + }) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Default, Merge)] +#[serde(deny_unknown_fields, default)] +pub struct Genesis { + pub public_key: UserField, + pub private_key: UserField, + #[serde(default)] + pub file: UserField, +} + +impl Complete for Genesis { + type Output = actual::Genesis; + + fn complete(self) -> CompleteResult { + let public_key = self + .public_key + .get() + .ok_or_else(|| CompleteError::missing_field("genesis.public_key"))?; + + match (self.private_key.get(), self.file.get()) { + (None, None) => Ok(actual::Genesis::Partial { public_key }), + (Some(private_key), Some(file)) => Ok(actual::Genesis::Full { + key_pair: KeyPair::new(public_key, private_key) + .map_err(GenesisConfigError::from) + .wrap_err("FIXME") + .map_err(CompleteError::Custom)?, + file, + }), + _ => Err(GenesisConfigError::Inconsistent) + .wrap_err("FIXME") + .map_err(CompleteError::Custom)?, + } + } +} + +impl FromEnv for Genesis { + fn from_env(env: &impl ReadEnv) -> FromEnvResult + where + Self: Sized, + { + let mut emitter = Emitter::new(); + + let public_key = ParseEnvResult::parse_simple( + &mut emitter, + env, + "GENESIS_PUBLIC_KEY", + "genesis.public_key", + ) + .into(); + let private_key = private_key_from_env( + &mut emitter, + env, + "GENESIS_PRIVATE_KEY", + "genesis.private_key", + ) + .into(); + let file = + ParseEnvResult::parse_simple(&mut emitter, env, "GENESIS_FILE", "genesis.file").into(); + + emitter.finish()?; + + Ok(Self { + public_key, + private_key, + file, + }) + } +} + +#[derive(Debug, displaydoc::Display, thiserror::Error)] +pub enum GenesisConfigError { + /// `genesis.file` and `genesis.private_key` should be set together + Inconsistent, + /// failed to construct the genesis's keypair using `genesis.public_key` and `genesis.private_key` configuration parameters + KeyPair(#[from] iroha_crypto::error::Error), +} + +/// `Kura` configuration. +#[derive(Clone, Deserialize, Serialize, Debug, Default, Merge)] +#[serde(deny_unknown_fields, default)] +pub struct Kura { + pub init_mode: Option, + pub block_store_path: Option, + pub debug: KuraDebug, +} + +#[derive(Clone, Deserialize, Serialize, Debug, Default, Merge)] +#[serde(deny_unknown_fields, default)] +pub struct KuraDebug { + output_new_blocks: Option, +} + +impl Complete for Kura { + type Output = actual::Kura; + + fn complete(self) -> CompleteResult { + Ok(actual::Kura { + init_mode: self.init_mode.unwrap_or_default(), + block_store_path: self + .block_store_path + .unwrap_or_else(|| PathBuf::from(DEFAULT_BLOCK_STORE_PATH)), + debug_output_new_blocks: self.debug.output_new_blocks.unwrap_or(false), + }) + } +} + +impl FromEnv for Kura { + fn from_env(env: &impl ReadEnv) -> FromEnvResult + where + Self: Sized, + { + let mut emitter = Emitter::new(); + + let init_mode = + ParseEnvResult::parse_simple(&mut emitter, env, "KURA_INIT_MODE", "kura.init_mode") + .into(); + let block_store_path = ParseEnvResult::parse_simple( + &mut emitter, + env, + "KURA_BLOCK_STORE", + "kura.block_store_path", + ) + .into(); + let debug_output_new_blocks = ParseEnvResult::parse_simple( + &mut emitter, + env, + "KURA_DEBUG_OUTPUT_NEW_BLOCKS", + "kura.debug.output_new_blocks", + ) + .into(); + + emitter.finish()?; + + Ok(Self { + init_mode, + block_store_path, + debug: KuraDebug { + output_new_blocks: debug_output_new_blocks, + }, + }) + } +} + +#[derive(Deserialize, Serialize, Debug, Default, Merge)] +#[serde(deny_unknown_fields, default)] +pub struct Sumeragi { + pub block_gossip_period: UserField, + pub max_blocks_per_gossip: UserField, + pub max_transactions_per_gossip: UserField, + pub transaction_gossip_period: UserField, + pub trusted_peers: UserTrustedPeers, + pub debug: SumeragiDebug, +} + +#[derive(Deserialize, Serialize, Debug, Default, Merge)] +#[serde(deny_unknown_fields, default)] +pub struct SumeragiDebug { + pub force_soft_fork: UserField, +} + +impl Complete for Sumeragi { + type Output = actual::Sumeragi; + + fn complete(self) -> CompleteResult { + Ok(actual::Sumeragi { + trusted_peers: construct_unique_vec(self.trusted_peers.peers) + .map_err(CompleteError::Custom)?, + debug_force_soft_fork: self.debug.force_soft_fork.unwrap_or(false), + }) + } +} + +impl FromEnvDefaultFallback for Sumeragi {} + +#[derive(Deserialize, Serialize, Default, PartialEq, Eq, Debug, Clone)] +#[serde(transparent)] +pub struct UserTrustedPeers { + // FIXME: doesn't raise an error on finding duplicates during deserialization + pub peers: Vec, +} + +impl Merge for UserTrustedPeers { + fn merge(&mut self, mut other: Self) { + self.peers.append(other.peers.as_mut()) + } +} + +// FIXME: handle duplicates properly, not here, and with details +fn construct_unique_vec( + unchecked: Vec, +) -> Result, eyre::Report> { + let mut unique = UniqueVec::new(); + for x in unchecked.into_iter() { + let pushed = unique.push(x); + if !pushed { + Err(eyre!("found duplicate"))? + } + } + Ok(unique) +} + +#[derive(Deserialize, Serialize, Debug, Default, Merge)] +#[serde(deny_unknown_fields, default)] +pub struct Network { + pub block_gossip_period: UserField, + pub max_blocks_per_gossip: UserField, + pub max_transactions_per_gossip: UserField, + pub transaction_gossip_period: UserField, +} + +impl Complete for Network { + type Output = (actual::BlockSync, actual::TransactionGossiper); + + fn complete(self) -> CompleteResult { + todo!() + } +} + +impl FromEnvDefaultFallback for Network {} + +#[derive(Deserialize, Serialize, Debug, Default, Merge)] +#[serde(deny_unknown_fields, default)] +pub struct Queue { + /// The upper limit of the number of transactions waiting in the queue. + pub max_transactions_in_queue: UserField, + /// The upper limit of the number of transactions waiting in the queue for single user. + /// Use this option to apply throttling. + pub max_transactions_in_queue_per_user: UserField, + /// The transaction will be dropped after this time if it is still in the queue. + pub transaction_time_to_live_ms: UserField, + /// The threshold to determine if a transaction has been tampered to have a future timestamp. + pub future_threshold_ms: UserField, +} + +impl Complete for Queue { + type Output = actual::Queue; + + fn complete(self) -> CompleteResult { + Ok(actual::Queue { + max_transactions_in_queue: self + .max_transactions_in_queue + .unwrap_or(DEFAULT_MAX_TRANSACTIONS_IN_QUEUE), + max_transactions_in_queue_per_user: self + .max_transactions_in_queue_per_user + .unwrap_or(DEFAULT_MAX_TRANSACTIONS_IN_QUEUE), + transaction_time_to_live: self + .transaction_time_to_live_ms + .map_or(DEFAULT_TRANSACTION_TIME_TO_LIVE, UserDuration::get), + future_threshold: self + .future_threshold_ms + .map_or(DEFAULT_FUTURE_THRESHOLD, UserDuration::get), + }) + } +} + +impl FromEnvDefaultFallback for Queue {} + +/// 'Logger' configuration. +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Default, Merge)] +// `tokio_console_addr` is not `Copy`, but warning appears without `tokio-console` feature +#[allow(missing_copy_implementations)] +#[serde(deny_unknown_fields, default)] +pub struct Logger { + /// Level of logging verbosity + pub level: UserField, + /// Output format + pub format: UserField, + #[cfg(feature = "tokio-console")] + /// Address of tokio console (only available under "tokio-console" feature) + pub tokio_console_addr: UserField, +} + +impl Complete for Logger { + type Output = actual::Logger; + + fn complete(self) -> CompleteResult { + Ok(actual::Logger { + level: self.level.unwrap_or_default(), + format: self.format.unwrap_or_default(), + #[cfg(feature = "tokio-console")] + tokio_console_addr: self + .tokio_console_addr + .get() + .unwrap_or_else(|| DEFAULT_TOKIO_CONSOLE_ADDR.clone()), + }) + } +} + +impl FromEnv for Logger { + fn from_env(env: &impl ReadEnv) -> FromEnvResult + where + Self: Sized, + { + let mut emitter = Emitter::new(); + + let level = + ParseEnvResult::parse_simple(&mut emitter, env, "LOG_LEVEL", "logger.level").into(); + let format = + ParseEnvResult::parse_simple(&mut emitter, env, "LOG_FORMAT", "logger.format").into(); + + emitter.finish()?; + + Ok(Self { + level, + format, + ..Self::default() + }) + } +} + +#[derive(Clone, Deserialize, Serialize, Debug, Default, Merge)] +#[serde(deny_unknown_fields, default)] +pub struct Telemetry { + /// The node's name to be seen on the telemetry + pub name: UserField, + /// The url of the telemetry, e.g., ws://127.0.0.1:8001/submit + pub url: UserField, + /// The minimum period of time in seconds to wait before reconnecting + pub min_retry_period: UserField, + /// The maximum exponent of 2 that is used for increasing delay between reconnections + pub max_retry_delay_exponent: UserField, + /// Dev telemetry configuration + #[serde(default)] + pub dev: DevUserLayer, +} + +#[derive(Clone, Deserialize, Serialize, Debug, Default, Merge)] +pub struct DevUserLayer { + /// The filepath that to write dev-telemetry to + pub file: UserField, +} + +impl Complete for Telemetry { + type Output = ( + Option, + Option, + ); + + fn complete(self) -> CompleteResult { + let Self { + name, + url, + max_retry_delay_exponent, + min_retry_period, + dev: DevUserLayer { file }, + } = self; + + let regular = match (name.get(), url.get()) { + (Some(name), Some(url)) => Some(actual::RegularTelemetry { + name, + url, + max_retry_delay_exponent: max_retry_delay_exponent + .get() + .unwrap_or(DEFAULT_MAX_RETRY_DELAY_EXPONENT), + min_retry_period: min_retry_period + .get() + .map_or(DEFAULT_MIN_RETRY_PERIOD, UserDuration::get), + }), + (None, None) => None, + // TODO improve error detail + _ => Err(eyre!( + "telemetry.name and telemetry.file should be set together" + )) + .map_err(CompleteError::Custom)?, + }; + + let dev = file + .as_ref() + .map(|file| actual::DevTelemetry { file: file.clone() }); + + Ok((regular, dev)) + } +} + +impl FromEnvDefaultFallback for Telemetry {} + +#[derive(Debug, Clone, Deserialize, Serialize, Default, Merge)] +#[serde(deny_unknown_fields, default)] +pub struct Snapshot { + /// The period of time to wait between attempts to create new snapshot. + pub create_every_ms: UserField, + /// Path to the directory where snapshots should be stored + pub store_path: UserField, + /// Flag to enable or disable snapshot creation + pub creation_enabled: UserField, +} + +impl Complete for Snapshot { + type Output = actual::Snapshot; + + fn complete(self) -> CompleteResult { + Ok(actual::Snapshot { + creation_enabled: self.creation_enabled.unwrap_or(DEFAULT_ENABLED), + create_every: self + .create_every_ms + .get() + .map_or(DEFAULT_SNAPSHOT_CREATE_EVERY_MS, UserDuration::get), + store_path: self + .store_path + .get() + .unwrap_or_else(|| PathBuf::from(DEFAULT_SNAPSHOT_PATH)), + }) + } +} + +impl FromEnv for Snapshot { + fn from_env(env: &impl ReadEnv) -> FromEnvResult + where + Self: Sized, + { + let mut emitter = Emitter::new(); + + let store_path = ParseEnvResult::parse_simple( + &mut emitter, + env, + "SNAPSHOT_STORE", + "snapshot.store_path", + ) + .into(); + let creation_enabled = ParseEnvResult::parse_simple( + &mut emitter, + env, + "SNAPSHOT_CREATION_ENABLED", + "snapshot.creation_enabled", + ) + .into(); + + emitter.finish()?; + + Ok(Self { + store_path, + creation_enabled, + ..Self::default() + }) + } +} + +#[derive(Deserialize, Serialize, Debug, Default, Merge)] +#[serde(deny_unknown_fields, default)] +pub struct ChainWide { + pub max_transactions_in_block: UserField, + pub block_time: UserField, + pub commit_time: UserField, + pub transaction_limits: UserField, + pub asset_metadata_limits: UserField, + pub asset_definition_metadata_limits: UserField, + pub account_metadata_limits: UserField, + pub domain_metadata_limits: UserField, + pub identifier_length_limits: UserField, + pub wasm_fuel_limit: UserField, + pub wasm_max_memory: UserField>, +} + +impl Complete for ChainWide { + type Output = actual::ChainWide; + + fn complete(self) -> CompleteResult { + Ok(actual::ChainWide { + max_transactions_in_block: self.max_transactions_in_block.unwrap_or(DEFAULT_MAX_TXS), + block_time: self + .block_time + .map_or(DEFAULT_BLOCK_TIME, UserDuration::get), + commit_time: self + .commit_time + .map_or(DEFAULT_COMMIT_TIME_LIMIT, UserDuration::get), + transaction_limits: self + .transaction_limits + .unwrap_or(DEFAULT_TRANSACTION_LIMITS), + asset_metadata_limits: self + .asset_metadata_limits + .unwrap_or(DEFAULT_METADATA_LIMITS), + asset_definition_metadata_limits: self + .asset_definition_metadata_limits + .unwrap_or(DEFAULT_METADATA_LIMITS), + account_metadata_limits: self + .account_metadata_limits + .unwrap_or(DEFAULT_METADATA_LIMITS), + domain_metadata_limits: self + .domain_metadata_limits + .unwrap_or(DEFAULT_METADATA_LIMITS), + identifier_length_limits: self + .identifier_length_limits + .unwrap_or(DEFAULT_IDENT_LENGTH_LIMITS), + wasm_runtime: actual::WasmRuntime { + fuel_limit: self.wasm_fuel_limit.unwrap_or(DEFAULT_WASM_FUEL_LIMIT), + max_memory: self + .wasm_max_memory + .unwrap_or(ByteSize(DEFAULT_WASM_MAX_MEMORY)), + }, + }) + } +} + +impl FromEnvDefaultFallback for ChainWide {} + +#[derive(Debug, Clone, Deserialize, Serialize, Default, Merge)] +#[serde(deny_unknown_fields, default)] +pub struct Torii { + pub address: UserField, + pub max_content_len: UserField>, + pub query_idle_time: UserField, +} + +impl Complete for Torii { + type Output = (actual::Torii, actual::LiveQueryStore); + + fn complete(self) -> CompleteResult { + let torii = actual::Torii { + address: self + .address + .get() + .ok_or_else(|| CompleteError::missing_field("torii.address"))?, + max_content_len: self + .max_content_len + .get() + .unwrap_or(ByteSize(DEFAULT_MAX_CONTENT_LENGTH)), + }; + + let query = actual::LiveQueryStore { + query_idle_time: self + .query_idle_time + .map_or(DEFAULT_QUERY_IDLE_TIME, UserDuration::get), + }; + + Ok((torii, query)) + } +} + +impl FromEnv for Torii { + fn from_env(env: &impl ReadEnv) -> FromEnvResult + where + Self: Sized, + { + let mut emitter = Emitter::new(); + + let address = + ParseEnvResult::parse_simple(&mut emitter, env, "API_ADDRESS", "torii.address").into(); + + emitter.finish()?; + + Ok(Self { + address, + ..Self::default() + }) + } +} + +#[cfg(test)] +mod tests { + use iroha_config_base::{FromEnv, TestEnv}; + + use crate::parameters::user_layer::{Iroha, Root}; + + #[test] + fn parses_private_key_from_env() { + let env = TestEnv::new() + .set("PRIVATE_KEY_DIGEST", "ed25519") + .set("PRIVATE_KEY_PAYLOAD", "8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb"); + + let private_key = Iroha::from_env(&env) + .expect("input is valid, should not fail") + .private_key + .get() + .expect("private key is provided, should not fail"); + + assert_eq!(private_key.digest_function(), "ed25519".parse().unwrap()); + assert_eq!(hex::encode( private_key.payload()), "8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb"); + } + + #[test] + fn fails_to_parse_private_key_in_env_without_digest() { + let env = TestEnv::new().set("PRIVATE_KEY_DIGEST", "ed25519"); + let error = Iroha::from_env(&env).expect_err("private key is incomplete, should fail"); + let expected = expect_test::expect![[r#" + `PRIVATE_KEY_DIGEST` env was provided, but `PRIVATE_KEY_PAYLOAD` was not + + Location: + config/src/parameters/iroha.rs:100:26"#]]; + expected.assert_eq(&format!("{error:?}")); + } + + #[test] + fn fails_to_parse_private_key_in_env_without_payload() { + let env = TestEnv::new().set("PRIVATE_KEY_PAYLOAD", "8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb"); + let error = Iroha::from_env(&env).expect_err("private key is incomplete, should fail"); + let expected = expect_test::expect![[r#" + `PRIVATE_KEY_PAYLOAD` env was provided, but `PRIVATE_KEY_DIGEST` was not + + Location: + config/src/parameters/iroha.rs:108:26"#]]; + expected.assert_eq(&format!("{error:?}")); + } + + #[test] + fn fails_to_parse_private_key_from_env_with_invalid_payload() { + let env = TestEnv::new() + .set("PRIVATE_KEY_DIGEST", "ed25519") + .set("PRIVATE_KEY_PAYLOAD", "foo"); + + let error = Iroha::from_env(&env).expect_err("input is invalid, should fail"); + + let expected = expect_test::expect![[r#" + failed to construct `iroha.private_key` from `PRIVATE_KEY_DIGEST` and `PRIVATE_KEY_PAYLOAD` environment variables + + Caused by: + Key could not be parsed. Odd number of digits + + Location: + config/src/parameters/iroha.rs:82:18"#]]; + expected.assert_eq(&format!("{error:?}")); + } + + #[test] + fn when_payload_provided_but_digest_is_invalid() { + let env = TestEnv::new() + .set("PRIVATE_KEY_DIGEST", "foo") + .set("PRIVATE_KEY_PAYLOAD", "8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb"); + + let error = Iroha::from_env(&env).expect_err("input is invalid, should fail"); + + // TODO: print the bad value and supported ones + let expected = expect_test::expect![[r#" + failed to parse `iroha.private_key.digest_function` field from `PRIVATE_KEY_DIGEST` env variable + + Caused by: + Algorithm not supported + + Location: + config/src/lib.rs:237:14"#]]; + expected.assert_eq(&format!("{error:?}")); + } + + #[test] + fn deserialize_empty_input_works() { + let _layer: Root = toml::from_str("").unwrap(); + } + + #[test] + fn deserialize_iroha_namespace_with_not_all_fields_works() { + let _layer: Root = toml::from_str( + r#" + [iroha] + p2p_address = "127.0.0.1:8080" + "#, + ) + .unwrap(); + } +} diff --git a/config/tests/fixtures.rs b/config/tests/fixtures.rs index b15dc5f72e6..ca400af1f5a 100644 --- a/config/tests/fixtures.rs +++ b/config/tests/fixtures.rs @@ -5,7 +5,7 @@ use std::{ }; use eyre::Result; -use iroha_config::parameters::UserLayer; +use iroha_config::parameters::user_layer::Root; use iroha_config_base::{Complete as _, FromEnv, TestEnv}; fn fixtures_dir() -> PathBuf { @@ -39,7 +39,7 @@ fn test_env_from_file(p: impl AsRef) -> TestEnv { /// it also gives an insight into every single default value #[test] fn minimal_config_snapshot() -> Result<()> { - let config = UserLayer::from_toml(fixtures_dir().join("minimal_config.toml"))?.complete()?; + let config = Root::from_toml(fixtures_dir().join("minimal_config.toml"))?.complete()?; let expected = expect_test::expect![[r#" Config { @@ -136,13 +136,13 @@ fn minimal_config_snapshot() -> Result<()> { #[test] fn config_with_genesis() -> Result<()> { - let _config = UserLayer::from_toml(fixtures_dir().join("with_genesis.toml"))?.complete()?; + let _config = Root::from_toml(fixtures_dir().join("with_genesis.toml"))?.complete()?; Ok(()) } #[test] fn missing_fields() -> Result<()> { - let error = UserLayer::from_toml(fixtures_dir().join("missing_fields.toml"))? + let error = Root::from_toml(fixtures_dir().join("missing_fields.toml"))? .complete() .expect_err("should fail with missing fields"); @@ -159,7 +159,7 @@ fn missing_fields() -> Result<()> { #[test] fn extra_fields() { - let error = UserLayer::from_toml(fixtures_dir().join("extra_fields.toml")) + let error = Root::from_toml(fixtures_dir().join("extra_fields.toml")) .expect_err("should fail with extra fields"); let expected = expect_test::expect![[r#" @@ -174,7 +174,7 @@ fn extra_fields() { #[test] fn inconsistent_genesis_config() -> Result<()> { - let error = UserLayer::from_toml(fixtures_dir().join("inconsistent_genesis.toml"))? + let error = Root::from_toml(fixtures_dir().join("inconsistent_genesis.toml"))? .complete() .expect_err("should fail with bad genesis config"); @@ -190,7 +190,7 @@ fn inconsistent_genesis_config() -> Result<()> { fn full_envs_set_is_consumed() -> Result<()> { let env = test_env_from_file(fixtures_dir().join("full.env")); - let layer = UserLayer::from_env(&env)?; + let layer = Root::from_env(&env)?; assert_eq!(env.unvisited(), HashSet::new()); @@ -306,8 +306,8 @@ fn multiple_env_parsing_errors() { fn config_from_file_and_env() -> Result<()> { let env = test_env_from_file(fixtures_dir().join("config_and_env.env")); - let _config = UserLayer::from_toml(fixtures_dir().join("config_and_env.toml"))? - .merge_chain(UserLayer::from_env(&env)?) + let _config = Root::from_toml(fixtures_dir().join("config_and_env.toml"))? + .merge_chain(Root::from_env(&env)?) .complete()?; Ok(()) diff --git a/core/src/block.rs b/core/src/block.rs index e87ce6e972d..af94c1c42fe 100644 --- a/core/src/block.rs +++ b/core/src/block.rs @@ -6,7 +6,7 @@ //! [`Block`]s are organised into a linear sequence over time (also known as the block chain). use std::error::Error as _; -use iroha_config::sumeragi::default::DEFAULT_CONSENSUS_ESTIMATION_MS; +use iroha_config::parameters::defaults::chain_wide::DEFAULT_CONSENSUS_ESTIMATION; use iroha_crypto::{HashOf, KeyPair, MerkleTree, SignatureOf, SignaturesOf}; use iroha_data_model::{ block::*, @@ -144,7 +144,10 @@ mod pending { .as_millis() .try_into() .expect("Time should fit into u64"), - consensus_estimation_ms: DEFAULT_CONSENSUS_ESTIMATION_MS, + consensus_estimation_ms: DEFAULT_CONSENSUS_ESTIMATION + .as_millis() + .try_into() + .expect("Time should fit into u64"), height: previous_height + 1, view_change_index, previous_block_hash, @@ -437,7 +440,10 @@ mod valid { BlockBuilder(Chained(BlockPayload { header: BlockHeader { timestamp_ms: 0, - consensus_estimation_ms: DEFAULT_CONSENSUS_ESTIMATION_MS, + consensus_estimation_ms: DEFAULT_CONSENSUS_ESTIMATION + .as_millis() + .try_into() + .expect("Should never overflow?"), height: 2, view_change_index: 0, previous_block_hash: None, diff --git a/core/src/block_sync.rs b/core/src/block_sync.rs index 2ff4ffe05ca..b8dd3ce9f4a 100644 --- a/core/src/block_sync.rs +++ b/core/src/block_sync.rs @@ -1,7 +1,7 @@ //! This module contains structures and messages for synchronization of blocks between peers. -use std::{fmt::Debug, sync::Arc, time::Duration}; +use std::{fmt::Debug, num::NonZeroU32, sync::Arc, time::Duration}; -use iroha_config::block_sync::Configuration; +use iroha_config::parameters::actual::BlockSync as Config; use iroha_crypto::HashOf; use iroha_data_model::{block::SignedBlock, prelude::*}; use iroha_logger::prelude::*; @@ -36,7 +36,7 @@ pub struct BlockSynchronizer { kura: Arc, peer_id: PeerId, gossip_period: Duration, - block_batch_size: u32, + block_batch_size: NonZeroU32, network: IrohaNetwork, latest_hash: Option>, previous_hash: Option>, @@ -105,7 +105,7 @@ impl BlockSynchronizer { /// Create [`Self`] from [`Configuration`] pub fn from_configuration( - config: &Configuration, + config: &Config, sumeragi: SumeragiHandle, kura: Arc, peer_id: PeerId, @@ -117,8 +117,8 @@ impl BlockSynchronizer { peer_id, sumeragi, kura, - gossip_period: Duration::from_millis(config.gossip_period_ms), - block_batch_size: config.block_batch_size, + gossip_period: config.gossip_period, + block_batch_size: config.batch_size, network, latest_hash, previous_hash, @@ -191,10 +191,11 @@ pub mod message { previous_hash, peer_id, }) => { - if block_sync.block_batch_size == 0 { - warn!("Error: not sending any blocks as batch_size is equal to zero."); - return; - } + // FIXME: is it okay to remove this behaviour? + // if block_sync.block_batch_size == 0 { + // warn!("Error: not sending any blocks as batch_size is equal to zero."); + // return; + // } let local_latest_block_hash = block_sync.latest_hash; if *latest_hash == local_latest_block_hash || *previous_hash == local_latest_block_hash @@ -214,7 +215,7 @@ pub mod message { }; let blocks = (start_height..) - .take(1 + block_sync.block_batch_size as usize) + .take(1 + block_sync.block_batch_size.get() as usize) .map_while(|height| block_sync.kura.get_block_by_height(height)) .skip_while(|block| Some(block.hash()) == *latest_hash) .map(|block| (*block).clone()) diff --git a/core/src/executor.rs b/core/src/executor.rs index 62af571fe49..10a076fdb0e 100644 --- a/core/src/executor.rs +++ b/core/src/executor.rs @@ -157,7 +157,7 @@ impl Executor { let runtime = wasm::RuntimeBuilder::::new() .with_engine(wsv.engine.clone()) // Cloning engine is cheap, see [`wasmtime::Engine`] docs - .with_configuration(wsv.config.wasm_runtime_config) + .with_configuration(wsv.config.wasm_runtime) .build()?; runtime.execute_executor_validate_transaction( @@ -191,7 +191,7 @@ impl Executor { let runtime = wasm::RuntimeBuilder::::new() .with_engine(wsv.engine.clone()) // Cloning engine is cheap, see [`wasmtime::Engine`] docs - .with_configuration(wsv.config.wasm_runtime_config) + .with_configuration(wsv.config.wasm_runtime) .build()?; runtime.execute_executor_validate_instruction( @@ -224,7 +224,7 @@ impl Executor { Self::UserProvided(UserProvidedExecutor(loaded_executor)) => { let runtime = wasm::RuntimeBuilder::::new() .with_engine(wsv.engine.clone()) // Cloning engine is cheap, see [`wasmtime::Engine`] docs - .with_configuration(wsv.config.wasm_runtime_config) + .with_configuration(wsv.config.wasm_runtime) .build()?; runtime.execute_executor_validate_query( @@ -259,7 +259,7 @@ impl Executor { let runtime = wasm::RuntimeBuilder::::new() .with_engine(wsv.engine.clone()) // Cloning engine is cheap, see [`wasmtime::Engine`] docs - .with_configuration(wsv.config.wasm_runtime_config) + .with_configuration(wsv.config.wasm_runtime) .build()?; runtime diff --git a/core/src/gossiper.rs b/core/src/gossiper.rs index e9dbe4604e4..8eefccf25a6 100644 --- a/core/src/gossiper.rs +++ b/core/src/gossiper.rs @@ -1,8 +1,8 @@ //! Gossiper is actor which is responsible for transaction gossiping -use std::{sync::Arc, time::Duration}; +use std::{num::NonZeroU32, sync::Arc, time::Duration}; -use iroha_config::sumeragi::Configuration; +use iroha_config::parameters::actual::TransactionGossiper as Config; use iroha_data_model::{transaction::SignedTransaction, ChainId}; use iroha_p2p::Broadcast; use parity_scale_codec::{Decode, Encode}; @@ -35,7 +35,7 @@ pub struct TransactionGossiper { chain_id: ChainId, /// The size of batch that is being gossiped. Smaller size leads /// to longer time to synchronise, useful if you have high packet loss. - gossip_batch_size: u32, + gossip_batch_size: NonZeroU32, /// The time between gossiping. More frequent gossiping shortens /// the time to sync, but can overload the network. gossip_period: Duration, @@ -61,7 +61,7 @@ impl TransactionGossiper { pub fn from_configuration( chain_id: ChainId, // Currently we are using configuration parameters from sumeragi not to break configuration - configuration: &Configuration, + config: &Config, network: IrohaNetwork, queue: Arc, sumeragi: SumeragiHandle, @@ -72,8 +72,8 @@ impl TransactionGossiper { queue, sumeragi, network, - gossip_batch_size: configuration.gossip_batch_size, - gossip_period: Duration::from_millis(configuration.gossip_period_ms), + gossip_batch_size: config.batch_size, + gossip_period: config.gossip_period, wsv, } } @@ -101,7 +101,7 @@ impl TransactionGossiper { fn gossip_transactions(&self) { let txs = self .queue - .n_random_transactions(self.gossip_batch_size, &self.wsv); + .n_random_transactions(self.gossip_batch_size.get(), &self.wsv); if txs.is_empty() { return; diff --git a/core/src/kiso.rs b/core/src/kiso.rs index a7f62be4449..35ed0713bc4 100644 --- a/core/src/kiso.rs +++ b/core/src/kiso.rs @@ -10,7 +10,7 @@ use eyre::Result; use iroha_config::{ client_api::{ConfigurationDTO, Logger as LoggerDTO}, - iroha::Configuration, + parameters::actual::Root, }; use iroha_logger::Level; use tokio::sync::{mpsc, oneshot, watch}; @@ -27,7 +27,7 @@ pub struct KisoHandle { impl KisoHandle { /// Spawn a new actor - pub fn new(state: Configuration) -> Self { + pub fn new(state: Root) -> Self { let (actor_sender, actor_receiver) = mpsc::channel(DEFAULT_CHANNEL_SIZE); let (log_level_update, _) = watch::channel(state.logger.level); let mut actor = Actor { @@ -106,7 +106,7 @@ pub enum Error { struct Actor { handle: mpsc::Receiver, - state: Configuration, + state: Root, // Current implementation is somewhat not scalable in terms of code writing: for any // future dynamic parameter, it will require its own `subscribe_on_` function in [`KisoHandle`], // new channel here, and new [`Message`] variant. If boilerplate expands, a more general solution will be @@ -151,20 +151,20 @@ mod tests { use std::time::Duration; use iroha_config::{ - base::proxy::LoadFromDisk, client_api::{ConfigurationDTO, Logger as LoggerDTO}, - iroha::{Configuration, ConfigurationProxy}, + parameters::actual::Root, }; use super::*; - fn test_config() -> Configuration { - // FIXME Specifying path here might break! Moreover, if the file is not found, - // the error will say that `public_key` is missing! - // Hopefully this will change: https://github.com/hyperledger/iroha/issues/2585 - ConfigurationProxy::from_path("../config/iroha_test_config.json") - .build() - .unwrap() + fn test_config() -> Root { + todo!() + // // FIXME Specifying path here might break! Moreover, if the file is not found, + // // the error will say that `public_key` is missing! + // // Hopefully this will change: https://github.com/hyperledger/iroha/issues/2585 + // ConfigurationProxy::from_path("../config/iroha_test_config.json") + // .build() + // .unwrap() } #[tokio::test] diff --git a/core/src/kura.rs b/core/src/kura.rs index f248248e247..5777a9ff50e 100644 --- a/core/src/kura.rs +++ b/core/src/kura.rs @@ -10,7 +10,7 @@ use std::{ sync::Arc, }; -use iroha_config::kura::{Configuration, Mode}; +use iroha_config::{kura::Mode, parameters::actual::Kura as Config}; use iroha_crypto::{Hash, HashOf}; use iroha_data_model::block::SignedBlock; use iroha_logger::prelude::*; @@ -49,7 +49,7 @@ impl Kura { /// Fails if there are filesystem errors when trying /// to access the block store indicated by the provided /// path. - pub fn new(config: &Configuration) -> Result> { + pub fn new(config: &Config) -> Result> { let block_store_path = Path::new(&config.block_store_path); let mut block_store = BlockStore::new(block_store_path, LockStatus::Unlocked); block_store.create_files_if_they_do_not_exist()?; @@ -1049,7 +1049,7 @@ mod tests { #[tokio::test] async fn strict_init_kura() { let temp_dir = TempDir::new().unwrap(); - Kura::new(&Configuration { + Kura::new(&Config { init_mode: Mode::Strict, block_store_path: temp_dir.path().to_str().unwrap().into(), debug_output_new_blocks: false, diff --git a/core/src/query/store.rs b/core/src/query/store.rs index 8e8c83c0687..024daf0cb4f 100644 --- a/core/src/query/store.rs +++ b/core/src/query/store.rs @@ -7,7 +7,7 @@ use std::{ }; use indexmap::IndexMap; -use iroha_config::live_query_store::Configuration; +use iroha_config::parameters::actual::LiveQueryStore as Config; use iroha_data_model::{ asset::AssetValue, query::{ @@ -74,10 +74,10 @@ pub struct LiveQueryStore { impl LiveQueryStore { /// Construct [`LiveQueryStore`] from configuration. - pub fn from_configuration(cfg: Configuration) -> Self { + pub fn from_configuration(cfg: Config) -> Self { Self { queries: IndexMap::new(), - query_idle_time: Duration::from_millis(cfg.query_idle_time_ms.into()), + query_idle_time: cfg.query_idle_time, } } @@ -86,13 +86,7 @@ impl LiveQueryStore { /// /// Not marked as `#[cfg(test)]` because it is used in benches as well. pub fn test() -> Self { - use iroha_config::base::proxy::Builder as _; - - LiveQueryStore::from_configuration( - iroha_config::live_query_store::ConfigurationProxy::default() - .build() - .expect("Failed to build LiveQueryStore configuration from proxy"), - ) + Self::from_configuration(Config::default()) } /// Start [`LiveQueryStore`]. Requires a [`tokio::runtime::Runtime`] being run diff --git a/core/src/queue.rs b/core/src/queue.rs index 53eeb75f4fb..f7846b2abc4 100644 --- a/core/src/queue.rs +++ b/core/src/queue.rs @@ -1,11 +1,12 @@ //! Module with queue actor use core::time::Duration; +use std::num::NonZeroUsize; use crossbeam_queue::ArrayQueue; use dashmap::{mapref::entry::Entry, DashMap}; use eyre::{Report, Result}; use indexmap::IndexSet; -use iroha_config::queue::Configuration; +use iroha_config::parameters::actual::Queue as Config; use iroha_crypto::HashOf; use iroha_data_model::{account::AccountId, transaction::prelude::*}; use iroha_logger::{debug, trace, warn}; @@ -53,9 +54,9 @@ pub struct Queue { /// Amount of transactions per user in the queue txs_per_user: DashMap, /// The maximum number of transactions in the queue - max_txs: usize, + max_txs: NonZeroUsize, /// The maximum number of transactions in the queue per user. Used to apply throttling - max_txs_per_user: usize, + max_txs_per_user: NonZeroUsize, /// Length of time after which transactions are dropped. pub tx_time_to_live: Duration, /// A point in time that is considered `Future` we cannot use @@ -98,15 +99,15 @@ pub struct Failure { impl Queue { /// Makes queue from configuration - pub fn from_configuration(cfg: &Configuration) -> Self { + pub fn from_configuration(cfg: &Config) -> Self { Self { - tx_hashes: ArrayQueue::new(cfg.max_transactions_in_queue as usize), + tx_hashes: ArrayQueue::new(cfg.max_transactions_in_queue.get()), accepted_txs: DashMap::new(), txs_per_user: DashMap::new(), - max_txs: cfg.max_transactions_in_queue as usize, - max_txs_per_user: cfg.max_transactions_in_queue_per_user as usize, - tx_time_to_live: Duration::from_millis(cfg.transaction_time_to_live_ms), - future_threshold: Duration::from_millis(cfg.future_threshold_ms), + max_txs: cfg.max_transactions_in_queue, + max_txs_per_user: cfg.max_transactions_in_queue_per_user, + tx_time_to_live: cfg.transaction_time_to_live, + future_threshold: cfg.future_threshold, } } @@ -209,7 +210,7 @@ impl Queue { } Entry::Vacant(entry) => entry, }; - if txs_len >= self.max_txs { + if txs_len >= self.max_txs.get() { warn!( max = self.max_txs, "Achieved maximum amount of transactions" @@ -349,7 +350,7 @@ impl Queue { } Entry::Occupied(mut occupied) => { let txs = *occupied.get(); - if txs >= self.max_txs_per_user { + if txs >= self.max_txs_per_user.get() { warn!( max_txs_per_user = self.max_txs_per_user, %account_id, @@ -380,9 +381,9 @@ impl Queue { #[cfg(test)] mod tests { - use std::{str::FromStr, sync::Arc, thread, time::Duration}; + use std::{ops::Mul, str::FromStr, sync::Arc, thread, time::Duration}; - use iroha_config::{base::proxy::Builder, queue::ConfigurationProxy}; + // use iroha_config::{base::proxy::Builder, queue::ConfigurationProxy}; use iroha_data_model::{prelude::*, transaction::TransactionLimits}; use iroha_primitives::must_use::MustUse; use rand::Rng as _; @@ -425,6 +426,14 @@ mod tests { World::with([domain], PeersIds::new()) } + fn config_factory() -> Config { + Config { + transaction_time_to_live: Duration::from_secs(100), + max_transactions_in_queue: 100.try_into().unwrap(), + ..Config::default() + } + } + #[test] async fn push_tx() { let key_pair = KeyPair::generate(); @@ -436,13 +445,7 @@ mod tests { query_handle, )); - let queue = Queue::from_configuration(&Configuration { - transaction_time_to_live_ms: 100_000, - max_transactions_in_queue: 100, - ..ConfigurationProxy::default() - .build() - .expect("Default queue config should always build") - }); + let queue = Queue::from_configuration(&config_factory()); queue .push(accepted_tx("alice@wonderland", &key_pair), &wsv) @@ -451,7 +454,7 @@ mod tests { #[test] async fn push_tx_overflow() { - let max_txs_in_queue = 10; + let max_transactions_in_queue = NonZeroUsize::new(10).unwrap(); let key_pair = KeyPair::generate(); let kura = Kura::blank_kura_for_testing(); @@ -462,15 +465,13 @@ mod tests { query_handle, )); - let queue = Queue::from_configuration(&Configuration { - transaction_time_to_live_ms: 100_000, - max_transactions_in_queue: max_txs_in_queue, - ..ConfigurationProxy::default() - .build() - .expect("Default queue config should always build") + let queue = Queue::from_configuration(&Config { + transaction_time_to_live: Duration::from_secs(100), + max_transactions_in_queue, + ..Config::default() }); - for _ in 0..max_txs_in_queue { + for _ in 0..max_transactions_in_queue.get() { queue .push(accepted_tx("alice@wonderland", &key_pair), &wsv) .expect("Failed to push tx into queue"); @@ -512,13 +513,7 @@ mod tests { )) }; - let queue = Queue::from_configuration(&Configuration { - transaction_time_to_live_ms: 100_000, - max_transactions_in_queue: 100, - ..ConfigurationProxy::default() - .build() - .expect("Default queue config should always build") - }); + let queue = Queue::from_configuration(&config_factory()); let instructions: [InstructionBox; 0] = []; let tx = TransactionBuilder::new(chain_id.clone(), "alice@wonderland".parse().expect("Valid")) @@ -581,13 +576,7 @@ mod tests { kura, query_handle, )); - let queue = Queue::from_configuration(&Configuration { - transaction_time_to_live_ms: 100_000, - max_transactions_in_queue: 100, - ..ConfigurationProxy::default() - .build() - .expect("Default queue config should always build") - }); + let queue = Queue::from_configuration(&config_factory()); for _ in 0..5 { queue .push(accepted_tx("alice@wonderland", &alice_key), &wsv) @@ -611,13 +600,7 @@ mod tests { ); let tx = accepted_tx("alice@wonderland", &alice_key); wsv.transactions.insert(tx.as_ref().hash(), 1); - let queue = Queue::from_configuration(&Configuration { - transaction_time_to_live_ms: 100_000, - max_transactions_in_queue: 100, - ..ConfigurationProxy::default() - .build() - .expect("Default queue config should always build") - }); + let queue = Queue::from_configuration(&config_factory()); assert!(matches!( queue.push(tx, &wsv), Err(Failure { @@ -640,13 +623,7 @@ mod tests { query_handle, ); let tx = accepted_tx("alice@wonderland", &alice_key); - let queue = Queue::from_configuration(&Configuration { - transaction_time_to_live_ms: 100_000, - max_transactions_in_queue: 100, - ..ConfigurationProxy::default() - .build() - .expect("Default queue config should always build") - }); + let queue = Queue::from_configuration(&config_factory()); queue.push(tx.clone(), &wsv).unwrap(); wsv.transactions.insert(tx.as_ref().hash(), 1); assert_eq!( @@ -669,13 +646,7 @@ mod tests { kura, query_handle, )); - let queue = Queue::from_configuration(&Configuration { - transaction_time_to_live_ms: 200, - max_transactions_in_queue: 100, - ..ConfigurationProxy::default() - .build() - .expect("Default queue config should always build") - }); + let queue = Queue::from_configuration(&config_factory()); for _ in 0..(max_txs_in_block - 1) { queue .push(accepted_tx("alice@wonderland", &alice_key), &wsv) @@ -719,13 +690,7 @@ mod tests { kura, query_handle, )); - let queue = Queue::from_configuration(&Configuration { - transaction_time_to_live_ms: 100_000, - max_transactions_in_queue: 100, - ..ConfigurationProxy::default() - .build() - .expect("Default queue config should always build") - }); + let queue = Queue::from_configuration(&config_factory()); queue .push(accepted_tx("alice@wonderland", &alice_key), &wsv) .expect("Failed to push tx into queue"); @@ -759,13 +724,7 @@ mod tests { kura, query_handle, )); - let queue = Queue::from_configuration(&Configuration { - transaction_time_to_live_ms: 100_000, - max_transactions_in_queue: 100, - ..ConfigurationProxy::default() - .build() - .expect("Default queue config should always build") - }); + let queue = Queue::from_configuration(&config_factory()); let instructions = [Fail { message: "expired".to_owned(), }]; @@ -806,12 +765,10 @@ mod tests { query_handle, ); - let queue = Arc::new(Queue::from_configuration(&Configuration { - transaction_time_to_live_ms: 100_000, - max_transactions_in_queue: 100_000_000, - ..ConfigurationProxy::default() - .build() - .expect("Default queue config should always build") + let queue = Arc::new(Queue::from_configuration(&Config { + transaction_time_to_live: Duration::from_secs(100), + max_transactions_in_queue: 100_000_000.try_into().unwrap(), + ..Config::default() })); let start_time = std::time::Instant::now(); @@ -869,7 +826,7 @@ mod tests { #[test] async fn push_tx_in_future() { - let future_threshold_ms = 1000; + let future_threshold = Duration::from_secs(1); let alice_id = "alice@wonderland"; let alice_key = KeyPair::generate(); @@ -881,11 +838,9 @@ mod tests { query_handle, )); - let queue = Queue::from_configuration(&Configuration { - future_threshold_ms, - ..ConfigurationProxy::default() - .build() - .expect("Default queue config should always build") + let queue = Queue::from_configuration(&Config { + future_threshold, + ..Config::default() }); let tx = accepted_tx(alice_id, &alice_key); @@ -900,7 +855,7 @@ mod tests { .with_executable(tx.0.instructions().clone()); let creation_time: u64 = tx.0.creation_time().as_millis().try_into().unwrap(); - new_tx.set_creation_time(creation_time + 2 * future_threshold_ms); + new_tx.set_creation_time(creation_time + future_threshold.mul(2).as_millis() as u64); let new_tx = new_tx.sign(&alice_key); let limits = TransactionLimits { @@ -945,13 +900,11 @@ mod tests { let query_handle = LiveQueryStore::test().start(); let mut wsv = WorldStateView::new(world, kura, query_handle); - let queue = Queue::from_configuration(&Configuration { - transaction_time_to_live_ms: 100_000, - max_transactions_in_queue: 100, - max_transactions_in_queue_per_user: 1, - ..ConfigurationProxy::default() - .build() - .expect("Default queue config should always build") + let queue = Queue::from_configuration(&Config { + transaction_time_to_live: Duration::from_secs(100), + max_transactions_in_queue: 100.try_into().unwrap(), + max_transactions_in_queue_per_user: 1.try_into().unwrap(), + ..Config::default() }); // First push by Alice should be fine diff --git a/core/src/smartcontracts/isi/domain.rs b/core/src/smartcontracts/isi/domain.rs index de0374be2ad..9bf0e4d5f22 100644 --- a/core/src/smartcontracts/isi/domain.rs +++ b/core/src/smartcontracts/isi/domain.rs @@ -48,7 +48,7 @@ pub mod isi { account_id .name - .validate_len(wsv.config.ident_length_limits) + .validate_len(wsv.config.identifier_length_limits) .map_err(Error::from)?; let domain = wsv.domain_mut(&account_id.domain_id)?; @@ -92,7 +92,7 @@ pub mod isi { asset_definition .id() .name - .validate_len(wsv.config.ident_length_limits) + .validate_len(wsv.config.identifier_length_limits) .map_err(Error::from)?; let asset_definition_id = asset_definition.id().clone(); diff --git a/core/src/smartcontracts/isi/world.rs b/core/src/smartcontracts/isi/world.rs index 44ae2f2eb2e..8b72e42f439 100644 --- a/core/src/smartcontracts/isi/world.rs +++ b/core/src/smartcontracts/isi/world.rs @@ -72,7 +72,7 @@ pub mod isi { domain_id .name - .validate_len(wsv.config.ident_length_limits) + .validate_len(wsv.config.identifier_length_limits) .map_err(Error::from)?; let world = wsv.world_mut(); diff --git a/core/src/smartcontracts/wasm.rs b/core/src/smartcontracts/wasm.rs index b361bc97b04..56a3788ac9e 100644 --- a/core/src/smartcontracts/wasm.rs +++ b/core/src/smartcontracts/wasm.rs @@ -6,10 +6,7 @@ use error::*; use import::traits::{ ExecuteOperations as _, GetExecutorPayloads as _, SetPermissionTokenSchema as _, }; -use iroha_config::{ - base::proxy::Builder, - wasm::{Configuration, ConfigurationProxy}, -}; +use iroha_config::parameters::actual::WasmRuntime as IrohaWasmConfig; use iroha_data_model::{ account::AccountId, executor::{self, MigrationResult}, @@ -343,13 +340,9 @@ pub mod state { /// /// Panics if failed to convert `u32` into `usize` which should not happen /// on any supported platform - pub fn store_limits_from_config(config: &Configuration) -> StoreLimits { + pub fn store_limits_from_config(config: &IrohaWasmConfig) -> StoreLimits { StoreLimitsBuilder::new() - .memory_size( - config.max_memory.try_into().expect( - "config.max_memory is a u32 so this can't fail on any supported platform", - ), - ) + .memory_size(config.max_memory.get() as usize) .instances(1) .memories(1) .tables(1) @@ -374,7 +367,7 @@ pub mod state { /// Create new [`OrdinaryState`] pub fn new( authority: AccountId, - config: Configuration, + config: IrohaWasmConfig, log_span: Span, wsv: W, specific_state: S, @@ -567,7 +560,7 @@ pub mod state { pub struct Runtime { engine: Engine, linker: Linker, - config: Configuration, + config: IrohaWasmConfig, } impl Runtime { @@ -1409,7 +1402,7 @@ impl<'wrld> import::traits::SetPermissionTokenSchema { engine: Option, - config: Option, + config: Option, linker: Option>, } @@ -1434,7 +1427,7 @@ impl RuntimeBuilder { /// Sets the [`Configuration`] to be used by the [`Runtime`] #[must_use] #[inline] - pub fn with_configuration(mut self, config: Configuration) -> Self { + pub fn with_configuration(mut self, config: IrohaWasmConfig) -> Self { self.config = Some(config); self } @@ -1451,11 +1444,7 @@ impl RuntimeBuilder { Ok(Runtime { engine, linker, - config: self.config.unwrap_or_else(|| { - ConfigurationProxy::default() - .build() - .expect("Error building WASM Runtime configuration from proxy. This is a bug") - }), + config: self.config.unwrap_or_default(), }) } } diff --git a/core/src/snapshot.rs b/core/src/snapshot.rs index 52aad1bd6ee..e4154d189da 100644 --- a/core/src/snapshot.rs +++ b/core/src/snapshot.rs @@ -6,7 +6,7 @@ use std::{ time::Duration, }; -use iroha_config::snapshot::Configuration; +use iroha_config::parameters::actual::Snapshot as Config; use iroha_crypto::HashOf; use iroha_data_model::block::SignedBlock; use iroha_logger::prelude::*; @@ -137,11 +137,11 @@ impl SnapshotMaker { } /// Create [`Self`] from [`Configuration`] - pub fn from_configuration(config: &Configuration, sumeragi: SumeragiHandle) -> Self { + pub fn from_configuration(config: &Config, sumeragi: SumeragiHandle) -> Self { Self { sumeragi, - snapshot_create_every: Duration::from_millis(config.create_every_ms), - snapshot_dir: config.dir_path.clone(), + snapshot_create_every: config.create_every, + snapshot_dir: config.store_path.clone(), snapshot_creation_enabled: config.creation_enabled, new_wsv_available: false, } diff --git a/core/src/sumeragi/mod.rs b/core/src/sumeragi/mod.rs index 165f273bc4f..31d07d06f1b 100644 --- a/core/src/sumeragi/mod.rs +++ b/core/src/sumeragi/mod.rs @@ -8,7 +8,7 @@ use std::{ }; use eyre::{Result, WrapErr as _}; -use iroha_config::sumeragi::Configuration; +use iroha_config::parameters::actual::{Iroha as IrohaConfig, Sumeragi as SumeragiConfig}; use iroha_crypto::{KeyPair, SignatureOf}; use iroha_data_model::{block::SignedBlock, prelude::*}; use iroha_genesis::GenesisNetwork; @@ -257,8 +257,9 @@ impl SumeragiHandle { #[allow(clippy::too_many_lines)] pub fn start( SumeragiStartArgs { + sumeragi_config, + iroha_config, chain_id, - configuration, events_sender, mut wsv, queue, @@ -281,8 +282,8 @@ impl SumeragiHandle { let mut current_topology = match wsv.height() { 0 => { - assert!(!configuration.trusted_peers.peers.is_empty()); - Topology::new(configuration.trusted_peers.peers.clone()) + assert!(!sumeragi_config.trusted_peers.is_empty()); + Topology::new(sumeragi_config.trusted_peers.clone()) } height => { let block_ref = kura.get_block_by_height(height).expect( @@ -313,21 +314,24 @@ impl SumeragiHandle { watch::channel(finalized_wsv.clone()); #[cfg(debug_assertions)] - let debug_force_soft_fork = configuration.debug_force_soft_fork; + let debug_force_soft_fork = sumeragi_config.debug_force_soft_fork; #[cfg(not(debug_assertions))] let debug_force_soft_fork = false; let sumeragi = main_loop::Sumeragi { chain_id, - key_pair: configuration.key_pair.clone(), + key_pair: iroha_config.key_pair.clone(), queue: Arc::clone(&queue), - peer_id: configuration.peer_id.clone(), + peer_id: PeerId::new( + &iroha_config.p2p_address, + &iroha_config.key_pair.public_key(), + ), events_sender, public_wsv_sender, public_finalized_wsv_sender, - commit_time: Duration::from_millis(configuration.commit_time_limit_ms), - block_time: Duration::from_millis(configuration.block_time_ms), - max_txs_in_block: configuration.max_transactions_in_block as usize, + commit_time: wsv.config.commit_time, + block_time: wsv.config.block_time, + max_txs_in_block: wsv.config.max_transactions_in_block.get() as usize, kura: Arc::clone(&kura), network: network.clone(), control_message_receiver, @@ -420,7 +424,8 @@ impl VotingBlock { #[allow(missing_docs)] pub struct SumeragiStartArgs { pub chain_id: ChainId, - pub configuration: Box, + pub sumeragi_config: Box, + pub iroha_config: Box, pub events_sender: EventsSender, pub wsv: WorldStateView, pub queue: Arc, diff --git a/core/src/wsv.rs b/core/src/wsv.rs index 4cabb24465a..29539fa658b 100644 --- a/core/src/wsv.rs +++ b/core/src/wsv.rs @@ -7,10 +7,7 @@ use std::{ use eyre::Result; use indexmap::IndexMap; -use iroha_config::{ - base::proxy::Builder, - wsv::{Configuration, ConfigurationProxy}, -}; +use iroha_config::parameters::actual::ChainWide as Config; use iroha_crypto::HashOf; use iroha_data_model::{ account::AccountId, @@ -270,7 +267,7 @@ pub struct WorldStateView { /// The world. Contains `domains`, `triggers`, `roles` and other data representing the current state of the blockchain. pub world: World, /// Configuration of World State View. - pub config: Configuration, + pub config: Config, /// Blockchain. pub block_hashes: Vec>, /// Hashes of transactions mapped onto block height where they stored @@ -400,10 +397,7 @@ impl WorldStateView { #[inline] pub fn new(world: World, kura: Arc, query_handle: LiveQueryStoreHandle) -> Self { // Added to remain backward compatible with other code primary in tests - let config = ConfigurationProxy::default() - .build() - .expect("Wsv proxy always builds"); - Self::from_configuration(config, world, kura, query_handle) + Self::from_configuration(Config::default(), world, kura, query_handle) } /// Get `Account`'s `Asset`s @@ -527,7 +521,7 @@ impl WorldStateView { } Wasm(LoadedWasm { module, .. }) => { let mut wasm_runtime = wasm::RuntimeBuilder::::new() - .with_configuration(self.config.wasm_runtime_config) + .with_configuration(self.config.wasm_runtime) .with_engine(self.engine.clone()) // Cloning engine is cheap .build()?; wasm_runtime @@ -590,7 +584,7 @@ impl WorldStateView { } Executable::Wasm(bytes) => { let mut wasm_runtime = wasm::RuntimeBuilder::::new() - .with_configuration(self.config.wasm_runtime_config) + .with_configuration(self.config.wasm_runtime) .with_engine(self.engine.clone()) // Cloning engine is cheap .build()?; wasm_runtime @@ -680,25 +674,24 @@ impl WorldStateView { fn apply_parameters(&mut self) { use iroha_data_model::parameter::default::*; + macro_rules! update_params { - ($ident:ident, $($param:expr => $config:expr),+ $(,)?) => { + ($($param:expr => $config:expr),+ $(,)?) => { $(if let Some(param) = self.query_param($param) { - let $ident = &mut self.config; $config = param; })+ - }; } + update_params! { - config, - WSV_ASSET_METADATA_LIMITS => config.asset_metadata_limits, - WSV_ASSET_DEFINITION_METADATA_LIMITS => config.asset_definition_metadata_limits, - WSV_ACCOUNT_METADATA_LIMITS => config.account_metadata_limits, - WSV_DOMAIN_METADATA_LIMITS => config.domain_metadata_limits, - WSV_IDENT_LENGTH_LIMITS => config.ident_length_limits, - WASM_FUEL_LIMIT => config.wasm_runtime_config.fuel_limit, - WASM_MAX_MEMORY => config.wasm_runtime_config.max_memory, - TRANSACTION_LIMITS => config.transaction_limits, + WSV_ASSET_METADATA_LIMITS => self.config.asset_metadata_limits, + WSV_ASSET_DEFINITION_METADATA_LIMITS => self.config.asset_definition_metadata_limits, + WSV_ACCOUNT_METADATA_LIMITS => self.config.account_metadata_limits, + WSV_DOMAIN_METADATA_LIMITS => self.config.domain_metadata_limits, + WSV_IDENT_LENGTH_LIMITS => self.config.identifier_length_limits, + WASM_FUEL_LIMIT => self.config.wasm_runtime.fuel_limit, + WASM_MAX_MEMORY => self.config.wasm_runtime.max_memory.0, + TRANSACTION_LIMITS => self.config.transaction_limits, } } @@ -923,7 +916,7 @@ impl WorldStateView { /// Construct [`WorldStateView`] with specific [`Configuration`]. #[inline] pub fn from_configuration( - config: Configuration, + config: Config, world: World, kura: Arc, query_handle: LiveQueryStoreHandle, diff --git a/logger/src/actor.rs b/logger/src/actor.rs index 2c908641c26..e9e2d91280e 100644 --- a/logger/src/actor.rs +++ b/logger/src/actor.rs @@ -1,6 +1,6 @@ //! Actor encapsulating interaction with logger & telemetry subsystems. -use iroha_config::parameters::logger::into_tracing_level; +use iroha_config::logger::into_tracing_level; use iroha_data_model::Level; use tokio::sync::{broadcast, mpsc, oneshot}; use tracing_core::Subscriber; diff --git a/logger/src/lib.rs b/logger/src/lib.rs index 17b0a6ff5d2..b1da6a88d9c 100644 --- a/logger/src/lib.rs +++ b/logger/src/lib.rs @@ -13,11 +13,8 @@ use std::{ use actor::LoggerHandle; use color_eyre::{eyre::eyre, Report, Result}; -use iroha_config::parameters::logger::into_tracing_level; -pub use iroha_config::{ - base::Complete as _, - parameters::logger::{Config, Format, Level, UserLayer as UserConfigLayer}, -}; +use iroha_config::logger::{into_tracing_level, Format}; +pub use iroha_config::{base::Complete as _, logger::Level, parameters::actual::Logger as Config}; use tracing::subscriber::set_global_default; pub use tracing::{ debug, debug_span, error, error_span, info, info_span, instrument as log, trace, trace_span, @@ -72,7 +69,6 @@ pub fn init_global(configuration: &Config, terminal_colors: bool) -> Result LoggerHandle { static LOGGER: OnceLock = OnceLock::new(); @@ -84,13 +80,9 @@ pub fn test_logger() -> LoggerHandle { // with ENV vars rather than by extending `test_logger` signature. This will both remain // `test_logger` simple and also will emphasise isolation which is necessary anyway in // case of singleton mocking (where the logger is the singleton). - let config = { - let mut layer = UserConfigLayer::default(); - let _ = layer.level.insert(Level::DEBUG); - let _ = layer.format.insert(Format::Pretty); - layer - .complete() - .expect("should not fail because other fields have defaults") + let config = Config { + level: Level::DEBUG, + format: Format::Pretty, }; init_global(&config, true).expect( diff --git a/telemetry/src/dev.rs b/telemetry/src/dev.rs index 1f99fa23f08..d247aae5fda 100644 --- a/telemetry/src/dev.rs +++ b/telemetry/src/dev.rs @@ -1,7 +1,7 @@ //! Module with development telemetry use eyre::{Result, WrapErr}; -use iroha_config::parameters::telemetry::DevTelemetryConfig; +use iroha_config::parameters::actual::DevTelemetryConfig; use iroha_logger::telemetry::Event as Telemetry; use tokio::{ fs::OpenOptions, diff --git a/telemetry/src/lib.rs b/telemetry/src/lib.rs index 3d6b97346bd..c3c208a877a 100644 --- a/telemetry/src/lib.rs +++ b/telemetry/src/lib.rs @@ -7,7 +7,9 @@ pub mod metrics; mod retry_period; pub mod ws; -pub use iroha_config::parameters::telemetry::Config; +pub use iroha_config::parameters::actual::{ + DevTelemetry as DevTelemetryConfig, RegularTelemetry as RegularTelemetryConfig, +}; pub use iroha_telemetry_derive::metrics; pub mod msg { diff --git a/telemetry/src/ws.rs b/telemetry/src/ws.rs index f2cf86ca661..4a1da03a862 100644 --- a/telemetry/src/ws.rs +++ b/telemetry/src/ws.rs @@ -3,7 +3,7 @@ use chrono::Local; use eyre::{eyre, Result}; use futures::{stream::SplitSink, Sink, SinkExt, StreamExt}; -use iroha_config::parameters::telemetry::RegularTelemetryConfig; +use iroha_config::parameters::actual::RegularTelemetry; use iroha_logger::telemetry::Event as Telemetry; use serde_json::Map; use tokio::{ @@ -28,12 +28,12 @@ const INTERNAL_CHANNEL_CAPACITY: usize = 10; /// # Errors /// Fails if unable to connect to the server pub async fn start( - RegularTelemetryConfig { + RegularTelemetry { name, url, max_retry_delay_exponent, min_retry_period, - }: RegularTelemetryConfig, + }: RegularTelemetry, telemetry: broadcast::Receiver, ) -> Result> { iroha_logger::info!(%url, "Starting telemetry"); diff --git a/tools/kagami/src/config.rs b/tools/kagami/src/config.rs deleted file mode 100644 index 8b137891791..00000000000 --- a/tools/kagami/src/config.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/tools/kagami/src/genesis.rs b/tools/kagami/src/genesis.rs index f98d772f0ff..f97d4cb4b92 100644 --- a/tools/kagami/src/genesis.rs +++ b/tools/kagami/src/genesis.rs @@ -1,7 +1,11 @@ use std::path::PathBuf; use clap::{ArgGroup, Parser, Subcommand}; -use iroha_config::parameters::chain_wide::*; +use iroha_config::parameters::defaults::chain_wide::{ + DEFAULT_BLOCK_TIME, DEFAULT_COMMIT_TIME_LIMIT, DEFAULT_IDENT_LENGTH_LIMITS, DEFAULT_MAX_TXS, + DEFAULT_METADATA_LIMITS, DEFAULT_TRANSACTION_LIMITS, DEFAULT_WASM_FUEL_LIMIT, + DEFAULT_WASM_MAX_MEMORY, +}; use iroha_data_model::{ asset::AssetValueType, metadata::Limits, From 95ea09b1db7fff78106fec37448fce4f2a581b10 Mon Sep 17 00:00:00 2001 From: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> Date: Wed, 17 Jan 2024 11:30:13 +0900 Subject: [PATCH 12/94] [refactor]: update more crates - `iroha_client` - `iroha_torii` - move Torii `uri` to `iroha_torii_const` Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- Cargo.lock | 9 ++ Cargo.toml | 2 + client/Cargo.toml | 1 + client/src/client.rs | 62 ++++++------- client/src/config.rs | 135 +++++----------------------- client/src/lib.rs | 38 ++++---- config/base/src/lib.rs | 2 +- config/src/parameters/actual.rs | 2 +- config/src/parameters/defaults.rs | 2 +- config/src/parameters/user_layer.rs | 2 +- configs/client/config.example.toml | 8 +- telemetry/src/dev.rs | 2 +- torii/Cargo.toml | 1 + torii/const/Cargo.toml | 21 +++++ torii/const/src/lib.rs | 38 ++++++++ torii/src/lib.rs | 47 ++-------- torii/src/routing.rs | 15 ++-- 17 files changed, 164 insertions(+), 223 deletions(-) create mode 100644 torii/const/Cargo.toml create mode 100644 torii/const/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 55df87cc21f..93d09c74de5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2706,6 +2706,7 @@ dependencies = [ "iroha_logger", "iroha_primitives", "iroha_telemetry", + "iroha_torii_const", "iroha_version", "iroha_wasm_builder", "once_cell", @@ -3272,6 +3273,7 @@ dependencies = [ "iroha_primitives", "iroha_schema_gen", "iroha_telemetry", + "iroha_torii_const", "iroha_torii_derive", "iroha_version", "parity-scale-codec", @@ -3283,6 +3285,13 @@ dependencies = [ "warp", ] +[[package]] +name = "iroha_torii_const" +version = "2.0.0-pre-rc.20" +dependencies = [ + "iroha_primitives", +] + [[package]] name = "iroha_torii_derive" version = "2.0.0-pre-rc.20" diff --git a/Cargo.toml b/Cargo.toml index 42bf1e42def..4f650890f42 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ iroha = { path = "cli" } iroha_dsl = { version = "=2.0.0-pre-rc.20", path = "dsl" } iroha_torii = { version = "=2.0.0-pre-rc.20", path = "torii" } iroha_torii_derive = { version = "=2.0.0-pre-rc.20", path = "torii/derive" } +iroha_torii_const = { version = "=2.0.0-pre-rc.20", path = "torii/const" } iroha_macro_utils = { version = "=2.0.0-pre-rc.20", path = "macro/utils" } iroha_telemetry = { version = "=2.0.0-pre-rc.20", path = "telemetry" } iroha_telemetry_derive = { version = "=2.0.0-pre-rc.20", path = "telemetry/derive" } @@ -242,6 +243,7 @@ members = [ "tools/wasm_test_runner", "torii", "torii/derive", + "torii/const", "version", "version/derive", "wasm_codec", diff --git a/client/Cargo.toml b/client/Cargo.toml index 5a3aba4aadb..5af00c30d12 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -54,6 +54,7 @@ iroha_data_model = { workspace = true, features = ["http"] } iroha_primitives = { workspace = true } iroha_logger = { workspace = true } iroha_telemetry = { workspace = true } +iroha_torii_const = { workspace = true } iroha_version = { workspace = true, features = ["http"] } attohttpc = { version = "0.26.1", default-features = false } diff --git a/client/src/client.rs b/client/src/client.rs index 3dcbbc44635..ae2c85fc0c6 100644 --- a/client/src/client.rs +++ b/client/src/client.rs @@ -13,8 +13,10 @@ use derive_more::{DebugCustom, Display}; use eyre::{eyre, Result, WrapErr}; use futures_util::StreamExt; use http_default::{AsyncWebSocketStream, WebSocketStream}; +use iroha_config::client_api::ConfigurationDTO; use iroha_logger::prelude::*; use iroha_telemetry::metrics::Status; +use iroha_torii_const::uri as torii_uri; use iroha_version::prelude::*; use parity_scale_codec::DecodeAll; use rand::Rng; @@ -22,7 +24,7 @@ use url::Url; use self::{blocks_api::AsyncBlockStream, events_api::AsyncEventStream}; use crate::{ - config::{api::ConfigurationDTO, Configuration}, + config::Config, crypto::{HashOf, KeyPair}, data_model::{ block::SignedBlock, @@ -361,7 +363,7 @@ pub struct QueryRequest { impl QueryRequest { #[cfg(test)] fn dummy() -> Self { - let torii_url = crate::config::torii::DEFAULT_API_ADDR; + let torii_url = torii_uri::DEFAULT_API_ADDR; Self { torii_url: format!("http://{torii_url}").parse().unwrap(), @@ -380,9 +382,7 @@ impl QueryRequest { fn assemble(self) -> DefaultRequestBuilder { let builder = DefaultRequestBuilder::new( HttpMethod::POST, - self.torii_url - .join(crate::config::torii::QUERY) - .expect("Valid URI"), + self.torii_url.join(torii_uri::QUERY).expect("Valid URI"), ) .headers(self.headers); @@ -406,7 +406,7 @@ impl Client { /// # Errors /// If configuration isn't valid (e.g public/private keys don't match) #[inline] - pub fn new(configuration: &Configuration) -> Result { + pub fn new(configuration: Config) -> Result { Self::with_headers(configuration, HashMap::new()) } @@ -418,10 +418,19 @@ impl Client { /// If configuration isn't valid (e.g public/private keys don't match) #[inline] pub fn with_headers( - configuration: &Configuration, + Config { + chain_id, + account_id, + torii_api_url, + key_pair, + basic_auth, + transaction_add_nonce, + transaction_ttl, + transaction_status_timeout, + }: Config, mut headers: HashMap, ) -> Result { - if let Some(basic_auth) = &configuration.basic_auth { + if let Some(basic_auth) = basic_auth { let credentials = format!("{}:{}", basic_auth.web_login, basic_auth.password); let engine = base64::engine::general_purpose::STANDARD; let encoded = base64::engine::Engine::encode(&engine, credentials); @@ -429,21 +438,14 @@ impl Client { } Ok(Self { - chain_id: configuration.chain_id.clone(), - torii_url: configuration.torii_api_url.clone(), - key_pair: KeyPair::new( - configuration.public_key.clone(), - configuration.private_key.clone(), - )?, - transaction_ttl: configuration - .transaction_time_to_live_ms - .map(|ttl| Duration::from_millis(ttl.into())), - transaction_status_timeout: Duration::from_millis( - configuration.transaction_status_timeout_ms, - ), - account_id: configuration.account_id.clone(), + chain_id, + torii_url: torii_api_url, + key_pair, + transaction_ttl: Some(transaction_ttl), + transaction_status_timeout, + account_id, headers, - add_transaction_nonce: configuration.add_transaction_nonce, + add_transaction_nonce: transaction_add_nonce, }) } @@ -668,7 +670,7 @@ impl Client { B::new( HttpMethod::POST, self.torii_url - .join(crate::config::torii::TRANSACTION) + .join(torii_uri::TRANSACTION) .expect("Valid URI"), ) .headers(self.headers.clone()) @@ -936,7 +938,7 @@ impl Client { event_filter, self.headers.clone(), self.torii_url - .join(crate::config::torii::SUBSCRIPTION) + .join(torii_uri::SUBSCRIPTION) .expect("Valid URI"), ) } @@ -972,7 +974,7 @@ impl Client { height, self.headers.clone(), self.torii_url - .join(crate::config::torii::BLOCKS_STREAM) + .join(torii_uri::BLOCKS_STREAM) .expect("Valid URI"), ) } @@ -990,7 +992,7 @@ impl Client { ) -> Result> { let url = self .torii_url - .join(crate::config::torii::MATCHING_PENDING_TRANSACTIONS) + .join(torii_uri::MATCHING_PENDING_TRANSACTIONS) .expect("Valid URI"); let body = transaction.encode(); @@ -1029,7 +1031,7 @@ impl Client { let resp = DefaultRequestBuilder::new( HttpMethod::GET, self.torii_url - .join(crate::config::torii::CONFIGURATION) + .join(torii_uri::CONFIGURATION) .expect("Valid URI"), ) .headers(&self.headers) @@ -1055,7 +1057,7 @@ impl Client { let body = serde_json::to_vec(&dto).wrap_err(format!("Failed to serialize {dto:?}"))?; let url = self .torii_url - .join(crate::config::torii::CONFIGURATION) + .join(torii_uri::CONFIGURATION) .expect("Valid URI"); let resp = DefaultRequestBuilder::new(HttpMethod::POST, url) .headers(&self.headers) @@ -1094,9 +1096,7 @@ impl Client { pub fn prepare_status_request(&self) -> B { B::new( HttpMethod::GET, - self.torii_url - .join(crate::config::torii::STATUS) - .expect("Valid URI"), + self.torii_url.join(torii_uri::STATUS).expect("Valid URI"), ) .headers(self.headers.clone()) } diff --git a/client/src/config.rs b/client/src/config.rs index 27b15ee27ad..56ddb317fe5 100644 --- a/client/src/config.rs +++ b/client/src/config.rs @@ -1,10 +1,13 @@ //! Module for client-related configuration and structs + +// FIXME +#![allow(unused, missing_docs)] + use core::str::FromStr; use std::{num::NonZeroU64, time::Duration}; use derive_more::Display; -use eyre::{Result, WrapErr}; -use iroha_config::base::{UserDuration, UserField}; +use eyre::Result; use iroha_crypto::prelude::*; use iroha_data_model::{prelude::*, ChainId}; use iroha_primitives::small::SmallStr; @@ -12,10 +15,9 @@ use serde::{Deserialize, Serialize}; use url::Url; #[allow(unsafe_code)] -const DEFAULT_TRANSACTION_TIME_TO_LIVE_MS: NonZeroU64 = - unsafe { NonZeroU64::new_unchecked(100_000) }; -const DEFAULT_TRANSACTION_STATUS_TIMEOUT_MS: u64 = 15_000; -const DEFAULT_ADD_TRANSACTION_NONCE: bool = false; +pub const DEFAULT_TRANSACTION_TIME_TO_LIVE: Duration = Duration::from_secs(100); +pub const DEFAULT_TRANSACTION_STATUS_TIMEOUT: Duration = Duration::from_secs(15); +pub const DEFAULT_ADD_TRANSACTION_NONCE: bool = false; /// Wrapper over `SmallStr` to provide basic auth login checking #[derive(Debug, Display, Clone, Serialize, PartialEq, Eq)] @@ -62,42 +64,19 @@ pub struct BasicAuth { pub password: SmallStr, } -/// `Configuration` provides an ability to define client parameters such as `TORII_URL`. -#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] -pub struct Configuration { - /// Unique id of the blockchain. Used for simple replay attack protection. - pub chain_id: ChainId, - /// Public key of the user account. - #[config(serde_as_str)] - pub public_key: PublicKey, - /// Private key of the user account. - pub private_key: PrivateKey, - /// User account id. - pub account_id: AccountId, - /// Basic Authentication credentials - pub basic_auth: Option, - /// Torii URL. - pub torii_api_url: Url, - /// Proposed transaction TTL in milliseconds. - pub transaction_time_to_live_ms: Option, - /// Transaction status wait timeout in milliseconds. - pub transaction_status_timeout_ms: u64, - /// If `true` add nonce, which make different hashes for transactions which occur repeatedly and simultaneously - pub add_transaction_nonce: bool, -} - -mod user_layers { +pub mod user_layer { use iroha_config::base::{Complete, CompleteResult, Merge, UserDuration, UserField}; use iroha_crypto::{PrivateKey, PublicKey}; - use iroha_data_model::account::AccountId; + use iroha_data_model::{account::AccountId, ChainId}; use serde::{Deserialize, Deserializer}; use url::Url; use crate::config::BasicAuth; - #[derive(Debug, Clone, Deserialize, Eq, PartialEq)] + #[derive(Debug, Clone, Deserialize, Eq, PartialEq, Default)] #[serde(deny_unknown_fields, default)] pub struct Root { + pub chain_id: UserField, pub account: Account, pub api: Api, pub transaction: Transaction, @@ -107,7 +86,11 @@ mod user_layers { type Output = super::Config; fn complete(self) -> CompleteResult { - // TODO: tx timeout should be smaller than ttl + // TODO + // # Errors + // - If the [`self.transaction_time_to_live_ms`] field is too small + // - If the [`self.transaction_status_timeout_ms`] field is smaller than [`self.transaction_time_to_live_ms`] + // - If the [`self.torii_api_url`] is malformed or had the wrong protocol todo!() } } @@ -118,14 +101,14 @@ mod user_layers { } } - #[derive(Debug, Clone, Deserialize, Eq, PartialEq)] + #[derive(Debug, Clone, Deserialize, Eq, PartialEq, Default)] #[serde(deny_unknown_fields, default)] pub struct Api { pub torii_url: UserField, pub basic_auth: UserField, } - #[derive(Debug, Clone, Deserialize, Eq, PartialEq)] + #[derive(Debug, Clone, Deserialize, Eq, PartialEq, Default)] #[serde(deny_unknown_fields, default)] pub struct Account { pub id: UserField, @@ -133,7 +116,7 @@ mod user_layers { pub private_key: UserField, } - #[derive(Debug, Clone, Deserialize, Eq, PartialEq)] + #[derive(Debug, Clone, Deserialize, Eq, PartialEq, Default)] #[serde(deny_unknown_fields, default)] pub struct Transaction { pub time_to_live: UserField, @@ -160,6 +143,7 @@ mod user_layers { } pub struct Config { + pub chain_id: ChainId, pub account_id: AccountId, pub key_pair: KeyPair, pub basic_auth: Option, @@ -168,80 +152,3 @@ pub struct Config { pub transaction_status_timeout: Duration, pub transaction_add_nonce: bool, } - -impl Default for ConfigurationProxy { - fn default() -> Self { - Self { - chain_id: None, - public_key: None, - private_key: None, - account_id: None, - basic_auth: Some(None), - torii_api_url: None, - transaction_time_to_live_ms: Some(Some(DEFAULT_TRANSACTION_TIME_TO_LIVE_MS)), - transaction_status_timeout_ms: Some(DEFAULT_TRANSACTION_STATUS_TIMEOUT_MS), - add_transaction_nonce: Some(DEFAULT_ADD_TRANSACTION_NONCE), - } - } -} - -// TODO: explain why these values were chosen. -const TTL_TOO_SMALL_THRESHOLD: u64 = 500; - -impl ConfigurationProxy { - /// Finalise Iroha client config proxy by checking that certain fields identify reasonable limits or - /// are well formatted. - /// - /// # Errors - /// - If the [`self.transaction_time_to_live_ms`] field is too small - /// - If the [`self.transaction_status_timeout_ms`] field is smaller than [`self.transaction_time_to_live_ms`] - /// - If the [`self.torii_api_url`] is malformed or had the wrong protocol - pub fn finish(&mut self) -> Result<()> { - if let Some(Some(tx_ttl)) = self.transaction_time_to_live_ms { - // Really small TTL would be detrimental to performance - if u64::from(tx_ttl) < TTL_TOO_SMALL_THRESHOLD { - eyre::bail!(ConfigError::InsaneValue { - field: "TRANSACTION_TIME_TO_LIVE_MS", - value: tx_ttl.to_string(), - message: format!(", because if it's smaller than {TTL_TOO_SMALL_THRESHOLD}, Iroha wouldn't be able to produce blocks on time.") - }); - } - // Timeouts bigger than transaction TTL don't make sense as then transaction would be discarded before this timeout - if let Some(timeout) = self.transaction_status_timeout_ms { - if timeout > tx_ttl.into() { - eyre::bail!(ConfigError::InsaneValue { - field: "TRANSACTION_STATUS_TIMEOUT_MS", - value: timeout.to_string(), - message: format!(", because it should be smaller than `TRANSACTION_TIME_TO_LIVE_MS`, which is {tx_ttl}") - }) - } - } - } - if let Some(api_url) = &self.torii_api_url { - if api_url.scheme() != "http" { - eyre::bail!(ConfigError::InsaneValue { - field: "TORII_API_URL", - value: api_url.to_string(), - message: ", because we only support HTTP".to_owned(), - }); - } - } - Ok(()) - } - - /// The wrapper around the client `ConfigurationProxy` that performs - /// finalisation prior to building `Configuration`. Just like - /// Iroha peer config, its `::build()` - /// method should never be used directly, as only this wrapper ensures final - /// coherence and fails if there are any issues. - /// - /// # Errors - /// - Finalisation fails - /// - Building fails, e.g. any of the inner fields had a `None` value when that - /// is not allowed by the defaults. - pub fn build(mut self) -> Result { - self.finish()?; - ::build(self) - .wrap_err("Failed to build `Configuration` from `ConfigurationProxy`") - } -} diff --git a/client/src/lib.rs b/client/src/lib.rs index 50f8bc8f245..548d2a3beac 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -10,33 +10,31 @@ mod query_builder; /// Module containing sample configurations for tests and benchmarks. pub mod samples { + use url::Url; + use crate::{ - config::{torii::DEFAULT_API_ADDR, Configuration, ConfigurationProxy}, + config::{ + Config, DEFAULT_ADD_TRANSACTION_NONCE, DEFAULT_TRANSACTION_STATUS_TIMEOUT, + DEFAULT_TRANSACTION_TIME_TO_LIVE, + }, crypto::KeyPair, data_model::ChainId, }; /// Get sample client configuration. - pub fn get_client_config(chain_id: ChainId, key_pair: &KeyPair) -> Configuration { - let (public_key, private_key) = key_pair.clone().into(); - ConfigurationProxy { - chain_id: Some(chain_id), - public_key: Some(public_key), - private_key: Some(private_key), - account_id: Some( - "alice@wonderland" - .parse() - .expect("This account ID should be valid"), - ), - torii_api_url: Some( - format!("http://{DEFAULT_API_ADDR}") - .parse() - .expect("Should be a valid url"), - ), - ..ConfigurationProxy::default() + pub fn get_client_config(chain_id: ChainId, key_pair: KeyPair, torii_api_url: Url) -> Config { + Config { + chain_id, + key_pair, + torii_api_url, + account_id: "alice@wonderland" + .parse() + .expect("This account ID should be valid"), + basic_auth: None, + transaction_ttl: DEFAULT_TRANSACTION_TIME_TO_LIVE, + transaction_status_timeout: DEFAULT_TRANSACTION_STATUS_TIMEOUT, + transaction_add_nonce: DEFAULT_ADD_TRANSACTION_NONCE, } - .build() - .expect("Client config should build as all required fields were provided") } } diff --git a/config/base/src/lib.rs b/config/base/src/lib.rs index 3b20b424ab6..2b7f4f6488a 100644 --- a/config/base/src/lib.rs +++ b/config/base/src/lib.rs @@ -49,7 +49,7 @@ macro_rules! impl_deserialize_from_str { } /// User-provided duration -#[derive(Debug, Copy, Clone, Deserialize, Serialize)] +#[derive(Debug, Copy, Clone, Deserialize, Serialize, Ord, PartialOrd, Eq, PartialEq)] pub struct UserDuration(Duration); impl UserDuration { diff --git a/config/src/parameters/actual.rs b/config/src/parameters/actual.rs index c3441c95507..a411d880a86 100644 --- a/config/src/parameters/actual.rs +++ b/config/src/parameters/actual.rs @@ -161,7 +161,7 @@ impl Default for WasmRuntime { #[derive(Debug)] pub struct Torii { pub address: SocketAddr, - pub max_content_len: ByteSize, + pub max_content_len: ByteSize, } /// Complete configuration needed to start regular telemetry. diff --git a/config/src/parameters/defaults.rs b/config/src/parameters/defaults.rs index 90fd84ff1c4..44fb20ed643 100644 --- a/config/src/parameters/defaults.rs +++ b/config/src/parameters/defaults.rs @@ -88,7 +88,7 @@ pub mod chain_wide { pub mod torii { use std::time::Duration; - pub const DEFAULT_MAX_CONTENT_LENGTH: u32 = 2_u32.pow(20) * 16; + pub const DEFAULT_MAX_CONTENT_LENGTH: u64 = 2_u64.pow(20) * 16; pub const DEFAULT_QUERY_IDLE_TIME: Duration = Duration::from_secs(30); } diff --git a/config/src/parameters/user_layer.rs b/config/src/parameters/user_layer.rs index c9300e75fd2..cfb005bd5bd 100644 --- a/config/src/parameters/user_layer.rs +++ b/config/src/parameters/user_layer.rs @@ -813,7 +813,7 @@ impl FromEnvDefaultFallback for ChainWide {} #[serde(deny_unknown_fields, default)] pub struct Torii { pub address: UserField, - pub max_content_len: UserField>, + pub max_content_len: UserField>, pub query_idle_time: UserField, } diff --git a/configs/client/config.example.toml b/configs/client/config.example.toml index 90eb2430226..4ddcbbfc7da 100644 --- a/configs/client/config.example.toml +++ b/configs/client/config.example.toml @@ -1,10 +1,10 @@ +chain_id = "00000000-0000-0000-0000-000000000000" + [account] id = "alice@wonderland" public_key = "ed01207233BFC89DCBD68C19FDE6CE6158225298EC1131B6A130D1AEB454C1AB5183C0" - -[account.private_key] -digest_function = "ed25519" -payload = "9ac47abf59b356e0bd7dcbbbb4dec080e302156a48ca907e47cb6aea1d32719e7233bfc89dcbd68c19fde6ce6158225298ec1131b6a130d1aeb454c1ab5183c0" +private_key.digest_function = "ed25519" +private_key.payload = "9ac47abf59b356e0bd7dcbbbb4dec080e302156a48ca907e47cb6aea1d32719e7233bfc89dcbd68c19fde6ce6158225298ec1131b6a130d1aeb454c1ab5183c0" [api] torii_url = "http://127.0.0.1:8080/" diff --git a/telemetry/src/dev.rs b/telemetry/src/dev.rs index d247aae5fda..9a6bb15c1fc 100644 --- a/telemetry/src/dev.rs +++ b/telemetry/src/dev.rs @@ -1,7 +1,7 @@ //! Module with development telemetry use eyre::{Result, WrapErr}; -use iroha_config::parameters::actual::DevTelemetryConfig; +use iroha_config::parameters::actual::DevTelemetry as DevTelemetryConfig; use iroha_logger::telemetry::Event as Telemetry; use tokio::{ fs::OpenOptions, diff --git a/torii/Cargo.toml b/torii/Cargo.toml index aa9359f97f1..3b363d6e01c 100644 --- a/torii/Cargo.toml +++ b/torii/Cargo.toml @@ -33,6 +33,7 @@ iroha_logger = { workspace = true } iroha_data_model = { workspace = true, features = ["http"] } iroha_version = { workspace = true, features = ["http"] } iroha_torii_derive = { workspace = true } +iroha_torii_const = { workspace = true } iroha_futures = { workspace = true } iroha_macro = { workspace = true } iroha_schema_gen = { workspace = true, optional = true } diff --git a/torii/const/Cargo.toml b/torii/const/Cargo.toml new file mode 100644 index 00000000000..ccabf87926b --- /dev/null +++ b/torii/const/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "iroha_torii_const" + +edition.workspace = true +version.workspace = true +authors.workspace = true + +description.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true + +license.workspace = true +keywords.workspace = true +categories.workspace = true + +[lints] +workspace = true + +[dependencies] +iroha_primitives.workspace = true \ No newline at end of file diff --git a/torii/const/src/lib.rs b/torii/const/src/lib.rs new file mode 100644 index 00000000000..6e25b7b11e4 --- /dev/null +++ b/torii/const/src/lib.rs @@ -0,0 +1,38 @@ +//! Constant values used in Torii that might be re-used by client libraries as well. + +pub mod uri { + //! URI that Torii uses to route incoming requests. + + /// Default socket for listening on external requests + pub const DEFAULT_API_ADDR: iroha_primitives::addr::SocketAddr = + iroha_primitives::addr::socket_addr!(127.0.0.1:8080); + /// Query URI is used to handle incoming Query requests. + pub const QUERY: &str = "query"; + /// Transaction URI is used to handle incoming ISI requests. + pub const TRANSACTION: &str = "transaction"; + /// Block URI is used to handle incoming Block requests. + pub const CONSENSUS: &str = "consensus"; + /// Health URI is used to handle incoming Healthcheck requests. + pub const HEALTH: &str = "health"; + /// The URI used for block synchronization. + pub const BLOCK_SYNC: &str = "block/sync"; + /// The web socket uri used to subscribe to block and transactions statuses. + pub const SUBSCRIPTION: &str = "events"; + /// The web socket uri used to subscribe to blocks stream. + pub const BLOCKS_STREAM: &str = "block/stream"; + /// Get pending transactions. + pub const PENDING_TRANSACTIONS: &str = "pending_transactions"; + /// The URI for local config changing inspecting + pub const CONFIGURATION: &str = "configuration"; + /// URI to report status for administration + pub const STATUS: &str = "status"; + /// Metrics URI is used to export metrics according to [Prometheus + /// Guidance](https://prometheus.io/docs/instrumenting/writing_exporters/). + pub const METRICS: &str = "metrics"; + /// URI for retrieving the schema with which Iroha was built. + pub const SCHEMA: &str = "schema"; + /// URI for getting the API version currently used + pub const API_VERSION: &str = "api_version"; + /// URI for getting cpu profile + pub const PROFILE: &str = "debug/pprof/profile"; +} diff --git a/torii/src/lib.rs b/torii/src/lib.rs index 749fd7ad9b8..0ad6dc946f4 100644 --- a/torii/src/lib.rs +++ b/torii/src/lib.rs @@ -13,7 +13,7 @@ use std::{ }; use futures::{stream::FuturesUnordered, StreamExt}; -use iroha_config::torii::Configuration as ToriiConfiguration; +use iroha_config::parameters::actual::Torii as Config; use iroha_core::{ kiso::{Error as KisoError, KisoHandle}, kura::Kura, @@ -25,6 +25,7 @@ use iroha_core::{ }; use iroha_data_model::ChainId; use iroha_primitives::addr::SocketAddr; +use iroha_torii_const::uri; use tokio::{sync::Notify, task}; use utils::*; use warp::{ @@ -38,44 +39,8 @@ use warp::{ pub(crate) mod utils; mod event; mod routing; -mod stream; -pub mod uri { - //! URI that [`Torii`](super::Torii) uses to route incoming requests. - - /// Default socket for listening on external requests - pub const DEFAULT_API_ADDR: iroha_primitives::addr::SocketAddr = - iroha_primitives::addr::socket_addr!(127.0.0.1:8080); - /// Query URI is used to handle incoming Query requests. - pub const QUERY: &str = "query"; - /// Transaction URI is used to handle incoming ISI requests. - pub const TRANSACTION: &str = "transaction"; - /// Block URI is used to handle incoming Block requests. - pub const CONSENSUS: &str = "consensus"; - /// Health URI is used to handle incoming Healthcheck requests. - pub const HEALTH: &str = "health"; - /// The URI used for block synchronization. - pub const BLOCK_SYNC: &str = "block/sync"; - /// The web socket uri used to subscribe to block and transactions statuses. - pub const SUBSCRIPTION: &str = "events"; - /// The web socket uri used to subscribe to blocks stream. - pub const BLOCKS_STREAM: &str = "block/stream"; - /// Get pending transactions. - pub const PENDING_TRANSACTIONS: &str = "pending_transactions"; - /// The URI for local config changing inspecting - pub const CONFIGURATION: &str = "configuration"; - /// URI to report status for administration - pub const STATUS: &str = "status"; - /// Metrics URI is used to export metrics according to [Prometheus - /// Guidance](https://prometheus.io/docs/instrumenting/writing_exporters/). - pub const METRICS: &str = "metrics"; - /// URI for retrieving the schema with which Iroha was built. - pub const SCHEMA: &str = "schema"; - /// URI for getting the API version currently used - pub const API_VERSION: &str = "api_version"; - /// URI for getting cpu profile - pub const PROFILE: &str = "debug/pprof/profile"; -} +mod stream; /// Main network handler and the only entrypoint of the Iroha. pub struct Torii { @@ -97,7 +62,7 @@ impl Torii { pub fn new( chain_id: ChainId, kiso: KisoHandle, - config: &ToriiConfiguration, + config: Config, queue: Arc, events: EventsSender, notify_shutdown: Arc, @@ -114,8 +79,8 @@ impl Torii { sumeragi, query_service, kura, - address: config.api_url.clone(), - transaction_max_content_length: config.max_content_len.into(), + address: config.address, + transaction_max_content_length: config.max_content_len.get(), } } diff --git a/torii/src/routing.rs b/torii/src/routing.rs index f615b82ed60..d7028451105 100644 --- a/torii/src/routing.rs +++ b/torii/src/routing.rs @@ -327,22 +327,21 @@ pub async fn handle_version(sumeragi: SumeragiHandle) -> Json { } #[cfg(feature = "telemetry")] -pub fn handle_metrics(sumeragi: &SumeragiHandle) -> Result { +fn update_metrics_gracefully(sumeragi: &SumeragiHandle) { if let Err(error) = sumeragi.update_metrics() { - iroha_logger::error!(%error, "Error while calling sumeragi::update_metrics."); + iroha_logger::error!(%error, "Error while calling `sumeragi::update_metrics`."); } +} + +#[cfg(feature = "telemetry")] +pub fn handle_metrics(sumeragi: &SumeragiHandle) -> Result { + update_metrics_gracefully(sumeragi); sumeragi .metrics() .try_to_string() .map_err(Error::Prometheus) } -fn update_metrics_gracefully(sumeragi: &SumeragiHandle) { - if let Err(error) = sumeragi.update_metrics() { - iroha_logger::error!(%error, "Error while calling `sumeragi::update_metrics`."); - } -} - #[cfg(feature = "telemetry")] #[allow(clippy::unnecessary_wraps)] pub fn handle_status( From 4d8ebf0f627fb08cde664796728c09bb925f1140 Mon Sep 17 00:00:00 2001 From: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> Date: Tue, 23 Jan 2024 06:12:08 +0900 Subject: [PATCH 13/94] [feat]: compile `iroha`! Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- Cargo.lock | 3 +- Cargo.toml | 1 + cli/Cargo.toml | 3 +- cli/src/lib.rs | 218 ++---- cli/src/main.rs | 25 +- cli/src/samples.rs | 99 +-- config/Cargo.toml | 2 +- config/base/src/lib.rs | 142 ++-- config/src/parameters/actual.rs | 69 +- config/src/parameters/user_layer.rs | 1062 +++++++++++++++++++-------- config/tests/fixtures.rs | 177 +++-- core/src/gossiper.rs | 3 +- core/src/sumeragi/mod.rs | 5 +- data_model/src/lib.rs | 4 +- logger/src/lib.rs | 7 +- telemetry/src/dev.rs | 6 +- 16 files changed, 1137 insertions(+), 689 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 93d09c74de5..ec1710a0552 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2671,15 +2671,16 @@ dependencies = [ "iroha_telemetry", "iroha_torii", "iroha_wasm_builder", + "json5", "once_cell", "owo-colors", "path-absolutize", - "serde_json", "serial_test", "supports-color 2.1.0", "tempfile", "thread-local-panic-hook", "tokio", + "toml 0.8.8", "tracing", "vergen", ] diff --git a/Cargo.toml b/Cargo.toml index 4f650890f42..c79a5457fad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -134,6 +134,7 @@ serde_yaml = "0.9.25" serde_with = { version = "3.3.0", default-features = false } parity-scale-codec = { version = "3.6.5", default-features = false } json5 = "0.4.1" +toml = "0.8.8" [workspace.lints] rustdoc.private_doc_tests = "deny" diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 3d744eded61..4ab631e22e2 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -65,7 +65,8 @@ thread-local-panic-hook = { version = "0.1.0", optional = true } [dev-dependencies] serial_test = "2.0.0" tempfile = { workspace = true } -serde_json = { workspace = true } +toml = { workspace = true } +json5 = { workspace = true } futures = { workspace = true } path-absolutize = { workspace = true } assertables = "7" diff --git a/cli/src/lib.rs b/cli/src/lib.rs index afb4f53e538..76f2221752f 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -6,16 +6,10 @@ //! should be constructed externally: (see `main.rs`). #[cfg(debug_assertions)] use core::sync::atomic::{AtomicBool, Ordering}; -use std::{path::PathBuf, sync::Arc}; +use std::{path::Path, 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, - telemetry::Configuration as TelemetryConfiguration, -}; +use iroha_config::parameters::{actual::Root as Config, user_layer::CliContext}; use iroha_core::{ block_sync::{BlockSynchronizer, BlockSynchronizerHandle}, gossiper::{TransactionGossiper, TransactionGossiperHandle}, @@ -28,11 +22,10 @@ use iroha_core::{ smartcontracts::isi::Registrable as _, snapshot::{try_read_snapshot, SnapshotMaker, SnapshotMakerHandle}, sumeragi::{SumeragiHandle, SumeragiStartArgs}, - tx::PeerId, IrohaNetwork, }; use iroha_data_model::prelude::*; -use iroha_genesis::GenesisNetwork; +use iroha_genesis::{GenesisNetwork, RawGenesisBlock}; use iroha_logger::actor::LoggerHandle; use iroha_torii::Torii; use tokio::{ @@ -201,19 +194,19 @@ impl Iroha { #[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 new( - config: Configuration, + config: Config, genesis: Option, logger: LoggerHandle, ) -> Result { - let listen_addr = config.torii.p2p_addr.clone(); - let network = IrohaNetwork::start(listen_addr, config.sumeragi.key_pair.clone()) - .await - .wrap_err("Unable to start P2P-network")?; + let network = + IrohaNetwork::start(config.torii.address.clone(), config.iroha.key_pair.clone()) + .await + .wrap_err("Unable to start P2P-network")?; let (events_sender, _) = broadcast::channel(10000); let world = World::with( - [genesis_domain(config.genesis.public_key.clone())], - config.sumeragi.trusted_peers.peers.clone(), + [genesis_domain(config.genesis.public_key().clone())], + config.sumeragi.trusted_peers.clone(), ); let kura = Kura::new(&config.kura)?; @@ -222,7 +215,7 @@ impl Iroha { let block_count = kura.init()?; let wsv = try_read_snapshot( - &config.snapshot.dir_path, + &config.snapshot.store_path, &kura, live_query_store_handle.clone(), block_count, @@ -231,7 +224,7 @@ impl Iroha { |error| { iroha_logger::warn!(%error, "Failed to load wsv from snapshot, creating empty wsv"); WorldStateView::from_configuration( - *config.wsv, + config.chain_wide, world, Arc::clone(&kura), live_query_store_handle.clone(), @@ -247,7 +240,7 @@ impl Iroha { ); let queue = Arc::new(Queue::from_configuration(&config.queue)); - match Self::start_telemetry(&logger, &config.telemetry).await? { + match Self::start_telemetry(&logger, &config).await? { TelemetryStartStatus::Started => iroha_logger::info!("Telemetry started"), TelemetryStartStatus::NotStarted => iroha_logger::warn!("Telemetry not started"), }; @@ -255,8 +248,9 @@ impl Iroha { let kura_thread_handler = Kura::start(Arc::clone(&kura)); let start_args = SumeragiStartArgs { - chain_id: config.chain_id.clone(), - configuration: config.sumeragi.clone(), + chain_id: config.iroha.chain_id.clone(), + sumeragi_config: config.sumeragi.clone(), + iroha_config: &config.iroha, events_sender: events_sender.clone(), wsv, queue: Arc::clone(&queue), @@ -274,14 +268,14 @@ impl Iroha { &config.block_sync, sumeragi.clone(), Arc::clone(&kura), - PeerId::new(config.torii.p2p_addr.clone(), config.public_key.clone()), + config.iroha.peer_id(), network.clone(), ) .start(); let gossiper = TransactionGossiper::from_configuration( - config.chain_id.clone(), - &config.sumeragi, + config.iroha.chain_id.clone(), + config.transaction_gossiper, network.clone(), Arc::clone(&queue), sumeragi.clone(), @@ -310,9 +304,9 @@ impl Iroha { let kiso = KisoHandle::new(config.clone()); let torii = Torii::new( - config.chain_id, + config.iroha.chain_id.clone(), kiso.clone(), - &config.torii, + config.torii, Arc::clone(&queue), events_sender, Arc::clone(¬ify_shutdown), @@ -376,30 +370,27 @@ impl Iroha { #[cfg(feature = "telemetry")] async fn start_telemetry( logger: &LoggerHandle, - config: &TelemetryConfiguration, + config: &Config, ) -> Result { - #[allow(unused)] - let (config_for_regular, config_for_dev) = config.parse(); - #[cfg(feature = "dev-telemetry")] { - if let Some(config) = config_for_dev { + if let Some(config) = &config.dev_telemetry { let receiver = logger .subscribe_on_telemetry(iroha_logger::telemetry::Channel::Future) .await .wrap_err("Failed to subscribe on telemetry")?; - let _handle = iroha_telemetry::dev::start(config, receiver) + let _handle = iroha_telemetry::dev::start(&config.file, receiver) .await .wrap_err("Failed to setup telemetry for futures")?; } } - if let Some(config) = config_for_regular { + if let Some(config) = &config.regular_telemetry { let receiver = logger .subscribe_on_telemetry(iroha_logger::telemetry::Channel::Regular) .await .wrap_err("Failed to subscribe on telemetry")?; - let _handle = iroha_telemetry::ws::start(config, receiver) + let _handle = iroha_telemetry::ws::start(config.clone(), receiver) .await .wrap_err("Failed to setup telemetry for websocket communication")?; @@ -498,23 +489,7 @@ fn genesis_domain(public_key: PublicKey) -> Domain { domain } -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) - }; -} - +// FIXME: update docs /// Read and parse Iroha configuration and genesis block. /// /// The pipeline of configuration reading is as follows: @@ -530,71 +505,25 @@ macro_rules! mutate_nested_option { /// # Errors /// - If provided user configuration is invalid or incomplete /// - If genesis config is invalid -pub fn read_config( - path: &Path, +pub fn read_config_and_genesis( + path: impl AsRef, 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 have 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 +) -> Result<(Config, Option)> { + use iroha_config::{ + base::{FromEnv as _, StdEnv, UnwrapPartial as _}, + parameters::{actual::Genesis, user_layer::RootPartial as RootLayer}, }; - // 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 = RootLayer::from_toml(path)? + .merge_chain(RootLayer::from_env(&StdEnv)?) + .unwrap_partial()? + .parse(CliContext { submit_genesis })?; - let config = config - .build() - .wrap_err("Failed to finalize configuration")?; + let genesis = if let Genesis::Full { key_pair, file } = &config.genesis { + let raw_block = RawGenesisBlock::from_path(file)?; - // 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, &config.chain_id, &key_pair) + GenesisNetwork::new(raw_block, &config.iroha.chain_id, &key_pair) .wrap_err("Failed to construct the genesis")?, ) } else { @@ -637,6 +566,7 @@ mod tests { mod config_integration { use assertables::{assert_contains, assert_contains_as_result}; + use iroha_config::parameters::user_layer::RootPartial as PartialUserConfig; use iroha_crypto::KeyPair; use iroha_genesis::{ExecutorMode, ExecutorPath}; use iroha_primitives::addr::socket_addr; @@ -644,24 +574,20 @@ mod tests { use super::*; - fn config_factory() -> ConfigurationProxy { - let key_pair = KeyPair::generate(); + fn config_factory() -> PartialUserConfig { + let (pubkey, privkey) = KeyPair::generate().into(); - let mut base = ConfigurationProxy { - chain_id: Some(ChainId::new("0")), + let mut base = PartialUserConfig::default(); - public_key: Some(key_pair.public_key().clone()), - private_key: Some(key_pair.private_key().clone()), + base.iroha.chain_id.set(ChainId::new("0".to_owned())); + base.iroha.public_key.set(pubkey.clone()); + base.iroha.private_key.set(privkey.clone()); + base.iroha.p2p_address.set(socket_addr!(127.0.0.1:1337)); - ..ConfigurationProxy::default() - }; - 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()); + base.genesis.public_key.set(pubkey); + base.genesis.private_key.set(privkey); - 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)); + base.torii.address.set(socket_addr!(127.0.0.1:8080)); base } @@ -676,28 +602,28 @@ mod tests { 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.genesis.file.set("./genesis/gen.json".into()); + cfg.kura.block_store_path.set("../storage".into()); + cfg.snapshot.store_path.set("../snapshots".into()); + cfg.telemetry.dev.file.set("../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"); + let config_path = dir.path().join("config/config.toml"); 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(config_path, toml::to_string(&config)?)?; + std::fs::write(genesis_path, json5::to_string(&genesis)?)?; std::fs::write(executor_path, "")?; - let config_path = Path::default(dir.path().join("config/config")); + let config_path = dir.path().join("config/config.toml"); // When - let (config, genesis) = read_config(&config_path, true)?; + let (config, genesis) = read_config_and_genesis(&config_path, true)?; // Then @@ -709,11 +635,15 @@ mod tests { dir.path().join("storage") ); assert_eq!( - config.snapshot.dir_path.absolutize()?, + config.snapshot.store_path.absolutize()?, dir.path().join("snapshots") ); assert_eq!( - config.telemetry.file.expect("Should be set").absolutize()?, + config + .dev_telemetry + .expect("dev telemetry should be set") + .file + .absolutize()?, dir.path().join("logs/telemetry") ); @@ -730,25 +660,19 @@ mod tests { let config = { let mut cfg = config_factory(); - cfg.genesis.as_mut().unwrap().file = Some(Some("./genesis.json".into())); + cfg.genesis.file.set("./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("config.toml"), toml::to_string(&config)?)?; + std::fs::write(dir.path().join("genesis.json"), json5::to_string(&genesis)?)?; std::fs::write(dir.path().join("executor.wasm"), "")?; - let config_path = Path::user_provided(dir.path().join("config.json"))?; + let config_path = dir.path().join("config.toml"); // When & Then - let report = read_config(&config_path, false).unwrap_err(); + let report = read_config_and_genesis(&config_path, false).unwrap_err(); assert_contains!( format!("{report}"), diff --git a/cli/src/main.rs b/cli/src/main.rs index 14ef7a587d5..8526c0dfa92 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -1,11 +1,10 @@ //! Iroha peer command-line interface. -use std::env; +use std::{env, path::PathBuf}; use clap::Parser; use color_eyre::eyre::Result; -use iroha_config::path::Path; -const DEFAULT_CONFIG_PATH: &str = "config"; +const DEFAULT_CONFIG_PATH: &str = "config.toml"; fn is_colouring_supported() -> bool { supports_color::on(supports_color::Stream::Stdout).is_some() @@ -31,10 +30,10 @@ struct Args { short, env("IROHA_CONFIG"), value_name("PATH"), - value_parser(Path::user_provided_str), - value_hint(clap::ValueHint::FilePath) + value_hint(clap::ValueHint::FilePath), + default_value_t = DEFAULT_CONFIG_PATH.to_owned() )] - config: Option, + config: String, /// Whether to enable ANSI colored output or not /// /// By default, Iroha determines whether the terminal supports colors or not. @@ -73,11 +72,8 @@ async fn main() -> Result<()> { color_eyre::install()?; } - 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 (config, genesis) = + iroha::read_config_and_genesis(PathBuf::from(args.config), args.submit_genesis)?; let logger = iroha_logger::init_global(&config.logger, args.terminal_colors)?; iroha_logger::info!( @@ -109,7 +105,7 @@ mod tests { fn default_args() -> Result<()> { let args = Args::try_parse_from(["test"])?; - assert_eq!(args.config, None); + assert_eq!(args.config, "config.toml".to_owned()); assert_eq!(args.terminal_colors, is_colouring_supported()); assert_eq!(args.submit_genesis, false); @@ -139,10 +135,7 @@ mod tests { 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()) - ); + assert_eq!(args.config, "/home/custom/file.json".to_owned()); Ok(()) } diff --git a/cli/src/samples.rs b/cli/src/samples.rs index 0a4c13870b2..bd610a44912 100644 --- a/cli/src/samples.rs +++ b/cli/src/samples.rs @@ -1,14 +1,23 @@ //! This module contains the sample configurations used for testing and benchmarking throughout Iroha. -use std::{collections::HashSet, path::Path, str::FromStr}; +use std::{collections::HashSet, path::Path, str::FromStr, time::Duration}; use iroha_config::{ - iroha::{Configuration, ConfigurationProxy}, - sumeragi::TrustedPeers, - torii::{uri::DEFAULT_API_ADDR, DEFAULT_TORII_P2P_ADDR}, + base::{UnwrapPartial, UserDuration}, + parameters::{ + actual::Root as Config, + user_layer::{CliContext, RootPartial as UserConfig}, + }, }; use iroha_crypto::{KeyPair, PublicKey}; use iroha_data_model::{peer::PeerId, prelude::*, ChainId}; -use iroha_primitives::unique_vec::UniqueVec; +use iroha_primitives::{ + addr::{socket_addr, SocketAddr}, + unique_vec::UniqueVec, +}; + +// FIXME: move to a global test-related place, re-use everywhere else +const DEFAULT_P2P_ADDR: SocketAddr = socket_addr!(127.0.0.1:1337); +const DEFAULT_TORII_ADDR: SocketAddr = socket_addr!(127.0.0.1:8080); /// Get sample trusted peers. The public key must be the same as `configuration.public_key` /// @@ -33,57 +42,57 @@ pub fn get_trusted_peers(public_key: Option<&PublicKey>) -> HashSet { .map(|(a, k)| PeerId::new(a.parse().expect("Valid"), PublicKey::from_str(k).unwrap())) .collect(); if let Some(pubkey) = public_key { - trusted_peers.insert(PeerId::new(DEFAULT_TORII_P2P_ADDR.clone(), pubkey.clone())); + trusted_peers.insert(PeerId { + address: DEFAULT_P2P_ADDR.clone(), + public_key: pubkey.clone(), + }); } trusted_peers } #[allow(clippy::implicit_hasher)] -/// Get a sample Iroha configuration proxy. Trusted peers must be +/// Get a sample Iroha configuration on user-layer level. Trusted peers must be /// specified in this function, including the current peer. Use [`get_trusted_peers`] /// to populate `trusted_peers` if in doubt. Almost equivalent to the [`get_config`] /// function, except the proxy is left unbuilt. /// /// # Panics /// - when [`KeyPair`] generation fails (rare case). -pub fn get_config_proxy( +pub fn get_user_config( peers: UniqueVec, chain_id: Option, key_pair: Option, -) -> ConfigurationProxy { - let chain_id = chain_id.unwrap_or_else(|| ChainId::new("0")); +) -> UserConfig { + let chain_id = chain_id.unwrap_or_else(|| ChainId::new("0".to_owned())); let (public_key, private_key) = key_pair.unwrap_or_else(KeyPair::generate).into(); iroha_logger::info!(%public_key); - ConfigurationProxy { - chain_id: Some(chain_id), - public_key: Some(public_key.clone()), - private_key: Some(private_key.clone()), - sumeragi: Some(Box::new(iroha_config::sumeragi::ConfigurationProxy { - max_transactions_in_block: Some(2), - trusted_peers: Some(TrustedPeers { peers }), - ..iroha_config::sumeragi::ConfigurationProxy::default() - })), - torii: Some(Box::new(iroha_config::torii::ConfigurationProxy { - p2p_addr: Some(DEFAULT_TORII_P2P_ADDR.clone()), - api_url: Some(DEFAULT_API_ADDR.clone()), - ..iroha_config::torii::ConfigurationProxy::default() - })), - block_sync: Some(iroha_config::block_sync::ConfigurationProxy { - block_batch_size: Some(1), - gossip_period_ms: Some(500), - ..iroha_config::block_sync::ConfigurationProxy::default() - }), - queue: Some(iroha_config::queue::ConfigurationProxy { - ..iroha_config::queue::ConfigurationProxy::default() - }), - genesis: Some(Box::new(iroha_config::genesis::ConfigurationProxy { - private_key: Some(Some(private_key)), - public_key: Some(public_key), - file: Some(Some("./genesis.json".into())), - })), - ..ConfigurationProxy::default() - } + + let mut config = UserConfig::new(); + + config.iroha.chain_id.set(chain_id); + config.iroha.public_key.set(public_key.clone()); + config.iroha.private_key.set(private_key.clone()); + config.iroha.p2p_address.set(DEFAULT_P2P_ADDR); + config + .chain_wide + .max_transactions_in_block + .set(2.try_into().unwrap()); + config.sumeragi.trusted_peers.peers = peers.to_vec(); + config.torii.address.set(DEFAULT_TORII_ADDR); + config + .network + .max_blocks_per_gossip + .set(1.try_into().unwrap()); + config + .network + .block_gossip_period + .set(UserDuration(Duration::from_millis(500))); + config.genesis.private_key.set(private_key); + config.genesis.public_key.set(public_key); + config.genesis.file.set("./genesis.json".into()); + + config } #[allow(clippy::implicit_hasher)] @@ -97,10 +106,14 @@ pub fn get_config( trusted_peers: UniqueVec, chain_id: Option, key_pair: Option, -) -> Configuration { - get_config_proxy(trusted_peers, chain_id, key_pair) - .build() - .expect("Iroha config should build as all required fields were provided") +) -> Config { + get_user_config(trusted_peers, chain_id, key_pair) + .unwrap_partial() + .expect("config should build as all required fields were provided") + .parse(CliContext { + submit_genesis: true, + }) + .expect("config should finalize as the input is semantically valid (or there is a bug)") } /// Construct executor from path. diff --git a/config/Cargo.toml b/config/Cargo.toml index 896ace64ace..83d9378f568 100644 --- a/config/Cargo.toml +++ b/config/Cargo.toml @@ -32,7 +32,7 @@ derive_more = { workspace = true } cfg-if = { workspace = true } once_cell = { workspace = true } nonzero_ext = "0.3.0" -toml = "0.8.8" +toml = { workspace = true } parse-display = { workspace = true } merge = "0.1.0" diff --git a/config/base/src/lib.rs b/config/base/src/lib.rs index 2b7f4f6488a..4dc347d957f 100644 --- a/config/base/src/lib.rs +++ b/config/base/src/lib.rs @@ -4,9 +4,12 @@ #![allow(missing_docs)] use std::{ + borrow::Cow, cell::RefCell, collections::{HashMap, HashSet}, + env::VarError, error::Error, + ffi::OsString, fmt::{Debug, Display, Formatter}, ops::Sub, str::FromStr, @@ -50,7 +53,7 @@ macro_rules! impl_deserialize_from_str { /// User-provided duration #[derive(Debug, Copy, Clone, Deserialize, Serialize, Ord, PartialOrd, Eq, PartialEq)] -pub struct UserDuration(Duration); +pub struct UserDuration(pub Duration); impl UserDuration { pub fn get(self) -> Duration { @@ -68,18 +71,26 @@ impl ByteSize { } } -pub trait Complete { - type Output; +#[derive(thiserror::Error, Debug)] +#[error("missing field: `{path}`")] +pub struct MissingFieldError { + path: String, +} - fn complete(self) -> CompleteResult; +impl MissingFieldError { + pub fn new(s: &str) -> Self { + Self { path: s.to_owned() } + } } -pub trait ReadEnv { - fn get(&self, key: impl AsRef) -> Option<&str>; +pub trait ReadEnv { + /// TODO document why cow + fn get(&self, key: impl AsRef) -> Result>, E>; } pub trait FromEnv { - fn from_env(env: &impl ReadEnv) -> FromEnvResult + // E: Error so that it could be wrapped into Report + fn from_env>(env: &R) -> FromEnvResult where Self: Sized; } @@ -92,7 +103,7 @@ impl FromEnv for T where T: FromEnvDefaultFallback + Default, { - fn from_env(_env: &impl ReadEnv) -> FromEnvResult + fn from_env>(_env: &R) -> FromEnvResult where Self: Sized, { @@ -134,29 +145,9 @@ impl Emitter { } } -#[derive(thiserror::Error, Debug)] -pub enum CompleteError { - #[error("Missing field: {path}")] - MissingField { path: String }, - #[error(transparent)] - Custom(#[from] Report), -} - -pub type CompleteResult = eyre::Result>; - -impl CompleteError { - pub fn missing_field(field_name: impl AsRef) -> Self { - Self::MissingField { - path: field_name.as_ref().to_string(), - } - } -} - -impl Emitter { +impl Emitter { pub fn emit_missing_field(&mut self, field_name: impl AsRef) { - self.emit(CompleteError::MissingField { - path: field_name.as_ref().to_string(), - }) + self.emit(MissingFieldError::new(field_name.as_ref())) } } @@ -238,13 +229,40 @@ impl TestEnv { } } -impl ReadEnv for TestEnv { - fn get(&self, key: impl AsRef) -> Option<&str> { +#[derive(thiserror::Error, Debug, Copy, Clone)] +#[error("should never occur")] +pub struct NeverError; + +impl ReadEnv for TestEnv { + fn get(&self, key: impl AsRef) -> Result>, NeverError> { self.visited.borrow_mut().insert(key.as_ref().to_string()); - self.map.get(key.as_ref()).map(std::string::String::as_str) + Ok(self + .map + .get(key.as_ref()) + .map(String::as_str) + .map(Cow::from)) + } +} + +#[derive(Debug, Copy, Clone)] +pub struct StdEnv; + +impl ReadEnv for StdEnv { + fn get(&self, key: impl AsRef) -> Result>, StdEnvError> { + match std::env::var(key.as_ref()) { + Ok(value) => Ok(Some(value.into())), + Err(VarError::NotPresent) => Ok(None), + Err(VarError::NotUnicode(input)) => Err(StdEnvError::NotUnicode(input)), + } } } +#[derive(Debug, thiserror::Error)] +pub enum StdEnvError { + #[error("the specified environment variable was found, but it did not contain valid unicode data: {0:?}")] + NotUnicode(OsString), +} + pub enum ParseEnvResult { Value(T), ParseError, @@ -256,25 +274,33 @@ where T: FromStr, ::Err: Error + Send + Sync + 'static, { - pub fn parse_simple( + pub fn parse_simple( emitter: &mut Emitter, - env: &impl ReadEnv, + env: &impl ReadEnv, env_key: impl AsRef, field_name: impl AsRef, ) -> Self { - match env + let read = match env .get(env_key.as_ref()) - .map(FromStr::from_str) - .transpose() - .wrap_err_with(|| { - eyre!( - "failed to parse `{}` field from `{}` env variable", - field_name.as_ref(), - env_key.as_ref() - ) - }) { - Ok(Some(x)) => Self::Value(x), - Ok(None) => Self::None, + .map_err(|err| eyre!("{err}")) + .wrap_err_with(|| eyre!("ooops")) + { + Ok(Some(value)) => value, + Ok(None) => return Self::None, + Err(report) => { + emitter.emit(report); + return Self::ParseError; + } + }; + + match FromStr::from_str(read.as_ref()).wrap_err_with(|| { + eyre!( + "failed to parse `{}` field from `{}` env variable", + field_name.as_ref(), + env_key.as_ref() + ) + }) { + Ok(value) => Self::Value(value), Err(report) => { emitter.emit(report); Self::ParseError @@ -330,6 +356,10 @@ impl UserField { pub fn get(self) -> Option { self.0 } + + pub fn set(&mut self, value: T) { + self.0.as_mut().map(|x| *x = value); + } } impl From> for UserField { @@ -339,15 +369,23 @@ impl From> for UserField { } } +pub trait UnwrapPartial { + type Output; + + fn unwrap_partial(self) -> UnwrapPartialResult; +} + +pub type UnwrapPartialResult = Result>; + #[cfg(test)] mod tests { use super::*; #[test] fn single_missing_field() { - let mut emitter = Emitter::new(); + let mut emitter: Emitter = Emitter::new(); - emitter.emit(CompleteError::missing_field("foo")); + emitter.emit_missing_field("foo"); let err = emitter.finish().unwrap_err(); @@ -356,10 +394,10 @@ mod tests { #[test] fn multiple_missing_fields() { - let mut emitter = Emitter::new(); + let mut emitter: Emitter = Emitter::new(); - emitter.emit(CompleteError::missing_field("foo")); - emitter.emit(CompleteError::missing_field("bar")); + emitter.emit_missing_field("foo"); + emitter.emit_missing_field("bar"); let err = emitter.finish().unwrap_err(); diff --git a/config/src/parameters/actual.rs b/config/src/parameters/actual.rs index a411d880a86..1656c42352f 100644 --- a/config/src/parameters/actual.rs +++ b/config/src/parameters/actual.rs @@ -7,17 +7,17 @@ use std::{ use iroha_config_base::ByteSize; use iroha_crypto::{KeyPair, PublicKey}; use iroha_data_model::{ - metadata::Limits as MetadataLimits, peer::PeerId, transaction::TransactionLimits, LengthLimits, - Level, + metadata::Limits as MetadataLimits, peer::PeerId, transaction::TransactionLimits, ChainId, + LengthLimits, Level, }; use iroha_genesis::RawGenesisBlock; use iroha_primitives::{addr::SocketAddr, unique_vec::UniqueVec}; use serde::{Deserialize, Serialize}; use url::Url; -use crate::{kura::Mode, logger::Format}; +use crate::{kura::Mode, logger::Format, parameters::user_layer}; -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Root { pub iroha: Iroha, pub genesis: Genesis, @@ -35,13 +35,20 @@ pub struct Root { pub chain_wide: ChainWide, } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Iroha { + pub chain_id: ChainId, pub key_pair: KeyPair, pub p2p_address: SocketAddr, } -#[derive(Debug)] +impl Iroha { + pub fn peer_id(&self) -> PeerId { + PeerId::new(&self.p2p_address, self.key_pair.public_key()) + } +} + +#[derive(Debug, Clone)] pub enum Genesis { /// The peer can only observe the genesis block Partial { @@ -57,47 +64,31 @@ pub enum Genesis { }, } -#[derive(Debug)] +impl Genesis { + pub fn public_key(&self) -> &PublicKey { + match self { + Genesis::Partial { public_key } => &public_key, + Genesis::Full { key_pair, .. } => key_pair.public_key(), + } + } +} + +#[derive(Debug, Clone)] pub struct Kura { pub init_mode: Mode, pub block_store_path: PathBuf, pub debug_output_new_blocks: bool, } -/// `Queue` configuration. -#[derive(Copy, Clone, Deserialize, Serialize, Debug)] -pub struct Queue { - pub max_transactions_in_queue: NonZeroUsize, - pub max_transactions_in_queue_per_user: NonZeroUsize, - pub transaction_time_to_live: Duration, - pub future_threshold: Duration, -} - impl Default for Queue { fn default() -> Self { todo!() } } -#[derive(Debug)] -pub struct Logger { - /// Level of logging verbosity - pub level: Level, - /// Output format - pub format: Format, - #[cfg(feature = "tokio-console")] - /// Address of tokio console (only available under "tokio-console" feature) - pub tokio_console_addr: SocketAddr, -} - -#[derive(Debug)] -pub struct Snapshot { - pub create_every: Duration, - pub store_path: PathBuf, - pub creation_enabled: bool, -} +pub use user_layer::{LoggerFull as Logger, QueueFull as Queue, SnapshotFull as Snapshot}; -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Sumeragi { pub trusted_peers: UniqueVec, pub debug_force_soft_fork: bool, @@ -114,13 +105,13 @@ impl Default for LiveQueryStore { } } -#[derive(Debug)] +#[derive(Debug, Clone, Copy)] pub struct BlockSync { pub gossip_period: Duration, pub batch_size: NonZeroU32, } -#[derive(Debug)] +#[derive(Debug, Clone, Copy)] pub struct TransactionGossiper { pub gossip_period: Duration, pub batch_size: NonZeroU32, @@ -158,14 +149,14 @@ impl Default for WasmRuntime { } } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Torii { pub address: SocketAddr, pub max_content_len: ByteSize, } /// Complete configuration needed to start regular telemetry. -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct RegularTelemetry { #[allow(missing_docs)] pub name: String, @@ -178,7 +169,7 @@ pub struct RegularTelemetry { } /// Complete configuration needed to start dev telemetry. -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct DevTelemetry { #[allow(missing_docs)] pub file: PathBuf, diff --git a/config/src/parameters/user_layer.rs b/config/src/parameters/user_layer.rs index cfb005bd5bd..5c4606eaa0a 100644 --- a/config/src/parameters/user_layer.rs +++ b/config/src/parameters/user_layer.rs @@ -1,21 +1,25 @@ use std::{ + error::Error, fmt::Debug, fs::File, io::Read, num::{NonZeroU32, NonZeroU64, NonZeroUsize}, ops::{Add, Div}, path::{Path, PathBuf}, + str::FromStr, + time::Duration, }; use eyre::{eyre, Report, WrapErr}; use iroha_config_base::{ - ByteSize, Complete, CompleteError, CompleteResult, Emitter, FromEnv, FromEnvDefaultFallback, - FromEnvResult, Merge, ParseEnvResult, ReadEnv, UserDuration, UserField, + ByteSize, Emitter, ErrorsCollection, FromEnv, FromEnvDefaultFallback, FromEnvResult, Merge, + MissingFieldError, ParseEnvResult, ReadEnv, UnwrapPartial, UnwrapPartialResult, UserDuration, + UserField, }; use iroha_crypto::{KeyPair, PrivateKey, PublicKey}; use iroha_data_model::{ - metadata::Limits as MetadataLimits, peer::PeerId, transaction::TransactionLimits, LengthLimits, - Level, + metadata::Limits as MetadataLimits, peer::PeerId, transaction::TransactionLimits, ChainId, + LengthLimits, Level, }; use iroha_primitives::{addr::SocketAddr, unique_vec::UniqueVec}; use serde::{Deserialize, Serialize}; @@ -24,25 +28,41 @@ use url::Url; use super::defaults::{ chain_wide::*, kura::*, logger::*, queue::*, snapshot::*, telemetry::*, torii::*, }; -use crate::{kura::Mode, logger::Format, parameters::actual}; +use crate::{ + kura::Mode, + logger::Format, + parameters::{ + actual, + defaults::network::{ + DEFAULT_BLOCK_GOSSIP_PERIOD, DEFAULT_MAX_BLOCKS_PER_GOSSIP, + DEFAULT_MAX_TRANSACTIONS_PER_GOSSIP, DEFAULT_TRANSACTION_GOSSIP_PERIOD, + }, + }, +}; #[derive(Deserialize, Serialize, Debug, Default, Merge)] #[serde(deny_unknown_fields, default)] -pub struct Root { - iroha: Iroha, - genesis: Genesis, - kura: Kura, - sumeragi: Sumeragi, - network: Network, - logger: Logger, - queue: Queue, - snapshot: Snapshot, - telemetry: Telemetry, - torii: Torii, - chain_wide: ChainWide, -} - -impl Root { +pub struct RootPartial { + pub iroha: IrohaPartial, + pub genesis: GenesisPartial, + pub kura: KuraPartial, + pub sumeragi: SumeragiPartial, + pub network: NetworkPartial, + pub logger: LoggerPartial, + pub queue: QueuePartial, + pub snapshot: SnapshotPartial, + pub telemetry: TelemetryPartial, + pub torii: ToriiPartial, + pub chain_wide: ChainWidePartial, +} + +impl RootPartial { + /// Creates new empty user configuration + pub fn new() -> Self { + // TODO: generate this function with macro. For now, use default + Default::default() + } + pub fn from_toml(path: impl AsRef) -> eyre::Result { let contents = { let mut file = File::open(path.as_ref()).wrap_err_with(|| { @@ -85,15 +105,121 @@ impl Root { } } -impl Complete for Root { - type Output = actual::Root; +#[derive(Debug)] +pub struct RootFull { + iroha: IrohaFull, + genesis: GenesisFull, + kura: KuraFull, + sumeragi: SumeragiFull, + network: NetworkFull, + logger: LoggerFull, + queue: QueueFull, + snapshot: SnapshotFull, + telemetry: TelemetryFull, + torii: ToriiFull, + chain_wide: ChainWideFull, +} - fn complete(self) -> CompleteResult { +impl RootFull { + pub fn parse(self, cli: CliContext) -> Result> { let mut emitter = Emitter::new(); - macro_rules! complete_nested { + let iroha = self.iroha.parse().map_or_else( + |err| { + emitter.emit(err); + None + }, + Some, + ); + + let genesis = self.genesis.parse(&cli).map_or_else( + |err| { + // FIXME + emitter.emit(eyre!("{err}")); + None + }, + Some, + ); + + let kura = self.kura.parse(); + + let sumeragi = self.sumeragi.parse().map_or_else( + |err| { + emitter.emit(err); + None + }, + Some, + ); + + let (block_sync, transaction_gossiper) = self.network.parse(); + + let logger = self.logger; + let queue = self.queue; + let snapshot = self.snapshot; + + let (torii, live_query_store) = self.torii.parse(); + + let telemetries = self.telemetry.parse().map_or_else( + |err| { + emitter.emit(err); + None + }, + Some, + ); + + let chain_wide = self.chain_wide.parse(); + + emitter.finish()?; + + let (regular_telemetry, dev_telemetry) = telemetries.unwrap(); + let iroha = iroha.unwrap(); + let genesis = genesis.unwrap(); + let sumeragi = sumeragi.unwrap(); + + if !cli.submit_genesis && sumeragi.trusted_peers.len() < 2 { + 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. + "))?; + } + + // TODO: validate that p2p_address and torii.address are not the same + + Ok(actual::Root { + iroha, + genesis, + sumeragi, + kura, + block_sync, + transaction_gossiper, + logger, + torii, + live_query_store, + queue, + regular_telemetry, + dev_telemetry, + chain_wide, + snapshot, + }) + } +} + +pub struct CliContext { + pub submit_genesis: bool, +} + +impl UnwrapPartial for RootPartial { + type Output = RootFull; + + fn unwrap_partial(self) -> UnwrapPartialResult { + let mut emitter = Emitter::new(); + + macro_rules! nested { ($item:expr) => { - match iroha_config_base::Complete::complete($item) { + match UnwrapPartial::unwrap_partial($item) { Ok(value) => Some(value), Err(error) => { emitter.emit_collection(error); @@ -103,49 +229,44 @@ impl Complete for Root { }; } - let iroha = complete_nested!(self.iroha); - let genesis = complete_nested!(self.genesis); - let kura = complete_nested!(self.kura); - let sumeragi = complete_nested!(self.sumeragi); - let network = complete_nested!(self.network); - let logger = complete_nested!(self.logger); - let queue = complete_nested!(self.queue); - let snapshot = complete_nested!(self.snapshot); - let telemetries = complete_nested!(self.telemetry); - let torii_and_query = complete_nested!(self.torii); - let chain_wide = complete_nested!(self.chain_wide); + let iroha = nested!(self.iroha); + let genesis = nested!(self.genesis); + let kura = nested!(self.kura); + let sumeragi = nested!(self.sumeragi); + let network = nested!(self.network); + let logger = nested!(self.logger); + let queue = nested!(self.queue); + let snapshot = nested!(self.snapshot); + let telemetry = nested!(self.telemetry); + let torii = nested!(self.torii); + let chain_wide = nested!(self.chain_wide); emitter.finish()?; - let (regular_telemetry, dev_telemetry) = telemetries.unwrap(); - let (torii, live_query_store) = torii_and_query.unwrap(); - let (block_sync, transaction_gossiper) = network.unwrap(); - - Ok(actual::Root { + Ok(RootFull { iroha: iroha.unwrap(), genesis: genesis.unwrap(), kura: kura.unwrap(), sumeragi: sumeragi.unwrap(), - block_sync, - transaction_gossiper, + telemetry: telemetry.unwrap(), logger: logger.unwrap(), queue: queue.unwrap(), snapshot: snapshot.unwrap(), - regular_telemetry, - dev_telemetry, - torii, - live_query_store, + torii: torii.unwrap(), + network: network.unwrap(), chain_wide: chain_wide.unwrap(), }) } } -impl FromEnv for Root { - fn from_env(env: &impl ReadEnv) -> FromEnvResult { - fn from_env_nested( - env: &impl ReadEnv, - emitter: &mut Emitter, - ) -> Option { +impl FromEnv for RootPartial { + fn from_env>(env: &R) -> FromEnvResult { + fn from_env_nested(env: &R, emitter: &mut Emitter) -> Option + where + T: FromEnv, + R: ReadEnv, + RE: Error, + { match FromEnv::from_env(env) { Ok(parsed) => Some(parsed), Err(errors) => { @@ -189,55 +310,66 @@ impl FromEnv for Root { #[derive(Deserialize, Serialize, Debug, Default, Merge)] #[serde(deny_unknown_fields, default)] -pub struct Iroha { +pub struct IrohaPartial { + pub chain_id: UserField, pub public_key: UserField, pub private_key: UserField, pub p2p_address: UserField, } -impl Complete for Iroha { - type Output = actual::Iroha; +#[derive(Debug)] +pub struct IrohaFull { + pub chain_id: ChainId, + pub public_key: PublicKey, + pub private_key: PrivateKey, + pub p2p_address: SocketAddr, +} - fn complete(self) -> CompleteResult { - let mut emitter = Emitter::::new(); +impl UnwrapPartial for IrohaPartial { + type Output = IrohaFull; - let key_pair = match (self.public_key.get(), self.private_key.get()) { - (Some(public_key), Some(private_key)) => { - KeyPair::new(public_key, private_key) - .map(Some) - .wrap_err("failed to construct a key pair from `iroha.public_key` and `iroha.private_key` configuration parameters") - .unwrap_or_else(|report| { - emitter.emit(CompleteError::Custom(report)); - None - }) - }, - (public_key, private_key) => { - if public_key.is_none() { - emitter.emit_missing_field("iroha.public_key"); - } - if private_key.is_none() { - emitter.emit_missing_field("iroha.private_key"); - } - None - } - }; + fn unwrap_partial(self) -> UnwrapPartialResult { + let mut emitter = Emitter::new(); + if self.chain_id.is_none() { + emitter.emit_missing_field("iroha.chain_id"); + } + if self.public_key.is_none() { + emitter.emit_missing_field("iroha.public_key"); + } + if self.private_key.is_none() { + emitter.emit_missing_field("iroha.private_key"); + } if self.p2p_address.is_none() { emitter.emit_missing_field("iroha.p2p_address"); } emitter.finish()?; - Ok(actual::Iroha { - key_pair: key_pair.unwrap(), + Ok(IrohaFull { + chain_id: self.chain_id.get().unwrap(), + public_key: self.public_key.get().unwrap(), + private_key: self.private_key.get().unwrap(), p2p_address: self.p2p_address.get().unwrap(), }) } } -pub(crate) fn private_key_from_env( +impl IrohaFull { + fn parse(self) -> Result { + let key_pair = KeyPair::new(self.public_key, self.private_key).wrap_err("failed to construct a key pair from `iroha.public_key` and `iroha.private_key` configuration parameters")?; + + Ok(actual::Iroha { + chain_id: self.chain_id, + key_pair, + p2p_address: self.p2p_address, + }) + } +} + +pub(crate) fn private_key_from_env( emitter: &mut Emitter, - env: &impl ReadEnv, + env: &impl ReadEnv, env_key_base: impl AsRef, name_base: impl AsRef, ) -> ParseEnvResult { @@ -248,54 +380,76 @@ pub(crate) fn private_key_from_env( let digest_function = ParseEnvResult::parse_simple(emitter, env, &digest_env, &digest_name); - let payload = env.get(&payload_env).map(ToOwned::to_owned); + let payload = match env + .get(&payload_env) + .map_err(|err| eyre!("{err}")) + .wrap_err("oops") + { + Ok(Some(value)) => ParseEnvResult::Value(value), + Ok(None) => ParseEnvResult::None, + Err(err) => { + emitter.emit(err); + ParseEnvResult::ParseError + } + }; match (digest_function, payload) { - (ParseEnvResult::Value(digest_function), Some(payload)) => { - PrivateKey::from_hex(digest_function, &payload) - .wrap_err_with(|| { - eyre!( - "failed to construct `{}` from `{}` and `{}` environment variables", - name_base.as_ref(), - &digest_env, - &payload_env - ) - }) - .map_or_else( - |report| { - emitter.emit(report); - ParseEnvResult::ParseError - }, - ParseEnvResult::Value, + (ParseEnvResult::Value(digest_function), ParseEnvResult::Value(payload)) => { + match PrivateKey::from_hex(digest_function, &payload).wrap_err_with(|| { + eyre!( + "failed to construct `{}` from `{}` and `{}` environment variables", + name_base.as_ref(), + &digest_env, + &payload_env ) + }) { + Ok(value) => return ParseEnvResult::Value(value), + Err(report) => { + emitter.emit(report); + } + } } - (ParseEnvResult::None, None) | (ParseEnvResult::ParseError, _) => ParseEnvResult::None, - (ParseEnvResult::Value(_), None) => { - emitter.emit(eyre!( - "`{}` env was provided, but `{}` was not", - &digest_env, - &payload_env - )); - ParseEnvResult::ParseError - } - (ParseEnvResult::None, Some(_)) => { + (ParseEnvResult::None, ParseEnvResult::None) => return ParseEnvResult::None, + (ParseEnvResult::Value(_), ParseEnvResult::None) => emitter.emit(eyre!( + "`{}` env was provided, but `{}` was not", + &digest_env, + &payload_env + )), + (ParseEnvResult::None, ParseEnvResult::Value(_)) => { emitter.emit(eyre!( "`{}` env was provided, but `{}` was not", &payload_env, &digest_env )); - ParseEnvResult::ParseError + } + (ParseEnvResult::ParseError, _) | (_, ParseEnvResult::ParseError) => { + // emitter already has these errors + // adding this branch for exhaustiveness } } + + ParseEnvResult::ParseError } -impl FromEnv for Iroha { - fn from_env(env: &impl ReadEnv) -> FromEnvResult +impl FromEnv for IrohaPartial { + fn from_env>(env: &R) -> FromEnvResult where Self: Sized, { let mut emitter = Emitter::new(); + let chain_id = env + .get("CHAIN_ID") + .map_err(|e| eyre!("{e}")) + .wrap_err("failed to read CHAIN_ID field (iroha.chain_id param)") + .map_or_else( + |err| { + emitter.emit(err); + None + }, + |maybe_value| maybe_value.map(|value| ChainId::new(value.into_owned())), + ) + .into(); let public_key = ParseEnvResult::parse_simple(&mut emitter, env, "PUBLIC_KEY", "iroha.public_key") .into(); @@ -308,6 +462,7 @@ impl FromEnv for Iroha { emitter.finish()?; Ok(Self { + chain_id, public_key, private_key, p2p_address, @@ -317,40 +472,41 @@ impl FromEnv for Iroha { #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Default, Merge)] #[serde(deny_unknown_fields, default)] -pub struct Genesis { +pub struct GenesisPartial { pub public_key: UserField, pub private_key: UserField, - #[serde(default)] pub file: UserField, } -impl Complete for Genesis { - type Output = actual::Genesis; +#[derive(Debug)] +pub struct GenesisFull { + pub public_key: PublicKey, + pub private_key: Option, + pub file: Option, +} + +impl UnwrapPartial for GenesisPartial { + type Output = GenesisFull; - fn complete(self) -> CompleteResult { + fn unwrap_partial(self) -> UnwrapPartialResult { let public_key = self .public_key .get() - .ok_or_else(|| CompleteError::missing_field("genesis.public_key"))?; + .ok_or_else(|| MissingFieldError::new("genesis.public_key"))?; - match (self.private_key.get(), self.file.get()) { - (None, None) => Ok(actual::Genesis::Partial { public_key }), - (Some(private_key), Some(file)) => Ok(actual::Genesis::Full { - key_pair: KeyPair::new(public_key, private_key) - .map_err(GenesisConfigError::from) - .wrap_err("FIXME") - .map_err(CompleteError::Custom)?, - file, - }), - _ => Err(GenesisConfigError::Inconsistent) - .wrap_err("FIXME") - .map_err(CompleteError::Custom)?, - } + let private_key = self.private_key.get(); + let file = self.file.get(); + + Ok(GenesisFull { + public_key, + private_key, + file, + }) } } -impl FromEnv for Genesis { - fn from_env(env: &impl ReadEnv) -> FromEnvResult +impl FromEnv for GenesisPartial { + fn from_env>(env: &R) -> FromEnvResult where Self: Sized, { @@ -383,6 +539,22 @@ impl FromEnv for Genesis { } } +impl GenesisFull { + fn parse(self, cli: &CliContext) -> Result { + match (self.private_key, self.file) { + (None, None) => Ok(actual::Genesis::Partial { + public_key: self.public_key, + }), + (Some(private_key), Some(file)) => Ok(actual::Genesis::Full { + key_pair: KeyPair::new(self.public_key, private_key) + .map_err(GenesisConfigError::from)?, + file, + }), + _ => Err(GenesisConfigError::Inconsistent), + } + } +} + #[derive(Debug, displaydoc::Display, thiserror::Error)] pub enum GenesisConfigError { /// `genesis.file` and `genesis.private_key` should be set together @@ -394,34 +566,91 @@ pub enum GenesisConfigError { /// `Kura` configuration. #[derive(Clone, Deserialize, Serialize, Debug, Default, Merge)] #[serde(deny_unknown_fields, default)] -pub struct Kura { - pub init_mode: Option, - pub block_store_path: Option, - pub debug: KuraDebug, +pub struct KuraPartial { + pub init_mode: UserField, + pub block_store_path: UserField, + pub debug: KuraDebugPartial, +} + +#[derive(Debug)] +pub struct KuraFull { + pub init_mode: Mode, + pub block_store_path: PathBuf, + pub debug: KuraDebugFull, +} + +impl UnwrapPartial for KuraPartial { + type Output = KuraFull; + + fn unwrap_partial(self) -> Result> { + let mut emitter = Emitter::new(); + + let init_mode = self.init_mode.unwrap_or_default(); + + let block_store_path = self + .block_store_path + .get() + .unwrap_or_else(|| PathBuf::from(DEFAULT_BLOCK_STORE_PATH)); + + let debug = UnwrapPartial::unwrap_partial(self.debug) + .map(Some) + .unwrap_or_else(|err| { + emitter.emit_collection(err); + None + }); + + emitter.finish()?; + + Ok(KuraFull { + init_mode, + block_store_path, + debug: debug.unwrap(), + }) + } +} + +impl KuraFull { + fn parse(self) -> actual::Kura { + let Self { + init_mode, + block_store_path, + debug: + KuraDebugFull { + output_new_blocks: debug_output_new_blocks, + }, + } = self; + + actual::Kura { + init_mode, + block_store_path, + debug_output_new_blocks, + } + } } #[derive(Clone, Deserialize, Serialize, Debug, Default, Merge)] #[serde(deny_unknown_fields, default)] -pub struct KuraDebug { - output_new_blocks: Option, +pub struct KuraDebugPartial { + output_new_blocks: UserField, } -impl Complete for Kura { - type Output = actual::Kura; +#[derive(Debug)] +pub struct KuraDebugFull { + output_new_blocks: bool, +} + +impl UnwrapPartial for KuraDebugPartial { + type Output = KuraDebugFull; - fn complete(self) -> CompleteResult { - Ok(actual::Kura { - init_mode: self.init_mode.unwrap_or_default(), - block_store_path: self - .block_store_path - .unwrap_or_else(|| PathBuf::from(DEFAULT_BLOCK_STORE_PATH)), - debug_output_new_blocks: self.debug.output_new_blocks.unwrap_or(false), + fn unwrap_partial(self) -> Result> { + Ok(KuraDebugFull { + output_new_blocks: self.output_new_blocks.unwrap_or(false), }) } } -impl FromEnv for Kura { - fn from_env(env: &impl ReadEnv) -> FromEnvResult +impl FromEnv for KuraPartial { + fn from_env>(env: &R) -> FromEnvResult where Self: Sized, { @@ -450,7 +679,7 @@ impl FromEnv for Kura { Ok(Self { init_mode, block_store_path, - debug: KuraDebug { + debug: KuraDebugPartial { output_new_blocks: debug_output_new_blocks, }, }) @@ -459,34 +688,86 @@ impl FromEnv for Kura { #[derive(Deserialize, Serialize, Debug, Default, Merge)] #[serde(deny_unknown_fields, default)] -pub struct Sumeragi { - pub block_gossip_period: UserField, - pub max_blocks_per_gossip: UserField, - pub max_transactions_per_gossip: UserField, - pub transaction_gossip_period: UserField, +pub struct SumeragiPartial { pub trusted_peers: UserTrustedPeers, - pub debug: SumeragiDebug, + pub debug: SumeragiDebugPartial, +} + +#[derive(Debug)] +pub struct SumeragiFull { + pub trusted_peers: Vec, + pub debug: SumeragiDebugFull, +} + +impl SumeragiFull { + fn parse(self) -> Result { + let Self { + trusted_peers, + debug: SumeragiDebugFull { force_soft_fork }, + } = self; + + let trusted_peers = construct_unique_vec(trusted_peers)?; + + Ok(actual::Sumeragi { + trusted_peers, + debug_force_soft_fork: force_soft_fork, + }) + } +} + +impl UnwrapPartial for SumeragiPartial { + type Output = SumeragiFull; + + fn unwrap_partial(self) -> UnwrapPartialResult { + let mut emitter = Emitter::new(); + + let trusted_peers = self.trusted_peers.unwrap_partial().map_or_else( + |err| { + emitter.emit_collection(err); + None + }, + Some, + ); + + let debug = self.debug.unwrap_partial().map_or_else( + |err| { + emitter.emit_collection(err); + None + }, + Some, + ); + + emitter.finish()?; + + Ok(SumeragiFull { + trusted_peers: trusted_peers.unwrap(), + debug: debug.unwrap(), + }) + } } #[derive(Deserialize, Serialize, Debug, Default, Merge)] #[serde(deny_unknown_fields, default)] -pub struct SumeragiDebug { +pub struct SumeragiDebugPartial { pub force_soft_fork: UserField, } -impl Complete for Sumeragi { - type Output = actual::Sumeragi; +impl UnwrapPartial for SumeragiDebugPartial { + type Output = SumeragiDebugFull; - fn complete(self) -> CompleteResult { - Ok(actual::Sumeragi { - trusted_peers: construct_unique_vec(self.trusted_peers.peers) - .map_err(CompleteError::Custom)?, - debug_force_soft_fork: self.debug.force_soft_fork.unwrap_or(false), + fn unwrap_partial(self) -> UnwrapPartialResult { + Ok(SumeragiDebugFull { + force_soft_fork: self.force_soft_fork.unwrap_or(false), }) } } -impl FromEnvDefaultFallback for Sumeragi {} +#[derive(Debug)] +pub struct SumeragiDebugFull { + pub force_soft_fork: bool, +} + +impl FromEnvDefaultFallback for SumeragiPartial {} #[derive(Deserialize, Serialize, Default, PartialEq, Eq, Debug, Clone)] #[serde(transparent)] @@ -495,6 +776,13 @@ pub struct UserTrustedPeers { pub peers: Vec, } +impl UnwrapPartial for UserTrustedPeers { + type Output = Vec; + fn unwrap_partial(self) -> UnwrapPartialResult { + Ok(self.peers) + } +} + impl Merge for UserTrustedPeers { fn merge(&mut self, mut other: Self) { self.peers.append(other.peers.as_mut()) @@ -517,42 +805,102 @@ fn construct_unique_vec( #[derive(Deserialize, Serialize, Debug, Default, Merge)] #[serde(deny_unknown_fields, default)] -pub struct Network { +pub struct NetworkPartial { pub block_gossip_period: UserField, pub max_blocks_per_gossip: UserField, pub max_transactions_per_gossip: UserField, pub transaction_gossip_period: UserField, } -impl Complete for Network { - type Output = (actual::BlockSync, actual::TransactionGossiper); +#[derive(Debug)] +pub struct NetworkFull { + pub block_gossip_period: Duration, + pub max_blocks_per_gossip: NonZeroU32, + pub max_transactions_per_gossip: NonZeroU32, + pub transaction_gossip_period: Duration, +} - fn complete(self) -> CompleteResult { - todo!() +impl UnwrapPartial for NetworkPartial { + type Output = NetworkFull; + + fn unwrap_partial(self) -> UnwrapPartialResult { + Ok(NetworkFull { + block_gossip_period: self + .block_gossip_period + .map(UserDuration::get) + .unwrap_or(DEFAULT_BLOCK_GOSSIP_PERIOD), + transaction_gossip_period: self + .transaction_gossip_period + .map(UserDuration::get) + .unwrap_or(DEFAULT_TRANSACTION_GOSSIP_PERIOD), + max_transactions_per_gossip: self + .max_transactions_per_gossip + .get() + .unwrap_or(DEFAULT_MAX_TRANSACTIONS_PER_GOSSIP), + max_blocks_per_gossip: self + .max_blocks_per_gossip + .get() + .unwrap_or(DEFAULT_MAX_BLOCKS_PER_GOSSIP), + }) + } +} + +impl NetworkFull { + fn parse(self) -> (actual::BlockSync, actual::TransactionGossiper) { + let Self { + max_blocks_per_gossip, + max_transactions_per_gossip, + block_gossip_period, + transaction_gossip_period, + } = self; + + ( + actual::BlockSync { + gossip_period: block_gossip_period, + batch_size: max_blocks_per_gossip, + }, + actual::TransactionGossiper { + gossip_period: transaction_gossip_period, + batch_size: max_transactions_per_gossip, + }, + ) } } -impl FromEnvDefaultFallback for Network {} +impl FromEnvDefaultFallback for NetworkPartial {} #[derive(Deserialize, Serialize, Debug, Default, Merge)] #[serde(deny_unknown_fields, default)] -pub struct Queue { +pub struct QueuePartial { /// The upper limit of the number of transactions waiting in the queue. pub max_transactions_in_queue: UserField, /// The upper limit of the number of transactions waiting in the queue for single user. /// Use this option to apply throttling. pub max_transactions_in_queue_per_user: UserField, /// The transaction will be dropped after this time if it is still in the queue. - pub transaction_time_to_live_ms: UserField, + pub transaction_time_to_live: UserField, + /// The threshold to determine if a transaction has been tampered to have a future timestamp. + pub future_threshold: UserField, +} + +#[derive(Debug, Clone, Copy)] +pub struct QueueFull { + /// The upper limit of the number of transactions waiting in the queue. + pub max_transactions_in_queue: NonZeroUsize, + /// The upper limit of the number of transactions waiting in the queue for single user. + /// Use this option to apply throttling. + pub max_transactions_in_queue_per_user: NonZeroUsize, + /// The transaction will be dropped after this time if it is still in the queue. + pub transaction_time_to_live: Duration, /// The threshold to determine if a transaction has been tampered to have a future timestamp. - pub future_threshold_ms: UserField, + pub future_threshold: Duration, } -impl Complete for Queue { - type Output = actual::Queue; +impl UnwrapPartial for QueuePartial { + type Output = QueueFull; - fn complete(self) -> CompleteResult { - Ok(actual::Queue { + fn unwrap_partial(self) -> UnwrapPartialResult { + Ok(QueueFull { max_transactions_in_queue: self .max_transactions_in_queue .unwrap_or(DEFAULT_MAX_TRANSACTIONS_IN_QUEUE), @@ -560,23 +908,23 @@ impl Complete for Queue { .max_transactions_in_queue_per_user .unwrap_or(DEFAULT_MAX_TRANSACTIONS_IN_QUEUE), transaction_time_to_live: self - .transaction_time_to_live_ms + .transaction_time_to_live .map_or(DEFAULT_TRANSACTION_TIME_TO_LIVE, UserDuration::get), future_threshold: self - .future_threshold_ms + .future_threshold .map_or(DEFAULT_FUTURE_THRESHOLD, UserDuration::get), }) } } -impl FromEnvDefaultFallback for Queue {} +impl FromEnvDefaultFallback for QueuePartial {} /// 'Logger' configuration. #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Default, Merge)] // `tokio_console_addr` is not `Copy`, but warning appears without `tokio-console` feature #[allow(missing_copy_implementations)] #[serde(deny_unknown_fields, default)] -pub struct Logger { +pub struct LoggerPartial { /// Level of logging verbosity pub level: UserField, /// Output format @@ -586,11 +934,22 @@ pub struct Logger { pub tokio_console_addr: UserField, } -impl Complete for Logger { - type Output = actual::Logger; +#[derive(Debug, Clone)] +pub struct LoggerFull { + /// Level of logging verbosity + pub level: Level, + /// Output format + pub format: Format, + #[cfg(feature = "tokio-console")] + /// Address of tokio console (only available under "tokio-console" feature) + pub tokio_console_addr: SocketAddr, +} - fn complete(self) -> CompleteResult { - Ok(actual::Logger { +impl UnwrapPartial for LoggerPartial { + type Output = LoggerFull; + + fn unwrap_partial(self) -> UnwrapPartialResult { + Ok(LoggerFull { level: self.level.unwrap_or_default(), format: self.format.unwrap_or_default(), #[cfg(feature = "tokio-console")] @@ -602,8 +961,8 @@ impl Complete for Logger { } } -impl FromEnv for Logger { - fn from_env(env: &impl ReadEnv) -> FromEnvResult +impl FromEnv for LoggerPartial { + fn from_env>(env: &R) -> FromEnvResult where Self: Sized, { @@ -626,89 +985,134 @@ impl FromEnv for Logger { #[derive(Clone, Deserialize, Serialize, Debug, Default, Merge)] #[serde(deny_unknown_fields, default)] -pub struct Telemetry { - /// The node's name to be seen on the telemetry +pub struct TelemetryPartial { pub name: UserField, - /// The url of the telemetry, e.g., ws://127.0.0.1:8001/submit pub url: UserField, - /// The minimum period of time in seconds to wait before reconnecting pub min_retry_period: UserField, - /// The maximum exponent of 2 that is used for increasing delay between reconnections pub max_retry_delay_exponent: UserField, - /// Dev telemetry configuration - #[serde(default)] - pub dev: DevUserLayer, + pub dev: TelemetryDevPartial, +} + +#[derive(Debug)] +pub struct TelemetryFull { + // Fields here are Options so that it is possible to warn the user if e.g. they provided `min_retry_period`, but haven't + // provided `name` and `url` + pub name: Option, + pub url: Option, + pub min_retry_period: Option, + pub max_retry_delay_exponent: Option, + pub dev: TelemetryDevFull, } #[derive(Clone, Deserialize, Serialize, Debug, Default, Merge)] -pub struct DevUserLayer { - /// The filepath that to write dev-telemetry to +pub struct TelemetryDevPartial { pub file: UserField, } -impl Complete for Telemetry { - type Output = ( - Option, - Option, - ); +#[derive(Debug)] +pub struct TelemetryDevFull { + pub file: Option, +} + +impl UnwrapPartial for TelemetryDevPartial { + type Output = TelemetryDevFull; + + fn unwrap_partial(self) -> UnwrapPartialResult { + Ok(TelemetryDevFull { + file: self.file.get(), + }) + } +} + +impl UnwrapPartial for TelemetryPartial { + type Output = TelemetryFull; - fn complete(self) -> CompleteResult { + fn unwrap_partial(self) -> UnwrapPartialResult { let Self { name, url, max_retry_delay_exponent, min_retry_period, - dev: DevUserLayer { file }, + dev, } = self; - let regular = match (name.get(), url.get()) { + Ok(TelemetryFull { + name: name.get(), + url: url.get(), + max_retry_delay_exponent: max_retry_delay_exponent.get(), + min_retry_period: min_retry_period.get().map(UserDuration::get), + dev: dev.unwrap_partial()?, + }) + } +} + +impl TelemetryFull { + fn parse( + self, + ) -> Result< + ( + Option, + Option, + ), + Report, + > { + let Self { + name, + url, + max_retry_delay_exponent, + min_retry_period, + dev: TelemetryDevFull { file }, + } = self; + + let regular = match (name, url) { (Some(name), Some(url)) => Some(actual::RegularTelemetry { name, url, max_retry_delay_exponent: max_retry_delay_exponent - .get() .unwrap_or(DEFAULT_MAX_RETRY_DELAY_EXPONENT), - min_retry_period: min_retry_period - .get() - .map_or(DEFAULT_MIN_RETRY_PERIOD, UserDuration::get), + min_retry_period: min_retry_period.unwrap_or(DEFAULT_MIN_RETRY_PERIOD), }), + // TODO warn user if they provided retry parameters while not providing essential ones (None, None) => None, - // TODO improve error detail - _ => Err(eyre!( - "telemetry.name and telemetry.file should be set together" - )) - .map_err(CompleteError::Custom)?, + _ => { + // TODO improve error detail + return Err(eyre!( + "telemetry.name and telemetry.file should be set together" + ))?; + } }; - let dev = file - .as_ref() - .map(|file| actual::DevTelemetry { file: file.clone() }); + let dev = file.map(|file| actual::DevTelemetry { file: file.clone() }); Ok((regular, dev)) } } -impl FromEnvDefaultFallback for Telemetry {} +impl FromEnvDefaultFallback for TelemetryPartial {} #[derive(Debug, Clone, Deserialize, Serialize, Default, Merge)] #[serde(deny_unknown_fields, default)] -pub struct Snapshot { - /// The period of time to wait between attempts to create new snapshot. - pub create_every_ms: UserField, - /// Path to the directory where snapshots should be stored +pub struct SnapshotPartial { + pub create_every: UserField, pub store_path: UserField, - /// Flag to enable or disable snapshot creation pub creation_enabled: UserField, } -impl Complete for Snapshot { - type Output = actual::Snapshot; +#[derive(Debug, Clone)] +pub struct SnapshotFull { + pub create_every: Duration, + pub store_path: PathBuf, + pub creation_enabled: bool, +} + +impl UnwrapPartial for SnapshotPartial { + type Output = SnapshotFull; - fn complete(self) -> CompleteResult { - Ok(actual::Snapshot { + fn unwrap_partial(self) -> UnwrapPartialResult { + Ok(SnapshotFull { creation_enabled: self.creation_enabled.unwrap_or(DEFAULT_ENABLED), create_every: self - .create_every_ms + .create_every .get() .map_or(DEFAULT_SNAPSHOT_CREATE_EVERY_MS, UserDuration::get), store_path: self @@ -719,8 +1123,8 @@ impl Complete for Snapshot { } } -impl FromEnv for Snapshot { - fn from_env(env: &impl ReadEnv) -> FromEnvResult +impl FromEnv for SnapshotPartial { + fn from_env>(env: &R) -> FromEnvResult where Self: Sized, { @@ -753,7 +1157,7 @@ impl FromEnv for Snapshot { #[derive(Deserialize, Serialize, Debug, Default, Merge)] #[serde(deny_unknown_fields, default)] -pub struct ChainWide { +pub struct ChainWidePartial { pub max_transactions_in_block: UserField, pub block_time: UserField, pub commit_time: UserField, @@ -767,11 +1171,26 @@ pub struct ChainWide { pub wasm_max_memory: UserField>, } -impl Complete for ChainWide { - type Output = actual::ChainWide; +#[derive(Debug)] +pub struct ChainWideFull { + pub max_transactions_in_block: NonZeroU32, + pub block_time: Duration, + pub commit_time: Duration, + pub transaction_limits: TransactionLimits, + pub asset_metadata_limits: MetadataLimits, + pub asset_definition_metadata_limits: MetadataLimits, + pub account_metadata_limits: MetadataLimits, + pub domain_metadata_limits: MetadataLimits, + pub identifier_length_limits: LengthLimits, + pub wasm_fuel_limit: u64, + pub wasm_max_memory: ByteSize, +} + +impl UnwrapPartial for ChainWidePartial { + type Output = ChainWideFull; - fn complete(self) -> CompleteResult { - Ok(actual::ChainWide { + fn unwrap_partial(self) -> UnwrapPartialResult { + Ok(ChainWideFull { max_transactions_in_block: self.max_transactions_in_block.unwrap_or(DEFAULT_MAX_TXS), block_time: self .block_time @@ -797,53 +1216,112 @@ impl Complete for ChainWide { identifier_length_limits: self .identifier_length_limits .unwrap_or(DEFAULT_IDENT_LENGTH_LIMITS), - wasm_runtime: actual::WasmRuntime { - fuel_limit: self.wasm_fuel_limit.unwrap_or(DEFAULT_WASM_FUEL_LIMIT), - max_memory: self - .wasm_max_memory - .unwrap_or(ByteSize(DEFAULT_WASM_MAX_MEMORY)), - }, + wasm_fuel_limit: self.wasm_fuel_limit.unwrap_or(DEFAULT_WASM_FUEL_LIMIT), + wasm_max_memory: self + .wasm_max_memory + .unwrap_or(ByteSize(DEFAULT_WASM_MAX_MEMORY)), }) } } -impl FromEnvDefaultFallback for ChainWide {} +impl FromEnvDefaultFallback for ChainWidePartial {} + +impl ChainWideFull { + fn parse(self) -> actual::ChainWide { + let Self { + max_transactions_in_block, + block_time, + commit_time, + transaction_limits, + asset_metadata_limits, + asset_definition_metadata_limits, + account_metadata_limits, + domain_metadata_limits, + identifier_length_limits, + wasm_fuel_limit, + wasm_max_memory, + } = self; + + actual::ChainWide { + max_transactions_in_block, + block_time, + commit_time, + transaction_limits, + asset_metadata_limits, + asset_definition_metadata_limits, + account_metadata_limits, + domain_metadata_limits, + identifier_length_limits, + wasm_runtime: actual::WasmRuntime { + fuel_limit: wasm_fuel_limit, + max_memory: wasm_max_memory, + }, + } + } +} #[derive(Debug, Clone, Deserialize, Serialize, Default, Merge)] #[serde(deny_unknown_fields, default)] -pub struct Torii { +pub struct ToriiPartial { pub address: UserField, pub max_content_len: UserField>, pub query_idle_time: UserField, } -impl Complete for Torii { - type Output = (actual::Torii, actual::LiveQueryStore); +#[derive(Debug)] +pub struct ToriiFull { + pub address: SocketAddr, + pub max_content_len: ByteSize, + pub query_idle_time: Duration, +} + +impl UnwrapPartial for ToriiPartial { + type Output = ToriiFull; + + fn unwrap_partial(self) -> UnwrapPartialResult { + let mut emitter = Emitter::new(); + + if self.address.is_none() { + emitter.emit_missing_field("torii.address"); + } - fn complete(self) -> CompleteResult { + let max_content_len = self + .max_content_len + .get() + .unwrap_or(ByteSize(DEFAULT_MAX_CONTENT_LENGTH)); + + let query_idle_time = self + .query_idle_time + .map(UserDuration::get) + .unwrap_or(DEFAULT_QUERY_IDLE_TIME); + + emitter.finish()?; + + Ok(ToriiFull { + address: self.address.get().unwrap(), + max_content_len, + query_idle_time, + }) + } +} + +impl ToriiFull { + fn parse(self) -> (actual::Torii, actual::LiveQueryStore) { let torii = actual::Torii { - address: self - .address - .get() - .ok_or_else(|| CompleteError::missing_field("torii.address"))?, - max_content_len: self - .max_content_len - .get() - .unwrap_or(ByteSize(DEFAULT_MAX_CONTENT_LENGTH)), + address: self.address, + max_content_len: self.max_content_len, }; let query = actual::LiveQueryStore { - query_idle_time: self - .query_idle_time - .map_or(DEFAULT_QUERY_IDLE_TIME, UserDuration::get), + query_idle_time: self.query_idle_time, }; - Ok((torii, query)) + (torii, query) } } -impl FromEnv for Torii { - fn from_env(env: &impl ReadEnv) -> FromEnvResult +impl FromEnv for ToriiPartial { + fn from_env>(env: &R) -> FromEnvResult where Self: Sized, { @@ -865,7 +1343,7 @@ impl FromEnv for Torii { mod tests { use iroha_config_base::{FromEnv, TestEnv}; - use crate::parameters::user_layer::{Iroha, Root}; + use crate::parameters::user_layer::{IrohaPartial, RootPartial}; #[test] fn parses_private_key_from_env() { @@ -873,7 +1351,7 @@ mod tests { .set("PRIVATE_KEY_DIGEST", "ed25519") .set("PRIVATE_KEY_PAYLOAD", "8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb"); - let private_key = Iroha::from_env(&env) + let private_key = IrohaPartial::from_env(&env) .expect("input is valid, should not fail") .private_key .get() @@ -886,25 +1364,23 @@ mod tests { #[test] fn fails_to_parse_private_key_in_env_without_digest() { let env = TestEnv::new().set("PRIVATE_KEY_DIGEST", "ed25519"); - let error = Iroha::from_env(&env).expect_err("private key is incomplete, should fail"); - let expected = expect_test::expect![[r#" - `PRIVATE_KEY_DIGEST` env was provided, but `PRIVATE_KEY_PAYLOAD` was not - - Location: - config/src/parameters/iroha.rs:100:26"#]]; - expected.assert_eq(&format!("{error:?}")); + let error = + IrohaPartial::from_env(&env).expect_err("private key is incomplete, should fail"); + let expected = expect_test::expect![ + "`PRIVATE_KEY_DIGEST` env was provided, but `PRIVATE_KEY_PAYLOAD` was not" + ]; + expected.assert_eq(&format!("{error:#}")); } #[test] fn fails_to_parse_private_key_in_env_without_payload() { let env = TestEnv::new().set("PRIVATE_KEY_PAYLOAD", "8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb"); - let error = Iroha::from_env(&env).expect_err("private key is incomplete, should fail"); - let expected = expect_test::expect![[r#" - `PRIVATE_KEY_PAYLOAD` env was provided, but `PRIVATE_KEY_DIGEST` was not - - Location: - config/src/parameters/iroha.rs:108:26"#]]; - expected.assert_eq(&format!("{error:?}")); + let error = + IrohaPartial::from_env(&env).expect_err("private key is incomplete, should fail"); + let expected = expect_test::expect![ + "`PRIVATE_KEY_PAYLOAD` env was provided, but `PRIVATE_KEY_DIGEST` was not" + ]; + expected.assert_eq(&format!("{error:#}")); } #[test] @@ -913,17 +1389,10 @@ mod tests { .set("PRIVATE_KEY_DIGEST", "ed25519") .set("PRIVATE_KEY_PAYLOAD", "foo"); - let error = Iroha::from_env(&env).expect_err("input is invalid, should fail"); + let error = IrohaPartial::from_env(&env).expect_err("input is invalid, should fail"); - let expected = expect_test::expect![[r#" - failed to construct `iroha.private_key` from `PRIVATE_KEY_DIGEST` and `PRIVATE_KEY_PAYLOAD` environment variables - - Caused by: - Key could not be parsed. Odd number of digits - - Location: - config/src/parameters/iroha.rs:82:18"#]]; - expected.assert_eq(&format!("{error:?}")); + let expected = expect_test::expect!["failed to construct `iroha.private_key` from `PRIVATE_KEY_DIGEST` and `PRIVATE_KEY_PAYLOAD` environment variables"]; + expected.assert_eq(&format!("{error:#}")); } #[test] @@ -932,28 +1401,21 @@ mod tests { .set("PRIVATE_KEY_DIGEST", "foo") .set("PRIVATE_KEY_PAYLOAD", "8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb"); - let error = Iroha::from_env(&env).expect_err("input is invalid, should fail"); + let error = IrohaPartial::from_env(&env).expect_err("input is invalid, should fail"); // TODO: print the bad value and supported ones - let expected = expect_test::expect![[r#" - failed to parse `iroha.private_key.digest_function` field from `PRIVATE_KEY_DIGEST` env variable - - Caused by: - Algorithm not supported - - Location: - config/src/lib.rs:237:14"#]]; - expected.assert_eq(&format!("{error:?}")); + let expected = expect_test::expect!["failed to parse `iroha.private_key.digest_function` field from `PRIVATE_KEY_DIGEST` env variable"]; + expected.assert_eq(&format!("{error:#}")); } #[test] fn deserialize_empty_input_works() { - let _layer: Root = toml::from_str("").unwrap(); + let _layer: RootPartial = toml::from_str("").unwrap(); } #[test] fn deserialize_iroha_namespace_with_not_all_fields_works() { - let _layer: Root = toml::from_str( + let _layer: RootPartial = toml::from_str( r#" [iroha] p2p_address = "127.0.0.1:8080" diff --git a/config/tests/fixtures.rs b/config/tests/fixtures.rs index ca400af1f5a..c32b1ba6ee7 100644 --- a/config/tests/fixtures.rs +++ b/config/tests/fixtures.rs @@ -5,8 +5,8 @@ use std::{ }; use eyre::Result; -use iroha_config::parameters::user_layer::Root; -use iroha_config_base::{Complete as _, FromEnv, TestEnv}; +use iroha_config::parameters::user_layer::{CliContext, RootPartial}; +use iroha_config_base::{FromEnv, TestEnv, UnwrapPartial as _}; fn fixtures_dir() -> PathBuf { // CWD is the crate's root @@ -39,11 +39,15 @@ fn test_env_from_file(p: impl AsRef) -> TestEnv { /// it also gives an insight into every single default value #[test] fn minimal_config_snapshot() -> Result<()> { - let config = Root::from_toml(fixtures_dir().join("minimal_config.toml"))?.complete()?; + let config = RootPartial::from_toml(fixtures_dir().join("minimal_config.toml"))? + .unwrap_partial()? + .parse(CliContext { + submit_genesis: false, + })?; let expected = expect_test::expect![[r#" - Config { - iroha: Config { + Root { + iroha: Iroha { key_pair: KeyPair { public_key: {digest: ed25519, payload: ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB}, private_key: {digest: ed25519, payload: 8F4C15E5D664DA3F13778801D23D4E89B76E94C1B94B389544168B6CB894F84F8BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB}, @@ -53,53 +57,56 @@ fn minimal_config_snapshot() -> Result<()> { genesis: Partial { public_key: {digest: ed25519, payload: ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB}, }, - kura: Config { + torii: Torii { + address: 127.0.0.1:8080, + max_content_len: ByteSize( + 16777216, + ), + }, + kura: Kura { init_mode: Strict, block_store_path: "./storage", debug_output_new_blocks: false, }, - sumeragi: Config { - block_gossip_period: 10s, - max_blocks_per_gossip: 4, - max_transactions_per_gossip: 500, - transaction_gossip_period: 1s, - trusted_peers: TrustedPeers { - peers: UniqueVec( - [], - ), - }, + sumeragi: Sumeragi { + trusted_peers: UniqueVec( + [], + ), + debug_force_soft_fork: false, }, - logger: Config { + block_sync: BlockSync { + gossip_period: 10s, + batch_size: 4, + }, + transaction_gossiper: TransactionGossiper { + gossip_period: 1s, + batch_size: 500, + }, + live_query_store: LiveQueryStore { + query_idle_time: 30s, + }, + logger: LoggerFull { level: INFO, format: Full, }, - queue: Config { + queue: QueueFull { max_transactions_in_queue: 65536, max_transactions_in_queue_per_user: 65536, - transaction_time_to_live_ms: 86400s, - future_threshold_ms: 1s, + transaction_time_to_live: 86400s, + future_threshold: 1s, }, - snapshot: Config { - create_every_ms: 60s, + snapshot: SnapshotFull { + create_every: 60s, store_path: "./storage", creation_enabled: true, }, - telemetry: Config { - regular: None, - dev: None, - }, - torii: Config { - address: 127.0.0.1:8080, - max_content_len: ByteSize( - 16777216, - ), - query_idle_time: 30s, - }, - chain_wide: Config { + regular_telemetry: None, + dev_telemetry: None, + chain_wide: ChainWide { max_transactions_in_block: 512, block_time: 2s, commit_time: 4s, - transactions_limits: TransactionLimits { + transaction_limits: TransactionLimits { max_instruction_number: 4096, max_wasm_size_bytes: 4194304, }, @@ -123,10 +130,12 @@ fn minimal_config_snapshot() -> Result<()> { min: 1, max: 128, }, - wasm_fuel_limit: 23000000, - wasm_max_memory: ByteSize( - 524288000, - ), + wasm_runtime: WasmRuntime { + fuel_limit: 23000000, + max_memory: ByteSize( + 524288000, + ), + }, }, }"#]]; expected.assert_eq(&format!("{config:#?}")); @@ -136,22 +145,26 @@ fn minimal_config_snapshot() -> Result<()> { #[test] fn config_with_genesis() -> Result<()> { - let _config = Root::from_toml(fixtures_dir().join("with_genesis.toml"))?.complete()?; + let _config = RootPartial::from_toml(fixtures_dir().join("with_genesis.toml"))? + .unwrap_partial()? + .parse(CliContext { + submit_genesis: false, + })?; Ok(()) } #[test] fn missing_fields() -> Result<()> { - let error = Root::from_toml(fixtures_dir().join("missing_fields.toml"))? - .complete() + let error = RootPartial::from_toml(fixtures_dir().join("missing_fields.toml"))? + .unwrap_partial() .expect_err("should fail with missing fields"); let expected = expect_test::expect![[r#" - Missing field: iroha.public_key - Missing field: iroha.private_key - Missing field: iroha.p2p_address - Missing field: genesis.public_key - Missing field: torii.address"#]]; + missing field: `iroha.public_key` + missing field: `iroha.private_key` + missing field: `iroha.p2p_address` + missing field: `genesis.public_key` + missing field: `torii.address`"#]]; expected.assert_eq(&format!("{error:#}")); Ok(()) @@ -159,7 +172,7 @@ fn missing_fields() -> Result<()> { #[test] fn extra_fields() { - let error = Root::from_toml(fixtures_dir().join("extra_fields.toml")) + let error = RootPartial::from_toml(fixtures_dir().join("extra_fields.toml")) .expect_err("should fail with extra fields"); let expected = expect_test::expect![[r#" @@ -167,18 +180,22 @@ fn extra_fields() { | 1 | i_am_unknown = true | ^^^^^^^^^^^^ - unknown field `i_am_unknown`, expected one of `iroha`, `genesis`, `kura`, `sumeragi`, `logger`, `queue`, `snapshot`, `telemetry`, `torii`, `chain_wide` + unknown field `i_am_unknown`, expected one of `iroha`, `genesis`, `kura`, `sumeragi`, `network`, `logger`, `queue`, `snapshot`, `telemetry`, `torii`, `chain_wide` "#]]; expected.assert_eq(&format!("{error:#}")); } #[test] fn inconsistent_genesis_config() -> Result<()> { - let error = Root::from_toml(fixtures_dir().join("inconsistent_genesis.toml"))? - .complete() + let error = RootPartial::from_toml(fixtures_dir().join("inconsistent_genesis.toml"))? + .unwrap_partial() + .expect("all fields are present") + .parse(CliContext { + submit_genesis: false, + }) .expect_err("should fail with bad genesis config"); - let expected = expect_test::expect!["FIXME"]; + let expected = expect_test::expect!["`genesis.file` and `genesis.private_key` should be set together"]; expected.assert_eq(&format!("{error:#}")); Ok(()) @@ -190,13 +207,13 @@ fn inconsistent_genesis_config() -> Result<()> { fn full_envs_set_is_consumed() -> Result<()> { let env = test_env_from_file(fixtures_dir().join("full.env")); - let layer = Root::from_env(&env)?; + let layer = RootPartial::from_env(&env)?; assert_eq!(env.unvisited(), HashSet::new()); let expected = expect_test::expect![[r#" - UserLayer { - iroha: UserLayer { + RootPartial { + iroha: IrohaPartial { public_key: Some( {digest: ed25519, payload: ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB}, ), @@ -207,7 +224,7 @@ fn full_envs_set_is_consumed() -> Result<()> { 127.0.0.1:5432, ), }, - genesis: UserLayer { + genesis: GenesisPartial { public_key: Some( {digest: ed25519, payload: ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB}, ), @@ -216,29 +233,34 @@ fn full_envs_set_is_consumed() -> Result<()> { ), file: None, }, - kura: UserLayer { + kura: KuraPartial { init_mode: Some( Strict, ), block_store_path: Some( "/store/path/from/env", ), - debug: DebugUserConfig { + debug: KuraDebugPartial { output_new_blocks: Some( false, ), }, }, - sumeragi: UserLayer { + sumeragi: SumeragiPartial { + trusted_peers: UserTrustedPeers { + peers: [], + }, + debug: SumeragiDebugPartial { + force_soft_fork: None, + }, + }, + network: NetworkPartial { block_gossip_period: None, max_blocks_per_gossip: None, max_transactions_per_gossip: None, transaction_gossip_period: None, - trusted_peers: UserTrustedPeers { - peers: [], - }, }, - logger: UserLayer { + logger: LoggerPartial { level: Some( DEBUG, ), @@ -246,14 +268,14 @@ fn full_envs_set_is_consumed() -> Result<()> { Pretty, ), }, - queue: UserLayer { + queue: QueuePartial { max_transactions_in_queue: None, max_transactions_in_queue_per_user: None, - transaction_time_to_live_ms: None, - future_threshold_ms: None, + transaction_time_to_live: None, + future_threshold: None, }, - snapshot: UserLayer { - create_every_ms: None, + snapshot: SnapshotPartial { + create_every: None, store_path: Some( "/snapshot/path/from/env", ), @@ -261,27 +283,27 @@ fn full_envs_set_is_consumed() -> Result<()> { false, ), }, - telemetry: UserLayer { + telemetry: TelemetryPartial { name: None, url: None, min_retry_period: None, max_retry_delay_exponent: None, - dev: DevUserLayer { + dev: TelemetryDevPartial { file: None, }, }, - torii: UserLayer { + torii: ToriiPartial { address: Some( 127.0.0.1:8080, ), max_content_len: None, query_idle_time: None, }, - chain_wide: UserLayer { + chain_wide: ChainWidePartial { max_transactions_in_block: None, block_time: None, commit_time: None, - transactions_limits: None, + transaction_limits: None, asset_metadata_limits: None, asset_definition_metadata_limits: None, account_metadata_limits: None, @@ -306,9 +328,12 @@ fn multiple_env_parsing_errors() { fn config_from_file_and_env() -> Result<()> { let env = test_env_from_file(fixtures_dir().join("config_and_env.env")); - let _config = Root::from_toml(fixtures_dir().join("config_and_env.toml"))? - .merge_chain(Root::from_env(&env)?) - .complete()?; + let _config = RootPartial::from_toml(fixtures_dir().join("config_and_env.toml"))? + .merge_chain(RootPartial::from_env(&env)?) + .unwrap_partial()? + .parse(CliContext { + submit_genesis: false, + })?; Ok(()) } diff --git a/core/src/gossiper.rs b/core/src/gossiper.rs index 8eefccf25a6..33c3ffb2ec0 100644 --- a/core/src/gossiper.rs +++ b/core/src/gossiper.rs @@ -60,8 +60,7 @@ impl TransactionGossiper { /// Construct [`Self`] from configuration pub fn from_configuration( chain_id: ChainId, - // Currently we are using configuration parameters from sumeragi not to break configuration - config: &Config, + config: Config, network: IrohaNetwork, queue: Arc, sumeragi: SumeragiHandle, diff --git a/core/src/sumeragi/mod.rs b/core/src/sumeragi/mod.rs index 31d07d06f1b..321b36a7fc4 100644 --- a/core/src/sumeragi/mod.rs +++ b/core/src/sumeragi/mod.rs @@ -322,10 +322,7 @@ impl SumeragiHandle { chain_id, key_pair: iroha_config.key_pair.clone(), queue: Arc::clone(&queue), - peer_id: PeerId::new( - &iroha_config.p2p_address, - &iroha_config.key_pair.public_key(), - ), + peer_id: iroha_config.peer_id(), events_sender, public_wsv_sender, public_finalized_wsv_sender, diff --git a/data_model/src/lib.rs b/data_model/src/lib.rs index b5692ff242b..a5360468b19 100644 --- a/data_model/src/lib.rs +++ b/data_model/src/lib.rs @@ -1014,7 +1014,7 @@ pub mod model { impl ChainId { /// Create new [`Self`] - pub fn new(inner: &str) -> Self { + pub fn new(inner: String) -> Self { Self(inner.into()) } } @@ -1025,7 +1025,7 @@ impl Decode for ChainId { input: &mut I, ) -> Result { let boxed: String = parity_scale_codec::Decode::decode(input)?; - Ok(Self::new(&boxed)) + Ok(Self::new(boxed)) } } diff --git a/logger/src/lib.rs b/logger/src/lib.rs index b1da6a88d9c..d3acea7c531 100644 --- a/logger/src/lib.rs +++ b/logger/src/lib.rs @@ -13,8 +13,11 @@ use std::{ use actor::LoggerHandle; use color_eyre::{eyre::eyre, Report, Result}; -use iroha_config::logger::{into_tracing_level, Format}; -pub use iroha_config::{base::Complete as _, logger::Level, parameters::actual::Logger as Config}; +use iroha_config::logger::into_tracing_level; +pub use iroha_config::{ + logger::{Format, Level}, + parameters::actual::Logger as Config, +}; use tracing::subscriber::set_global_default; pub use tracing::{ debug, debug_span, error, error_span, info, info_span, instrument as log, trace, trace_span, diff --git a/telemetry/src/dev.rs b/telemetry/src/dev.rs index 9a6bb15c1fc..0b31bbd5398 100644 --- a/telemetry/src/dev.rs +++ b/telemetry/src/dev.rs @@ -1,5 +1,7 @@ //! Module with development telemetry +use std::path::Path; + use eyre::{Result, WrapErr}; use iroha_config::parameters::actual::DevTelemetry as DevTelemetryConfig; use iroha_logger::telemetry::Event as Telemetry; @@ -15,9 +17,7 @@ use tokio_stream::{wrappers::BroadcastStream, StreamExt}; /// # Errors /// Fails if unable to open the file pub async fn start( - DevTelemetryConfig { - file: telemetry_file, - }: DevTelemetryConfig, + telemetry_file: impl AsRef, telemetry: Receiver, ) -> Result> { let mut stream = crate::futures::get_stream(BroadcastStream::new(telemetry).fuse()); From a08af33a8f9db7cddd64c9fd22c3135b622d0604 Mon Sep 17 00:00:00 2001 From: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> Date: Tue, 23 Jan 2024 07:51:36 +0900 Subject: [PATCH 14/94] [feat]: compile `iroha_client`! Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- Cargo.lock | 1 + cli/src/lib.rs | 7 +- cli/src/samples.rs | 2 +- client/Cargo.toml | 1 + client/benches/tps/utils.rs | 2 +- client/src/client.rs | 90 +++---- client/src/config.rs | 230 ++++++++++++++++-- client/tests/integration/add_account.rs | 2 +- client/tests/integration/add_domain.rs | 2 +- client/tests/integration/asset.rs | 4 +- client/tests/integration/asset_propagation.rs | 2 +- client/tests/integration/burn_public_keys.rs | 2 +- client/tests/integration/connected_peers.rs | 4 +- client/tests/integration/domain_owner.rs | 4 +- client/tests/integration/events/pipeline.rs | 2 +- .../integration/multiple_blocks_created.rs | 2 +- .../integration/multisignature_account.rs | 2 +- .../integration/multisignature_transaction.rs | 21 +- client/tests/integration/offline_peers.rs | 2 +- client/tests/integration/permissions.rs | 8 +- client/tests/integration/restart_peer.rs | 2 +- client/tests/integration/roles.rs | 2 +- .../integration/triggers/time_trigger.rs | 19 +- client/tests/integration/tx_history.rs | 2 +- client/tests/integration/unregister_peer.rs | 2 +- client/tests/integration/unstable_network.rs | 5 +- client/tests/integration/upgrade.rs | 2 +- config/base/src/lib.rs | 13 + config/src/parameters/actual.rs | 6 + config/src/parameters/user_layer.rs | 6 +- core/benches/blocks/common.rs | 2 +- core/benches/kura.rs | 2 +- core/benches/validation.rs | 8 +- core/src/block.rs | 6 +- core/src/queue.rs | 8 +- core/src/smartcontracts/isi/query.rs | 4 +- core/src/sumeragi/main_loop.rs | 14 +- core/test_network/src/lib.rs | 137 +++++------ data_model/src/lib.rs | 18 +- genesis/src/lib.rs | 2 +- telemetry/src/dev.rs | 1 - tools/swarm/src/compose.rs | 8 +- 42 files changed, 413 insertions(+), 246 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ec1710a0552..92cac09c3c1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2710,6 +2710,7 @@ dependencies = [ "iroha_torii_const", "iroha_version", "iroha_wasm_builder", + "merge", "once_cell", "parity-scale-codec", "rand", diff --git a/cli/src/lib.rs b/cli/src/lib.rs index 76f2221752f..e1d3703367b 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -514,8 +514,9 @@ pub fn read_config_and_genesis( parameters::{actual::Genesis, user_layer::RootPartial as RootLayer}, }; - let config = RootLayer::from_toml(path)? - .merge_chain(RootLayer::from_env(&StdEnv)?) + let config = RootLayer::from_toml(path)?; + let config = config.merge(RootLayer::from_env(&StdEnv)?); + let config = config .unwrap_partial()? .parse(CliContext { submit_genesis })?; @@ -579,7 +580,7 @@ mod tests { let mut base = PartialUserConfig::default(); - base.iroha.chain_id.set(ChainId::new("0".to_owned())); + base.iroha.chain_id.set(ChainId::from("0")); base.iroha.public_key.set(pubkey.clone()); base.iroha.private_key.set(privkey.clone()); base.iroha.p2p_address.set(socket_addr!(127.0.0.1:1337)); diff --git a/cli/src/samples.rs b/cli/src/samples.rs index bd610a44912..3cba47c0da3 100644 --- a/cli/src/samples.rs +++ b/cli/src/samples.rs @@ -63,7 +63,7 @@ pub fn get_user_config( chain_id: Option, key_pair: Option, ) -> UserConfig { - let chain_id = chain_id.unwrap_or_else(|| ChainId::new("0".to_owned())); + let chain_id = chain_id.unwrap_or_else(|| ChainId::from("0")); let (public_key, private_key) = key_pair.unwrap_or_else(KeyPair::generate).into(); iroha_logger::info!(%public_key); diff --git a/client/Cargo.toml b/client/Cargo.toml index 5af00c30d12..1063175850b 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -73,6 +73,7 @@ tokio = { workspace = true, features = ["rt"] } tokio-tungstenite = { workspace = true } tungstenite = { workspace = true } futures-util = "0.3.28" +merge = "0.1.0" [dev-dependencies] iroha_wasm_builder = { workspace = true } diff --git a/client/benches/tps/utils.rs b/client/benches/tps/utils.rs index 3901d11b266..f078481269b 100644 --- a/client/benches/tps/utils.rs +++ b/client/benches/tps/utils.rs @@ -196,7 +196,7 @@ impl MeasurerUnit { /// Spawn who periodically submits transactions fn spawn_transaction_submitter(&self, shutdown_signal: mpsc::Receiver<()>) -> JoinHandle<()> { - let chain_id = ChainId::new("0"); + let chain_id = ChainId::from("0"); let submitter = self.client.clone(); let interval_us_per_tx = self.config.interval_us_per_tx; diff --git a/client/src/client.rs b/client/src/client.rs index ae2c85fc0c6..421a18674ef 100644 --- a/client/src/client.rs +++ b/client/src/client.rs @@ -402,20 +402,14 @@ impl QueryRequest { /// Representation of `Iroha` client. impl Client { /// Constructor for client from configuration - /// - /// # Errors - /// If configuration isn't valid (e.g public/private keys don't match) #[inline] - pub fn new(configuration: Config) -> Result { + pub fn new(configuration: Config) -> Self { Self::with_headers(configuration, HashMap::new()) } /// Constructor for client from configuration and headers /// - /// *Authorization* header will be added, if `login` and `password` fields are presented - /// - /// # Errors - /// If configuration isn't valid (e.g public/private keys don't match) + /// *Authorization* header will be added if `basic_auth` is presented #[inline] pub fn with_headers( Config { @@ -429,7 +423,7 @@ impl Client { transaction_status_timeout, }: Config, mut headers: HashMap, - ) -> Result { + ) -> Self { if let Some(basic_auth) = basic_auth { let credentials = format!("{}:{}", basic_auth.web_login, basic_auth.password); let engine = base64::engine::general_purpose::STANDARD; @@ -437,7 +431,7 @@ impl Client { headers.insert(String::from("Authorization"), format!("Basic {encoded}")); } - Ok(Self { + Self { chain_id, torii_url: torii_api_url, key_pair, @@ -446,7 +440,7 @@ impl Client { account_id, headers, add_transaction_nonce: transaction_add_nonce, - }) + } } /// Builds transaction out of supplied instructions or wasm. @@ -1595,33 +1589,34 @@ mod tests { use iroha_primitives::small::SmallStr; use super::*; - use crate::config::{torii::DEFAULT_API_ADDR, BasicAuth, ConfigurationProxy, WebLogin}; + use crate::config::{BasicAuth, Config, WebLogin}; const LOGIN: &str = "mad_hatter"; const PASSWORD: &str = "ilovetea"; // `mad_hatter:ilovetea` encoded with base64 const ENCRYPTED_CREDENTIALS: &str = "bWFkX2hhdHRlcjppbG92ZXRlYQ=="; + fn config_factory() -> Config { + Config { + chain_id: ChainId::from("0"), + key_pair: KeyPair::generate(), + account_id: "alice@wonderland" + .parse() + .expect("This account ID should be valid"), + torii_api_url: "http://127.0.0.1:8080".parse().unwrap(), + basic_auth: None, + transaction_add_nonce: false, + transaction_ttl: Duration::from_secs(5), + transaction_status_timeout: Duration::from_secs(10), + } + } + #[test] fn txs_same_except_for_nonce_have_different_hashes() { - let (public_key, private_key) = KeyPair::generate().into(); - - let cfg = ConfigurationProxy { - chain_id: Some(ChainId::new("0")), - public_key: Some(public_key), - private_key: Some(private_key), - account_id: Some( - "alice@wonderland" - .parse() - .expect("This account ID should be valid"), - ), - torii_api_url: Some(format!("http://{DEFAULT_API_ADDR}").parse().unwrap()), - add_transaction_nonce: Some(true), - ..ConfigurationProxy::default() - } - .build() - .expect("Client config should build as all required fields were provided"); - let client = Client::new(&cfg).expect("Invalid client configuration"); + let client = Client::new(Config { + transaction_add_nonce: true, + ..config_factory() + }); let build_transaction = || client.build_transaction(Vec::::new(), UnlimitedMetadata::new()); @@ -1650,34 +1645,13 @@ mod tests { #[test] fn authorization_header() { - let basic_auth = BasicAuth { - web_login: WebLogin::from_str(LOGIN).expect("Failed to create valid `WebLogin`"), - password: SmallStr::from_str(PASSWORD), - }; - - let cfg = ConfigurationProxy { - chain_id: Some(ChainId::new("0")), - public_key: Some( - "ed01207233BFC89DCBD68C19FDE6CE6158225298EC1131B6A130D1AEB454C1AB5183C0" - .parse() - .expect("Public key not in mulithash format"), - ), - private_key: Some(crate::crypto::PrivateKey::from_hex( - crate::crypto::Algorithm::Ed25519, - "9AC47ABF59B356E0BD7DCBBBB4DEC080E302156A48CA907E47CB6AEA1D32719E7233BFC89DCBD68C19FDE6CE6158225298EC1131B6A130D1AEB454C1AB5183C0" - ).expect("Private key not hex encoded")), - account_id: Some( - "alice@wonderland" - .parse() - .expect("This account ID should be valid"), - ), - torii_api_url: Some(format!("http://{DEFAULT_API_ADDR}").parse().unwrap()), - basic_auth: Some(Some(basic_auth)), - ..ConfigurationProxy::default() - } - .build() - .expect("Client config should build as all required fields were provided"); - let client = Client::new(&cfg).expect("Invalid client configuration"); + let client = Client::new(Config { + basic_auth: Some(BasicAuth { + web_login: WebLogin::from_str(LOGIN).expect("Failed to create valid `WebLogin`"), + password: SmallStr::from_str(PASSWORD), + }), + ..config_factory() + }); let value = client .headers diff --git a/client/src/config.rs b/client/src/config.rs index 56ddb317fe5..9bfe44b14e8 100644 --- a/client/src/config.rs +++ b/client/src/config.rs @@ -65,65 +65,239 @@ pub struct BasicAuth { } pub mod user_layer { - use iroha_config::base::{Complete, CompleteResult, Merge, UserDuration, UserField}; - use iroha_crypto::{PrivateKey, PublicKey}; + use std::time::Duration; + + use eyre::{eyre, Context, Report}; + use iroha_config::base::{ + Emitter, ErrorsCollection, Merge, MissingFieldError, UnwrapPartial, UnwrapPartialResult, + UserDuration, UserField, + }; + use iroha_crypto::{KeyPair, PrivateKey, PublicKey}; use iroha_data_model::{account::AccountId, ChainId}; use serde::{Deserialize, Deserializer}; use url::Url; use crate::config::BasicAuth; - #[derive(Debug, Clone, Deserialize, Eq, PartialEq, Default)] + #[derive(Debug, Clone, Deserialize, Eq, PartialEq, Default, Merge)] #[serde(deny_unknown_fields, default)] - pub struct Root { + pub struct RootPartial { pub chain_id: UserField, - pub account: Account, - pub api: Api, - pub transaction: Transaction, + pub account: AccountPartial, + pub api: ApiPartial, + pub transaction: TransactionPartial, } - impl Complete for Root { - type Output = super::Config; + impl RootPartial { + pub fn new() -> Self { + // TODO: gen with macro + Default::default() + } - fn complete(self) -> CompleteResult { - // TODO - // # Errors - // - If the [`self.transaction_time_to_live_ms`] field is too small - // - If the [`self.transaction_status_timeout_ms`] field is smaller than [`self.transaction_time_to_live_ms`] - // - If the [`self.torii_api_url`] is malformed or had the wrong protocol - todo!() + pub fn merge_chain(mut self, other: Self) -> Self { + self.merge(other); + self } } - impl Merge for Root { - fn merge(&mut self, other: Self) { - todo!() + #[derive(Clone, Debug)] + pub struct RootFull { + pub chain_id: ChainId, + pub account: AccountFull, + pub api: ApiFull, + pub transaction: TransactionFull, + } + + impl UnwrapPartial for RootPartial { + type Output = RootFull; + + fn unwrap_partial(self) -> UnwrapPartialResult { + let mut emitter = Emitter::new(); + + if self.chain_id.is_none() { + emitter.emit_missing_field("chain_id"); + } + let account = emitter.try_unwrap_partial(self.account); + let api = emitter.try_unwrap_partial(self.api); + let transaction = emitter.try_unwrap_partial(self.transaction); + + emitter.finish()?; + + Ok(RootFull { + chain_id: self.chain_id.get().unwrap(), + account: account.unwrap(), + api: api.unwrap(), + transaction: transaction.unwrap(), + }) } } - #[derive(Debug, Clone, Deserialize, Eq, PartialEq, Default)] + impl RootFull { + pub fn parse(self) -> Result> { + let Self { + chain_id, + account: + AccountFull { + id: account_id, + public_key, + private_key, + }, + transaction: + TransactionFull { + time_to_live: tx_ttl, + status_timeout: tx_timeout, + add_nonce: tx_add_nonce, + }, + api: + ApiFull { + torii_url, + basic_auth, + }, + } = self; + + let mut emitter = Emitter::new(); + + // TODO: validate if TTL is too small? + + if tx_timeout < tx_ttl { + // TODO: + // would be nice to provide a nice report with spans in the input + // pointing out source data in provided config files + // FIXME: explain why it should be smaller + emitter.emit(eyre!( + "transaction status timeout should be smaller than its time-to-live" + )) + } + + let key_pair = KeyPair::new(public_key, private_key) + .wrap_err("failed to construct a key pair") + .map_or_else( + |err| { + emitter.emit(err); + None + }, + Some, + ); + + emitter.finish()?; + + Ok(super::Config { + chain_id, + account_id, + key_pair: key_pair.unwrap(), + torii_api_url: torii_url.0, + basic_auth, + transaction_ttl: tx_ttl, + transaction_status_timeout: tx_timeout, + transaction_add_nonce: tx_add_nonce, + }) + } + } + + #[derive(Debug, Clone, Deserialize, Eq, PartialEq, Default, Merge)] #[serde(deny_unknown_fields, default)] - pub struct Api { + pub struct ApiPartial { pub torii_url: UserField, pub basic_auth: UserField, } - #[derive(Debug, Clone, Deserialize, Eq, PartialEq, Default)] + #[derive(Debug, Clone)] + pub struct ApiFull { + pub torii_url: OnlyHttpUrl, + pub basic_auth: Option, + } + + impl UnwrapPartial for ApiPartial { + type Output = ApiFull; + + fn unwrap_partial(self) -> UnwrapPartialResult { + Ok(ApiFull { + torii_url: self + .torii_url + .get() + .ok_or_else(|| MissingFieldError::new("api.torii_url"))?, + basic_auth: self.basic_auth.get(), + }) + } + } + + #[derive(Debug, Clone, Deserialize, Eq, PartialEq, Default, Merge)] #[serde(deny_unknown_fields, default)] - pub struct Account { + pub struct AccountPartial { pub id: UserField, pub public_key: UserField, pub private_key: UserField, } - #[derive(Debug, Clone, Deserialize, Eq, PartialEq, Default)] + #[derive(Debug, Clone)] + pub struct AccountFull { + pub id: AccountId, + pub public_key: PublicKey, + pub private_key: PrivateKey, + } + + impl UnwrapPartial for AccountPartial { + type Output = AccountFull; + + fn unwrap_partial(self) -> UnwrapPartialResult { + let mut emitter = Emitter::new(); + + if self.id.is_none() { + emitter.emit_missing_field("account.id"); + } + if self.public_key.is_none() { + emitter.emit_missing_field("account.public_key"); + } + if self.private_key.is_none() { + emitter.emit_missing_field("account.private_key"); + } + + emitter.finish()?; + + Ok(AccountFull { + id: self.id.get().unwrap(), + public_key: self.public_key.get().unwrap(), + private_key: self.private_key.get().unwrap(), + }) + } + } + + #[derive(Debug, Clone, Deserialize, Eq, PartialEq, Default, Merge)] #[serde(deny_unknown_fields, default)] - pub struct Transaction { + pub struct TransactionPartial { pub time_to_live: UserField, pub status_timeout: UserField, pub add_nonce: UserField, } + #[derive(Debug, Clone, Copy)] + pub struct TransactionFull { + pub time_to_live: Duration, + pub status_timeout: Duration, + pub add_nonce: bool, + } + + impl UnwrapPartial for TransactionPartial { + type Output = TransactionFull; + + fn unwrap_partial(self) -> UnwrapPartialResult { + Ok(TransactionFull { + time_to_live: self + .time_to_live + .get() + .map_or(super::DEFAULT_TRANSACTION_TIME_TO_LIVE, UserDuration::get), + status_timeout: self + .status_timeout + .get() + .map_or(super::DEFAULT_TRANSACTION_STATUS_TIMEOUT, UserDuration::get), + add_nonce: self + .add_nonce + .get() + .unwrap_or(super::DEFAULT_ADD_TRANSACTION_NONCE), + }) + } + } + #[derive(Debug, Clone, Eq, PartialEq)] pub struct OnlyHttpUrl(Url); @@ -134,19 +308,21 @@ pub mod user_layer { { let url = Url::deserialize(deserializer)?; if url.scheme() == "http" { - Err(serde::de::Error::custom("only HTTP is supported")) - } else { Ok(Self(url)) + } else { + Err(serde::de::Error::custom("only HTTP scheme is supported")) } } } } +#[derive(Clone, Debug)] pub struct Config { pub chain_id: ChainId, pub account_id: AccountId, pub key_pair: KeyPair, pub basic_auth: Option, + // FIXME: or use `OnlyHttpUrl` here? pub torii_api_url: Url, pub transaction_ttl: Duration, pub transaction_status_timeout: Duration, diff --git a/client/tests/integration/add_account.rs b/client/tests/integration/add_account.rs index d46b3bb65af..e15c185d690 100644 --- a/client/tests/integration/add_account.rs +++ b/client/tests/integration/add_account.rs @@ -2,7 +2,7 @@ use std::thread; use eyre::Result; use iroha_client::{client, data_model::prelude::*}; -use iroha_config::iroha::Configuration; +use iroha_config::parameters::actual::Root as Configuration; use test_network::*; #[test] diff --git a/client/tests/integration/add_domain.rs b/client/tests/integration/add_domain.rs index bb889c25c15..b1260496a1f 100644 --- a/client/tests/integration/add_domain.rs +++ b/client/tests/integration/add_domain.rs @@ -2,7 +2,7 @@ use std::thread; use eyre::Result; use iroha_client::{client, data_model::prelude::*}; -use iroha_config::iroha::Configuration; +use iroha_config::parameters::actual::Root as Configuration; use test_network::*; #[test] diff --git a/client/tests/integration/asset.rs b/client/tests/integration/asset.rs index 31d77cc26ae..99d536c2f2f 100644 --- a/client/tests/integration/asset.rs +++ b/client/tests/integration/asset.rs @@ -6,7 +6,7 @@ use iroha_client::{ crypto::{KeyPair, PublicKey}, data_model::prelude::*, }; -use iroha_config::iroha::Configuration; +use iroha_config::parameters::actual::Root as Configuration; use iroha_primitives::fixed::Fixed; use serde_json::json; use test_network::*; @@ -277,7 +277,7 @@ fn find_rate_and_make_exchange_isi_should_succeed() { alice_id.clone(), ); - let chain_id = ChainId::new("0"); + let chain_id = ChainId::from("0"); let grant_asset_transfer_tx = TransactionBuilder::new(chain_id, asset_id.account_id().clone()) .with_instructions([allow_alice_to_transfer_asset]) diff --git a/client/tests/integration/asset_propagation.rs b/client/tests/integration/asset_propagation.rs index 6047dea994e..4886210ca42 100644 --- a/client/tests/integration/asset_propagation.rs +++ b/client/tests/integration/asset_propagation.rs @@ -9,7 +9,7 @@ use iroha_client::{ prelude::*, }, }; -use iroha_config::iroha::Configuration; +use iroha_config::parameters::actual::Root as Configuration; use test_network::*; #[test] diff --git a/client/tests/integration/burn_public_keys.rs b/client/tests/integration/burn_public_keys.rs index 2bd40263a08..a50bb6cec0e 100644 --- a/client/tests/integration/burn_public_keys.rs +++ b/client/tests/integration/burn_public_keys.rs @@ -13,7 +13,7 @@ fn submit( HashOf, eyre::Result>, ) { - let chain_id = ChainId::new("0"); + let chain_id = ChainId::from("0"); let tx = if let Some((account_id, keypair)) = submitter { TransactionBuilder::new(chain_id, account_id) diff --git a/client/tests/integration/connected_peers.rs b/client/tests/integration/connected_peers.rs index 6cc1df7cf26..64e46b2aafa 100644 --- a/client/tests/integration/connected_peers.rs +++ b/client/tests/integration/connected_peers.rs @@ -8,7 +8,7 @@ use iroha_client::{ peer::Peer as DataModelPeer, }, }; -use iroha_config::iroha::Configuration; +use iroha_config::parameters::actual::Root as Configuration; use iroha_primitives::unique_vec; use rand::{seq::SliceRandom, thread_rng, Rng}; use test_network::*; @@ -39,7 +39,7 @@ fn register_new_peer() -> Result<()> { // Start new peer let mut configuration = Configuration::test(); - configuration.sumeragi.trusted_peers.peers = + configuration.sumeragi.trusted_peers = unique_vec![peer_clients.choose(&mut thread_rng()).unwrap().0.id.clone()]; let rt = Runtime::test(); let new_peer = rt.block_on( diff --git a/client/tests/integration/domain_owner.rs b/client/tests/integration/domain_owner.rs index f36ce73df85..13d9bde6d29 100644 --- a/client/tests/integration/domain_owner.rs +++ b/client/tests/integration/domain_owner.rs @@ -147,7 +147,7 @@ fn domain_owner_asset_definition_permissions() -> Result<()> { let (_rt, _peer, test_client) = ::new().with_port(11_085).start_with_runtime(); wait_for_genesis_committed(&[test_client.clone()], 0); - let chain_id = ChainId::new("0"); + let chain_id = ChainId::from("0"); let kingdom_id: DomainId = "kingdom".parse()?; let bob_id: AccountId = "bob@kingdom".parse()?; let rabbit_id: AccountId = "rabbit@kingdom".parse()?; @@ -212,7 +212,7 @@ fn domain_owner_asset_definition_permissions() -> Result<()> { #[test] fn domain_owner_asset_permissions() -> Result<()> { - let chain_id = ChainId::new("0"); + let chain_id = ChainId::from("0"); let (_rt, _peer, test_client) = ::new().with_port(11_090).start_with_runtime(); wait_for_genesis_committed(&[test_client.clone()], 0); diff --git a/client/tests/integration/events/pipeline.rs b/client/tests/integration/events/pipeline.rs index 89d6c91bf0b..5474401d757 100644 --- a/client/tests/integration/events/pipeline.rs +++ b/client/tests/integration/events/pipeline.rs @@ -8,7 +8,7 @@ use iroha_client::{ prelude::*, }, }; -use iroha_config::iroha::Configuration; +use iroha_config::parameters::actual::Root as Configuration; use test_network::*; // Needed to re-enable ignored tests. diff --git a/client/tests/integration/multiple_blocks_created.rs b/client/tests/integration/multiple_blocks_created.rs index ccc6c3b020e..47bb1e21912 100644 --- a/client/tests/integration/multiple_blocks_created.rs +++ b/client/tests/integration/multiple_blocks_created.rs @@ -9,7 +9,7 @@ use iroha_client::{ prelude::*, }, }; -use iroha_config::iroha::Configuration; +use iroha_config::parameters::actual::Root as Configuration; use test_network::*; const N_BLOCKS: usize = 510; diff --git a/client/tests/integration/multisignature_account.rs b/client/tests/integration/multisignature_account.rs index be7aa0a224f..9490ddb1f00 100644 --- a/client/tests/integration/multisignature_account.rs +++ b/client/tests/integration/multisignature_account.rs @@ -6,7 +6,7 @@ use iroha_client::{ crypto::KeyPair, data_model::prelude::*, }; -use iroha_config::iroha::Configuration; +use iroha_config::parameters::actual::Root as Configuration; use test_network::*; #[test] diff --git a/client/tests/integration/multisignature_transaction.rs b/client/tests/integration/multisignature_transaction.rs index 176e03061bb..7bcd2134f6e 100644 --- a/client/tests/integration/multisignature_transaction.rs +++ b/client/tests/integration/multisignature_transaction.rs @@ -3,14 +3,14 @@ use std::{str::FromStr as _, thread, time::Duration}; use eyre::Result; use iroha_client::{ client::{self, Client, QueryResult}, - config::Configuration as ClientConfiguration, + config::Config as ClientConfiguration, crypto::KeyPair, data_model::{ parameter::{default::MAX_TRANSACTIONS_IN_BLOCK, ParametersBuilder}, prelude::*, }, }; -use iroha_config::iroha::Configuration; +use iroha_config::parameters::actual::Root as Configuration; use test_network::*; #[allow(clippy::too_many_lines)] @@ -40,7 +40,7 @@ fn multisignature_transactions_should_wait_for_all_signatures() -> Result<()> { ); let mut client_configuration = ClientConfiguration::test(&network.genesis.api_address); - let client = Client::new(&client_configuration)?; + let client = Client::new(client_configuration.clone()); let instructions: [InstructionBox; 2] = [create_asset.into(), set_signature_condition.into()]; client.submit_all_blocking(instructions)?; @@ -49,11 +49,9 @@ fn multisignature_transactions_should_wait_for_all_signatures() -> Result<()> { let asset_id = AssetId::new(asset_definition_id, alice_id.clone()); let mint_asset = Mint::asset_quantity(quantity, asset_id.clone()); - let (public_key1, private_key1) = alice_key_pair.into(); client_configuration.account_id = alice_id.clone(); - client_configuration.public_key = public_key1; - client_configuration.private_key = private_key1; - let client = Client::new(&client_configuration)?; + client_configuration.key_pair = alice_key_pair; + let client = Client::new(client_configuration.clone()); let instructions = [mint_asset.clone()]; let transaction = client.build_transaction(instructions, UnlimitedMetadata::new()); client.submit_transaction(&client.sign_transaction(transaction))?; @@ -66,7 +64,7 @@ fn multisignature_transactions_should_wait_for_all_signatures() -> Result<()> { ) .parse() .unwrap(); - let client_1 = Client::new(&client_configuration).expect("Invalid client configuration"); + let client_1 = Client::new(client_configuration.clone()); let request = client::asset::by_account_id(alice_id); let assets = client_1 .request(request.clone())? @@ -76,10 +74,9 @@ fn multisignature_transactions_should_wait_for_all_signatures() -> Result<()> { 2, // Alice has roses and cabbage from Genesis, but doesn't yet have camomile "Multisignature transaction was committed before all required signatures were added" ); - let (public_key2, private_key2) = key_pair_2.into(); - client_configuration.public_key = public_key2; - client_configuration.private_key = private_key2; - let client_2 = Client::new(&client_configuration)?; + + client_configuration.key_pair = key_pair_2; + let client_2 = Client::new(client_configuration); let instructions = [mint_asset]; let transaction = client_2.build_transaction(instructions, UnlimitedMetadata::new()); let transaction = client_2 diff --git a/client/tests/integration/offline_peers.rs b/client/tests/integration/offline_peers.rs index 7627c6ddfbd..2d9aa5b6375 100644 --- a/client/tests/integration/offline_peers.rs +++ b/client/tests/integration/offline_peers.rs @@ -6,7 +6,7 @@ use iroha_client::{ prelude::*, }, }; -use iroha_config::iroha::Configuration; +use iroha_config::parameters::actual::Root as Configuration; use iroha_crypto::KeyPair; use iroha_primitives::addr::socket_addr; use test_network::*; diff --git a/client/tests/integration/permissions.rs b/client/tests/integration/permissions.rs index 5a9bf6881ee..0d95e964396 100644 --- a/client/tests/integration/permissions.rs +++ b/client/tests/integration/permissions.rs @@ -63,7 +63,7 @@ fn get_assets(iroha_client: &Client, id: &AccountId) -> Vec { #[test] #[ignore = "ignore, more in #2851"] fn permissions_disallow_asset_transfer() { - let chain_id = ChainId::new("0"); + let chain_id = ChainId::from("0"); let (_rt, _peer, iroha_client) = ::new().with_port(10_730).start_with_runtime(); wait_for_genesis_committed(&[iroha_client.clone()], 0); @@ -120,7 +120,7 @@ fn permissions_disallow_asset_transfer() { #[test] #[ignore = "ignore, more in #2851"] fn permissions_disallow_asset_burn() { - let chain_id = ChainId::new("0"); + let chain_id = ChainId::from("0"); let (_rt, _peer, iroha_client) = ::new().with_port(10_735).start_with_runtime(); @@ -195,7 +195,7 @@ fn account_can_query_only_its_own_domain() -> Result<()> { #[test] fn permissions_differ_not_only_by_names() { - let chain_id = ChainId::new("0"); + let chain_id = ChainId::from("0"); let (_rt, _not_drop, client) = ::new().with_port(10_745).start_with_runtime(); @@ -292,7 +292,7 @@ fn permissions_differ_not_only_by_names() { #[test] #[allow(deprecated)] fn stored_vs_granted_token_payload() -> Result<()> { - let chain_id = ChainId::new("0"); + let chain_id = ChainId::from("0"); let (_rt, _peer, iroha_client) = ::new().with_port(10_730).start_with_runtime(); wait_for_genesis_committed(&[iroha_client.clone()], 0); diff --git a/client/tests/integration/restart_peer.rs b/client/tests/integration/restart_peer.rs index 6c62a7bb393..dcf322d543c 100644 --- a/client/tests/integration/restart_peer.rs +++ b/client/tests/integration/restart_peer.rs @@ -5,7 +5,7 @@ use iroha_client::{ client::{self, Client, QueryResult}, data_model::prelude::*, }; -use iroha_config::iroha::Configuration; +use iroha_config::parameters::actual::Root as Configuration; use rand::{seq::SliceRandom, thread_rng, Rng}; use test_network::*; use tokio::runtime::Runtime; diff --git a/client/tests/integration/roles.rs b/client/tests/integration/roles.rs index f5a3f266f95..f7cc75fdaa4 100644 --- a/client/tests/integration/roles.rs +++ b/client/tests/integration/roles.rs @@ -46,7 +46,7 @@ fn register_role_with_empty_token_params() -> Result<()> { /// @s8sato added: This test represents #2081 case. #[test] fn register_and_grant_role_for_metadata_access() -> Result<()> { - let chain_id = ChainId::new("0"); + let chain_id = ChainId::from("0"); let (_rt, _peer, test_client) = ::new().with_port(10_700).start_with_runtime(); wait_for_genesis_committed(&vec![test_client.clone()], 0); diff --git a/client/tests/integration/triggers/time_trigger.rs b/client/tests/integration/triggers/time_trigger.rs index 9b9c76d3fe6..319467fc24a 100644 --- a/client/tests/integration/triggers/time_trigger.rs +++ b/client/tests/integration/triggers/time_trigger.rs @@ -5,7 +5,7 @@ use iroha_client::{ client::{self, Client, QueryResult}, data_model::{prelude::*, transaction::WasmSmartContract}, }; -use iroha_config::sumeragi::default::DEFAULT_CONSENSUS_ESTIMATION_MS; +use iroha_config::parameters::defaults::chain_wide::DEFAULT_CONSENSUS_ESTIMATION; use iroha_logger::info; use test_network::*; @@ -24,9 +24,9 @@ macro_rules! const_assert { #[test] #[allow(clippy::cast_precision_loss)] fn time_trigger_execution_count_error_should_be_less_than_15_percent() -> Result<()> { - const PERIOD_MS: u64 = 100; + const PERIOD: Duration = Duration::from_millis(100); const ACCEPTABLE_ERROR_PERCENT: u8 = 15; - const_assert!(PERIOD_MS < DEFAULT_CONSENSUS_ESTIMATION_MS); + const_assert!(PERIOD.as_millis() < DEFAULT_CONSENSUS_ESTIMATION.as_millis()); const_assert!(ACCEPTABLE_ERROR_PERCENT <= 100); let (_rt, _peer, mut test_client) = ::new().with_port(10_775).start_with_runtime(); @@ -42,8 +42,7 @@ fn time_trigger_execution_count_error_should_be_less_than_15_percent() -> Result let prev_value = get_asset_value(&mut test_client, asset_id.clone())?; - let schedule = - TimeSchedule::starting_at(start_time).with_period(Duration::from_millis(PERIOD_MS)); + let schedule = TimeSchedule::starting_at(start_time).with_period(PERIOD); let instruction = Mint::asset_quantity(1_u32, asset_id.clone()); let register_trigger = Register::trigger(Trigger::new( "mint_rose".parse()?, @@ -63,10 +62,10 @@ fn time_trigger_execution_count_error_should_be_less_than_15_percent() -> Result Duration::from_secs(1), 3, )?; - std::thread::sleep(Duration::from_millis(DEFAULT_CONSENSUS_ESTIMATION_MS)); + std::thread::sleep(DEFAULT_CONSENSUS_ESTIMATION); let finish_time = current_time(); - let average_count = finish_time.saturating_sub(start_time).as_millis() / u128::from(PERIOD_MS); + let average_count = finish_time.saturating_sub(start_time).as_millis() / PERIOD.as_millis(); let actual_value = get_asset_value(&mut test_client, asset_id)?; let expected_value = prev_value + u32::try_from(average_count)?; @@ -83,7 +82,7 @@ fn time_trigger_execution_count_error_should_be_less_than_15_percent() -> Result #[test] fn change_asset_metadata_after_1_sec() -> Result<()> { - const PERIOD_MS: u64 = 1000; + const PERIOD: Duration = Duration::from_secs(1); let (_rt, _peer, mut test_client) = ::new().with_port(10_660).start_with_runtime(); wait_for_genesis_committed(&vec![test_client.clone()], 0); @@ -96,7 +95,7 @@ fn change_asset_metadata_after_1_sec() -> Result<()> { let account_id = AccountId::from_str("alice@wonderland").expect("Valid"); let key = Name::from_str("petal")?; - let schedule = TimeSchedule::starting_at(start_time + Duration::from_millis(PERIOD_MS)); + let schedule = TimeSchedule::starting_at(start_time + PERIOD); let instruction = SetKeyValue::asset_definition(asset_definition_id.clone(), key.clone(), 3_u32.to_value()); let register_trigger = Register::trigger(Trigger::new( @@ -114,7 +113,7 @@ fn change_asset_metadata_after_1_sec() -> Result<()> { &mut test_client, &account_id, Duration::from_secs(1), - usize::try_from(PERIOD_MS / DEFAULT_CONSENSUS_ESTIMATION_MS + 1)?, + usize::try_from(PERIOD.as_millis() / DEFAULT_CONSENSUS_ESTIMATION.as_millis() + 1)?, )?; let value = test_client diff --git a/client/tests/integration/tx_history.rs b/client/tests/integration/tx_history.rs index 1a5fad4192d..e7fa1ca2243 100644 --- a/client/tests/integration/tx_history.rs +++ b/client/tests/integration/tx_history.rs @@ -9,7 +9,7 @@ use iroha_client::{ client::{transaction, QueryResult}, data_model::{prelude::*, query::Pagination}, }; -use iroha_config::iroha::Configuration; +use iroha_config::parameters::actual::Root as Configuration; use test_network::*; #[ignore = "ignore, more in #2851"] diff --git a/client/tests/integration/unregister_peer.rs b/client/tests/integration/unregister_peer.rs index fb45c9db8c4..2f856d38d6a 100644 --- a/client/tests/integration/unregister_peer.rs +++ b/client/tests/integration/unregister_peer.rs @@ -9,7 +9,7 @@ use iroha_client::{ prelude::*, }, }; -use iroha_config::iroha::Configuration; +use iroha_config::parameters::actual::Root as Configuration; use test_network::*; // Note the test is marked as `unstable`, not the network. diff --git a/client/tests/integration/unstable_network.rs b/client/tests/integration/unstable_network.rs index 84d1b2d9762..3fd793a5c8f 100644 --- a/client/tests/integration/unstable_network.rs +++ b/client/tests/integration/unstable_network.rs @@ -5,7 +5,7 @@ use iroha_client::{ client::{self, Client, QueryResult}, data_model::{prelude::*, Level}, }; -use iroha_config::iroha::Configuration; +use iroha_config::parameters::actual::Root as Configuration; use rand::seq::SliceRandom; use test_network::*; use tokio::runtime::Runtime; @@ -53,7 +53,8 @@ fn unstable_network( // Given let (network, iroha_client) = rt.block_on(async { let mut configuration = Configuration::test(); - configuration.sumeragi.max_transactions_in_block = MAX_TRANSACTIONS_IN_BLOCK; + configuration.chain_wide.max_transactions_in_block = + MAX_TRANSACTIONS_IN_BLOCK.try_into().unwrap(); configuration.logger.level = Level::INFO; #[cfg(debug_assertions)] { diff --git a/client/tests/integration/upgrade.rs b/client/tests/integration/upgrade.rs index ec53fdbbe8b..b7d0f9a1c04 100644 --- a/client/tests/integration/upgrade.rs +++ b/client/tests/integration/upgrade.rs @@ -12,7 +12,7 @@ use test_network::*; #[test] fn executor_upgrade_should_work() -> Result<()> { - let chain_id = ChainId::new("0"); + let chain_id = ChainId::from("0"); let (_rt, _peer, client) = ::new().with_port(10_795).start_with_runtime(); wait_for_genesis_committed(&vec![client.clone()], 0); diff --git a/config/base/src/lib.rs b/config/base/src/lib.rs index 4dc347d957f..7133b62ca83 100644 --- a/config/base/src/lib.rs +++ b/config/base/src/lib.rs @@ -149,6 +149,19 @@ impl Emitter { pub fn emit_missing_field(&mut self, field_name: impl AsRef) { self.emit(MissingFieldError::new(field_name.as_ref())) } + + pub fn try_unwrap_partial(&mut self, partial: P) -> Option + where + P: UnwrapPartial, + { + partial.unwrap_partial().map_or_else( + |err| { + self.emit_collection(err); + None + }, + Some, + ) + } } pub struct ErrorsCollection(Vec); diff --git a/config/src/parameters/actual.rs b/config/src/parameters/actual.rs index 1656c42352f..e457817631f 100644 --- a/config/src/parameters/actual.rs +++ b/config/src/parameters/actual.rs @@ -131,6 +131,12 @@ pub struct ChainWide { pub wasm_runtime: WasmRuntime, } +impl ChainWide { + pub fn pipeline_time(&self) -> Duration { + self.block_time + self.commit_time + } +} + impl Default for ChainWide { fn default() -> Self { todo!() diff --git a/config/src/parameters/user_layer.rs b/config/src/parameters/user_layer.rs index 5c4606eaa0a..84c6dc2afd0 100644 --- a/config/src/parameters/user_layer.rs +++ b/config/src/parameters/user_layer.rs @@ -99,8 +99,8 @@ impl RootPartial { } // FIXME workaround the inconvenient way `Merge::merge` works - pub fn merge_chain(mut self, other: Self) -> Self { - self.merge(other); + pub fn merge(mut self, other: Self) -> Self { + Merge::merge(&mut self, other); self } } @@ -447,7 +447,7 @@ impl FromEnv for IrohaPartial { emitter.emit(err); None }, - |maybe_value| maybe_value.map(|value| ChainId::new(value.into_owned())), + |maybe_value| maybe_value.map(ChainId::from), ) .into(); let public_key = diff --git a/core/benches/blocks/common.rs b/core/benches/blocks/common.rs index 1bd989de4a4..ad5238d1157 100644 --- a/core/benches/blocks/common.rs +++ b/core/benches/blocks/common.rs @@ -27,7 +27,7 @@ pub fn create_block( account_id: AccountId, key_pair: &KeyPair, ) -> CommittedBlock { - let chain_id = ChainId::new("0"); + let chain_id = ChainId::from("0"); let transaction = TransactionBuilder::new(chain_id.clone(), account_id) .with_instructions(instructions) diff --git a/core/benches/kura.rs b/core/benches/kura.rs index 9dd90d7b268..6e1a832b29c 100644 --- a/core/benches/kura.rs +++ b/core/benches/kura.rs @@ -19,7 +19,7 @@ use iroha_primitives::unique_vec::UniqueVec; use tokio::{fs, runtime::Runtime}; async fn measure_block_size_for_n_executors(n_executors: u32) { - let chain_id = ChainId::new("0"); + let chain_id = ChainId::from("0"); let alice_id = AccountId::from_str("alice@test").expect("tested"); let bob_id = AccountId::from_str("bob@test").expect("tested"); diff --git a/core/benches/validation.rs b/core/benches/validation.rs index 089d6e29e2a..310bd2d277e 100644 --- a/core/benches/validation.rs +++ b/core/benches/validation.rs @@ -93,7 +93,7 @@ fn build_test_and_transient_wsv(keys: KeyPair) -> WorldStateView { } fn accept_transaction(criterion: &mut Criterion) { - let chain_id = ChainId::new("0"); + let chain_id = ChainId::from("0"); let keys = KeyPair::generate(); let transaction = build_test_transaction(&keys, chain_id.clone()); @@ -111,7 +111,7 @@ fn accept_transaction(criterion: &mut Criterion) { } fn sign_transaction(criterion: &mut Criterion) { - let chain_id = ChainId::new("0"); + let chain_id = ChainId::from("0"); let keys = KeyPair::generate(); let transaction = build_test_transaction(&keys, chain_id); @@ -131,7 +131,7 @@ fn sign_transaction(criterion: &mut Criterion) { } fn validate_transaction(criterion: &mut Criterion) { - let chain_id = ChainId::new("0"); + let chain_id = ChainId::from("0"); let keys = KeyPair::generate(); let transaction = AcceptedTransaction::accept( @@ -157,7 +157,7 @@ fn validate_transaction(criterion: &mut Criterion) { } fn sign_blocks(criterion: &mut Criterion) { - let chain_id = ChainId::new("0"); + let chain_id = ChainId::from("0"); let keys = KeyPair::generate(); let transaction = AcceptedTransaction::accept( diff --git a/core/src/block.rs b/core/src/block.rs index af94c1c42fe..b2b0a6bc82d 100644 --- a/core/src/block.rs +++ b/core/src/block.rs @@ -693,7 +693,7 @@ mod tests { #[tokio::test] async fn should_reject_due_to_repetition() { - let chain_id = ChainId::new("0"); + let chain_id = ChainId::from("0"); // Predefined world state let alice_id = AccountId::from_str("alice@wonderland").expect("Valid"); @@ -736,7 +736,7 @@ mod tests { #[tokio::test] async fn tx_order_same_in_validation_and_revalidation() { - let chain_id = ChainId::new("0"); + let chain_id = ChainId::from("0"); // Predefined world state let alice_id = AccountId::from_str("alice@wonderland").expect("Valid"); @@ -802,7 +802,7 @@ mod tests { #[tokio::test] async fn failed_transactions_revert() { - let chain_id = ChainId::new("0"); + let chain_id = ChainId::from("0"); // Predefined world state let alice_id = AccountId::from_str("alice@wonderland").expect("Valid"); diff --git a/core/src/queue.rs b/core/src/queue.rs index f7846b2abc4..8a22689f00a 100644 --- a/core/src/queue.rs +++ b/core/src/queue.rs @@ -396,7 +396,7 @@ mod tests { }; fn accepted_tx(account_id: &str, key: &KeyPair) -> AcceptedTransaction { - let chain_id = ChainId::new("0"); + let chain_id = ChainId::from("0"); let message = std::iter::repeat_with(rand::random::) .take(16) @@ -489,7 +489,7 @@ mod tests { #[test] async fn push_multisignature_tx() { - let chain_id = ChainId::new("0"); + let chain_id = ChainId::from("0"); let max_txs_in_block = 2; let key_pairs = [KeyPair::generate(), KeyPair::generate()]; @@ -713,7 +713,7 @@ mod tests { async fn custom_expired_transaction_is_rejected() { const TTL_MS: u64 = 100; - let chain_id = ChainId::new("0"); + let chain_id = ChainId::from("0"); let max_txs_in_block = 2; let alice_key = KeyPair::generate(); @@ -847,7 +847,7 @@ mod tests { assert!(queue.push(tx.clone(), &wsv).is_ok()); // create the same tx but with timestamp in the future let tx = { - let chain_id = ChainId::new("0"); + let chain_id = ChainId::from("0"); let mut new_tx = TransactionBuilder::new( chain_id.clone(), AccountId::from_str(alice_id).expect("Valid"), diff --git a/core/src/smartcontracts/isi/query.rs b/core/src/smartcontracts/isi/query.rs index db2ab543e21..f7695a93c1c 100644 --- a/core/src/smartcontracts/isi/query.rs +++ b/core/src/smartcontracts/isi/query.rs @@ -249,7 +249,7 @@ mod tests { valid_tx_per_block: usize, invalid_tx_per_block: usize, ) -> Result { - let chain_id = ChainId::new("0"); + let chain_id = ChainId::from("0"); let kura = Kura::blank_kura_for_testing(); let query_handle = LiveQueryStore::test().start(); @@ -411,7 +411,7 @@ mod tests { #[test] async fn find_transaction() -> Result<()> { - let chain_id = ChainId::new("0"); + let chain_id = ChainId::from("0"); let kura = Kura::blank_kura_for_testing(); let query_handle = LiveQueryStore::test().start(); diff --git a/core/src/sumeragi/main_loop.rs b/core/src/sumeragi/main_loop.rs index 06c8e2c2b97..b545e281338 100644 --- a/core/src/sumeragi/main_loop.rs +++ b/core/src/sumeragi/main_loop.rs @@ -1271,7 +1271,7 @@ mod tests { #[test] #[allow(clippy::redundant_clone)] async fn block_sync_invalid_block() { - let chain_id = ChainId::new("0"); + let chain_id = ChainId::from("0"); let leader_key_pair = KeyPair::generate(); let topology = Topology::new(unique_vec![PeerId::new( @@ -1291,7 +1291,7 @@ mod tests { #[test] async fn block_sync_invalid_soft_fork_block() { - let chain_id = ChainId::new("0"); + let chain_id = ChainId::from("0"); let leader_key_pair = KeyPair::generate(); let topology = Topology::new(unique_vec![PeerId::new( @@ -1322,7 +1322,7 @@ mod tests { #[test] #[allow(clippy::redundant_clone)] async fn block_sync_not_proper_height() { - let chain_id = ChainId::new("0"); + let chain_id = ChainId::from("0"); let topology = Topology::new(UniqueVec::new()); let leader_key_pair = KeyPair::generate(); @@ -1349,7 +1349,7 @@ mod tests { #[test] #[allow(clippy::redundant_clone)] async fn block_sync_commit_block() { - let chain_id = ChainId::new("0"); + let chain_id = ChainId::from("0"); let leader_key_pair = KeyPair::generate(); let topology = Topology::new(unique_vec![PeerId::new( @@ -1365,7 +1365,7 @@ mod tests { #[test] async fn block_sync_replace_top_block() { - let chain_id = ChainId::new("0"); + let chain_id = ChainId::from("0"); let leader_key_pair = KeyPair::generate(); let topology = Topology::new(unique_vec![PeerId::new( @@ -1393,7 +1393,7 @@ mod tests { #[test] async fn block_sync_small_view_change_index() { - let chain_id = ChainId::new("0"); + let chain_id = ChainId::from("0"); let leader_key_pair = KeyPair::generate(); let topology = Topology::new(unique_vec![PeerId::new( @@ -1434,7 +1434,7 @@ mod tests { #[test] #[allow(clippy::redundant_clone)] async fn block_sync_genesis_block_do_not_replace() { - let chain_id = ChainId::new("0"); + let chain_id = ChainId::from("0"); let topology = Topology::new(UniqueVec::new()); let leader_key_pair = KeyPair::generate(); diff --git a/core/test_network/src/lib.rs b/core/test_network/src/lib.rs index 120447c2aff..9bb1ac10324 100644 --- a/core/test_network/src/lib.rs +++ b/core/test_network/src/lib.rs @@ -9,19 +9,14 @@ use futures::{prelude::*, stream::FuturesUnordered}; use iroha::Iroha; use iroha_client::{ client::{Client, QueryOutput}, + config::Config as ClientConfiguration, data_model::{isi::Instruction, peer::Peer as DataModelPeer, prelude::*, query::Query, Level}, }; -use iroha_config::{ - base::proxy::{LoadFromEnv, Override}, - iroha::{Configuration, ConfigurationProxy}, - r#mod::Configuration as ClientConfiguration, - sumeragi::Configuration as SumeragiConfiguration, - torii::Configuration as ToriiConfiguration, -}; +use iroha_config::parameters::actual::Root as Configuration; use iroha_crypto::prelude::*; use iroha_data_model::ChainId; use iroha_genesis::{GenesisNetwork, RawGenesisBlock}; -use iroha_logger::{Configuration as LoggerConfiguration, InstrumentFutures}; +use iroha_logger::InstrumentFutures; use iroha_primitives::{ addr::{socket_addr, SocketAddr}, unique_vec, @@ -52,7 +47,7 @@ pub struct Network { /// Get a standardized blockchain id pub fn get_chain_id() -> ChainId { - ChainId::new("0") + ChainId::from("0") } /// Get a standardised key-pair from the hard-coded literals. @@ -131,13 +126,15 @@ impl TestGenesis for GenesisNetwork { first_transaction.append_instruction(isi); } - let chain_id = ChainId::new("0"); - 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, &chain_id, &key_pair).expect("Failed to init genesis") + GenesisNetwork::new(genesis, &cfg.iroha.chain_id, { + use iroha_config::parameters::actual::Genesis; + if let Genesis::Full { key_pair, .. } = &cfg.genesis { + key_pair + } else { + unreachable!("test config should contain full genesis config (or it is a bug)") + } + }) + .expect("Failed to init genesis") } } @@ -219,7 +216,7 @@ impl Network { ); let mut config = Configuration::test(); - config.sumeragi.trusted_peers.peers = + config.sumeragi.trusted_peers = UniqueVec::from_iter(self.peers().map(|peer| &peer.id).cloned()); let peer = PeerBuilder::new() @@ -274,7 +271,7 @@ impl Network { .collect::>>()?; let mut configuration = default_configuration.unwrap_or_else(Configuration::test); - configuration.sumeragi.trusted_peers.peers = + configuration.sumeragi.trusted_peers = UniqueVec::from_iter(peers.iter().map(|peer| peer.id.clone())); let mut genesis_peer = peers.remove(0); @@ -401,22 +398,18 @@ impl Drop for Peer { impl Peer { /// Returns per peer config with all addresses, keys, and id set up. fn get_config(&self, configuration: Configuration) -> Configuration { + use iroha_config::parameters::actual::{Iroha, Torii}; + Configuration { - sumeragi: Box::new(SumeragiConfiguration { + iroha: Iroha { key_pair: self.key_pair.clone(), - peer_id: self.id.clone(), - ..*configuration.sumeragi - }), - torii: Box::new(ToriiConfiguration { - p2p_addr: self.p2p_address.clone(), - api_url: self.api_address.clone(), - ..*configuration.torii - }), - logger: Box::new(LoggerConfiguration { - ..*configuration.logger - }), - public_key: self.key_pair.public_key().clone(), - private_key: self.key_pair.private_key().clone(), + p2p_address: self.p2p_address.clone(), + ..configuration.iroha + }, + torii: Torii { + address: self.api_address.clone(), + ..configuration.torii + }, ..configuration } } @@ -604,7 +597,7 @@ impl PeerBuilder { pub async fn start_with_peer(self, peer: &mut Peer) { let configuration = self.configuration.unwrap_or_else(|| { let mut config = Configuration::test(); - config.sumeragi.trusted_peers.peers = unique_vec![peer.id.clone()]; + config.sumeragi.trusted_peers = unique_vec![peer.id.clone()]; config }); let genesis = match self.genesis { @@ -637,10 +630,7 @@ impl PeerBuilder { let client = Client::test(&peer.api_address); - time::sleep(Duration::from_millis( - configuration.sumeragi.pipeline_time_ms(), - )) - .await; + time::sleep(configuration.chain_wide.pipeline_time()).await; (peer, client) } @@ -678,7 +668,7 @@ pub trait TestConfiguration { /// Client configuration mocking trait. pub trait TestClientConfiguration { /// Creates test client configuration - fn test(api_url: &SocketAddr) -> Self; + fn test(api_address: &SocketAddr) -> Self; } /// Client mocking trait @@ -768,61 +758,68 @@ impl TestRuntime for Runtime { impl TestConfiguration for Configuration { fn test() -> Self { - let mut sample_proxy = iroha::samples::get_config_proxy( + use iroha_config::{ + base::{FromEnv as _, StdEnv, UnwrapPartial as _}, + parameters::user_layer::{CliContext, RootPartial}, + }; + + let mut layer = iroha::samples::get_user_config( UniqueVec::new(), Some(get_chain_id()), Some(get_key_pair()), - ); - let env_proxy = - ConfigurationProxy::from_std_env().expect("Test env variables should parse properly"); + ) + .merge(RootPartial::from_env(&StdEnv).expect("test env variables should parse properly")); + let (public_key, private_key) = KeyPair::generate().into(); - sample_proxy.public_key = Some(public_key); - sample_proxy.private_key = Some(private_key); - sample_proxy.override_with(env_proxy) - .build() - .expect("Test Iroha config failed to build. This is either a programmer error or a compiler bug.") + layer.iroha.public_key.set(public_key); + layer.iroha.private_key.set(private_key); + + layer + .unwrap_partial() + .expect("should not fail as all fields are present") + .parse(CliContext { + submit_genesis: true, + }) + .expect("Test Iroha config failed to build. This is likely to be a bug.") } fn pipeline_time() -> Duration { - Duration::from_millis(Self::test().sumeragi.pipeline_time_ms()) + Self::test().chain_wide.pipeline_time() } fn block_sync_gossip_time() -> Duration { - Duration::from_millis(Self::test().block_sync.gossip_period_ms) + Self::test().block_sync.gossip_period } } impl TestClientConfiguration for ClientConfiguration { - fn test(api_url: &SocketAddr) -> Self { - let mut configuration = - iroha_client::samples::get_client_config(get_chain_id(), &get_key_pair()); - configuration.torii_api_url = format!("http://{api_url}") - .parse() - .expect("Should be valid url"); - configuration + fn test(api_address: &SocketAddr) -> Self { + iroha_client::samples::get_client_config( + get_chain_id(), + get_key_pair().clone(), + format!("http://{api_address}") + .parse() + .expect("should be valid url"), + ) } } impl TestClient for Client { - fn test(api_url: &SocketAddr) -> Self { - Client::new(&ClientConfiguration::test(api_url)).expect("Invalid client configuration") + fn test(api_addr: &SocketAddr) -> Self { + Client::new(ClientConfiguration::test(api_addr)) } - fn test_with_key(api_url: &SocketAddr, keys: KeyPair) -> Self { - let mut configuration = ClientConfiguration::test(api_url); - let (public_key, private_key) = keys.into(); - configuration.public_key = public_key; - configuration.private_key = private_key; - Client::new(&configuration).expect("Invalid client configuration") + fn test_with_key(api_addr: &SocketAddr, keys: KeyPair) -> Self { + let mut configuration = ClientConfiguration::test(api_addr); + configuration.key_pair = keys; + Client::new(configuration) } - fn test_with_account(api_url: &SocketAddr, keys: KeyPair, account_id: &AccountId) -> Self { - let mut configuration = ClientConfiguration::test(api_url); + fn test_with_account(api_addr: &SocketAddr, keys: KeyPair, account_id: &AccountId) -> Self { + let mut configuration = ClientConfiguration::test(api_addr); configuration.account_id = account_id.clone(); - let (public_key, private_key) = keys.into(); - configuration.public_key = public_key; - configuration.private_key = private_key; - Client::new(&configuration).expect("Invalid client configuration") + configuration.key_pair = keys; + Client::new(configuration) } fn for_each_event(self, event_filter: FilterBox, f: impl Fn(Result)) { diff --git a/data_model/src/lib.rs b/data_model/src/lib.rs index a5360468b19..857d8002d40 100644 --- a/data_model/src/lib.rs +++ b/data_model/src/lib.rs @@ -592,6 +592,15 @@ pub mod model { #[ffi_type(unsafe {robust})] pub struct ChainId(Box); + impl From for ChainId + where + T: Into>, + { + fn from(value: T) -> Self { + ChainId(value.into()) + } + } + /// Sized container for all possible identifications. #[derive( Debug, @@ -1011,13 +1020,6 @@ pub mod model { /// in the next request to continue fetching results of the original query pub cursor: crate::query::cursor::ForwardCursor, } - - impl ChainId { - /// Create new [`Self`] - pub fn new(inner: String) -> Self { - Self(inner.into()) - } - } } impl Decode for ChainId { @@ -1025,7 +1027,7 @@ impl Decode for ChainId { input: &mut I, ) -> Result { let boxed: String = parity_scale_codec::Decode::decode(input)?; - Ok(Self::new(boxed)) + Ok(Self::from(boxed)) } } diff --git a/genesis/src/lib.rs b/genesis/src/lib.rs index 7f039b88922..1b4d2acb403 100644 --- a/genesis/src/lib.rs +++ b/genesis/src/lib.rs @@ -361,7 +361,7 @@ mod tests { #[test] fn load_new_genesis_block() -> Result<()> { - let chain_id = ChainId::new("0"); + let chain_id = ChainId::from("0"); let genesis_key_pair = KeyPair::generate(); let (alice_public_key, _) = KeyPair::generate().into(); diff --git a/telemetry/src/dev.rs b/telemetry/src/dev.rs index 0b31bbd5398..4e5a8fb69f2 100644 --- a/telemetry/src/dev.rs +++ b/telemetry/src/dev.rs @@ -3,7 +3,6 @@ use std::path::Path; use eyre::{Result, WrapErr}; -use iroha_config::parameters::actual::DevTelemetry as DevTelemetryConfig; use iroha_logger::telemetry::Event as Telemetry; use tokio::{ fs::OpenOptions, diff --git a/tools/swarm/src/compose.rs b/tools/swarm/src/compose.rs index 7fdf3b0997c..b7873a69dd6 100644 --- a/tools/swarm/src/compose.rs +++ b/tools/swarm/src/compose.rs @@ -397,7 +397,7 @@ impl DockerComposeBuilder<'_> { ) })?; - let chain_id = ChainId::new("00000000-0000-0000-0000-000000000000"); + let chain_id = ChainId::from("00000000-0000-0000-0000-000000000000"); let peers = peer_generator::generate_peers(self.peers, self.seed) .wrap_err("Failed to generate peers")?; let genesis_key_pair = generate_key_pair(self.seed, GENESIS_KEYPAIR_SEED) @@ -668,7 +668,7 @@ mod tests { fn default_config_with_swarm_env_is_exhaustive() { let keypair = KeyPair::generate(); let env: TestEnv = CompactPeerEnv { - chain_id: ChainId::new("00000000-0000-0000-0000-000000000000"), + chain_id: ChainId::from("00000000-0000-0000-0000-000000000000"), key_pair: keypair.clone(), genesis_public_key: keypair.public_key().clone(), genesis_private_key: Some(keypair.private_key().clone()), @@ -705,7 +705,7 @@ mod tests { services: { let mut map = BTreeMap::new(); - let chain_id = ChainId::new("00000000-0000-0000-0000-000000000000"); + let chain_id = ChainId::from("00000000-0000-0000-0000-000000000000"); let key_pair = KeyPair::generate_with_configuration(KeyGenConfiguration::from_seed(vec![ 1, 5, 1, 2, 2, 3, 4, 1, 2, 3, @@ -777,7 +777,7 @@ mod tests { #[test] fn empty_genesis_public_key_is_skipped_in_env() { - let chain_id = ChainId::new("00000000-0000-0000-0000-000000000000"); + let chain_id = ChainId::from("00000000-0000-0000-0000-000000000000"); let key_pair = KeyPair::generate_with_configuration(KeyGenConfiguration::from_seed(vec![0, 1, 2])) From d018064cd2a1caab810bbe17313d9b5495297470 Mon Sep 17 00:00:00 2001 From: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> Date: Tue, 23 Jan 2024 08:39:06 +0900 Subject: [PATCH 15/94] [feat]: compile `iroha_client_cli`! Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- cli/src/main.rs | 8 +--- client/src/config.rs | 20 +++++++--- client_cli/src/main.rs | 87 +++++++++++++++++++----------------------- 3 files changed, 55 insertions(+), 60 deletions(-) diff --git a/cli/src/main.rs b/cli/src/main.rs index 8526c0dfa92..549f6ec6713 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -18,13 +18,7 @@ fn default_terminal_colors_str() -> clap::builder::OsStr { #[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. + /// Path to the configuration file #[arg( long, short, diff --git a/client/src/config.rs b/client/src/config.rs index 9bfe44b14e8..b14001b7e12 100644 --- a/client/src/config.rs +++ b/client/src/config.rs @@ -8,6 +8,7 @@ use std::{num::NonZeroU64, time::Duration}; use derive_more::Display; use eyre::Result; +pub use iroha_config::base; use iroha_crypto::prelude::*; use iroha_data_model::{prelude::*, ChainId}; use iroha_primitives::small::SmallStr; @@ -65,12 +66,12 @@ pub struct BasicAuth { } pub mod user_layer { - use std::time::Duration; + use std::{path::Path, time::Duration}; use eyre::{eyre, Context, Report}; use iroha_config::base::{ - Emitter, ErrorsCollection, Merge, MissingFieldError, UnwrapPartial, UnwrapPartialResult, - UserDuration, UserField, + Emitter, ErrorsCollection, FromEnvDefaultFallback, Merge, MissingFieldError, UnwrapPartial, + UnwrapPartialResult, UserDuration, UserField, }; use iroha_crypto::{KeyPair, PrivateKey, PublicKey}; use iroha_data_model::{account::AccountId, ChainId}; @@ -94,12 +95,19 @@ pub mod user_layer { Default::default() } - pub fn merge_chain(mut self, other: Self) -> Self { - self.merge(other); + pub fn from_toml(path: impl AsRef) -> eyre::Result { + todo!() + } + + pub fn merge(mut self, other: Self) -> Self { + Merge::merge(&mut self, other); self } } + // FIXME: should config be read from ENV? + impl FromEnvDefaultFallback for RootPartial {} + #[derive(Clone, Debug)] pub struct RootFull { pub chain_id: ChainId, @@ -316,7 +324,7 @@ pub mod user_layer { } } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Serialize)] pub struct Config { pub chain_id: ChainId, pub account_id: AccountId, diff --git a/client_cli/src/main.rs b/client_cli/src/main.rs index 80a45b35066..0724308372c 100644 --- a/client_cli/src/main.rs +++ b/client_cli/src/main.rs @@ -2,7 +2,7 @@ use std::{ fs::{self, read as read_file}, io::{stdin, stdout}, - path::PathBuf, + path::{Path, PathBuf}, str::FromStr, time::Duration, }; @@ -16,12 +16,13 @@ use dialoguer::Confirm; use erased_serde::Serialize; use iroha_client::{ client::{Client, QueryResult}, - config::{path::Path, Configuration as ClientConfiguration, ConfigurationProxy}, + config::Config, data_model::prelude::*, }; -use iroha_config_base::proxy::{LoadFromDisk, LoadFromEnv, Override}; use iroha_primitives::addr::{Ipv4Addr, Ipv6Addr, SocketAddr}; +const DEFAULT_CONFIG_PATH: &str = "iroha_client.toml"; + /// Re-usable clap `--metadata ` (`-m`) argument. /// Should be combined with `#[command(flatten)]` attr. #[derive(clap::Args, Debug, Clone)] @@ -90,21 +91,15 @@ impl FromStr for ValueArg { #[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 { - /// 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. + /// Path to the configuration file #[arg( short, long, value_name("PATH"), value_hint(clap::ValueHint::FilePath), - value_parser(Path::user_provided_str) + default_value_t = DEFAULT_CONFIG_PATH.to_owned() )] - config: Option, + config: String, /// More verbose output #[arg(short, long)] verbose: bool, @@ -146,7 +141,11 @@ enum Subcommand { /// Context inside which command is executed trait RunContext { /// Get access to configuration - fn configuration(&self) -> &ClientConfiguration; + fn configuration(&self) -> &Config; + + fn client_from_configuration(&self) -> Client { + Client::new(self.configuration().clone()) + } /// Skip check for MST fn skip_mst_check(&self) -> bool; @@ -161,12 +160,12 @@ trait RunContext { struct PrintJsonContext { write: W, - config: ClientConfiguration, + config: Config, skip_mst_check: bool, } impl RunContext for PrintJsonContext { - fn configuration(&self) -> &ClientConfiguration { + fn configuration(&self) -> &Config { &self.config } @@ -204,14 +203,13 @@ impl RunArgs for Subcommand { } } -// TODO: move into config. +// TODO: move into config? const RETRY_COUNT_MST: u32 = 1; const RETRY_IN_MST: Duration = Duration::from_millis(100); -static DEFAULT_CONFIG_PATH: &str = "config"; - fn main() -> Result<()> { color_eyre::install()?; + let Args { config: config_path, subcommand, @@ -219,22 +217,7 @@ fn main() -> Result<()> { skip_mst_check, } = clap::Parser::parse(); - 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 { - 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")?; + let config = load_config(config_path)?; if verbose { eprintln!( @@ -253,6 +236,19 @@ fn main() -> Result<()> { subcommand.run(&mut context) } +fn load_config(path: impl AsRef) -> Result { + use iroha_client::config::{ + base::{FromEnv, StdEnv, UnwrapPartial}, + user_layer::RootPartial, + }; + + let layer = RootPartial::from_toml(path)?; + let layer = layer.merge(RootPartial::from_env(&StdEnv)?); + let config = layer.unwrap_partial()?.parse()?; + + Ok(config) +} + /// Submit instruction with metadata to network. /// /// # Errors @@ -263,7 +259,7 @@ fn submit( metadata: UnlimitedMetadata, context: &mut dyn RunContext, ) -> Result<()> { - let iroha_client = Client::new(context.configuration())?; + let iroha_client = context.client_from_configuration(); let instructions = instructions.into(); let tx = iroha_client.build_transaction(instructions, metadata); let transactions = if context.skip_mst_check() { @@ -322,7 +318,6 @@ mod filter { } mod events { - use iroha_client::client::Client; use super::*; @@ -349,7 +344,7 @@ mod events { } fn listen(filter: FilterBox, context: &mut dyn RunContext) -> Result<()> { - let iroha_client = Client::new(context.configuration())?; + let iroha_client = context.client_from_configuration(); eprintln!("Listening to events with filter: {filter:?}"); iroha_client .listen_for_events(filter) @@ -362,8 +357,6 @@ mod events { mod blocks { use std::num::NonZeroU64; - use iroha_client::client::Client; - use super::*; /// Get block stream from iroha peer @@ -381,7 +374,7 @@ mod blocks { } fn listen(height: NonZeroU64, context: &mut dyn RunContext) -> Result<()> { - let iroha_client = Client::new(context.configuration())?; + let iroha_client = context.client_from_configuration(); eprintln!("Listening to blocks from height: {height}"); iroha_client .listen_for_blocks(height) @@ -446,7 +439,7 @@ mod domain { impl RunArgs for List { fn run(self, context: &mut dyn RunContext) -> Result<()> { - let client = Client::new(context.configuration())?; + let client = context.client_from_configuration(); let vec = match self { Self::All => client @@ -682,7 +675,7 @@ mod account { impl RunArgs for List { fn run(self, context: &mut dyn RunContext) -> Result<()> { - let client = Client::new(context.configuration())?; + let client = context.client_from_configuration(); let vec = match self { Self::All => client @@ -752,7 +745,7 @@ mod account { impl RunArgs for ListPermissions { fn run(self, context: &mut dyn RunContext) -> Result<()> { - let client = Client::new(context.configuration())?; + let client = context.client_from_configuration(); let find_all_permissions = FindPermissionTokensByAccountId::new(self.id); let permissions = client .request(find_all_permissions) @@ -765,7 +758,7 @@ mod account { mod asset { use iroha_client::{ - client::{self, asset, Client}, + client::{self, asset}, data_model::{asset::AssetDefinition, name::Name}, }; @@ -939,7 +932,7 @@ mod asset { impl RunArgs for Get { fn run(self, context: &mut dyn RunContext) -> Result<()> { let Self { asset_id } = self; - let iroha_client = Client::new(context.configuration())?; + let iroha_client = context.client_from_configuration(); let asset = iroha_client .request(asset::by_id(asset_id)) .wrap_err("Failed to get asset.")?; @@ -959,7 +952,7 @@ mod asset { impl RunArgs for List { fn run(self, context: &mut dyn RunContext) -> Result<()> { - let client = Client::new(context.configuration())?; + let client = context.client_from_configuration(); let vec = match self { Self::All => client @@ -1033,7 +1026,7 @@ mod asset { impl RunArgs for GetKeyValue { fn run(self, context: &mut dyn RunContext) -> Result<()> { let Self { asset_id, key } = self; - let client = Client::new(context.configuration())?; + let client = context.client_from_configuration(); let find_key_value = FindAssetKeyValueByIdAndKey::new(asset_id, key); let asset = client .request(find_key_value) From a36ebf05e8fbd97edd81a8dcabd8fbf1cf3457cd Mon Sep 17 00:00:00 2001 From: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> Date: Tue, 23 Jan 2024 09:09:36 +0900 Subject: [PATCH 16/94] [feat]: compile everything *_* Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- client/benches/torii.rs | 44 +++++----- client/examples/million_accounts_genesis.rs | 20 ++--- client/examples/tutorial.rs | 54 +++++++------ config/base/src/lib.rs | 6 ++ config/src/parameters/actual.rs | 11 ++- config/src/parameters/user_layer.rs | 11 +++ config/tests/fixtures.rs | 5 +- core/benches/blocks/common.rs | 4 +- core/benches/kura.rs | 4 +- data_model/src/lib.rs | 5 +- logger/src/lib.rs | 1 + logger/tests/setting_logger.rs | 7 +- p2p/tests/integration/p2p.rs | 16 +--- tools/kagami/src/genesis.rs | 2 +- tools/swarm/src/compose.rs | 90 ++++++--------------- 15 files changed, 123 insertions(+), 157 deletions(-) diff --git a/client/benches/torii.rs b/client/benches/torii.rs index dd503e3c396..f13025e3190 100644 --- a/client/benches/torii.rs +++ b/client/benches/torii.rs @@ -17,17 +17,6 @@ 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"); @@ -52,7 +41,10 @@ fn query_requests(criterion: &mut Criterion) { ) .build(), &chain_id, - &get_genesis_key_pair(&configuration), + configuration + .genesis + .key_pair() + .expect("genesis config should be full, probably a bug"), ) .expect("genesis creation failed"); @@ -81,12 +73,13 @@ fn query_requests(criterion: &mut Criterion) { quantity, AssetId::new(asset_definition_id, account_id.clone()), ); - let mut client_config = - iroha_client::samples::get_client_config(get_chain_id(), &get_key_pair()); - - client_config.torii_api_url = format!("http://{}", peer.api_address).parse().unwrap(); + let client_config = iroha_client::samples::get_client_config( + get_chain_id(), + get_key_pair(), + format!("http://{}", peer.api_address).parse().unwrap(), + ); - let iroha_client = Client::new(&client_config).expect("Invalid client configuration"); + let iroha_client = Client::new(client_config); thread::sleep(std::time::Duration::from_millis(5000)); let instructions: [InstructionBox; 4] = [ @@ -148,7 +141,7 @@ fn instruction_submits(criterion: &mut Criterion) { .domain("wonderland".parse().expect("Valid")) .account( "alice".parse().expect("Valid"), - configuration.public_key.clone(), + configuration.iroha.key_pair.public_key().clone(), ) .finish_domain() .executor( @@ -156,7 +149,10 @@ fn instruction_submits(criterion: &mut Criterion) { ) .build(), &chain_id, - &get_genesis_key_pair(&configuration), + configuration + .genesis + .key_pair() + .expect("config should be full; probably a bug"), ) .expect("failed to create genesis"); let builder = PeerBuilder::new() @@ -170,10 +166,12 @@ fn instruction_submits(criterion: &mut Criterion) { let (public_key, _) = KeyPair::generate().into(); let create_account = Register::account(Account::new(account_id.clone(), [public_key])).into(); let asset_definition_id = AssetDefinitionId::new(domain_id, "xor".parse().expect("Valid")); - let mut client_config = - iroha_client::samples::get_client_config(get_chain_id(), &get_key_pair()); - client_config.torii_api_url = format!("http://{}", peer.api_address).parse().unwrap(); - let iroha_client = Client::new(&client_config).expect("Invalid client configuration"); + let client_config = iroha_client::samples::get_client_config( + get_chain_id(), + get_key_pair(), + format!("http://{}", peer.api_address).parse().unwrap(), + ); + let iroha_client = Client::new(client_config); thread::sleep(std::time::Duration::from_millis(5000)); let _ = iroha_client .submit_all([create_domain, create_account]) diff --git a/client/examples/million_accounts_genesis.rs b/client/examples/million_accounts_genesis.rs index 737e9236246..2e48cbc2822 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::{crypto::KeyPair, data_model::prelude::*}; +use iroha_client::data_model::prelude::*; use iroha_data_model::isi::InstructionBox; use iroha_genesis::{GenesisNetwork, RawGenesisBlock, RawGenesisBlockBuilder}; use iroha_primitives::unique_vec; @@ -45,18 +45,14 @@ fn main_genesis() { Some(get_key_pair()), ); let rt = Runtime::test(); - let genesis = GenesisNetwork::new(generate_genesis(1_000_000_u32), &chain_id, &{ - let private_key = configuration + let genesis = GenesisNetwork::new( + generate_genesis(1_000_000_u32), + &chain_id, + 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") - }) + .key_pair() + .expect("should be available in the config; probably a bug"), + ) .expect("genesis creation failed"); let builder = PeerBuilder::new() diff --git a/client/examples/tutorial.rs b/client/examples/tutorial.rs index b83a2665ae3..b20722e4412 100644 --- a/client/examples/tutorial.rs +++ b/client/examples/tutorial.rs @@ -1,50 +1,52 @@ //! This file contains examples from the Rust tutorial. //! -use std::fs::File; use eyre::{Error, WrapErr}; -use iroha_client::config::Configuration; +use iroha_client::config::{base::UnwrapPartial, user_layer::RootPartial as UserConfig, Config}; // #region rust_config_crates // #endregion rust_config_crates fn main() { // #region rust_config_load - let config_loc = "../configs/client/config.json"; - let file = File::open(config_loc) + let config_loc = "../configs/client/config.toml"; + let config = UserConfig::from_toml(config_loc) .wrap_err("Unable to load the configuration file at `.....`") - .expect("Config file is loading normally."); - let config: Configuration = serde_json::from_reader(file) - .wrap_err("Failed to parse `../configs/client/config.json`") - .expect("Verified in tests"); + .expect("Config file is loading normally.") + .unwrap_partial() + .expect("Config should have all required fields") + .parse() + .expect("Config should be semantically valid"); // #endregion rust_config_load // Your code goes here… - json_config_client_test(&config) + json_config_client_test(config.clone()) .expect("JSON config client example is expected to work correctly"); - domain_registration_test(&config) + domain_registration_test(config.clone()) .expect("Domain registration example is expected to work correctly"); account_definition_test().expect("Account definition example is expected to work correctly"); - account_registration_test(&config) + account_registration_test(config.clone()) .expect("Account registration example is expected to work correctly"); - asset_registration_test(&config) + asset_registration_test(config.clone()) .expect("Asset registration example is expected to work correctly"); - asset_minting_test(&config).expect("Asset minting example is expected to work correctly"); - asset_burning_test(&config).expect("Asset burning example is expected to work correctly"); + asset_minting_test(config.clone()) + .expect("Asset minting example is expected to work correctly"); + asset_burning_test(config.clone()) + .expect("Asset burning example is expected to work correctly"); // output_visualising_test(&config).expect(msg: "Visualising outputs example is expected to work correctly"); println!("Success!"); } -fn json_config_client_test(config: &Configuration) -> Result<(), Error> { +fn json_config_client_test(config: Config) -> Result<(), Error> { use iroha_client::client::Client; // Initialise a client with a provided config - let _current_client: Client = Client::new(config)?; + let _current_client = Client::new(config); Ok(()) } -fn domain_registration_test(config: &Configuration) -> Result<(), Error> { +fn domain_registration_test(config: Config) -> Result<(), Error> { // #region domain_register_example_crates use iroha_client::{ client::Client, @@ -67,7 +69,7 @@ fn domain_registration_test(config: &Configuration) -> Result<(), Error> { // #region rust_client_create // Create an Iroha client - let iroha_client: Client = Client::new(config)?; + let iroha_client = Client::new(config); // #endregion rust_client_create // #region domain_register_example_prepare_tx @@ -108,7 +110,7 @@ fn account_definition_test() -> Result<(), Error> { Ok(()) } -fn account_registration_test(config: &Configuration) -> Result<(), Error> { +fn account_registration_test(config: Config) -> Result<(), Error> { // #region register_account_crates use iroha_client::{ client::Client, @@ -121,7 +123,7 @@ fn account_registration_test(config: &Configuration) -> Result<(), Error> { // #endregion register_account_crates // Create an Iroha client - let iroha_client: Client = Client::new(config)?; + let iroha_client = Client::new(config); // #region register_account_create // Create an AccountId instance by providing the account and domain name @@ -156,7 +158,7 @@ fn account_registration_test(config: &Configuration) -> Result<(), Error> { Ok(()) } -fn asset_registration_test(config: &Configuration) -> Result<(), Error> { +fn asset_registration_test(config: Config) -> Result<(), Error> { // #region register_asset_crates use std::str::FromStr as _; @@ -169,7 +171,7 @@ fn asset_registration_test(config: &Configuration) -> Result<(), Error> { // #endregion register_asset_crates // Create an Iroha client - let iroha_client: Client = Client::new(config)?; + let iroha_client = Client::new(config); // #region register_asset_create_asset // Create an asset @@ -206,7 +208,7 @@ fn asset_registration_test(config: &Configuration) -> Result<(), Error> { Ok(()) } -fn asset_minting_test(config: &Configuration) -> Result<(), Error> { +fn asset_minting_test(config: Config) -> Result<(), Error> { // #region mint_asset_crates use std::str::FromStr; @@ -217,7 +219,7 @@ fn asset_minting_test(config: &Configuration) -> Result<(), Error> { // #endregion mint_asset_crates // Create an Iroha client - let iroha_client: Client = Client::new(config)?; + let iroha_client = Client::new(config); // Define the instances of an Asset and Account // #region mint_asset_define_asset_account @@ -257,7 +259,7 @@ fn asset_minting_test(config: &Configuration) -> Result<(), Error> { Ok(()) } -fn asset_burning_test(config: &Configuration) -> Result<(), Error> { +fn asset_burning_test(config: Config) -> Result<(), Error> { // #region burn_asset_crates use std::str::FromStr; @@ -268,7 +270,7 @@ fn asset_burning_test(config: &Configuration) -> Result<(), Error> { // #endregion burn_asset_crates // Create an Iroha client - let iroha_client: Client = Client::new(config)?; + let iroha_client = Client::new(config); // #region burn_asset_define_asset_account // Define the instances of an Asset and Account diff --git a/config/base/src/lib.rs b/config/base/src/lib.rs index 7133b62ca83..57cbd1581d9 100644 --- a/config/base/src/lib.rs +++ b/config/base/src/lib.rs @@ -71,6 +71,12 @@ impl ByteSize { } } +impl From for ByteSize { + fn from(value: T) -> Self { + Self(value) + } +} + #[derive(thiserror::Error, Debug)] #[error("missing field: `{path}`")] pub struct MissingFieldError { diff --git a/config/src/parameters/actual.rs b/config/src/parameters/actual.rs index e457817631f..d4cddcd6d06 100644 --- a/config/src/parameters/actual.rs +++ b/config/src/parameters/actual.rs @@ -67,8 +67,15 @@ pub enum Genesis { impl Genesis { pub fn public_key(&self) -> &PublicKey { match self { - Genesis::Partial { public_key } => &public_key, - Genesis::Full { key_pair, .. } => key_pair.public_key(), + Self::Partial { public_key } => &public_key, + Self::Full { key_pair, .. } => key_pair.public_key(), + } + } + + pub fn key_pair(&self) -> Option<&KeyPair> { + match self { + Self::Partial { .. } => None, + Self::Full { key_pair, .. } => Some(key_pair), } } } diff --git a/config/src/parameters/user_layer.rs b/config/src/parameters/user_layer.rs index 84c6dc2afd0..d612a20e1b0 100644 --- a/config/src/parameters/user_layer.rs +++ b/config/src/parameters/user_layer.rs @@ -945,6 +945,17 @@ pub struct LoggerFull { pub tokio_console_addr: SocketAddr, } +impl Default for LoggerFull { + fn default() -> Self { + Self { + level: Level::default(), + format: Format::default(), + #[cfg(feature = "tokio-console")] + tokio_console_addr: DEFAULT_TOKIO_CONSOLE_ADDR, + } + } +} + impl UnwrapPartial for LoggerPartial { type Output = LoggerFull; diff --git a/config/tests/fixtures.rs b/config/tests/fixtures.rs index c32b1ba6ee7..ee1eb6d51e8 100644 --- a/config/tests/fixtures.rs +++ b/config/tests/fixtures.rs @@ -195,7 +195,8 @@ fn inconsistent_genesis_config() -> Result<()> { }) .expect_err("should fail with bad genesis config"); - let expected = expect_test::expect!["`genesis.file` and `genesis.private_key` should be set together"]; + let expected = + expect_test::expect!["`genesis.file` and `genesis.private_key` should be set together"]; expected.assert_eq(&format!("{error:#}")); Ok(()) @@ -329,7 +330,7 @@ fn config_from_file_and_env() -> Result<()> { let env = test_env_from_file(fixtures_dir().join("config_and_env.env")); let _config = RootPartial::from_toml(fixtures_dir().join("config_and_env.toml"))? - .merge_chain(RootPartial::from_env(&env)?) + .merge(RootPartial::from_env(&env)?) .unwrap_partial()? .parse(CliContext { submit_genesis: false, diff --git a/core/benches/blocks/common.rs b/core/benches/blocks/common.rs index ad5238d1157..4d59f22af41 100644 --- a/core/benches/blocks/common.rs +++ b/core/benches/blocks/common.rs @@ -185,8 +185,8 @@ pub fn build_wsv( ); let mut wsv = WorldStateView::new(World::with([domain], UniqueVec::new()), kura, query_handle); wsv.config.transaction_limits = TransactionLimits::new(u64::MAX, u64::MAX); - wsv.config.wasm_runtime_config.fuel_limit = u64::MAX; - wsv.config.wasm_runtime_config.max_memory = u32::MAX; + wsv.config.wasm_runtime.fuel_limit = u64::MAX; + wsv.config.wasm_runtime.max_memory = u32::MAX.into(); { let path_to_executor = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) diff --git a/core/benches/kura.rs b/core/benches/kura.rs index 6e1a832b29c..ec267d3df8e 100644 --- a/core/benches/kura.rs +++ b/core/benches/kura.rs @@ -4,7 +4,7 @@ use std::str::FromStr as _; use byte_unit::Byte; use criterion::{criterion_group, criterion_main, Criterion}; -use iroha_config::kura::Configuration; +use iroha_config::parameters::actual::Kura as Config; use iroha_core::{ block::*, kura::{BlockStore, LockStatus}, @@ -40,7 +40,7 @@ async fn measure_block_size_for_n_executors(n_executors: u32) { let tx = AcceptedTransaction::accept(tx, &chain_id, &transaction_limits) .expect("Failed to accept Transaction."); let dir = tempfile::tempdir().expect("Could not create tempfile."); - let cfg = Configuration { + let cfg = Config { init_mode: iroha_config::kura::Mode::Strict, debug_output_new_blocks: false, block_store_path: dir.path().to_str().unwrap().into(), diff --git a/data_model/src/lib.rs b/data_model/src/lib.rs index 857d8002d40..3275d4dce6b 100644 --- a/data_model/src/lib.rs +++ b/data_model/src/lib.rs @@ -1037,10 +1037,7 @@ mod test { #[test] fn parse_level_from_str() { - assert_eq!( - "INFO".parse::().unwrap(), - crate::model::Level::INFO - ); + assert_eq!("INFO".parse::().unwrap(), Level::INFO); } } diff --git a/logger/src/lib.rs b/logger/src/lib.rs index d3acea7c531..35916a9a66f 100644 --- a/logger/src/lib.rs +++ b/logger/src/lib.rs @@ -86,6 +86,7 @@ pub fn test_logger() -> LoggerHandle { let config = Config { level: Level::DEBUG, format: Format::Pretty, + ..Config::default() }; init_global(&config, true).expect( diff --git a/logger/tests/setting_logger.rs b/logger/tests/setting_logger.rs index 209a6b45928..6f118366562 100644 --- a/logger/tests/setting_logger.rs +++ b/logger/tests/setting_logger.rs @@ -1,11 +1,8 @@ -use iroha_config::base::proxy::Builder; -use iroha_logger::{init_global, ConfigurationProxy}; +use iroha_logger::{init_global, Config}; #[tokio::test] async fn setting_logger_twice_fails() { - let cfg = ConfigurationProxy::default() - .build() - .expect("Default logger config always builds"); + let cfg = Config::default(); let first = init_global(&cfg, false); assert!(first.is_ok()); diff --git a/p2p/tests/integration/p2p.rs b/p2p/tests/integration/p2p.rs index 61acb9ddaa0..a1b688231e0 100644 --- a/p2p/tests/integration/p2p.rs +++ b/p2p/tests/integration/p2p.rs @@ -3,15 +3,14 @@ use std::{ fmt::Debug, sync::{ atomic::{AtomicU32, Ordering}, - Arc, Once, + Arc, }, }; use futures::{prelude::*, stream::FuturesUnordered, task::AtomicWaker}; -use iroha_config_base::proxy::Builder; use iroha_crypto::KeyPair; use iroha_data_model::prelude::PeerId; -use iroha_logger::{prelude::*, ConfigurationProxy}; +use iroha_logger::{prelude::*, test_logger}; use iroha_p2p::{network::message::*, NetworkHandle}; use iroha_primitives::addr::socket_addr; use parity_scale_codec::{Decode, Encode}; @@ -24,16 +23,7 @@ use tokio::{ struct TestMessage(String); fn setup_logger() { - static INIT: Once = Once::new(); - - INIT.call_once(|| { - let mut config = ConfigurationProxy::default() - .build() - .expect("Default logger config failed to build. This is a programmer error"); - config.level = iroha_logger::Level::TRACE; - config.format = iroha_logger::Format::Pretty; - iroha_logger::init_global(&config, true).unwrap(); - }) + test_logger(); } /// This test creates a network and one peer. diff --git a/tools/kagami/src/genesis.rs b/tools/kagami/src/genesis.rs index f97d4cb4b92..71c94ccae09 100644 --- a/tools/kagami/src/genesis.rs +++ b/tools/kagami/src/genesis.rs @@ -191,7 +191,7 @@ pub fn generate_default(executor: ExecutorMode) -> color_eyre::Result for ResolvedImageSource { #[cfg(test)] mod tests { use std::{ - cell::RefCell, collections::{BTreeMap, BTreeSet, HashMap, HashSet}, - env::VarError, - ffi::OsStr, path::{Path, PathBuf}, str::FromStr, }; - use color_eyre::eyre::Context; use iroha_config::{ - base::proxy::{FetchEnv, LoadFromEnv, Override}, - iroha::ConfigurationProxy, + base::TestEnv, + parameters::user_layer::RootPartial, // iroha::ConfigurationProxy, + }; + use iroha_config::{ + base::{FromEnv, ReadEnv, UnwrapPartial}, + parameters::user_layer::CliContext, }; use iroha_crypto::{KeyGenConfiguration, KeyPair}; - use iroha_primitives::addr::SocketAddr; + use iroha_primitives::addr::{socket_addr, SocketAddr}; use path_absolutize::Absolutize; use super::*; @@ -603,34 +603,12 @@ mod tests { } } - #[derive(Debug)] - struct TestEnv { - env: HashMap, - /// Set of env variables that weren't fetched yet - untouched: RefCell>, - } - impl From for TestEnv { fn from(peer_env: FullPeerEnv) -> Self { let json = serde_json::to_string(&peer_env).expect("Must be serializable"); - let env: HashMap<_, serde_json::Value> = + let env: HashMap<_, String> = serde_json::from_str(&json).expect("Must be deserializable into a hash map"); - let untouched = env.keys().cloned().collect(); - Self { - env: env - .into_iter() - .map(|(k, v)| { - let s = if let serde_json::Value::String(s) = v { - s - } else { - v.to_string() - }; - - (k, s) - }) - .collect(), - untouched: RefCell::new(untouched), - } + Self::with_map(env) } } @@ -641,29 +619,6 @@ mod tests { } } - impl FetchEnv for TestEnv { - fn fetch>(&self, key: K) -> Result { - let key_str = key - .as_ref() - .to_str() - .ok_or_else(|| VarError::NotUnicode(key.as_ref().into()))?; - - let res = self.env.get(key_str).ok_or(VarError::NotPresent).cloned(); - - if res.is_ok() { - self.untouched.borrow_mut().remove(key_str); - } - - res - } - } - - impl TestEnv { - fn assert_everything_covered(&self) { - assert_eq!(*self.untouched.borrow(), HashSet::new()); - } - } - #[test] fn default_config_with_swarm_env_is_exhaustive() { let keypair = KeyPair::generate(); @@ -672,23 +627,28 @@ mod tests { key_pair: keypair.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(), + p2p_addr: socket_addr!(127.0.0.1:1337), + api_addr: socket_addr!(127.0.0.1:1338), trusted_peers: BTreeSet::new(), } .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")); - - let _cfg = proxy - .build() - .wrap_err("Failed to build configuration") - .expect("Default configuration with swarm's env should be exhaustive"); + let _ = env + .get("IROHA_CONFIG") + .expect("never occurs") + .expect("should be presented"); + + let _cfg = RootPartial::from_env(&env) + .expect("valid env") + .unwrap_partial() + .expect("should not fail as input has all required fields") + .parse(CliContext { + submit_genesis: true, + }) + .expect("should not fail as input is valid"); - env.assert_everything_covered(); + assert_eq!(env.unvisited(), HashSet::new()); } #[test] From 1e60a931b53efc546fb4a3de2ad3b95d058720cd Mon Sep 17 00:00:00 2001 From: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> Date: Tue, 23 Jan 2024 09:26:31 +0900 Subject: [PATCH 17/94] [fix]: chores Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- cli/src/lib.rs | 4 ++-- client/tests/integration/domain_owner.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cli/src/lib.rs b/cli/src/lib.rs index e1d3703367b..c82d479fee9 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -249,8 +249,8 @@ impl Iroha { let start_args = SumeragiStartArgs { chain_id: config.iroha.chain_id.clone(), - sumeragi_config: config.sumeragi.clone(), - iroha_config: &config.iroha, + sumeragi_config: Box::new(config.sumeragi.clone()), + iroha_config: Box::new(config.iroha.clone()), events_sender: events_sender.clone(), wsv, queue: Arc::clone(&queue), diff --git a/client/tests/integration/domain_owner.rs b/client/tests/integration/domain_owner.rs index 13d9bde6d29..d2901928317 100644 --- a/client/tests/integration/domain_owner.rs +++ b/client/tests/integration/domain_owner.rs @@ -8,7 +8,7 @@ use test_network::*; #[test] fn domain_owner_domain_permissions() -> Result<()> { - let chain_id = ChainId::new("0"); + let chain_id = ChainId::from("0"); let (_rt, _peer, test_client) = ::new().with_port(11_080).start_with_runtime(); wait_for_genesis_committed(&[test_client.clone()], 0); From c7cf027165578432a9e423fefc111e7ba78e0e17 Mon Sep 17 00:00:00 2001 From: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> Date: Thu, 25 Jan 2024 07:26:10 +0900 Subject: [PATCH 18/94] [refactor]: pass tests - update signature of `PeerId::new` (avoid extra clone) - fix `UserField::set` - fix deps of `iroha_config_base` - resolve runtime todos in `iroha_config` - update `iroha_swarm` generation output - some other refactoring Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- Cargo.lock | 1 + cli/src/lib.rs | 8 +- cli/src/main.rs | 12 +- config/base/Cargo.toml | 4 +- config/base/src/lib.rs | 11 +- config/iroha_test_config.json | 123 ------------- config/iroha_test_config.toml | 35 ++++ config/src/parameters/actual.rs | 38 +++- config/src/parameters/defaults.rs | 4 +- config/src/parameters/user_layer.rs | 86 +++++---- config/tests/fixtures.rs | 118 ++++++++++-- config/tests/fixtures/bad.extra_fields.toml | 4 + config/tests/fixtures/bad.missing_fields.toml | 1 + .../tests/fixtures/bad.multiple-bad-envs.env | 6 + .../fixtures/bad.torii_addr_eq_p2p_addr.toml | 17 ++ config/tests/fixtures/config_and_env.toml | 12 -- config/tests/fixtures/extra_fields.toml | 3 - config/tests/fixtures/full.env | 1 + .../genesis_with_unexisting_executor.json | 4 - .../tests/fixtures/inconsistent_genesis.toml | 16 +- .../fixtures/minimal_alone_with_genesis.toml | 15 ++ ...g_and_env.env => minimal_file_and_env.env} | 0 ..._config.toml => minimal_file_and_env.toml} | 8 +- .../fixtures/minimal_with_trusted_peers.toml | 17 ++ config/tests/fixtures/missing_fields.toml | 0 config/tests/fixtures/with_genesis.toml | 18 -- core/src/kiso.rs | 21 ++- core/src/queue.rs | 32 ++-- core/src/sumeragi/network_topology.rs | 2 +- p2p/src/network.rs | 2 +- tools/kagami/src/genesis.rs | 4 +- tools/swarm/Cargo.toml | 1 + tools/swarm/src/compose.rs | 168 +++++++++++------- 33 files changed, 455 insertions(+), 337 deletions(-) delete mode 100644 config/iroha_test_config.json create mode 100644 config/iroha_test_config.toml create mode 100644 config/tests/fixtures/bad.extra_fields.toml create mode 100644 config/tests/fixtures/bad.missing_fields.toml create mode 100644 config/tests/fixtures/bad.multiple-bad-envs.env create mode 100644 config/tests/fixtures/bad.torii_addr_eq_p2p_addr.toml delete mode 100644 config/tests/fixtures/config_and_env.toml delete mode 100644 config/tests/fixtures/extra_fields.toml delete mode 100644 config/tests/fixtures/genesis_with_unexisting_executor.json create mode 100644 config/tests/fixtures/minimal_alone_with_genesis.toml rename config/tests/fixtures/{config_and_env.env => minimal_file_and_env.env} (100%) rename config/tests/fixtures/{minimal_config.toml => minimal_file_and_env.toml} (68%) create mode 100644 config/tests/fixtures/minimal_with_trusted_peers.toml delete mode 100644 config/tests/fixtures/missing_fields.toml delete mode 100644 config/tests/fixtures/with_genesis.toml diff --git a/Cargo.lock b/Cargo.lock index 92cac09c3c1..5a657414f6e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3206,6 +3206,7 @@ dependencies = [ "color-eyre", "derive_more", "expect-test", + "hex", "inquire", "iroha_config", "iroha_crypto", diff --git a/cli/src/lib.rs b/cli/src/lib.rs index c82d479fee9..e7c48032e79 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -239,7 +239,7 @@ impl Iroha { }, ); - let queue = Arc::new(Queue::from_configuration(&config.queue)); + let queue = Arc::new(Queue::from_configuration(config.queue)); match Self::start_telemetry(&logger, &config).await? { TelemetryStartStatus::Started => iroha_logger::info!("Telemetry started"), TelemetryStartStatus::NotStarted => iroha_logger::warn!("Telemetry not started"), @@ -607,7 +607,7 @@ mod tests { cfg.kura.block_store_path.set("../storage".into()); cfg.snapshot.store_path.set("../snapshots".into()); cfg.telemetry.dev.file.set("../logs/telemetry".into()); - cfg + toml::Value::try_from(cfg)? }; let dir = tempfile::tempdir()?; @@ -662,7 +662,7 @@ mod tests { let config = { let mut cfg = config_factory(); cfg.genesis.file.set("./genesis.json".into()); - cfg + toml::Value::try_from(cfg)? }; let dir = tempfile::tempdir()?; @@ -676,7 +676,7 @@ mod tests { let report = read_config_and_genesis(&config_path, false).unwrap_err(); assert_contains!( - format!("{report}"), + format!("{report:#}"), "The network consists from this one peer only" ); diff --git a/cli/src/main.rs b/cli/src/main.rs index 549f6ec6713..8524155c21c 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -90,8 +90,6 @@ async fn main() -> Result<()> { #[cfg(test)] mod tests { - use assertables::{assert_contains, assert_contains_as_result}; - use super::*; #[test] @@ -135,12 +133,8 @@ mod tests { } #[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`"); + fn user_can_provide_any_extension() { + let _args = Args::try_parse_from(["test", "--config", "file.toml.but.not"]) + .expect("should allow doing this as well"); } } diff --git a/config/base/Cargo.toml b/config/base/Cargo.toml index ae2fda8ccc7..0da79550ff1 100644 --- a/config/base/Cargo.toml +++ b/config/base/Cargo.toml @@ -13,7 +13,7 @@ workspace = true [dependencies] merge = "0.1.0" drop_bomb = { workspace = true } -derive_more = { workspace = true } +derive_more = { workspace = true, features = ["from", "deref", "deref_mut"] } eyre = { workspace = true } -serde = { workspace = true } +serde = { workspace = true, features = ["derive"] } thiserror = { workspace = true } diff --git a/config/base/src/lib.rs b/config/base/src/lib.rs index 57cbd1581d9..8eb4598a0b0 100644 --- a/config/base/src/lib.rs +++ b/config/base/src/lib.rs @@ -371,13 +371,13 @@ impl Merge for UserField { } } -impl UserField { +impl UserField { pub fn get(self) -> Option { self.0 } pub fn set(&mut self, value: T) { - self.0.as_mut().map(|x| *x = value); + self.0 = Some(value); } } @@ -408,7 +408,7 @@ mod tests { let err = emitter.finish().unwrap_err(); - assert_eq!(format!("{err}"), "Missing field: foo") + assert_eq!(format!("{err}"), "missing field: `foo`") } #[test] @@ -420,7 +420,10 @@ mod tests { let err = emitter.finish().unwrap_err(); - assert_eq!(format!("{err}"), "Missing field: foo\nMissing field: bar") + assert_eq!( + format!("{err}"), + "missing field: `foo`\nmissing field: `bar`" + ) } #[test] diff --git a/config/iroha_test_config.json b/config/iroha_test_config.json deleted file mode 100644 index 53339579831..00000000000 --- a/config/iroha_test_config.json +++ /dev/null @@ -1,123 +0,0 @@ -{ - "CHAIN_ID": "00000000-0000-0000-0000-000000000000", - "PUBLIC_KEY": "ed01201C61FAF8FE94E253B93114240394F79A607B7FA55F9E5A41EBEC74B88055768B", - "PRIVATE_KEY": { - "digest_function": "ed25519", - "payload": "282ED9F3CF92811C3818DBC4AE594ED59DC1A2F78E4241E31924E101D6B1FB831C61FAF8FE94E253B93114240394F79A607B7FA55F9E5A41EBEC74B88055768B" - }, - "KURA": { - "INIT_MODE": "strict", - "BLOCK_STORE_PATH": "./storage", - "BLOCKS_PER_STORAGE_FILE": 1000, - "ACTOR_CHANNEL_CAPACITY": 100, - "DEBUG_OUTPUT_NEW_BLOCKS": false - }, - "SUMERAGI": { - "BLOCK_TIME_MS": 1000, - "TRUSTED_PEERS": [ - { - "address": "127.0.0.1:1337", - "public_key": "ed01201C61FAF8FE94E253B93114240394F79A607B7FA55F9E5A41EBEC74B88055768B" - }, - { - "address": "127.0.0.1:1338", - "public_key": "ed0120CC25624D62896D3A0BFD8940F928DC2ABF27CC57CEFEB442AA96D9081AAE58A1" - }, - { - "address": "127.0.0.1:1339", - "public_key": "ed0120FACA9E8AA83225CB4D16D67F27DD4F93FC30FFA11ADC1F5C88FD5495ECC91020" - }, - { - "address": "127.0.0.1:1340", - "public_key": "ed01208E351A70B6A603ED285D666B8D689B680865913BA03CE29FB7D13A166C4E7F1F" - } - ], - "COMMIT_TIME_LIMIT_MS": 2000, - "MAX_TRANSACTIONS_IN_BLOCK": 8192, - "ACTOR_CHANNEL_CAPACITY": 100, - "GOSSIP_BATCH_SIZE": 500, - "GOSSIP_PERIOD_MS": 1000, - "DEBUG_FORCE_SOFT_FORK": false - }, - "TORII": { - "P2P_ADDR": "127.0.0.1:1337", - "API_URL": "127.0.0.1:8080", - "MAX_TRANSACTION_SIZE": 32768, - "MAX_CONTENT_LEN": 16384000 - }, - "BLOCK_SYNC": { - "GOSSIP_PERIOD_MS": 10000, - "BLOCK_BATCH_SIZE": 4, - "ACTOR_CHANNEL_CAPACITY": 100 - }, - "QUEUE": { - "MAX_TRANSACTIONS_IN_QUEUE": 65536, - "MAX_TRANSACTIONS_IN_QUEUE_PER_USER": 65536, - "TRANSACTION_TIME_TO_LIVE_MS": 86400000, - "FUTURE_THRESHOLD_MS": 1000 - }, - "LOGGER": { - "LEVEL": "INFO", - "FORMAT": "full", - "TOKIO_CONSOLE_ADDR": "127.0.0.1:5555" - }, - "GENESIS": { - "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, - "FILE": "./genesis.json" - }, - "WSV": { - "ASSET_METADATA_LIMITS": { - "max_len": 1048576, - "max_entry_byte_size": 4096 - }, - "ASSET_DEFINITION_METADATA_LIMITS": { - "max_len": 1048576, - "max_entry_byte_size": 4096 - }, - "ACCOUNT_METADATA_LIMITS": { - "max_len": 1048576, - "max_entry_byte_size": 4096 - }, - "DOMAIN_METADATA_LIMITS": { - "max_len": 1048576, - "max_entry_byte_size": 4096 - }, - "IDENT_LENGTH_LIMITS": { - "min": 1, - "max": 128 - }, - "TRANSACTION_LIMITS": { - "max_instruction_number": 4096, - "max_wasm_size_bytes": 4194304 - }, - "WASM_RUNTIME_CONFIG": { - "FUEL_LIMIT": 1000000, - "MAX_MEMORY": 524288000 - } - }, - "NETWORK": { - "ACTOR_CHANNEL_CAPACITY": 100 - }, - "TELEMETRY": { - "NAME": null, - "URL": null, - "MIN_RETRY_PERIOD": 1, - "MAX_RETRY_DELAY_EXPONENT": 4, - "FILE": null - }, - "SNAPSHOT": { - "CREATE_EVERY_MS": 60000, - "DIR_PATH": "./storage", - "CREATION_ENABLED": true - }, - "LIVE_QUERY_STORE": { - "QUERY_IDLE_TIME_MS": 30000 - } -} diff --git a/config/iroha_test_config.toml b/config/iroha_test_config.toml new file mode 100644 index 00000000000..492a47c7817 --- /dev/null +++ b/config/iroha_test_config.toml @@ -0,0 +1,35 @@ +[iroha] +chain_id = "00000000-0000-0000-0000-000000000000" +public_key = "ed01201C61FAF8FE94E253B93114240394F79A607B7FA55F9E5A41EBEC74B88055768B" +private_key.digest_function = "ed25519" +private_key.payload = "282ED9F3CF92811C3818DBC4AE594ED59DC1A2F78E4241E31924E101D6B1FB831C61FAF8FE94E253B93114240394F79A607B7FA55F9E5A41EBEC74B88055768B" +p2p_address = "127.0.0.1:1337" + +[genesis] +public_key = "ed01204CFFD0EE429B1BDD36B3910EC570852B8BB63F18750341772FB46BC856C5CAAF" +file = "./genesis.json" +private_key.digest_function = "ed25519" +private_key.payload = "D748E18CE60CB30DEA3E73C9019B7AF45A8D465E3D71BCC9A5EF99A008205E534CFFD0EE429B1BDD36B3910EC570852B8BB63F18750341772FB46BC856C5CAAF" + +[torii] +address = "127.0.0.1:8080" + +[[sumeragi.trusted_peers]] +address = "127.0.0.1:1337" +public_key = "ed01201C61FAF8FE94E253B93114240394F79A607B7FA55F9E5A41EBEC74B88055768B" + +[[sumeragi.trusted_peers]] +address = "127.0.0.1:1338" +public_key = "ed0120CC25624D62896D3A0BFD8940F928DC2ABF27CC57CEFEB442AA96D9081AAE58A1" + +[[sumeragi.trusted_peers]] +address = "127.0.0.1:1339" +public_key = "ed0120FACA9E8AA83225CB4D16D67F27DD4F93FC30FFA11ADC1F5C88FD5495ECC91020" + +[[sumeragi.trusted_peers]] +address = "127.0.0.1:1340" +public_key = "ed01208E351A70B6A603ED285D666B8D689B680865913BA03CE29FB7D13A166C4E7F1F" + +[logger] +format = "pretty" + diff --git a/config/src/parameters/actual.rs b/config/src/parameters/actual.rs index d4cddcd6d06..e9d42cf7e1f 100644 --- a/config/src/parameters/actual.rs +++ b/config/src/parameters/actual.rs @@ -15,7 +15,11 @@ use iroha_primitives::{addr::SocketAddr, unique_vec::UniqueVec}; use serde::{Deserialize, Serialize}; use url::Url; -use crate::{kura::Mode, logger::Format, parameters::user_layer}; +use crate::{ + kura::Mode, + logger::Format, + parameters::{defaults, user_layer}, +}; #[derive(Debug, Clone)] pub struct Root { @@ -44,7 +48,7 @@ pub struct Iroha { impl Iroha { pub fn peer_id(&self) -> PeerId { - PeerId::new(&self.p2p_address, self.key_pair.public_key()) + PeerId::new(self.p2p_address.clone(), self.key_pair.public_key().clone()) } } @@ -89,7 +93,13 @@ pub struct Kura { impl Default for Queue { fn default() -> Self { - todo!() + Self { + transaction_time_to_live: defaults::queue::DEFAULT_TRANSACTION_TIME_TO_LIVE, + future_threshold: defaults::queue::DEFAULT_FUTURE_THRESHOLD, + max_transactions_in_queue: defaults::queue::DEFAULT_MAX_TRANSACTIONS_IN_QUEUE, + max_transactions_in_queue_per_user: + defaults::queue::DEFAULT_MAX_TRANSACTIONS_IN_QUEUE_PER_USER, + } } } @@ -108,7 +118,9 @@ pub struct LiveQueryStore { impl Default for LiveQueryStore { fn default() -> Self { - todo!() + Self { + query_idle_time: defaults::torii::DEFAULT_QUERY_IDLE_TIME, + } } } @@ -146,7 +158,18 @@ impl ChainWide { impl Default for ChainWide { fn default() -> Self { - todo!() + Self { + max_transactions_in_block: defaults::chain_wide::DEFAULT_MAX_TXS, + block_time: defaults::chain_wide::DEFAULT_BLOCK_TIME, + commit_time: defaults::chain_wide::DEFAULT_COMMIT_TIME, + transaction_limits: defaults::chain_wide::DEFAULT_TRANSACTION_LIMITS, + domain_metadata_limits: defaults::chain_wide::DEFAULT_METADATA_LIMITS, + account_metadata_limits: defaults::chain_wide::DEFAULT_METADATA_LIMITS, + asset_definition_metadata_limits: defaults::chain_wide::DEFAULT_METADATA_LIMITS, + asset_metadata_limits: defaults::chain_wide::DEFAULT_METADATA_LIMITS, + identifier_length_limits: defaults::chain_wide::DEFAULT_IDENT_LENGTH_LIMITS, + wasm_runtime: WasmRuntime::default(), + } } } @@ -158,7 +181,10 @@ pub struct WasmRuntime { impl Default for WasmRuntime { fn default() -> Self { - todo!() + Self { + fuel_limit: defaults::chain_wide::DEFAULT_WASM_FUEL_LIMIT, + max_memory: ByteSize(defaults::chain_wide::DEFAULT_WASM_MAX_MEMORY), + } } } diff --git a/config/src/parameters/defaults.rs b/config/src/parameters/defaults.rs index 44fb20ed643..82c0803ab9b 100644 --- a/config/src/parameters/defaults.rs +++ b/config/src/parameters/defaults.rs @@ -56,13 +56,13 @@ pub mod chain_wide { pub const DEFAULT_MAX_TXS: NonZeroU32 = nonzero!(2_u32.pow(9)); pub const DEFAULT_BLOCK_TIME: Duration = Duration::from_secs(2); - pub const DEFAULT_COMMIT_TIME_LIMIT: Duration = Duration::from_secs(4); + pub const DEFAULT_COMMIT_TIME: Duration = Duration::from_secs(4); pub const DEFAULT_WASM_FUEL_LIMIT: u64 = 30_000_000; pub const DEFAULT_WASM_MAX_MEMORY: u32 = 500 * 2_u32.pow(20); /// Default estimation of consensus duration. pub const DEFAULT_CONSENSUS_ESTIMATION: Duration = - match DEFAULT_BLOCK_TIME.checked_add(match DEFAULT_COMMIT_TIME_LIMIT.checked_div(2) { + match DEFAULT_BLOCK_TIME.checked_add(match DEFAULT_COMMIT_TIME.checked_div(2) { Some(x) => x, None => unreachable!(), }) { diff --git a/config/src/parameters/user_layer.rs b/config/src/parameters/user_layer.rs index d612a20e1b0..8f42b97c43b 100644 --- a/config/src/parameters/user_layer.rs +++ b/config/src/parameters/user_layer.rs @@ -25,17 +25,14 @@ use iroha_primitives::{addr::SocketAddr, unique_vec::UniqueVec}; use serde::{Deserialize, Serialize}; use url::Url; -use super::defaults::{ - chain_wide::*, kura::*, logger::*, queue::*, snapshot::*, telemetry::*, torii::*, -}; use crate::{ kura::Mode, logger::Format, parameters::{ actual, - defaults::network::{ - DEFAULT_BLOCK_GOSSIP_PERIOD, DEFAULT_MAX_BLOCKS_PER_GOSSIP, - DEFAULT_MAX_TRANSACTIONS_PER_GOSSIP, DEFAULT_TRANSACTION_GOSSIP_PERIOD, + defaults::{ + chain_wide::*, kura::*, logger::*, network::*, queue::*, snapshot::*, telemetry::*, + torii::*, }, }, }; @@ -143,13 +140,26 @@ impl RootFull { let kura = self.kura.parse(); - let sumeragi = self.sumeragi.parse().map_or_else( - |err| { + let sumeragi = match self.sumeragi.parse() { + Ok(mut sumeragi) => { + if !cli.submit_genesis && sumeragi.trusted_peers.len() == 0 { + emitter.emit(eyre!("\ + The network consists from this one peer only (no `sumeragi.trusted_peers` provided). \ + 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.\ + ")); + None + } else { + Some(sumeragi) + } + } + Err(err) => { emitter.emit(err); None - }, - Some, - ); + } + }; let (block_sync, transaction_gossiper) = self.network.parse(); @@ -169,22 +179,24 @@ impl RootFull { let chain_wide = self.chain_wide.parse(); + if let Some(iroha) = &iroha { + if iroha.p2p_address == torii.address { + emitter.emit(eyre!( + "`iroha.p2p_address` and `torii.address` should not be the same" + )) + } + } + emitter.finish()?; let (regular_telemetry, dev_telemetry) = telemetries.unwrap(); let iroha = iroha.unwrap(); let genesis = genesis.unwrap(); - let sumeragi = sumeragi.unwrap(); - - if !cli.submit_genesis && sumeragi.trusted_peers.len() < 2 { - 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 sumeragi = { + let mut cfg = sumeragi.unwrap(); + cfg.trusted_peers.push(iroha.peer_id()); + cfg + }; // TODO: validate that p2p_address and torii.address are not the same @@ -541,15 +553,17 @@ impl FromEnv for GenesisPartial { impl GenesisFull { fn parse(self, cli: &CliContext) -> Result { - match (self.private_key, self.file) { - (None, None) => Ok(actual::Genesis::Partial { + match (self.private_key, self.file, cli.submit_genesis) { + (None, None, false) => Ok(actual::Genesis::Partial { public_key: self.public_key, }), - (Some(private_key), Some(file)) => Ok(actual::Genesis::Full { + (Some(private_key), Some(file), true) => Ok(actual::Genesis::Full { key_pair: KeyPair::new(self.public_key, private_key) .map_err(GenesisConfigError::from)?, file, }), + (Some(_), Some(_), false) => Err(GenesisConfigError::GenesisWithoutSubmit), + (None, None, true) => Err(GenesisConfigError::SubmitWithoutGenesis), _ => Err(GenesisConfigError::Inconsistent), } } @@ -557,6 +571,10 @@ impl GenesisFull { #[derive(Debug, displaydoc::Display, thiserror::Error)] pub enum GenesisConfigError { + /// `genesis.file` and `genesis.private_key` are presented, but `--submit-genesis` was not set + GenesisWithoutSubmit, + /// `--submit-genesis` was set, but `genesis.file` and `genesis.private_key` are not presented + SubmitWithoutGenesis, /// `genesis.file` and `genesis.private_key` should be set together Inconsistent, /// failed to construct the genesis's keypair using `genesis.public_key` and `genesis.private_key` configuration parameters @@ -1016,6 +1034,7 @@ pub struct TelemetryFull { } #[derive(Clone, Deserialize, Serialize, Debug, Default, Merge)] +#[serde(deny_unknown_fields, default)] pub struct TelemetryDevPartial { pub file: UserField, } @@ -1208,7 +1227,7 @@ impl UnwrapPartial for ChainWidePartial { .map_or(DEFAULT_BLOCK_TIME, UserDuration::get), commit_time: self .commit_time - .map_or(DEFAULT_COMMIT_TIME_LIMIT, UserDuration::get), + .map_or(DEFAULT_COMMIT_TIME, UserDuration::get), transaction_limits: self .transaction_limits .unwrap_or(DEFAULT_TRANSACTION_LIMITS), @@ -1354,7 +1373,11 @@ impl FromEnv for ToriiPartial { mod tests { use iroha_config_base::{FromEnv, TestEnv}; - use crate::parameters::user_layer::{IrohaPartial, RootPartial}; + use super::*; + use crate::parameters::{ + actual::{Logger, Sumeragi}, + user_layer::{IrohaPartial, RootPartial, SnapshotPartial}, + }; #[test] fn parses_private_key_from_env() { @@ -1426,12 +1449,11 @@ mod tests { #[test] fn deserialize_iroha_namespace_with_not_all_fields_works() { - let _layer: RootPartial = toml::from_str( - r#" + let _layer: RootPartial = toml::toml! { [iroha] p2p_address = "127.0.0.1:8080" - "#, - ) - .unwrap(); + } + .try_into() + .expect("should not fail when not all fields in `iroha` are presented at a time"); } } diff --git a/config/tests/fixtures.rs b/config/tests/fixtures.rs index ee1eb6d51e8..cca2cb84e0b 100644 --- a/config/tests/fixtures.rs +++ b/config/tests/fixtures.rs @@ -39,7 +39,7 @@ fn test_env_from_file(p: impl AsRef) -> TestEnv { /// it also gives an insight into every single default value #[test] fn minimal_config_snapshot() -> Result<()> { - let config = RootPartial::from_toml(fixtures_dir().join("minimal_config.toml"))? + let config = RootPartial::from_toml(fixtures_dir().join("minimal_with_trusted_peers.toml"))? .unwrap_partial()? .parse(CliContext { submit_genesis: false, @@ -48,6 +48,9 @@ fn minimal_config_snapshot() -> Result<()> { let expected = expect_test::expect![[r#" Root { iroha: Iroha { + chain_id: ChainId( + "0", + ), key_pair: KeyPair { public_key: {digest: ed25519, payload: ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB}, private_key: {digest: ed25519, payload: 8F4C15E5D664DA3F13778801D23D4E89B76E94C1B94B389544168B6CB894F84F8BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB}, @@ -70,7 +73,12 @@ fn minimal_config_snapshot() -> Result<()> { }, sumeragi: Sumeragi { trusted_peers: UniqueVec( - [], + [ + PeerId { + address: 127.0.0.1:1338, + public_key: {digest: ed25519, payload: ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB}, + }, + ], ), debug_force_soft_fork: false, }, @@ -145,21 +153,72 @@ fn minimal_config_snapshot() -> Result<()> { #[test] fn config_with_genesis() -> Result<()> { - let _config = RootPartial::from_toml(fixtures_dir().join("with_genesis.toml"))? + let _config = RootPartial::from_toml(fixtures_dir().join("minimal_alone_with_genesis.toml"))? + .unwrap_partial()? + .parse(CliContext { + submit_genesis: true, + })?; + Ok(()) +} + +#[test] +fn minimal_with_genesis_but_no_cli_arg_fails() -> Result<()> { + let error = RootPartial::from_toml(fixtures_dir().join("minimal_alone_with_genesis.toml"))? .unwrap_partial()? .parse(CliContext { submit_genesis: false, + }) + .expect_err("should fail since `--submit-genesis=false`"); + + let expected = expect_test::expect![[r#" + `genesis.file` and `genesis.private_key` are presented, but `--submit-genesis` was not set + The network consists from this one peer only (no `sumeragi.trusted_peers` provided). 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."#]]; + expected.assert_eq(&format!("{error:#}")); + + Ok(()) +} + +#[test] +fn minimal_without_genesis_but_with_submit_fails() -> Result<()> { + let error = RootPartial::from_toml(fixtures_dir().join("minimal_with_trusted_peers.toml"))? + .unwrap_partial()? + .parse(CliContext { + submit_genesis: true, + }) + .expect_err( + "should fail since there is no genesis in the config, but `--submit-genesis=true`", + ); + + let expected = expect_test::expect!["`--submit-genesis` was set, but `genesis.file` and `genesis.private_key` are not presented"]; + expected.assert_eq(&format!("{error:#}")); + + Ok(()) +} + +#[test] +fn self_is_presented_in_trusted_peers() -> Result<()> { + let config = RootPartial::from_toml(fixtures_dir().join("minimal_alone_with_genesis.toml"))? + .unwrap_partial()? + .parse(CliContext { + submit_genesis: true, })?; + + assert!(config + .sumeragi + .trusted_peers + .contains(&config.iroha.peer_id())); + Ok(()) } #[test] fn missing_fields() -> Result<()> { - let error = RootPartial::from_toml(fixtures_dir().join("missing_fields.toml"))? + let error = RootPartial::from_toml(fixtures_dir().join("bad.missing_fields.toml"))? .unwrap_partial() .expect_err("should fail with missing fields"); let expected = expect_test::expect![[r#" + missing field: `iroha.chain_id` missing field: `iroha.public_key` missing field: `iroha.private_key` missing field: `iroha.p2p_address` @@ -175,13 +234,7 @@ fn extra_fields() { let error = RootPartial::from_toml(fixtures_dir().join("extra_fields.toml")) .expect_err("should fail with extra fields"); - let expected = expect_test::expect![[r#" - failed to parse toml: TOML parse error at line 1, column 1 - | - 1 | i_am_unknown = true - | ^^^^^^^^^^^^ - unknown field `i_am_unknown`, expected one of `iroha`, `genesis`, `kura`, `sumeragi`, `network`, `logger`, `queue`, `snapshot`, `telemetry`, `torii`, `chain_wide` - "#]]; + let expected = expect_test::expect!["cannot open file at location `tests/fixtures/extra_fields.toml`: No such file or directory (os error 2)"]; expected.assert_eq(&format!("{error:#}")); } @@ -195,8 +248,9 @@ fn inconsistent_genesis_config() -> Result<()> { }) .expect_err("should fail with bad genesis config"); - let expected = - expect_test::expect!["`genesis.file` and `genesis.private_key` should be set together"]; + let expected = expect_test::expect![[r#" + `genesis.file` and `genesis.private_key` should be set together + The network consists from this one peer only (no `sumeragi.trusted_peers` provided). 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."#]]; expected.assert_eq(&format!("{error:#}")); Ok(()) @@ -215,6 +269,11 @@ fn full_envs_set_is_consumed() -> Result<()> { let expected = expect_test::expect![[r#" RootPartial { iroha: IrohaPartial { + chain_id: Some( + ChainId( + "0-0", + ), + ), public_key: Some( {digest: ed25519, payload: ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB}, ), @@ -320,16 +379,25 @@ fn full_envs_set_is_consumed() -> Result<()> { } #[test] -#[ignore] fn multiple_env_parsing_errors() { - todo!("put invalid data into multiple ENV variables in different modules and check the error report") + let env = test_env_from_file(fixtures_dir().join("bad.multiple-bad-envs.env")); + + let error = RootPartial::from_env(&env).expect_err("the input from env is invalid"); + + let expected = expect_test::expect![[r#" + `PRIVATE_KEY_PAYLOAD` env was provided, but `PRIVATE_KEY_DIGEST` was not + failed to parse `genesis.private_key.digest_function` field from `GENESIS_PRIVATE_KEY_DIGEST` env variable + failed to parse `kura.debug.output_new_blocks` field from `KURA_DEBUG_OUTPUT_NEW_BLOCKS` env variable + failed to parse `logger.format` field from `LOG_FORMAT` env variable + failed to parse `torii.address` field from `API_ADDRESS` env variable"#]]; + expected.assert_eq(&format!("{error:#}")); } #[test] fn config_from_file_and_env() -> Result<()> { - let env = test_env_from_file(fixtures_dir().join("config_and_env.env")); + let env = test_env_from_file(fixtures_dir().join("minimal_file_and_env.env")); - let _config = RootPartial::from_toml(fixtures_dir().join("config_and_env.toml"))? + let _config = RootPartial::from_toml(fixtures_dir().join("minimal_file_and_env.toml"))? .merge(RootPartial::from_env(&env)?) .unwrap_partial()? .parse(CliContext { @@ -338,3 +406,19 @@ fn config_from_file_and_env() -> Result<()> { Ok(()) } + +#[test] +fn fail_if_torii_address_and_p2p_address_are_equal() -> Result<()> { + let error = RootPartial::from_toml(fixtures_dir().join("bad.torii_addr_eq_p2p_addr.toml"))? + .unwrap_partial() + .expect("should not fail, all fields are present") + .parse(CliContext { + submit_genesis: false, + }) + .expect_err("should fail because of bad input"); + + let expected = expect_test::expect!["`iroha.p2p_address` and `torii.address` should not be the same"]; + expected.assert_eq(&format!("{error:#}")); + + Ok(()) +} diff --git a/config/tests/fixtures/bad.extra_fields.toml b/config/tests/fixtures/bad.extra_fields.toml new file mode 100644 index 00000000000..bc2baaf8783 --- /dev/null +++ b/config/tests/fixtures/bad.extra_fields.toml @@ -0,0 +1,4 @@ +# Iroha should not silently ignore extra fields +i_am_unknown = true +foo = false +bar = 0.5 \ No newline at end of file diff --git a/config/tests/fixtures/bad.missing_fields.toml b/config/tests/fixtures/bad.missing_fields.toml new file mode 100644 index 00000000000..d5bd33cac2e --- /dev/null +++ b/config/tests/fixtures/bad.missing_fields.toml @@ -0,0 +1 @@ +# all fields are missing \ No newline at end of file diff --git a/config/tests/fixtures/bad.multiple-bad-envs.env b/config/tests/fixtures/bad.multiple-bad-envs.env new file mode 100644 index 00000000000..12ab82cf92e --- /dev/null +++ b/config/tests/fixtures/bad.multiple-bad-envs.env @@ -0,0 +1,6 @@ +PRIVATE_KEY_PAYLOAD=8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb +GENESIS_PRIVATE_KEY_DIGEST=BAD BAD BAD +GENESIS_PRIVATE_KEY_PAYLOAD=8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb +API_ADDRESS=BAD BAD BAD +KURA_DEBUG_OUTPUT_NEW_BLOCKS=TrueЪ +LOG_FORMAT=what format? diff --git a/config/tests/fixtures/bad.torii_addr_eq_p2p_addr.toml b/config/tests/fixtures/bad.torii_addr_eq_p2p_addr.toml new file mode 100644 index 00000000000..21587e2aca3 --- /dev/null +++ b/config/tests/fixtures/bad.torii_addr_eq_p2p_addr.toml @@ -0,0 +1,17 @@ +[iroha] +chain_id = "0" +public_key = "ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB" +private_key.digest_function = "ed25519" +private_key.payload = "8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb" +# same as `torii.address` +p2p_address = "127.0.0.1:8080" + +[genesis] +public_key = "ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB" + +[torii] +address = "127.0.0.1:8080" + +[[sumeragi.trusted_peers]] +address = "127.0.0.1:1338" +public_key = "ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB" diff --git a/config/tests/fixtures/config_and_env.toml b/config/tests/fixtures/config_and_env.toml deleted file mode 100644 index 0fcc69fa88e..00000000000 --- a/config/tests/fixtures/config_and_env.toml +++ /dev/null @@ -1,12 +0,0 @@ -[iroha] -public_key = "ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB" -p2p_address = "127.0.0.1:1337" - -[iroha.private_key] -digest_function = "ed25519" -payload = "8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb" - -[genesis] -public_key = "ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB" - -# `torii.address` should be in ENV \ No newline at end of file diff --git a/config/tests/fixtures/extra_fields.toml b/config/tests/fixtures/extra_fields.toml deleted file mode 100644 index faf88829e30..00000000000 --- a/config/tests/fixtures/extra_fields.toml +++ /dev/null @@ -1,3 +0,0 @@ -i_am_unknown = true -foo = false -bar = 0.5 \ No newline at end of file diff --git a/config/tests/fixtures/full.env b/config/tests/fixtures/full.env index 6f81e8cdd8a..fe21b9dd8ee 100644 --- a/config/tests/fixtures/full.env +++ b/config/tests/fixtures/full.env @@ -1,3 +1,4 @@ +CHAIN_ID=0-0 PUBLIC_KEY=ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB PRIVATE_KEY_DIGEST=ed25519 PRIVATE_KEY_PAYLOAD=8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb diff --git a/config/tests/fixtures/genesis_with_unexisting_executor.json b/config/tests/fixtures/genesis_with_unexisting_executor.json deleted file mode 100644 index 7802fc241ae..00000000000 --- a/config/tests/fixtures/genesis_with_unexisting_executor.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "transactions": [], - "executor": "non_existing.wasm" -} \ No newline at end of file diff --git a/config/tests/fixtures/inconsistent_genesis.toml b/config/tests/fixtures/inconsistent_genesis.toml index 740c2a82b74..ce423f04fe2 100644 --- a/config/tests/fixtures/inconsistent_genesis.toml +++ b/config/tests/fixtures/inconsistent_genesis.toml @@ -1,18 +1,16 @@ [iroha] +chain_id = "0" public_key = "ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB" +private_key.digest_function = "ed25519" +private_key.payload = "8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb" p2p_address = "127.0.0.1:1337" -[iroha.private_key] -digest_function = "ed25519" -payload = "8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb" - [genesis] public_key = "ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB" - -# it should be also provided with a file -[genesis.private_key] -digest_function = "ed25519" -payload = "8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb" +private_key.digest_function = "ed25519" +private_key.payload = "8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb" +# should fail without it: +# file = ... [torii] address = "127.0.0.1:8080" \ No newline at end of file diff --git a/config/tests/fixtures/minimal_alone_with_genesis.toml b/config/tests/fixtures/minimal_alone_with_genesis.toml new file mode 100644 index 00000000000..590bf7c48ad --- /dev/null +++ b/config/tests/fixtures/minimal_alone_with_genesis.toml @@ -0,0 +1,15 @@ +[iroha] +chain_id = "0" +public_key = "ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB" +private_key.digest_function = "ed25519" +private_key.payload = "8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb" +p2p_address = "127.0.0.1:1337" + +[genesis] +public_key = "ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB" +file = "./empty_ok_genesis.json" +private_key.digest_function = "ed25519" +private_key.payload = "8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb" + +[torii] +address = "127.0.0.1:8080" \ No newline at end of file diff --git a/config/tests/fixtures/config_and_env.env b/config/tests/fixtures/minimal_file_and_env.env similarity index 100% rename from config/tests/fixtures/config_and_env.env rename to config/tests/fixtures/minimal_file_and_env.env diff --git a/config/tests/fixtures/minimal_config.toml b/config/tests/fixtures/minimal_file_and_env.toml similarity index 68% rename from config/tests/fixtures/minimal_config.toml rename to config/tests/fixtures/minimal_file_and_env.toml index 20783f81be9..23504540be3 100644 --- a/config/tests/fixtures/minimal_config.toml +++ b/config/tests/fixtures/minimal_file_and_env.toml @@ -1,4 +1,5 @@ [iroha] +chain_id = "0" public_key = "ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB" p2p_address = "127.0.0.1:1337" @@ -9,5 +10,8 @@ payload = "8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62 [genesis] public_key = "ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB" -[torii] -address = "127.0.0.1:8080" \ No newline at end of file +# `torii.address` should be in ENV + +[[sumeragi.trusted_peers]] +address = "127.0.0.1:1338" +public_key = "ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB" diff --git a/config/tests/fixtures/minimal_with_trusted_peers.toml b/config/tests/fixtures/minimal_with_trusted_peers.toml new file mode 100644 index 00000000000..49da4352211 --- /dev/null +++ b/config/tests/fixtures/minimal_with_trusted_peers.toml @@ -0,0 +1,17 @@ +[iroha] +chain_id = "0" +public_key = "ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB" +private_key.digest_function = "ed25519" +private_key.payload = "8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb" +p2p_address = "127.0.0.1:1337" + +[genesis] +public_key = "ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB" + +[torii] +address = "127.0.0.1:8080" + +# we have this so that config will be complete without a full genesis +[[sumeragi.trusted_peers]] +address = "127.0.0.1:1338" +public_key = "ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB" diff --git a/config/tests/fixtures/missing_fields.toml b/config/tests/fixtures/missing_fields.toml deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/config/tests/fixtures/with_genesis.toml b/config/tests/fixtures/with_genesis.toml deleted file mode 100644 index 3df699d8f79..00000000000 --- a/config/tests/fixtures/with_genesis.toml +++ /dev/null @@ -1,18 +0,0 @@ -[iroha] -public_key = "ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB" -p2p_address = "127.0.0.1:1337" - -[iroha.private_key] -digest_function = "ed25519" -payload = "8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb" - -[genesis] -public_key = "ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB" -file = "./empty_ok_genesis.json" - -[genesis.private_key] -digest_function = "ed25519" -payload = "8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb" - -[torii] -address = "127.0.0.1:8080" \ No newline at end of file diff --git a/core/src/kiso.rs b/core/src/kiso.rs index 35ed0713bc4..4c09e5e146c 100644 --- a/core/src/kiso.rs +++ b/core/src/kiso.rs @@ -158,13 +158,20 @@ mod tests { use super::*; fn test_config() -> Root { - todo!() - // // FIXME Specifying path here might break! Moreover, if the file is not found, - // // the error will say that `public_key` is missing! - // // Hopefully this will change: https://github.com/hyperledger/iroha/issues/2585 - // ConfigurationProxy::from_path("../config/iroha_test_config.json") - // .build() - // .unwrap() + use iroha_config::{ + base::UnwrapPartial, + parameters::user_layer::{CliContext, RootPartial}, + }; + + // FIXME Specifying path here might break! + RootPartial::from_toml("../config/iroha_test_config.toml") + .expect("test config should be valid (or it is a bug)") + .unwrap_partial() + .expect("test config should be exhaustive") + .parse(CliContext { + submit_genesis: true, + }) + .expect("test config should be valid") } #[tokio::test] diff --git a/core/src/queue.rs b/core/src/queue.rs index 8a22689f00a..c82357da70d 100644 --- a/core/src/queue.rs +++ b/core/src/queue.rs @@ -99,7 +99,7 @@ pub struct Failure { impl Queue { /// Makes queue from configuration - pub fn from_configuration(cfg: &Config) -> Self { + pub fn from_configuration(cfg: Config) -> Self { Self { tx_hashes: ArrayQueue::new(cfg.max_transactions_in_queue.get()), accepted_txs: DashMap::new(), @@ -445,7 +445,7 @@ mod tests { query_handle, )); - let queue = Queue::from_configuration(&config_factory()); + let queue = Queue::from_configuration(config_factory()); queue .push(accepted_tx("alice@wonderland", &key_pair), &wsv) @@ -465,7 +465,7 @@ mod tests { query_handle, )); - let queue = Queue::from_configuration(&Config { + let queue = Queue::from_configuration(Config { transaction_time_to_live: Duration::from_secs(100), max_transactions_in_queue, ..Config::default() @@ -513,7 +513,7 @@ mod tests { )) }; - let queue = Queue::from_configuration(&config_factory()); + let queue = Queue::from_configuration(config_factory()); let instructions: [InstructionBox; 0] = []; let tx = TransactionBuilder::new(chain_id.clone(), "alice@wonderland".parse().expect("Valid")) @@ -576,7 +576,10 @@ mod tests { kura, query_handle, )); - let queue = Queue::from_configuration(&config_factory()); + let queue = Queue::from_configuration(Config { + transaction_time_to_live: Duration::from_secs(100), + ..config_factory() + }); for _ in 0..5 { queue .push(accepted_tx("alice@wonderland", &alice_key), &wsv) @@ -600,7 +603,7 @@ mod tests { ); let tx = accepted_tx("alice@wonderland", &alice_key); wsv.transactions.insert(tx.as_ref().hash(), 1); - let queue = Queue::from_configuration(&config_factory()); + let queue = Queue::from_configuration(config_factory()); assert!(matches!( queue.push(tx, &wsv), Err(Failure { @@ -623,7 +626,7 @@ mod tests { query_handle, ); let tx = accepted_tx("alice@wonderland", &alice_key); - let queue = Queue::from_configuration(&config_factory()); + let queue = Queue::from_configuration(config_factory()); queue.push(tx.clone(), &wsv).unwrap(); wsv.transactions.insert(tx.as_ref().hash(), 1); assert_eq!( @@ -646,7 +649,10 @@ mod tests { kura, query_handle, )); - let queue = Queue::from_configuration(&config_factory()); + let queue = Queue::from_configuration(Config { + transaction_time_to_live: Duration::from_millis(200), + ..config_factory() + }); for _ in 0..(max_txs_in_block - 1) { queue .push(accepted_tx("alice@wonderland", &alice_key), &wsv) @@ -690,7 +696,7 @@ mod tests { kura, query_handle, )); - let queue = Queue::from_configuration(&config_factory()); + let queue = Queue::from_configuration(config_factory()); queue .push(accepted_tx("alice@wonderland", &alice_key), &wsv) .expect("Failed to push tx into queue"); @@ -724,7 +730,7 @@ mod tests { kura, query_handle, )); - let queue = Queue::from_configuration(&config_factory()); + let queue = Queue::from_configuration(config_factory()); let instructions = [Fail { message: "expired".to_owned(), }]; @@ -765,7 +771,7 @@ mod tests { query_handle, ); - let queue = Arc::new(Queue::from_configuration(&Config { + let queue = Arc::new(Queue::from_configuration(Config { transaction_time_to_live: Duration::from_secs(100), max_transactions_in_queue: 100_000_000.try_into().unwrap(), ..Config::default() @@ -838,7 +844,7 @@ mod tests { query_handle, )); - let queue = Queue::from_configuration(&Config { + let queue = Queue::from_configuration(Config { future_threshold, ..Config::default() }); @@ -900,7 +906,7 @@ mod tests { let query_handle = LiveQueryStore::test().start(); let mut wsv = WorldStateView::new(world, kura, query_handle); - let queue = Queue::from_configuration(&Config { + let queue = Queue::from_configuration(Config { transaction_time_to_live: Duration::from_secs(100), max_transactions_in_queue: 100.try_into().unwrap(), max_transactions_in_queue_per_user: 1.try_into().unwrap(), diff --git a/core/src/sumeragi/network_topology.rs b/core/src/sumeragi/network_topology.rs index 0d2def7260d..cb8f51089b5 100644 --- a/core/src/sumeragi/network_topology.rs +++ b/core/src/sumeragi/network_topology.rs @@ -285,7 +285,7 @@ macro_rules! test_peers { }}; ($($id:literal),+$(,)?: $key_pair_iter:expr) => { ::iroha_primitives::unique_vec![ - $(PeerId::new(([0, 0, 0, 0], $id).into(), $key_pair_iter.next().expect("Not enough key pairs").public_key().clone())),+ + $(PeerId::new((([0, 0, 0, 0], $id).into()), $key_pair_iter.next().expect("Not enough key pairs").public_key().clone())),+ ] }; } diff --git a/p2p/src/network.rs b/p2p/src/network.rs index 453e4d23ad4..c9ccb6dac07 100644 --- a/p2p/src/network.rs +++ b/p2p/src/network.rs @@ -351,7 +351,7 @@ impl NetworkBase { }; iroha_logger::debug!(listen_addr = %self.listen_addr, %peer.conn_id, "Disconnecting peer"); - let peer_id = PeerId::new(peer.p2p_addr, public_key); + let peer_id = PeerId::new(peer.p2p_addr, public_key.clone()); Self::remove_online_peer(&self.online_peers_sender, &peer_id); } diff --git a/tools/kagami/src/genesis.rs b/tools/kagami/src/genesis.rs index 71c94ccae09..5d582748607 100644 --- a/tools/kagami/src/genesis.rs +++ b/tools/kagami/src/genesis.rs @@ -2,7 +2,7 @@ use std::path::PathBuf; use clap::{ArgGroup, Parser, Subcommand}; use iroha_config::parameters::defaults::chain_wide::{ - DEFAULT_BLOCK_TIME, DEFAULT_COMMIT_TIME_LIMIT, DEFAULT_IDENT_LENGTH_LIMITS, DEFAULT_MAX_TXS, + DEFAULT_BLOCK_TIME, DEFAULT_COMMIT_TIME, DEFAULT_IDENT_LENGTH_LIMITS, DEFAULT_MAX_TXS, DEFAULT_METADATA_LIMITS, DEFAULT_TRANSACTION_LIMITS, DEFAULT_WASM_FUEL_LIMIT, DEFAULT_WASM_MAX_MEMORY, }; @@ -181,7 +181,7 @@ pub fn generate_default(executor: ExecutorMode) -> color_eyre::Result, - torii_p2p_addr: SocketAddr, - torii_api_url: SocketAddr, - iroha_genesis_public_key: PublicKey, + public_key: PublicKey, + private_key_digest: Algorithm, + private_key_payload: SerializeAsHex>, + p2p_address: SocketAddr, + api_address: SocketAddr, + genesis_public_key: PublicKey, + #[serde(skip_serializing_if = "Option::is_none")] + genesis_private_key_digest: Option, #[serde(skip_serializing_if = "Option::is_none")] - iroha_genesis_private_key: Option>, + genesis_private_key_payload: Option>>, #[serde(skip_serializing_if = "Option::is_none")] - iroha_genesis_file: Option, + genesis_file: Option, #[serde(skip_serializing_if = "Option::is_none")] sumeragi_trusted_peers: Option>>, } @@ -328,26 +332,33 @@ 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(SerializeAsJsonStr(private_key)), - Some(PATH_TO_GENESIS.to_string()), - ) - }); + let (genesis_private_key_digest, genesis_private_key_payload, genesis_file) = value + .genesis_private_key + .map_or((None, None, None), |private_key| { + ( + Some(private_key.digest_function()), + Some(SerializeAsHex(private_key.payload().to_owned())), + Some(PATH_TO_GENESIS.to_string()), + ) + }); + + let (private_key_digest, private_key_payload) = ( + value.key_pair.private_key().digest_function(), + SerializeAsHex(value.key_pair.private_key().payload().to_owned()), + ); Self { - iroha_chain_id: value.chain_id, + chain_id: value.chain_id, 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_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, + public_key: value.key_pair.public_key().clone(), + private_key_digest, + private_key_payload, + genesis_public_key: value.genesis_public_key, + genesis_private_key_digest, + genesis_private_key_payload, + genesis_file, + p2p_address: value.p2p_addr, + api_address: value.api_addr, sumeragi_trusted_peers: if value.trusted_peers.is_empty() { None } else { @@ -375,6 +386,23 @@ where } } +#[derive(Debug)] +struct SerializeAsHex(T); + +impl serde::Serialize for SerializeAsHex +where + T: AsRef<[u8]>, +{ + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + // FIXME maybe there is a way to avoid extra allocation, doesn't matter much + let data = hex::encode(&self.0); + serializer.serialize_str(&data) + } +} + #[derive(Debug)] pub struct DockerComposeBuilder<'a> { /// Needed to compute a relative source build path @@ -714,15 +742,17 @@ mod tests { build: . platform: linux/amd64 environment: - IROHA_CHAIN_ID: 00000000-0000-0000-0000-000000000000 + CHAIN_ID: 00000000-0000-0000-0000-000000000000 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_PUBLIC_KEY: ed012039E5BF092186FACC358770792A493CA98A83740643A3D41389483CF334F748C8 - IROHA_GENESIS_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"db9d90d20f969177bd5882f9fe211d14d1399d5440d04e3468783d169bbc4a8e39e5bf092186facc358770792a493ca98a83740643a3d41389483cf334f748c8"}' - IROHA_GENESIS_FILE: /config/genesis.json + PUBLIC_KEY: ed012039E5BF092186FACC358770792A493CA98A83740643A3D41389483CF334F748C8 + PRIVATE_KEY_DIGEST: ed25519 + PRIVATE_KEY_PAYLOAD: db9d90d20f969177bd5882f9fe211d14d1399d5440d04e3468783d169bbc4a8e39e5bf092186facc358770792a493ca98a83740643a3d41389483cf334f748c8 + P2P_ADDRESS: iroha1:1339 + API_ADDRESS: iroha1:1338 + GENESIS_PUBLIC_KEY: ed012039E5BF092186FACC358770792A493CA98A83740643A3D41389483CF334F748C8 + GENESIS_PRIVATE_KEY_DIGEST: ed25519 + GENESIS_PRIVATE_KEY_PAYLOAD: db9d90d20f969177bd5882f9fe211d14d1399d5440d04e3468783d169bbc4a8e39e5bf092186facc358770792a493ca98a83740643a3d41389483cf334f748c8 + GENESIS_FILE: /config/genesis.json ports: - 1337:1337 - 8080:8080 @@ -756,13 +786,14 @@ mod tests { let actual = serde_yaml::to_string(&env).unwrap(); let expected = expect_test::expect![[r#" - IROHA_CHAIN_ID: 00000000-0000-0000-0000-000000000000 + CHAIN_ID: 00000000-0000-0000-0000-000000000000 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_PUBLIC_KEY: ed0120415388A90FA238196737746A70565D041CFB32EAA0C89FF8CB244C7F832A6EBD + PUBLIC_KEY: ed0120415388A90FA238196737746A70565D041CFB32EAA0C89FF8CB244C7F832A6EBD + PRIVATE_KEY_DIGEST: ed25519 + PRIVATE_KEY_PAYLOAD: 6bf163fd75192b81a78cb20c5f8cb917f591ac6635f2577e6ca305c27a456a5d415388a90fa238196737746a70565d041cfb32eaa0c89ff8cb244c7f832a6ebd + P2P_ADDRESS: iroha0:1337 + API_ADDRESS: iroha0:1337 + GENESIS_PUBLIC_KEY: ed0120415388A90FA238196737746A70565D041CFB32EAA0C89FF8CB244C7F832A6EBD "#]]; expected.assert_eq(&actual); } @@ -798,15 +829,17 @@ mod tests { build: ./iroha-cloned platform: linux/amd64 environment: - IROHA_CHAIN_ID: 00000000-0000-0000-0000-000000000000 + CHAIN_ID: 00000000-0000-0000-0000-000000000000 IROHA_CONFIG: /config/config.json - IROHA_PUBLIC_KEY: ed0120F0321EB4139163C35F88BF78520FF7071499D7F4E79854550028A196C7B49E13 - IROHA_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"5f8d1291bf6b762ee748a87182345d135fd167062857aa4f20ba39f25e74c4b0f0321eb4139163c35f88bf78520ff7071499d7f4e79854550028a196c7b49e13"}' - TORII_P2P_ADDR: 0.0.0.0:1337 - TORII_API_URL: 0.0.0.0:8080 - IROHA_GENESIS_PUBLIC_KEY: ed01203420F48A9EEB12513B8EB7DAF71979CE80A1013F5F341C10DCDA4F6AA19F97A9 - IROHA_GENESIS_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"5a6d5f06a90d29ad906e2f6ea8b41b4ef187849d0d397081a4a15ffcbe71e7c73420f48a9eeb12513b8eb7daf71979ce80a1013f5f341c10dcda4f6aa19f97a9"}' - IROHA_GENESIS_FILE: /config/genesis.json + PUBLIC_KEY: ed0120F0321EB4139163C35F88BF78520FF7071499D7F4E79854550028A196C7B49E13 + PRIVATE_KEY_DIGEST: ed25519 + PRIVATE_KEY_PAYLOAD: 5f8d1291bf6b762ee748a87182345d135fd167062857aa4f20ba39f25e74c4b0f0321eb4139163c35f88bf78520ff7071499d7f4e79854550028a196c7b49e13 + P2P_ADDRESS: 0.0.0.0:1337 + API_ADDRESS: 0.0.0.0:8080 + GENESIS_PUBLIC_KEY: ed01203420F48A9EEB12513B8EB7DAF71979CE80A1013F5F341C10DCDA4F6AA19F97A9 + GENESIS_PRIVATE_KEY_DIGEST: ed25519 + GENESIS_PRIVATE_KEY_PAYLOAD: 5a6d5f06a90d29ad906e2f6ea8b41b4ef187849d0d397081a4a15ffcbe71e7c73420f48a9eeb12513b8eb7daf71979ce80a1013f5f341c10dcda4f6aa19f97a9 + 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 @@ -825,13 +858,14 @@ mod tests { build: ./iroha-cloned platform: linux/amd64 environment: - IROHA_CHAIN_ID: 00000000-0000-0000-0000-000000000000 + CHAIN_ID: 00000000-0000-0000-0000-000000000000 IROHA_CONFIG: /config/config.json - IROHA_PUBLIC_KEY: ed0120A88554AA5C86D28D0EEBEC497235664433E807881CD31E12A1AF6C4D8B0F026C - IROHA_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"8d34d2c6a699c61e7a9d5aabbbd07629029dfb4f9a0800d65aa6570113edb465a88554aa5c86d28d0eebec497235664433e807881cd31e12a1af6c4d8b0f026c"}' - TORII_P2P_ADDR: 0.0.0.0:1338 - TORII_API_URL: 0.0.0.0:8081 - IROHA_GENESIS_PUBLIC_KEY: ed01203420F48A9EEB12513B8EB7DAF71979CE80A1013F5F341C10DCDA4F6AA19F97A9 + PUBLIC_KEY: ed0120A88554AA5C86D28D0EEBEC497235664433E807881CD31E12A1AF6C4D8B0F026C + PRIVATE_KEY_DIGEST: ed25519 + PRIVATE_KEY_PAYLOAD: 8d34d2c6a699c61e7a9d5aabbbd07629029dfb4f9a0800d65aa6570113edb465a88554aa5c86d28d0eebec497235664433e807881cd31e12a1af6c4d8b0f026c + P2P_ADDRESS: 0.0.0.0:1338 + API_ADDRESS: 0.0.0.0:8081 + 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 @@ -849,13 +883,14 @@ mod tests { build: ./iroha-cloned platform: linux/amd64 environment: - IROHA_CHAIN_ID: 00000000-0000-0000-0000-000000000000 + CHAIN_ID: 00000000-0000-0000-0000-000000000000 IROHA_CONFIG: /config/config.json - IROHA_PUBLIC_KEY: ed0120312C1B7B5DE23D366ADCF23CD6DB92CE18B2AA283C7D9F5033B969C2DC2B92F4 - IROHA_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"cf4515a82289f312868027568c0da0ee3f0fde7fef1b69deb47b19fde7cbc169312c1b7b5de23d366adcf23cd6db92ce18b2aa283c7d9f5033b969c2dc2b92f4"}' - TORII_P2P_ADDR: 0.0.0.0:1339 - TORII_API_URL: 0.0.0.0:8082 - IROHA_GENESIS_PUBLIC_KEY: ed01203420F48A9EEB12513B8EB7DAF71979CE80A1013F5F341C10DCDA4F6AA19F97A9 + PUBLIC_KEY: ed0120312C1B7B5DE23D366ADCF23CD6DB92CE18B2AA283C7D9F5033B969C2DC2B92F4 + PRIVATE_KEY_DIGEST: ed25519 + PRIVATE_KEY_PAYLOAD: cf4515a82289f312868027568c0da0ee3f0fde7fef1b69deb47b19fde7cbc169312c1b7b5de23d366adcf23cd6db92ce18b2aa283c7d9f5033b969c2dc2b92f4 + P2P_ADDRESS: 0.0.0.0:1339 + API_ADDRESS: 0.0.0.0:8082 + 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 @@ -873,13 +908,14 @@ mod tests { build: ./iroha-cloned platform: linux/amd64 environment: - IROHA_CHAIN_ID: 00000000-0000-0000-0000-000000000000 + CHAIN_ID: 00000000-0000-0000-0000-000000000000 IROHA_CONFIG: /config/config.json - IROHA_PUBLIC_KEY: ed0120854457B2E3D6082181DA73DC01C1E6F93A72D0C45268DC8845755287E98A5DEE - IROHA_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"ab0e99c2b845b4ac7b3e88d25a860793c7eb600a25c66c75cba0bae91e955aa6854457b2e3d6082181da73dc01c1e6f93a72d0c45268dc8845755287e98a5dee"}' - TORII_P2P_ADDR: 0.0.0.0:1340 - TORII_API_URL: 0.0.0.0:8083 - IROHA_GENESIS_PUBLIC_KEY: ed01203420F48A9EEB12513B8EB7DAF71979CE80A1013F5F341C10DCDA4F6AA19F97A9 + PUBLIC_KEY: ed0120854457B2E3D6082181DA73DC01C1E6F93A72D0C45268DC8845755287E98A5DEE + PRIVATE_KEY_DIGEST: ed25519 + PRIVATE_KEY_PAYLOAD: ab0e99c2b845b4ac7b3e88d25a860793c7eb600a25c66c75cba0bae91e955aa6854457b2e3d6082181da73dc01c1e6f93a72d0c45268dc8845755287e98a5dee + P2P_ADDRESS: 0.0.0.0:1340 + API_ADDRESS: 0.0.0.0:8083 + 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 From f80b5d295dc12aac9aa728e4fc896abed7adcc26 Mon Sep 17 00:00:00 2001 From: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> Date: Fri, 26 Jan 2024 19:18:59 +0900 Subject: [PATCH 19/94] [feat]: implement `extends` Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- config/src/parameters/user_layer.rs | 152 +++++++++++++++++- config/tests/fixtures.rs | 36 ++++- .../tests/fixtures/bad.extends_nowhere.toml | 1 + ...bad-envs.env => bad.multiple_bad_envs.env} | 0 .../fixtures/bad.torii_addr_eq_p2p_addr.toml | 14 +- config/tests/fixtures/base.toml | 12 ++ config/tests/fixtures/base_trusted_peers.toml | 3 + .../tests/fixtures/inconsistent_genesis.toml | 11 +- .../fixtures/minimal_alone_with_genesis.toml | 11 +- .../tests/fixtures/minimal_file_and_env.toml | 12 +- .../fixtures/minimal_with_trusted_peers.toml | 18 +-- config/tests/fixtures/multiple_extends.1.toml | 2 + config/tests/fixtures/multiple_extends.2.toml | 5 + .../tests/fixtures/multiple_extends.2a.toml | 2 + config/tests/fixtures/multiple_extends.toml | 6 + 15 files changed, 217 insertions(+), 68 deletions(-) create mode 100644 config/tests/fixtures/bad.extends_nowhere.toml rename config/tests/fixtures/{bad.multiple-bad-envs.env => bad.multiple_bad_envs.env} (100%) create mode 100644 config/tests/fixtures/base.toml create mode 100644 config/tests/fixtures/base_trusted_peers.toml create mode 100644 config/tests/fixtures/multiple_extends.1.toml create mode 100644 config/tests/fixtures/multiple_extends.2.toml create mode 100644 config/tests/fixtures/multiple_extends.2a.toml create mode 100644 config/tests/fixtures/multiple_extends.toml diff --git a/config/src/parameters/user_layer.rs b/config/src/parameters/user_layer.rs index 8f42b97c43b..57963a87be7 100644 --- a/config/src/parameters/user_layer.rs +++ b/config/src/parameters/user_layer.rs @@ -5,7 +5,7 @@ use std::{ io::Read, num::{NonZeroU32, NonZeroU64, NonZeroUsize}, ops::{Add, Div}, - path::{Path, PathBuf}, + path::{Iter, Path, PathBuf}, str::FromStr, time::Duration, }; @@ -37,9 +37,63 @@ use crate::{ }, }; +#[derive(Debug, Default, Serialize, Deserialize, Eq, PartialEq)] +#[serde(untagged)] +pub enum ExtendsPaths { + #[default] + None, + Single(PathBuf), + Multiple(Vec), +} + +impl Merge for ExtendsPaths { + fn merge(&mut self, other: Self) { + match (self, other) { + (Self::None, Self::None) => {} + _ => unreachable!( + "It is a bug. `ExtendsPaths` should be resolved to `None` before merging." + ), + } + } +} + +pub enum ExtendsPathsIter<'a> { + None, + Single(Option<&'a PathBuf>), + Multiple(std::slice::Iter<'a, PathBuf>), +} + +impl ExtendsPaths { + pub fn iter(&self) -> ExtendsPathsIter<'_> { + match &self { + Self::None => ExtendsPathsIter::None, + Self::Single(x) => ExtendsPathsIter::Single(Some(x)), + Self::Multiple(vec) => ExtendsPathsIter::Multiple(vec.iter()), + } + } + + /// Marks this instance as used, so that subsequent [`Merge`] doesn't fail + pub fn used(&mut self) { + *self = Self::None + } +} + +impl<'a> Iterator for ExtendsPathsIter<'a> { + type Item = &'a PathBuf; + + fn next(&mut self) -> Option { + match self { + Self::None => None, + Self::Single(x) => x.take(), + Self::Multiple(iter) => iter.next(), + } + } +} + #[derive(Deserialize, Serialize, Debug, Default, Merge)] #[serde(deny_unknown_fields, default)] pub struct RootPartial { + pub extends: ExtendsPaths, pub iroha: IrohaPartial, pub genesis: GenesisPartial, pub kura: KuraPartial, @@ -69,15 +123,40 @@ impl RootPartial { file.read_to_string(&mut contents)?; contents }; - let mut parsed: Self = toml::from_str(&contents).wrap_err("failed to parse toml")?; - parsed.normalise_paths( - path.as_ref() - .parent() - .expect("the config file path could not be empty or root"), - ); - Ok(parsed) + let mut layer: Self = toml::from_str(&contents).wrap_err("failed to parse toml")?; + + let base_path = path + .as_ref() + .parent() + .expect("the config file path could not be empty or root"); + + layer.normalise_paths(base_path); + + if let Some(base) = + layer + .extends + .iter() + .try_fold(None, |acc: Option, extends_path| { + // extends path is not normalised relative to the config file yet + let full_path = base_path.join(extends_path); + + let base = Self::from_toml(&full_path) + .wrap_err_with(|| eyre!("cannot extend from `{}`", full_path.display()))?; + + match acc { + None => Ok::, Report>(Some(base)), + Some(other_base) => Ok(Some(other_base.merge(base))), + } + })? + { + layer.extends.used(); + layer = base.merge(layer); + }; + + Ok(layer) } + /// **Note:** this function doesn't affect `extends` fn normalise_paths(&mut self, relative_to: impl AsRef) { let path = relative_to.as_ref(); @@ -305,6 +384,7 @@ impl FromEnv for RootPartial { emitter.finish()?; Ok(Self { + extends: ExtendsPaths::None, iroha: iroha.unwrap(), genesis: genesis.unwrap(), kura: kura.unwrap(), @@ -1456,4 +1536,60 @@ mod tests { .try_into() .expect("should not fail when not all fields in `iroha` are presented at a time"); } + + #[derive(Deserialize, Default)] + #[serde(default)] + struct TestExtends { + extends: ExtendsPaths, + } + + #[test] + fn parse_empty_extends() { + let value: TestExtends = toml::from_str("").expect("should be fine with empty input"); + + assert_eq!(value.extends, ExtendsPaths::None); + } + + #[test] + fn parse_single_extends_path() { + let value: TestExtends = toml::toml! { + extends = "./path" + } + .try_into() + .unwrap(); + + assert_eq!(value.extends, ExtendsPaths::Single("./path".into())); + } + + #[test] + fn parse_multiple_extends_paths() { + let value: TestExtends = toml::toml! { + extends = ["foo", "bar", "baz"] + } + .try_into() + .unwrap(); + + assert_eq!( + value.extends, + ExtendsPaths::Multiple(vec!["foo".into(), "bar".into(), "baz".into()]) + ); + } + + #[test] + fn iterating_over_extends() { + impl ExtendsPaths { + fn into_str_vec(&self) -> Vec<&str> { + self.iter().map(|p| p.to_str().unwrap()).collect() + } + } + + let empty = ExtendsPaths::None; + assert_eq!(empty.into_str_vec(), Vec::<&str>::new()); + + let single = ExtendsPaths::Single("single".into()); + assert_eq!(single.into_str_vec(), vec!["single"]); + + let multi = ExtendsPaths::Multiple(vec!["foo".into(), "bar".into(), "baz".into()]); + assert_eq!(multi.into_str_vec(), vec!["foo", "bar", "baz"]); + } } diff --git a/config/tests/fixtures.rs b/config/tests/fixtures.rs index cca2cb84e0b..0aff9757c88 100644 --- a/config/tests/fixtures.rs +++ b/config/tests/fixtures.rs @@ -268,6 +268,7 @@ fn full_envs_set_is_consumed() -> Result<()> { let expected = expect_test::expect![[r#" RootPartial { + extends: None, iroha: IrohaPartial { chain_id: Some( ChainId( @@ -380,7 +381,7 @@ fn full_envs_set_is_consumed() -> Result<()> { #[test] fn multiple_env_parsing_errors() { - let env = test_env_from_file(fixtures_dir().join("bad.multiple-bad-envs.env")); + let env = test_env_from_file(fixtures_dir().join("bad.multiple_bad_envs.env")); let error = RootPartial::from_env(&env).expect_err("the input from env is invalid"); @@ -408,7 +409,7 @@ fn config_from_file_and_env() -> Result<()> { } #[test] -fn fail_if_torii_address_and_p2p_address_are_equal() -> Result<()> { +fn fails_if_torii_address_and_p2p_address_are_equal() -> Result<()> { let error = RootPartial::from_toml(fixtures_dir().join("bad.torii_addr_eq_p2p_addr.toml"))? .unwrap_partial() .expect("should not fail, all fields are present") @@ -417,8 +418,37 @@ fn fail_if_torii_address_and_p2p_address_are_equal() -> Result<()> { }) .expect_err("should fail because of bad input"); - let expected = expect_test::expect!["`iroha.p2p_address` and `torii.address` should not be the same"]; + let expected = + expect_test::expect!["`iroha.p2p_address` and `torii.address` should not be the same"]; expected.assert_eq(&format!("{error:#}")); Ok(()) } + +#[test] +fn fails_if_extends_leads_to_nowhere() { + let error = RootPartial::from_toml(fixtures_dir().join("bad.extends_nowhere.toml")) + .expect_err("should fail with bad input"); + + let expected = expect_test::expect!["cannot extend from `tests/fixtures/nowhere.toml`: cannot open file at location `tests/fixtures/nowhere.toml`: No such file or directory (os error 2)"]; + expected.assert_eq(&format!("{error:#}")); +} + +#[test] +fn multiple_extends_works() -> Result<()> { + // we are looking into `logger` in particular + let layer = RootPartial::from_toml(fixtures_dir().join("multiple_extends.toml"))?.logger; + + let expected = expect_test::expect![[r#" + LoggerPartial { + level: Some( + ERROR, + ), + format: Some( + Compact, + ), + }"#]]; + expected.assert_eq(&format!("{layer:#?}")); + + Ok(()) +} diff --git a/config/tests/fixtures/bad.extends_nowhere.toml b/config/tests/fixtures/bad.extends_nowhere.toml new file mode 100644 index 00000000000..30129b39359 --- /dev/null +++ b/config/tests/fixtures/bad.extends_nowhere.toml @@ -0,0 +1 @@ +extends = "nowhere.toml" \ No newline at end of file diff --git a/config/tests/fixtures/bad.multiple-bad-envs.env b/config/tests/fixtures/bad.multiple_bad_envs.env similarity index 100% rename from config/tests/fixtures/bad.multiple-bad-envs.env rename to config/tests/fixtures/bad.multiple_bad_envs.env diff --git a/config/tests/fixtures/bad.torii_addr_eq_p2p_addr.toml b/config/tests/fixtures/bad.torii_addr_eq_p2p_addr.toml index 21587e2aca3..ae1c04c7624 100644 --- a/config/tests/fixtures/bad.torii_addr_eq_p2p_addr.toml +++ b/config/tests/fixtures/bad.torii_addr_eq_p2p_addr.toml @@ -1,17 +1,7 @@ +extends = ["base.toml", "base_trusted_peers.toml"] + [iroha] -chain_id = "0" -public_key = "ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB" -private_key.digest_function = "ed25519" -private_key.payload = "8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb" -# same as `torii.address` p2p_address = "127.0.0.1:8080" -[genesis] -public_key = "ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB" - [torii] address = "127.0.0.1:8080" - -[[sumeragi.trusted_peers]] -address = "127.0.0.1:1338" -public_key = "ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB" diff --git a/config/tests/fixtures/base.toml b/config/tests/fixtures/base.toml new file mode 100644 index 00000000000..2760b625dcc --- /dev/null +++ b/config/tests/fixtures/base.toml @@ -0,0 +1,12 @@ +[iroha] +chain_id = "0" +public_key = "ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB" +private_key.digest_function = "ed25519" +private_key.payload = "8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb" +p2p_address = "127.0.0.1:1337" + +[genesis] +public_key = "ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB" + +[torii] +address = "127.0.0.1:8080" \ No newline at end of file diff --git a/config/tests/fixtures/base_trusted_peers.toml b/config/tests/fixtures/base_trusted_peers.toml new file mode 100644 index 00000000000..1314cd70026 --- /dev/null +++ b/config/tests/fixtures/base_trusted_peers.toml @@ -0,0 +1,3 @@ +[[sumeragi.trusted_peers]] +address = "127.0.0.1:1338" +public_key = "ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB" diff --git a/config/tests/fixtures/inconsistent_genesis.toml b/config/tests/fixtures/inconsistent_genesis.toml index ce423f04fe2..e6f38ffd2b6 100644 --- a/config/tests/fixtures/inconsistent_genesis.toml +++ b/config/tests/fixtures/inconsistent_genesis.toml @@ -1,16 +1,7 @@ -[iroha] -chain_id = "0" -public_key = "ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB" -private_key.digest_function = "ed25519" -private_key.payload = "8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb" -p2p_address = "127.0.0.1:1337" +extends = "base.toml" [genesis] -public_key = "ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB" private_key.digest_function = "ed25519" private_key.payload = "8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb" # should fail without it: # file = ... - -[torii] -address = "127.0.0.1:8080" \ No newline at end of file diff --git a/config/tests/fixtures/minimal_alone_with_genesis.toml b/config/tests/fixtures/minimal_alone_with_genesis.toml index 590bf7c48ad..a6689041d21 100644 --- a/config/tests/fixtures/minimal_alone_with_genesis.toml +++ b/config/tests/fixtures/minimal_alone_with_genesis.toml @@ -1,15 +1,6 @@ -[iroha] -chain_id = "0" -public_key = "ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB" -private_key.digest_function = "ed25519" -private_key.payload = "8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb" -p2p_address = "127.0.0.1:1337" +extends = "base.toml" [genesis] -public_key = "ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB" file = "./empty_ok_genesis.json" private_key.digest_function = "ed25519" private_key.payload = "8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb" - -[torii] -address = "127.0.0.1:8080" \ No newline at end of file diff --git a/config/tests/fixtures/minimal_file_and_env.toml b/config/tests/fixtures/minimal_file_and_env.toml index 23504540be3..f3406882248 100644 --- a/config/tests/fixtures/minimal_file_and_env.toml +++ b/config/tests/fixtures/minimal_file_and_env.toml @@ -1,17 +1,13 @@ +extends = "base_trusted_peers.toml" + [iroha] chain_id = "0" public_key = "ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB" p2p_address = "127.0.0.1:1337" - -[iroha.private_key] -digest_function = "ed25519" -payload = "8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb" +private_key.digest_function = "ed25519" +private_key.payload = "8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb" [genesis] public_key = "ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB" # `torii.address` should be in ENV - -[[sumeragi.trusted_peers]] -address = "127.0.0.1:1338" -public_key = "ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB" diff --git a/config/tests/fixtures/minimal_with_trusted_peers.toml b/config/tests/fixtures/minimal_with_trusted_peers.toml index 49da4352211..12ebd580cbc 100644 --- a/config/tests/fixtures/minimal_with_trusted_peers.toml +++ b/config/tests/fixtures/minimal_with_trusted_peers.toml @@ -1,17 +1 @@ -[iroha] -chain_id = "0" -public_key = "ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB" -private_key.digest_function = "ed25519" -private_key.payload = "8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb" -p2p_address = "127.0.0.1:1337" - -[genesis] -public_key = "ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB" - -[torii] -address = "127.0.0.1:8080" - -# we have this so that config will be complete without a full genesis -[[sumeragi.trusted_peers]] -address = "127.0.0.1:1338" -public_key = "ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB" +extends = ["base.toml", "base_trusted_peers.toml"] diff --git a/config/tests/fixtures/multiple_extends.1.toml b/config/tests/fixtures/multiple_extends.1.toml new file mode 100644 index 00000000000..46b1262777b --- /dev/null +++ b/config/tests/fixtures/multiple_extends.1.toml @@ -0,0 +1,2 @@ +[logger] +format = "pretty" \ No newline at end of file diff --git a/config/tests/fixtures/multiple_extends.2.toml b/config/tests/fixtures/multiple_extends.2.toml new file mode 100644 index 00000000000..47e9616ccfd --- /dev/null +++ b/config/tests/fixtures/multiple_extends.2.toml @@ -0,0 +1,5 @@ +# sets level +extends = "multiple_extends.2a.toml" + +[logger] +format = "compact" \ No newline at end of file diff --git a/config/tests/fixtures/multiple_extends.2a.toml b/config/tests/fixtures/multiple_extends.2a.toml new file mode 100644 index 00000000000..c7b048bc674 --- /dev/null +++ b/config/tests/fixtures/multiple_extends.2a.toml @@ -0,0 +1,2 @@ +[logger] +level = "DEBUG" \ No newline at end of file diff --git a/config/tests/fixtures/multiple_extends.toml b/config/tests/fixtures/multiple_extends.toml new file mode 100644 index 00000000000..83b87043034 --- /dev/null +++ b/config/tests/fixtures/multiple_extends.toml @@ -0,0 +1,6 @@ +# 1 - sets format, 2 - sets format and level +extends = ["multiple_extends.1.toml", "multiple_extends.2.toml"] + +[logger] +# final value +level = "ERROR" \ No newline at end of file From 3c7c259479c4cb96570852acff1a4613abf3f38e Mon Sep 17 00:00:00 2001 From: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> Date: Fri, 26 Jan 2024 19:19:24 +0900 Subject: [PATCH 20/94] [chore]: update default snapshot storage Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- config/src/parameters/defaults.rs | 2 +- config/tests/fixtures.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/config/src/parameters/defaults.rs b/config/src/parameters/defaults.rs index 82c0803ab9b..852c9726458 100644 --- a/config/src/parameters/defaults.rs +++ b/config/src/parameters/defaults.rs @@ -45,7 +45,7 @@ pub mod network { pub mod snapshot { use super::*; - pub const DEFAULT_SNAPSHOT_PATH: &str = "./storage"; + pub const DEFAULT_SNAPSHOT_PATH: &str = "./storage/snapshot"; // Default frequency of making snapshots is 1 minute, need to be adjusted for larger world state view size pub const DEFAULT_SNAPSHOT_CREATE_EVERY_MS: Duration = Duration::from_secs(60); pub const DEFAULT_ENABLED: bool = true; diff --git a/config/tests/fixtures.rs b/config/tests/fixtures.rs index 0aff9757c88..916e6605ff5 100644 --- a/config/tests/fixtures.rs +++ b/config/tests/fixtures.rs @@ -105,7 +105,7 @@ fn minimal_config_snapshot() -> Result<()> { }, snapshot: SnapshotFull { create_every: 60s, - store_path: "./storage", + store_path: "./storage/snapshot", creation_enabled: true, }, regular_telemetry: None, From 55f48ad6956b6fe158fe84e60a0fcdcbf6641400 Mon Sep 17 00:00:00 2001 From: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> Date: Mon, 29 Jan 2024 05:56:30 +0900 Subject: [PATCH 21/94] [fix]: update after rebase Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- torii/const/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/torii/const/src/lib.rs b/torii/const/src/lib.rs index 6e25b7b11e4..241522c09b6 100644 --- a/torii/const/src/lib.rs +++ b/torii/const/src/lib.rs @@ -21,7 +21,7 @@ pub mod uri { /// The web socket uri used to subscribe to blocks stream. pub const BLOCKS_STREAM: &str = "block/stream"; /// Get pending transactions. - pub const PENDING_TRANSACTIONS: &str = "pending_transactions"; + pub const MATCHING_PENDING_TRANSACTIONS: &str = "matching_pending_transactions"; /// The URI for local config changing inspecting pub const CONFIGURATION: &str = "configuration"; /// URI to report status for administration From d5d589a73e797296f68c999d1a1b63e45c5a8afc Mon Sep 17 00:00:00 2001 From: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> Date: Mon, 29 Jan 2024 08:22:32 +0900 Subject: [PATCH 22/94] [refactor]: update just everything - `test_env.py`: use toml configs - remove other channel configs - put example configs into `config_samples` dir - put docker setup into `config_samples/swarm` dir - pytests: split settings for CLI and config paths - make test env runnable Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- .github/workflows/iroha2-dev-pr.yml | 1 + .github/workflows/iroha2-release-pr.yml | 1 + CONTRIBUTING.md | 2 +- Cargo.lock | 1 + README.md | 13 +- cli/README.md | 13 +- cli/src/lib.rs | 10 +- client/Cargo.toml | 1 + client/README.md | 8 +- client/examples/tutorial.rs | 2 +- client/src/config.rs | 16 +- client_cli/pytests/README.md | 6 +- client_cli/pytests/common/settings.py | 5 +- .../examples/client.example.toml | 6 +- config_samples/examples/peer.example.toml | 1 + .../examples/prometheus.example.yml | 0 config_samples/swarm/client.toml | 12 ++ .../swarm/docker-compose.local.yml | 77 +++---- .../swarm/docker-compose.single.yml | 32 +++ .../swarm/docker-compose.yml | 69 +++--- .../swarm}/executor.wasm | Bin .../swarm}/genesis.json | 0 configs/client/config.json | 17 -- configs/client/lts/config.json | 95 --------- configs/client/stable/config.json | 95 --------- configs/peer/config.json | 95 --------- configs/peer/lts/config.json | 98 --------- configs/peer/lts/executor.wasm | Bin 501157 -> 0 bytes configs/peer/lts/genesis.json | 201 ------------------ configs/peer/stable/config.json | 98 --------- configs/peer/stable/executor.wasm | Bin 501157 -> 0 bytes configs/peer/stable/genesis.json | 201 ------------------ core/benches/blocks/common.rs | 2 +- core/benches/validation.rs | 2 +- core/test_network/src/lib.rs | 7 +- crypto/src/lib.rs | 2 +- default_executor/README.md | 2 +- docker-compose.single.yml | 31 --- scripts/requirements.txt | 1 + scripts/test_env.py | 147 +++++++------ scripts/tests/consistency.sh | 18 +- scripts/tests/panic_on_invalid_genesis.sh | 2 +- tools/swarm/README.md | 6 +- tools/swarm/src/cli.rs | 4 +- tools/swarm/src/compose.rs | 33 +-- 45 files changed, 285 insertions(+), 1148 deletions(-) rename configs/client/config.example.toml => config_samples/examples/client.example.toml (88%) create mode 100644 config_samples/examples/peer.example.toml rename configs/prometheus.yml => config_samples/examples/prometheus.example.yml (100%) create mode 100644 config_samples/swarm/client.toml rename docker-compose.local.yml => config_samples/swarm/docker-compose.local.yml (50%) create mode 100644 config_samples/swarm/docker-compose.single.yml rename docker-compose.yml => config_samples/swarm/docker-compose.yml (52%) rename {configs/peer => config_samples/swarm}/executor.wasm (100%) rename {configs/peer => config_samples/swarm}/genesis.json (100%) delete mode 100644 configs/client/config.json delete mode 100644 configs/client/lts/config.json delete mode 100644 configs/client/stable/config.json delete mode 100644 configs/peer/lts/config.json delete mode 100644 configs/peer/lts/executor.wasm delete mode 100644 configs/peer/lts/genesis.json delete mode 100644 configs/peer/stable/config.json delete mode 100644 configs/peer/stable/executor.wasm delete mode 100644 configs/peer/stable/genesis.json delete mode 100644 docker-compose.single.yml create mode 100644 scripts/requirements.txt diff --git a/.github/workflows/iroha2-dev-pr.yml b/.github/workflows/iroha2-dev-pr.yml index bdaddbf774a..20e622e774b 100644 --- a/.github/workflows/iroha2-dev-pr.yml +++ b/.github/workflows/iroha2-dev-pr.yml @@ -143,6 +143,7 @@ jobs: cargo build --bin iroha - name: Setup test Iroha 2 environment on the bare metal run: | + pip3 install -r scripts/requirements.txt --no-input ./scripts/test_env.py setup - name: Mark binaries as executable run: | diff --git a/.github/workflows/iroha2-release-pr.yml b/.github/workflows/iroha2-release-pr.yml index cd1a94b8623..f064ca99b0e 100644 --- a/.github/workflows/iroha2-release-pr.yml +++ b/.github/workflows/iroha2-release-pr.yml @@ -36,6 +36,7 @@ jobs: cargo build --bin iroha - name: Setup test Iroha 2 environment on bare metal run: | + pip3 install -r scripts/requirements.txt --no-input ./scripts/test_env.py setup - name: Mark binaries as executable run: | diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5ef9c16a5ec..1d592221fd9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -222,7 +222,7 @@ Follow these commit guidelines: - To run the source-code based tests, execute [`cargo test`](https://doc.rust-lang.org/cargo/commands/cargo-test.html) in the Iroha root. Note that this is a long process. - To run benchmarks, execute [`cargo bench`](https://doc.rust-lang.org/cargo/commands/cargo-bench.html) from the Iroha root. To help debug benchmark outputs, set the `debug_assertions` environment variable like so: `RUSTFLAGS="--cfg debug_assertions" cargo bench`. - If you are working on a particular component, be mindful that when you run `cargo test` in a [workspace](https://doc.rust-lang.org/cargo/reference/workspaces.html), it will only run the tests for that workspace, which usually doesn't include any [integration tests](https://www.testingxperts.com/blog/what-is-integration-testing). -- If you want to test your changes on a minimal network, the provided [`docker-compose.yml`](docker-compose.yml) creates a network of 4 Iroha peers in docker containers that can be used to test consensus and asset propagation-related logic. We recommend interacting with that network using either [`iroha-python`](https://github.com/hyperledger/iroha-python), or the included `iroha_client_cli`. +- If you want to test your changes on a minimal network, the provided [`docker-compose.yml`](config_samples/swarm/docker-compose.yml) creates a network of 4 Iroha peers in docker containers that can be used to test consensus and asset propagation-related logic. We recommend interacting with that network using either [`iroha-python`](https://github.com/hyperledger/iroha-python), or the included `iroha_client_cli`. - Do not remove failing tests. Even tests that are ignored will be run in our pipeline eventually. - If possible, please benchmark your code both before and after making your changes, as a significant performance regression can break existing users' installations. diff --git a/Cargo.lock b/Cargo.lock index 5a657414f6e..ead62be6751 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2721,6 +2721,7 @@ dependencies = [ "thiserror", "tokio", "tokio-tungstenite", + "toml 0.8.8", "tracing-flame", "tracing-subscriber", "tungstenite", diff --git a/README.md b/README.md index d89ee642b3e..7fbd2d93091 100644 --- a/README.md +++ b/README.md @@ -124,9 +124,7 @@ docker compose up With the `docker-compose` instance running, use [Iroha Client CLI](./client_cli/README.md): ```bash -cp configs/client/config.json target/debug/config.json -cd target/debug -./iroha_client_cli --help +cargo run --bin iroha_client_cli -- --config ./config_samples/swarm/client.toml ``` ## Integration @@ -166,12 +164,11 @@ A brief overview on how to configure and maintain an Iroha instance: 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 +iroha --config /path/to/config.toml + +# or ENV +IROHA_CONFIG=/path/to/config.toml iroha ``` **Note:** detailed configuration reference is [work in progress](https://github.com/hyperledger/iroha-2-docs/issues/392). diff --git a/cli/README.md b/cli/README.md index 87f1c1aed06..40499ee4240 100644 --- a/cli/README.md +++ b/cli/README.md @@ -82,17 +82,20 @@ You may deploy Iroha as a [native binary](#native-binary) or by using [Docker](# ### Native binary + + 1. Prepare a deployment environment. If you plan on running the `iroha` peer binary from the directory `deploy`, copy `config.json` and `genesis.json`: ```bash - cp ./target/release/iroha - cp ./configs/peer/config.json deploy - cp ./configs/peer/genesis.json deploy + # FIXME + # cp ./target/release/iroha + # cp ./config_samples/peer/config.json deploy + # cp ./config_samples/peer/genesis.json deploy ``` -2. Make necessary edits to `config.json` and `genesis.json`, such as: +2. Make the necessary edits to `config.json` and `genesis.json`, such as: - Generate new key pairs and add their values to `genesis.json`) - Adjust the port values for your initial set of trusted peers @@ -111,7 +114,7 @@ You may deploy Iroha as a [native binary](#native-binary) or by using [Docker](# ### Docker -We provide a sample configuration for Docker in [`docker-compose.yml`](../docker-compose.yml). We highly recommend that you adjust the `config.json` to include a set of new key pairs. +We provide a sample configuration for Docker in [`docker-compose.yml`](../config_samples/swarm/docker-compose.yml). We highly recommend that you adjust the `config.json` to include a set of new key pairs. [Generate the keys](#generating-keys) and put them into `services.*.environment` in `docker-compose.yml`. Don't forget to update the public keys of `TRUSTED_PEERS`. diff --git a/cli/src/lib.rs b/cli/src/lib.rs index e7c48032e79..e23f13db0e1 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -198,10 +198,12 @@ impl Iroha { genesis: Option, logger: LoggerHandle, ) -> Result { - let network = - IrohaNetwork::start(config.torii.address.clone(), config.iroha.key_pair.clone()) - .await - .wrap_err("Unable to start P2P-network")?; + let network = IrohaNetwork::start( + config.iroha.p2p_address.clone(), + config.iroha.key_pair.clone(), + ) + .await + .wrap_err("Unable to start P2P-network")?; let (events_sender, _) = broadcast::channel(10000); let world = World::with( diff --git a/client/Cargo.toml b/client/Cargo.toml index 1063175850b..c747e674069 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -74,6 +74,7 @@ tokio-tungstenite = { workspace = true } tungstenite = { workspace = true } futures-util = "0.3.28" merge = "0.1.0" +toml = { workspace = true } [dev-dependencies] iroha_wasm_builder = { workspace = true } diff --git a/client/README.md b/client/README.md index 10073589a11..c15f7c9b6ad 100644 --- a/client/README.md +++ b/client/README.md @@ -16,15 +16,9 @@ Follow the [Iroha 2 tutorial](https://hyperledger.github.io/iroha-2-docs/guide/r Add the following to the manifest file of your Rust project: ```toml -iroha_client = { git = "https://github.com/hyperledger/iroha/", branch="iroha2-dev" } +iroha_client = { git = "https://github.com/hyperledger/iroha", branch = "iroha2-dev" } ``` ## Examples -```rust -let configuration = - &Configuration::from_path("config.json").expect("Failed to load configuration."); -let mut iroha_client = Client::new(configuration); -``` - We highly recommend looking at the sample [`iroha_client_cli`](../client_cli) implementation binary as well as our [tutorial](https://hyperledger.github.io/iroha-2-docs/guide/rust.html) for more examples and explanations. diff --git a/client/examples/tutorial.rs b/client/examples/tutorial.rs index b20722e4412..8f59897620a 100644 --- a/client/examples/tutorial.rs +++ b/client/examples/tutorial.rs @@ -8,7 +8,7 @@ use iroha_client::config::{base::UnwrapPartial, user_layer::RootPartial as UserC fn main() { // #region rust_config_load - let config_loc = "../configs/client/config.toml"; + let config_loc = "../config_samples/swarm/client.toml"; let config = UserConfig::from_toml(config_loc) .wrap_err("Unable to load the configuration file at `.....`") .expect("Config file is loading normally.") diff --git a/client/src/config.rs b/client/src/config.rs index b14001b7e12..12b9039878b 100644 --- a/client/src/config.rs +++ b/client/src/config.rs @@ -66,7 +66,7 @@ pub struct BasicAuth { } pub mod user_layer { - use std::{path::Path, time::Duration}; + use std::{fs::File, io::Read, path::Path, time::Duration}; use eyre::{eyre, Context, Report}; use iroha_config::base::{ @@ -96,7 +96,17 @@ pub mod user_layer { } pub fn from_toml(path: impl AsRef) -> eyre::Result { - todo!() + let contents = { + let mut contents = String::new(); + File::open(path.as_ref()) + .wrap_err_with(|| { + eyre!("cannot open file at location `{}`", path.as_ref().display()) + })? + .read_to_string(&mut contents)?; + contents + }; + let layer: Self = toml::from_str(&contents).wrap_err("failed to parse toml")?; + Ok(layer) } pub fn merge(mut self, other: Self) -> Self { @@ -167,7 +177,7 @@ pub mod user_layer { // TODO: validate if TTL is too small? - if tx_timeout < tx_ttl { + if tx_timeout > tx_ttl { // TODO: // would be nice to provide a nice report with spans in the input // pointing out source data in provided config files diff --git a/client_cli/pytests/README.md b/client_cli/pytests/README.md index ad585372c95..8a89105933c 100644 --- a/client_cli/pytests/README.md +++ b/client_cli/pytests/README.md @@ -64,7 +64,8 @@ The test model has the following structure: 3. Configure the tests by creating the following `.env` file in _this_ (`/client_cli/pytests/`) directory: ```shell - CLIENT_CLI_DIR=/path/to/iroha_client_cli/with/config.json/dir/ + CLIENT_CLI_BINARY=/path/to/iroha_client_cli + CLIENT_CLI_CONFIG=/path/to/config.toml TORII_API_PORT_MIN=8080 TORII_API_PORT_MAX=8083 ``` @@ -161,7 +162,8 @@ The variables: **Example**: ```shell -CLIENT_CLI_DIR=/path/to/iroha_client_cli/with/config.json/dir/ +CLIENT_CLI_BINARY=/path/to/iroha_client_cli +CLIENT_CLI_CONFIG=/path/to/config.toml TORII_API_PORT_MIN=8080 TORII_API_PORT_MAX=8083 ``` diff --git a/client_cli/pytests/common/settings.py b/client_cli/pytests/common/settings.py index c79aa7765f2..d5de68f753f 100644 --- a/client_cli/pytests/common/settings.py +++ b/client_cli/pytests/common/settings.py @@ -13,10 +13,9 @@ (os.path.dirname (os.path.abspath(__file__)))) -ROOT_DIR = os.environ.get("CLIENT_CLI_DIR", BASE_DIR) -PATH_CONFIG_CLIENT_CLI = os.path.join(ROOT_DIR, "config.json") -CLIENT_CLI_PATH = os.path.join(ROOT_DIR, "iroha_client_cli") +PATH_CONFIG_CLIENT_CLI = os.environ["CLIENT_CLI_CONFIG"] +CLIENT_CLI_PATH = os.environ["CLIENT_CLI_BINARY"] PORT_MIN = int(os.getenv('TORII_API_PORT_MIN', '8080')) PORT_MAX = int(os.getenv('TORII_API_PORT_MAX', '8083')) diff --git a/configs/client/config.example.toml b/config_samples/examples/client.example.toml similarity index 88% rename from configs/client/config.example.toml rename to config_samples/examples/client.example.toml index 4ddcbbfc7da..ebf76b24b54 100644 --- a/configs/client/config.example.toml +++ b/config_samples/examples/client.example.toml @@ -8,10 +8,8 @@ private_key.payload = "9ac47abf59b356e0bd7dcbbbb4dec080e302156a48ca907e47cb6aea1 [api] torii_url = "http://127.0.0.1:8080/" - -[api.basic_auth] -login = "mad_hatter" -password = "ilovetea" +basic_auth.login = "mad_hatter" +basic_auth.password = "ilovetea" [transaction] time_to_live = 100_000 diff --git a/config_samples/examples/peer.example.toml b/config_samples/examples/peer.example.toml new file mode 100644 index 00000000000..3352266360e --- /dev/null +++ b/config_samples/examples/peer.example.toml @@ -0,0 +1 @@ +# TODO: show default values, point out required fields diff --git a/configs/prometheus.yml b/config_samples/examples/prometheus.example.yml similarity index 100% rename from configs/prometheus.yml rename to config_samples/examples/prometheus.example.yml diff --git a/config_samples/swarm/client.toml b/config_samples/swarm/client.toml new file mode 100644 index 00000000000..77aa53b5107 --- /dev/null +++ b/config_samples/swarm/client.toml @@ -0,0 +1,12 @@ +chain_id = "00000000-0000-0000-0000-000000000000" + +[account] +id = "alice@wonderland" +public_key = "ed01207233BFC89DCBD68C19FDE6CE6158225298EC1131B6A130D1AEB454C1AB5183C0" +private_key.digest_function = "ed25519" +private_key.payload = "9ac47abf59b356e0bd7dcbbbb4dec080e302156a48ca907e47cb6aea1d32719e7233bfc89dcbd68c19fde6ce6158225298ec1131b6a130d1aeb454c1ab5183c0" + +[api] +torii_url = "http://127.0.0.1:8080/" +basic_auth.web_login = "mad_hatter" +basic_auth.password = "ilovetea" diff --git a/docker-compose.local.yml b/config_samples/swarm/docker-compose.local.yml similarity index 50% rename from docker-compose.local.yml rename to config_samples/swarm/docker-compose.local.yml index 6c2cd371db2..3c7b9d8c2eb 100644 --- a/docker-compose.local.yml +++ b/config_samples/swarm/docker-compose.local.yml @@ -4,24 +4,25 @@ version: '3.8' services: iroha0: - build: ./ + build: ../.. platform: linux/amd64 environment: - IROHA_CHAIN_ID: 00000000-0000-0000-0000-000000000000 - IROHA_CONFIG: /config/config.json - IROHA_PUBLIC_KEY: ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB - IROHA_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb"}' - TORII_P2P_ADDR: 0.0.0.0:1337 - TORII_API_URL: 0.0.0.0:8080 - IROHA_GENESIS_PUBLIC_KEY: ed01204164BF554923ECE1FD412D241036D863A6AE430476C898248B8237D77534CFC4 - IROHA_GENESIS_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"82b3bde54aebeca4146257da0de8d59d8e46d5fe34887dcd8072866792fcb3ad4164bf554923ece1fd412d241036d863a6ae430476c898248b8237d77534cfc4"}' - IROHA_GENESIS_FILE: /config/genesis.json + CHAIN_ID: 00000000-0000-0000-0000-000000000000 + PUBLIC_KEY: ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB + PRIVATE_KEY_DIGEST: ed25519 + PRIVATE_KEY_PAYLOAD: 8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb + P2P_ADDRESS: 0.0.0.0:1337 + API_ADDRESS: 0.0.0.0:8080 + GENESIS_PUBLIC_KEY: ed01204164BF554923ECE1FD412D241036D863A6AE430476C898248B8237D77534CFC4 + GENESIS_PRIVATE_KEY_DIGEST: ed25519 + GENESIS_PRIVATE_KEY_PAYLOAD: 82b3bde54aebeca4146257da0de8d59d8e46d5fe34887dcd8072866792fcb3ad4164bf554923ece1fd412d241036d863a6ae430476c898248b8237d77534cfc4 + 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 - 8080:8080 volumes: - - ./configs/peer:/config + - ../../configs/peer:/config init: true command: iroha --submit-genesis healthcheck: @@ -31,22 +32,22 @@ services: retries: 30 start_period: 4s iroha1: - build: ./ + build: ../.. platform: linux/amd64 environment: - IROHA_CHAIN_ID: 00000000-0000-0000-0000-000000000000 - IROHA_CONFIG: /config/config.json - IROHA_PUBLIC_KEY: ed0120815BBDC9775D28C3633269B25F22D048E2AA2E36017CBE5AD85F15220BEB6F6F - IROHA_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"c02ffad5e455e7ec620d74de5769681e4d8385906bce5a437eb67452a9efbbc2815bbdc9775d28c3633269b25f22d048e2aa2e36017cbe5ad85f15220beb6f6f"}' - TORII_P2P_ADDR: 0.0.0.0:1338 - TORII_API_URL: 0.0.0.0:8081 - IROHA_GENESIS_PUBLIC_KEY: ed01204164BF554923ECE1FD412D241036D863A6AE430476C898248B8237D77534CFC4 + CHAIN_ID: 00000000-0000-0000-0000-000000000000 + PUBLIC_KEY: ed0120815BBDC9775D28C3633269B25F22D048E2AA2E36017CBE5AD85F15220BEB6F6F + PRIVATE_KEY_DIGEST: ed25519 + PRIVATE_KEY_PAYLOAD: c02ffad5e455e7ec620d74de5769681e4d8385906bce5a437eb67452a9efbbc2815bbdc9775d28c3633269b25f22d048e2aa2e36017cbe5ad85f15220beb6f6f + P2P_ADDRESS: 0.0.0.0:1338 + API_ADDRESS: 0.0.0.0:8081 + 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 - 8081:8081 volumes: - - ./configs/peer:/config + - ../../configs/peer:/config init: true healthcheck: test: test $(curl -s http://127.0.0.1:8081/status/blocks) -gt 0 @@ -55,22 +56,22 @@ services: retries: 30 start_period: 4s iroha2: - build: ./ + build: ../.. platform: linux/amd64 environment: - IROHA_CHAIN_ID: 00000000-0000-0000-0000-000000000000 - IROHA_CONFIG: /config/config.json - IROHA_PUBLIC_KEY: ed0120F417E0371E6ADB32FD66749477402B1AB67F84A8E9B082E997980CC91F327736 - IROHA_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"29c5ed1409cb10fd791bc4ff8a6cb5e22a5fae7e36f448ef3ea2988b1319a88bf417e0371e6adb32fd66749477402b1ab67f84a8e9b082e997980cc91f327736"}' - TORII_P2P_ADDR: 0.0.0.0:1339 - TORII_API_URL: 0.0.0.0:8082 - IROHA_GENESIS_PUBLIC_KEY: ed01204164BF554923ECE1FD412D241036D863A6AE430476C898248B8237D77534CFC4 + CHAIN_ID: 00000000-0000-0000-0000-000000000000 + PUBLIC_KEY: ed0120F417E0371E6ADB32FD66749477402B1AB67F84A8E9B082E997980CC91F327736 + PRIVATE_KEY_DIGEST: ed25519 + PRIVATE_KEY_PAYLOAD: 29c5ed1409cb10fd791bc4ff8a6cb5e22a5fae7e36f448ef3ea2988b1319a88bf417e0371e6adb32fd66749477402b1ab67f84a8e9b082e997980cc91f327736 + P2P_ADDRESS: 0.0.0.0:1339 + API_ADDRESS: 0.0.0.0:8082 + 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 - 8082:8082 volumes: - - ./configs/peer:/config + - ../../configs/peer:/config init: true healthcheck: test: test $(curl -s http://127.0.0.1:8082/status/blocks) -gt 0 @@ -79,22 +80,22 @@ services: retries: 30 start_period: 4s iroha3: - build: ./ + build: ../.. platform: linux/amd64 environment: - IROHA_CHAIN_ID: 00000000-0000-0000-0000-000000000000 - IROHA_CONFIG: /config/config.json - IROHA_PUBLIC_KEY: ed0120A66522370D60B9C09E79ADE2E9BB1EF2E78733A944B999B3A6AEE687CE476D61 - IROHA_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"5eed4855fad183c451aac39dfc50831607e4cf408c98e2b977f3ce4a2df42ce2a66522370d60b9c09e79ade2e9bb1ef2e78733a944b999b3a6aee687ce476d61"}' - TORII_P2P_ADDR: 0.0.0.0:1340 - TORII_API_URL: 0.0.0.0:8083 - IROHA_GENESIS_PUBLIC_KEY: ed01204164BF554923ECE1FD412D241036D863A6AE430476C898248B8237D77534CFC4 + CHAIN_ID: 00000000-0000-0000-0000-000000000000 + PUBLIC_KEY: ed0120A66522370D60B9C09E79ADE2E9BB1EF2E78733A944B999B3A6AEE687CE476D61 + PRIVATE_KEY_DIGEST: ed25519 + PRIVATE_KEY_PAYLOAD: 5eed4855fad183c451aac39dfc50831607e4cf408c98e2b977f3ce4a2df42ce2a66522370d60b9c09e79ade2e9bb1ef2e78733a944b999b3a6aee687ce476d61 + P2P_ADDRESS: 0.0.0.0:1340 + API_ADDRESS: 0.0.0.0:8083 + 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 - 8083:8083 volumes: - - ./configs/peer:/config + - ../../configs/peer:/config init: true healthcheck: test: test $(curl -s http://127.0.0.1:8083/status/blocks) -gt 0 diff --git a/config_samples/swarm/docker-compose.single.yml b/config_samples/swarm/docker-compose.single.yml new file mode 100644 index 00000000000..697c7fe3588 --- /dev/null +++ b/config_samples/swarm/docker-compose.single.yml @@ -0,0 +1,32 @@ +# This file is generated by iroha_swarm. +# Do not edit it manually. + +version: '3.8' +services: + iroha0: + build: ../.. + platform: linux/amd64 + environment: + CHAIN_ID: 00000000-0000-0000-0000-000000000000 + PUBLIC_KEY: ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB + PRIVATE_KEY_DIGEST: ed25519 + PRIVATE_KEY_PAYLOAD: 8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb + P2P_ADDRESS: 0.0.0.0:1337 + API_ADDRESS: 0.0.0.0:8080 + GENESIS_PUBLIC_KEY: ed01204164BF554923ECE1FD412D241036D863A6AE430476C898248B8237D77534CFC4 + GENESIS_PRIVATE_KEY_DIGEST: ed25519 + GENESIS_PRIVATE_KEY_PAYLOAD: 82b3bde54aebeca4146257da0de8d59d8e46d5fe34887dcd8072866792fcb3ad4164bf554923ece1fd412d241036d863a6ae430476c898248b8237d77534cfc4 + GENESIS_FILE: /config/genesis.json + ports: + - 1337:1337 + - 8080:8080 + volumes: + - ../../configs/peer:/config + init: true + command: iroha --submit-genesis + healthcheck: + test: test $(curl -s http://127.0.0.1:8080/status/blocks) -gt 0 + interval: 2s + timeout: 1s + retries: 30 + start_period: 4s diff --git a/docker-compose.yml b/config_samples/swarm/docker-compose.yml similarity index 52% rename from docker-compose.yml rename to config_samples/swarm/docker-compose.yml index af679a88066..b90fe946091 100644 --- a/docker-compose.yml +++ b/config_samples/swarm/docker-compose.yml @@ -7,21 +7,22 @@ services: image: hyperledger/iroha2:dev platform: linux/amd64 environment: - IROHA_CHAIN_ID: 00000000-0000-0000-0000-000000000000 - IROHA_CONFIG: /config/config.json - IROHA_PUBLIC_KEY: ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB - IROHA_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb"}' - TORII_P2P_ADDR: 0.0.0.0:1337 - TORII_API_URL: 0.0.0.0:8080 - IROHA_GENESIS_PUBLIC_KEY: ed01204164BF554923ECE1FD412D241036D863A6AE430476C898248B8237D77534CFC4 - IROHA_GENESIS_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"82b3bde54aebeca4146257da0de8d59d8e46d5fe34887dcd8072866792fcb3ad4164bf554923ece1fd412d241036d863a6ae430476c898248b8237d77534cfc4"}' - IROHA_GENESIS_FILE: /config/genesis.json + CHAIN_ID: 00000000-0000-0000-0000-000000000000 + PUBLIC_KEY: ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB + PRIVATE_KEY_DIGEST: ed25519 + PRIVATE_KEY_PAYLOAD: 8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb + P2P_ADDRESS: 0.0.0.0:1337 + API_ADDRESS: 0.0.0.0:8080 + GENESIS_PUBLIC_KEY: ed01204164BF554923ECE1FD412D241036D863A6AE430476C898248B8237D77534CFC4 + GENESIS_PRIVATE_KEY_DIGEST: ed25519 + GENESIS_PRIVATE_KEY_PAYLOAD: 82b3bde54aebeca4146257da0de8d59d8e46d5fe34887dcd8072866792fcb3ad4164bf554923ece1fd412d241036d863a6ae430476c898248b8237d77534cfc4 + 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 - 8080:8080 volumes: - - ./configs/peer:/config + - ../../configs/peer:/config init: true command: iroha --submit-genesis healthcheck: @@ -34,19 +35,19 @@ services: image: hyperledger/iroha2:dev platform: linux/amd64 environment: - IROHA_CHAIN_ID: 00000000-0000-0000-0000-000000000000 - IROHA_CONFIG: /config/config.json - IROHA_PUBLIC_KEY: ed0120815BBDC9775D28C3633269B25F22D048E2AA2E36017CBE5AD85F15220BEB6F6F - IROHA_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"c02ffad5e455e7ec620d74de5769681e4d8385906bce5a437eb67452a9efbbc2815bbdc9775d28c3633269b25f22d048e2aa2e36017cbe5ad85f15220beb6f6f"}' - TORII_P2P_ADDR: 0.0.0.0:1338 - TORII_API_URL: 0.0.0.0:8081 - IROHA_GENESIS_PUBLIC_KEY: ed01204164BF554923ECE1FD412D241036D863A6AE430476C898248B8237D77534CFC4 + CHAIN_ID: 00000000-0000-0000-0000-000000000000 + PUBLIC_KEY: ed0120815BBDC9775D28C3633269B25F22D048E2AA2E36017CBE5AD85F15220BEB6F6F + PRIVATE_KEY_DIGEST: ed25519 + PRIVATE_KEY_PAYLOAD: c02ffad5e455e7ec620d74de5769681e4d8385906bce5a437eb67452a9efbbc2815bbdc9775d28c3633269b25f22d048e2aa2e36017cbe5ad85f15220beb6f6f + P2P_ADDRESS: 0.0.0.0:1338 + API_ADDRESS: 0.0.0.0:8081 + 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 - 8081:8081 volumes: - - ./configs/peer:/config + - ../../configs/peer:/config init: true healthcheck: test: test $(curl -s http://127.0.0.1:8081/status/blocks) -gt 0 @@ -58,19 +59,19 @@ services: image: hyperledger/iroha2:dev platform: linux/amd64 environment: - IROHA_CHAIN_ID: 00000000-0000-0000-0000-000000000000 - IROHA_CONFIG: /config/config.json - IROHA_PUBLIC_KEY: ed0120F417E0371E6ADB32FD66749477402B1AB67F84A8E9B082E997980CC91F327736 - IROHA_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"29c5ed1409cb10fd791bc4ff8a6cb5e22a5fae7e36f448ef3ea2988b1319a88bf417e0371e6adb32fd66749477402b1ab67f84a8e9b082e997980cc91f327736"}' - TORII_P2P_ADDR: 0.0.0.0:1339 - TORII_API_URL: 0.0.0.0:8082 - IROHA_GENESIS_PUBLIC_KEY: ed01204164BF554923ECE1FD412D241036D863A6AE430476C898248B8237D77534CFC4 + CHAIN_ID: 00000000-0000-0000-0000-000000000000 + PUBLIC_KEY: ed0120F417E0371E6ADB32FD66749477402B1AB67F84A8E9B082E997980CC91F327736 + PRIVATE_KEY_DIGEST: ed25519 + PRIVATE_KEY_PAYLOAD: 29c5ed1409cb10fd791bc4ff8a6cb5e22a5fae7e36f448ef3ea2988b1319a88bf417e0371e6adb32fd66749477402b1ab67f84a8e9b082e997980cc91f327736 + P2P_ADDRESS: 0.0.0.0:1339 + API_ADDRESS: 0.0.0.0:8082 + 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 - 8082:8082 volumes: - - ./configs/peer:/config + - ../../configs/peer:/config init: true healthcheck: test: test $(curl -s http://127.0.0.1:8082/status/blocks) -gt 0 @@ -82,19 +83,19 @@ services: image: hyperledger/iroha2:dev platform: linux/amd64 environment: - IROHA_CHAIN_ID: 00000000-0000-0000-0000-000000000000 - IROHA_CONFIG: /config/config.json - IROHA_PUBLIC_KEY: ed0120A66522370D60B9C09E79ADE2E9BB1EF2E78733A944B999B3A6AEE687CE476D61 - IROHA_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"5eed4855fad183c451aac39dfc50831607e4cf408c98e2b977f3ce4a2df42ce2a66522370d60b9c09e79ade2e9bb1ef2e78733a944b999b3a6aee687ce476d61"}' - TORII_P2P_ADDR: 0.0.0.0:1340 - TORII_API_URL: 0.0.0.0:8083 - IROHA_GENESIS_PUBLIC_KEY: ed01204164BF554923ECE1FD412D241036D863A6AE430476C898248B8237D77534CFC4 + CHAIN_ID: 00000000-0000-0000-0000-000000000000 + PUBLIC_KEY: ed0120A66522370D60B9C09E79ADE2E9BB1EF2E78733A944B999B3A6AEE687CE476D61 + PRIVATE_KEY_DIGEST: ed25519 + PRIVATE_KEY_PAYLOAD: 5eed4855fad183c451aac39dfc50831607e4cf408c98e2b977f3ce4a2df42ce2a66522370d60b9c09e79ade2e9bb1ef2e78733a944b999b3a6aee687ce476d61 + P2P_ADDRESS: 0.0.0.0:1340 + API_ADDRESS: 0.0.0.0:8083 + 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 - 8083:8083 volumes: - - ./configs/peer:/config + - ../../configs/peer:/config init: true healthcheck: test: test $(curl -s http://127.0.0.1:8083/status/blocks) -gt 0 diff --git a/configs/peer/executor.wasm b/config_samples/swarm/executor.wasm similarity index 100% rename from configs/peer/executor.wasm rename to config_samples/swarm/executor.wasm diff --git a/configs/peer/genesis.json b/config_samples/swarm/genesis.json similarity index 100% rename from configs/peer/genesis.json rename to config_samples/swarm/genesis.json diff --git a/configs/client/config.json b/configs/client/config.json deleted file mode 100644 index b8a507409ac..00000000000 --- a/configs/client/config.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "CHAIN_ID": "00000000-0000-0000-0000-000000000000", - "PUBLIC_KEY": "ed01207233BFC89DCBD68C19FDE6CE6158225298EC1131B6A130D1AEB454C1AB5183C0", - "PRIVATE_KEY": { - "digest_function": "ed25519", - "payload": "9ac47abf59b356e0bd7dcbbbb4dec080e302156a48ca907e47cb6aea1d32719e7233bfc89dcbd68c19fde6ce6158225298ec1131b6a130d1aeb454c1ab5183c0" - }, - "ACCOUNT_ID": "alice@wonderland", - "BASIC_AUTH": { - "web_login": "mad_hatter", - "password": "ilovetea" - }, - "TORII_API_URL": "http://127.0.0.1:8080/", - "TRANSACTION_TIME_TO_LIVE_MS": 100000, - "TRANSACTION_STATUS_TIMEOUT_MS": 15000, - "ADD_TRANSACTION_NONCE": false -} diff --git a/configs/client/lts/config.json b/configs/client/lts/config.json deleted file mode 100644 index e1763c4d801..00000000000 --- a/configs/client/lts/config.json +++ /dev/null @@ -1,95 +0,0 @@ -{ - "PUBLIC_KEY": null, - "PRIVATE_KEY": null, - "DISABLE_PANIC_TERMINAL_COLORS": false, - "KURA": { - "INIT_MODE": "strict", - "BLOCK_STORE_PATH": "./storage", - "BLOCKS_PER_STORAGE_FILE": 1000, - "ACTOR_CHANNEL_CAPACITY": 100, - "DEBUG_OUTPUT_NEW_BLOCKS": false - }, - "SUMERAGI": { - "KEY_PAIR": null, - "PEER_ID": null, - "BLOCK_TIME_MS": 1000, - "TRUSTED_PEERS": null, - "COMMIT_TIME_LIMIT_MS": 2000, - "TX_RECEIPT_TIME_LIMIT_MS": 500, - "TRANSACTION_LIMITS": { - "max_instruction_number": 4096, - "max_wasm_size_bytes": 4194304 - }, - "ACTOR_CHANNEL_CAPACITY": 100, - "GOSSIP_BATCH_SIZE": 500, - "GOSSIP_PERIOD_MS": 1000 - }, - "TORII": { - "P2P_ADDR": null, - "API_URL": null, - "TELEMETRY_URL": null, - "MAX_TRANSACTION_SIZE": 32768, - "MAX_CONTENT_LEN": 16384000 - }, - "BLOCK_SYNC": { - "GOSSIP_PERIOD_MS": 10000, - "BLOCK_BATCH_SIZE": 4, - "ACTOR_CHANNEL_CAPACITY": 100 - }, - "QUEUE": { - "MAXIMUM_TRANSACTIONS_IN_BLOCK": 8192, - "MAXIMUM_TRANSACTIONS_IN_QUEUE": 65536, - "TRANSACTION_TIME_TO_LIVE_MS": 86400000, - "FUTURE_THRESHOLD_MS": 1000 - }, - "LOGGER": { - "MAX_LOG_LEVEL": "INFO", - "TELEMETRY_CAPACITY": 1000, - "COMPACT_MODE": false, - "LOG_FILE_PATH": null, - "TERMINAL_COLORS": true - }, - "GENESIS": { - "ACCOUNT_PUBLIC_KEY": null, - "ACCOUNT_PRIVATE_KEY": null, - "WAIT_FOR_PEERS_RETRY_COUNT_LIMIT": 100, - "WAIT_FOR_PEERS_RETRY_PERIOD_MS": 500, - "GENESIS_SUBMISSION_DELAY_MS": 1000 - }, - "WSV": { - "ASSET_METADATA_LIMITS": { - "max_len": 1048576, - "max_entry_byte_size": 4096 - }, - "ASSET_DEFINITION_METADATA_LIMITS": { - "max_len": 1048576, - "max_entry_byte_size": 4096 - }, - "ACCOUNT_METADATA_LIMITS": { - "max_len": 1048576, - "max_entry_byte_size": 4096 - }, - "DOMAIN_METADATA_LIMITS": { - "max_len": 1048576, - "max_entry_byte_size": 4096 - }, - "IDENT_LENGTH_LIMITS": { - "min": 1, - "max": 128 - }, - "WASM_RUNTIME_CONFIG": { - "FUEL_LIMIT": 1000000, - "MAX_MEMORY": 524288000 - } - }, - "NETWORK": { - "ACTOR_CHANNEL_CAPACITY": 100 - }, - "TELEMETRY": { - "NAME": null, - "URL": null, - "MIN_RETRY_PERIOD": 1, - "MAX_RETRY_DELAY_EXPONENT": 4, - "FILE": null - } -} diff --git a/configs/client/stable/config.json b/configs/client/stable/config.json deleted file mode 100644 index e1763c4d801..00000000000 --- a/configs/client/stable/config.json +++ /dev/null @@ -1,95 +0,0 @@ -{ - "PUBLIC_KEY": null, - "PRIVATE_KEY": null, - "DISABLE_PANIC_TERMINAL_COLORS": false, - "KURA": { - "INIT_MODE": "strict", - "BLOCK_STORE_PATH": "./storage", - "BLOCKS_PER_STORAGE_FILE": 1000, - "ACTOR_CHANNEL_CAPACITY": 100, - "DEBUG_OUTPUT_NEW_BLOCKS": false - }, - "SUMERAGI": { - "KEY_PAIR": null, - "PEER_ID": null, - "BLOCK_TIME_MS": 1000, - "TRUSTED_PEERS": null, - "COMMIT_TIME_LIMIT_MS": 2000, - "TX_RECEIPT_TIME_LIMIT_MS": 500, - "TRANSACTION_LIMITS": { - "max_instruction_number": 4096, - "max_wasm_size_bytes": 4194304 - }, - "ACTOR_CHANNEL_CAPACITY": 100, - "GOSSIP_BATCH_SIZE": 500, - "GOSSIP_PERIOD_MS": 1000 - }, - "TORII": { - "P2P_ADDR": null, - "API_URL": null, - "TELEMETRY_URL": null, - "MAX_TRANSACTION_SIZE": 32768, - "MAX_CONTENT_LEN": 16384000 - }, - "BLOCK_SYNC": { - "GOSSIP_PERIOD_MS": 10000, - "BLOCK_BATCH_SIZE": 4, - "ACTOR_CHANNEL_CAPACITY": 100 - }, - "QUEUE": { - "MAXIMUM_TRANSACTIONS_IN_BLOCK": 8192, - "MAXIMUM_TRANSACTIONS_IN_QUEUE": 65536, - "TRANSACTION_TIME_TO_LIVE_MS": 86400000, - "FUTURE_THRESHOLD_MS": 1000 - }, - "LOGGER": { - "MAX_LOG_LEVEL": "INFO", - "TELEMETRY_CAPACITY": 1000, - "COMPACT_MODE": false, - "LOG_FILE_PATH": null, - "TERMINAL_COLORS": true - }, - "GENESIS": { - "ACCOUNT_PUBLIC_KEY": null, - "ACCOUNT_PRIVATE_KEY": null, - "WAIT_FOR_PEERS_RETRY_COUNT_LIMIT": 100, - "WAIT_FOR_PEERS_RETRY_PERIOD_MS": 500, - "GENESIS_SUBMISSION_DELAY_MS": 1000 - }, - "WSV": { - "ASSET_METADATA_LIMITS": { - "max_len": 1048576, - "max_entry_byte_size": 4096 - }, - "ASSET_DEFINITION_METADATA_LIMITS": { - "max_len": 1048576, - "max_entry_byte_size": 4096 - }, - "ACCOUNT_METADATA_LIMITS": { - "max_len": 1048576, - "max_entry_byte_size": 4096 - }, - "DOMAIN_METADATA_LIMITS": { - "max_len": 1048576, - "max_entry_byte_size": 4096 - }, - "IDENT_LENGTH_LIMITS": { - "min": 1, - "max": 128 - }, - "WASM_RUNTIME_CONFIG": { - "FUEL_LIMIT": 1000000, - "MAX_MEMORY": 524288000 - } - }, - "NETWORK": { - "ACTOR_CHANNEL_CAPACITY": 100 - }, - "TELEMETRY": { - "NAME": null, - "URL": null, - "MIN_RETRY_PERIOD": 1, - "MAX_RETRY_DELAY_EXPONENT": 4, - "FILE": null - } -} diff --git a/configs/peer/config.json b/configs/peer/config.json index 649b25f31c4..e69de29bb2d 100644 --- a/configs/peer/config.json +++ b/configs/peer/config.json @@ -1,95 +0,0 @@ -{ - "CHAIN_ID": null, - "PUBLIC_KEY": null, - "PRIVATE_KEY": null, - "KURA": { - "INIT_MODE": "strict", - "BLOCK_STORE_PATH": "./storage", - "DEBUG_OUTPUT_NEW_BLOCKS": false - }, - "SUMERAGI": { - "KEY_PAIR": null, - "PEER_ID": null, - "BLOCK_TIME_MS": 2000, - "TRUSTED_PEERS": null, - "COMMIT_TIME_LIMIT_MS": 4000, - "MAX_TRANSACTIONS_IN_BLOCK": 512, - "ACTOR_CHANNEL_CAPACITY": 100, - "GOSSIP_BATCH_SIZE": 500, - "GOSSIP_PERIOD_MS": 1000 - }, - "TORII": { - "P2P_ADDR": null, - "API_URL": null, - "MAX_TRANSACTION_SIZE": 32768, - "MAX_CONTENT_LEN": 16384000 - }, - "BLOCK_SYNC": { - "GOSSIP_PERIOD_MS": 10000, - "BLOCK_BATCH_SIZE": 4, - "ACTOR_CHANNEL_CAPACITY": 100 - }, - "QUEUE": { - "MAX_TRANSACTIONS_IN_QUEUE": 65536, - "MAX_TRANSACTIONS_IN_QUEUE_PER_USER": 65536, - "TRANSACTION_TIME_TO_LIVE_MS": 86400000, - "FUTURE_THRESHOLD_MS": 1000 - }, - "LOGGER": { - "LEVEL": "INFO", - "FORMAT": "full" - }, - "GENESIS": { - "PUBLIC_KEY": null, - "PRIVATE_KEY": null, - "FILE": null - }, - "WSV": { - "ASSET_METADATA_LIMITS": { - "max_len": 1048576, - "max_entry_byte_size": 4096 - }, - "ASSET_DEFINITION_METADATA_LIMITS": { - "max_len": 1048576, - "max_entry_byte_size": 4096 - }, - "ACCOUNT_METADATA_LIMITS": { - "max_len": 1048576, - "max_entry_byte_size": 4096 - }, - "DOMAIN_METADATA_LIMITS": { - "max_len": 1048576, - "max_entry_byte_size": 4096 - }, - "IDENT_LENGTH_LIMITS": { - "min": 1, - "max": 128 - }, - "TRANSACTION_LIMITS": { - "max_instruction_number": 4096, - "max_wasm_size_bytes": 4194304 - }, - "WASM_RUNTIME_CONFIG": { - "FUEL_LIMIT": 55000000, - "MAX_MEMORY": 524288000 - } - }, - "NETWORK": { - "ACTOR_CHANNEL_CAPACITY": 100 - }, - "TELEMETRY": { - "NAME": null, - "URL": null, - "MIN_RETRY_PERIOD": 1, - "MAX_RETRY_DELAY_EXPONENT": 4, - "FILE": null - }, - "SNAPSHOT": { - "CREATE_EVERY_MS": 60000, - "DIR_PATH": "./storage", - "CREATION_ENABLED": true - }, - "LIVE_QUERY_STORE": { - "QUERY_IDLE_TIME_MS": 30000 - } -} diff --git a/configs/peer/lts/config.json b/configs/peer/lts/config.json deleted file mode 100644 index ef36a9f525c..00000000000 --- a/configs/peer/lts/config.json +++ /dev/null @@ -1,98 +0,0 @@ -{ - "PUBLIC_KEY": null, - "PRIVATE_KEY": null, - "DISABLE_PANIC_TERMINAL_COLORS": false, - "KURA": { - "INIT_MODE": "strict", - "BLOCK_STORE_PATH": "./storage", - "BLOCKS_PER_STORAGE_FILE": 1000, - "ACTOR_CHANNEL_CAPACITY": 100, - "DEBUG_OUTPUT_NEW_BLOCKS": false - }, - "SUMERAGI": { - "KEY_PAIR": null, - "PEER_ID": null, - "BLOCK_TIME_MS": 2000, - "TRUSTED_PEERS": null, - "COMMIT_TIME_LIMIT_MS": 4000, - "MAX_TRANSACTIONS_IN_BLOCK": 512, - "ACTOR_CHANNEL_CAPACITY": 100, - "GOSSIP_BATCH_SIZE": 500, - "GOSSIP_PERIOD_MS": 1000 - }, - "TORII": { - "P2P_ADDR": null, - "API_URL": null, - "MAX_TRANSACTION_SIZE": 32768, - "MAX_CONTENT_LEN": 16384000, - "FETCH_SIZE": 10, - "QUERY_IDLE_TIME_MS": 30000 - }, - "BLOCK_SYNC": { - "GOSSIP_PERIOD_MS": 10000, - "BLOCK_BATCH_SIZE": 4, - "ACTOR_CHANNEL_CAPACITY": 100 - }, - "QUEUE": { - "MAX_TRANSACTIONS_IN_QUEUE": 65536, - "MAX_TRANSACTIONS_IN_QUEUE_PER_USER": 65536, - "TRANSACTION_TIME_TO_LIVE_MS": 86400000, - "FUTURE_THRESHOLD_MS": 1000 - }, - "LOGGER": { - "MAX_LOG_LEVEL": "INFO", - "TELEMETRY_CAPACITY": 1000, - "COMPACT_MODE": false, - "LOG_FILE_PATH": null, - "TERMINAL_COLORS": true - }, - "GENESIS": { - "ACCOUNT_PUBLIC_KEY": null, - "ACCOUNT_PRIVATE_KEY": null - }, - "WSV": { - "ASSET_METADATA_LIMITS": { - "max_len": 1048576, - "max_entry_byte_size": 4096 - }, - "ASSET_DEFINITION_METADATA_LIMITS": { - "max_len": 1048576, - "max_entry_byte_size": 4096 - }, - "ACCOUNT_METADATA_LIMITS": { - "max_len": 1048576, - "max_entry_byte_size": 4096 - }, - "DOMAIN_METADATA_LIMITS": { - "max_len": 1048576, - "max_entry_byte_size": 4096 - }, - "IDENT_LENGTH_LIMITS": { - "min": 1, - "max": 128 - }, - "TRANSACTION_LIMITS": { - "max_instruction_number": 4096, - "max_wasm_size_bytes": 4194304 - }, - "WASM_RUNTIME_CONFIG": { - "FUEL_LIMIT": 23000000, - "MAX_MEMORY": 524288000 - } - }, - "NETWORK": { - "ACTOR_CHANNEL_CAPACITY": 100 - }, - "TELEMETRY": { - "NAME": null, - "URL": null, - "MIN_RETRY_PERIOD": 1, - "MAX_RETRY_DELAY_EXPONENT": 4, - "FILE": null - }, - "SNAPSHOT": { - "CREATE_EVERY_MS": 60000, - "DIR_PATH": "./storage", - "CREATION_ENABLED": true - } -} diff --git a/configs/peer/lts/executor.wasm b/configs/peer/lts/executor.wasm deleted file mode 100644 index 544c9e29dfa4cb65d7bdae1f6ccd1e2cbb3e8fb2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 501157 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

w6v6Y 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

4QR6$WMWHA6qEt{+0bfc`Ta`WMhoag_Rz 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 T0*gHt-N3_*$^5S;4%GY~ehcff diff --git a/configs/peer/lts/genesis.json b/configs/peer/lts/genesis.json deleted file mode 100644 index 2ca5d0365ed..00000000000 --- a/configs/peer/lts/genesis.json +++ /dev/null @@ -1,201 +0,0 @@ -{ - "transactions": [ - [ - { - "Register": { - "NewDomain": { - "id": "wonderland", - "logo": null, - "metadata": { - "key": { - "String": "value" - } - } - } - } - }, - { - "Register": { - "NewAccount": { - "id": "alice@wonderland", - "signatories": [ - "ed01207233BFC89DCBD68C19FDE6CE6158225298EC1131B6A130D1AEB454C1AB5183C0" - ], - "metadata": { - "key": { - "String": "value" - } - } - } - } - }, - { - "Register": { - "NewAccount": { - "id": "bob@wonderland", - "signatories": [ - "ed01207233BFC89DCBD68C19FDE6CE6158225298EC1131B6A130D1AEB454C1AB5183C0" - ], - "metadata": { - "key": { - "String": "value" - } - } - } - } - }, - { - "Register": { - "NewAssetDefinition": { - "id": "rose#wonderland", - "value_type": "Quantity", - "mintable": "Infinitely", - "logo": null, - "metadata": {} - } - } - }, - { - "Register": { - "NewDomain": { - "id": "garden_of_live_flowers", - "logo": null, - "metadata": {} - } - } - }, - { - "Register": { - "NewAccount": { - "id": "carpenter@garden_of_live_flowers", - "signatories": [ - "ed01207233BFC89DCBD68C19FDE6CE6158225298EC1131B6A130D1AEB454C1AB5183C0" - ], - "metadata": {} - } - } - }, - { - "Register": { - "NewAssetDefinition": { - "id": "cabbage#garden_of_live_flowers", - "value_type": "Quantity", - "mintable": "Infinitely", - "logo": null, - "metadata": {} - } - } - }, - { - "Mint": { - "object": "13_u32", - "destination_id": { - "AssetId": "rose##alice@wonderland" - } - } - }, - { - "Mint": { - "object": "44_u32", - "destination_id": { - "AssetId": "cabbage#garden_of_live_flowers#alice@wonderland" - } - } - }, - { - "Grant": { - "object": { - "PermissionToken": { - "definition_id": "CanSetParameters", - "payload": null - } - }, - "destination_id": { - "AccountId": "alice@wonderland" - } - } - }, - { - "Sequence": [ - { - "NewParameter": { - "Parameter": "?MaxTransactionsInBlock=512" - } - }, - { - "NewParameter": { - "Parameter": "?BlockTime=2000" - } - }, - { - "NewParameter": { - "Parameter": "?CommitTimeLimit=4000" - } - }, - { - "NewParameter": { - "Parameter": "?TransactionLimits=4096,4194304_TL" - } - }, - { - "NewParameter": { - "Parameter": "?WSVAssetMetadataLimits=1048576,4096_ML" - } - }, - { - "NewParameter": { - "Parameter": "?WSVAssetDefinitionMetadataLimits=1048576,4096_ML" - } - }, - { - "NewParameter": { - "Parameter": "?WSVAccountMetadataLimits=1048576,4096_ML" - } - }, - { - "NewParameter": { - "Parameter": "?WSVDomainMetadataLimits=1048576,4096_ML" - } - }, - { - "NewParameter": { - "Parameter": "?WSVIdentLengthLimits=1,128_LL" - } - }, - { - "NewParameter": { - "Parameter": "?WASMFuelLimit=23000000" - } - }, - { - "NewParameter": { - "Parameter": "?WASMMaxMemory=524288000" - } - } - ] - }, - { - "Register": { - "NewRole": { - "id": "ALICE_METADATA_ACCESS", - "permissions": [ - { - "definition_id": "CanRemoveKeyValueInUserAccount", - "payload": { - "account_id": "alice@wonderland" - } - }, - { - "definition_id": "CanSetKeyValueInUserAccount", - "payload": { - "account_id": "alice@wonderland" - } - } - ] - } - } - } - ] - ], - "executor": "./executor.wasm" -} diff --git a/configs/peer/stable/config.json b/configs/peer/stable/config.json deleted file mode 100644 index ef36a9f525c..00000000000 --- a/configs/peer/stable/config.json +++ /dev/null @@ -1,98 +0,0 @@ -{ - "PUBLIC_KEY": null, - "PRIVATE_KEY": null, - "DISABLE_PANIC_TERMINAL_COLORS": false, - "KURA": { - "INIT_MODE": "strict", - "BLOCK_STORE_PATH": "./storage", - "BLOCKS_PER_STORAGE_FILE": 1000, - "ACTOR_CHANNEL_CAPACITY": 100, - "DEBUG_OUTPUT_NEW_BLOCKS": false - }, - "SUMERAGI": { - "KEY_PAIR": null, - "PEER_ID": null, - "BLOCK_TIME_MS": 2000, - "TRUSTED_PEERS": null, - "COMMIT_TIME_LIMIT_MS": 4000, - "MAX_TRANSACTIONS_IN_BLOCK": 512, - "ACTOR_CHANNEL_CAPACITY": 100, - "GOSSIP_BATCH_SIZE": 500, - "GOSSIP_PERIOD_MS": 1000 - }, - "TORII": { - "P2P_ADDR": null, - "API_URL": null, - "MAX_TRANSACTION_SIZE": 32768, - "MAX_CONTENT_LEN": 16384000, - "FETCH_SIZE": 10, - "QUERY_IDLE_TIME_MS": 30000 - }, - "BLOCK_SYNC": { - "GOSSIP_PERIOD_MS": 10000, - "BLOCK_BATCH_SIZE": 4, - "ACTOR_CHANNEL_CAPACITY": 100 - }, - "QUEUE": { - "MAX_TRANSACTIONS_IN_QUEUE": 65536, - "MAX_TRANSACTIONS_IN_QUEUE_PER_USER": 65536, - "TRANSACTION_TIME_TO_LIVE_MS": 86400000, - "FUTURE_THRESHOLD_MS": 1000 - }, - "LOGGER": { - "MAX_LOG_LEVEL": "INFO", - "TELEMETRY_CAPACITY": 1000, - "COMPACT_MODE": false, - "LOG_FILE_PATH": null, - "TERMINAL_COLORS": true - }, - "GENESIS": { - "ACCOUNT_PUBLIC_KEY": null, - "ACCOUNT_PRIVATE_KEY": null - }, - "WSV": { - "ASSET_METADATA_LIMITS": { - "max_len": 1048576, - "max_entry_byte_size": 4096 - }, - "ASSET_DEFINITION_METADATA_LIMITS": { - "max_len": 1048576, - "max_entry_byte_size": 4096 - }, - "ACCOUNT_METADATA_LIMITS": { - "max_len": 1048576, - "max_entry_byte_size": 4096 - }, - "DOMAIN_METADATA_LIMITS": { - "max_len": 1048576, - "max_entry_byte_size": 4096 - }, - "IDENT_LENGTH_LIMITS": { - "min": 1, - "max": 128 - }, - "TRANSACTION_LIMITS": { - "max_instruction_number": 4096, - "max_wasm_size_bytes": 4194304 - }, - "WASM_RUNTIME_CONFIG": { - "FUEL_LIMIT": 23000000, - "MAX_MEMORY": 524288000 - } - }, - "NETWORK": { - "ACTOR_CHANNEL_CAPACITY": 100 - }, - "TELEMETRY": { - "NAME": null, - "URL": null, - "MIN_RETRY_PERIOD": 1, - "MAX_RETRY_DELAY_EXPONENT": 4, - "FILE": null - }, - "SNAPSHOT": { - "CREATE_EVERY_MS": 60000, - "DIR_PATH": "./storage", - "CREATION_ENABLED": true - } -} diff --git a/configs/peer/stable/executor.wasm b/configs/peer/stable/executor.wasm deleted file mode 100644 index 544c9e29dfa4cb65d7bdae1f6ccd1e2cbb3e8fb2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 501157 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

w6v6Y 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

4QR6$WMWHA6qEt{+0bfc`Ta`WMhoag_Rz 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 T0*gHt-N3_*$^5S;4%GY~ehcff diff --git a/configs/peer/stable/genesis.json b/configs/peer/stable/genesis.json deleted file mode 100644 index 2ca5d0365ed..00000000000 --- a/configs/peer/stable/genesis.json +++ /dev/null @@ -1,201 +0,0 @@ -{ - "transactions": [ - [ - { - "Register": { - "NewDomain": { - "id": "wonderland", - "logo": null, - "metadata": { - "key": { - "String": "value" - } - } - } - } - }, - { - "Register": { - "NewAccount": { - "id": "alice@wonderland", - "signatories": [ - "ed01207233BFC89DCBD68C19FDE6CE6158225298EC1131B6A130D1AEB454C1AB5183C0" - ], - "metadata": { - "key": { - "String": "value" - } - } - } - } - }, - { - "Register": { - "NewAccount": { - "id": "bob@wonderland", - "signatories": [ - "ed01207233BFC89DCBD68C19FDE6CE6158225298EC1131B6A130D1AEB454C1AB5183C0" - ], - "metadata": { - "key": { - "String": "value" - } - } - } - } - }, - { - "Register": { - "NewAssetDefinition": { - "id": "rose#wonderland", - "value_type": "Quantity", - "mintable": "Infinitely", - "logo": null, - "metadata": {} - } - } - }, - { - "Register": { - "NewDomain": { - "id": "garden_of_live_flowers", - "logo": null, - "metadata": {} - } - } - }, - { - "Register": { - "NewAccount": { - "id": "carpenter@garden_of_live_flowers", - "signatories": [ - "ed01207233BFC89DCBD68C19FDE6CE6158225298EC1131B6A130D1AEB454C1AB5183C0" - ], - "metadata": {} - } - } - }, - { - "Register": { - "NewAssetDefinition": { - "id": "cabbage#garden_of_live_flowers", - "value_type": "Quantity", - "mintable": "Infinitely", - "logo": null, - "metadata": {} - } - } - }, - { - "Mint": { - "object": "13_u32", - "destination_id": { - "AssetId": "rose##alice@wonderland" - } - } - }, - { - "Mint": { - "object": "44_u32", - "destination_id": { - "AssetId": "cabbage#garden_of_live_flowers#alice@wonderland" - } - } - }, - { - "Grant": { - "object": { - "PermissionToken": { - "definition_id": "CanSetParameters", - "payload": null - } - }, - "destination_id": { - "AccountId": "alice@wonderland" - } - } - }, - { - "Sequence": [ - { - "NewParameter": { - "Parameter": "?MaxTransactionsInBlock=512" - } - }, - { - "NewParameter": { - "Parameter": "?BlockTime=2000" - } - }, - { - "NewParameter": { - "Parameter": "?CommitTimeLimit=4000" - } - }, - { - "NewParameter": { - "Parameter": "?TransactionLimits=4096,4194304_TL" - } - }, - { - "NewParameter": { - "Parameter": "?WSVAssetMetadataLimits=1048576,4096_ML" - } - }, - { - "NewParameter": { - "Parameter": "?WSVAssetDefinitionMetadataLimits=1048576,4096_ML" - } - }, - { - "NewParameter": { - "Parameter": "?WSVAccountMetadataLimits=1048576,4096_ML" - } - }, - { - "NewParameter": { - "Parameter": "?WSVDomainMetadataLimits=1048576,4096_ML" - } - }, - { - "NewParameter": { - "Parameter": "?WSVIdentLengthLimits=1,128_LL" - } - }, - { - "NewParameter": { - "Parameter": "?WASMFuelLimit=23000000" - } - }, - { - "NewParameter": { - "Parameter": "?WASMMaxMemory=524288000" - } - } - ] - }, - { - "Register": { - "NewRole": { - "id": "ALICE_METADATA_ACCESS", - "permissions": [ - { - "definition_id": "CanRemoveKeyValueInUserAccount", - "payload": { - "account_id": "alice@wonderland" - } - }, - { - "definition_id": "CanSetKeyValueInUserAccount", - "payload": { - "account_id": "alice@wonderland" - } - } - ] - } - } - } - ] - ], - "executor": "./executor.wasm" -} diff --git a/core/benches/blocks/common.rs b/core/benches/blocks/common.rs index 4d59f22af41..eb396b7fcdf 100644 --- a/core/benches/blocks/common.rs +++ b/core/benches/blocks/common.rs @@ -190,7 +190,7 @@ pub fn build_wsv( { let path_to_executor = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("../configs/peer/executor.wasm"); + .join("../config_samples/swarm/executor.wasm"); let wasm = std::fs::read(&path_to_executor) .unwrap_or_else(|_| panic!("Failed to read file: {}", path_to_executor.display())); let executor = Executor::new(WasmSmartContract::from_compiled(wasm)); diff --git a/core/benches/validation.rs b/core/benches/validation.rs index 310bd2d277e..8c69e23ce95 100644 --- a/core/benches/validation.rs +++ b/core/benches/validation.rs @@ -79,7 +79,7 @@ fn build_test_and_transient_wsv(keys: KeyPair) -> WorldStateView { { let path_to_executor = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("../configs/peer/executor.wasm"); + .join("../config_samples/swarm/executor.wasm"); let wasm = std::fs::read(&path_to_executor) .unwrap_or_else(|_| panic!("Failed to read file: {}", path_to_executor.display())); let executor = Executor::new(WasmSmartContract::from_compiled(wasm)); diff --git a/core/test_network/src/lib.rs b/core/test_network/src/lib.rs index 9bb1ac10324..89cf6fd9c95 100644 --- a/core/test_network/src/lib.rs +++ b/core/test_network/src/lib.rs @@ -80,9 +80,10 @@ impl TestGenesis for GenesisNetwork { // TODO: Fix this somehow. Probably we need to make `kagami` a library (#3253). let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR")); - let mut genesis = - RawGenesisBlock::from_path(manifest_dir.join("../../configs/peer/genesis.json")) - .expect("Failed to deserialize genesis block from file"); + let mut genesis = RawGenesisBlock::from_path( + manifest_dir.join("../../config_samples/swarm/genesis.json"), + ) + .expect("Failed to deserialize genesis block from file"); let rose_definition_id = AssetDefinitionId::from_str("rose#wonderland").expect("valid names"); diff --git a/crypto/src/lib.rs b/crypto/src/lib.rs index 853af121700..d547204e7e1 100755 --- a/crypto/src/lib.rs +++ b/crypto/src/lib.rs @@ -649,7 +649,7 @@ impl PrivateKey { } /// Key payload - fn payload(&self) -> Vec { + pub fn payload(&self) -> Vec { match self.0.borrow() { PrivateKeyInner::Ed25519(key) => key.to_keypair_bytes().to_vec(), PrivateKeyInner::Secp256k1(key) => key.to_bytes().to_vec(), diff --git a/default_executor/README.md b/default_executor/README.md index a404dd83950..a2db2d88dc1 100644 --- a/default_executor/README.md +++ b/default_executor/README.md @@ -4,5 +4,5 @@ Use the [Wasm Builder CLI](../tools/wasm_builder_cli) in order to build it: ```bash cargo run --bin iroha_wasm_builder_cli -- \ - build ./default_executor --optimize --outfile ./configs/peer/executor.wasm + build ./default_executor --optimize --outfile ./config_samples/swarm/executor.wasm ``` \ No newline at end of file diff --git a/docker-compose.single.yml b/docker-compose.single.yml deleted file mode 100644 index 240d84cf190..00000000000 --- a/docker-compose.single.yml +++ /dev/null @@ -1,31 +0,0 @@ -# This file is generated by iroha_swarm. -# Do not edit it manually. - -version: '3.8' -services: - iroha0: - build: ./ - platform: linux/amd64 - environment: - IROHA_CHAIN_ID: 00000000-0000-0000-0000-000000000000 - IROHA_CONFIG: /config/config.json - IROHA_PUBLIC_KEY: ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB - IROHA_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb"}' - TORII_P2P_ADDR: 0.0.0.0:1337 - TORII_API_URL: 0.0.0.0:8080 - 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 - volumes: - - ./configs/peer:/config - init: true - command: iroha --submit-genesis - healthcheck: - test: test $(curl -s http://127.0.0.1:8080/status/blocks) -gt 0 - interval: 2s - timeout: 1s - retries: 30 - start_period: 4s diff --git a/scripts/requirements.txt b/scripts/requirements.txt new file mode 100644 index 00000000000..6b0fcf8b9ef --- /dev/null +++ b/scripts/requirements.txt @@ -0,0 +1 @@ +tomli_w==1.0.0 diff --git a/scripts/test_env.py b/scripts/test_env.py index b6d3b858910..bf3f39d16f9 100755 --- a/scripts/test_env.py +++ b/scripts/test_env.py @@ -17,8 +17,10 @@ import time import urllib.error import urllib.request -import uuid +import tomli_w +SWARM_CONFIGS_DIRECTORY = pathlib.Path("config_samples/swarm") +SHARED_CONFIG_FILE_NAME = "config.base.toml" class Network: """ @@ -27,36 +29,34 @@ class Network: def __init__(self, args: argparse.Namespace): logging.info("Setting up test environment...") - self.out_dir = args.out_dir - peers_dir = args.out_dir.joinpath("peers") + self.out_dir = pathlib.Path(args.out_dir) + peers_dir = self.out_dir / "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) - # 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) + logging.info("Generating shared configuration...") + trusted_peers = [{"address": f"{peer.host_ip}:{peer.p2p_port}", "public_key": peer.public_key} for peer in self.peers] + shared_config = { + "iroha": { + "chain_id": "00000000-0000-0000-0000-000000000000", + }, + "genesis": { + "public_key": self.peers[0].public_key + }, + "sumeragi": { + "trusted_peers": trusted_peers + }, + "logger": { + "level": "INFO", + "format": "pretty", + } + } + with open(peers_dir / SHARED_CONFIG_FILE_NAME, "wb") as f: + tomli_w.dump(shared_config, f) - self.shared_env["IROHA_CHAIN_ID"] = "00000000-0000-0000-0000-000000000000" - self.shared_env["IROHA_CONFIG"] = str(peers_dir.joinpath("config.json")) - self.shared_env["IROHA_GENESIS_PUBLIC_KEY"] = self.peers[0].public_key + copy_or_prompt_build_bin("iroha", args.root_dir, peers_dir) - 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)) - 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): @@ -79,7 +79,7 @@ def wait_for_genesis(self, n_tries: int): def run(self): for i, peer in enumerate(self.peers): - peer.run(shared_env=self.shared_env, submit_genesis=(i == 0)) + peer.run(submit_genesis=(i == 0)) self.wait_for_genesis(20) class _Peer: @@ -93,14 +93,15 @@ def __init__(self, args: argparse.Namespace, nth: int): self.p2p_port = 1337 + nth self.api_port = 8080 + nth 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.out_dir = pathlib.Path(args.out_dir) + self.root_dir = pathlib.Path(args.root_dir) + self.peer_dir = self.out_dir / "peers" / self.name + self.config_path = self.peer_dir / "config.toml" self.host_ip = args.host_ip logging.info(f"Peer {self.name} generating key pair...") - command = [f"{self.out_dir}/kagami", "crypto", "-j"] + command = [self.out_dir / "kagami", "crypto", "-j"] if args.peer_name_as_seed: command.extend(["-s", self.name]) kagami = subprocess.run(command, capture_output=True) @@ -108,42 +109,67 @@ def __init__(self, args: argparse.Namespace, nth: int): logging.error("Kagami failed to generate a key pair.") sys.exit(3) str_keypair = kagami.stdout - json_keypair = json.loads(str_keypair) - # public key is a string, private key is a json object - self.public_key = json_keypair['public_key'] - self.private_key = json.dumps(json_keypair['private_key']) + # dict with `{ public_key: string, private_key: { digest_function: string, payload: string } }` + self.key_pair = json.loads(str_keypair) os.makedirs(self.peer_dir, exist_ok=True) - os.makedirs(self.peer_dir.joinpath("storage"), exist_ok=True) + config = { + "extends": f"../{SHARED_CONFIG_FILE_NAME}", + "iroha": { + "public_key": self.public_key, + "private_key": self.private_key, + "p2p_address": f"{self.host_ip}:{self.p2p_port}" + }, + "torii": { + "address": f"{self.host_ip}:{self.api_port}" + }, + "kura": { + "block_store_path": "storage" + }, + "snapshot": { + "store_path": "storage/snapshot" + }, + # it is not available in debug iroha build + # "logger": { + # "tokio_console_addr": f"{self.host_ip}:{self.tokio_console_port}", + # } + } + if nth == 0: + try: + shutil.copy2(self.root_dir / SWARM_CONFIGS_DIRECTORY / "genesis.json", self.peer_dir) + # assuming that `genesis.json` contains path to the executor as `./executor.wasm` + shutil.copy2(self.root_dir / SWARM_CONFIGS_DIRECTORY / "executor.wasm", self.peer_dir) + except FileNotFoundError: + target = self.root_dir / SWARM_CONFIGS_DIRECTORY + logging.error(f"Some of the config files are missing. \ + Please provide them in the `{target}` directory") + sys.exit(1) + config["genesis"] = { + "private_key": self.private_key, + "file": "./genesis.json" + } + with open(self.config_path, "wb") as f: + tomli_w.dump(config, f) logging.info(f"Peer {self.name} initialized") - def run(self, shared_env: dict(), submit_genesis: bool = False): - logging.info(f"Running peer {self.name}...") + @property + def public_key(self): + return self.key_pair["public_key"] + + @property + def private_key(self): + return self.key_pair["private_key"] - 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")) + def run(self, submit_genesis: bool = False): + logging.info(f"Running peer {self.name}...") # FD never gets closed - stdout_file = open(self.peer_dir.joinpath(".stdout"), "w") - stderr_file = open(self.peer_dir.joinpath(".stderr"), "w") + stdout_file = open(self.peer_dir / ".stdout", "w") + stderr_file = open(self.peer_dir / ".stderr", "w") # These processes are created detached from the parent process already - 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) + subprocess.Popen([self.name, "--config", self.config_path] + (["--submit-genesis"] if submit_genesis else []), + executable=self.out_dir / "peers/iroha", stdout=stdout_file, stderr=stderr_file) def pos_int(arg): if int(arg) > 0: @@ -152,8 +178,9 @@ def pos_int(arg): raise argparse.ArgumentTypeError(f"Argument {arg} must be a positive integer") def copy_or_prompt_build_bin(bin_name: str, root_dir: pathlib.Path, target_dir: pathlib.Path): + bin_path = root_dir / "target/debug" / bin_name try: - shutil.copy2(f"{root_dir}/target/debug/{bin_name}", target_dir) + shutil.copy2(bin_path, target_dir) except FileNotFoundError: logging.error(f"The binary `{bin_name}` wasn't found in `{root_dir}` directory") while True: @@ -163,7 +190,7 @@ def copy_or_prompt_build_bin(bin_name: str, root_dir: pathlib.Path, target_dir: ["cargo", "build", "--bin", bin_name], cwd=root_dir ) - shutil.copy2(f"{root_dir}/target/debug/{bin_name}", target_dir) + shutil.copy2(bin_path, target_dir) break elif prompt.lower() in ["n", "no"]: logging.critical("Can't launch the network without the binary. Aborting...") @@ -195,7 +222,7 @@ def setup(args: argparse.Namespace): copy_or_prompt_build_bin("iroha_client_cli", args.root_dir, args.out_dir) with open(os.path.join(args.out_dir, "metadata.json"), "w") as f: f.write('{"comment":{"String": "Hello Meta!"}}') - shutil.copy2(f"{args.root_dir}/configs/client/config.json", args.out_dir) + shutil.copy2(pathlib.Path(args.root_dir) / SWARM_CONFIGS_DIRECTORY / "client.toml", args.out_dir) copy_or_prompt_build_bin("kagami", args.root_dir, args.out_dir) Network(args).run() diff --git a/scripts/tests/consistency.sh b/scripts/tests/consistency.sh index 64305cbbb3e..01c5a4fb1c3 100755 --- a/scripts/tests/consistency.sh +++ b/scripts/tests/consistency.sh @@ -3,8 +3,8 @@ set -e case $1 in "genesis") - cargo run --release --bin kagami -- genesis --executor-path-in-genesis ./executor.wasm | diff - configs/peer/genesis.json || { - echo 'Please re-generate the genesis with `cargo run --release --bin kagami -- genesis --executor-path-in-genesis ./executor.wasm > configs/peer/genesis.json`' + cargo run --release --bin kagami -- genesis --executor-path-in-genesis ./executor.wasm | diff - config_samples/swarm/genesis.json || { + echo 'Please re-generate the genesis with `cargo run --release --bin kagami -- genesis --executor-path-in-genesis ./executor.wasm > config_samples/swarm/genesis.json`' exit 1 };; "schema") @@ -19,7 +19,7 @@ case $1 in # FIXME: not nice; add an option to `kagami swarm` to print content into stdout? # it is not a default behaviour because Kagami resolves `build` path relative # to the output file location - temp_file="docker-compose.TMP.yml" + temp_file="configs_samples/swarm/docker-compose.TMP.yml" full_cmd="$cmd_base --outfile $temp_file" eval "$full_cmd" @@ -30,19 +30,19 @@ case $1 in } command_base_for_single() { - echo "cargo run --release --bin iroha_swarm -- -p 1 -s Iroha --force --config-dir ./configs/peer --health-check --build ." + echo "cargo run --release --bin iroha_swarm -- -p 1 -s Iroha --force --config-dir ./config_samples/swarm --health-check --build ." } command_base_for_multiple_local() { - echo "cargo run --release --bin iroha_swarm -- -p 4 -s Iroha --force --config-dir ./configs/peer --health-check --build ." + echo "cargo run --release --bin iroha_swarm -- -p 4 -s Iroha --force --config-dir ./config_samples/swarm --health-check --build ." } command_base_for_default() { - echo "cargo run --release --bin iroha_swarm -- -p 4 -s Iroha --force --config-dir ./configs/peer --health-check --image hyperledger/iroha2:dev" + echo "cargo run --release --bin iroha_swarm -- -p 4 -s Iroha --force --config-dir ./config_samples/swarm --health-check --image hyperledger/iroha2:dev" } - do_check "$(command_base_for_single)" "docker-compose.single.yml" - do_check "$(command_base_for_multiple_local)" "docker-compose.local.yml" - do_check "$(command_base_for_default)" "docker-compose.yml" + do_check "$(command_base_for_single)" "configs_samples/swarm/docker-compose.single.yml" + do_check "$(command_base_for_multiple_local)" "configs_samples/swarm/docker-compose.local.yml" + do_check "$(command_base_for_default)" "configs_samples/swarm/docker-compose.yml" esac diff --git a/scripts/tests/panic_on_invalid_genesis.sh b/scripts/tests/panic_on_invalid_genesis.sh index ed95926b645..51d270a3a73 100755 --- a/scripts/tests/panic_on_invalid_genesis.sh +++ b/scripts/tests/panic_on_invalid_genesis.sh @@ -18,6 +18,6 @@ trap 'rm -rf -- "$IROHA2_GENESIS_PATH" "$KURA_BLOCK_STORE_PATH"' EXIT # Create invalid genesis # NewAssetDefinition replaced with AssetDefinition -sed 's/NewAssetDefinition/AssetDefinition/' ./configs/peer/genesis.json > $IROHA2_GENESIS_PATH +sed 's/NewAssetDefinition/AssetDefinition/' ./config_samples/swarm/genesis.json > $IROHA2_GENESIS_PATH timeout 1m target/debug/iroha --submit-genesis 2>&1 | tee /dev/stderr | grep -q 'Transaction validation failed in genesis block' diff --git a/tools/swarm/README.md b/tools/swarm/README.md index 3db15c69284..d0bf02a9ea8 100644 --- a/tools/swarm/README.md +++ b/tools/swarm/README.md @@ -21,14 +21,14 @@ iroha_swarm ## Examples -Generate `docker-compose.dev.yml` with 5 peers, using `iroha` utf-8 bytes as a cryptographic seed, using `./configs/peer` as a directory with configuration, and using `.` as a directory with `Dockerfile` of Iroha: +Generate `docker-compose.dev.yml` with 5 peers, using `iroha` utf-8 bytes as a cryptographic seed, using `./peer_config` as a directory with configuration, and using `.` as a directory with `Dockerfile` of Iroha: ```bash iroha_swarm \ --build . \ --peers 5 \ --seed iroha \ - --config-dir ./configs/peer \ + --config-dir ./peer_config \ --outfile docker-compose.dev.yml ``` @@ -39,6 +39,6 @@ iroha_swarm \ --image hyperledger/iroha2:dev \ --peers 5 \ --seed iroha \ - --config-dir ./configs/peer \ + --config-dir ./peer_config \ --outfile docker-compose.dev.yml ``` diff --git a/tools/swarm/src/cli.rs b/tools/swarm/src/cli.rs index fc51fec01be..e72a247ebce 100644 --- a/tools/swarm/src/cli.rs +++ b/tools/swarm/src/cli.rs @@ -35,11 +35,13 @@ 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 `genesis.json` with executor. #[arg(long, short)] pub config_dir: PathBuf, #[command(flatten)] pub source: SourceArgs, + // TODO: add an argument to specify an optional configuration file path? + // or think about other ways for users to customise peers' configuration } #[derive(Args, Debug)] diff --git a/tools/swarm/src/compose.rs b/tools/swarm/src/compose.rs index 2c4b2ac3275..170fa65517a 100644 --- a/tools/swarm/src/compose.rs +++ b/tools/swarm/src/compose.rs @@ -24,7 +24,6 @@ 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"; @@ -302,7 +301,6 @@ pub enum ServiceSource { #[serde(rename_all = "UPPERCASE")] struct FullPeerEnv { chain_id: ChainId, - iroha_config: String, public_key: PublicKey, private_key_digest: Algorithm, private_key_payload: SerializeAsHex>, @@ -336,20 +334,19 @@ impl From for FullPeerEnv { .genesis_private_key .map_or((None, None, None), |private_key| { ( - Some(private_key.digest_function()), - Some(SerializeAsHex(private_key.payload().to_owned())), + Some(private_key.algorithm()), + Some(SerializeAsHex(private_key.payload())), Some(PATH_TO_GENESIS.to_string()), ) }); let (private_key_digest, private_key_payload) = ( - value.key_pair.private_key().digest_function(), - SerializeAsHex(value.key_pair.private_key().payload().to_owned()), + value.key_pair.private_key().algorithm(), + SerializeAsHex(value.key_pair.private_key().payload()), ); Self { chain_id: value.chain_id, - iroha_config: PATH_TO_CONFIG.to_string(), public_key: value.key_pair.public_key().clone(), private_key_digest, private_key_payload, @@ -608,12 +605,8 @@ mod tests { }; use iroha_config::{ - base::TestEnv, - parameters::user_layer::RootPartial, // iroha::ConfigurationProxy, - }; - use iroha_config::{ - base::{FromEnv, ReadEnv, UnwrapPartial}, - parameters::user_layer::CliContext, + base::{FromEnv, TestEnv, UnwrapPartial}, + parameters::user_layer::{CliContext, RootPartial}, }; use iroha_crypto::{KeyGenConfiguration, KeyPair}; use iroha_primitives::addr::{socket_addr, SocketAddr}; @@ -661,12 +654,6 @@ mod tests { } .into(); - // pretending like we've read `IROHA_CONFIG` env to know the config location - let _ = env - .get("IROHA_CONFIG") - .expect("never occurs") - .expect("should be presented"); - let _cfg = RootPartial::from_env(&env) .expect("valid env") .unwrap_partial() @@ -743,7 +730,6 @@ mod tests { platform: linux/amd64 environment: CHAIN_ID: 00000000-0000-0000-0000-000000000000 - IROHA_CONFIG: /config/config.json PUBLIC_KEY: ed012039E5BF092186FACC358770792A493CA98A83740643A3D41389483CF334F748C8 PRIVATE_KEY_DIGEST: ed25519 PRIVATE_KEY_PAYLOAD: db9d90d20f969177bd5882f9fe211d14d1399d5440d04e3468783d169bbc4a8e39e5bf092186facc358770792a493ca98a83740643a3d41389483cf334f748c8 @@ -766,7 +752,7 @@ mod tests { } #[test] - fn empty_genesis_public_key_is_skipped_in_env() { + fn empty_genesis_private_key_is_skipped_in_env() { let chain_id = ChainId::from("00000000-0000-0000-0000-000000000000"); let key_pair = @@ -787,7 +773,6 @@ mod tests { let actual = serde_yaml::to_string(&env).unwrap(); let expected = expect_test::expect![[r#" CHAIN_ID: 00000000-0000-0000-0000-000000000000 - IROHA_CONFIG: /config/config.json PUBLIC_KEY: ed0120415388A90FA238196737746A70565D041CFB32EAA0C89FF8CB244C7F832A6EBD PRIVATE_KEY_DIGEST: ed25519 PRIVATE_KEY_PAYLOAD: 6bf163fd75192b81a78cb20c5f8cb917f591ac6635f2577e6ca305c27a456a5d415388a90fa238196737746a70565d041cfb32eaa0c89ff8cb244c7f832a6ebd @@ -830,7 +815,6 @@ mod tests { platform: linux/amd64 environment: CHAIN_ID: 00000000-0000-0000-0000-000000000000 - IROHA_CONFIG: /config/config.json PUBLIC_KEY: ed0120F0321EB4139163C35F88BF78520FF7071499D7F4E79854550028A196C7B49E13 PRIVATE_KEY_DIGEST: ed25519 PRIVATE_KEY_PAYLOAD: 5f8d1291bf6b762ee748a87182345d135fd167062857aa4f20ba39f25e74c4b0f0321eb4139163c35f88bf78520ff7071499d7f4e79854550028a196c7b49e13 @@ -859,7 +843,6 @@ mod tests { platform: linux/amd64 environment: CHAIN_ID: 00000000-0000-0000-0000-000000000000 - IROHA_CONFIG: /config/config.json PUBLIC_KEY: ed0120A88554AA5C86D28D0EEBEC497235664433E807881CD31E12A1AF6C4D8B0F026C PRIVATE_KEY_DIGEST: ed25519 PRIVATE_KEY_PAYLOAD: 8d34d2c6a699c61e7a9d5aabbbd07629029dfb4f9a0800d65aa6570113edb465a88554aa5c86d28d0eebec497235664433e807881cd31e12a1af6c4d8b0f026c @@ -884,7 +867,6 @@ mod tests { platform: linux/amd64 environment: CHAIN_ID: 00000000-0000-0000-0000-000000000000 - IROHA_CONFIG: /config/config.json PUBLIC_KEY: ed0120312C1B7B5DE23D366ADCF23CD6DB92CE18B2AA283C7D9F5033B969C2DC2B92F4 PRIVATE_KEY_DIGEST: ed25519 PRIVATE_KEY_PAYLOAD: cf4515a82289f312868027568c0da0ee3f0fde7fef1b69deb47b19fde7cbc169312c1b7b5de23d366adcf23cd6db92ce18b2aa283c7d9f5033b969c2dc2b92f4 @@ -909,7 +891,6 @@ mod tests { platform: linux/amd64 environment: CHAIN_ID: 00000000-0000-0000-0000-000000000000 - IROHA_CONFIG: /config/config.json PUBLIC_KEY: ed0120854457B2E3D6082181DA73DC01C1E6F93A72D0C45268DC8845755287E98A5DEE PRIVATE_KEY_DIGEST: ed25519 PRIVATE_KEY_PAYLOAD: ab0e99c2b845b4ac7b3e88d25a860793c7eb600a25c66c75cba0bae91e955aa6854457b2e3d6082181da73dc01c1e6f93a72d0c45268dc8845755287e98a5dee From 381c9dcfe1aaf42b1b5f548e4d7494dd2235c132 Mon Sep 17 00:00:00 2001 From: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> Date: Mon, 29 Jan 2024 08:52:00 +0900 Subject: [PATCH 23/94] [refactor]: refactored config boilerplate into dedicated modules Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- cli/src/lib.rs | 7 +- client/src/config.rs | 271 +---- client/src/config/user_layer.rs | 119 +++ client/src/config/user_layer/boilerplate.rs | 164 +++ config/src/parameters/actual.rs | 2 +- config/src/parameters/user_layer.rs | 935 +----------------- .../src/parameters/user_layer/boilerplate.rs | 864 ++++++++++++++++ 7 files changed, 1208 insertions(+), 1154 deletions(-) create mode 100644 client/src/config/user_layer.rs create mode 100644 client/src/config/user_layer/boilerplate.rs create mode 100644 config/src/parameters/user_layer/boilerplate.rs diff --git a/cli/src/lib.rs b/cli/src/lib.rs index e23f13db0e1..77a3bab57c2 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -9,7 +9,10 @@ use core::sync::atomic::{AtomicBool, Ordering}; use std::{path::Path, sync::Arc}; use color_eyre::eyre::{eyre, Result, WrapErr}; -use iroha_config::parameters::{actual::Root as Config, user_layer::CliContext}; +use iroha_config::parameters::{ + actual::Root as Config, + user_layer::{CliContext, RootPartial as RootLayer}, +}; use iroha_core::{ block_sync::{BlockSynchronizer, BlockSynchronizerHandle}, gossiper::{TransactionGossiper, TransactionGossiperHandle}, @@ -513,7 +516,7 @@ pub fn read_config_and_genesis( ) -> Result<(Config, Option)> { use iroha_config::{ base::{FromEnv as _, StdEnv, UnwrapPartial as _}, - parameters::{actual::Genesis, user_layer::RootPartial as RootLayer}, + parameters::actual::Genesis, }; let config = RootLayer::from_toml(path)?; diff --git a/client/src/config.rs b/client/src/config.rs index 12b9039878b..a3523f184e7 100644 --- a/client/src/config.rs +++ b/client/src/config.rs @@ -15,6 +15,8 @@ use iroha_primitives::small::SmallStr; use serde::{Deserialize, Serialize}; use url::Url; +pub mod user_layer; + #[allow(unsafe_code)] pub const DEFAULT_TRANSACTION_TIME_TO_LIVE: Duration = Duration::from_secs(100); pub const DEFAULT_TRANSACTION_STATUS_TIMEOUT: Duration = Duration::from_secs(15); @@ -65,275 +67,6 @@ pub struct BasicAuth { pub password: SmallStr, } -pub mod user_layer { - use std::{fs::File, io::Read, path::Path, time::Duration}; - - use eyre::{eyre, Context, Report}; - use iroha_config::base::{ - Emitter, ErrorsCollection, FromEnvDefaultFallback, Merge, MissingFieldError, UnwrapPartial, - UnwrapPartialResult, UserDuration, UserField, - }; - use iroha_crypto::{KeyPair, PrivateKey, PublicKey}; - use iroha_data_model::{account::AccountId, ChainId}; - use serde::{Deserialize, Deserializer}; - use url::Url; - - use crate::config::BasicAuth; - - #[derive(Debug, Clone, Deserialize, Eq, PartialEq, Default, Merge)] - #[serde(deny_unknown_fields, default)] - pub struct RootPartial { - pub chain_id: UserField, - pub account: AccountPartial, - pub api: ApiPartial, - pub transaction: TransactionPartial, - } - - impl RootPartial { - pub fn new() -> Self { - // TODO: gen with macro - Default::default() - } - - pub fn from_toml(path: impl AsRef) -> eyre::Result { - let contents = { - let mut contents = String::new(); - File::open(path.as_ref()) - .wrap_err_with(|| { - eyre!("cannot open file at location `{}`", path.as_ref().display()) - })? - .read_to_string(&mut contents)?; - contents - }; - let layer: Self = toml::from_str(&contents).wrap_err("failed to parse toml")?; - Ok(layer) - } - - pub fn merge(mut self, other: Self) -> Self { - Merge::merge(&mut self, other); - self - } - } - - // FIXME: should config be read from ENV? - impl FromEnvDefaultFallback for RootPartial {} - - #[derive(Clone, Debug)] - pub struct RootFull { - pub chain_id: ChainId, - pub account: AccountFull, - pub api: ApiFull, - pub transaction: TransactionFull, - } - - impl UnwrapPartial for RootPartial { - type Output = RootFull; - - fn unwrap_partial(self) -> UnwrapPartialResult { - let mut emitter = Emitter::new(); - - if self.chain_id.is_none() { - emitter.emit_missing_field("chain_id"); - } - let account = emitter.try_unwrap_partial(self.account); - let api = emitter.try_unwrap_partial(self.api); - let transaction = emitter.try_unwrap_partial(self.transaction); - - emitter.finish()?; - - Ok(RootFull { - chain_id: self.chain_id.get().unwrap(), - account: account.unwrap(), - api: api.unwrap(), - transaction: transaction.unwrap(), - }) - } - } - - impl RootFull { - pub fn parse(self) -> Result> { - let Self { - chain_id, - account: - AccountFull { - id: account_id, - public_key, - private_key, - }, - transaction: - TransactionFull { - time_to_live: tx_ttl, - status_timeout: tx_timeout, - add_nonce: tx_add_nonce, - }, - api: - ApiFull { - torii_url, - basic_auth, - }, - } = self; - - let mut emitter = Emitter::new(); - - // TODO: validate if TTL is too small? - - if tx_timeout > tx_ttl { - // TODO: - // would be nice to provide a nice report with spans in the input - // pointing out source data in provided config files - // FIXME: explain why it should be smaller - emitter.emit(eyre!( - "transaction status timeout should be smaller than its time-to-live" - )) - } - - let key_pair = KeyPair::new(public_key, private_key) - .wrap_err("failed to construct a key pair") - .map_or_else( - |err| { - emitter.emit(err); - None - }, - Some, - ); - - emitter.finish()?; - - Ok(super::Config { - chain_id, - account_id, - key_pair: key_pair.unwrap(), - torii_api_url: torii_url.0, - basic_auth, - transaction_ttl: tx_ttl, - transaction_status_timeout: tx_timeout, - transaction_add_nonce: tx_add_nonce, - }) - } - } - - #[derive(Debug, Clone, Deserialize, Eq, PartialEq, Default, Merge)] - #[serde(deny_unknown_fields, default)] - pub struct ApiPartial { - pub torii_url: UserField, - pub basic_auth: UserField, - } - - #[derive(Debug, Clone)] - pub struct ApiFull { - pub torii_url: OnlyHttpUrl, - pub basic_auth: Option, - } - - impl UnwrapPartial for ApiPartial { - type Output = ApiFull; - - fn unwrap_partial(self) -> UnwrapPartialResult { - Ok(ApiFull { - torii_url: self - .torii_url - .get() - .ok_or_else(|| MissingFieldError::new("api.torii_url"))?, - basic_auth: self.basic_auth.get(), - }) - } - } - - #[derive(Debug, Clone, Deserialize, Eq, PartialEq, Default, Merge)] - #[serde(deny_unknown_fields, default)] - pub struct AccountPartial { - pub id: UserField, - pub public_key: UserField, - pub private_key: UserField, - } - - #[derive(Debug, Clone)] - pub struct AccountFull { - pub id: AccountId, - pub public_key: PublicKey, - pub private_key: PrivateKey, - } - - impl UnwrapPartial for AccountPartial { - type Output = AccountFull; - - fn unwrap_partial(self) -> UnwrapPartialResult { - let mut emitter = Emitter::new(); - - if self.id.is_none() { - emitter.emit_missing_field("account.id"); - } - if self.public_key.is_none() { - emitter.emit_missing_field("account.public_key"); - } - if self.private_key.is_none() { - emitter.emit_missing_field("account.private_key"); - } - - emitter.finish()?; - - Ok(AccountFull { - id: self.id.get().unwrap(), - public_key: self.public_key.get().unwrap(), - private_key: self.private_key.get().unwrap(), - }) - } - } - - #[derive(Debug, Clone, Deserialize, Eq, PartialEq, Default, Merge)] - #[serde(deny_unknown_fields, default)] - pub struct TransactionPartial { - pub time_to_live: UserField, - pub status_timeout: UserField, - pub add_nonce: UserField, - } - - #[derive(Debug, Clone, Copy)] - pub struct TransactionFull { - pub time_to_live: Duration, - pub status_timeout: Duration, - pub add_nonce: bool, - } - - impl UnwrapPartial for TransactionPartial { - type Output = TransactionFull; - - fn unwrap_partial(self) -> UnwrapPartialResult { - Ok(TransactionFull { - time_to_live: self - .time_to_live - .get() - .map_or(super::DEFAULT_TRANSACTION_TIME_TO_LIVE, UserDuration::get), - status_timeout: self - .status_timeout - .get() - .map_or(super::DEFAULT_TRANSACTION_STATUS_TIMEOUT, UserDuration::get), - add_nonce: self - .add_nonce - .get() - .unwrap_or(super::DEFAULT_ADD_TRANSACTION_NONCE), - }) - } - } - - #[derive(Debug, Clone, Eq, PartialEq)] - pub struct OnlyHttpUrl(Url); - - impl<'de> Deserialize<'de> for OnlyHttpUrl { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let url = Url::deserialize(deserializer)?; - if url.scheme() == "http" { - Ok(Self(url)) - } else { - Err(serde::de::Error::custom("only HTTP scheme is supported")) - } - } - } -} - #[derive(Clone, Debug, Serialize)] pub struct Config { pub chain_id: ChainId, diff --git a/client/src/config/user_layer.rs b/client/src/config/user_layer.rs new file mode 100644 index 00000000000..2631239cbba --- /dev/null +++ b/client/src/config/user_layer.rs @@ -0,0 +1,119 @@ +use std::{io::Read, time::Duration}; + +use eyre::{eyre, Context, Report}; +use iroha_config::base::{Emitter, ErrorsCollection, FromEnvDefaultFallback, Merge, UnwrapPartial}; +use iroha_crypto::{KeyPair, PrivateKey, PublicKey}; +use iroha_data_model::{account::AccountId, ChainId}; +use serde::{Deserialize, Deserializer}; +use url::Url; + +use crate::config::BasicAuth; + +mod boilerplate; +pub use boilerplate::*; + +#[derive(Clone, Debug)] +pub struct Root { + pub chain_id: ChainId, + pub account: Account, + pub api: Api, + pub transaction: Transaction, +} + +impl Root { + pub fn parse(self) -> Result> { + let Self { + chain_id, + account: + Account { + id: account_id, + public_key, + private_key, + }, + transaction: + Transaction { + time_to_live: tx_ttl, + status_timeout: tx_timeout, + add_nonce: tx_add_nonce, + }, + api: Api { + torii_url, + basic_auth, + }, + } = self; + + let mut emitter = Emitter::new(); + + // TODO: validate if TTL is too small? + + if tx_timeout > tx_ttl { + // TODO: + // would be nice to provide a nice report with spans in the input + // pointing out source data in provided config files + // FIXME: explain why it should be smaller + emitter.emit(eyre!( + "transaction status timeout should be smaller than its time-to-live" + )) + } + + let key_pair = KeyPair::new(public_key, private_key) + .wrap_err("failed to construct a key pair") + .map_or_else( + |err| { + emitter.emit(err); + None + }, + Some, + ); + + emitter.finish()?; + + Ok(super::Config { + chain_id, + account_id, + key_pair: key_pair.unwrap(), + torii_api_url: torii_url.0, + basic_auth, + transaction_ttl: tx_ttl, + transaction_status_timeout: tx_timeout, + transaction_add_nonce: tx_add_nonce, + }) + } +} + +#[derive(Debug, Clone)] +pub struct Api { + pub torii_url: OnlyHttpUrl, + pub basic_auth: Option, +} + +#[derive(Debug, Clone)] +pub struct Account { + pub id: AccountId, + pub public_key: PublicKey, + pub private_key: PrivateKey, +} + +#[derive(Debug, Clone, Copy)] +pub struct Transaction { + pub time_to_live: Duration, + pub status_timeout: Duration, + pub add_nonce: bool, +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct OnlyHttpUrl(Url); + +impl<'de> Deserialize<'de> for OnlyHttpUrl { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let url = Url::deserialize(deserializer)?; + if url.scheme() == "http" { + Ok(Self(url)) + } else { + Err(serde::de::Error::custom("only HTTP scheme is supported")) + } + } +} diff --git a/client/src/config/user_layer/boilerplate.rs b/client/src/config/user_layer/boilerplate.rs new file mode 100644 index 00000000000..bc527933c75 --- /dev/null +++ b/client/src/config/user_layer/boilerplate.rs @@ -0,0 +1,164 @@ +//! Code to be generated by a proc macro in future + +use std::{fs::File, io::Read, path::Path}; + +use eyre::{eyre, WrapErr}; +use iroha_crypto::{PrivateKey, PublicKey}; +use iroha_data_model::{account::AccountId, ChainId}; +use serde::Deserialize; + +use crate::config::{ + base::{ + Emitter, FromEnvDefaultFallback, Merge, MissingFieldError, UnwrapPartial, + UnwrapPartialResult, UserDuration, UserField, + }, + user_layer::{Account, Api, OnlyHttpUrl, Root, Transaction}, + BasicAuth, DEFAULT_ADD_TRANSACTION_NONCE, DEFAULT_TRANSACTION_STATUS_TIMEOUT, + DEFAULT_TRANSACTION_TIME_TO_LIVE, +}; + +#[derive(Debug, Clone, Deserialize, Eq, PartialEq, Default, Merge)] +#[serde(deny_unknown_fields, default)] +pub struct RootPartial { + pub chain_id: UserField, + pub account: AccountPartial, + pub api: ApiPartial, + pub transaction: TransactionPartial, +} + +impl RootPartial { + pub fn new() -> Self { + // TODO: gen with macro + Default::default() + } + + pub fn from_toml(path: impl AsRef) -> eyre::Result { + let contents = { + let mut contents = String::new(); + File::open(path.as_ref()) + .wrap_err_with(|| { + eyre!("cannot open file at location `{}`", path.as_ref().display()) + })? + .read_to_string(&mut contents)?; + contents + }; + let layer: Self = toml::from_str(&contents).wrap_err("failed to parse toml")?; + Ok(layer) + } + + pub fn merge(mut self, other: Self) -> Self { + Merge::merge(&mut self, other); + self + } +} + +// FIXME: should config be read from ENV? +impl FromEnvDefaultFallback for RootPartial {} + +impl UnwrapPartial for RootPartial { + type Output = Root; + + fn unwrap_partial(self) -> UnwrapPartialResult { + let mut emitter = Emitter::new(); + + if self.chain_id.is_none() { + emitter.emit_missing_field("chain_id"); + } + let account = emitter.try_unwrap_partial(self.account); + let api = emitter.try_unwrap_partial(self.api); + let transaction = emitter.try_unwrap_partial(self.transaction); + + emitter.finish()?; + + Ok(Root { + chain_id: self.chain_id.get().unwrap(), + account: account.unwrap(), + api: api.unwrap(), + transaction: transaction.unwrap(), + }) + } +} + +#[derive(Debug, Clone, Deserialize, Eq, PartialEq, Default, Merge)] +#[serde(deny_unknown_fields, default)] +pub struct ApiPartial { + pub torii_url: UserField, + pub basic_auth: UserField, +} + +impl UnwrapPartial for ApiPartial { + type Output = Api; + + fn unwrap_partial(self) -> UnwrapPartialResult { + Ok(Api { + torii_url: self + .torii_url + .get() + .ok_or_else(|| MissingFieldError::new("api.torii_url"))?, + basic_auth: self.basic_auth.get(), + }) + } +} + +#[derive(Debug, Clone, Deserialize, Eq, PartialEq, Default, Merge)] +#[serde(deny_unknown_fields, default)] +pub struct AccountPartial { + pub id: UserField, + pub public_key: UserField, + pub private_key: UserField, +} + +impl UnwrapPartial for AccountPartial { + type Output = Account; + + fn unwrap_partial(self) -> UnwrapPartialResult { + let mut emitter = Emitter::new(); + + if self.id.is_none() { + emitter.emit_missing_field("account.id"); + } + if self.public_key.is_none() { + emitter.emit_missing_field("account.public_key"); + } + if self.private_key.is_none() { + emitter.emit_missing_field("account.private_key"); + } + + emitter.finish()?; + + Ok(Account { + id: self.id.get().unwrap(), + public_key: self.public_key.get().unwrap(), + private_key: self.private_key.get().unwrap(), + }) + } +} + +#[derive(Debug, Clone, Deserialize, Eq, PartialEq, Default, Merge)] +#[serde(deny_unknown_fields, default)] +pub struct TransactionPartial { + pub time_to_live: UserField, + pub status_timeout: UserField, + pub add_nonce: UserField, +} + +impl UnwrapPartial for TransactionPartial { + type Output = Transaction; + + fn unwrap_partial(self) -> UnwrapPartialResult { + Ok(Transaction { + time_to_live: self + .time_to_live + .get() + .map_or(DEFAULT_TRANSACTION_TIME_TO_LIVE, UserDuration::get), + status_timeout: self + .status_timeout + .get() + .map_or(DEFAULT_TRANSACTION_STATUS_TIMEOUT, UserDuration::get), + add_nonce: self + .add_nonce + .get() + .unwrap_or(DEFAULT_ADD_TRANSACTION_NONCE), + }) + } +} diff --git a/config/src/parameters/actual.rs b/config/src/parameters/actual.rs index e9d42cf7e1f..cb4bd624373 100644 --- a/config/src/parameters/actual.rs +++ b/config/src/parameters/actual.rs @@ -103,7 +103,7 @@ impl Default for Queue { } } -pub use user_layer::{LoggerFull as Logger, QueueFull as Queue, SnapshotFull as Snapshot}; +pub use user_layer::{Logger, Queue, Snapshot}; #[derive(Debug, Clone)] pub struct Sumeragi { diff --git a/config/src/parameters/user_layer.rs b/config/src/parameters/user_layer.rs index 57963a87be7..0f16a319bde 100644 --- a/config/src/parameters/user_layer.rs +++ b/config/src/parameters/user_layer.rs @@ -1,20 +1,18 @@ use std::{ error::Error, fmt::Debug, - fs::File, io::Read, - num::{NonZeroU32, NonZeroU64, NonZeroUsize}, + num::{NonZeroU32, NonZeroUsize}, ops::{Add, Div}, - path::{Iter, Path, PathBuf}, + path::PathBuf, str::FromStr, time::Duration, }; use eyre::{eyre, Report, WrapErr}; use iroha_config_base::{ - ByteSize, Emitter, ErrorsCollection, FromEnv, FromEnvDefaultFallback, FromEnvResult, Merge, - MissingFieldError, ParseEnvResult, ReadEnv, UnwrapPartial, UnwrapPartialResult, UserDuration, - UserField, + ByteSize, Emitter, ErrorsCollection, FromEnv, FromEnvDefaultFallback, Merge, ParseEnvResult, + ReadEnv, UnwrapPartial, UnwrapPartialResult, }; use iroha_crypto::{KeyPair, PrivateKey, PublicKey}; use iroha_data_model::{ @@ -30,13 +28,13 @@ use crate::{ logger::Format, parameters::{ actual, - defaults::{ - chain_wide::*, kura::*, logger::*, network::*, queue::*, snapshot::*, telemetry::*, - torii::*, - }, + defaults::{logger::*, telemetry::*}, }, }; +mod boilerplate; +pub use boilerplate::*; + #[derive(Debug, Default, Serialize, Deserialize, Eq, PartialEq)] #[serde(untagged)] pub enum ExtendsPaths { @@ -90,113 +88,22 @@ impl<'a> Iterator for ExtendsPathsIter<'a> { } } -#[derive(Deserialize, Serialize, Debug, Default, Merge)] -#[serde(deny_unknown_fields, default)] -pub struct RootPartial { - pub extends: ExtendsPaths, - pub iroha: IrohaPartial, - pub genesis: GenesisPartial, - pub kura: KuraPartial, - pub sumeragi: SumeragiPartial, - pub network: NetworkPartial, - pub logger: LoggerPartial, - pub queue: QueuePartial, - pub snapshot: SnapshotPartial, - pub telemetry: TelemetryPartial, - pub torii: ToriiPartial, - pub chain_wide: ChainWidePartial, -} - -impl RootPartial { - /// Creates new empty user configuration - pub fn new() -> Self { - // TODO: generate this function with macro. For now, use default - Default::default() - } - - pub fn from_toml(path: impl AsRef) -> eyre::Result { - let contents = { - let mut file = File::open(path.as_ref()).wrap_err_with(|| { - eyre!("cannot open file at location `{}`", path.as_ref().display()) - })?; - let mut contents = String::new(); - file.read_to_string(&mut contents)?; - contents - }; - let mut layer: Self = toml::from_str(&contents).wrap_err("failed to parse toml")?; - - let base_path = path - .as_ref() - .parent() - .expect("the config file path could not be empty or root"); - - layer.normalise_paths(base_path); - - if let Some(base) = - layer - .extends - .iter() - .try_fold(None, |acc: Option, extends_path| { - // extends path is not normalised relative to the config file yet - let full_path = base_path.join(extends_path); - - let base = Self::from_toml(&full_path) - .wrap_err_with(|| eyre!("cannot extend from `{}`", full_path.display()))?; - - match acc { - None => Ok::, Report>(Some(base)), - Some(other_base) => Ok(Some(other_base.merge(base))), - } - })? - { - layer.extends.used(); - layer = base.merge(layer); - }; - - Ok(layer) - } - - /// **Note:** this function doesn't affect `extends` - fn normalise_paths(&mut self, relative_to: impl AsRef) { - let path = relative_to.as_ref(); - - macro_rules! patch { - ($value:expr) => { - $value.as_mut().map(|x| { - *x = path.join(&x); - }) - }; - } - - patch!(self.genesis.file); - patch!(self.snapshot.store_path); - patch!(self.kura.block_store_path); - patch!(self.telemetry.dev.file); - } - - // FIXME workaround the inconvenient way `Merge::merge` works - pub fn merge(mut self, other: Self) -> Self { - Merge::merge(&mut self, other); - self - } -} - #[derive(Debug)] -pub struct RootFull { - iroha: IrohaFull, - genesis: GenesisFull, - kura: KuraFull, - sumeragi: SumeragiFull, - network: NetworkFull, - logger: LoggerFull, - queue: QueueFull, - snapshot: SnapshotFull, - telemetry: TelemetryFull, - torii: ToriiFull, - chain_wide: ChainWideFull, -} - -impl RootFull { +pub struct Root { + iroha: Iroha, + genesis: Genesis, + kura: Kura, + sumeragi: Sumeragi, + network: Network, + logger: Logger, + queue: Queue, + snapshot: Snapshot, + telemetry: Telemetry, + torii: Torii, + chain_wide: ChainWide, +} + +impl Root { pub fn parse(self, cli: CliContext) -> Result> { let mut emitter = Emitter::new(); @@ -302,152 +209,15 @@ pub struct CliContext { pub submit_genesis: bool, } -impl UnwrapPartial for RootPartial { - type Output = RootFull; - - fn unwrap_partial(self) -> UnwrapPartialResult { - let mut emitter = Emitter::new(); - - macro_rules! nested { - ($item:expr) => { - match UnwrapPartial::unwrap_partial($item) { - Ok(value) => Some(value), - Err(error) => { - emitter.emit_collection(error); - None - } - } - }; - } - - let iroha = nested!(self.iroha); - let genesis = nested!(self.genesis); - let kura = nested!(self.kura); - let sumeragi = nested!(self.sumeragi); - let network = nested!(self.network); - let logger = nested!(self.logger); - let queue = nested!(self.queue); - let snapshot = nested!(self.snapshot); - let telemetry = nested!(self.telemetry); - let torii = nested!(self.torii); - let chain_wide = nested!(self.chain_wide); - - emitter.finish()?; - - Ok(RootFull { - iroha: iroha.unwrap(), - genesis: genesis.unwrap(), - kura: kura.unwrap(), - sumeragi: sumeragi.unwrap(), - telemetry: telemetry.unwrap(), - logger: logger.unwrap(), - queue: queue.unwrap(), - snapshot: snapshot.unwrap(), - torii: torii.unwrap(), - network: network.unwrap(), - chain_wide: chain_wide.unwrap(), - }) - } -} - -impl FromEnv for RootPartial { - fn from_env>(env: &R) -> FromEnvResult { - fn from_env_nested(env: &R, emitter: &mut Emitter) -> Option - where - T: FromEnv, - R: ReadEnv, - RE: Error, - { - match FromEnv::from_env(env) { - Ok(parsed) => Some(parsed), - Err(errors) => { - emitter.emit_collection(errors); - None - } - } - } - - let mut emitter = Emitter::new(); - - let iroha = from_env_nested(env, &mut emitter); - let genesis = from_env_nested(env, &mut emitter); - let kura = from_env_nested(env, &mut emitter); - let sumeragi = from_env_nested(env, &mut emitter); - let network = from_env_nested(env, &mut emitter); - let logger = from_env_nested(env, &mut emitter); - let queue = from_env_nested(env, &mut emitter); - let snapshot = from_env_nested(env, &mut emitter); - let telemetry = from_env_nested(env, &mut emitter); - let torii = from_env_nested(env, &mut emitter); - let chain_wide = from_env_nested(env, &mut emitter); - - emitter.finish()?; - - Ok(Self { - extends: ExtendsPaths::None, - iroha: iroha.unwrap(), - genesis: genesis.unwrap(), - kura: kura.unwrap(), - sumeragi: sumeragi.unwrap(), - network: network.unwrap(), - logger: logger.unwrap(), - queue: queue.unwrap(), - snapshot: snapshot.unwrap(), - telemetry: telemetry.unwrap(), - torii: torii.unwrap(), - chain_wide: chain_wide.unwrap(), - }) - } -} - -#[derive(Deserialize, Serialize, Debug, Default, Merge)] -#[serde(deny_unknown_fields, default)] -pub struct IrohaPartial { - pub chain_id: UserField, - pub public_key: UserField, - pub private_key: UserField, - pub p2p_address: UserField, -} - #[derive(Debug)] -pub struct IrohaFull { +pub struct Iroha { pub chain_id: ChainId, pub public_key: PublicKey, pub private_key: PrivateKey, pub p2p_address: SocketAddr, } -impl UnwrapPartial for IrohaPartial { - type Output = IrohaFull; - - fn unwrap_partial(self) -> UnwrapPartialResult { - let mut emitter = Emitter::new(); - - if self.chain_id.is_none() { - emitter.emit_missing_field("iroha.chain_id"); - } - if self.public_key.is_none() { - emitter.emit_missing_field("iroha.public_key"); - } - if self.private_key.is_none() { - emitter.emit_missing_field("iroha.private_key"); - } - if self.p2p_address.is_none() { - emitter.emit_missing_field("iroha.p2p_address"); - } - - emitter.finish()?; - - Ok(IrohaFull { - chain_id: self.chain_id.get().unwrap(), - public_key: self.public_key.get().unwrap(), - private_key: self.private_key.get().unwrap(), - p2p_address: self.p2p_address.get().unwrap(), - }) - } -} - -impl IrohaFull { +impl Iroha { fn parse(self) -> Result { let key_pair = KeyPair::new(self.public_key, self.private_key).wrap_err("failed to construct a key pair from `iroha.public_key` and `iroha.private_key` configuration parameters")?; @@ -523,115 +293,14 @@ pub(crate) fn private_key_from_env( ParseEnvResult::ParseError } -impl FromEnv for IrohaPartial { - fn from_env>(env: &R) -> FromEnvResult - where - Self: Sized, - { - let mut emitter = Emitter::new(); - - let chain_id = env - .get("CHAIN_ID") - .map_err(|e| eyre!("{e}")) - .wrap_err("failed to read CHAIN_ID field (iroha.chain_id param)") - .map_or_else( - |err| { - emitter.emit(err); - None - }, - |maybe_value| maybe_value.map(ChainId::from), - ) - .into(); - let public_key = - ParseEnvResult::parse_simple(&mut emitter, env, "PUBLIC_KEY", "iroha.public_key") - .into(); - let private_key = - private_key_from_env(&mut emitter, env, "PRIVATE_KEY", "iroha.private_key").into(); - let p2p_address = - ParseEnvResult::parse_simple(&mut emitter, env, "P2P_ADDRESS", "iroha.p2p_address") - .into(); - - emitter.finish()?; - - Ok(Self { - chain_id, - public_key, - private_key, - p2p_address, - }) - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Default, Merge)] -#[serde(deny_unknown_fields, default)] -pub struct GenesisPartial { - pub public_key: UserField, - pub private_key: UserField, - pub file: UserField, -} - #[derive(Debug)] -pub struct GenesisFull { +pub struct Genesis { pub public_key: PublicKey, pub private_key: Option, pub file: Option, } -impl UnwrapPartial for GenesisPartial { - type Output = GenesisFull; - - fn unwrap_partial(self) -> UnwrapPartialResult { - let public_key = self - .public_key - .get() - .ok_or_else(|| MissingFieldError::new("genesis.public_key"))?; - - let private_key = self.private_key.get(); - let file = self.file.get(); - - Ok(GenesisFull { - public_key, - private_key, - file, - }) - } -} - -impl FromEnv for GenesisPartial { - fn from_env>(env: &R) -> FromEnvResult - where - Self: Sized, - { - let mut emitter = Emitter::new(); - - let public_key = ParseEnvResult::parse_simple( - &mut emitter, - env, - "GENESIS_PUBLIC_KEY", - "genesis.public_key", - ) - .into(); - let private_key = private_key_from_env( - &mut emitter, - env, - "GENESIS_PRIVATE_KEY", - "genesis.private_key", - ) - .into(); - let file = - ParseEnvResult::parse_simple(&mut emitter, env, "GENESIS_FILE", "genesis.file").into(); - - emitter.finish()?; - - Ok(Self { - public_key, - private_key, - file, - }) - } -} - -impl GenesisFull { +impl Genesis { fn parse(self, cli: &CliContext) -> Result { match (self.private_key, self.file, cli.submit_genesis) { (None, None, false) => Ok(actual::Genesis::Partial { @@ -661,59 +330,20 @@ pub enum GenesisConfigError { KeyPair(#[from] iroha_crypto::error::Error), } -/// `Kura` configuration. -#[derive(Clone, Deserialize, Serialize, Debug, Default, Merge)] -#[serde(deny_unknown_fields, default)] -pub struct KuraPartial { - pub init_mode: UserField, - pub block_store_path: UserField, - pub debug: KuraDebugPartial, -} - #[derive(Debug)] -pub struct KuraFull { +pub struct Kura { pub init_mode: Mode, pub block_store_path: PathBuf, - pub debug: KuraDebugFull, + pub debug: KuraDebug, } -impl UnwrapPartial for KuraPartial { - type Output = KuraFull; - - fn unwrap_partial(self) -> Result> { - let mut emitter = Emitter::new(); - - let init_mode = self.init_mode.unwrap_or_default(); - - let block_store_path = self - .block_store_path - .get() - .unwrap_or_else(|| PathBuf::from(DEFAULT_BLOCK_STORE_PATH)); - - let debug = UnwrapPartial::unwrap_partial(self.debug) - .map(Some) - .unwrap_or_else(|err| { - emitter.emit_collection(err); - None - }); - - emitter.finish()?; - - Ok(KuraFull { - init_mode, - block_store_path, - debug: debug.unwrap(), - }) - } -} - -impl KuraFull { +impl Kura { fn parse(self) -> actual::Kura { let Self { init_mode, block_store_path, debug: - KuraDebugFull { + KuraDebug { output_new_blocks: debug_output_new_blocks, }, } = self; @@ -726,82 +356,22 @@ impl KuraFull { } } -#[derive(Clone, Deserialize, Serialize, Debug, Default, Merge)] -#[serde(deny_unknown_fields, default)] -pub struct KuraDebugPartial { - output_new_blocks: UserField, -} - #[derive(Debug)] -pub struct KuraDebugFull { +pub struct KuraDebug { output_new_blocks: bool, } -impl UnwrapPartial for KuraDebugPartial { - type Output = KuraDebugFull; - - fn unwrap_partial(self) -> Result> { - Ok(KuraDebugFull { - output_new_blocks: self.output_new_blocks.unwrap_or(false), - }) - } -} - -impl FromEnv for KuraPartial { - fn from_env>(env: &R) -> FromEnvResult - where - Self: Sized, - { - let mut emitter = Emitter::new(); - - let init_mode = - ParseEnvResult::parse_simple(&mut emitter, env, "KURA_INIT_MODE", "kura.init_mode") - .into(); - let block_store_path = ParseEnvResult::parse_simple( - &mut emitter, - env, - "KURA_BLOCK_STORE", - "kura.block_store_path", - ) - .into(); - let debug_output_new_blocks = ParseEnvResult::parse_simple( - &mut emitter, - env, - "KURA_DEBUG_OUTPUT_NEW_BLOCKS", - "kura.debug.output_new_blocks", - ) - .into(); - - emitter.finish()?; - - Ok(Self { - init_mode, - block_store_path, - debug: KuraDebugPartial { - output_new_blocks: debug_output_new_blocks, - }, - }) - } -} - -#[derive(Deserialize, Serialize, Debug, Default, Merge)] -#[serde(deny_unknown_fields, default)] -pub struct SumeragiPartial { - pub trusted_peers: UserTrustedPeers, - pub debug: SumeragiDebugPartial, -} - #[derive(Debug)] -pub struct SumeragiFull { +pub struct Sumeragi { pub trusted_peers: Vec, - pub debug: SumeragiDebugFull, + pub debug: SumeragiDebug, } -impl SumeragiFull { +impl Sumeragi { fn parse(self) -> Result { let Self { trusted_peers, - debug: SumeragiDebugFull { force_soft_fork }, + debug: SumeragiDebug { force_soft_fork }, } = self; let trusted_peers = construct_unique_vec(trusted_peers)?; @@ -813,60 +383,11 @@ impl SumeragiFull { } } -impl UnwrapPartial for SumeragiPartial { - type Output = SumeragiFull; - - fn unwrap_partial(self) -> UnwrapPartialResult { - let mut emitter = Emitter::new(); - - let trusted_peers = self.trusted_peers.unwrap_partial().map_or_else( - |err| { - emitter.emit_collection(err); - None - }, - Some, - ); - - let debug = self.debug.unwrap_partial().map_or_else( - |err| { - emitter.emit_collection(err); - None - }, - Some, - ); - - emitter.finish()?; - - Ok(SumeragiFull { - trusted_peers: trusted_peers.unwrap(), - debug: debug.unwrap(), - }) - } -} - -#[derive(Deserialize, Serialize, Debug, Default, Merge)] -#[serde(deny_unknown_fields, default)] -pub struct SumeragiDebugPartial { - pub force_soft_fork: UserField, -} - -impl UnwrapPartial for SumeragiDebugPartial { - type Output = SumeragiDebugFull; - - fn unwrap_partial(self) -> UnwrapPartialResult { - Ok(SumeragiDebugFull { - force_soft_fork: self.force_soft_fork.unwrap_or(false), - }) - } -} - #[derive(Debug)] -pub struct SumeragiDebugFull { +pub struct SumeragiDebug { pub force_soft_fork: bool, } -impl FromEnvDefaultFallback for SumeragiPartial {} - #[derive(Deserialize, Serialize, Default, PartialEq, Eq, Debug, Clone)] #[serde(transparent)] pub struct UserTrustedPeers { @@ -901,49 +422,15 @@ fn construct_unique_vec( Ok(unique) } -#[derive(Deserialize, Serialize, Debug, Default, Merge)] -#[serde(deny_unknown_fields, default)] -pub struct NetworkPartial { - pub block_gossip_period: UserField, - pub max_blocks_per_gossip: UserField, - pub max_transactions_per_gossip: UserField, - pub transaction_gossip_period: UserField, -} - #[derive(Debug)] -pub struct NetworkFull { +pub struct Network { pub block_gossip_period: Duration, pub max_blocks_per_gossip: NonZeroU32, pub max_transactions_per_gossip: NonZeroU32, pub transaction_gossip_period: Duration, } -impl UnwrapPartial for NetworkPartial { - type Output = NetworkFull; - - fn unwrap_partial(self) -> UnwrapPartialResult { - Ok(NetworkFull { - block_gossip_period: self - .block_gossip_period - .map(UserDuration::get) - .unwrap_or(DEFAULT_BLOCK_GOSSIP_PERIOD), - transaction_gossip_period: self - .transaction_gossip_period - .map(UserDuration::get) - .unwrap_or(DEFAULT_TRANSACTION_GOSSIP_PERIOD), - max_transactions_per_gossip: self - .max_transactions_per_gossip - .get() - .unwrap_or(DEFAULT_MAX_TRANSACTIONS_PER_GOSSIP), - max_blocks_per_gossip: self - .max_blocks_per_gossip - .get() - .unwrap_or(DEFAULT_MAX_BLOCKS_PER_GOSSIP), - }) - } -} - -impl NetworkFull { +impl Network { fn parse(self) -> (actual::BlockSync, actual::TransactionGossiper) { let Self { max_blocks_per_gossip, @@ -965,24 +452,8 @@ impl NetworkFull { } } -impl FromEnvDefaultFallback for NetworkPartial {} - -#[derive(Deserialize, Serialize, Debug, Default, Merge)] -#[serde(deny_unknown_fields, default)] -pub struct QueuePartial { - /// The upper limit of the number of transactions waiting in the queue. - pub max_transactions_in_queue: UserField, - /// The upper limit of the number of transactions waiting in the queue for single user. - /// Use this option to apply throttling. - pub max_transactions_in_queue_per_user: UserField, - /// The transaction will be dropped after this time if it is still in the queue. - pub transaction_time_to_live: UserField, - /// The threshold to determine if a transaction has been tampered to have a future timestamp. - pub future_threshold: UserField, -} - #[derive(Debug, Clone, Copy)] -pub struct QueueFull { +pub struct Queue { /// The upper limit of the number of transactions waiting in the queue. pub max_transactions_in_queue: NonZeroUsize, /// The upper limit of the number of transactions waiting in the queue for single user. @@ -994,46 +465,8 @@ pub struct QueueFull { pub future_threshold: Duration, } -impl UnwrapPartial for QueuePartial { - type Output = QueueFull; - - fn unwrap_partial(self) -> UnwrapPartialResult { - Ok(QueueFull { - max_transactions_in_queue: self - .max_transactions_in_queue - .unwrap_or(DEFAULT_MAX_TRANSACTIONS_IN_QUEUE), - max_transactions_in_queue_per_user: self - .max_transactions_in_queue_per_user - .unwrap_or(DEFAULT_MAX_TRANSACTIONS_IN_QUEUE), - transaction_time_to_live: self - .transaction_time_to_live - .map_or(DEFAULT_TRANSACTION_TIME_TO_LIVE, UserDuration::get), - future_threshold: self - .future_threshold - .map_or(DEFAULT_FUTURE_THRESHOLD, UserDuration::get), - }) - } -} - -impl FromEnvDefaultFallback for QueuePartial {} - -/// 'Logger' configuration. -#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Default, Merge)] -// `tokio_console_addr` is not `Copy`, but warning appears without `tokio-console` feature -#[allow(missing_copy_implementations)] -#[serde(deny_unknown_fields, default)] -pub struct LoggerPartial { - /// Level of logging verbosity - pub level: UserField, - /// Output format - pub format: UserField, - #[cfg(feature = "tokio-console")] - /// Address of tokio console (only available under "tokio-console" feature) - pub tokio_console_addr: UserField, -} - #[derive(Debug, Clone)] -pub struct LoggerFull { +pub struct Logger { /// Level of logging verbosity pub level: Level, /// Output format @@ -1043,7 +476,7 @@ pub struct LoggerFull { pub tokio_console_addr: SocketAddr, } -impl Default for LoggerFull { +impl Default for Logger { fn default() -> Self { Self { level: Level::default(), @@ -1054,109 +487,23 @@ impl Default for LoggerFull { } } -impl UnwrapPartial for LoggerPartial { - type Output = LoggerFull; - - fn unwrap_partial(self) -> UnwrapPartialResult { - Ok(LoggerFull { - level: self.level.unwrap_or_default(), - format: self.format.unwrap_or_default(), - #[cfg(feature = "tokio-console")] - tokio_console_addr: self - .tokio_console_addr - .get() - .unwrap_or_else(|| DEFAULT_TOKIO_CONSOLE_ADDR.clone()), - }) - } -} - -impl FromEnv for LoggerPartial { - fn from_env>(env: &R) -> FromEnvResult - where - Self: Sized, - { - let mut emitter = Emitter::new(); - - let level = - ParseEnvResult::parse_simple(&mut emitter, env, "LOG_LEVEL", "logger.level").into(); - let format = - ParseEnvResult::parse_simple(&mut emitter, env, "LOG_FORMAT", "logger.format").into(); - - emitter.finish()?; - - Ok(Self { - level, - format, - ..Self::default() - }) - } -} - -#[derive(Clone, Deserialize, Serialize, Debug, Default, Merge)] -#[serde(deny_unknown_fields, default)] -pub struct TelemetryPartial { - pub name: UserField, - pub url: UserField, - pub min_retry_period: UserField, - pub max_retry_delay_exponent: UserField, - pub dev: TelemetryDevPartial, -} - #[derive(Debug)] -pub struct TelemetryFull { +pub struct Telemetry { // Fields here are Options so that it is possible to warn the user if e.g. they provided `min_retry_period`, but haven't // provided `name` and `url` pub name: Option, pub url: Option, pub min_retry_period: Option, pub max_retry_delay_exponent: Option, - pub dev: TelemetryDevFull, -} - -#[derive(Clone, Deserialize, Serialize, Debug, Default, Merge)] -#[serde(deny_unknown_fields, default)] -pub struct TelemetryDevPartial { - pub file: UserField, + pub dev: TelemetryDev, } #[derive(Debug)] -pub struct TelemetryDevFull { +pub struct TelemetryDev { pub file: Option, } -impl UnwrapPartial for TelemetryDevPartial { - type Output = TelemetryDevFull; - - fn unwrap_partial(self) -> UnwrapPartialResult { - Ok(TelemetryDevFull { - file: self.file.get(), - }) - } -} - -impl UnwrapPartial for TelemetryPartial { - type Output = TelemetryFull; - - fn unwrap_partial(self) -> UnwrapPartialResult { - let Self { - name, - url, - max_retry_delay_exponent, - min_retry_period, - dev, - } = self; - - Ok(TelemetryFull { - name: name.get(), - url: url.get(), - max_retry_delay_exponent: max_retry_delay_exponent.get(), - min_retry_period: min_retry_period.get().map(UserDuration::get), - dev: dev.unwrap_partial()?, - }) - } -} - -impl TelemetryFull { +impl Telemetry { fn parse( self, ) -> Result< @@ -1171,7 +518,7 @@ impl TelemetryFull { url, max_retry_delay_exponent, min_retry_period, - dev: TelemetryDevFull { file }, + dev: TelemetryDev { file }, } = self; let regular = match (name, url) { @@ -1198,91 +545,15 @@ impl TelemetryFull { } } -impl FromEnvDefaultFallback for TelemetryPartial {} - -#[derive(Debug, Clone, Deserialize, Serialize, Default, Merge)] -#[serde(deny_unknown_fields, default)] -pub struct SnapshotPartial { - pub create_every: UserField, - pub store_path: UserField, - pub creation_enabled: UserField, -} - #[derive(Debug, Clone)] -pub struct SnapshotFull { +pub struct Snapshot { pub create_every: Duration, pub store_path: PathBuf, pub creation_enabled: bool, } -impl UnwrapPartial for SnapshotPartial { - type Output = SnapshotFull; - - fn unwrap_partial(self) -> UnwrapPartialResult { - Ok(SnapshotFull { - creation_enabled: self.creation_enabled.unwrap_or(DEFAULT_ENABLED), - create_every: self - .create_every - .get() - .map_or(DEFAULT_SNAPSHOT_CREATE_EVERY_MS, UserDuration::get), - store_path: self - .store_path - .get() - .unwrap_or_else(|| PathBuf::from(DEFAULT_SNAPSHOT_PATH)), - }) - } -} - -impl FromEnv for SnapshotPartial { - fn from_env>(env: &R) -> FromEnvResult - where - Self: Sized, - { - let mut emitter = Emitter::new(); - - let store_path = ParseEnvResult::parse_simple( - &mut emitter, - env, - "SNAPSHOT_STORE", - "snapshot.store_path", - ) - .into(); - let creation_enabled = ParseEnvResult::parse_simple( - &mut emitter, - env, - "SNAPSHOT_CREATION_ENABLED", - "snapshot.creation_enabled", - ) - .into(); - - emitter.finish()?; - - Ok(Self { - store_path, - creation_enabled, - ..Self::default() - }) - } -} - -#[derive(Deserialize, Serialize, Debug, Default, Merge)] -#[serde(deny_unknown_fields, default)] -pub struct ChainWidePartial { - pub max_transactions_in_block: UserField, - pub block_time: UserField, - pub commit_time: UserField, - pub transaction_limits: UserField, - pub asset_metadata_limits: UserField, - pub asset_definition_metadata_limits: UserField, - pub account_metadata_limits: UserField, - pub domain_metadata_limits: UserField, - pub identifier_length_limits: UserField, - pub wasm_fuel_limit: UserField, - pub wasm_max_memory: UserField>, -} - #[derive(Debug)] -pub struct ChainWideFull { +pub struct ChainWide { pub max_transactions_in_block: NonZeroU32, pub block_time: Duration, pub commit_time: Duration, @@ -1296,47 +567,7 @@ pub struct ChainWideFull { pub wasm_max_memory: ByteSize, } -impl UnwrapPartial for ChainWidePartial { - type Output = ChainWideFull; - - fn unwrap_partial(self) -> UnwrapPartialResult { - Ok(ChainWideFull { - max_transactions_in_block: self.max_transactions_in_block.unwrap_or(DEFAULT_MAX_TXS), - block_time: self - .block_time - .map_or(DEFAULT_BLOCK_TIME, UserDuration::get), - commit_time: self - .commit_time - .map_or(DEFAULT_COMMIT_TIME, UserDuration::get), - transaction_limits: self - .transaction_limits - .unwrap_or(DEFAULT_TRANSACTION_LIMITS), - asset_metadata_limits: self - .asset_metadata_limits - .unwrap_or(DEFAULT_METADATA_LIMITS), - asset_definition_metadata_limits: self - .asset_definition_metadata_limits - .unwrap_or(DEFAULT_METADATA_LIMITS), - account_metadata_limits: self - .account_metadata_limits - .unwrap_or(DEFAULT_METADATA_LIMITS), - domain_metadata_limits: self - .domain_metadata_limits - .unwrap_or(DEFAULT_METADATA_LIMITS), - identifier_length_limits: self - .identifier_length_limits - .unwrap_or(DEFAULT_IDENT_LENGTH_LIMITS), - wasm_fuel_limit: self.wasm_fuel_limit.unwrap_or(DEFAULT_WASM_FUEL_LIMIT), - wasm_max_memory: self - .wasm_max_memory - .unwrap_or(ByteSize(DEFAULT_WASM_MAX_MEMORY)), - }) - } -} - -impl FromEnvDefaultFallback for ChainWidePartial {} - -impl ChainWideFull { +impl ChainWide { fn parse(self) -> actual::ChainWide { let Self { max_transactions_in_block, @@ -1370,52 +601,14 @@ impl ChainWideFull { } } -#[derive(Debug, Clone, Deserialize, Serialize, Default, Merge)] -#[serde(deny_unknown_fields, default)] -pub struct ToriiPartial { - pub address: UserField, - pub max_content_len: UserField>, - pub query_idle_time: UserField, -} - #[derive(Debug)] -pub struct ToriiFull { +pub struct Torii { pub address: SocketAddr, pub max_content_len: ByteSize, pub query_idle_time: Duration, } -impl UnwrapPartial for ToriiPartial { - type Output = ToriiFull; - - fn unwrap_partial(self) -> UnwrapPartialResult { - let mut emitter = Emitter::new(); - - if self.address.is_none() { - emitter.emit_missing_field("torii.address"); - } - - let max_content_len = self - .max_content_len - .get() - .unwrap_or(ByteSize(DEFAULT_MAX_CONTENT_LENGTH)); - - let query_idle_time = self - .query_idle_time - .map(UserDuration::get) - .unwrap_or(DEFAULT_QUERY_IDLE_TIME); - - emitter.finish()?; - - Ok(ToriiFull { - address: self.address.get().unwrap(), - max_content_len, - query_idle_time, - }) - } -} - -impl ToriiFull { +impl Torii { fn parse(self) -> (actual::Torii, actual::LiveQueryStore) { let torii = actual::Torii { address: self.address, @@ -1430,34 +623,12 @@ impl ToriiFull { } } -impl FromEnv for ToriiPartial { - fn from_env>(env: &R) -> FromEnvResult - where - Self: Sized, - { - let mut emitter = Emitter::new(); - - let address = - ParseEnvResult::parse_simple(&mut emitter, env, "API_ADDRESS", "torii.address").into(); - - emitter.finish()?; - - Ok(Self { - address, - ..Self::default() - }) - } -} - #[cfg(test)] mod tests { use iroha_config_base::{FromEnv, TestEnv}; use super::*; - use crate::parameters::{ - actual::{Logger, Sumeragi}, - user_layer::{IrohaPartial, RootPartial, SnapshotPartial}, - }; + use crate::parameters::user_layer::boilerplate::{IrohaPartial, RootPartial}; #[test] fn parses_private_key_from_env() { diff --git a/config/src/parameters/user_layer/boilerplate.rs b/config/src/parameters/user_layer/boilerplate.rs new file mode 100644 index 00000000000..c1cfbf4b58b --- /dev/null +++ b/config/src/parameters/user_layer/boilerplate.rs @@ -0,0 +1,864 @@ +//! Code that should be generated by a procmacro in future. + +use std::{ + error::Error, + fs::File, + io::Read, + num::{NonZeroU32, NonZeroUsize}, + path::{Path, PathBuf}, +}; + +use eyre::{eyre, Report, WrapErr}; +use iroha_config_base::{ + ByteSize, Emitter, ErrorsCollection, FromEnv, FromEnvDefaultFallback, FromEnvResult, Merge, + MissingFieldError, ParseEnvResult, ReadEnv, UnwrapPartial, UnwrapPartialResult, UserDuration, + UserField, +}; +use iroha_crypto::{PrivateKey, PublicKey}; +use iroha_data_model::{ + metadata::Limits as MetadataLimits, transaction::TransactionLimits, ChainId, LengthLimits, + Level, +}; +use iroha_primitives::addr::SocketAddr; +use serde::{Deserialize, Serialize}; +use url::Url; + +use crate::{ + kura::Mode, + logger::Format, + parameters::{ + defaults::{ + chain_wide::*, kura::*, logger::*, network::*, queue::*, snapshot::*, torii::*, + }, + user_layer, + user_layer::{ + ChainWide, ExtendsPaths, Genesis, Iroha, Kura, KuraDebug, Logger, Network, Queue, Root, + Snapshot, Sumeragi, SumeragiDebug, Telemetry, TelemetryDev, Torii, UserTrustedPeers, + }, + }, +}; + +#[derive(Deserialize, Serialize, Debug, Default, Merge)] +#[serde(deny_unknown_fields, default)] +pub struct RootPartial { + pub extends: ExtendsPaths, + pub iroha: IrohaPartial, + pub genesis: GenesisPartial, + pub kura: KuraPartial, + pub sumeragi: SumeragiPartial, + pub network: NetworkPartial, + pub logger: LoggerPartial, + pub queue: QueuePartial, + pub snapshot: SnapshotPartial, + pub telemetry: TelemetryPartial, + pub torii: ToriiPartial, + pub chain_wide: ChainWidePartial, +} + +impl RootPartial { + /// Creates new empty user configuration + pub fn new() -> Self { + // TODO: generate this function with macro. For now, use default + Default::default() + } + + pub fn from_toml(path: impl AsRef) -> eyre::Result { + let contents = { + let mut file = File::open(path.as_ref()).wrap_err_with(|| { + eyre!("cannot open file at location `{}`", path.as_ref().display()) + })?; + let mut contents = String::new(); + file.read_to_string(&mut contents)?; + contents + }; + let mut layer: Self = toml::from_str(&contents).wrap_err("failed to parse toml")?; + + let base_path = path + .as_ref() + .parent() + .expect("the config file path could not be empty or root"); + + layer.normalise_paths(base_path); + + if let Some(base) = + layer + .extends + .iter() + .try_fold(None, |acc: Option, extends_path| { + // extends path is not normalised relative to the config file yet + let full_path = base_path.join(extends_path); + + let base = Self::from_toml(&full_path) + .wrap_err_with(|| eyre!("cannot extend from `{}`", full_path.display()))?; + + match acc { + None => Ok::, Report>(Some(base)), + Some(other_base) => Ok(Some(other_base.merge(base))), + } + })? + { + layer.extends.used(); + layer = base.merge(layer); + }; + + Ok(layer) + } + + /// **Note:** this function doesn't affect `extends` + fn normalise_paths(&mut self, relative_to: impl AsRef) { + let path = relative_to.as_ref(); + + macro_rules! patch { + ($value:expr) => { + $value.as_mut().map(|x| { + *x = path.join(&x); + }) + }; + } + + patch!(self.genesis.file); + patch!(self.snapshot.store_path); + patch!(self.kura.block_store_path); + patch!(self.telemetry.dev.file); + } + + // FIXME workaround the inconvenient way `Merge::merge` works + pub fn merge(mut self, other: Self) -> Self { + Merge::merge(&mut self, other); + self + } +} + +impl UnwrapPartial for RootPartial { + type Output = Root; + + fn unwrap_partial(self) -> UnwrapPartialResult { + let mut emitter = Emitter::new(); + + macro_rules! nested { + ($item:expr) => { + match UnwrapPartial::unwrap_partial($item) { + Ok(value) => Some(value), + Err(error) => { + emitter.emit_collection(error); + None + } + } + }; + } + + let iroha = nested!(self.iroha); + let genesis = nested!(self.genesis); + let kura = nested!(self.kura); + let sumeragi = nested!(self.sumeragi); + let network = nested!(self.network); + let logger = nested!(self.logger); + let queue = nested!(self.queue); + let snapshot = nested!(self.snapshot); + let telemetry = nested!(self.telemetry); + let torii = nested!(self.torii); + let chain_wide = nested!(self.chain_wide); + + emitter.finish()?; + + Ok(Root { + iroha: iroha.unwrap(), + genesis: genesis.unwrap(), + kura: kura.unwrap(), + sumeragi: sumeragi.unwrap(), + telemetry: telemetry.unwrap(), + logger: logger.unwrap(), + queue: queue.unwrap(), + snapshot: snapshot.unwrap(), + torii: torii.unwrap(), + network: network.unwrap(), + chain_wide: chain_wide.unwrap(), + }) + } +} + +impl FromEnv for RootPartial { + fn from_env>(env: &R) -> FromEnvResult { + fn from_env_nested(env: &R, emitter: &mut Emitter) -> Option + where + T: FromEnv, + R: ReadEnv, + RE: Error, + { + match FromEnv::from_env(env) { + Ok(parsed) => Some(parsed), + Err(errors) => { + emitter.emit_collection(errors); + None + } + } + } + + let mut emitter = Emitter::new(); + + let iroha = from_env_nested(env, &mut emitter); + let genesis = from_env_nested(env, &mut emitter); + let kura = from_env_nested(env, &mut emitter); + let sumeragi = from_env_nested(env, &mut emitter); + let network = from_env_nested(env, &mut emitter); + let logger = from_env_nested(env, &mut emitter); + let queue = from_env_nested(env, &mut emitter); + let snapshot = from_env_nested(env, &mut emitter); + let telemetry = from_env_nested(env, &mut emitter); + let torii = from_env_nested(env, &mut emitter); + let chain_wide = from_env_nested(env, &mut emitter); + + emitter.finish()?; + + Ok(Self { + extends: ExtendsPaths::None, + iroha: iroha.unwrap(), + genesis: genesis.unwrap(), + kura: kura.unwrap(), + sumeragi: sumeragi.unwrap(), + network: network.unwrap(), + logger: logger.unwrap(), + queue: queue.unwrap(), + snapshot: snapshot.unwrap(), + telemetry: telemetry.unwrap(), + torii: torii.unwrap(), + chain_wide: chain_wide.unwrap(), + }) + } +} + +#[derive(Deserialize, Serialize, Debug, Default, Merge)] +#[serde(deny_unknown_fields, default)] +pub struct IrohaPartial { + pub chain_id: UserField, + pub public_key: UserField, + pub private_key: UserField, + pub p2p_address: UserField, +} + +impl UnwrapPartial for IrohaPartial { + type Output = Iroha; + + fn unwrap_partial(self) -> UnwrapPartialResult { + let mut emitter = Emitter::new(); + + if self.chain_id.is_none() { + emitter.emit_missing_field("iroha.chain_id"); + } + if self.public_key.is_none() { + emitter.emit_missing_field("iroha.public_key"); + } + if self.private_key.is_none() { + emitter.emit_missing_field("iroha.private_key"); + } + if self.p2p_address.is_none() { + emitter.emit_missing_field("iroha.p2p_address"); + } + + emitter.finish()?; + + Ok(Iroha { + chain_id: self.chain_id.get().unwrap(), + public_key: self.public_key.get().unwrap(), + private_key: self.private_key.get().unwrap(), + p2p_address: self.p2p_address.get().unwrap(), + }) + } +} + +impl FromEnv for IrohaPartial { + fn from_env>(env: &R) -> FromEnvResult + where + Self: Sized, + { + let mut emitter = Emitter::new(); + + let chain_id = env + .get("CHAIN_ID") + .map_err(|e| eyre!("{e}")) + .wrap_err("failed to read CHAIN_ID field (iroha.chain_id param)") + .map_or_else( + |err| { + emitter.emit(err); + None + }, + |maybe_value| maybe_value.map(ChainId::from), + ) + .into(); + let public_key = + ParseEnvResult::parse_simple(&mut emitter, env, "PUBLIC_KEY", "iroha.public_key") + .into(); + let private_key = + user_layer::private_key_from_env(&mut emitter, env, "PRIVATE_KEY", "iroha.private_key") + .into(); + let p2p_address = + ParseEnvResult::parse_simple(&mut emitter, env, "P2P_ADDRESS", "iroha.p2p_address") + .into(); + + emitter.finish()?; + + Ok(Self { + chain_id, + public_key, + private_key, + p2p_address, + }) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Default, Merge)] +#[serde(deny_unknown_fields, default)] +pub struct GenesisPartial { + pub public_key: UserField, + pub private_key: UserField, + pub file: UserField, +} + +impl UnwrapPartial for GenesisPartial { + type Output = Genesis; + + fn unwrap_partial(self) -> UnwrapPartialResult { + let public_key = self + .public_key + .get() + .ok_or_else(|| MissingFieldError::new("genesis.public_key"))?; + + let private_key = self.private_key.get(); + let file = self.file.get(); + + Ok(Genesis { + public_key, + private_key, + file, + }) + } +} + +impl FromEnv for GenesisPartial { + fn from_env>(env: &R) -> FromEnvResult + where + Self: Sized, + { + let mut emitter = Emitter::new(); + + let public_key = ParseEnvResult::parse_simple( + &mut emitter, + env, + "GENESIS_PUBLIC_KEY", + "genesis.public_key", + ) + .into(); + let private_key = user_layer::private_key_from_env( + &mut emitter, + env, + "GENESIS_PRIVATE_KEY", + "genesis.private_key", + ) + .into(); + let file = + ParseEnvResult::parse_simple(&mut emitter, env, "GENESIS_FILE", "genesis.file").into(); + + emitter.finish()?; + + Ok(Self { + public_key, + private_key, + file, + }) + } +} + +/// `Kura` configuration. +#[derive(Clone, Deserialize, Serialize, Debug, Default, Merge)] +#[serde(deny_unknown_fields, default)] +pub struct KuraPartial { + pub init_mode: UserField, + pub block_store_path: UserField, + pub debug: KuraDebugPartial, +} + +impl UnwrapPartial for KuraPartial { + type Output = Kura; + + fn unwrap_partial(self) -> Result> { + let mut emitter = Emitter::new(); + + let init_mode = self.init_mode.unwrap_or_default(); + + let block_store_path = self + .block_store_path + .get() + .unwrap_or_else(|| PathBuf::from(DEFAULT_BLOCK_STORE_PATH)); + + let debug = UnwrapPartial::unwrap_partial(self.debug) + .map(Some) + .unwrap_or_else(|err| { + emitter.emit_collection(err); + None + }); + + emitter.finish()?; + + Ok(Kura { + init_mode, + block_store_path, + debug: debug.unwrap(), + }) + } +} + +#[derive(Clone, Deserialize, Serialize, Debug, Default, Merge)] +#[serde(deny_unknown_fields, default)] +pub struct KuraDebugPartial { + output_new_blocks: UserField, +} + +impl UnwrapPartial for KuraDebugPartial { + type Output = KuraDebug; + + fn unwrap_partial(self) -> Result> { + Ok(KuraDebug { + output_new_blocks: self.output_new_blocks.unwrap_or(false), + }) + } +} + +impl FromEnv for KuraPartial { + fn from_env>(env: &R) -> FromEnvResult + where + Self: Sized, + { + let mut emitter = Emitter::new(); + + let init_mode = + ParseEnvResult::parse_simple(&mut emitter, env, "KURA_INIT_MODE", "kura.init_mode") + .into(); + let block_store_path = ParseEnvResult::parse_simple( + &mut emitter, + env, + "KURA_BLOCK_STORE", + "kura.block_store_path", + ) + .into(); + let debug_output_new_blocks = ParseEnvResult::parse_simple( + &mut emitter, + env, + "KURA_DEBUG_OUTPUT_NEW_BLOCKS", + "kura.debug.output_new_blocks", + ) + .into(); + + emitter.finish()?; + + Ok(Self { + init_mode, + block_store_path, + debug: KuraDebugPartial { + output_new_blocks: debug_output_new_blocks, + }, + }) + } +} + +#[derive(Deserialize, Serialize, Debug, Default, Merge)] +#[serde(deny_unknown_fields, default)] +pub struct SumeragiPartial { + pub trusted_peers: UserTrustedPeers, + pub debug: SumeragiDebugPartial, +} + +impl UnwrapPartial for SumeragiPartial { + type Output = Sumeragi; + + fn unwrap_partial(self) -> UnwrapPartialResult { + let mut emitter = Emitter::new(); + + let trusted_peers = self.trusted_peers.unwrap_partial().map_or_else( + |err| { + emitter.emit_collection(err); + None + }, + Some, + ); + + let debug = self.debug.unwrap_partial().map_or_else( + |err| { + emitter.emit_collection(err); + None + }, + Some, + ); + + emitter.finish()?; + + Ok(Sumeragi { + trusted_peers: trusted_peers.unwrap(), + debug: debug.unwrap(), + }) + } +} + +#[derive(Deserialize, Serialize, Debug, Default, Merge)] +#[serde(deny_unknown_fields, default)] +pub struct SumeragiDebugPartial { + pub force_soft_fork: UserField, +} + +impl UnwrapPartial for SumeragiDebugPartial { + type Output = SumeragiDebug; + + fn unwrap_partial(self) -> UnwrapPartialResult { + Ok(SumeragiDebug { + force_soft_fork: self.force_soft_fork.unwrap_or(false), + }) + } +} + +impl FromEnvDefaultFallback for SumeragiPartial {} + +#[derive(Deserialize, Serialize, Debug, Default, Merge)] +#[serde(deny_unknown_fields, default)] +pub struct NetworkPartial { + pub block_gossip_period: UserField, + pub max_blocks_per_gossip: UserField, + pub max_transactions_per_gossip: UserField, + pub transaction_gossip_period: UserField, +} + +impl UnwrapPartial for NetworkPartial { + type Output = Network; + + fn unwrap_partial(self) -> UnwrapPartialResult { + Ok(Network { + block_gossip_period: self + .block_gossip_period + .map(UserDuration::get) + .unwrap_or(DEFAULT_BLOCK_GOSSIP_PERIOD), + transaction_gossip_period: self + .transaction_gossip_period + .map(UserDuration::get) + .unwrap_or(DEFAULT_TRANSACTION_GOSSIP_PERIOD), + max_transactions_per_gossip: self + .max_transactions_per_gossip + .get() + .unwrap_or(DEFAULT_MAX_TRANSACTIONS_PER_GOSSIP), + max_blocks_per_gossip: self + .max_blocks_per_gossip + .get() + .unwrap_or(DEFAULT_MAX_BLOCKS_PER_GOSSIP), + }) + } +} + +impl FromEnvDefaultFallback for NetworkPartial {} + +#[derive(Deserialize, Serialize, Debug, Default, Merge)] +#[serde(deny_unknown_fields, default)] +pub struct QueuePartial { + /// The upper limit of the number of transactions waiting in the queue. + pub max_transactions_in_queue: UserField, + /// The upper limit of the number of transactions waiting in the queue for single user. + /// Use this option to apply throttling. + pub max_transactions_in_queue_per_user: UserField, + /// The transaction will be dropped after this time if it is still in the queue. + pub transaction_time_to_live: UserField, + /// The threshold to determine if a transaction has been tampered to have a future timestamp. + pub future_threshold: UserField, +} + +impl UnwrapPartial for QueuePartial { + type Output = Queue; + + fn unwrap_partial(self) -> UnwrapPartialResult { + Ok(Queue { + max_transactions_in_queue: self + .max_transactions_in_queue + .unwrap_or(DEFAULT_MAX_TRANSACTIONS_IN_QUEUE), + max_transactions_in_queue_per_user: self + .max_transactions_in_queue_per_user + .unwrap_or(DEFAULT_MAX_TRANSACTIONS_IN_QUEUE), + transaction_time_to_live: self + .transaction_time_to_live + .map_or(DEFAULT_TRANSACTION_TIME_TO_LIVE, UserDuration::get), + future_threshold: self + .future_threshold + .map_or(DEFAULT_FUTURE_THRESHOLD, UserDuration::get), + }) + } +} + +impl FromEnvDefaultFallback for QueuePartial {} + +/// 'Logger' configuration. +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Default, Merge)] +// `tokio_console_addr` is not `Copy`, but warning appears without `tokio-console` feature +#[allow(missing_copy_implementations)] +#[serde(deny_unknown_fields, default)] +pub struct LoggerPartial { + /// Level of logging verbosity + pub level: UserField, + /// Output format + pub format: UserField, + #[cfg(feature = "tokio-console")] + /// Address of tokio console (only available under "tokio-console" feature) + pub tokio_console_addr: UserField, +} + +impl UnwrapPartial for LoggerPartial { + type Output = Logger; + + fn unwrap_partial(self) -> UnwrapPartialResult { + Ok(Logger { + level: self.level.unwrap_or_default(), + format: self.format.unwrap_or_default(), + #[cfg(feature = "tokio-console")] + tokio_console_addr: self + .tokio_console_addr + .get() + .unwrap_or_else(|| DEFAULT_TOKIO_CONSOLE_ADDR.clone()), + }) + } +} + +impl FromEnv for LoggerPartial { + fn from_env>(env: &R) -> FromEnvResult + where + Self: Sized, + { + let mut emitter = Emitter::new(); + + let level = + ParseEnvResult::parse_simple(&mut emitter, env, "LOG_LEVEL", "logger.level").into(); + let format = + ParseEnvResult::parse_simple(&mut emitter, env, "LOG_FORMAT", "logger.format").into(); + + emitter.finish()?; + + Ok(Self { + level, + format, + ..Self::default() + }) + } +} + +#[derive(Clone, Deserialize, Serialize, Debug, Default, Merge)] +#[serde(deny_unknown_fields, default)] +pub struct TelemetryPartial { + pub name: UserField, + pub url: UserField, + pub min_retry_period: UserField, + pub max_retry_delay_exponent: UserField, + pub dev: TelemetryDevPartial, +} + +#[derive(Clone, Deserialize, Serialize, Debug, Default, Merge)] +#[serde(deny_unknown_fields, default)] +pub struct TelemetryDevPartial { + pub file: UserField, +} + +impl UnwrapPartial for TelemetryDevPartial { + type Output = TelemetryDev; + + fn unwrap_partial(self) -> UnwrapPartialResult { + Ok(TelemetryDev { + file: self.file.get(), + }) + } +} + +impl UnwrapPartial for TelemetryPartial { + type Output = Telemetry; + + fn unwrap_partial(self) -> UnwrapPartialResult { + let Self { + name, + url, + max_retry_delay_exponent, + min_retry_period, + dev, + } = self; + + Ok(Telemetry { + name: name.get(), + url: url.get(), + max_retry_delay_exponent: max_retry_delay_exponent.get(), + min_retry_period: min_retry_period.get().map(UserDuration::get), + dev: dev.unwrap_partial()?, + }) + } +} + +impl FromEnvDefaultFallback for TelemetryPartial {} + +#[derive(Debug, Clone, Deserialize, Serialize, Default, Merge)] +#[serde(deny_unknown_fields, default)] +pub struct SnapshotPartial { + pub create_every: UserField, + pub store_path: UserField, + pub creation_enabled: UserField, +} + +impl UnwrapPartial for SnapshotPartial { + type Output = Snapshot; + + fn unwrap_partial(self) -> UnwrapPartialResult { + Ok(Snapshot { + creation_enabled: self.creation_enabled.unwrap_or(DEFAULT_ENABLED), + create_every: self + .create_every + .get() + .map_or(DEFAULT_SNAPSHOT_CREATE_EVERY_MS, UserDuration::get), + store_path: self + .store_path + .get() + .unwrap_or_else(|| PathBuf::from(DEFAULT_SNAPSHOT_PATH)), + }) + } +} + +impl FromEnv for SnapshotPartial { + fn from_env>(env: &R) -> FromEnvResult + where + Self: Sized, + { + let mut emitter = Emitter::new(); + + let store_path = ParseEnvResult::parse_simple( + &mut emitter, + env, + "SNAPSHOT_STORE", + "snapshot.store_path", + ) + .into(); + let creation_enabled = ParseEnvResult::parse_simple( + &mut emitter, + env, + "SNAPSHOT_CREATION_ENABLED", + "snapshot.creation_enabled", + ) + .into(); + + emitter.finish()?; + + Ok(Self { + store_path, + creation_enabled, + ..Self::default() + }) + } +} + +#[derive(Deserialize, Serialize, Debug, Default, Merge)] +#[serde(deny_unknown_fields, default)] +pub struct ChainWidePartial { + pub max_transactions_in_block: UserField, + pub block_time: UserField, + pub commit_time: UserField, + pub transaction_limits: UserField, + pub asset_metadata_limits: UserField, + pub asset_definition_metadata_limits: UserField, + pub account_metadata_limits: UserField, + pub domain_metadata_limits: UserField, + pub identifier_length_limits: UserField, + pub wasm_fuel_limit: UserField, + pub wasm_max_memory: UserField>, +} + +impl UnwrapPartial for ChainWidePartial { + type Output = ChainWide; + + fn unwrap_partial(self) -> UnwrapPartialResult { + Ok(ChainWide { + max_transactions_in_block: self.max_transactions_in_block.unwrap_or(DEFAULT_MAX_TXS), + block_time: self + .block_time + .map_or(DEFAULT_BLOCK_TIME, UserDuration::get), + commit_time: self + .commit_time + .map_or(DEFAULT_COMMIT_TIME, UserDuration::get), + transaction_limits: self + .transaction_limits + .unwrap_or(DEFAULT_TRANSACTION_LIMITS), + asset_metadata_limits: self + .asset_metadata_limits + .unwrap_or(DEFAULT_METADATA_LIMITS), + asset_definition_metadata_limits: self + .asset_definition_metadata_limits + .unwrap_or(DEFAULT_METADATA_LIMITS), + account_metadata_limits: self + .account_metadata_limits + .unwrap_or(DEFAULT_METADATA_LIMITS), + domain_metadata_limits: self + .domain_metadata_limits + .unwrap_or(DEFAULT_METADATA_LIMITS), + identifier_length_limits: self + .identifier_length_limits + .unwrap_or(DEFAULT_IDENT_LENGTH_LIMITS), + wasm_fuel_limit: self.wasm_fuel_limit.unwrap_or(DEFAULT_WASM_FUEL_LIMIT), + wasm_max_memory: self + .wasm_max_memory + .unwrap_or(ByteSize(DEFAULT_WASM_MAX_MEMORY)), + }) + } +} + +impl FromEnvDefaultFallback for ChainWidePartial {} + +#[derive(Debug, Clone, Deserialize, Serialize, Default, Merge)] +#[serde(deny_unknown_fields, default)] +pub struct ToriiPartial { + pub address: UserField, + pub max_content_len: UserField>, + pub query_idle_time: UserField, +} + +impl UnwrapPartial for ToriiPartial { + type Output = Torii; + + fn unwrap_partial(self) -> UnwrapPartialResult { + let mut emitter = Emitter::new(); + + if self.address.is_none() { + emitter.emit_missing_field("torii.address"); + } + + let max_content_len = self + .max_content_len + .get() + .unwrap_or(ByteSize(DEFAULT_MAX_CONTENT_LENGTH)); + + let query_idle_time = self + .query_idle_time + .map(UserDuration::get) + .unwrap_or(DEFAULT_QUERY_IDLE_TIME); + + emitter.finish()?; + + Ok(Torii { + address: self.address.get().unwrap(), + max_content_len, + query_idle_time, + }) + } +} + +impl FromEnv for ToriiPartial { + fn from_env>(env: &R) -> FromEnvResult + where + Self: Sized, + { + let mut emitter = Emitter::new(); + + let address = + ParseEnvResult::parse_simple(&mut emitter, env, "API_ADDRESS", "torii.address").into(); + + emitter.finish()?; + + Ok(Self { + address, + ..Self::default() + }) + } +} From 5f02d156f7fdc97d9b3f2f1f983f76ab4b02adf8 Mon Sep 17 00:00:00 2001 From: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> Date: Mon, 29 Jan 2024 09:09:06 +0900 Subject: [PATCH 24/94] [refactor]: apply some lints Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- cli/src/lib.rs | 2 +- config/src/parameters/actual.rs | 2 +- config/src/parameters/user_layer.rs | 25 +++++++++++-------- .../src/parameters/user_layer/boilerplate.rs | 13 ++++++---- logger/src/lib.rs | 1 + telemetry/src/dev.rs | 4 +-- 6 files changed, 27 insertions(+), 20 deletions(-) diff --git a/cli/src/lib.rs b/cli/src/lib.rs index 77a3bab57c2..da1d31289fa 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -384,7 +384,7 @@ impl Iroha { .subscribe_on_telemetry(iroha_logger::telemetry::Channel::Future) .await .wrap_err("Failed to subscribe on telemetry")?; - let _handle = iroha_telemetry::dev::start(&config.file, receiver) + let _handle = iroha_telemetry::dev::start(config.file.clone(), receiver) .await .wrap_err("Failed to setup telemetry for futures")?; } diff --git a/config/src/parameters/actual.rs b/config/src/parameters/actual.rs index cb4bd624373..997aa7c9ac9 100644 --- a/config/src/parameters/actual.rs +++ b/config/src/parameters/actual.rs @@ -71,7 +71,7 @@ pub enum Genesis { impl Genesis { pub fn public_key(&self) -> &PublicKey { match self { - Self::Partial { public_key } => &public_key, + Self::Partial { public_key } => public_key, Self::Full { key_pair, .. } => key_pair.public_key(), } } diff --git a/config/src/parameters/user_layer.rs b/config/src/parameters/user_layer.rs index 0f16a319bde..1a891402dc1 100644 --- a/config/src/parameters/user_layer.rs +++ b/config/src/parameters/user_layer.rs @@ -62,6 +62,7 @@ pub enum ExtendsPathsIter<'a> { } impl ExtendsPaths { + #[allow(clippy::iter_without_into_iter)] // extra for this case pub fn iter(&self) -> ExtendsPathsIter<'_> { match &self { Self::None => ExtendsPathsIter::None, @@ -115,7 +116,7 @@ impl Root { Some, ); - let genesis = self.genesis.parse(&cli).map_or_else( + let genesis = self.genesis.parse(cli).map_or_else( |err| { // FIXME emitter.emit(eyre!("{err}")); @@ -189,22 +190,23 @@ impl Root { Ok(actual::Root { iroha, genesis, - sumeragi, + torii, kura, + sumeragi, block_sync, transaction_gossiper, - logger, - torii, live_query_store, + logger, queue, + snapshot, regular_telemetry, dev_telemetry, chain_wide, - snapshot, }) } } +#[derive(Copy, Clone)] pub struct CliContext { pub submit_genesis: bool, } @@ -301,7 +303,7 @@ pub struct Genesis { } impl Genesis { - fn parse(self, cli: &CliContext) -> Result { + fn parse(self, cli: CliContext) -> Result { match (self.private_key, self.file, cli.submit_genesis) { (None, None, false) => Ok(actual::Genesis::Partial { public_key: self.public_key, @@ -413,7 +415,7 @@ fn construct_unique_vec( unchecked: Vec, ) -> Result, eyre::Report> { let mut unique = UniqueVec::new(); - for x in unchecked.into_iter() { + for x in unchecked { let pushed = unique.push(x); if !pushed { Err(eyre!("found duplicate"))? @@ -476,6 +478,7 @@ pub struct Logger { pub tokio_console_addr: SocketAddr, } +#[allow(clippy::derivable_impls)] // triggers in absence of `tokio-console` feature impl Default for Logger { fn default() -> Self { Self { @@ -749,18 +752,18 @@ mod tests { #[test] fn iterating_over_extends() { impl ExtendsPaths { - fn into_str_vec(&self) -> Vec<&str> { + fn as_str_vec(&self) -> Vec<&str> { self.iter().map(|p| p.to_str().unwrap()).collect() } } let empty = ExtendsPaths::None; - assert_eq!(empty.into_str_vec(), Vec::<&str>::new()); + assert_eq!(empty.as_str_vec(), Vec::<&str>::new()); let single = ExtendsPaths::Single("single".into()); - assert_eq!(single.into_str_vec(), vec!["single"]); + assert_eq!(single.as_str_vec(), vec!["single"]); let multi = ExtendsPaths::Multiple(vec!["foo".into(), "bar".into(), "baz".into()]); - assert_eq!(multi.into_str_vec(), vec!["foo", "bar", "baz"]); + assert_eq!(multi.as_str_vec(), vec!["foo", "bar", "baz"]); } } diff --git a/config/src/parameters/user_layer/boilerplate.rs b/config/src/parameters/user_layer/boilerplate.rs index c1cfbf4b58b..c772e83ddf3 100644 --- a/config/src/parameters/user_layer/boilerplate.rs +++ b/config/src/parameters/user_layer/boilerplate.rs @@ -59,7 +59,7 @@ impl RootPartial { /// Creates new empty user configuration pub fn new() -> Self { // TODO: generate this function with macro. For now, use default - Default::default() + Self::default() } pub fn from_toml(path: impl AsRef) -> eyre::Result { @@ -123,6 +123,7 @@ impl RootPartial { } // FIXME workaround the inconvenient way `Merge::merge` works + #[must_use] pub fn merge(mut self, other: Self) -> Self { Merge::merge(&mut self, other); self @@ -390,12 +391,13 @@ impl UnwrapPartial for KuraPartial { .get() .unwrap_or_else(|| PathBuf::from(DEFAULT_BLOCK_STORE_PATH)); - let debug = UnwrapPartial::unwrap_partial(self.debug) - .map(Some) - .unwrap_or_else(|err| { + let debug = UnwrapPartial::unwrap_partial(self.debug).map_or_else( + |err| { emitter.emit_collection(err); None - }); + }, + Some, + ); emitter.finish()?; @@ -634,6 +636,7 @@ impl FromEnv for LoggerPartial { emitter.finish()?; + #[allow(clippy::needless_update)] // triggers if tokio console addr is feature-gated Ok(Self { level, format, diff --git a/logger/src/lib.rs b/logger/src/lib.rs index 35916a9a66f..0afa4e84c16 100644 --- a/logger/src/lib.rs +++ b/logger/src/lib.rs @@ -83,6 +83,7 @@ pub fn test_logger() -> LoggerHandle { // with ENV vars rather than by extending `test_logger` signature. This will both remain // `test_logger` simple and also will emphasise isolation which is necessary anyway in // case of singleton mocking (where the logger is the singleton). + #[allow(clippy::needless_update)] // triggers without "tokio-console" feature let config = Config { level: Level::DEBUG, format: Format::Pretty, diff --git a/telemetry/src/dev.rs b/telemetry/src/dev.rs index 4e5a8fb69f2..f5e4886449a 100644 --- a/telemetry/src/dev.rs +++ b/telemetry/src/dev.rs @@ -1,6 +1,6 @@ //! Module with development telemetry -use std::path::Path; +use std::path::PathBuf; use eyre::{Result, WrapErr}; use iroha_logger::telemetry::Event as Telemetry; @@ -16,7 +16,7 @@ use tokio_stream::{wrappers::BroadcastStream, StreamExt}; /// # Errors /// Fails if unable to open the file pub async fn start( - telemetry_file: impl AsRef, + telemetry_file: PathBuf, telemetry: Receiver, ) -> Result> { let mut stream = crate::futures::get_stream(BroadcastStream::new(telemetry).fuse()); From 8da4b83f9c42510e6bd119f63e5e2dd65b7a8b8c Mon Sep 17 00:00:00 2001 From: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> Date: Mon, 29 Jan 2024 09:13:11 +0900 Subject: [PATCH 25/94] [refactor]: update config naming in `[queue]` also run tests with all features enabled Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- config/src/parameters/actual.rs | 5 ++--- config/src/parameters/user_layer.rs | 4 ++-- config/src/parameters/user_layer/boilerplate.rs | 12 +++++------- config/tests/fixtures.rs | 17 ++++++++++------- core/src/queue.rs | 16 ++++++++-------- 5 files changed, 27 insertions(+), 27 deletions(-) diff --git a/config/src/parameters/actual.rs b/config/src/parameters/actual.rs index 997aa7c9ac9..9b72a716699 100644 --- a/config/src/parameters/actual.rs +++ b/config/src/parameters/actual.rs @@ -96,9 +96,8 @@ impl Default for Queue { Self { transaction_time_to_live: defaults::queue::DEFAULT_TRANSACTION_TIME_TO_LIVE, future_threshold: defaults::queue::DEFAULT_FUTURE_THRESHOLD, - max_transactions_in_queue: defaults::queue::DEFAULT_MAX_TRANSACTIONS_IN_QUEUE, - max_transactions_in_queue_per_user: - defaults::queue::DEFAULT_MAX_TRANSACTIONS_IN_QUEUE_PER_USER, + size: defaults::queue::DEFAULT_MAX_TRANSACTIONS_IN_QUEUE, + size_per_user: defaults::queue::DEFAULT_MAX_TRANSACTIONS_IN_QUEUE_PER_USER, } } } diff --git a/config/src/parameters/user_layer.rs b/config/src/parameters/user_layer.rs index 1a891402dc1..1fc04db0265 100644 --- a/config/src/parameters/user_layer.rs +++ b/config/src/parameters/user_layer.rs @@ -457,10 +457,10 @@ impl Network { #[derive(Debug, Clone, Copy)] pub struct Queue { /// The upper limit of the number of transactions waiting in the queue. - pub max_transactions_in_queue: NonZeroUsize, + pub size: NonZeroUsize, /// The upper limit of the number of transactions waiting in the queue for single user. /// Use this option to apply throttling. - pub max_transactions_in_queue_per_user: NonZeroUsize, + pub size_per_user: NonZeroUsize, /// The transaction will be dropped after this time if it is still in the queue. pub transaction_time_to_live: Duration, /// The threshold to determine if a transaction has been tampered to have a future timestamp. diff --git a/config/src/parameters/user_layer/boilerplate.rs b/config/src/parameters/user_layer/boilerplate.rs index c772e83ddf3..d6523d66aeb 100644 --- a/config/src/parameters/user_layer/boilerplate.rs +++ b/config/src/parameters/user_layer/boilerplate.rs @@ -558,10 +558,10 @@ impl FromEnvDefaultFallback for NetworkPartial {} #[serde(deny_unknown_fields, default)] pub struct QueuePartial { /// The upper limit of the number of transactions waiting in the queue. - pub max_transactions_in_queue: UserField, + pub size: UserField, /// The upper limit of the number of transactions waiting in the queue for single user. /// Use this option to apply throttling. - pub max_transactions_in_queue_per_user: UserField, + pub size_per_user: UserField, /// The transaction will be dropped after this time if it is still in the queue. pub transaction_time_to_live: UserField, /// The threshold to determine if a transaction has been tampered to have a future timestamp. @@ -573,11 +573,9 @@ impl UnwrapPartial for QueuePartial { fn unwrap_partial(self) -> UnwrapPartialResult { Ok(Queue { - max_transactions_in_queue: self - .max_transactions_in_queue - .unwrap_or(DEFAULT_MAX_TRANSACTIONS_IN_QUEUE), - max_transactions_in_queue_per_user: self - .max_transactions_in_queue_per_user + size: self.size.unwrap_or(DEFAULT_MAX_TRANSACTIONS_IN_QUEUE), + size_per_user: self + .size_per_user .unwrap_or(DEFAULT_MAX_TRANSACTIONS_IN_QUEUE), transaction_time_to_live: self .transaction_time_to_live diff --git a/config/tests/fixtures.rs b/config/tests/fixtures.rs index 916e6605ff5..31bd73444d0 100644 --- a/config/tests/fixtures.rs +++ b/config/tests/fixtures.rs @@ -93,17 +93,18 @@ fn minimal_config_snapshot() -> Result<()> { live_query_store: LiveQueryStore { query_idle_time: 30s, }, - logger: LoggerFull { + logger: Logger { level: INFO, format: Full, + tokio_console_addr: 127.0.0.1:5555, }, - queue: QueueFull { - max_transactions_in_queue: 65536, - max_transactions_in_queue_per_user: 65536, + queue: Queue { + size: 65536, + size_per_user: 65536, transaction_time_to_live: 86400s, future_threshold: 1s, }, - snapshot: SnapshotFull { + snapshot: Snapshot { create_every: 60s, store_path: "./storage/snapshot", creation_enabled: true, @@ -328,10 +329,11 @@ fn full_envs_set_is_consumed() -> Result<()> { format: Some( Pretty, ), + tokio_console_addr: None, }, queue: QueuePartial { - max_transactions_in_queue: None, - max_transactions_in_queue_per_user: None, + size: None, + size_per_user: None, transaction_time_to_live: None, future_threshold: None, }, @@ -447,6 +449,7 @@ fn multiple_extends_works() -> Result<()> { format: Some( Compact, ), + tokio_console_addr: None, }"#]]; expected.assert_eq(&format!("{layer:#?}")); diff --git a/core/src/queue.rs b/core/src/queue.rs index c82357da70d..54960950dc3 100644 --- a/core/src/queue.rs +++ b/core/src/queue.rs @@ -101,11 +101,11 @@ impl Queue { /// Makes queue from configuration pub fn from_configuration(cfg: Config) -> Self { Self { - tx_hashes: ArrayQueue::new(cfg.max_transactions_in_queue.get()), + tx_hashes: ArrayQueue::new(cfg.size.get()), accepted_txs: DashMap::new(), txs_per_user: DashMap::new(), - max_txs: cfg.max_transactions_in_queue, - max_txs_per_user: cfg.max_transactions_in_queue_per_user, + max_txs: cfg.size, + max_txs_per_user: cfg.size_per_user, tx_time_to_live: cfg.transaction_time_to_live, future_threshold: cfg.future_threshold, } @@ -429,7 +429,7 @@ mod tests { fn config_factory() -> Config { Config { transaction_time_to_live: Duration::from_secs(100), - max_transactions_in_queue: 100.try_into().unwrap(), + size: 100.try_into().unwrap(), ..Config::default() } } @@ -467,7 +467,7 @@ mod tests { let queue = Queue::from_configuration(Config { transaction_time_to_live: Duration::from_secs(100), - max_transactions_in_queue, + size: max_transactions_in_queue, ..Config::default() }); @@ -773,7 +773,7 @@ mod tests { let queue = Arc::new(Queue::from_configuration(Config { transaction_time_to_live: Duration::from_secs(100), - max_transactions_in_queue: 100_000_000.try_into().unwrap(), + size: 100_000_000.try_into().unwrap(), ..Config::default() })); @@ -908,8 +908,8 @@ mod tests { let queue = Queue::from_configuration(Config { transaction_time_to_live: Duration::from_secs(100), - max_transactions_in_queue: 100.try_into().unwrap(), - max_transactions_in_queue_per_user: 1.try_into().unwrap(), + size: 100.try_into().unwrap(), + size_per_user: 1.try_into().unwrap(), ..Config::default() }); From c2388983c8e428ef518a9fe72530ef0f197074e2 Mon Sep 17 00:00:00 2001 From: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> Date: Mon, 29 Jan 2024 09:33:50 +0900 Subject: [PATCH 26/94] [feat]: implement `Config::load` shorthand Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- cli/src/lib.rs | 17 ++++------------- client/examples/tutorial.rs | 11 ++--------- client/src/config.rs | 18 +++++++++++++++++- client/src/config/user_layer.rs | 8 ++++---- client/src/config/user_layer/boilerplate.rs | 10 +++++----- client_cli/src/main.rs | 17 ++--------------- config/src/parameters/actual.rs | 21 +++++++++++++++++++-- core/src/kiso.rs | 20 ++++++++------------ 8 files changed, 61 insertions(+), 61 deletions(-) diff --git a/cli/src/lib.rs b/cli/src/lib.rs index da1d31289fa..3620a0304e0 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -9,10 +9,7 @@ use core::sync::atomic::{AtomicBool, Ordering}; use std::{path::Path, sync::Arc}; use color_eyre::eyre::{eyre, Result, WrapErr}; -use iroha_config::parameters::{ - actual::Root as Config, - user_layer::{CliContext, RootPartial as RootLayer}, -}; +use iroha_config::parameters::{actual::Root as Config, user_layer::CliContext}; use iroha_core::{ block_sync::{BlockSynchronizer, BlockSynchronizerHandle}, gossiper::{TransactionGossiper, TransactionGossiperHandle}, @@ -514,16 +511,10 @@ pub fn read_config_and_genesis( path: impl AsRef, submit_genesis: bool, ) -> Result<(Config, Option)> { - use iroha_config::{ - base::{FromEnv as _, StdEnv, UnwrapPartial as _}, - parameters::actual::Genesis, - }; + use iroha_config::parameters::actual::Genesis; - let config = RootLayer::from_toml(path)?; - let config = config.merge(RootLayer::from_env(&StdEnv)?); - let config = config - .unwrap_partial()? - .parse(CliContext { submit_genesis })?; + let config = Config::load(path, CliContext { submit_genesis }) + .wrap_err("failed to load configuration")?; let genesis = if let Genesis::Full { key_pair, file } = &config.genesis { let raw_block = RawGenesisBlock::from_path(file)?; diff --git a/client/examples/tutorial.rs b/client/examples/tutorial.rs index 8f59897620a..9c56ee391fd 100644 --- a/client/examples/tutorial.rs +++ b/client/examples/tutorial.rs @@ -2,20 +2,13 @@ //! use eyre::{Error, WrapErr}; -use iroha_client::config::{base::UnwrapPartial, user_layer::RootPartial as UserConfig, Config}; +use iroha_client::config::Config; // #region rust_config_crates // #endregion rust_config_crates fn main() { // #region rust_config_load - let config_loc = "../config_samples/swarm/client.toml"; - let config = UserConfig::from_toml(config_loc) - .wrap_err("Unable to load the configuration file at `.....`") - .expect("Config file is loading normally.") - .unwrap_partial() - .expect("Config should have all required fields") - .parse() - .expect("Config should be semantically valid"); + let config = Config::load("../config_samples/swarm/client.toml").unwrap(); // #endregion rust_config_load // Your code goes here… diff --git a/client/src/config.rs b/client/src/config.rs index a3523f184e7..1f0c5e3b65b 100644 --- a/client/src/config.rs +++ b/client/src/config.rs @@ -4,17 +4,20 @@ #![allow(unused, missing_docs)] use core::str::FromStr; -use std::{num::NonZeroU64, time::Duration}; +use std::{num::NonZeroU64, path::Path, time::Duration}; use derive_more::Display; use eyre::Result; pub use iroha_config::base; +use iroha_config::base::UnwrapPartial; use iroha_crypto::prelude::*; use iroha_data_model::{prelude::*, ChainId}; use iroha_primitives::small::SmallStr; use serde::{Deserialize, Serialize}; use url::Url; +use crate::config::user_layer::RootPartial; + pub mod user_layer; #[allow(unsafe_code)] @@ -79,3 +82,16 @@ pub struct Config { pub transaction_status_timeout: Duration, pub transaction_add_nonce: bool, } + +impl Config { + /// Loads configuration from a file + /// + /// # Errors + /// - unable to load config from a TOML file + /// - unable to validate loaded config + pub fn load(path: impl AsRef) -> std::result::Result { + let config = RootPartial::from_toml(path)?; + let config = config.unwrap_partial()?.parse()?; + Ok(config) + } +} diff --git a/client/src/config/user_layer.rs b/client/src/config/user_layer.rs index 2631239cbba..6e35bb3d217 100644 --- a/client/src/config/user_layer.rs +++ b/client/src/config/user_layer.rs @@ -1,5 +1,8 @@ -use std::{io::Read, time::Duration}; +mod boilerplate; + +use std::{fs::File, io::Read, path::Path, time::Duration}; +pub use boilerplate::*; use eyre::{eyre, Context, Report}; use iroha_config::base::{Emitter, ErrorsCollection, FromEnvDefaultFallback, Merge, UnwrapPartial}; use iroha_crypto::{KeyPair, PrivateKey, PublicKey}; @@ -9,9 +12,6 @@ use url::Url; use crate::config::BasicAuth; -mod boilerplate; -pub use boilerplate::*; - #[derive(Clone, Debug)] pub struct Root { pub chain_id: ChainId, diff --git a/client/src/config/user_layer/boilerplate.rs b/client/src/config/user_layer/boilerplate.rs index bc527933c75..faf20606d1b 100644 --- a/client/src/config/user_layer/boilerplate.rs +++ b/client/src/config/user_layer/boilerplate.rs @@ -2,16 +2,16 @@ use std::{fs::File, io::Read, path::Path}; -use eyre::{eyre, WrapErr}; +use eyre::{eyre, Context}; +use iroha_config::base::{ + Emitter, FromEnvDefaultFallback, Merge, MissingFieldError, UnwrapPartial, UnwrapPartialResult, + UserDuration, UserField, +}; use iroha_crypto::{PrivateKey, PublicKey}; use iroha_data_model::{account::AccountId, ChainId}; use serde::Deserialize; use crate::config::{ - base::{ - Emitter, FromEnvDefaultFallback, Merge, MissingFieldError, UnwrapPartial, - UnwrapPartialResult, UserDuration, UserField, - }, user_layer::{Account, Api, OnlyHttpUrl, Root, Transaction}, BasicAuth, DEFAULT_ADD_TRANSACTION_NONCE, DEFAULT_TRANSACTION_STATUS_TIMEOUT, DEFAULT_TRANSACTION_TIME_TO_LIVE, diff --git a/client_cli/src/main.rs b/client_cli/src/main.rs index 0724308372c..2adfda13882 100644 --- a/client_cli/src/main.rs +++ b/client_cli/src/main.rs @@ -2,7 +2,7 @@ use std::{ fs::{self, read as read_file}, io::{stdin, stdout}, - path::{Path, PathBuf}, + path::PathBuf, str::FromStr, time::Duration, }; @@ -217,7 +217,7 @@ fn main() -> Result<()> { skip_mst_check, } = clap::Parser::parse(); - let config = load_config(config_path)?; + let config = Config::load(config_path)?; if verbose { eprintln!( @@ -236,19 +236,6 @@ fn main() -> Result<()> { subcommand.run(&mut context) } -fn load_config(path: impl AsRef) -> Result { - use iroha_client::config::{ - base::{FromEnv, StdEnv, UnwrapPartial}, - user_layer::RootPartial, - }; - - let layer = RootPartial::from_toml(path)?; - let layer = layer.merge(RootPartial::from_env(&StdEnv)?); - let config = layer.unwrap_partial()?.parse()?; - - Ok(config) -} - /// Submit instruction with metadata to network. /// /// # Errors diff --git a/config/src/parameters/actual.rs b/config/src/parameters/actual.rs index 9b72a716699..6676382cf5d 100644 --- a/config/src/parameters/actual.rs +++ b/config/src/parameters/actual.rs @@ -1,10 +1,10 @@ use std::{ num::{NonZeroU32, NonZeroU64, NonZeroUsize}, - path::PathBuf, + path::{Path, PathBuf}, time::Duration, }; -use iroha_config_base::ByteSize; +use iroha_config_base::{ByteSize, FromEnv, StdEnv, UnwrapPartial}; use iroha_crypto::{KeyPair, PublicKey}; use iroha_data_model::{ metadata::Limits as MetadataLimits, peer::PeerId, transaction::TransactionLimits, ChainId, @@ -39,6 +39,21 @@ pub struct Root { pub chain_wide: ChainWide, } +impl Root { + /// Loads configuration from a file and environment variables + /// + /// # Errors + /// - unable to load config from a TOML file + /// - unable to parse config from envs + /// - unable to validate loaded config + pub fn load(path: impl AsRef, cli: CliContext) -> Result { + let config = RootPartial::from_toml(path)?; + let config = config.merge(RootPartial::from_env(&StdEnv)?); + let config = config.unwrap_partial()?.parse(cli)?; + Ok(config) + } +} + #[derive(Debug, Clone)] pub struct Iroha { pub chain_id: ChainId, @@ -104,6 +119,8 @@ impl Default for Queue { pub use user_layer::{Logger, Queue, Snapshot}; +use crate::parameters::user_layer::{CliContext, RootPartial}; + #[derive(Debug, Clone)] pub struct Sumeragi { pub trusted_peers: UniqueVec, diff --git a/core/src/kiso.rs b/core/src/kiso.rs index 4c09e5e146c..486e97c7949 100644 --- a/core/src/kiso.rs +++ b/core/src/kiso.rs @@ -158,20 +158,16 @@ mod tests { use super::*; fn test_config() -> Root { - use iroha_config::{ - base::UnwrapPartial, - parameters::user_layer::{CliContext, RootPartial}, - }; + use iroha_config::parameters::user_layer::CliContext; - // FIXME Specifying path here might break! - RootPartial::from_toml("../config/iroha_test_config.toml") - .expect("test config should be valid (or it is a bug)") - .unwrap_partial() - .expect("test config should be exhaustive") - .parse(CliContext { + Root::load( + // FIXME Specifying path here might break! + "../config/iroha_test_config.toml", + CliContext { submit_genesis: true, - }) - .expect("test config should be valid") + }, + ) + .expect("test config should be valid, it is probably a bug") } #[tokio::test] From c9891a630b443bff17122cebb8b8e5d3534f968e Mon Sep 17 00:00:00 2001 From: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> Date: Mon, 29 Jan 2024 09:35:25 +0900 Subject: [PATCH 27/94] [docs]: add comment Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- config/src/parameters/user_layer.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/config/src/parameters/user_layer.rs b/config/src/parameters/user_layer.rs index 1fc04db0265..b7ec1d3a42e 100644 --- a/config/src/parameters/user_layer.rs +++ b/config/src/parameters/user_layer.rs @@ -470,6 +470,9 @@ pub struct Queue { #[derive(Debug, Clone)] pub struct Logger { /// Level of logging verbosity + // TODO: parse user provided value in a case insensitive way, + // because `format` is set in lowercase, and `LOG_LEVEL=INFO` + `LOG_FORMAT=pretty` + // looks inconsistent pub level: Level, /// Output format pub format: Format, From d371665f32207aaeb3a1d9b15e1b075393abf222 Mon Sep 17 00:00:00 2001 From: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> Date: Mon, 29 Jan 2024 16:58:05 +0900 Subject: [PATCH 28/94] [ci]: fix typo Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- scripts/tests/consistency.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/tests/consistency.sh b/scripts/tests/consistency.sh index 01c5a4fb1c3..305151b4f78 100755 --- a/scripts/tests/consistency.sh +++ b/scripts/tests/consistency.sh @@ -19,7 +19,7 @@ case $1 in # FIXME: not nice; add an option to `kagami swarm` to print content into stdout? # it is not a default behaviour because Kagami resolves `build` path relative # to the output file location - temp_file="configs_samples/swarm/docker-compose.TMP.yml" + temp_file="config_samples/swarm/docker-compose.TMP.yml" full_cmd="$cmd_base --outfile $temp_file" eval "$full_cmd" @@ -42,7 +42,7 @@ case $1 in } - do_check "$(command_base_for_single)" "configs_samples/swarm/docker-compose.single.yml" - do_check "$(command_base_for_multiple_local)" "configs_samples/swarm/docker-compose.local.yml" - do_check "$(command_base_for_default)" "configs_samples/swarm/docker-compose.yml" + do_check "$(command_base_for_single)" "config_samples/swarm/docker-compose.single.yml" + do_check "$(command_base_for_multiple_local)" "config_samples/swarm/docker-compose.local.yml" + do_check "$(command_base_for_default)" "config_samples/swarm/docker-compose.yml" esac From 2d4760c2edf271aacc255d4f8cbb8917ab1c1756 Mon Sep 17 00:00:00 2001 From: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> Date: Tue, 30 Jan 2024 06:02:30 +0900 Subject: [PATCH 29/94] [ci]: install `tomli_w` via pacman Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- .github/workflows/iroha2-dev-pr.yml | 5 ++++- .github/workflows/iroha2-release-pr.yml | 3 ++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/iroha2-dev-pr.yml b/.github/workflows/iroha2-dev-pr.yml index 20e622e774b..776c8ca993d 100644 --- a/.github/workflows/iroha2-dev-pr.yml +++ b/.github/workflows/iroha2-dev-pr.yml @@ -143,7 +143,10 @@ jobs: cargo build --bin iroha - name: Setup test Iroha 2 environment on the bare metal run: | - pip3 install -r scripts/requirements.txt --no-input + # FIXME: not sure how to install it in the right way. `requirements.txt` triggers + # "This environment is externally managed" error + pacman -S python-tomli-w + # pip3 install -r scripts/requirements.txt --no-input ./scripts/test_env.py setup - name: Mark binaries as executable run: | diff --git a/.github/workflows/iroha2-release-pr.yml b/.github/workflows/iroha2-release-pr.yml index f064ca99b0e..208627ee56f 100644 --- a/.github/workflows/iroha2-release-pr.yml +++ b/.github/workflows/iroha2-release-pr.yml @@ -36,7 +36,8 @@ jobs: cargo build --bin iroha - name: Setup test Iroha 2 environment on bare metal run: | - pip3 install -r scripts/requirements.txt --no-input + # TODO + # pip3 install -r scripts/requirements.txt --no-input ./scripts/test_env.py setup - name: Mark binaries as executable run: | From 7cc054107f336c9e61687ba581215f8b5fb4459d Mon Sep 17 00:00:00 2001 From: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> Date: Tue, 30 Jan 2024 06:02:44 +0900 Subject: [PATCH 30/94] [chore]: fix whitespace Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- torii/src/lib.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/torii/src/lib.rs b/torii/src/lib.rs index 0ad6dc946f4..986af0fc5e9 100644 --- a/torii/src/lib.rs +++ b/torii/src/lib.rs @@ -39,7 +39,6 @@ use warp::{ pub(crate) mod utils; mod event; mod routing; - mod stream; /// Main network handler and the only entrypoint of the Iroha. From 4d2ddbed959d01e19c4b56d3766539dab26f9985 Mon Sep 17 00:00:00 2001 From: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> Date: Tue, 30 Jan 2024 06:10:00 +0900 Subject: [PATCH 31/94] [revert]: update the snapshot store path Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- config/src/parameters/defaults.rs | 3 ++- config/tests/fixtures.rs | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/config/src/parameters/defaults.rs b/config/src/parameters/defaults.rs index 852c9726458..935294590b2 100644 --- a/config/src/parameters/defaults.rs +++ b/config/src/parameters/defaults.rs @@ -45,7 +45,8 @@ pub mod network { pub mod snapshot { use super::*; - pub const DEFAULT_SNAPSHOT_PATH: &str = "./storage/snapshot"; + // TODO: nest to `./storage/snapshot` for easier management + pub const DEFAULT_SNAPSHOT_PATH: &str = "./storage"; // Default frequency of making snapshots is 1 minute, need to be adjusted for larger world state view size pub const DEFAULT_SNAPSHOT_CREATE_EVERY_MS: Duration = Duration::from_secs(60); pub const DEFAULT_ENABLED: bool = true; diff --git a/config/tests/fixtures.rs b/config/tests/fixtures.rs index 31bd73444d0..84097aa9ba6 100644 --- a/config/tests/fixtures.rs +++ b/config/tests/fixtures.rs @@ -106,7 +106,7 @@ fn minimal_config_snapshot() -> Result<()> { }, snapshot: Snapshot { create_every: 60s, - store_path: "./storage/snapshot", + store_path: "./storage", creation_enabled: true, }, regular_telemetry: None, From f76a5914c7ece16678e3549ae88012ad70fe28da Mon Sep 17 00:00:00 2001 From: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> Date: Tue, 30 Jan 2024 17:18:54 +0900 Subject: [PATCH 32/94] [refactor]: fix pytests - do not mutate client config from tests, override via env instead - add `TORII_URL` env var to client config Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- client/src/config/user_layer.rs | 54 ++++++++++++++++--- client/src/config/user_layer/boilerplate.rs | 50 +++++++++++++++-- client_cli/pytests/poetry.lock | 10 ++-- client_cli/pytests/pyproject.toml | 1 + .../pytests/src/client_cli/client_cli.py | 6 ++- .../pytests/src/client_cli/configuration.py | 50 +++++++++-------- config_samples/examples/client.example.toml | 1 + 7 files changed, 133 insertions(+), 39 deletions(-) diff --git a/client/src/config/user_layer.rs b/client/src/config/user_layer.rs index 6e35bb3d217..55e0d17d4a0 100644 --- a/client/src/config/user_layer.rs +++ b/client/src/config/user_layer.rs @@ -1,6 +1,6 @@ mod boilerplate; -use std::{fs::File, io::Read, path::Path, time::Duration}; +use std::{fs::File, io::Read, path::Path, str::FromStr, time::Duration}; pub use boilerplate::*; use eyre::{eyre, Context, Report}; @@ -104,16 +104,54 @@ pub struct Transaction { #[derive(Debug, Clone, Eq, PartialEq)] pub struct OnlyHttpUrl(Url); -impl<'de> Deserialize<'de> for OnlyHttpUrl { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let url = Url::deserialize(deserializer)?; +impl FromStr for OnlyHttpUrl { + type Err = ParseHttpUrlError; + + fn from_str(s: &str) -> Result { + let url = Url::from_str(s)?; if url.scheme() == "http" { Ok(Self(url)) } else { - Err(serde::de::Error::custom("only HTTP scheme is supported")) + Err(ParseHttpUrlError::NotHttp { + found: url.scheme().to_owned(), + }) } } } + +#[derive(Debug, thiserror::Error)] +pub enum ParseHttpUrlError { + #[error(transparent)] + Parse(#[from] url::ParseError), + #[error("expected `http` scheme, found: `{found}`")] + NotHttp { found: String }, +} + +iroha_config::base::impl_deserialize_from_str!(OnlyHttpUrl); + +#[cfg(test)] +mod tests { + use std::collections::HashSet; + + use iroha_config::base::{FromEnv as _, TestEnv}; + + use super::*; + + #[test] + fn parses_all_envs() { + let env = TestEnv::new().set("TORII_URL", "http://localhost:8080"); + + let layer = RootPartial::from_env(&env).expect("should not fail since env is valid"); + + assert_eq!(env.unvisited(), HashSet::new()) + } + + #[test] + fn non_http_url_error() { + let error = "https://localhost:1123" + .parse::() + .expect_err("should not allow https"); + + assert_eq!(format!("{error}"), "expected `http` scheme, found: `https`"); + } +} diff --git a/client/src/config/user_layer/boilerplate.rs b/client/src/config/user_layer/boilerplate.rs index faf20606d1b..1ecb9f331cb 100644 --- a/client/src/config/user_layer/boilerplate.rs +++ b/client/src/config/user_layer/boilerplate.rs @@ -1,10 +1,10 @@ //! Code to be generated by a proc macro in future -use std::{fs::File, io::Read, path::Path}; +use std::{error::Error, fs::File, io::Read, path::Path}; use eyre::{eyre, Context}; use iroha_config::base::{ - Emitter, FromEnvDefaultFallback, Merge, MissingFieldError, UnwrapPartial, UnwrapPartialResult, + Emitter, FromEnv, Merge, MissingFieldError, ParseEnvResult, UnwrapPartial, UnwrapPartialResult, UserDuration, UserField, }; use iroha_crypto::{PrivateKey, PublicKey}; @@ -12,6 +12,7 @@ use iroha_data_model::{account::AccountId, ChainId}; use serde::Deserialize; use crate::config::{ + base::{FromEnvResult, ReadEnv}, user_layer::{Account, Api, OnlyHttpUrl, Root, Transaction}, BasicAuth, DEFAULT_ADD_TRANSACTION_NONCE, DEFAULT_TRANSACTION_STATUS_TIMEOUT, DEFAULT_TRANSACTION_TIME_TO_LIVE, @@ -53,7 +54,31 @@ impl RootPartial { } // FIXME: should config be read from ENV? -impl FromEnvDefaultFallback for RootPartial {} +impl FromEnv for RootPartial { + fn from_env>(env: &R) -> FromEnvResult + where + Self: Sized, + { + let mut emitter = Emitter::new(); + + let api = ApiPartial::from_env(env).map_or_else( + |err| { + emitter.emit_collection(err); + None + }, + Some, + ); + + emitter.finish()?; + + Ok(Self { + chain_id: None.into(), + api: api.unwrap(), + account: AccountPartial::default(), + transaction: TransactionPartial::default(), + }) + } +} impl UnwrapPartial for RootPartial { type Output = Root; @@ -100,6 +125,25 @@ impl UnwrapPartial for ApiPartial { } } +impl FromEnv for ApiPartial { + fn from_env>(env: &R) -> FromEnvResult + where + Self: Sized, + { + let mut emitter = Emitter::new(); + + let torii_url = + ParseEnvResult::parse_simple(&mut emitter, env, "TORII_URL", "api.torii_url").into(); + + emitter.finish()?; + + Ok(Self { + torii_url, + basic_auth: None.into(), + }) + } +} + #[derive(Debug, Clone, Deserialize, Eq, PartialEq, Default, Merge)] #[serde(deny_unknown_fields, default)] pub struct AccountPartial { diff --git a/client_cli/pytests/poetry.lock b/client_cli/pytests/poetry.lock index 0da88839fa3..53341202632 100644 --- a/client_cli/pytests/poetry.lock +++ b/client_cli/pytests/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. [[package]] name = "allure-pytest" @@ -530,13 +530,13 @@ files = [ [[package]] name = "tomlkit" -version = "0.11.8" +version = "0.12.3" description = "Style preserving TOML library" optional = false python-versions = ">=3.7" files = [ - {file = "tomlkit-0.11.8-py3-none-any.whl", hash = "sha256:8c726c4c202bdb148667835f68d68780b9a003a9ec34167b6c673b38eff2a171"}, - {file = "tomlkit-0.11.8.tar.gz", hash = "sha256:9330fc7faa1db67b541b28e62018c17d20be733177d290a13b24c62d1614e0c3"}, + {file = "tomlkit-0.12.3-py3-none-any.whl", hash = "sha256:b0a645a9156dc7cb5d3a1f0d4bab66db287fcb8e0430bdd4664a095ea16414ba"}, + {file = "tomlkit-0.12.3.tar.gz", hash = "sha256:75baf5012d06501f07bee5bf8e801b9f343e7aac5a92581f20f80ce632e6b5a4"}, ] [[package]] @@ -637,4 +637,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "8c3a17410644637cb551ef878cacf0d76e83eded4d32af50e1312f934e24639b" +content-hash = "101321a5a8443974ff254d60b75c4a59c0da6a8b2e2387a8a3f666692a58b834" diff --git a/client_cli/pytests/pyproject.toml b/client_cli/pytests/pyproject.toml index 0fdeaaead5f..707d8df5d4b 100644 --- a/client_cli/pytests/pyproject.toml +++ b/client_cli/pytests/pyproject.toml @@ -11,6 +11,7 @@ faker = "*" allure-python-commons = "*" cryptography = "*" python-dotenv = "*" +tomlkit = "^0.12.3" [tool.poetry.dev-dependencies] pytest = "*" diff --git a/client_cli/pytests/src/client_cli/client_cli.py b/client_cli/pytests/src/client_cli/client_cli.py index cdd33b58c59..dfdc8629ef8 100644 --- a/client_cli/pytests/src/client_cli/client_cli.py +++ b/client_cli/pytests/src/client_cli/client_cli.py @@ -254,19 +254,21 @@ def execute(self, command=None): :return: The current ClientCli object. :rtype: ClientCli """ + self.config.randomise_torii_url() if command is None: command = self.command else: command = [self.BASE_PATH] + self.BASE_FLAGS + command.split() allure_command = ' '.join(map(str, command[3:])) print(allure_command) - with allure.step(f'{allure_command} on the {str(self.config.torii_api_port)} peer'): + with allure.step(f'{allure_command} on the {str(self.config.torii_url)} peer'): try: with subprocess.Popen( command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, - text=True + text=True, + env=self.config.env ) as process: self.stdout, self.stderr = process.communicate() allure.attach( diff --git a/client_cli/pytests/src/client_cli/configuration.py b/client_cli/pytests/src/client_cli/configuration.py index 2f69e4edc57..89ae2be444b 100644 --- a/client_cli/pytests/src/client_cli/configuration.py +++ b/client_cli/pytests/src/client_cli/configuration.py @@ -2,7 +2,7 @@ This module provides a Config class to manage Iroha network configuration. """ -import json +import tomlkit import os import random from urllib.parse import urlparse @@ -11,8 +11,8 @@ class Config: """ Configuration class to handle Iroha network configuration. The class provides methods for loading - the configuration from a file, updating the TORII_API_URL with a random port number from the specified - range, and accessing the configuration values. + the configuration from a file, accessing the configuration values, and randomising Torii URL + to access different peers. :param port_min: The minimum port number for the TORII_API_URL. :type port_min: int @@ -24,6 +24,7 @@ def __init__(self, port_min, port_max): self.file = None self.port_min = port_min self.port_max = port_max + self._envs = dict() def load(self, path_config_client_cli): """ @@ -35,35 +36,42 @@ def load(self, path_config_client_cli): """ if not os.path.exists(path_config_client_cli): raise IOError(f"No config file found at {path_config_client_cli}") + # TODO use toml with open(path_config_client_cli, 'r', encoding='utf-8') as config_file: - self._config = json.load(config_file) + self._config = tomlkit.load(config_file) self.file = path_config_client_cli - def update_torii_api_port(self): + def randomise_torii_url(self): """ - Update the TORII_API_URL configuration value - with a random port number from the specified range. + Update Torii URL. + Note that in order for update to take effect, + `self.env` should be used when executing the client cli. :return: None """ - if self._config is None: - raise ValueError("No configuration loaded. Use load_config(path_config_client_cli) to load the configuration.") - parsed_url = urlparse(self._config['TORII_API_URL']) - new_netloc = parsed_url.hostname + ':' + str(random.randint(self.port_min, self.port_max)) - self._config['TORII_API_URL'] = parsed_url._replace(netloc=new_netloc).geturl() - with open(self.file, 'w', encoding='utf-8') as config_file: - json.dump(self._config, config_file) + parsed_url = urlparse(self._config['api']["torii_url"]) + random_port = random.randint(self.port_min, self.port_max) + self._envs["TORII_URL"] = parsed_url._replace(netloc=f"{parsed_url.hostname}:{random_port}").geturl() @property - def torii_api_port(self): + def torii_url(self): """ - Get the TORII_API_URL configuration value after updating the port number. + Get the Torii URL set in ENV vars. - :return: The updated TORII_API_URL. + :return: Torii URL :rtype: str """ - self.update_torii_api_port() - return self._config['TORII_API_URL'] + return self._envs["TORII_URL"] + + @property + def env(self): + """ + Get the environment variables set to execute the client cli with. + + :return: Dictionary with env vars (mixed with existing OS vars) + :rtype: dict + """ + return {**os.environ, **self._envs} @property def account_id(self): @@ -73,7 +81,7 @@ def account_id(self): :return: The ACCOUNT_ID. :rtype: str """ - return self._config['ACCOUNT_ID'] + return self._config['account']["id"] @property def account_name(self): @@ -103,4 +111,4 @@ def public_key(self): :return: The public key. :rtype: str """ - return self._config['PUBLIC_KEY'].split('ed0120')[1] + return self._config["account"]['public_key'].split('ed0120')[1] diff --git a/config_samples/examples/client.example.toml b/config_samples/examples/client.example.toml index ebf76b24b54..9f7e44b6f0f 100644 --- a/config_samples/examples/client.example.toml +++ b/config_samples/examples/client.example.toml @@ -7,6 +7,7 @@ private_key.digest_function = "ed25519" private_key.payload = "9ac47abf59b356e0bd7dcbbbb4dec080e302156a48ca907e47cb6aea1d32719e7233bfc89dcbd68c19fde6ce6158225298ec1131b6a130d1aeb454c1ab5183c0" [api] +# Might be set via `TORII_URL` env var torii_url = "http://127.0.0.1:8080/" basic_auth.login = "mad_hatter" basic_auth.password = "ilovetea" From 7e6276e05a16421e9a8adde29aa6b22e6485a360 Mon Sep 17 00:00:00 2001 From: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> Date: Thu, 1 Feb 2024 12:47:57 +0900 Subject: [PATCH 33/94] [refactor]: apply suggestions from code review Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- cli/src/lib.rs | 14 ++++++-------- config/base/src/lib.rs | 7 ++----- core/src/block_sync.rs | 2 +- core/src/gossiper.rs | 2 +- core/src/query/store.rs | 4 ++-- core/src/queue.rs | 26 +++++++++++++------------- core/src/snapshot.rs | 2 +- core/src/wsv.rs | 4 ++-- logger/src/lib.rs | 1 - 9 files changed, 28 insertions(+), 34 deletions(-) diff --git a/cli/src/lib.rs b/cli/src/lib.rs index 3620a0304e0..a3c5dd0f473 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -212,8 +212,7 @@ impl Iroha { ); let kura = Kura::new(&config.kura)?; - let live_query_store_handle = - LiveQueryStore::from_configuration(config.live_query_store).start(); + let live_query_store_handle = LiveQueryStore::from_config(config.live_query_store).start(); let block_count = kura.init()?; let wsv = try_read_snapshot( @@ -225,7 +224,7 @@ impl Iroha { .map_or_else( |error| { iroha_logger::warn!(%error, "Failed to load wsv from snapshot, creating empty wsv"); - WorldStateView::from_configuration( + WorldStateView::from_config( config.chain_wide, world, Arc::clone(&kura), @@ -241,7 +240,7 @@ impl Iroha { }, ); - let queue = Arc::new(Queue::from_configuration(config.queue)); + let queue = Arc::new(Queue::from_config(config.queue)); match Self::start_telemetry(&logger, &config).await? { TelemetryStartStatus::Started => iroha_logger::info!("Telemetry started"), TelemetryStartStatus::NotStarted => iroha_logger::warn!("Telemetry not started"), @@ -266,7 +265,7 @@ impl Iroha { .await .expect("Failed to join task with Sumeragi start"); - let block_sync = BlockSynchronizer::from_configuration( + let block_sync = BlockSynchronizer::from_config( &config.block_sync, sumeragi.clone(), Arc::clone(&kura), @@ -275,7 +274,7 @@ impl Iroha { ) .start(); - let gossiper = TransactionGossiper::from_configuration( + let gossiper = TransactionGossiper::from_config( config.iroha.chain_id.clone(), config.transaction_gossiper, network.clone(), @@ -300,8 +299,7 @@ impl Iroha { } .start(); - let snapshot_maker = - SnapshotMaker::from_configuration(&config.snapshot, sumeragi.clone()).start(); + let snapshot_maker = SnapshotMaker::from_config(&config.snapshot, sumeragi.clone()).start(); let kiso = KisoHandle::new(config.clone()); diff --git a/config/base/src/lib.rs b/config/base/src/lib.rs index 8eb4598a0b0..13ac707d262 100644 --- a/config/base/src/lib.rs +++ b/config/base/src/lib.rs @@ -41,7 +41,7 @@ macro_rules! impl_deserialize_from_str { impl<'de> $crate::serde::Deserialize<'de> for $ty { fn deserialize(deserializer: D) -> std::result::Result where - D: serde::Deserializer<'de>, + D: $crate::serde::Deserializer<'de>, { String::deserialize(deserializer)? .parse() @@ -156,10 +156,7 @@ impl Emitter { self.emit(MissingFieldError::new(field_name.as_ref())) } - pub fn try_unwrap_partial(&mut self, partial: P) -> Option - where - P: UnwrapPartial, - { + pub fn try_unwrap_partial(&mut self, partial: P) -> Option { partial.unwrap_partial().map_or_else( |err| { self.emit_collection(err); diff --git a/core/src/block_sync.rs b/core/src/block_sync.rs index b8dd3ce9f4a..eb3174e8ca3 100644 --- a/core/src/block_sync.rs +++ b/core/src/block_sync.rs @@ -104,7 +104,7 @@ impl BlockSynchronizer { } /// Create [`Self`] from [`Configuration`] - pub fn from_configuration( + pub fn from_config( config: &Config, sumeragi: SumeragiHandle, kura: Arc, diff --git a/core/src/gossiper.rs b/core/src/gossiper.rs index 33c3ffb2ec0..fd158498b36 100644 --- a/core/src/gossiper.rs +++ b/core/src/gossiper.rs @@ -58,7 +58,7 @@ impl TransactionGossiper { } /// Construct [`Self`] from configuration - pub fn from_configuration( + pub fn from_config( chain_id: ChainId, config: Config, network: IrohaNetwork, diff --git a/core/src/query/store.rs b/core/src/query/store.rs index 024daf0cb4f..2855ea4fe3c 100644 --- a/core/src/query/store.rs +++ b/core/src/query/store.rs @@ -74,7 +74,7 @@ pub struct LiveQueryStore { impl LiveQueryStore { /// Construct [`LiveQueryStore`] from configuration. - pub fn from_configuration(cfg: Config) -> Self { + pub fn from_config(cfg: Config) -> Self { Self { queries: IndexMap::new(), query_idle_time: cfg.query_idle_time, @@ -86,7 +86,7 @@ impl LiveQueryStore { /// /// Not marked as `#[cfg(test)]` because it is used in benches as well. pub fn test() -> Self { - Self::from_configuration(Config::default()) + Self::from_config(Config::default()) } /// Start [`LiveQueryStore`]. Requires a [`tokio::runtime::Runtime`] being run diff --git a/core/src/queue.rs b/core/src/queue.rs index 54960950dc3..585309fad2f 100644 --- a/core/src/queue.rs +++ b/core/src/queue.rs @@ -99,7 +99,7 @@ pub struct Failure { impl Queue { /// Makes queue from configuration - pub fn from_configuration(cfg: Config) -> Self { + pub fn from_config(cfg: Config) -> Self { Self { tx_hashes: ArrayQueue::new(cfg.size.get()), accepted_txs: DashMap::new(), @@ -445,7 +445,7 @@ mod tests { query_handle, )); - let queue = Queue::from_configuration(config_factory()); + let queue = Queue::from_config(config_factory()); queue .push(accepted_tx("alice@wonderland", &key_pair), &wsv) @@ -465,7 +465,7 @@ mod tests { query_handle, )); - let queue = Queue::from_configuration(Config { + let queue = Queue::from_config(Config { transaction_time_to_live: Duration::from_secs(100), size: max_transactions_in_queue, ..Config::default() @@ -513,7 +513,7 @@ mod tests { )) }; - let queue = Queue::from_configuration(config_factory()); + let queue = Queue::from_config(config_factory()); let instructions: [InstructionBox; 0] = []; let tx = TransactionBuilder::new(chain_id.clone(), "alice@wonderland".parse().expect("Valid")) @@ -576,7 +576,7 @@ mod tests { kura, query_handle, )); - let queue = Queue::from_configuration(Config { + let queue = Queue::from_config(Config { transaction_time_to_live: Duration::from_secs(100), ..config_factory() }); @@ -603,7 +603,7 @@ mod tests { ); let tx = accepted_tx("alice@wonderland", &alice_key); wsv.transactions.insert(tx.as_ref().hash(), 1); - let queue = Queue::from_configuration(config_factory()); + let queue = Queue::from_config(config_factory()); assert!(matches!( queue.push(tx, &wsv), Err(Failure { @@ -626,7 +626,7 @@ mod tests { query_handle, ); let tx = accepted_tx("alice@wonderland", &alice_key); - let queue = Queue::from_configuration(config_factory()); + let queue = Queue::from_config(config_factory()); queue.push(tx.clone(), &wsv).unwrap(); wsv.transactions.insert(tx.as_ref().hash(), 1); assert_eq!( @@ -649,7 +649,7 @@ mod tests { kura, query_handle, )); - let queue = Queue::from_configuration(Config { + let queue = Queue::from_config(Config { transaction_time_to_live: Duration::from_millis(200), ..config_factory() }); @@ -696,7 +696,7 @@ mod tests { kura, query_handle, )); - let queue = Queue::from_configuration(config_factory()); + let queue = Queue::from_config(config_factory()); queue .push(accepted_tx("alice@wonderland", &alice_key), &wsv) .expect("Failed to push tx into queue"); @@ -730,7 +730,7 @@ mod tests { kura, query_handle, )); - let queue = Queue::from_configuration(config_factory()); + let queue = Queue::from_config(config_factory()); let instructions = [Fail { message: "expired".to_owned(), }]; @@ -771,7 +771,7 @@ mod tests { query_handle, ); - let queue = Arc::new(Queue::from_configuration(Config { + let queue = Arc::new(Queue::from_config(Config { transaction_time_to_live: Duration::from_secs(100), size: 100_000_000.try_into().unwrap(), ..Config::default() @@ -844,7 +844,7 @@ mod tests { query_handle, )); - let queue = Queue::from_configuration(Config { + let queue = Queue::from_config(Config { future_threshold, ..Config::default() }); @@ -906,7 +906,7 @@ mod tests { let query_handle = LiveQueryStore::test().start(); let mut wsv = WorldStateView::new(world, kura, query_handle); - let queue = Queue::from_configuration(Config { + let queue = Queue::from_config(Config { transaction_time_to_live: Duration::from_secs(100), size: 100.try_into().unwrap(), size_per_user: 1.try_into().unwrap(), diff --git a/core/src/snapshot.rs b/core/src/snapshot.rs index e4154d189da..2256199c158 100644 --- a/core/src/snapshot.rs +++ b/core/src/snapshot.rs @@ -137,7 +137,7 @@ impl SnapshotMaker { } /// Create [`Self`] from [`Configuration`] - pub fn from_configuration(config: &Config, sumeragi: SumeragiHandle) -> Self { + pub fn from_config(config: &Config, sumeragi: SumeragiHandle) -> Self { Self { sumeragi, snapshot_create_every: config.create_every, diff --git a/core/src/wsv.rs b/core/src/wsv.rs index 29539fa658b..36e1b9a31e7 100644 --- a/core/src/wsv.rs +++ b/core/src/wsv.rs @@ -397,7 +397,7 @@ impl WorldStateView { #[inline] pub fn new(world: World, kura: Arc, query_handle: LiveQueryStoreHandle) -> Self { // Added to remain backward compatible with other code primary in tests - Self::from_configuration(Config::default(), world, kura, query_handle) + Self::from_config(Config::default(), world, kura, query_handle) } /// Get `Account`'s `Asset`s @@ -915,7 +915,7 @@ impl WorldStateView { /// Construct [`WorldStateView`] with specific [`Configuration`]. #[inline] - pub fn from_configuration( + pub fn from_config( config: Config, world: World, kura: Arc, diff --git a/logger/src/lib.rs b/logger/src/lib.rs index 0afa4e84c16..87a5ac3ed60 100644 --- a/logger/src/lib.rs +++ b/logger/src/lib.rs @@ -77,7 +77,6 @@ pub fn test_logger() -> LoggerHandle { LOGGER .get_or_init(|| { - // let mut config = // NOTE: if this config should be changed for some specific tests, consider // isolating those tests into a separate process and controlling default logger config // with ENV vars rather than by extending `test_logger` signature. This will both remain From 1f7b5dc34150d713fb455dbaba62b0471a2256db Mon Sep 17 00:00:00 2001 From: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> Date: Thu, 1 Feb 2024 12:51:55 +0900 Subject: [PATCH 34/94] [refactor]: curl up `SumeragiStartArgs` Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- cli/src/lib.rs | 5 ++--- core/src/sumeragi/mod.rs | 20 +++++++++++--------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/cli/src/lib.rs b/cli/src/lib.rs index a3c5dd0f473..9f77cc5f0ea 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -249,9 +249,8 @@ impl Iroha { let kura_thread_handler = Kura::start(Arc::clone(&kura)); let start_args = SumeragiStartArgs { - chain_id: config.iroha.chain_id.clone(), - sumeragi_config: Box::new(config.sumeragi.clone()), - iroha_config: Box::new(config.iroha.clone()), + sumeragi_config: config.sumeragi.clone(), + iroha_config: config.iroha.clone(), events_sender: events_sender.clone(), wsv, queue: Arc::clone(&queue), diff --git a/core/src/sumeragi/mod.rs b/core/src/sumeragi/mod.rs index 321b36a7fc4..63a518d2b20 100644 --- a/core/src/sumeragi/mod.rs +++ b/core/src/sumeragi/mod.rs @@ -259,7 +259,6 @@ impl SumeragiHandle { SumeragiStartArgs { sumeragi_config, iroha_config, - chain_id, events_sender, mut wsv, queue, @@ -297,14 +296,16 @@ impl SumeragiHandle { let block_iter_except_last = (&mut blocks_iter).take(block_count.saturating_sub(skip_block_count + 1)); for block in block_iter_except_last { - current_topology = Self::replay_block(&chain_id, &block, &mut wsv, current_topology); + current_topology = + Self::replay_block(&iroha_config.chain_id, &block, &mut wsv, current_topology); } // finalized_wsv is one block behind let finalized_wsv = wsv.clone(); if let Some(block) = blocks_iter.next() { - current_topology = Self::replay_block(&chain_id, &block, &mut wsv, current_topology); + current_topology = + Self::replay_block(&iroha_config.chain_id, &block, &mut wsv, current_topology); } info!("Sumeragi has finished loading blocks and setting up the WSV"); @@ -318,11 +319,13 @@ impl SumeragiHandle { #[cfg(not(debug_assertions))] let debug_force_soft_fork = false; + let peer_id = iroha_config.peer_id(); + let sumeragi = main_loop::Sumeragi { - chain_id, - key_pair: iroha_config.key_pair.clone(), + chain_id: iroha_config.chain_id, + key_pair: iroha_config.key_pair, queue: Arc::clone(&queue), - peer_id: iroha_config.peer_id(), + peer_id, events_sender, public_wsv_sender, public_finalized_wsv_sender, @@ -420,9 +423,8 @@ impl VotingBlock { /// Arguments for [`SumeragiHandle::start`] function #[allow(missing_docs)] pub struct SumeragiStartArgs { - pub chain_id: ChainId, - pub sumeragi_config: Box, - pub iroha_config: Box, + pub iroha_config: IrohaConfig, + pub sumeragi_config: SumeragiConfig, pub events_sender: EventsSender, pub wsv: WorldStateView, pub queue: Arc, From bfe51ac04420234ca09221baee6cdb2d3bf0c93e Mon Sep 17 00:00:00 2001 From: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> Date: Thu, 1 Feb 2024 12:57:13 +0900 Subject: [PATCH 35/94] [refactor]: refine telemetry - simplify `regular_telemetry` to just `telemetry` - clearer config passing Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- cli/src/lib.rs | 4 ++-- config/src/parameters/actual.rs | 4 ++-- config/src/parameters/user_layer.rs | 14 +++----------- telemetry/src/dev.rs | 10 +++------- telemetry/src/lib.rs | 2 +- telemetry/src/ws.rs | 6 +++--- 6 files changed, 14 insertions(+), 26 deletions(-) diff --git a/cli/src/lib.rs b/cli/src/lib.rs index 9f77cc5f0ea..3bd072d9a44 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -378,13 +378,13 @@ impl Iroha { .subscribe_on_telemetry(iroha_logger::telemetry::Channel::Future) .await .wrap_err("Failed to subscribe on telemetry")?; - let _handle = iroha_telemetry::dev::start(config.file.clone(), receiver) + let _handle = iroha_telemetry::dev::start(config.clone(), receiver) .await .wrap_err("Failed to setup telemetry for futures")?; } } - if let Some(config) = &config.regular_telemetry { + if let Some(config) = &config.telemetry { let receiver = logger .subscribe_on_telemetry(iroha_logger::telemetry::Channel::Regular) .await diff --git a/config/src/parameters/actual.rs b/config/src/parameters/actual.rs index 6676382cf5d..1e8457a6010 100644 --- a/config/src/parameters/actual.rs +++ b/config/src/parameters/actual.rs @@ -34,7 +34,7 @@ pub struct Root { pub logger: Logger, pub queue: Queue, pub snapshot: Snapshot, - pub regular_telemetry: Option, + pub telemetry: Option, pub dev_telemetry: Option, pub chain_wide: ChainWide, } @@ -212,7 +212,7 @@ pub struct Torii { /// Complete configuration needed to start regular telemetry. #[derive(Debug, Clone)] -pub struct RegularTelemetry { +pub struct Telemetry { #[allow(missing_docs)] pub name: String, #[allow(missing_docs)] diff --git a/config/src/parameters/user_layer.rs b/config/src/parameters/user_layer.rs index b7ec1d3a42e..1ebd9f01ebd 100644 --- a/config/src/parameters/user_layer.rs +++ b/config/src/parameters/user_layer.rs @@ -199,7 +199,7 @@ impl Root { logger, queue, snapshot, - regular_telemetry, + telemetry: regular_telemetry, dev_telemetry, chain_wide, }) @@ -510,15 +510,7 @@ pub struct TelemetryDev { } impl Telemetry { - fn parse( - self, - ) -> Result< - ( - Option, - Option, - ), - Report, - > { + fn parse(self) -> Result<(Option, Option), Report> { let Self { name, url, @@ -528,7 +520,7 @@ impl Telemetry { } = self; let regular = match (name, url) { - (Some(name), Some(url)) => Some(actual::RegularTelemetry { + (Some(name), Some(url)) => Some(actual::Telemetry { name, url, max_retry_delay_exponent: max_retry_delay_exponent diff --git a/telemetry/src/dev.rs b/telemetry/src/dev.rs index f5e4886449a..9afab4b16c1 100644 --- a/telemetry/src/dev.rs +++ b/telemetry/src/dev.rs @@ -1,8 +1,7 @@ //! Module with development telemetry -use std::path::PathBuf; - use eyre::{Result, WrapErr}; +use iroha_config::parameters::actual::DevTelemetry as Config; use iroha_logger::telemetry::Event as Telemetry; use tokio::{ fs::OpenOptions, @@ -15,10 +14,7 @@ use tokio_stream::{wrappers::BroadcastStream, StreamExt}; /// Starts telemetry writing to a file /// # Errors /// Fails if unable to open the file -pub async fn start( - telemetry_file: PathBuf, - telemetry: Receiver, -) -> Result> { +pub async fn start(config: Config, telemetry: Receiver) -> Result> { let mut stream = crate::futures::get_stream(BroadcastStream::new(telemetry).fuse()); let mut file = OpenOptions::new() @@ -29,7 +25,7 @@ pub async fn start( //.append(true) .create(true) .truncate(true) - .open(telemetry_file) + .open(config.file) .await .wrap_err("Failed to create and open file for telemetry")?; diff --git a/telemetry/src/lib.rs b/telemetry/src/lib.rs index c3c208a877a..c1d179ebd8a 100644 --- a/telemetry/src/lib.rs +++ b/telemetry/src/lib.rs @@ -8,7 +8,7 @@ mod retry_period; pub mod ws; pub use iroha_config::parameters::actual::{ - DevTelemetry as DevTelemetryConfig, RegularTelemetry as RegularTelemetryConfig, + DevTelemetry as DevTelemetryConfig, Telemetry as RegularTelemetryConfig, }; pub use iroha_telemetry_derive::metrics; diff --git a/telemetry/src/ws.rs b/telemetry/src/ws.rs index 4a1da03a862..e09d038854b 100644 --- a/telemetry/src/ws.rs +++ b/telemetry/src/ws.rs @@ -3,7 +3,7 @@ use chrono::Local; use eyre::{eyre, Result}; use futures::{stream::SplitSink, Sink, SinkExt, StreamExt}; -use iroha_config::parameters::actual::RegularTelemetry; +use iroha_config::parameters::actual::Telemetry as Config; use iroha_logger::telemetry::Event as Telemetry; use serde_json::Map; use tokio::{ @@ -28,12 +28,12 @@ const INTERNAL_CHANNEL_CAPACITY: usize = 10; /// # Errors /// Fails if unable to connect to the server pub async fn start( - RegularTelemetry { + Config { name, url, max_retry_delay_exponent, min_retry_period, - }: RegularTelemetry, + }: Config, telemetry: broadcast::Receiver, ) -> Result> { iroha_logger::info!(%url, "Starting telemetry"); From 33d36faae286895c2228b9f6144d7415cfd3763e Mon Sep 17 00:00:00 2001 From: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> Date: Thu, 1 Feb 2024 12:59:11 +0900 Subject: [PATCH 36/94] [refactor]: use `Infallible` Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- config/base/src/lib.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/config/base/src/lib.rs b/config/base/src/lib.rs index 13ac707d262..cb9c0324c58 100644 --- a/config/base/src/lib.rs +++ b/config/base/src/lib.rs @@ -7,6 +7,7 @@ use std::{ borrow::Cow, cell::RefCell, collections::{HashMap, HashSet}, + convert::Infallible, env::VarError, error::Error, ffi::OsString, @@ -245,12 +246,8 @@ impl TestEnv { } } -#[derive(thiserror::Error, Debug, Copy, Clone)] -#[error("should never occur")] -pub struct NeverError; - -impl ReadEnv for TestEnv { - fn get(&self, key: impl AsRef) -> Result>, NeverError> { +impl ReadEnv for TestEnv { + fn get(&self, key: impl AsRef) -> Result>, Infallible> { self.visited.borrow_mut().insert(key.as_ref().to_string()); Ok(self .map From 4b5f09e9bc774ea6ba4eac9cd964e6cf9db9ec2c Mon Sep 17 00:00:00 2001 From: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> Date: Mon, 5 Feb 2024 07:38:59 +0900 Subject: [PATCH 37/94] [refactor]: simplify client config - `nonce` instead of `add_nonce` - move `[api]` to root Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- client/src/config.rs | 2 +- client/src/config/user_layer.rs | 18 ++---- client/src/config/user_layer/boilerplate.rs | 71 ++++----------------- client/src/lib.rs | 4 +- config_samples/examples/client.example.toml | 12 ++-- config_samples/swarm/client.toml | 8 +-- 6 files changed, 32 insertions(+), 83 deletions(-) diff --git a/client/src/config.rs b/client/src/config.rs index 1f0c5e3b65b..4e3695aca06 100644 --- a/client/src/config.rs +++ b/client/src/config.rs @@ -23,7 +23,7 @@ pub mod user_layer; #[allow(unsafe_code)] pub const DEFAULT_TRANSACTION_TIME_TO_LIVE: Duration = Duration::from_secs(100); pub const DEFAULT_TRANSACTION_STATUS_TIMEOUT: Duration = Duration::from_secs(15); -pub const DEFAULT_ADD_TRANSACTION_NONCE: bool = false; +pub const DEFAULT_TRANSACTION_NONCE: bool = false; /// Wrapper over `SmallStr` to provide basic auth login checking #[derive(Debug, Display, Clone, Serialize, PartialEq, Eq)] diff --git a/client/src/config/user_layer.rs b/client/src/config/user_layer.rs index 55e0d17d4a0..7a984eb764c 100644 --- a/client/src/config/user_layer.rs +++ b/client/src/config/user_layer.rs @@ -15,8 +15,9 @@ use crate::config::BasicAuth; #[derive(Clone, Debug)] pub struct Root { pub chain_id: ChainId, + pub torii_url: OnlyHttpUrl, + pub basic_auth: Option, pub account: Account, - pub api: Api, pub transaction: Transaction, } @@ -24,6 +25,8 @@ impl Root { pub fn parse(self) -> Result> { let Self { chain_id, + torii_url, + basic_auth, account: Account { id: account_id, @@ -34,12 +37,8 @@ impl Root { Transaction { time_to_live: tx_ttl, status_timeout: tx_timeout, - add_nonce: tx_add_nonce, + nonce: tx_add_nonce, }, - api: Api { - torii_url, - basic_auth, - }, } = self; let mut emitter = Emitter::new(); @@ -82,10 +81,7 @@ impl Root { } #[derive(Debug, Clone)] -pub struct Api { - pub torii_url: OnlyHttpUrl, - pub basic_auth: Option, -} +pub struct Api {} #[derive(Debug, Clone)] pub struct Account { @@ -98,7 +94,7 @@ pub struct Account { pub struct Transaction { pub time_to_live: Duration, pub status_timeout: Duration, - pub add_nonce: bool, + pub nonce: bool, } #[derive(Debug, Clone, Eq, PartialEq)] diff --git a/client/src/config/user_layer/boilerplate.rs b/client/src/config/user_layer/boilerplate.rs index 1ecb9f331cb..826e5e6e9d0 100644 --- a/client/src/config/user_layer/boilerplate.rs +++ b/client/src/config/user_layer/boilerplate.rs @@ -14,7 +14,7 @@ use serde::Deserialize; use crate::config::{ base::{FromEnvResult, ReadEnv}, user_layer::{Account, Api, OnlyHttpUrl, Root, Transaction}, - BasicAuth, DEFAULT_ADD_TRANSACTION_NONCE, DEFAULT_TRANSACTION_STATUS_TIMEOUT, + BasicAuth, DEFAULT_TRANSACTION_NONCE, DEFAULT_TRANSACTION_STATUS_TIMEOUT, DEFAULT_TRANSACTION_TIME_TO_LIVE, }; @@ -22,8 +22,9 @@ use crate::config::{ #[serde(deny_unknown_fields, default)] pub struct RootPartial { pub chain_id: UserField, + pub torii_url: UserField, + pub basic_auth: UserField, pub account: AccountPartial, - pub api: ApiPartial, pub transaction: TransactionPartial, } @@ -61,19 +62,15 @@ impl FromEnv for RootPartial { { let mut emitter = Emitter::new(); - let api = ApiPartial::from_env(env).map_or_else( - |err| { - emitter.emit_collection(err); - None - }, - Some, - ); + let torii_url = + ParseEnvResult::parse_simple(&mut emitter, env, "TORII_URL", "torii_url").into(); emitter.finish()?; Ok(Self { chain_id: None.into(), - api: api.unwrap(), + torii_url, + basic_auth: None.into(), account: AccountPartial::default(), transaction: TransactionPartial::default(), }) @@ -89,61 +86,24 @@ impl UnwrapPartial for RootPartial { if self.chain_id.is_none() { emitter.emit_missing_field("chain_id"); } + if self.torii_url.is_none() { + emitter.emit_missing_field("torii_url"); + } let account = emitter.try_unwrap_partial(self.account); - let api = emitter.try_unwrap_partial(self.api); let transaction = emitter.try_unwrap_partial(self.transaction); emitter.finish()?; Ok(Root { chain_id: self.chain_id.get().unwrap(), + torii_url: self.torii_url.get().unwrap(), + basic_auth: self.basic_auth.get(), account: account.unwrap(), - api: api.unwrap(), transaction: transaction.unwrap(), }) } } -#[derive(Debug, Clone, Deserialize, Eq, PartialEq, Default, Merge)] -#[serde(deny_unknown_fields, default)] -pub struct ApiPartial { - pub torii_url: UserField, - pub basic_auth: UserField, -} - -impl UnwrapPartial for ApiPartial { - type Output = Api; - - fn unwrap_partial(self) -> UnwrapPartialResult { - Ok(Api { - torii_url: self - .torii_url - .get() - .ok_or_else(|| MissingFieldError::new("api.torii_url"))?, - basic_auth: self.basic_auth.get(), - }) - } -} - -impl FromEnv for ApiPartial { - fn from_env>(env: &R) -> FromEnvResult - where - Self: Sized, - { - let mut emitter = Emitter::new(); - - let torii_url = - ParseEnvResult::parse_simple(&mut emitter, env, "TORII_URL", "api.torii_url").into(); - - emitter.finish()?; - - Ok(Self { - torii_url, - basic_auth: None.into(), - }) - } -} - #[derive(Debug, Clone, Deserialize, Eq, PartialEq, Default, Merge)] #[serde(deny_unknown_fields, default)] pub struct AccountPartial { @@ -183,7 +143,7 @@ impl UnwrapPartial for AccountPartial { pub struct TransactionPartial { pub time_to_live: UserField, pub status_timeout: UserField, - pub add_nonce: UserField, + pub nonce: UserField, } impl UnwrapPartial for TransactionPartial { @@ -199,10 +159,7 @@ impl UnwrapPartial for TransactionPartial { .status_timeout .get() .map_or(DEFAULT_TRANSACTION_STATUS_TIMEOUT, UserDuration::get), - add_nonce: self - .add_nonce - .get() - .unwrap_or(DEFAULT_ADD_TRANSACTION_NONCE), + nonce: self.nonce.get().unwrap_or(DEFAULT_TRANSACTION_NONCE), }) } } diff --git a/client/src/lib.rs b/client/src/lib.rs index 548d2a3beac..3ccb8fdb45c 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -14,7 +14,7 @@ pub mod samples { use crate::{ config::{ - Config, DEFAULT_ADD_TRANSACTION_NONCE, DEFAULT_TRANSACTION_STATUS_TIMEOUT, + Config, DEFAULT_TRANSACTION_NONCE, DEFAULT_TRANSACTION_STATUS_TIMEOUT, DEFAULT_TRANSACTION_TIME_TO_LIVE, }, crypto::KeyPair, @@ -33,7 +33,7 @@ pub mod samples { basic_auth: None, transaction_ttl: DEFAULT_TRANSACTION_TIME_TO_LIVE, transaction_status_timeout: DEFAULT_TRANSACTION_STATUS_TIMEOUT, - transaction_add_nonce: DEFAULT_ADD_TRANSACTION_NONCE, + transaction_add_nonce: DEFAULT_TRANSACTION_NONCE, } } } diff --git a/config_samples/examples/client.example.toml b/config_samples/examples/client.example.toml index 9f7e44b6f0f..7b1939efe77 100644 --- a/config_samples/examples/client.example.toml +++ b/config_samples/examples/client.example.toml @@ -1,4 +1,8 @@ chain_id = "00000000-0000-0000-0000-000000000000" +# Might be set via `TORII_URL` env var +torii_url = "http://127.0.0.1:8080/" +basic_auth.login = "mad_hatter" +basic_auth.password = "ilovetea" [account] id = "alice@wonderland" @@ -6,14 +10,8 @@ public_key = "ed01207233BFC89DCBD68C19FDE6CE6158225298EC1131B6A130D1AEB454C1AB51 private_key.digest_function = "ed25519" private_key.payload = "9ac47abf59b356e0bd7dcbbbb4dec080e302156a48ca907e47cb6aea1d32719e7233bfc89dcbd68c19fde6ce6158225298ec1131b6a130d1aeb454c1ab5183c0" -[api] -# Might be set via `TORII_URL` env var -torii_url = "http://127.0.0.1:8080/" -basic_auth.login = "mad_hatter" -basic_auth.password = "ilovetea" - [transaction] time_to_live = 100_000 status_timeout = 100_000 -add_nonce = false +nonce = false diff --git a/config_samples/swarm/client.toml b/config_samples/swarm/client.toml index 77aa53b5107..d0a4836ccb9 100644 --- a/config_samples/swarm/client.toml +++ b/config_samples/swarm/client.toml @@ -1,12 +1,10 @@ chain_id = "00000000-0000-0000-0000-000000000000" +torii_url = "http://127.0.0.1:8080/" +basic_auth.web_login = "mad_hatter" +basic_auth.password = "ilovetea" [account] id = "alice@wonderland" public_key = "ed01207233BFC89DCBD68C19FDE6CE6158225298EC1131B6A130D1AEB454C1AB5183C0" private_key.digest_function = "ed25519" private_key.payload = "9ac47abf59b356e0bd7dcbbbb4dec080e302156a48ca907e47cb6aea1d32719e7233bfc89dcbd68c19fde6ce6158225298ec1131b6a130d1aeb454c1ab5183c0" - -[api] -torii_url = "http://127.0.0.1:8080/" -basic_auth.web_login = "mad_hatter" -basic_auth.password = "ilovetea" From 7456a821b3de48584d5e97f02b92f0248847e60d Mon Sep 17 00:00:00 2001 From: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> Date: Mon, 5 Feb 2024 07:40:25 +0900 Subject: [PATCH 38/94] [refactor]: update `--config` arg - remove default value - remove `IROHA_CONFIG` env Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- cli/src/main.rs | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/cli/src/main.rs b/cli/src/main.rs index 8524155c21c..7f527419364 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -4,8 +4,6 @@ use std::{env, path::PathBuf}; use clap::Parser; use color_eyre::eyre::Result; -const DEFAULT_CONFIG_PATH: &str = "config.toml"; - fn is_colouring_supported() -> bool { supports_color::on(supports_color::Stream::Stdout).is_some() } @@ -19,14 +17,7 @@ fn default_terminal_colors_str() -> clap::builder::OsStr { #[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 - #[arg( - long, - short, - env("IROHA_CONFIG"), - value_name("PATH"), - value_hint(clap::ValueHint::FilePath), - default_value_t = DEFAULT_CONFIG_PATH.to_owned() - )] + #[arg(long, short, value_name("PATH"), value_hint(clap::ValueHint::FilePath))] config: String, /// Whether to enable ANSI colored output or not /// From f2e150880c12c6834924eed872546f3078b97724 Mon Sep 17 00:00:00 2001 From: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> Date: Mon, 5 Feb 2024 07:43:51 +0900 Subject: [PATCH 39/94] [docs]: update `read_config_and_genesis` docs Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- cli/src/lib.rs | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/cli/src/lib.rs b/cli/src/lib.rs index 3bd072d9a44..728d8985349 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -488,22 +488,12 @@ fn genesis_domain(public_key: PublicKey) -> Domain { domain } -// FIXME: update docs -/// 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. +/// Read configuration and then a genesis block if specified. /// /// # Errors -/// - If provided user configuration is invalid or incomplete -/// - If genesis config is invalid +/// - If failed to read the config +/// - If failed to load the genesis block +/// - If failed to build a genesis network pub fn read_config_and_genesis( path: impl AsRef, submit_genesis: bool, From 20bc0e8c925c3f6962d79aee736ac5a43e629162 Mon Sep 17 00:00:00 2001 From: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> Date: Mon, 5 Feb 2024 07:44:53 +0900 Subject: [PATCH 40/94] [refactor]: chore Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- client/src/config.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/client/src/config.rs b/client/src/config.rs index 4e3695aca06..00f72a23765 100644 --- a/client/src/config.rs +++ b/client/src/config.rs @@ -90,8 +90,6 @@ impl Config { /// - unable to load config from a TOML file /// - unable to validate loaded config pub fn load(path: impl AsRef) -> std::result::Result { - let config = RootPartial::from_toml(path)?; - let config = config.unwrap_partial()?.parse()?; - Ok(config) + Ok(RootPartial::from_toml(path)?.unwrap_partial()?.parse()?) } } From f6698715e7536037c28cc0f84d1bd50bfa948528 Mon Sep 17 00:00:00 2001 From: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> Date: Mon, 5 Feb 2024 07:48:30 +0900 Subject: [PATCH 41/94] [refactor]: just `idle_time`, without `query_` Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- config/src/parameters/actual.rs | 4 ++-- config/src/parameters/user_layer.rs | 2 +- config/tests/fixtures.rs | 4 ++-- core/src/query/store.rs | 8 ++++---- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/config/src/parameters/actual.rs b/config/src/parameters/actual.rs index 1e8457a6010..9d423d5d343 100644 --- a/config/src/parameters/actual.rs +++ b/config/src/parameters/actual.rs @@ -129,13 +129,13 @@ pub struct Sumeragi { #[derive(Debug, Clone, Copy)] pub struct LiveQueryStore { - pub query_idle_time: Duration, + pub idle_time: Duration, } impl Default for LiveQueryStore { fn default() -> Self { Self { - query_idle_time: defaults::torii::DEFAULT_QUERY_IDLE_TIME, + idle_time: defaults::torii::DEFAULT_QUERY_IDLE_TIME, } } } diff --git a/config/src/parameters/user_layer.rs b/config/src/parameters/user_layer.rs index 1ebd9f01ebd..20c36695243 100644 --- a/config/src/parameters/user_layer.rs +++ b/config/src/parameters/user_layer.rs @@ -614,7 +614,7 @@ impl Torii { }; let query = actual::LiveQueryStore { - query_idle_time: self.query_idle_time, + idle_time: self.query_idle_time, }; (torii, query) diff --git a/config/tests/fixtures.rs b/config/tests/fixtures.rs index 84097aa9ba6..ea09432283e 100644 --- a/config/tests/fixtures.rs +++ b/config/tests/fixtures.rs @@ -91,7 +91,7 @@ fn minimal_config_snapshot() -> Result<()> { batch_size: 500, }, live_query_store: LiveQueryStore { - query_idle_time: 30s, + idle_time: 30s, }, logger: Logger { level: INFO, @@ -109,7 +109,7 @@ fn minimal_config_snapshot() -> Result<()> { store_path: "./storage", creation_enabled: true, }, - regular_telemetry: None, + telemetry: None, dev_telemetry: None, chain_wide: ChainWide { max_transactions_in_block: 512, diff --git a/core/src/query/store.rs b/core/src/query/store.rs index 2855ea4fe3c..6691ee24a48 100644 --- a/core/src/query/store.rs +++ b/core/src/query/store.rs @@ -69,7 +69,7 @@ type LiveQuery = Batched>; #[derive(Debug)] pub struct LiveQueryStore { queries: IndexMap, - query_idle_time: Duration, + idle_time: Duration, } impl LiveQueryStore { @@ -77,7 +77,7 @@ impl LiveQueryStore { pub fn from_config(cfg: Config) -> Self { Self { queries: IndexMap::new(), - query_idle_time: cfg.query_idle_time, + idle_time: cfg.idle_time, } } @@ -99,14 +99,14 @@ impl LiveQueryStore { let (message_sender, mut message_receiver) = mpsc::channel(1); - let mut idle_interval = tokio::time::interval(self.query_idle_time); + let mut idle_interval = tokio::time::interval(self.idle_time); tokio::task::spawn(async move { loop { tokio::select! { _ = idle_interval.tick() => { self.queries - .retain(|_, (_, last_access_time)| last_access_time.elapsed() <= self.query_idle_time); + .retain(|_, (_, last_access_time)| last_access_time.elapsed() <= self.idle_time); }, msg = message_receiver.recv() => { let Some(msg) = msg else { From 0a53afee9479407d812d416246bef4157e47d1a0 Mon Sep 17 00:00:00 2001 From: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> Date: Mon, 5 Feb 2024 07:49:42 +0900 Subject: [PATCH 42/94] [revert]: use `ident_length_limits` in _actual_ config Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- config/src/parameters/actual.rs | 4 ++-- config/src/parameters/user_layer.rs | 2 +- config/tests/fixtures.rs | 2 +- core/src/smartcontracts/isi/domain.rs | 4 ++-- core/src/smartcontracts/isi/world.rs | 2 +- core/src/wsv.rs | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/config/src/parameters/actual.rs b/config/src/parameters/actual.rs index 9d423d5d343..79710fc8890 100644 --- a/config/src/parameters/actual.rs +++ b/config/src/parameters/actual.rs @@ -162,7 +162,7 @@ pub struct ChainWide { pub asset_definition_metadata_limits: MetadataLimits, pub account_metadata_limits: MetadataLimits, pub domain_metadata_limits: MetadataLimits, - pub identifier_length_limits: LengthLimits, + pub ident_length_limits: LengthLimits, pub wasm_runtime: WasmRuntime, } @@ -183,7 +183,7 @@ impl Default for ChainWide { account_metadata_limits: defaults::chain_wide::DEFAULT_METADATA_LIMITS, asset_definition_metadata_limits: defaults::chain_wide::DEFAULT_METADATA_LIMITS, asset_metadata_limits: defaults::chain_wide::DEFAULT_METADATA_LIMITS, - identifier_length_limits: defaults::chain_wide::DEFAULT_IDENT_LENGTH_LIMITS, + ident_length_limits: defaults::chain_wide::DEFAULT_IDENT_LENGTH_LIMITS, wasm_runtime: WasmRuntime::default(), } } diff --git a/config/src/parameters/user_layer.rs b/config/src/parameters/user_layer.rs index 20c36695243..a886ea04c4a 100644 --- a/config/src/parameters/user_layer.rs +++ b/config/src/parameters/user_layer.rs @@ -590,7 +590,7 @@ impl ChainWide { asset_definition_metadata_limits, account_metadata_limits, domain_metadata_limits, - identifier_length_limits, + ident_length_limits: identifier_length_limits, wasm_runtime: actual::WasmRuntime { fuel_limit: wasm_fuel_limit, max_memory: wasm_max_memory, diff --git a/config/tests/fixtures.rs b/config/tests/fixtures.rs index ea09432283e..5894835e2a5 100644 --- a/config/tests/fixtures.rs +++ b/config/tests/fixtures.rs @@ -135,7 +135,7 @@ fn minimal_config_snapshot() -> Result<()> { max_len: 1048576, max_entry_byte_size: 4096, }, - identifier_length_limits: LengthLimits { + ident_length_limits: LengthLimits { min: 1, max: 128, }, diff --git a/core/src/smartcontracts/isi/domain.rs b/core/src/smartcontracts/isi/domain.rs index 9bf0e4d5f22..de0374be2ad 100644 --- a/core/src/smartcontracts/isi/domain.rs +++ b/core/src/smartcontracts/isi/domain.rs @@ -48,7 +48,7 @@ pub mod isi { account_id .name - .validate_len(wsv.config.identifier_length_limits) + .validate_len(wsv.config.ident_length_limits) .map_err(Error::from)?; let domain = wsv.domain_mut(&account_id.domain_id)?; @@ -92,7 +92,7 @@ pub mod isi { asset_definition .id() .name - .validate_len(wsv.config.identifier_length_limits) + .validate_len(wsv.config.ident_length_limits) .map_err(Error::from)?; let asset_definition_id = asset_definition.id().clone(); diff --git a/core/src/smartcontracts/isi/world.rs b/core/src/smartcontracts/isi/world.rs index 8b72e42f439..44ae2f2eb2e 100644 --- a/core/src/smartcontracts/isi/world.rs +++ b/core/src/smartcontracts/isi/world.rs @@ -72,7 +72,7 @@ pub mod isi { domain_id .name - .validate_len(wsv.config.identifier_length_limits) + .validate_len(wsv.config.ident_length_limits) .map_err(Error::from)?; let world = wsv.world_mut(); diff --git a/core/src/wsv.rs b/core/src/wsv.rs index 36e1b9a31e7..dbfc968b8a6 100644 --- a/core/src/wsv.rs +++ b/core/src/wsv.rs @@ -688,7 +688,7 @@ impl WorldStateView { WSV_ASSET_DEFINITION_METADATA_LIMITS => self.config.asset_definition_metadata_limits, WSV_ACCOUNT_METADATA_LIMITS => self.config.account_metadata_limits, WSV_DOMAIN_METADATA_LIMITS => self.config.domain_metadata_limits, - WSV_IDENT_LENGTH_LIMITS => self.config.identifier_length_limits, + WSV_IDENT_LENGTH_LIMITS => self.config.ident_length_limits, WASM_FUEL_LIMIT => self.config.wasm_runtime.fuel_limit, WASM_MAX_MEMORY => self.config.wasm_runtime.max_memory.0, TRANSACTION_LIMITS => self.config.transaction_limits, From de8fff5a2038b29b9c8c2e1454a12cbc64748ae8 Mon Sep 17 00:00:00 2001 From: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> Date: Mon, 5 Feb 2024 07:51:13 +0900 Subject: [PATCH 43/94] [refactor]: use `Config`, not vague `Root` Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- core/src/kiso.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/src/kiso.rs b/core/src/kiso.rs index 486e97c7949..d3dc2e76483 100644 --- a/core/src/kiso.rs +++ b/core/src/kiso.rs @@ -10,7 +10,7 @@ use eyre::Result; use iroha_config::{ client_api::{ConfigurationDTO, Logger as LoggerDTO}, - parameters::actual::Root, + parameters::actual::Root as Config, }; use iroha_logger::Level; use tokio::sync::{mpsc, oneshot, watch}; @@ -27,7 +27,7 @@ pub struct KisoHandle { impl KisoHandle { /// Spawn a new actor - pub fn new(state: Root) -> Self { + pub fn new(state: Config) -> Self { let (actor_sender, actor_receiver) = mpsc::channel(DEFAULT_CHANNEL_SIZE); let (log_level_update, _) = watch::channel(state.logger.level); let mut actor = Actor { @@ -106,7 +106,7 @@ pub enum Error { struct Actor { handle: mpsc::Receiver, - state: Root, + state: Config, // Current implementation is somewhat not scalable in terms of code writing: for any // future dynamic parameter, it will require its own `subscribe_on_` function in [`KisoHandle`], // new channel here, and new [`Message`] variant. If boilerplate expands, a more general solution will be From f255308d4f03e020227326bf314fe5037eb4b918 Mon Sep 17 00:00:00 2001 From: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> Date: Mon, 5 Feb 2024 07:52:08 +0900 Subject: [PATCH 44/94] [chore]: remove comment Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- core/src/queue.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/core/src/queue.rs b/core/src/queue.rs index 585309fad2f..4ce8464556a 100644 --- a/core/src/queue.rs +++ b/core/src/queue.rs @@ -383,7 +383,6 @@ impl Queue { mod tests { use std::{ops::Mul, str::FromStr, sync::Arc, thread, time::Duration}; - // use iroha_config::{base::proxy::Builder, queue::ConfigurationProxy}; use iroha_data_model::{prelude::*, transaction::TransactionLimits}; use iroha_primitives::must_use::MustUse; use rand::Rng as _; From 469386805ab49973d09bb1031e6a10e4a62ce586 Mon Sep 17 00:00:00 2001 From: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> Date: Mon, 5 Feb 2024 07:56:01 +0900 Subject: [PATCH 45/94] [refactor]: use `capacity` term in Queue Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- config/src/parameters/actual.rs | 4 +-- config/src/parameters/user_layer.rs | 4 +-- .../src/parameters/user_layer/boilerplate.rs | 10 +++--- config/tests/fixtures.rs | 8 ++--- core/src/queue.rs | 32 +++++++++---------- 5 files changed, 29 insertions(+), 29 deletions(-) diff --git a/config/src/parameters/actual.rs b/config/src/parameters/actual.rs index 79710fc8890..13a3b0eb697 100644 --- a/config/src/parameters/actual.rs +++ b/config/src/parameters/actual.rs @@ -111,8 +111,8 @@ impl Default for Queue { Self { transaction_time_to_live: defaults::queue::DEFAULT_TRANSACTION_TIME_TO_LIVE, future_threshold: defaults::queue::DEFAULT_FUTURE_THRESHOLD, - size: defaults::queue::DEFAULT_MAX_TRANSACTIONS_IN_QUEUE, - size_per_user: defaults::queue::DEFAULT_MAX_TRANSACTIONS_IN_QUEUE_PER_USER, + capacity: defaults::queue::DEFAULT_MAX_TRANSACTIONS_IN_QUEUE, + capacity_per_user: defaults::queue::DEFAULT_MAX_TRANSACTIONS_IN_QUEUE_PER_USER, } } } diff --git a/config/src/parameters/user_layer.rs b/config/src/parameters/user_layer.rs index a886ea04c4a..7a547676c98 100644 --- a/config/src/parameters/user_layer.rs +++ b/config/src/parameters/user_layer.rs @@ -457,10 +457,10 @@ impl Network { #[derive(Debug, Clone, Copy)] pub struct Queue { /// The upper limit of the number of transactions waiting in the queue. - pub size: NonZeroUsize, + pub capacity: NonZeroUsize, /// The upper limit of the number of transactions waiting in the queue for single user. /// Use this option to apply throttling. - pub size_per_user: NonZeroUsize, + pub capacity_per_user: NonZeroUsize, /// The transaction will be dropped after this time if it is still in the queue. pub transaction_time_to_live: Duration, /// The threshold to determine if a transaction has been tampered to have a future timestamp. diff --git a/config/src/parameters/user_layer/boilerplate.rs b/config/src/parameters/user_layer/boilerplate.rs index d6523d66aeb..0eecaac9f57 100644 --- a/config/src/parameters/user_layer/boilerplate.rs +++ b/config/src/parameters/user_layer/boilerplate.rs @@ -558,10 +558,10 @@ impl FromEnvDefaultFallback for NetworkPartial {} #[serde(deny_unknown_fields, default)] pub struct QueuePartial { /// The upper limit of the number of transactions waiting in the queue. - pub size: UserField, + pub capacity: UserField, /// The upper limit of the number of transactions waiting in the queue for single user. /// Use this option to apply throttling. - pub size_per_user: UserField, + pub capacity_per_user: UserField, /// The transaction will be dropped after this time if it is still in the queue. pub transaction_time_to_live: UserField, /// The threshold to determine if a transaction has been tampered to have a future timestamp. @@ -573,9 +573,9 @@ impl UnwrapPartial for QueuePartial { fn unwrap_partial(self) -> UnwrapPartialResult { Ok(Queue { - size: self.size.unwrap_or(DEFAULT_MAX_TRANSACTIONS_IN_QUEUE), - size_per_user: self - .size_per_user + capacity: self.capacity.unwrap_or(DEFAULT_MAX_TRANSACTIONS_IN_QUEUE), + capacity_per_user: self + .capacity_per_user .unwrap_or(DEFAULT_MAX_TRANSACTIONS_IN_QUEUE), transaction_time_to_live: self .transaction_time_to_live diff --git a/config/tests/fixtures.rs b/config/tests/fixtures.rs index 5894835e2a5..fcc35ea687a 100644 --- a/config/tests/fixtures.rs +++ b/config/tests/fixtures.rs @@ -99,8 +99,8 @@ fn minimal_config_snapshot() -> Result<()> { tokio_console_addr: 127.0.0.1:5555, }, queue: Queue { - size: 65536, - size_per_user: 65536, + capacity: 65536, + capacity_per_user: 65536, transaction_time_to_live: 86400s, future_threshold: 1s, }, @@ -332,8 +332,8 @@ fn full_envs_set_is_consumed() -> Result<()> { tokio_console_addr: None, }, queue: QueuePartial { - size: None, - size_per_user: None, + capacity: None, + capacity_per_user: None, transaction_time_to_live: None, future_threshold: None, }, diff --git a/core/src/queue.rs b/core/src/queue.rs index 4ce8464556a..1f5112b8163 100644 --- a/core/src/queue.rs +++ b/core/src/queue.rs @@ -54,9 +54,9 @@ pub struct Queue { /// Amount of transactions per user in the queue txs_per_user: DashMap, /// The maximum number of transactions in the queue - max_txs: NonZeroUsize, + capacity: NonZeroUsize, /// The maximum number of transactions in the queue per user. Used to apply throttling - max_txs_per_user: NonZeroUsize, + capacity_per_user: NonZeroUsize, /// Length of time after which transactions are dropped. pub tx_time_to_live: Duration, /// A point in time that is considered `Future` we cannot use @@ -101,11 +101,11 @@ impl Queue { /// Makes queue from configuration pub fn from_config(cfg: Config) -> Self { Self { - tx_hashes: ArrayQueue::new(cfg.size.get()), + tx_hashes: ArrayQueue::new(cfg.capacity.get()), accepted_txs: DashMap::new(), txs_per_user: DashMap::new(), - max_txs: cfg.size, - max_txs_per_user: cfg.size_per_user, + capacity: cfg.capacity, + capacity_per_user: cfg.capacity_per_user, tx_time_to_live: cfg.transaction_time_to_live, future_threshold: cfg.future_threshold, } @@ -210,9 +210,9 @@ impl Queue { } Entry::Vacant(entry) => entry, }; - if txs_len >= self.max_txs.get() { + if txs_len >= self.capacity.get() { warn!( - max = self.max_txs, + max = self.capacity, "Achieved maximum amount of transactions" ); return Err(Failure { @@ -350,9 +350,9 @@ impl Queue { } Entry::Occupied(mut occupied) => { let txs = *occupied.get(); - if txs >= self.max_txs_per_user.get() { + if txs >= self.capacity_per_user.get() { warn!( - max_txs_per_user = self.max_txs_per_user, + max_txs_per_user = self.capacity_per_user, %account_id, "Account reached maximum allowed number of transactions in the queue per user" ); @@ -428,7 +428,7 @@ mod tests { fn config_factory() -> Config { Config { transaction_time_to_live: Duration::from_secs(100), - size: 100.try_into().unwrap(), + capacity: 100.try_into().unwrap(), ..Config::default() } } @@ -453,7 +453,7 @@ mod tests { #[test] async fn push_tx_overflow() { - let max_transactions_in_queue = NonZeroUsize::new(10).unwrap(); + let capacity = NonZeroUsize::new(10).unwrap(); let key_pair = KeyPair::generate(); let kura = Kura::blank_kura_for_testing(); @@ -466,11 +466,11 @@ mod tests { let queue = Queue::from_config(Config { transaction_time_to_live: Duration::from_secs(100), - size: max_transactions_in_queue, + capacity, ..Config::default() }); - for _ in 0..max_transactions_in_queue.get() { + for _ in 0..capacity.get() { queue .push(accepted_tx("alice@wonderland", &key_pair), &wsv) .expect("Failed to push tx into queue"); @@ -772,7 +772,7 @@ mod tests { let queue = Arc::new(Queue::from_config(Config { transaction_time_to_live: Duration::from_secs(100), - size: 100_000_000.try_into().unwrap(), + capacity: 100_000_000.try_into().unwrap(), ..Config::default() })); @@ -907,8 +907,8 @@ mod tests { let queue = Queue::from_config(Config { transaction_time_to_live: Duration::from_secs(100), - size: 100.try_into().unwrap(), - size_per_user: 1.try_into().unwrap(), + capacity: 100.try_into().unwrap(), + capacity_per_user: 1.try_into().unwrap(), ..Config::default() }); From 250c1db236ee627d4db68e83f1196ed075a04a46 Mon Sep 17 00:00:00 2001 From: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> Date: Mon, 5 Feb 2024 08:01:04 +0900 Subject: [PATCH 46/94] [refactor]: `set_creation_time_ms`, use `*` instead of `mul` Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- client/src/client.rs | 2 +- core/src/queue.rs | 4 ++-- data_model/src/transaction.rs | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/client/src/client.rs b/client/src/client.rs index 421a18674ef..2c5c3d4b531 100644 --- a/client/src/client.rs +++ b/client/src/client.rs @@ -1630,7 +1630,7 @@ mod tests { .with_executable(tx1.instructions().clone()) .with_metadata(tx1.metadata().clone()); - tx.set_creation_time(tx1.creation_time().as_millis().try_into().unwrap()); + tx.set_creation_time_ms(tx1.creation_time().as_millis().try_into().unwrap()); if let Some(nonce) = tx1.nonce() { tx.set_nonce(nonce); } diff --git a/core/src/queue.rs b/core/src/queue.rs index 1f5112b8163..17362c88fc4 100644 --- a/core/src/queue.rs +++ b/core/src/queue.rs @@ -381,7 +381,7 @@ impl Queue { #[cfg(test)] mod tests { - use std::{ops::Mul, str::FromStr, sync::Arc, thread, time::Duration}; + use std::{str::FromStr, sync::Arc, thread, time::Duration}; use iroha_data_model::{prelude::*, transaction::TransactionLimits}; use iroha_primitives::must_use::MustUse; @@ -860,7 +860,7 @@ mod tests { .with_executable(tx.0.instructions().clone()); let creation_time: u64 = tx.0.creation_time().as_millis().try_into().unwrap(); - new_tx.set_creation_time(creation_time + future_threshold.mul(2).as_millis() as u64); + new_tx.set_creation_time_ms(creation_time + (future_threshold * 2).as_millis() as u64); let new_tx = new_tx.sign(&alice_key); let limits = TransactionLimits { diff --git a/data_model/src/transaction.rs b/data_model/src/transaction.rs index 63a8a682953..015343eec9f 100644 --- a/data_model/src/transaction.rs +++ b/data_model/src/transaction.rs @@ -735,8 +735,8 @@ mod http { } /// Set creation time of transaction - pub fn set_creation_time(&mut self, creation_time_ms: u64) -> &mut Self { - self.payload.creation_time_ms = creation_time_ms; + pub fn set_creation_time_ms(&mut self, value: u64) -> &mut Self { + self.payload.creation_time_ms = value; self } From 5bfaf8fbf4b781aec2da80b0dd68ffe326a5e166 Mon Sep 17 00:00:00 2001 From: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> Date: Mon, 5 Feb 2024 08:22:25 +0900 Subject: [PATCH 47/94] [fix]: remove dead code Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- client/src/config/user_layer.rs | 3 --- client/src/config/user_layer/boilerplate.rs | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/client/src/config/user_layer.rs b/client/src/config/user_layer.rs index 7a984eb764c..af9d539ba61 100644 --- a/client/src/config/user_layer.rs +++ b/client/src/config/user_layer.rs @@ -80,9 +80,6 @@ impl Root { } } -#[derive(Debug, Clone)] -pub struct Api {} - #[derive(Debug, Clone)] pub struct Account { pub id: AccountId, diff --git a/client/src/config/user_layer/boilerplate.rs b/client/src/config/user_layer/boilerplate.rs index 826e5e6e9d0..3bb311a4dc6 100644 --- a/client/src/config/user_layer/boilerplate.rs +++ b/client/src/config/user_layer/boilerplate.rs @@ -13,7 +13,7 @@ use serde::Deserialize; use crate::config::{ base::{FromEnvResult, ReadEnv}, - user_layer::{Account, Api, OnlyHttpUrl, Root, Transaction}, + user_layer::{Account, OnlyHttpUrl, Root, Transaction}, BasicAuth, DEFAULT_TRANSACTION_NONCE, DEFAULT_TRANSACTION_STATUS_TIMEOUT, DEFAULT_TRANSACTION_TIME_TO_LIVE, }; From 55d7aed881b7e13a360f747ec1b2d2b0ba39d0e4 Mon Sep 17 00:00:00 2001 From: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> Date: Mon, 5 Feb 2024 08:28:22 +0900 Subject: [PATCH 48/94] [refactor]: update `--config` args - use `PathBuf` again - remove default value at Client CLI Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- cli/src/main.rs | 9 ++++----- client_cli/src/main.rs | 12 ++---------- 2 files changed, 6 insertions(+), 15 deletions(-) diff --git a/cli/src/main.rs b/cli/src/main.rs index 7f527419364..ae905d73bd3 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -18,7 +18,7 @@ fn default_terminal_colors_str() -> clap::builder::OsStr { struct Args { /// Path to the configuration file #[arg(long, short, value_name("PATH"), value_hint(clap::ValueHint::FilePath))] - config: String, + config: PathBuf, /// Whether to enable ANSI colored output or not /// /// By default, Iroha determines whether the terminal supports colors or not. @@ -57,8 +57,7 @@ async fn main() -> Result<()> { color_eyre::install()?; } - let (config, genesis) = - iroha::read_config_and_genesis(PathBuf::from(args.config), args.submit_genesis)?; + let (config, genesis) = iroha::read_config_and_genesis(args.config, args.submit_genesis)?; let logger = iroha_logger::init_global(&config.logger, args.terminal_colors)?; iroha_logger::info!( @@ -88,7 +87,7 @@ mod tests { fn default_args() -> Result<()> { let args = Args::try_parse_from(["test"])?; - assert_eq!(args.config, "config.toml".to_owned()); + assert_eq!(args.config, PathBuf::from("config.toml")); assert_eq!(args.terminal_colors, is_colouring_supported()); assert_eq!(args.submit_genesis, false); @@ -118,7 +117,7 @@ mod tests { fn user_provided_config_path_works() -> Result<()> { let args = Args::try_parse_from(["test", "--config", "/home/custom/file.json"])?; - assert_eq!(args.config, "/home/custom/file.json".to_owned()); + assert_eq!(args.config, PathBuf::from("/home/custom/file.json")); Ok(()) } diff --git a/client_cli/src/main.rs b/client_cli/src/main.rs index 2adfda13882..33725bc449f 100644 --- a/client_cli/src/main.rs +++ b/client_cli/src/main.rs @@ -21,8 +21,6 @@ use iroha_client::{ }; use iroha_primitives::addr::{Ipv4Addr, Ipv6Addr, SocketAddr}; -const DEFAULT_CONFIG_PATH: &str = "iroha_client.toml"; - /// Re-usable clap `--metadata ` (`-m`) argument. /// Should be combined with `#[command(flatten)]` attr. #[derive(clap::Args, Debug, Clone)] @@ -92,14 +90,8 @@ impl FromStr for ValueArg { #[command(name = "iroha_client_cli", version = concat!("version=", env!("CARGO_PKG_VERSION"), " git_commit_sha=", env!("VERGEN_GIT_SHA")), author)] struct Args { /// Path to the configuration file - #[arg( - short, - long, - value_name("PATH"), - value_hint(clap::ValueHint::FilePath), - default_value_t = DEFAULT_CONFIG_PATH.to_owned() - )] - config: String, + #[arg(short, long, value_name("PATH"), value_hint(clap::ValueHint::FilePath))] + config: PathBuf, /// More verbose output #[arg(short, long)] verbose: bool, From 152d4cb80536acd4b20e44489034680143fda881 Mon Sep 17 00:00:00 2001 From: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> Date: Mon, 5 Feb 2024 09:02:24 +0900 Subject: [PATCH 49/94] [chore]: cleaning Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- client/src/config/user_layer/boilerplate.rs | 1 - client_cli/pytests/src/client_cli/configuration.py | 1 - config/src/parameters/actual.rs | 10 +++++----- core/src/block_sync.rs | 5 ----- 4 files changed, 5 insertions(+), 12 deletions(-) diff --git a/client/src/config/user_layer/boilerplate.rs b/client/src/config/user_layer/boilerplate.rs index 3bb311a4dc6..7ce49726fd5 100644 --- a/client/src/config/user_layer/boilerplate.rs +++ b/client/src/config/user_layer/boilerplate.rs @@ -54,7 +54,6 @@ impl RootPartial { } } -// FIXME: should config be read from ENV? impl FromEnv for RootPartial { fn from_env>(env: &R) -> FromEnvResult where diff --git a/client_cli/pytests/src/client_cli/configuration.py b/client_cli/pytests/src/client_cli/configuration.py index 89ae2be444b..4c39c5bcdff 100644 --- a/client_cli/pytests/src/client_cli/configuration.py +++ b/client_cli/pytests/src/client_cli/configuration.py @@ -36,7 +36,6 @@ def load(self, path_config_client_cli): """ if not os.path.exists(path_config_client_cli): raise IOError(f"No config file found at {path_config_client_cli}") - # TODO use toml with open(path_config_client_cli, 'r', encoding='utf-8') as config_file: self._config = tomlkit.load(config_file) self.file = path_config_client_cli diff --git a/config/src/parameters/actual.rs b/config/src/parameters/actual.rs index 13a3b0eb697..5f39cd4a74a 100644 --- a/config/src/parameters/actual.rs +++ b/config/src/parameters/actual.rs @@ -14,11 +14,15 @@ use iroha_genesis::RawGenesisBlock; use iroha_primitives::{addr::SocketAddr, unique_vec::UniqueVec}; use serde::{Deserialize, Serialize}; use url::Url; +pub use user_layer::{Logger, Queue, Snapshot}; use crate::{ kura::Mode, logger::Format, - parameters::{defaults, user_layer}, + parameters::{ + defaults, user_layer, + user_layer::{CliContext, RootPartial}, + }, }; #[derive(Debug, Clone)] @@ -117,10 +121,6 @@ impl Default for Queue { } } -pub use user_layer::{Logger, Queue, Snapshot}; - -use crate::parameters::user_layer::{CliContext, RootPartial}; - #[derive(Debug, Clone)] pub struct Sumeragi { pub trusted_peers: UniqueVec, diff --git a/core/src/block_sync.rs b/core/src/block_sync.rs index eb3174e8ca3..4919f14f304 100644 --- a/core/src/block_sync.rs +++ b/core/src/block_sync.rs @@ -191,11 +191,6 @@ pub mod message { previous_hash, peer_id, }) => { - // FIXME: is it okay to remove this behaviour? - // if block_sync.block_batch_size == 0 { - // warn!("Error: not sending any blocks as batch_size is equal to zero."); - // return; - // } let local_latest_block_hash = block_sync.latest_hash; if *latest_hash == local_latest_block_hash || *previous_hash == local_latest_block_hash From 609bd2620f65b13792dc803820d7b97d22c5aba3 Mon Sep 17 00:00:00 2001 From: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> Date: Mon, 5 Feb 2024 09:32:26 +0900 Subject: [PATCH 50/94] [refactor]: use `strum` in place of `parse_display` Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- Cargo.lock | 1 - config/Cargo.toml | 1 - config/src/kura.rs | 8 +++----- config/src/logger.rs | 6 ++---- data_model/src/lib.rs | 4 ++-- 5 files changed, 7 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ead62be6751..0de004ef7e1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2764,7 +2764,6 @@ dependencies = [ "merge", "nonzero_ext", "once_cell", - "parse-display", "proptest", "serde", "serde_json", diff --git a/config/Cargo.toml b/config/Cargo.toml index 83d9378f568..b0d1241da88 100644 --- a/config/Cargo.toml +++ b/config/Cargo.toml @@ -33,7 +33,6 @@ cfg-if = { workspace = true } once_cell = { workspace = true } nonzero_ext = "0.3.0" toml = { workspace = true } -parse-display = { workspace = true } merge = "0.1.0" [dev-dependencies] diff --git a/config/src/kura.rs b/config/src/kura.rs index a7d4f63e68a..8ead6ba500f 100644 --- a/config/src/kura.rs +++ b/config/src/kura.rs @@ -1,11 +1,9 @@ use iroha_config_base::{impl_deserialize_from_str, impl_serialize_display}; -use serde::Serializer; +use serde::{Deserialize, Serialize, Serializer}; /// Kura initialization mode. -#[derive( - Debug, Clone, Copy, PartialEq, Eq, Default, parse_display::Display, parse_display::FromStr, -)] -#[display(style = "snake_case")] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, strum::EnumString, strum::Display)] +#[strum(serialize_all = "snake_case")] pub enum Mode { /// Strict validation of all blocks. #[default] diff --git a/config/src/logger.rs b/config/src/logger.rs index 1a15d445504..47dba0636a7 100644 --- a/config/src/logger.rs +++ b/config/src/logger.rs @@ -13,10 +13,8 @@ pub fn into_tracing_level(level: Level) -> tracing::Level { } /// Reflects formatters in [`tracing_subscriber::fmt::format`] -#[derive( - Debug, Copy, Clone, Eq, PartialEq, parse_display::Display, parse_display::FromStr, Default, -)] -#[display(style = "snake_case")] +#[derive(Debug, Copy, Clone, Eq, PartialEq, strum::Display, strum::EnumString, Default)] +#[strum(serialize_all = "snake_case")] pub enum Format { /// See [`tracing_subscriber::fmt::format::Full`] #[default] diff --git a/data_model/src/lib.rs b/data_model/src/lib.rs index 3275d4dce6b..561228a73cc 100644 --- a/data_model/src/lib.rs +++ b/data_model/src/lib.rs @@ -974,7 +974,6 @@ pub mod model { /// Log level for reading from environment and (de)serializing #[derive( Debug, - Display, Clone, Copy, Default, @@ -988,7 +987,8 @@ pub mod model { Decode, FromRepr, IntoSchema, - parse_display::FromStr, + strum::Display, + strum::EnumString, )] #[allow(clippy::upper_case_acronyms)] #[repr(u8)] From 980c8ef31d0e62c07f8716fee90b2aa15fb6c3c4 Mon Sep 17 00:00:00 2001 From: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> Date: Mon, 5 Feb 2024 09:34:46 +0900 Subject: [PATCH 51/94] [refactor]: no unsafe code any more Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- client/src/config.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/client/src/config.rs b/client/src/config.rs index 00f72a23765..47dd77c5782 100644 --- a/client/src/config.rs +++ b/client/src/config.rs @@ -20,7 +20,6 @@ use crate::config::user_layer::RootPartial; pub mod user_layer; -#[allow(unsafe_code)] pub const DEFAULT_TRANSACTION_TIME_TO_LIVE: Duration = Duration::from_secs(100); pub const DEFAULT_TRANSACTION_STATUS_TIMEOUT: Duration = Duration::from_secs(15); pub const DEFAULT_TRANSACTION_NONCE: bool = false; From 375dce23be19c4a31c9e14f055ea37bb62d47149 Mon Sep 17 00:00:00 2001 From: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> Date: Wed, 7 Feb 2024 06:10:04 +0900 Subject: [PATCH 52/94] [refactor]: docs & refinements - document code in: - `iroha_config_base` - `iroha_config` - `iroha_client::config` - refactor `iroha_config_base` APIs - move `ExtendsPaths` into `iroha_config_base` - remove `[iroha]` user config section - move `chain_id` and key pair to the root - move `p2p_address` to `network.address` - rename `user_layer` modules to `user` - add `_bytes` suffix for relevant fields, put a TODO to add a newtype for it Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- Cargo.lock | 2 + cli/src/lib.rs | 26 +- cli/src/samples.rs | 14 +- client/benches/torii.rs | 2 +- client/src/config.rs | 14 +- client/src/config/{user_layer.rs => user.rs} | 24 +- .../{user_layer => user}/boilerplate.rs | 16 +- config/base/Cargo.toml | 4 + config/base/src/lib.rs | 252 +++++++++++++-- config/src/kura.rs | 3 +- config/src/lib.rs | 5 +- config/src/logger.rs | 2 + config/src/parameters/actual.rs | 54 ++-- config/src/parameters/defaults.rs | 25 +- config/src/parameters/mod.rs | 4 +- .../src/parameters/{user_layer.rs => user.rs} | 295 ++++++------------ .../{user_layer => user}/boilerplate.rs | 241 +++++++------- config/tests/fixtures.rs | 52 ++- .../fixtures/bad.torii_addr_eq_p2p_addr.toml | 4 +- config/tests/fixtures/base.toml | 5 +- .../tests/fixtures/minimal_file_and_env.toml | 5 +- core/benches/blocks/common.rs | 2 +- core/src/kiso.rs | 2 +- core/src/smartcontracts/wasm.rs | 2 +- core/src/sumeragi/mod.rs | 18 +- core/src/wsv.rs | 2 +- core/test_network/src/lib.rs | 14 +- tools/kagami/src/genesis.rs | 4 +- tools/swarm/src/compose.rs | 2 +- torii/src/lib.rs | 2 +- 30 files changed, 593 insertions(+), 504 deletions(-) rename client/src/config/{user_layer.rs => user.rs} (83%) rename client/src/config/{user_layer => user}/boilerplate.rs (90%) rename config/src/parameters/{user_layer.rs => user.rs} (71%) rename config/src/parameters/{user_layer => user}/boilerplate.rs (86%) diff --git a/Cargo.lock b/Cargo.lock index 0de004ef7e1..79b1d9a674f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2785,8 +2785,10 @@ dependencies = [ "drop_bomb", "eyre", "merge", + "num-traits", "serde", "thiserror", + "toml 0.8.8", ] [[package]] diff --git a/cli/src/lib.rs b/cli/src/lib.rs index 728d8985349..e2ba5e5c936 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -9,7 +9,7 @@ use core::sync::atomic::{AtomicBool, Ordering}; use std::{path::Path, sync::Arc}; use color_eyre::eyre::{eyre, Result, WrapErr}; -use iroha_config::parameters::{actual::Root as Config, user_layer::CliContext}; +use iroha_config::parameters::{actual::Root as Config, user::CliContext}; use iroha_core::{ block_sync::{BlockSynchronizer, BlockSynchronizerHandle}, gossiper::{TransactionGossiper, TransactionGossiperHandle}, @@ -199,8 +199,8 @@ impl Iroha { logger: LoggerHandle, ) -> Result { let network = IrohaNetwork::start( - config.iroha.p2p_address.clone(), - config.iroha.key_pair.clone(), + config.common.p2p_address.clone(), + config.common.key_pair.clone(), ) .await .wrap_err("Unable to start P2P-network")?; @@ -250,7 +250,7 @@ impl Iroha { let start_args = SumeragiStartArgs { sumeragi_config: config.sumeragi.clone(), - iroha_config: config.iroha.clone(), + common_config: config.common.clone(), events_sender: events_sender.clone(), wsv, queue: Arc::clone(&queue), @@ -268,13 +268,13 @@ impl Iroha { &config.block_sync, sumeragi.clone(), Arc::clone(&kura), - config.iroha.peer_id(), + config.common.peer_id(), network.clone(), ) .start(); let gossiper = TransactionGossiper::from_config( - config.iroha.chain_id.clone(), + config.common.chain_id.clone(), config.transaction_gossiper, network.clone(), Arc::clone(&queue), @@ -303,7 +303,7 @@ impl Iroha { let kiso = KisoHandle::new(config.clone()); let torii = Torii::new( - config.iroha.chain_id.clone(), + config.common.chain_id.clone(), kiso.clone(), config.torii, Arc::clone(&queue), @@ -507,7 +507,7 @@ pub fn read_config_and_genesis( let raw_block = RawGenesisBlock::from_path(file)?; Some( - GenesisNetwork::new(raw_block, &config.iroha.chain_id, &key_pair) + GenesisNetwork::new(raw_block, &config.common.chain_id, &key_pair) .wrap_err("Failed to construct the genesis")?, ) } else { @@ -550,7 +550,7 @@ mod tests { mod config_integration { use assertables::{assert_contains, assert_contains_as_result}; - use iroha_config::parameters::user_layer::RootPartial as PartialUserConfig; + use iroha_config::parameters::user::RootPartial as PartialUserConfig; use iroha_crypto::KeyPair; use iroha_genesis::{ExecutorMode, ExecutorPath}; use iroha_primitives::addr::socket_addr; @@ -563,10 +563,10 @@ mod tests { let mut base = PartialUserConfig::default(); - base.iroha.chain_id.set(ChainId::from("0")); - base.iroha.public_key.set(pubkey.clone()); - base.iroha.private_key.set(privkey.clone()); - base.iroha.p2p_address.set(socket_addr!(127.0.0.1:1337)); + base.chain_id.set(ChainId::from("0")); + base.public_key.set(pubkey.clone()); + base.private_key.set(privkey.clone()); + base.network.address.set(socket_addr!(127.0.0.1:1337)); base.genesis.public_key.set(pubkey); base.genesis.private_key.set(privkey); diff --git a/cli/src/samples.rs b/cli/src/samples.rs index 3cba47c0da3..fd882cba8eb 100644 --- a/cli/src/samples.rs +++ b/cli/src/samples.rs @@ -2,10 +2,10 @@ use std::{collections::HashSet, path::Path, str::FromStr, time::Duration}; use iroha_config::{ - base::{UnwrapPartial, UserDuration}, + base::{HumanDuration, UnwrapPartial}, parameters::{ actual::Root as Config, - user_layer::{CliContext, RootPartial as UserConfig}, + user::{CliContext, RootPartial as UserConfig}, }, }; use iroha_crypto::{KeyPair, PublicKey}; @@ -70,10 +70,10 @@ pub fn get_user_config( let mut config = UserConfig::new(); - config.iroha.chain_id.set(chain_id); - config.iroha.public_key.set(public_key.clone()); - config.iroha.private_key.set(private_key.clone()); - config.iroha.p2p_address.set(DEFAULT_P2P_ADDR); + config.chain_id.set(chain_id); + config.public_key.set(public_key.clone()); + config.private_key.set(private_key.clone()); + config.network.address.set(DEFAULT_P2P_ADDR); config .chain_wide .max_transactions_in_block @@ -87,7 +87,7 @@ pub fn get_user_config( config .network .block_gossip_period - .set(UserDuration(Duration::from_millis(500))); + .set(HumanDuration(Duration::from_millis(500))); config.genesis.private_key.set(private_key); config.genesis.public_key.set(public_key); config.genesis.file.set("./genesis.json".into()); diff --git a/client/benches/torii.rs b/client/benches/torii.rs index f13025e3190..383366a2796 100644 --- a/client/benches/torii.rs +++ b/client/benches/torii.rs @@ -141,7 +141,7 @@ fn instruction_submits(criterion: &mut Criterion) { .domain("wonderland".parse().expect("Valid")) .account( "alice".parse().expect("Valid"), - configuration.iroha.key_pair.public_key().clone(), + configuration.common.key_pair.public_key().clone(), ) .finish_domain() .executor( diff --git a/client/src/config.rs b/client/src/config.rs index 47dd77c5782..5340d8d85a7 100644 --- a/client/src/config.rs +++ b/client/src/config.rs @@ -1,10 +1,7 @@ //! Module for client-related configuration and structs -// FIXME -#![allow(unused, missing_docs)] - use core::str::FromStr; -use std::{num::NonZeroU64, path::Path, time::Duration}; +use std::{path::Path, time::Duration}; use derive_more::Display; use eyre::Result; @@ -16,12 +13,15 @@ use iroha_primitives::small::SmallStr; use serde::{Deserialize, Serialize}; use url::Url; -use crate::config::user_layer::RootPartial; +use crate::config::user::RootPartial; -pub mod user_layer; +pub mod user; +#[allow(missing_docs)] pub const DEFAULT_TRANSACTION_TIME_TO_LIVE: Duration = Duration::from_secs(100); +#[allow(missing_docs)] pub const DEFAULT_TRANSACTION_STATUS_TIMEOUT: Duration = Duration::from_secs(15); +#[allow(missing_docs)] pub const DEFAULT_TRANSACTION_NONCE: bool = false; /// Wrapper over `SmallStr` to provide basic auth login checking @@ -69,7 +69,9 @@ pub struct BasicAuth { pub password: SmallStr, } +/// Complete client configuration #[derive(Clone, Debug, Serialize)] +#[allow(missing_docs)] pub struct Config { pub chain_id: ChainId, pub account_id: AccountId, diff --git a/client/src/config/user_layer.rs b/client/src/config/user.rs similarity index 83% rename from client/src/config/user_layer.rs rename to client/src/config/user.rs index af9d539ba61..3007efdebee 100644 --- a/client/src/config/user_layer.rs +++ b/client/src/config/user.rs @@ -1,18 +1,21 @@ +//! User configuration view. + mod boilerplate; -use std::{fs::File, io::Read, path::Path, str::FromStr, time::Duration}; +use std::{str::FromStr, time::Duration}; pub use boilerplate::*; use eyre::{eyre, Context, Report}; -use iroha_config::base::{Emitter, ErrorsCollection, FromEnvDefaultFallback, Merge, UnwrapPartial}; +use iroha_config::base::{Emitter, ErrorsCollection}; use iroha_crypto::{KeyPair, PrivateKey, PublicKey}; use iroha_data_model::{account::AccountId, ChainId}; -use serde::{Deserialize, Deserializer}; use url::Url; use crate::config::BasicAuth; +/// Root of the user configuration #[derive(Clone, Debug)] +#[allow(missing_docs)] pub struct Root { pub chain_id: ChainId, pub torii_url: OnlyHttpUrl, @@ -22,6 +25,8 @@ pub struct Root { } impl Root { + /// Validates user configuration for semantic errors and constructs a complete + /// [`super::Config`]. pub fn parse(self) -> Result> { let Self { chain_id, @@ -81,6 +86,7 @@ impl Root { } #[derive(Debug, Clone)] +#[allow(missing_docs)] pub struct Account { pub id: AccountId, pub public_key: PublicKey, @@ -88,12 +94,14 @@ pub struct Account { } #[derive(Debug, Clone, Copy)] +#[allow(missing_docs)] pub struct Transaction { pub time_to_live: Duration, pub status_timeout: Duration, pub nonce: bool, } +/// A [`Url`] that might only have HTTP scheme inside #[derive(Debug, Clone, Eq, PartialEq)] pub struct OnlyHttpUrl(Url); @@ -112,12 +120,18 @@ impl FromStr for OnlyHttpUrl { } } +/// Possible errors that might occur for [`FromStr::from_str`] for [`OnlyHttpUrl`]. #[derive(Debug, thiserror::Error)] pub enum ParseHttpUrlError { + /// Unable to parse the url #[error(transparent)] Parse(#[from] url::ParseError), + /// Parsed fine, but doesn't contain HTTP #[error("expected `http` scheme, found: `{found}`")] - NotHttp { found: String }, + NotHttp { + /// What scheme was actually found + found: String, + }, } iroha_config::base::impl_deserialize_from_str!(OnlyHttpUrl); @@ -134,7 +148,7 @@ mod tests { fn parses_all_envs() { let env = TestEnv::new().set("TORII_URL", "http://localhost:8080"); - let layer = RootPartial::from_env(&env).expect("should not fail since env is valid"); + let _layer = RootPartial::from_env(&env).expect("should not fail since env is valid"); assert_eq!(env.unvisited(), HashSet::new()) } diff --git a/client/src/config/user_layer/boilerplate.rs b/client/src/config/user/boilerplate.rs similarity index 90% rename from client/src/config/user_layer/boilerplate.rs rename to client/src/config/user/boilerplate.rs index 7ce49726fd5..205e7f5a64e 100644 --- a/client/src/config/user_layer/boilerplate.rs +++ b/client/src/config/user/boilerplate.rs @@ -1,11 +1,13 @@ //! Code to be generated by a proc macro in future +#![allow(missing_docs)] + use std::{error::Error, fs::File, io::Read, path::Path}; use eyre::{eyre, Context}; use iroha_config::base::{ - Emitter, FromEnv, Merge, MissingFieldError, ParseEnvResult, UnwrapPartial, UnwrapPartialResult, - UserDuration, UserField, + Emitter, FromEnv, HumanDuration, Merge, ParseEnvResult, UnwrapPartial, UnwrapPartialResult, + UserField, }; use iroha_crypto::{PrivateKey, PublicKey}; use iroha_data_model::{account::AccountId, ChainId}; @@ -13,7 +15,7 @@ use serde::Deserialize; use crate::config::{ base::{FromEnvResult, ReadEnv}, - user_layer::{Account, OnlyHttpUrl, Root, Transaction}, + user::{Account, OnlyHttpUrl, Root, Transaction}, BasicAuth, DEFAULT_TRANSACTION_NONCE, DEFAULT_TRANSACTION_STATUS_TIMEOUT, DEFAULT_TRANSACTION_TIME_TO_LIVE, }; @@ -140,8 +142,8 @@ impl UnwrapPartial for AccountPartial { #[derive(Debug, Clone, Deserialize, Eq, PartialEq, Default, Merge)] #[serde(deny_unknown_fields, default)] pub struct TransactionPartial { - pub time_to_live: UserField, - pub status_timeout: UserField, + pub time_to_live: UserField, + pub status_timeout: UserField, pub nonce: UserField, } @@ -153,11 +155,11 @@ impl UnwrapPartial for TransactionPartial { time_to_live: self .time_to_live .get() - .map_or(DEFAULT_TRANSACTION_TIME_TO_LIVE, UserDuration::get), + .map_or(DEFAULT_TRANSACTION_TIME_TO_LIVE, HumanDuration::get), status_timeout: self .status_timeout .get() - .map_or(DEFAULT_TRANSACTION_STATUS_TIMEOUT, UserDuration::get), + .map_or(DEFAULT_TRANSACTION_STATUS_TIMEOUT, HumanDuration::get), nonce: self.nonce.get().unwrap_or(DEFAULT_TRANSACTION_NONCE), }) } diff --git a/config/base/Cargo.toml b/config/base/Cargo.toml index 0da79550ff1..87f78d0b976 100644 --- a/config/base/Cargo.toml +++ b/config/base/Cargo.toml @@ -17,3 +17,7 @@ derive_more = { workspace = true, features = ["from", "deref", "deref_mut"] } eyre = { workspace = true } serde = { workspace = true, features = ["derive"] } thiserror = { workspace = true } +num-traits = "0.2.17" + +[dev-dependencies] +toml = { workspace = true } diff --git a/config/base/src/lib.rs b/config/base/src/lib.rs index cb9c0324c58..f7403e12519 100644 --- a/config/base/src/lib.rs +++ b/config/base/src/lib.rs @@ -1,8 +1,5 @@ //! Utilities behind Iroha configurations -// FIXME -#![allow(missing_docs)] - use std::{ borrow::Cow, cell::RefCell, @@ -13,6 +10,7 @@ use std::{ ffi::OsString, fmt::{Debug, Display, Formatter}, ops::Sub, + path::PathBuf, str::FromStr, time::Duration, }; @@ -22,6 +20,7 @@ pub use merge::Merge; pub use serde; use serde::{Deserialize, Serialize}; +/// Implement [`serde::Serialize`] using `Display` for a given type #[macro_export] macro_rules! impl_serialize_display { ($ty:ty) => { @@ -36,6 +35,7 @@ macro_rules! impl_serialize_display { }; } +/// Implement [`serde::Deserialize`] using [`FromStr`] for a given type #[macro_export] macro_rules! impl_deserialize_from_str { ($ty:ty) => { @@ -52,32 +52,30 @@ macro_rules! impl_deserialize_from_str { }; } -/// User-provided duration +/// [`Duration`], but can parse a human-readable string. +/// TODO: currently deserializes just as [`Duration`] #[derive(Debug, Copy, Clone, Deserialize, Serialize, Ord, PartialOrd, Eq, PartialEq)] -pub struct UserDuration(pub Duration); +pub struct HumanDuration(pub Duration); -impl UserDuration { +impl HumanDuration { + /// Get the [`Duration`] pub fn get(self) -> Duration { self.0 } } -/// Byte size +/// Representation of amount of bytes, parseable from a human-readable string. #[derive(Debug, Copy, Clone, Deserialize, Serialize)] -pub struct ByteSize(pub T); +pub struct HumanBytes(pub T); -impl ByteSize { - pub fn get(&self) -> T { +impl HumanBytes { + /// Get the number of bytes + pub fn get(self) -> T { self.0 } } -impl From for ByteSize { - fn from(value: T) -> Self { - Self(value) - } -} - +/// Error representing a missing field in the configuration #[derive(thiserror::Error, Debug)] #[error("missing field: `{path}`")] pub struct MissingFieldError { @@ -85,25 +83,40 @@ pub struct MissingFieldError { } impl MissingFieldError { + /// Create an instance pub fn new(s: &str) -> Self { Self { path: s.to_owned() } } } +/// Provides environment variables pub trait ReadEnv { - /// TODO document why cow - fn get(&self, key: impl AsRef) -> Result>, E>; -} - + /// Read a value of an environment variable. + /// + /// This is a fallible operation, which might return an empty value if the given key is not + /// present. + /// + /// [`Cow`] is used for flexibility. The read value might be given both as an owned and as a + /// borrowed string depending on the structure that implements [`ReadEnv`]. On the receiving + /// part, it might be convenient to parse the string while just borrowing it + /// (e.g. with [`FromStr`]), but might be also convenient to own the value. [`Cow`] covers all + /// of this. + fn read_env(&self, key: impl AsRef) -> Result>, E>; +} + +/// Constructs from environment variables pub trait FromEnv { - // E: Error so that it could be wrapped into Report + /// Constructs from environment variables using [`ReadEnv`] + // `E: Error` so that it could be wrapped into a Report fn from_env>(env: &R) -> FromEnvResult where Self: Sized; } +/// Result of [`FromEnv::from_env`]. Intended to contain multiple possible errors at once. pub type FromEnvResult = eyre::Result>; +/// Marker trait to implement [`FromEnv`] if a type implements [`Default`] pub trait FromEnvDefaultFallback {} impl FromEnv for T @@ -118,12 +131,16 @@ where } } +/// Simple collector of errors. +/// +/// Will panic on [`Drop`] if contains errors that are not handled with [`Emitter::finish`]. pub struct Emitter { errors: Vec, bomb: drop_bomb::DropBomb, } impl Emitter { + /// Create a new empty emitter pub fn new() -> Self { Self { errors: Vec::new(), @@ -133,15 +150,19 @@ impl Emitter { } } + /// Emit a single error pub fn emit(&mut self, error: T) { self.errors.push(error); } + /// Emit a collection of errors pub fn emit_collection(&mut self, mut errors: ErrorsCollection) { self.errors.append(&mut errors.0); } - pub fn finish(mut self) -> eyre::Result<(), ErrorsCollection> { + /// Transform the emitter into a [`Result`], containing an [`ErrorCollection`] if + /// any errors were emitted. + pub fn finish(mut self) -> Result<(), ErrorsCollection> { self.bomb.defuse(); if self.errors.is_empty() { @@ -153,10 +174,15 @@ impl Emitter { } impl Emitter { + /// Shorthand to emit a [`MissingFieldError`]. pub fn emit_missing_field(&mut self, field_name: impl AsRef) { self.emit(MissingFieldError::new(field_name.as_ref())) } + /// Tries to [`UnwrapPartial`], collecting errors on failure. + /// + /// This method is relevant for [`Emitter`], because [`UnwrapPartial`] + /// returns a collection of [`MissingFieldError`]s. pub fn try_unwrap_partial(&mut self, partial: P) -> Option { partial.unwrap_partial().map_or_else( |err| { @@ -168,10 +194,12 @@ impl Emitter { } } +/// An [`Error`] containing multiple errors inside pub struct ErrorsCollection(Vec); impl Error for ErrorsCollection {} +/// Displays each error on a new line impl Display for ErrorsCollection where T: Display, @@ -217,6 +245,7 @@ impl IntoIterator for ErrorsCollection { } } +/// An implementation of [`ReadEnv`] for testing convenience. #[derive(Default)] pub struct TestEnv { map: HashMap, @@ -224,14 +253,17 @@ pub struct TestEnv { } impl TestEnv { + /// Create new empty environment pub fn new() -> Self { Self::default() } + /// Create an environment with a given map pub fn with_map(map: HashMap) -> Self { Self { map, ..Self::new() } } + /// Set a key-value pair #[must_use] pub fn set(mut self, key: impl AsRef, value: impl AsRef) -> Self { self.map @@ -239,6 +271,7 @@ impl TestEnv { self } + /// Get a set of keys not visited yet by [`ReadEnv::read_env`] pub fn unvisited(&self) -> HashSet { let all_keys: HashSet<_> = self.map.keys().map(ToOwned::to_owned).collect(); let visited: HashSet<_> = self.visited.borrow().clone(); @@ -247,7 +280,7 @@ impl TestEnv { } impl ReadEnv for TestEnv { - fn get(&self, key: impl AsRef) -> Result>, Infallible> { + fn read_env(&self, key: impl AsRef) -> Result>, Infallible> { self.visited.borrow_mut().insert(key.as_ref().to_string()); Ok(self .map @@ -257,11 +290,12 @@ impl ReadEnv for TestEnv { } } +/// Implemented of [`ReadEnv`] on top of [`std::env::var`]. #[derive(Debug, Copy, Clone)] pub struct StdEnv; impl ReadEnv for StdEnv { - fn get(&self, key: impl AsRef) -> Result>, StdEnvError> { + fn read_env(&self, key: impl AsRef) -> Result>, StdEnvError> { match std::env::var(key.as_ref()) { Ok(value) => Ok(Some(value.into())), Err(VarError::NotPresent) => Ok(None), @@ -270,15 +304,25 @@ impl ReadEnv for StdEnv { } } +/// An error that might occur while reading from std env. +/// +/// - **Q: Why just [`VarError`] is not used?** +/// - A: Because [`VarError::NotPresent`] is `Ok(None)` in terms of [`ReadEnv`] #[derive(Debug, thiserror::Error)] pub enum StdEnvError { + /// Reflects [`VarError::NotUnicode`] #[error("the specified environment variable was found, but it did not contain valid unicode data: {0:?}")] NotUnicode(OsString), } +/// A tool that simplifies work with graceful parsing of multiple values in combination +/// with [`Emitter`] pub enum ParseEnvResult { + /// Value was found and parsed Value(T), - ParseError, + /// An error occurred while reading or parsing the environment + Error, + /// Value was not found, no error occurred None, } @@ -287,14 +331,16 @@ where T: FromStr, ::Err: Error + Send + Sync + 'static, { + /// _Simple_ parsing using [`FromStr`] pub fn parse_simple( emitter: &mut Emitter, env: &impl ReadEnv, env_key: impl AsRef, field_name: impl AsRef, ) -> Self { + // FIXME: errors handling is such a mess now let read = match env - .get(env_key.as_ref()) + .read_env(env_key.as_ref()) .map_err(|err| eyre!("{err}")) .wrap_err_with(|| eyre!("ooops")) { @@ -302,7 +348,7 @@ where Ok(None) => return Self::None, Err(report) => { emitter.emit(report); - return Self::ParseError; + return Self::Error; } }; @@ -316,21 +362,31 @@ where Ok(value) => Self::Value(value), Err(report) => { emitter.emit(report); - Self::ParseError + Self::Error } } } } +/// During this conversion, [`ParseEnvResult::Error`] is interpreted as [`None`]. impl From> for Option { fn from(value: ParseEnvResult) -> Self { match value { - ParseEnvResult::None | ParseEnvResult::ParseError => None, + ParseEnvResult::None | ParseEnvResult::Error => None, ParseEnvResult::Value(x) => Some(x), } } } +/// Value container to be used in the partial layers. +/// +/// In partial layers, values might be present or not. +/// Partial layers consisting from [`UserField`] might be _incomplete_, +/// merged into each other (with [`merge::Merge`]), +/// and finally unwrapped (with [`UnwrapPartial`]) into a _complete_ layer of data. +/// +/// Partial layers might consist of fields other than [`UserField`], but their types should follow +/// the same conventions. This might be used e.g. to implement custom merge strategy. #[derive( Serialize, Deserialize, @@ -345,18 +401,21 @@ impl From> for Option { )] pub struct UserField(Option); +/// Delegating debug repr to [`Option`] impl Debug for UserField { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { self.0.fmt(f) } } +/// Empty user field impl Default for UserField { fn default() -> Self { Self(None) } } +/// The other's value takes precedence over the self's impl Merge for UserField { fn merge(&mut self, other: Self) { if let Some(value) = other.0 { @@ -365,11 +424,13 @@ impl Merge for UserField { } } -impl UserField { +impl UserField { + /// Get the field value pub fn get(self) -> Option { self.0 } + /// Set the field value pub fn set(&mut self, value: T) { self.0 = Some(value); } @@ -382,14 +443,88 @@ impl From> for UserField { } } +/// Conversion from a layer's partial state into its full state, with all required +/// fields presented. pub trait UnwrapPartial { + /// The output of unwrapping, i.e. the full layer type Output; + /// Unwraps the partial, emitting multiple [`MissingFieldError`] in case of absense. fn unwrap_partial(self) -> UnwrapPartialResult; } +/// Used for [`UnwrapPartial::unwrap_partial`] pub type UnwrapPartialResult = Result>; +/// A tool to implement "extends" mechanism, i.e. mixins. +/// +/// It allows users to provide a path of other files that should be used as +/// a _base_ layer. +/// +/// ```toml +/// # contents of this file will be merged into the contents of `base.toml` +/// extends = "./base.toml" +/// ``` +/// +/// It is possible to specify multiple extensions at once: +/// +/// ```toml +/// # read `foo`, then merge `bar`, then merge `baz`, then merge this file's contents +/// extends = ["foo", "bar", "baz"] +/// ``` +/// +/// From the developer side, it should be used as a field on a partial layer: +/// +/// ``` +/// use iroha_config_base::ExtendsPaths; +/// +/// struct SomePartial { +/// extends: Option, +/// // ..other fields +/// } +/// ``` +/// +/// When this layer is constructed from a file, `ExtendsPaths` should be handled e.g. +/// with [`ExtendsPaths::iter`]. +#[derive(Debug, Serialize, Deserialize, Eq, PartialEq)] +#[serde(untagged)] +pub enum ExtendsPaths { + /// A single path to extend from + Single(PathBuf), + /// A chain of paths to extend from + Chain(Vec), +} + +/// Iterator over [`ExtendsPaths`] for convenience +pub enum ExtendsPathsIter<'a> { + #[allow(missing_docs)] + Single(Option<&'a PathBuf>), + #[allow(missing_docs)] + Multiple(std::slice::Iter<'a, PathBuf>), +} + +impl ExtendsPaths { + /// Normalise into an iterator over chain of paths to extend from + #[allow(clippy::iter_without_into_iter)] // extra for this case + pub fn iter(&self) -> ExtendsPathsIter<'_> { + match &self { + Self::Single(x) => ExtendsPathsIter::Single(Some(x)), + Self::Chain(vec) => ExtendsPathsIter::Multiple(vec.iter()), + } + } +} + +impl<'a> Iterator for ExtendsPathsIter<'a> { + type Item = &'a PathBuf; + + fn next(&mut self) -> Option { + match self { + Self::Single(x) => x.take(), + Self::Multiple(iter) => iter.next(), + } + } +} + #[cfg(test)] mod tests { use super::*; @@ -434,4 +569,61 @@ mod tests { field.merge(UserField(None)); assert_eq!(field, UserField(Some(4))); } + + #[derive(Deserialize, Default)] + #[serde(default)] + struct TestExtends { + extends: Option, + } + + #[test] + fn parse_empty_extends() { + let value: TestExtends = toml::from_str("").expect("should be fine with empty input"); + + assert_eq!(value.extends, None); + } + + #[test] + fn parse_single_extends_path() { + let value: TestExtends = toml::toml! { + extends = "./path" + } + .try_into() + .unwrap(); + + assert_eq!(value.extends, Some(ExtendsPaths::Single("./path".into()))); + } + + #[test] + fn parse_multiple_extends_paths() { + let value: TestExtends = toml::toml! { + extends = ["foo", "bar", "baz"] + } + .try_into() + .unwrap(); + + assert_eq!( + value.extends, + Some(ExtendsPaths::Chain(vec![ + "foo".into(), + "bar".into(), + "baz".into() + ])) + ); + } + + #[test] + fn iterating_over_extends() { + impl ExtendsPaths { + fn as_str_vec(&self) -> Vec<&str> { + self.iter().map(|p| p.to_str().unwrap()).collect() + } + } + + let single = ExtendsPaths::Single("single".into()); + assert_eq!(single.as_str_vec(), vec!["single"]); + + let multi = ExtendsPaths::Chain(vec!["foo".into(), "bar".into(), "baz".into()]); + assert_eq!(multi.as_str_vec(), vec!["foo", "bar", "baz"]); + } } diff --git a/config/src/kura.rs b/config/src/kura.rs index 8ead6ba500f..655655db656 100644 --- a/config/src/kura.rs +++ b/config/src/kura.rs @@ -1,5 +1,6 @@ +//! Configuration tools related to Kura specifically. + use iroha_config_base::{impl_deserialize_from_str, impl_serialize_display}; -use serde::{Deserialize, Serialize, Serializer}; /// Kura initialization mode. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default, strum::EnumString, strum::Display)] diff --git a/config/src/lib.rs b/config/src/lib.rs index c7c6a680e5f..1697443be46 100644 --- a/config/src/lib.rs +++ b/config/src/lib.rs @@ -1,11 +1,8 @@ //! Iroha configuration and related utilities. -// FIXME -#![allow(unused, missing_docs, missing_copy_implementations)] +pub use iroha_config_base as base; pub mod client_api; pub mod kura; pub mod logger; pub mod parameters; - -pub use iroha_config_base as base; diff --git a/config/src/logger.rs b/config/src/logger.rs index 47dba0636a7..b8a97aa9712 100644 --- a/config/src/logger.rs +++ b/config/src/logger.rs @@ -1,3 +1,5 @@ +//! Configuration utils related to Logger specifically. + use iroha_config_base::{impl_deserialize_from_str, impl_serialize_display}; pub use iroha_data_model::Level; diff --git a/config/src/parameters/actual.rs b/config/src/parameters/actual.rs index 5f39cd4a74a..620fe22dc78 100644 --- a/config/src/parameters/actual.rs +++ b/config/src/parameters/actual.rs @@ -1,33 +1,36 @@ +//! "Actual" layer of Iroha configuration parameters. It contains strongly-typed validated +//! structures in a way that is efficient for Iroha internally. + use std::{ - num::{NonZeroU32, NonZeroU64, NonZeroUsize}, + num::NonZeroU32, path::{Path, PathBuf}, time::Duration, }; -use iroha_config_base::{ByteSize, FromEnv, StdEnv, UnwrapPartial}; +use iroha_config_base::{FromEnv, StdEnv, UnwrapPartial}; use iroha_crypto::{KeyPair, PublicKey}; use iroha_data_model::{ metadata::Limits as MetadataLimits, peer::PeerId, transaction::TransactionLimits, ChainId, - LengthLimits, Level, + LengthLimits, }; -use iroha_genesis::RawGenesisBlock; use iroha_primitives::{addr::SocketAddr, unique_vec::UniqueVec}; use serde::{Deserialize, Serialize}; use url::Url; -pub use user_layer::{Logger, Queue, Snapshot}; +pub use user::{Logger, Queue, Snapshot}; use crate::{ kura::Mode, - logger::Format, parameters::{ - defaults, user_layer, - user_layer::{CliContext, RootPartial}, + defaults, user, + user::{CliContext, RootPartial}, }, }; +/// Parsed configuration root #[derive(Debug, Clone)] +#[allow(missing_docs)] pub struct Root { - pub iroha: Iroha, + pub common: Common, pub genesis: Genesis, pub torii: Torii, pub kura: Kura, @@ -58,19 +61,23 @@ impl Root { } } +/// Common options shared between multiple places +#[allow(missing_docs)] #[derive(Debug, Clone)] -pub struct Iroha { +pub struct Common { pub chain_id: ChainId, pub key_pair: KeyPair, pub p2p_address: SocketAddr, } -impl Iroha { +impl Common { + /// Construct an id of this peer pub fn peer_id(&self) -> PeerId { PeerId::new(self.p2p_address.clone(), self.key_pair.public_key().clone()) } } +/// Parsed genesis configuration #[derive(Debug, Clone)] pub enum Genesis { /// The peer can only observe the genesis block @@ -88,6 +95,7 @@ pub enum Genesis { } impl Genesis { + /// Access the public key, which is always present in the genesis config pub fn public_key(&self) -> &PublicKey { match self { Self::Partial { public_key } => public_key, @@ -95,6 +103,7 @@ impl Genesis { } } + /// Access the key pair, if present pub fn key_pair(&self) -> Option<&KeyPair> { match self { Self::Partial { .. } => None, @@ -103,6 +112,7 @@ impl Genesis { } } +#[allow(missing_docs)] #[derive(Debug, Clone)] pub struct Kura { pub init_mode: Mode, @@ -122,12 +132,14 @@ impl Default for Queue { } #[derive(Debug, Clone)] +#[allow(missing_docs)] pub struct Sumeragi { pub trusted_peers: UniqueVec, pub debug_force_soft_fork: bool, } #[derive(Debug, Clone, Copy)] +#[allow(missing_docs)] pub struct LiveQueryStore { pub idle_time: Duration, } @@ -140,6 +152,7 @@ impl Default for LiveQueryStore { } } +#[allow(missing_docs)] #[derive(Debug, Clone, Copy)] pub struct BlockSync { pub gossip_period: Duration, @@ -147,12 +160,14 @@ pub struct BlockSync { } #[derive(Debug, Clone, Copy)] +#[allow(missing_docs)] pub struct TransactionGossiper { pub gossip_period: Duration, pub batch_size: NonZeroU32, } #[derive(Debug, Clone, Copy, Serialize, Deserialize)] +#[allow(missing_docs)] pub struct ChainWide { pub max_transactions_in_block: NonZeroU32, pub block_time: Duration, @@ -167,6 +182,7 @@ pub struct ChainWide { } impl ChainWide { + /// Calculate pipeline time based on the block time and commit time pub fn pipeline_time(&self) -> Duration { self.block_time + self.commit_time } @@ -189,43 +205,43 @@ impl Default for ChainWide { } } +#[allow(missing_docs)] #[derive(Debug, Clone, Copy, Serialize, Deserialize)] pub struct WasmRuntime { pub fuel_limit: u64, - pub max_memory: ByteSize, + // TODO: wrap into a `Bytes` newtype + pub max_memory_bytes: u32, } impl Default for WasmRuntime { fn default() -> Self { Self { fuel_limit: defaults::chain_wide::DEFAULT_WASM_FUEL_LIMIT, - max_memory: ByteSize(defaults::chain_wide::DEFAULT_WASM_MAX_MEMORY), + max_memory_bytes: defaults::chain_wide::DEFAULT_WASM_MAX_MEMORY_BYTES, } } } #[derive(Debug, Clone)] +#[allow(missing_docs)] pub struct Torii { pub address: SocketAddr, - pub max_content_len: ByteSize, + pub max_content_len_bytes: u64, } /// Complete configuration needed to start regular telemetry. #[derive(Debug, Clone)] +#[allow(missing_docs)] pub struct Telemetry { - #[allow(missing_docs)] pub name: String, - #[allow(missing_docs)] pub url: Url, - #[allow(missing_docs)] pub min_retry_period: Duration, - #[allow(missing_docs)] pub max_retry_delay_exponent: u8, } /// Complete configuration needed to start dev telemetry. #[derive(Debug, Clone)] +#[allow(missing_docs)] pub struct DevTelemetry { - #[allow(missing_docs)] pub file: PathBuf, } diff --git a/config/src/parameters/defaults.rs b/config/src/parameters/defaults.rs index 935294590b2..c82c13bfb8a 100644 --- a/config/src/parameters/defaults.rs +++ b/config/src/parameters/defaults.rs @@ -1,13 +1,14 @@ -//! Module with a set of default values. +//! Parameters default values + +// TODO: document if needed +#![allow(missing_docs)] use std::{ - num::{NonZeroU32, NonZeroU64, NonZeroUsize}, - ops::{Add, Div}, + num::{NonZeroU32, NonZeroUsize}, time::Duration, }; use iroha_data_model::{prelude::MetadataLimits, transaction::TransactionLimits, LengthLimits}; -use iroha_primitives::addr::{socket_addr, SocketAddr}; use nonzero_ext::nonzero; pub mod queue { @@ -15,19 +16,18 @@ pub mod queue { pub const DEFAULT_MAX_TRANSACTIONS_IN_QUEUE: NonZeroUsize = nonzero!(2_usize.pow(16)); pub const DEFAULT_MAX_TRANSACTIONS_IN_QUEUE_PER_USER: NonZeroUsize = nonzero!(2_usize.pow(16)); - pub const DEFAULT_TRANSACTION_TIME_TO_LIVE: Duration = Duration::from_secs(24 * 60 * 60); // 24 hours + pub const DEFAULT_TRANSACTION_TIME_TO_LIVE: Duration = Duration::from_secs(24 * 60 * 60); pub const DEFAULT_FUTURE_THRESHOLD: Duration = Duration::from_secs(1); } pub mod kura { - use super::*; - pub const DEFAULT_BLOCK_STORE_PATH: &str = "./storage"; } + +#[cfg(feature = "tokio-console")] pub mod logger { - use super::*; + use iroha_primitives::addr::{socket_addr, SocketAddr}; - #[cfg(feature = "tokio-console")] pub const DEFAULT_TOKIO_CONSOLE_ADDR: SocketAddr = socket_addr!(127.0.0.1:5555); } @@ -45,21 +45,22 @@ pub mod network { pub mod snapshot { use super::*; - // TODO: nest to `./storage/snapshot` for easier management - pub const DEFAULT_SNAPSHOT_PATH: &str = "./storage"; + pub const DEFAULT_SNAPSHOT_PATH: &str = "./storage/snapshot"; // Default frequency of making snapshots is 1 minute, need to be adjusted for larger world state view size pub const DEFAULT_SNAPSHOT_CREATE_EVERY_MS: Duration = Duration::from_secs(60); pub const DEFAULT_ENABLED: bool = true; } pub mod chain_wide { + use super::*; pub const DEFAULT_MAX_TXS: NonZeroU32 = nonzero!(2_u32.pow(9)); pub const DEFAULT_BLOCK_TIME: Duration = Duration::from_secs(2); pub const DEFAULT_COMMIT_TIME: Duration = Duration::from_secs(4); pub const DEFAULT_WASM_FUEL_LIMIT: u64 = 30_000_000; - pub const DEFAULT_WASM_MAX_MEMORY: u32 = 500 * 2_u32.pow(20); + // TODO: wrap into a `Bytes` newtype + pub const DEFAULT_WASM_MAX_MEMORY_BYTES: u32 = 500 * 2_u32.pow(20); /// Default estimation of consensus duration. pub const DEFAULT_CONSENSUS_ESTIMATION: Duration = diff --git a/config/src/parameters/mod.rs b/config/src/parameters/mod.rs index be1e61bd1d2..7a4e330ccc6 100644 --- a/config/src/parameters/mod.rs +++ b/config/src/parameters/mod.rs @@ -1,3 +1,5 @@ +//! Iroha configuration parameters on different layers and their default values. + pub mod actual; pub mod defaults; -pub mod user_layer; +pub mod user; diff --git a/config/src/parameters/user_layer.rs b/config/src/parameters/user.rs similarity index 71% rename from config/src/parameters/user_layer.rs rename to config/src/parameters/user.rs index 7a547676c98..fc54d043b6c 100644 --- a/config/src/parameters/user_layer.rs +++ b/config/src/parameters/user.rs @@ -1,18 +1,27 @@ +//! User configuration view. Contains structures in a format that is +//! convenient from the user perspective. It is less strict and not necessarily valid upon +//! successful parsing of the user-provided content. +//! +//! It begins with [`Root`], containing sub-modules. Every structure has its `-Partial` +//! representation (e.g. [`RootPartial`]). + +// This module's usage is documented in high detail in the Configuration Reference +// (TODO link to docs) +#![allow(missing_docs)] + use std::{ error::Error, fmt::Debug, - io::Read, num::{NonZeroU32, NonZeroUsize}, - ops::{Add, Div}, path::PathBuf, - str::FromStr, time::Duration, }; +pub use boilerplate::*; use eyre::{eyre, Report, WrapErr}; use iroha_config_base::{ - ByteSize, Emitter, ErrorsCollection, FromEnv, FromEnvDefaultFallback, Merge, ParseEnvResult, - ReadEnv, UnwrapPartial, UnwrapPartialResult, + Emitter, ErrorsCollection, HumanBytes, Merge, ParseEnvResult, ReadEnv, UnwrapPartial, + UnwrapPartialResult, }; use iroha_crypto::{KeyPair, PrivateKey, PublicKey}; use iroha_data_model::{ @@ -26,72 +35,16 @@ use url::Url; use crate::{ kura::Mode, logger::Format, - parameters::{ - actual, - defaults::{logger::*, telemetry::*}, - }, + parameters::{actual, defaults::telemetry::*}, }; mod boilerplate; -pub use boilerplate::*; - -#[derive(Debug, Default, Serialize, Deserialize, Eq, PartialEq)] -#[serde(untagged)] -pub enum ExtendsPaths { - #[default] - None, - Single(PathBuf), - Multiple(Vec), -} - -impl Merge for ExtendsPaths { - fn merge(&mut self, other: Self) { - match (self, other) { - (Self::None, Self::None) => {} - _ => unreachable!( - "It is a bug. `ExtendsPaths` should be resolved to `None` before merging." - ), - } - } -} - -pub enum ExtendsPathsIter<'a> { - None, - Single(Option<&'a PathBuf>), - Multiple(std::slice::Iter<'a, PathBuf>), -} - -impl ExtendsPaths { - #[allow(clippy::iter_without_into_iter)] // extra for this case - pub fn iter(&self) -> ExtendsPathsIter<'_> { - match &self { - Self::None => ExtendsPathsIter::None, - Self::Single(x) => ExtendsPathsIter::Single(Some(x)), - Self::Multiple(vec) => ExtendsPathsIter::Multiple(vec.iter()), - } - } - - /// Marks this instance as used, so that subsequent [`Merge`] doesn't fail - pub fn used(&mut self) { - *self = Self::None - } -} - -impl<'a> Iterator for ExtendsPathsIter<'a> { - type Item = &'a PathBuf; - - fn next(&mut self) -> Option { - match self { - Self::None => None, - Self::Single(x) => x.take(), - Self::Multiple(iter) => iter.next(), - } - } -} #[derive(Debug)] pub struct Root { - iroha: Iroha, + chain_id: ChainId, + public_key: PublicKey, + private_key: PrivateKey, genesis: Genesis, kura: Kura, sumeragi: Sumeragi, @@ -108,13 +61,13 @@ impl Root { pub fn parse(self, cli: CliContext) -> Result> { let mut emitter = Emitter::new(); - let iroha = self.iroha.parse().map_or_else( - |err| { - emitter.emit(err); - None - }, - Some, - ); + let key_pair = + KeyPair::new(self.public_key, self.private_key) + .wrap_err("failed to construct a key pair from `iroha.public_key` and `iroha.private_key` configuration parameters") + .map_or_else(|err| { + emitter.emit(err); + None + }, Some); let genesis = self.genesis.parse(cli).map_or_else( |err| { @@ -127,28 +80,27 @@ impl Root { let kura = self.kura.parse(); - let sumeragi = match self.sumeragi.parse() { - Ok(mut sumeragi) => { - if !cli.submit_genesis && sumeragi.trusted_peers.len() == 0 { - emitter.emit(eyre!("\ - The network consists from this one peer only (no `sumeragi.trusted_peers` provided). \ - 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.\ - ")); - None - } else { - Some(sumeragi) - } - } - Err(err) => { + let sumeragi = self.sumeragi.parse().map_or_else( + |err| { emitter.emit(err); None + }, + Some, + ); + + if let Some(ref config) = sumeragi { + if !cli.submit_genesis && config.trusted_peers.len() == 0 { + emitter.emit(eyre!("\ + The network consists from this one peer only (no `sumeragi.trusted_peers` provided). \ + 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 (block_sync, transaction_gossiper) = self.network.parse(); + let (p2p_address, block_sync, transaction_gossiper) = self.network.parse(); let logger = self.logger; let queue = self.queue; @@ -166,29 +118,29 @@ impl Root { let chain_wide = self.chain_wide.parse(); - if let Some(iroha) = &iroha { - if iroha.p2p_address == torii.address { - emitter.emit(eyre!( - "`iroha.p2p_address` and `torii.address` should not be the same" - )) - } + if p2p_address == torii.address { + emitter.emit(eyre!( + "`iroha.p2p_address` and `torii.address` should not be the same" + )) } emitter.finish()?; - let (regular_telemetry, dev_telemetry) = telemetries.unwrap(); - let iroha = iroha.unwrap(); + let peer = actual::Common { + chain_id: self.chain_id, + key_pair: key_pair.unwrap(), + p2p_address, + }; + let (telemetry, dev_telemetry) = telemetries.unwrap(); let genesis = genesis.unwrap(); let sumeragi = { - let mut cfg = sumeragi.unwrap(); - cfg.trusted_peers.push(iroha.peer_id()); - cfg + let mut x = sumeragi.unwrap(); + x.trusted_peers.push(peer.peer_id()); + x }; - // TODO: validate that p2p_address and torii.address are not the same - Ok(actual::Root { - iroha, + common: peer, genesis, torii, kura, @@ -199,7 +151,7 @@ impl Root { logger, queue, snapshot, - telemetry: regular_telemetry, + telemetry, dev_telemetry, chain_wide, }) @@ -211,26 +163,6 @@ pub struct CliContext { pub submit_genesis: bool, } -#[derive(Debug)] -pub struct Iroha { - pub chain_id: ChainId, - pub public_key: PublicKey, - pub private_key: PrivateKey, - pub p2p_address: SocketAddr, -} - -impl Iroha { - fn parse(self) -> Result { - let key_pair = KeyPair::new(self.public_key, self.private_key).wrap_err("failed to construct a key pair from `iroha.public_key` and `iroha.private_key` configuration parameters")?; - - Ok(actual::Iroha { - chain_id: self.chain_id, - key_pair, - p2p_address: self.p2p_address, - }) - } -} - pub(crate) fn private_key_from_env( emitter: &mut Emitter, env: &impl ReadEnv, @@ -244,16 +176,17 @@ pub(crate) fn private_key_from_env( let digest_function = ParseEnvResult::parse_simple(emitter, env, &digest_env, &digest_name); + // FIXME: errors handling is a mess let payload = match env - .get(&payload_env) - .map_err(|err| eyre!("{err}")) + .read_env(&payload_env) + .map_err(|err| eyre!("failed to read {payload_name}: {err}")) .wrap_err("oops") { Ok(Some(value)) => ParseEnvResult::Value(value), Ok(None) => ParseEnvResult::None, Err(err) => { emitter.emit(err); - ParseEnvResult::ParseError + ParseEnvResult::Error } }; @@ -286,13 +219,13 @@ pub(crate) fn private_key_from_env( &digest_env )); } - (ParseEnvResult::ParseError, _) | (_, ParseEnvResult::ParseError) => { + (ParseEnvResult::Error, _) | (_, ParseEnvResult::Error) => { // emitter already has these errors // adding this branch for exhaustiveness } } - ParseEnvResult::ParseError + ParseEnvResult::Error } #[derive(Debug)] @@ -358,7 +291,7 @@ impl Kura { } } -#[derive(Debug)] +#[derive(Debug, Copy, Clone)] pub struct KuraDebug { output_new_blocks: bool, } @@ -385,7 +318,7 @@ impl Sumeragi { } } -#[derive(Debug)] +#[derive(Debug, Copy, Clone)] pub struct SumeragiDebug { pub force_soft_fork: bool, } @@ -424,8 +357,10 @@ fn construct_unique_vec( Ok(unique) } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Network { + /// Peer-to-peer address + pub address: SocketAddr, pub block_gossip_period: Duration, pub max_blocks_per_gossip: NonZeroU32, pub max_transactions_per_gossip: NonZeroU32, @@ -433,8 +368,9 @@ pub struct Network { } impl Network { - fn parse(self) -> (actual::BlockSync, actual::TransactionGossiper) { + fn parse(self) -> (SocketAddr, actual::BlockSync, actual::TransactionGossiper) { let Self { + address, max_blocks_per_gossip, max_transactions_per_gossip, block_gossip_period, @@ -442,6 +378,7 @@ impl Network { } = self; ( + address, actual::BlockSync { gossip_period: block_gossip_period, batch_size: max_blocks_per_gossip, @@ -467,6 +404,7 @@ pub struct Queue { pub future_threshold: Duration, } +#[allow(missing_copy_implementations)] // triggered without tokio-console #[derive(Debug, Clone)] pub struct Logger { /// Level of logging verbosity @@ -488,7 +426,7 @@ impl Default for Logger { level: Level::default(), format: Format::default(), #[cfg(feature = "tokio-console")] - tokio_console_addr: DEFAULT_TOKIO_CONSOLE_ADDR, + tokio_console_addr: super::defaults::logger::DEFAULT_TOKIO_CONSOLE_ADDR, } } } @@ -550,7 +488,7 @@ pub struct Snapshot { pub creation_enabled: bool, } -#[derive(Debug)] +#[derive(Debug, Copy, Clone)] pub struct ChainWide { pub max_transactions_in_block: NonZeroU32, pub block_time: Duration, @@ -562,7 +500,7 @@ pub struct ChainWide { pub domain_metadata_limits: MetadataLimits, pub identifier_length_limits: LengthLimits, pub wasm_fuel_limit: u64, - pub wasm_max_memory: ByteSize, + pub wasm_max_memory: HumanBytes, } impl ChainWide { @@ -593,7 +531,7 @@ impl ChainWide { ident_length_limits: identifier_length_limits, wasm_runtime: actual::WasmRuntime { fuel_limit: wasm_fuel_limit, - max_memory: wasm_max_memory, + max_memory_bytes: wasm_max_memory.get(), }, } } @@ -602,7 +540,7 @@ impl ChainWide { #[derive(Debug)] pub struct Torii { pub address: SocketAddr, - pub max_content_len: ByteSize, + pub max_content_len: HumanBytes, pub query_idle_time: Duration, } @@ -610,7 +548,7 @@ impl Torii { fn parse(self) -> (actual::Torii, actual::LiveQueryStore) { let torii = actual::Torii { address: self.address, - max_content_len: self.max_content_len, + max_content_len_bytes: self.max_content_len.get(), }; let query = actual::LiveQueryStore { @@ -625,8 +563,7 @@ impl Torii { mod tests { use iroha_config_base::{FromEnv, TestEnv}; - use super::*; - use crate::parameters::user_layer::boilerplate::{IrohaPartial, RootPartial}; + use super::super::user::boilerplate::RootPartial; #[test] fn parses_private_key_from_env() { @@ -634,7 +571,7 @@ mod tests { .set("PRIVATE_KEY_DIGEST", "ed25519") .set("PRIVATE_KEY_PAYLOAD", "8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb"); - let private_key = IrohaPartial::from_env(&env) + let private_key = RootPartial::from_env(&env) .expect("input is valid, should not fail") .private_key .get() @@ -648,7 +585,7 @@ mod tests { fn fails_to_parse_private_key_in_env_without_digest() { let env = TestEnv::new().set("PRIVATE_KEY_DIGEST", "ed25519"); let error = - IrohaPartial::from_env(&env).expect_err("private key is incomplete, should fail"); + RootPartial::from_env(&env).expect_err("private key is incomplete, should fail"); let expected = expect_test::expect![ "`PRIVATE_KEY_DIGEST` env was provided, but `PRIVATE_KEY_PAYLOAD` was not" ]; @@ -659,7 +596,7 @@ mod tests { fn fails_to_parse_private_key_in_env_without_payload() { let env = TestEnv::new().set("PRIVATE_KEY_PAYLOAD", "8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb"); let error = - IrohaPartial::from_env(&env).expect_err("private key is incomplete, should fail"); + RootPartial::from_env(&env).expect_err("private key is incomplete, should fail"); let expected = expect_test::expect![ "`PRIVATE_KEY_PAYLOAD` env was provided, but `PRIVATE_KEY_DIGEST` was not" ]; @@ -672,7 +609,7 @@ mod tests { .set("PRIVATE_KEY_DIGEST", "ed25519") .set("PRIVATE_KEY_PAYLOAD", "foo"); - let error = IrohaPartial::from_env(&env).expect_err("input is invalid, should fail"); + let error = RootPartial::from_env(&env).expect_err("input is invalid, should fail"); let expected = expect_test::expect!["failed to construct `iroha.private_key` from `PRIVATE_KEY_DIGEST` and `PRIVATE_KEY_PAYLOAD` environment variables"]; expected.assert_eq(&format!("{error:#}")); @@ -684,7 +621,7 @@ mod tests { .set("PRIVATE_KEY_DIGEST", "foo") .set("PRIVATE_KEY_PAYLOAD", "8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb"); - let error = IrohaPartial::from_env(&env).expect_err("input is invalid, should fail"); + let error = RootPartial::from_env(&env).expect_err("input is invalid, should fail"); // TODO: print the bad value and supported ones let expected = expect_test::expect!["failed to parse `iroha.private_key.digest_function` field from `PRIVATE_KEY_DIGEST` env variable"]; @@ -697,68 +634,12 @@ mod tests { } #[test] - fn deserialize_iroha_namespace_with_not_all_fields_works() { + fn deserialize_network_namespace_with_not_all_fields_works() { let _layer: RootPartial = toml::toml! { - [iroha] - p2p_address = "127.0.0.1:8080" + [network] + address = "127.0.0.1:8080" } .try_into() - .expect("should not fail when not all fields in `iroha` are presented at a time"); - } - - #[derive(Deserialize, Default)] - #[serde(default)] - struct TestExtends { - extends: ExtendsPaths, - } - - #[test] - fn parse_empty_extends() { - let value: TestExtends = toml::from_str("").expect("should be fine with empty input"); - - assert_eq!(value.extends, ExtendsPaths::None); - } - - #[test] - fn parse_single_extends_path() { - let value: TestExtends = toml::toml! { - extends = "./path" - } - .try_into() - .unwrap(); - - assert_eq!(value.extends, ExtendsPaths::Single("./path".into())); - } - - #[test] - fn parse_multiple_extends_paths() { - let value: TestExtends = toml::toml! { - extends = ["foo", "bar", "baz"] - } - .try_into() - .unwrap(); - - assert_eq!( - value.extends, - ExtendsPaths::Multiple(vec!["foo".into(), "bar".into(), "baz".into()]) - ); - } - - #[test] - fn iterating_over_extends() { - impl ExtendsPaths { - fn as_str_vec(&self) -> Vec<&str> { - self.iter().map(|p| p.to_str().unwrap()).collect() - } - } - - let empty = ExtendsPaths::None; - assert_eq!(empty.as_str_vec(), Vec::<&str>::new()); - - let single = ExtendsPaths::Single("single".into()); - assert_eq!(single.as_str_vec(), vec!["single"]); - - let multi = ExtendsPaths::Multiple(vec!["foo".into(), "bar".into(), "baz".into()]); - assert_eq!(multi.as_str_vec(), vec!["foo", "bar", "baz"]); + .expect("should not fail when not all fields in `network` are presented at a time"); } } diff --git a/config/src/parameters/user_layer/boilerplate.rs b/config/src/parameters/user/boilerplate.rs similarity index 86% rename from config/src/parameters/user_layer/boilerplate.rs rename to config/src/parameters/user/boilerplate.rs index 0eecaac9f57..1e2b502afb0 100644 --- a/config/src/parameters/user_layer/boilerplate.rs +++ b/config/src/parameters/user/boilerplate.rs @@ -1,5 +1,7 @@ //! Code that should be generated by a procmacro in future. +#![allow(missing_docs)] + use std::{ error::Error, fs::File, @@ -10,9 +12,9 @@ use std::{ use eyre::{eyre, Report, WrapErr}; use iroha_config_base::{ - ByteSize, Emitter, ErrorsCollection, FromEnv, FromEnvDefaultFallback, FromEnvResult, Merge, - MissingFieldError, ParseEnvResult, ReadEnv, UnwrapPartial, UnwrapPartialResult, UserDuration, - UserField, + Emitter, ErrorsCollection, ExtendsPaths, FromEnv, FromEnvDefaultFallback, FromEnvResult, + HumanBytes, HumanDuration, Merge, MissingFieldError, ParseEnvResult, ReadEnv, UnwrapPartial, + UnwrapPartialResult, UserField, }; use iroha_crypto::{PrivateKey, PublicKey}; use iroha_data_model::{ @@ -27,13 +29,11 @@ use crate::{ kura::Mode, logger::Format, parameters::{ - defaults::{ - chain_wide::*, kura::*, logger::*, network::*, queue::*, snapshot::*, torii::*, - }, - user_layer, - user_layer::{ - ChainWide, ExtendsPaths, Genesis, Iroha, Kura, KuraDebug, Logger, Network, Queue, Root, - Snapshot, Sumeragi, SumeragiDebug, Telemetry, TelemetryDev, Torii, UserTrustedPeers, + defaults::{chain_wide::*, kura::*, network::*, queue::*, snapshot::*, torii::*}, + user, + user::{ + ChainWide, Genesis, Kura, KuraDebug, Logger, Network, Queue, Root, Snapshot, Sumeragi, + SumeragiDebug, Telemetry, TelemetryDev, Torii, UserTrustedPeers, }, }, }; @@ -41,8 +41,10 @@ use crate::{ #[derive(Deserialize, Serialize, Debug, Default, Merge)] #[serde(deny_unknown_fields, default)] pub struct RootPartial { - pub extends: ExtendsPaths, - pub iroha: IrohaPartial, + pub extends: Option, + pub chain_id: UserField, + pub public_key: UserField, + pub private_key: UserField, pub genesis: GenesisPartial, pub kura: KuraPartial, pub sumeragi: SumeragiPartial, @@ -80,9 +82,8 @@ impl RootPartial { layer.normalise_paths(base_path); - if let Some(base) = - layer - .extends + if let Some(paths) = layer.extends.take() { + let base = paths .iter() .try_fold(None, |acc: Option, extends_path| { // extends path is not normalised relative to the config file yet @@ -95,11 +96,11 @@ impl RootPartial { None => Ok::, Report>(Some(base)), Some(other_base) => Ok(Some(other_base.merge(base))), } - })? - { - layer.extends.used(); - layer = base.merge(layer); - }; + })?; + if let Some(base) = base { + layer = base.merge(layer) + }; + } Ok(layer) } @@ -148,7 +149,16 @@ impl UnwrapPartial for RootPartial { }; } - let iroha = nested!(self.iroha); + if self.chain_id.is_none() { + emitter.emit_missing_field("chain_id"); + } + if self.public_key.is_none() { + emitter.emit_missing_field("public_key"); + } + if self.private_key.is_none() { + emitter.emit_missing_field("private_key"); + } + let genesis = nested!(self.genesis); let kura = nested!(self.kura); let sumeragi = nested!(self.sumeragi); @@ -163,7 +173,9 @@ impl UnwrapPartial for RootPartial { emitter.finish()?; Ok(Root { - iroha: iroha.unwrap(), + chain_id: self.chain_id.get().unwrap(), + public_key: self.public_key.get().unwrap(), + private_key: self.private_key.get().unwrap(), genesis: genesis.unwrap(), kura: kura.unwrap(), sumeragi: sumeragi.unwrap(), @@ -197,7 +209,25 @@ impl FromEnv for RootPartial { let mut emitter = Emitter::new(); - let iroha = from_env_nested(env, &mut emitter); + let chain_id = env + .read_env("CHAIN_ID") + .map_err(|e| eyre!("{e}")) + .wrap_err("failed to read CHAIN_ID field (iroha.chain_id param)") + .map_or_else( + |err| { + emitter.emit(err); + None + }, + |maybe_value| maybe_value.map(ChainId::from), + ) + .into(); + let public_key = + ParseEnvResult::parse_simple(&mut emitter, env, "PUBLIC_KEY", "iroha.public_key") + .into(); + let private_key = + user::private_key_from_env(&mut emitter, env, "PRIVATE_KEY", "iroha.private_key") + .into(); + let genesis = from_env_nested(env, &mut emitter); let kura = from_env_nested(env, &mut emitter); let sumeragi = from_env_nested(env, &mut emitter); @@ -212,8 +242,10 @@ impl FromEnv for RootPartial { emitter.finish()?; Ok(Self { - extends: ExtendsPaths::None, - iroha: iroha.unwrap(), + extends: None, + chain_id, + public_key, + private_key, genesis: genesis.unwrap(), kura: kura.unwrap(), sumeragi: sumeragi.unwrap(), @@ -228,85 +260,6 @@ impl FromEnv for RootPartial { } } -#[derive(Deserialize, Serialize, Debug, Default, Merge)] -#[serde(deny_unknown_fields, default)] -pub struct IrohaPartial { - pub chain_id: UserField, - pub public_key: UserField, - pub private_key: UserField, - pub p2p_address: UserField, -} - -impl UnwrapPartial for IrohaPartial { - type Output = Iroha; - - fn unwrap_partial(self) -> UnwrapPartialResult { - let mut emitter = Emitter::new(); - - if self.chain_id.is_none() { - emitter.emit_missing_field("iroha.chain_id"); - } - if self.public_key.is_none() { - emitter.emit_missing_field("iroha.public_key"); - } - if self.private_key.is_none() { - emitter.emit_missing_field("iroha.private_key"); - } - if self.p2p_address.is_none() { - emitter.emit_missing_field("iroha.p2p_address"); - } - - emitter.finish()?; - - Ok(Iroha { - chain_id: self.chain_id.get().unwrap(), - public_key: self.public_key.get().unwrap(), - private_key: self.private_key.get().unwrap(), - p2p_address: self.p2p_address.get().unwrap(), - }) - } -} - -impl FromEnv for IrohaPartial { - fn from_env>(env: &R) -> FromEnvResult - where - Self: Sized, - { - let mut emitter = Emitter::new(); - - let chain_id = env - .get("CHAIN_ID") - .map_err(|e| eyre!("{e}")) - .wrap_err("failed to read CHAIN_ID field (iroha.chain_id param)") - .map_or_else( - |err| { - emitter.emit(err); - None - }, - |maybe_value| maybe_value.map(ChainId::from), - ) - .into(); - let public_key = - ParseEnvResult::parse_simple(&mut emitter, env, "PUBLIC_KEY", "iroha.public_key") - .into(); - let private_key = - user_layer::private_key_from_env(&mut emitter, env, "PRIVATE_KEY", "iroha.private_key") - .into(); - let p2p_address = - ParseEnvResult::parse_simple(&mut emitter, env, "P2P_ADDRESS", "iroha.p2p_address") - .into(); - - emitter.finish()?; - - Ok(Self { - chain_id, - public_key, - private_key, - p2p_address, - }) - } -} - #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Default, Merge)] #[serde(deny_unknown_fields, default)] pub struct GenesisPartial { @@ -349,7 +302,7 @@ impl FromEnv for GenesisPartial { "genesis.public_key", ) .into(); - let private_key = user_layer::private_key_from_env( + let private_key = user::private_key_from_env( &mut emitter, env, "GENESIS_PRIVATE_KEY", @@ -521,24 +474,30 @@ impl FromEnvDefaultFallback for SumeragiPartial {} #[derive(Deserialize, Serialize, Debug, Default, Merge)] #[serde(deny_unknown_fields, default)] pub struct NetworkPartial { - pub block_gossip_period: UserField, + pub address: UserField, + pub block_gossip_period: UserField, pub max_blocks_per_gossip: UserField, pub max_transactions_per_gossip: UserField, - pub transaction_gossip_period: UserField, + pub transaction_gossip_period: UserField, } impl UnwrapPartial for NetworkPartial { type Output = Network; fn unwrap_partial(self) -> UnwrapPartialResult { + if self.address.is_none() { + return Err(MissingFieldError::new("network.address").into()); + } + Ok(Network { + address: self.address.get().unwrap(), block_gossip_period: self .block_gossip_period - .map(UserDuration::get) + .map(HumanDuration::get) .unwrap_or(DEFAULT_BLOCK_GOSSIP_PERIOD), transaction_gossip_period: self .transaction_gossip_period - .map(UserDuration::get) + .map(HumanDuration::get) .unwrap_or(DEFAULT_TRANSACTION_GOSSIP_PERIOD), max_transactions_per_gossip: self .max_transactions_per_gossip @@ -552,7 +511,26 @@ impl UnwrapPartial for NetworkPartial { } } -impl FromEnvDefaultFallback for NetworkPartial {} +impl FromEnv for NetworkPartial { + fn from_env>(env: &R) -> FromEnvResult + where + Self: Sized, + { + let mut emitter = Emitter::new(); + + // TODO: also parse `NETWORK_ADDRESS`? + let address = + ParseEnvResult::parse_simple(&mut emitter, env, "P2P_ADDRESS", "network.address") + .into(); + + emitter.finish()?; + + Ok(Self { + address, + ..Self::default() + }) + } +} #[derive(Deserialize, Serialize, Debug, Default, Merge)] #[serde(deny_unknown_fields, default)] @@ -563,9 +541,9 @@ pub struct QueuePartial { /// Use this option to apply throttling. pub capacity_per_user: UserField, /// The transaction will be dropped after this time if it is still in the queue. - pub transaction_time_to_live: UserField, + pub transaction_time_to_live: UserField, /// The threshold to determine if a transaction has been tampered to have a future timestamp. - pub future_threshold: UserField, + pub future_threshold: UserField, } impl UnwrapPartial for QueuePartial { @@ -579,10 +557,10 @@ impl UnwrapPartial for QueuePartial { .unwrap_or(DEFAULT_MAX_TRANSACTIONS_IN_QUEUE), transaction_time_to_live: self .transaction_time_to_live - .map_or(DEFAULT_TRANSACTION_TIME_TO_LIVE, UserDuration::get), + .map_or(DEFAULT_TRANSACTION_TIME_TO_LIVE, HumanDuration::get), future_threshold: self .future_threshold - .map_or(DEFAULT_FUTURE_THRESHOLD, UserDuration::get), + .map_or(DEFAULT_FUTURE_THRESHOLD, HumanDuration::get), }) } } @@ -612,10 +590,9 @@ impl UnwrapPartial for LoggerPartial { level: self.level.unwrap_or_default(), format: self.format.unwrap_or_default(), #[cfg(feature = "tokio-console")] - tokio_console_addr: self - .tokio_console_addr - .get() - .unwrap_or_else(|| DEFAULT_TOKIO_CONSOLE_ADDR.clone()), + tokio_console_addr: self.tokio_console_addr.get().unwrap_or_else(|| { + super::super::defaults::logger::DEFAULT_TOKIO_CONSOLE_ADDR.clone() + }), }) } } @@ -648,7 +625,7 @@ impl FromEnv for LoggerPartial { pub struct TelemetryPartial { pub name: UserField, pub url: UserField, - pub min_retry_period: UserField, + pub min_retry_period: UserField, pub max_retry_delay_exponent: UserField, pub dev: TelemetryDevPartial, } @@ -685,7 +662,7 @@ impl UnwrapPartial for TelemetryPartial { name: name.get(), url: url.get(), max_retry_delay_exponent: max_retry_delay_exponent.get(), - min_retry_period: min_retry_period.get().map(UserDuration::get), + min_retry_period: min_retry_period.get().map(HumanDuration::get), dev: dev.unwrap_partial()?, }) } @@ -696,7 +673,7 @@ impl FromEnvDefaultFallback for TelemetryPartial {} #[derive(Debug, Clone, Deserialize, Serialize, Default, Merge)] #[serde(deny_unknown_fields, default)] pub struct SnapshotPartial { - pub create_every: UserField, + pub create_every: UserField, pub store_path: UserField, pub creation_enabled: UserField, } @@ -710,7 +687,7 @@ impl UnwrapPartial for SnapshotPartial { create_every: self .create_every .get() - .map_or(DEFAULT_SNAPSHOT_CREATE_EVERY_MS, UserDuration::get), + .map_or(DEFAULT_SNAPSHOT_CREATE_EVERY_MS, HumanDuration::get), store_path: self .store_path .get() @@ -755,8 +732,8 @@ impl FromEnv for SnapshotPartial { #[serde(deny_unknown_fields, default)] pub struct ChainWidePartial { pub max_transactions_in_block: UserField, - pub block_time: UserField, - pub commit_time: UserField, + pub block_time: UserField, + pub commit_time: UserField, pub transaction_limits: UserField, pub asset_metadata_limits: UserField, pub asset_definition_metadata_limits: UserField, @@ -764,7 +741,7 @@ pub struct ChainWidePartial { pub domain_metadata_limits: UserField, pub identifier_length_limits: UserField, pub wasm_fuel_limit: UserField, - pub wasm_max_memory: UserField>, + pub wasm_max_memory: UserField>, } impl UnwrapPartial for ChainWidePartial { @@ -775,10 +752,10 @@ impl UnwrapPartial for ChainWidePartial { max_transactions_in_block: self.max_transactions_in_block.unwrap_or(DEFAULT_MAX_TXS), block_time: self .block_time - .map_or(DEFAULT_BLOCK_TIME, UserDuration::get), + .map_or(DEFAULT_BLOCK_TIME, HumanDuration::get), commit_time: self .commit_time - .map_or(DEFAULT_COMMIT_TIME, UserDuration::get), + .map_or(DEFAULT_COMMIT_TIME, HumanDuration::get), transaction_limits: self .transaction_limits .unwrap_or(DEFAULT_TRANSACTION_LIMITS), @@ -800,7 +777,7 @@ impl UnwrapPartial for ChainWidePartial { wasm_fuel_limit: self.wasm_fuel_limit.unwrap_or(DEFAULT_WASM_FUEL_LIMIT), wasm_max_memory: self .wasm_max_memory - .unwrap_or(ByteSize(DEFAULT_WASM_MAX_MEMORY)), + .unwrap_or(HumanBytes(DEFAULT_WASM_MAX_MEMORY_BYTES)), }) } } @@ -811,8 +788,8 @@ impl FromEnvDefaultFallback for ChainWidePartial {} #[serde(deny_unknown_fields, default)] pub struct ToriiPartial { pub address: UserField, - pub max_content_len: UserField>, - pub query_idle_time: UserField, + pub max_content_len: UserField>, + pub query_idle_time: UserField, } impl UnwrapPartial for ToriiPartial { @@ -828,11 +805,11 @@ impl UnwrapPartial for ToriiPartial { let max_content_len = self .max_content_len .get() - .unwrap_or(ByteSize(DEFAULT_MAX_CONTENT_LENGTH)); + .unwrap_or(HumanBytes(DEFAULT_MAX_CONTENT_LENGTH)); let query_idle_time = self .query_idle_time - .map(UserDuration::get) + .map(HumanDuration::get) .unwrap_or(DEFAULT_QUERY_IDLE_TIME); emitter.finish()?; diff --git a/config/tests/fixtures.rs b/config/tests/fixtures.rs index fcc35ea687a..26ac08959a3 100644 --- a/config/tests/fixtures.rs +++ b/config/tests/fixtures.rs @@ -5,7 +5,7 @@ use std::{ }; use eyre::Result; -use iroha_config::parameters::user_layer::{CliContext, RootPartial}; +use iroha_config::parameters::user::{CliContext, RootPartial}; use iroha_config_base::{FromEnv, TestEnv, UnwrapPartial as _}; fn fixtures_dir() -> PathBuf { @@ -47,7 +47,7 @@ fn minimal_config_snapshot() -> Result<()> { let expected = expect_test::expect![[r#" Root { - iroha: Iroha { + common: Common { chain_id: ChainId( "0", ), @@ -62,9 +62,7 @@ fn minimal_config_snapshot() -> Result<()> { }, torii: Torii { address: 127.0.0.1:8080, - max_content_len: ByteSize( - 16777216, - ), + max_content_len_bytes: 16777216, }, kura: Kura { init_mode: Strict, @@ -106,7 +104,7 @@ fn minimal_config_snapshot() -> Result<()> { }, snapshot: Snapshot { create_every: 60s, - store_path: "./storage", + store_path: "./storage/snapshot", creation_enabled: true, }, telemetry: None, @@ -141,9 +139,7 @@ fn minimal_config_snapshot() -> Result<()> { }, wasm_runtime: WasmRuntime { fuel_limit: 23000000, - max_memory: ByteSize( - 524288000, - ), + max_memory_bytes: 524288000, }, }, }"#]]; @@ -207,7 +203,7 @@ fn self_is_presented_in_trusted_peers() -> Result<()> { assert!(config .sumeragi .trusted_peers - .contains(&config.iroha.peer_id())); + .contains(&config.common.peer_id())); Ok(()) } @@ -219,11 +215,11 @@ fn missing_fields() -> Result<()> { .expect_err("should fail with missing fields"); let expected = expect_test::expect![[r#" - missing field: `iroha.chain_id` - missing field: `iroha.public_key` - missing field: `iroha.private_key` - missing field: `iroha.p2p_address` + missing field: `chain_id` + missing field: `public_key` + missing field: `private_key` missing field: `genesis.public_key` + missing field: `network.address` missing field: `torii.address`"#]]; expected.assert_eq(&format!("{error:#}")); @@ -270,22 +266,17 @@ fn full_envs_set_is_consumed() -> Result<()> { let expected = expect_test::expect![[r#" RootPartial { extends: None, - iroha: IrohaPartial { - chain_id: Some( - ChainId( - "0-0", - ), - ), - public_key: Some( - {digest: ed25519, payload: ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB}, - ), - private_key: Some( - {digest: ed25519, payload: 8F4C15E5D664DA3F13778801D23D4E89B76E94C1B94B389544168B6CB894F84F8BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB}, + chain_id: Some( + ChainId( + "0-0", ), - p2p_address: Some( - 127.0.0.1:5432, - ), - }, + ), + public_key: Some( + {digest: ed25519, payload: ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB}, + ), + private_key: Some( + {digest: ed25519, payload: 8F4C15E5D664DA3F13778801D23D4E89B76E94C1B94B389544168B6CB894F84F8BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB}, + ), genesis: GenesisPartial { public_key: Some( {digest: ed25519, payload: ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB}, @@ -317,6 +308,9 @@ fn full_envs_set_is_consumed() -> Result<()> { }, }, network: NetworkPartial { + address: Some( + 127.0.0.1:5432, + ), block_gossip_period: None, max_blocks_per_gossip: None, max_transactions_per_gossip: None, diff --git a/config/tests/fixtures/bad.torii_addr_eq_p2p_addr.toml b/config/tests/fixtures/bad.torii_addr_eq_p2p_addr.toml index ae1c04c7624..79f9c324cee 100644 --- a/config/tests/fixtures/bad.torii_addr_eq_p2p_addr.toml +++ b/config/tests/fixtures/bad.torii_addr_eq_p2p_addr.toml @@ -1,7 +1,7 @@ extends = ["base.toml", "base_trusted_peers.toml"] -[iroha] -p2p_address = "127.0.0.1:8080" +[network] +address = "127.0.0.1:8080" [torii] address = "127.0.0.1:8080" diff --git a/config/tests/fixtures/base.toml b/config/tests/fixtures/base.toml index 2760b625dcc..3ca6d219477 100644 --- a/config/tests/fixtures/base.toml +++ b/config/tests/fixtures/base.toml @@ -1,9 +1,10 @@ -[iroha] chain_id = "0" public_key = "ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB" private_key.digest_function = "ed25519" private_key.payload = "8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb" -p2p_address = "127.0.0.1:1337" + +[network] +address = "127.0.0.1:1337" [genesis] public_key = "ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB" diff --git a/config/tests/fixtures/minimal_file_and_env.toml b/config/tests/fixtures/minimal_file_and_env.toml index f3406882248..abdd50e85c2 100644 --- a/config/tests/fixtures/minimal_file_and_env.toml +++ b/config/tests/fixtures/minimal_file_and_env.toml @@ -1,12 +1,13 @@ extends = "base_trusted_peers.toml" -[iroha] chain_id = "0" public_key = "ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB" -p2p_address = "127.0.0.1:1337" private_key.digest_function = "ed25519" private_key.payload = "8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb" +[network] +address = "127.0.0.1:1337" + [genesis] public_key = "ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB" diff --git a/core/benches/blocks/common.rs b/core/benches/blocks/common.rs index eb396b7fcdf..4f9018eb168 100644 --- a/core/benches/blocks/common.rs +++ b/core/benches/blocks/common.rs @@ -186,7 +186,7 @@ pub fn build_wsv( let mut wsv = WorldStateView::new(World::with([domain], UniqueVec::new()), kura, query_handle); wsv.config.transaction_limits = TransactionLimits::new(u64::MAX, u64::MAX); wsv.config.wasm_runtime.fuel_limit = u64::MAX; - wsv.config.wasm_runtime.max_memory = u32::MAX.into(); + wsv.config.wasm_runtime.max_memory_bytes = u32::MAX.into(); { let path_to_executor = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) diff --git a/core/src/kiso.rs b/core/src/kiso.rs index d3dc2e76483..ca93ae0819c 100644 --- a/core/src/kiso.rs +++ b/core/src/kiso.rs @@ -158,7 +158,7 @@ mod tests { use super::*; fn test_config() -> Root { - use iroha_config::parameters::user_layer::CliContext; + use iroha_config::parameters::user::CliContext; Root::load( // FIXME Specifying path here might break! diff --git a/core/src/smartcontracts/wasm.rs b/core/src/smartcontracts/wasm.rs index 56a3788ac9e..c7d9c6fa84d 100644 --- a/core/src/smartcontracts/wasm.rs +++ b/core/src/smartcontracts/wasm.rs @@ -342,7 +342,7 @@ pub mod state { /// on any supported platform pub fn store_limits_from_config(config: &IrohaWasmConfig) -> StoreLimits { StoreLimitsBuilder::new() - .memory_size(config.max_memory.get() as usize) + .memory_size(config.max_memory_bytes as usize) .instances(1) .memories(1) .tables(1) diff --git a/core/src/sumeragi/mod.rs b/core/src/sumeragi/mod.rs index 63a518d2b20..b08525a4ea1 100644 --- a/core/src/sumeragi/mod.rs +++ b/core/src/sumeragi/mod.rs @@ -8,7 +8,7 @@ use std::{ }; use eyre::{Result, WrapErr as _}; -use iroha_config::parameters::actual::{Iroha as IrohaConfig, Sumeragi as SumeragiConfig}; +use iroha_config::parameters::actual::{Common as CommonConfig, Sumeragi as SumeragiConfig}; use iroha_crypto::{KeyPair, SignatureOf}; use iroha_data_model::{block::SignedBlock, prelude::*}; use iroha_genesis::GenesisNetwork; @@ -258,7 +258,7 @@ impl SumeragiHandle { pub fn start( SumeragiStartArgs { sumeragi_config, - iroha_config, + common_config, events_sender, mut wsv, queue, @@ -297,7 +297,7 @@ impl SumeragiHandle { (&mut blocks_iter).take(block_count.saturating_sub(skip_block_count + 1)); for block in block_iter_except_last { current_topology = - Self::replay_block(&iroha_config.chain_id, &block, &mut wsv, current_topology); + Self::replay_block(&common_config.chain_id, &block, &mut wsv, current_topology); } // finalized_wsv is one block behind @@ -305,7 +305,7 @@ impl SumeragiHandle { if let Some(block) = blocks_iter.next() { current_topology = - Self::replay_block(&iroha_config.chain_id, &block, &mut wsv, current_topology); + Self::replay_block(&common_config.chain_id, &block, &mut wsv, current_topology); } info!("Sumeragi has finished loading blocks and setting up the WSV"); @@ -319,13 +319,13 @@ impl SumeragiHandle { #[cfg(not(debug_assertions))] let debug_force_soft_fork = false; - let peer_id = iroha_config.peer_id(); + let peer_id = common_config.peer_id(); let sumeragi = main_loop::Sumeragi { - chain_id: iroha_config.chain_id, - key_pair: iroha_config.key_pair, - queue: Arc::clone(&queue), + chain_id: common_config.chain_id, + key_pair: common_config.key_pair, peer_id, + queue: Arc::clone(&queue), events_sender, public_wsv_sender, public_finalized_wsv_sender, @@ -423,8 +423,8 @@ impl VotingBlock { /// Arguments for [`SumeragiHandle::start`] function #[allow(missing_docs)] pub struct SumeragiStartArgs { - pub iroha_config: IrohaConfig, pub sumeragi_config: SumeragiConfig, + pub common_config: CommonConfig, pub events_sender: EventsSender, pub wsv: WorldStateView, pub queue: Arc, diff --git a/core/src/wsv.rs b/core/src/wsv.rs index dbfc968b8a6..cc7086d6a24 100644 --- a/core/src/wsv.rs +++ b/core/src/wsv.rs @@ -690,7 +690,7 @@ impl WorldStateView { WSV_DOMAIN_METADATA_LIMITS => self.config.domain_metadata_limits, WSV_IDENT_LENGTH_LIMITS => self.config.ident_length_limits, WASM_FUEL_LIMIT => self.config.wasm_runtime.fuel_limit, - WASM_MAX_MEMORY => self.config.wasm_runtime.max_memory.0, + WASM_MAX_MEMORY => self.config.wasm_runtime.max_memory_bytes, TRANSACTION_LIMITS => self.config.transaction_limits, } } diff --git a/core/test_network/src/lib.rs b/core/test_network/src/lib.rs index 89cf6fd9c95..3e0bf4ea8b1 100644 --- a/core/test_network/src/lib.rs +++ b/core/test_network/src/lib.rs @@ -127,7 +127,7 @@ impl TestGenesis for GenesisNetwork { first_transaction.append_instruction(isi); } - GenesisNetwork::new(genesis, &cfg.iroha.chain_id, { + GenesisNetwork::new(genesis, &cfg.common.chain_id, { use iroha_config::parameters::actual::Genesis; if let Genesis::Full { key_pair, .. } = &cfg.genesis { key_pair @@ -399,13 +399,13 @@ impl Drop for Peer { impl Peer { /// Returns per peer config with all addresses, keys, and id set up. fn get_config(&self, configuration: Configuration) -> Configuration { - use iroha_config::parameters::actual::{Iroha, Torii}; + use iroha_config::parameters::actual::{Common, Torii}; Configuration { - iroha: Iroha { + common: Common { key_pair: self.key_pair.clone(), p2p_address: self.p2p_address.clone(), - ..configuration.iroha + ..configuration.common }, torii: Torii { address: self.api_address.clone(), @@ -761,7 +761,7 @@ impl TestConfiguration for Configuration { fn test() -> Self { use iroha_config::{ base::{FromEnv as _, StdEnv, UnwrapPartial as _}, - parameters::user_layer::{CliContext, RootPartial}, + parameters::user::{CliContext, RootPartial}, }; let mut layer = iroha::samples::get_user_config( @@ -772,8 +772,8 @@ impl TestConfiguration for Configuration { .merge(RootPartial::from_env(&StdEnv).expect("test env variables should parse properly")); let (public_key, private_key) = KeyPair::generate().into(); - layer.iroha.public_key.set(public_key); - layer.iroha.private_key.set(private_key); + layer.public_key.set(public_key); + layer.private_key.set(private_key); layer .unwrap_partial() diff --git a/tools/kagami/src/genesis.rs b/tools/kagami/src/genesis.rs index 5d582748607..4c6d5e67e75 100644 --- a/tools/kagami/src/genesis.rs +++ b/tools/kagami/src/genesis.rs @@ -4,7 +4,7 @@ use clap::{ArgGroup, Parser, Subcommand}; use iroha_config::parameters::defaults::chain_wide::{ DEFAULT_BLOCK_TIME, DEFAULT_COMMIT_TIME, DEFAULT_IDENT_LENGTH_LIMITS, DEFAULT_MAX_TXS, DEFAULT_METADATA_LIMITS, DEFAULT_TRANSACTION_LIMITS, DEFAULT_WASM_FUEL_LIMIT, - DEFAULT_WASM_MAX_MEMORY, + DEFAULT_WASM_MAX_MEMORY_BYTES, }; use iroha_data_model::{ asset::AssetValueType, @@ -192,7 +192,7 @@ pub fn generate_default(executor: ExecutorMode) -> color_eyre::Result Date: Wed, 7 Feb 2024 06:29:48 +0900 Subject: [PATCH 53/94] [docs]: fill `peer.example.toml` Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- config_samples/examples/peer.example.toml | 76 ++++++++++++++++++++++- 1 file changed, 75 insertions(+), 1 deletion(-) diff --git a/config_samples/examples/peer.example.toml b/config_samples/examples/peer.example.toml index 3352266360e..5af4ecf3726 100644 --- a/config_samples/examples/peer.example.toml +++ b/config_samples/examples/peer.example.toml @@ -1 +1,75 @@ -# TODO: show default values, point out required fields +## For the full reference, go to (TODO put link) + +## You can use another TOML file to extend from. +## For a single file extension: +# extends = "./base.toml" +## For a chain of extensions: +# extends = ["base-1.toml", "base-2.toml"] + +# chain_id = +# public_key = +# private_key.digest_function = +# private_key.payload = + +[network] +# address = +# block_gossip_period = "10s" +# max_blocks_per_gossip = 4 +# max_transactions_per_gossip = 500 +# transaction_gossip_period = "1s" + +[genesis] +# public_key = + +## Combine these with `--submit-genesis` CLI argument +# file = +# private_key.digest_function = +# private_key.payload = + +[torii] +# address = +# max_content_len = "16mb" +# query_idle_time = "30s" + +[kura] +# init_mode = "strict" +# block_store_path = "./storage" + +[kura.debug] +# output_new_blocks = false + +## Add more of this section for each trusted peer +# [[sumeragi.trusted_peers]] +# address = +# public_key = + +[sumeragi.debug] +# force_soft_fork = false + +[logger] +# level = "INFO" +# format = "full" + +[queue] +# capacity = 65536 +# capacity_per_user = 65536 +# transaction_time_to_live = "86400s" +# future_threshold = "1s" + +[snapshot] +# create_every = "60s" +# store_path = "./storage/snapshot" +# creation_enabled = true + +[telemetry] +# name = +# url = +# min_retry_period = +# max_retry_delay_exponent = + +[telemetry.dev] +## FIXME: is it JSON5? +# file = "./dev-telemetry.json5" + +## TODO: remove it from the config file entirely +# [chain_wide] \ No newline at end of file From 59511917cd9dd01813445f60622c1dcf1b8bd48c Mon Sep 17 00:00:00 2001 From: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> Date: Wed, 7 Feb 2024 06:31:49 +0900 Subject: [PATCH 54/94] [chore]: re-export `ConfigurationDTO` from client Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- client/src/client.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/client.rs b/client/src/client.rs index 2c5c3d4b531..ab60013a32e 100644 --- a/client/src/client.rs +++ b/client/src/client.rs @@ -13,7 +13,7 @@ use derive_more::{DebugCustom, Display}; use eyre::{eyre, Result, WrapErr}; use futures_util::StreamExt; use http_default::{AsyncWebSocketStream, WebSocketStream}; -use iroha_config::client_api::ConfigurationDTO; +pub use iroha_config::client_api::ConfigurationDTO; use iroha_logger::prelude::*; use iroha_telemetry::metrics::Status; use iroha_torii_const::uri as torii_uri; From e1923064bea5c662dfe412b2438ea36917aa3688 Mon Sep 17 00:00:00 2001 From: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> Date: Fri, 9 Feb 2024 05:47:37 +0900 Subject: [PATCH 55/94] [test]: fix them Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- cli/src/main.rs | 7 +++--- config/iroha_test_config.toml | 5 ++-- config/src/parameters/user.rs | 2 +- config/tests/fixtures.rs | 44 ++++++++++++++++++++++++++++------- 4 files changed, 42 insertions(+), 16 deletions(-) diff --git a/cli/src/main.rs b/cli/src/main.rs index ae905d73bd3..b76c8cc0467 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -85,9 +85,8 @@ mod tests { #[test] #[allow(clippy::bool_assert_comparison)] // for expressiveness fn default_args() -> Result<()> { - let args = Args::try_parse_from(["test"])?; + let args = Args::try_parse_from(["test", "--config", "config.toml"])?; - assert_eq!(args.config, PathBuf::from("config.toml")); assert_eq!(args.terminal_colors, is_colouring_supported()); assert_eq!(args.submit_genesis, false); @@ -98,11 +97,11 @@ mod tests { #[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) + Ok(Args::try_parse_from(["test", arg, "--config", "config.toml"])?.terminal_colors) } assert_eq!( - Args::try_parse_from(["test"])?.terminal_colors, + Args::try_parse_from(["test", "--config", "config.toml"])?.terminal_colors, is_colouring_supported() ); assert_eq!(try_with("--terminal-colors")?, true); diff --git a/config/iroha_test_config.toml b/config/iroha_test_config.toml index 492a47c7817..4495aa70645 100644 --- a/config/iroha_test_config.toml +++ b/config/iroha_test_config.toml @@ -1,9 +1,10 @@ -[iroha] chain_id = "00000000-0000-0000-0000-000000000000" public_key = "ed01201C61FAF8FE94E253B93114240394F79A607B7FA55F9E5A41EBEC74B88055768B" private_key.digest_function = "ed25519" private_key.payload = "282ED9F3CF92811C3818DBC4AE594ED59DC1A2F78E4241E31924E101D6B1FB831C61FAF8FE94E253B93114240394F79A607B7FA55F9E5A41EBEC74B88055768B" -p2p_address = "127.0.0.1:1337" + +[network] +address = "127.0.0.1:1337" [genesis] public_key = "ed01204CFFD0EE429B1BDD36B3910EC570852B8BB63F18750341772FB46BC856C5CAAF" diff --git a/config/src/parameters/user.rs b/config/src/parameters/user.rs index fc54d043b6c..49e464ae440 100644 --- a/config/src/parameters/user.rs +++ b/config/src/parameters/user.rs @@ -577,7 +577,7 @@ mod tests { .get() .expect("private key is provided, should not fail"); - assert_eq!(private_key.digest_function(), "ed25519".parse().unwrap()); + assert_eq!(private_key.algorithm(), "ed25519".parse().unwrap()); assert_eq!(hex::encode( private_key.payload()), "8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb"); } diff --git a/config/tests/fixtures.rs b/config/tests/fixtures.rs index 26ac08959a3..4ea81c7de70 100644 --- a/config/tests/fixtures.rs +++ b/config/tests/fixtures.rs @@ -52,13 +52,23 @@ fn minimal_config_snapshot() -> Result<()> { "0", ), key_pair: KeyPair { - public_key: {digest: ed25519, payload: ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB}, - private_key: {digest: ed25519, payload: 8F4C15E5D664DA3F13778801D23D4E89B76E94C1B94B389544168B6CB894F84F8BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB}, + public_key: PublicKey( + ed25519( + "ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB", + ), + ), + private_key: ed25519( + "8F4C15E5D664DA3F13778801D23D4E89B76E94C1B94B389544168B6CB894F84F8BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB", + ), }, p2p_address: 127.0.0.1:1337, }, genesis: Partial { - public_key: {digest: ed25519, payload: ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB}, + public_key: PublicKey( + ed25519( + "ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB", + ), + ), }, torii: Torii { address: 127.0.0.1:8080, @@ -74,7 +84,11 @@ fn minimal_config_snapshot() -> Result<()> { [ PeerId { address: 127.0.0.1:1338, - public_key: {digest: ed25519, payload: ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB}, + public_key: PublicKey( + ed25519( + "ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB", + ), + ), }, ], ), @@ -138,7 +152,7 @@ fn minimal_config_snapshot() -> Result<()> { max: 128, }, wasm_runtime: WasmRuntime { - fuel_limit: 23000000, + fuel_limit: 30000000, max_memory_bytes: 524288000, }, }, @@ -272,17 +286,29 @@ fn full_envs_set_is_consumed() -> Result<()> { ), ), public_key: Some( - {digest: ed25519, payload: ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB}, + PublicKey( + ed25519( + "ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB", + ), + ), ), private_key: Some( - {digest: ed25519, payload: 8F4C15E5D664DA3F13778801D23D4E89B76E94C1B94B389544168B6CB894F84F8BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB}, + ed25519( + "8F4C15E5D664DA3F13778801D23D4E89B76E94C1B94B389544168B6CB894F84F8BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB", + ), ), genesis: GenesisPartial { public_key: Some( - {digest: ed25519, payload: ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB}, + PublicKey( + ed25519( + "ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB", + ), + ), ), private_key: Some( - {digest: ed25519, payload: 8F4C15E5D664DA3F13778801D23D4E89B76E94C1B94B389544168B6CB894F84F8BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB}, + ed25519( + "8F4C15E5D664DA3F13778801D23D4E89B76E94C1B94B389544168B6CB894F84F8BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB", + ), ), file: None, }, From 213dc9112b27089a5c783ee1e579414d54651fea Mon Sep 17 00:00:00 2001 From: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> Date: Fri, 9 Feb 2024 06:06:48 +0900 Subject: [PATCH 56/94] [refactor]: apply lints Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- cli/src/lib.rs | 6 +- cli/src/samples.rs | 4 +- client/benches/torii.rs | 4 +- client/examples/million_accounts_genesis.rs | 2 +- client/examples/tutorial.rs | 11 --- client/src/config/user.rs | 34 ++++++++- client/src/config/user/boilerplate.rs | 24 +----- config/base/src/lib.rs | 20 ++++- config/src/parameters/user.rs | 82 ++++++++++++++++++++- config/src/parameters/user/boilerplate.rs | 70 +----------------- config/tests/fixtures.rs | 4 + core/benches/blocks/common.rs | 2 +- core/src/queue.rs | 6 +- core/test_network/src/lib.rs | 2 +- p2p/src/network.rs | 6 +- tools/swarm/src/compose.rs | 2 + torii/src/routing.rs | 2 +- 17 files changed, 160 insertions(+), 121 deletions(-) diff --git a/cli/src/lib.rs b/cli/src/lib.rs index e2ba5e5c936..3639ac4724f 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -507,7 +507,7 @@ pub fn read_config_and_genesis( let raw_block = RawGenesisBlock::from_path(file)?; Some( - GenesisNetwork::new(raw_block, &config.common.chain_id, &key_pair) + GenesisNetwork::new(raw_block, &config.common.chain_id, key_pair) .wrap_err("Failed to construct the genesis")?, ) } else { @@ -607,7 +607,7 @@ mod tests { // When - let (config, genesis) = read_config_and_genesis(&config_path, true)?; + let (config, genesis) = read_config_and_genesis(config_path, true)?; // Then @@ -656,7 +656,7 @@ mod tests { // When & Then - let report = read_config_and_genesis(&config_path, false).unwrap_err(); + let report = read_config_and_genesis(config_path, false).unwrap_err(); assert_contains!( format!("{report:#}"), diff --git a/cli/src/samples.rs b/cli/src/samples.rs index fd882cba8eb..5e20bf61f83 100644 --- a/cli/src/samples.rs +++ b/cli/src/samples.rs @@ -59,7 +59,7 @@ pub fn get_trusted_peers(public_key: Option<&PublicKey>) -> HashSet { /// # Panics /// - when [`KeyPair`] generation fails (rare case). pub fn get_user_config( - peers: UniqueVec, + peers: &UniqueVec, chain_id: Option, key_pair: Option, ) -> UserConfig { @@ -103,7 +103,7 @@ pub fn get_user_config( /// # Panics /// - when [`KeyPair`] generation fails (rare case). pub fn get_config( - trusted_peers: UniqueVec, + trusted_peers: &UniqueVec, chain_id: Option, key_pair: Option, ) -> Config { diff --git a/client/benches/torii.rs b/client/benches/torii.rs index 383366a2796..4813e7e461c 100644 --- a/client/benches/torii.rs +++ b/client/benches/torii.rs @@ -22,7 +22,7 @@ fn query_requests(criterion: &mut Criterion) { let chain_id = get_chain_id(); let configuration = get_config( - unique_vec![peer.id.clone()], + &unique_vec![peer.id.clone()], Some(chain_id.clone()), Some(get_key_pair()), ); @@ -132,7 +132,7 @@ fn instruction_submits(criterion: &mut Criterion) { let chain_id = get_chain_id(); let configuration = get_config( - unique_vec![peer.id.clone()], + &unique_vec![peer.id.clone()], Some(chain_id.clone()), Some(get_key_pair()), ); diff --git a/client/examples/million_accounts_genesis.rs b/client/examples/million_accounts_genesis.rs index 2e48cbc2822..de951f7add3 100644 --- a/client/examples/million_accounts_genesis.rs +++ b/client/examples/million_accounts_genesis.rs @@ -40,7 +40,7 @@ fn main_genesis() { let chain_id = get_chain_id(); let configuration = get_config( - unique_vec![peer.id.clone()], + &unique_vec![peer.id.clone()], Some(chain_id.clone()), Some(get_key_pair()), ); diff --git a/client/examples/tutorial.rs b/client/examples/tutorial.rs index 9c56ee391fd..f5f316d437c 100644 --- a/client/examples/tutorial.rs +++ b/client/examples/tutorial.rs @@ -13,8 +13,6 @@ fn main() { // Your code goes here… - json_config_client_test(config.clone()) - .expect("JSON config client example is expected to work correctly"); domain_registration_test(config.clone()) .expect("Domain registration example is expected to work correctly"); account_definition_test().expect("Account definition example is expected to work correctly"); @@ -30,15 +28,6 @@ fn main() { println!("Success!"); } -fn json_config_client_test(config: Config) -> Result<(), Error> { - use iroha_client::client::Client; - - // Initialise a client with a provided config - let _current_client = Client::new(config); - - Ok(()) -} - fn domain_registration_test(config: Config) -> Result<(), Error> { // #region domain_register_example_crates use iroha_client::{ diff --git a/client/src/config/user.rs b/client/src/config/user.rs index 3007efdebee..c62f3c2d8ce 100644 --- a/client/src/config/user.rs +++ b/client/src/config/user.rs @@ -2,17 +2,46 @@ mod boilerplate; -use std::{str::FromStr, time::Duration}; +use std::{fs::File, io::Read, path::Path, str::FromStr, time::Duration}; pub use boilerplate::*; use eyre::{eyre, Context, Report}; use iroha_config::base::{Emitter, ErrorsCollection}; use iroha_crypto::{KeyPair, PrivateKey, PublicKey}; use iroha_data_model::{account::AccountId, ChainId}; +use merge::Merge; use url::Url; use crate::config::BasicAuth; +impl RootPartial { + /// Reads the partial layer from TOML + /// + /// # Errors + /// - File not found + /// - Not valid TOML or content + pub fn from_toml(path: impl AsRef) -> eyre::Result { + let contents = { + let mut contents = String::new(); + File::open(path.as_ref()) + .wrap_err_with(|| { + eyre!("cannot open file at location `{}`", path.as_ref().display()) + })? + .read_to_string(&mut contents)?; + contents + }; + let layer: Self = toml::from_str(&contents).wrap_err("failed to parse toml")?; + Ok(layer) + } + + /// Merge other into self + #[must_use] + pub fn merge(mut self, other: Self) -> Self { + Merge::merge(&mut self, other); + self + } +} + /// Root of the user configuration #[derive(Clone, Debug)] #[allow(missing_docs)] @@ -27,6 +56,9 @@ pub struct Root { impl Root { /// Validates user configuration for semantic errors and constructs a complete /// [`super::Config`]. + /// + /// # Errors + /// If a set of validity errors occurs. pub fn parse(self) -> Result> { let Self { chain_id, diff --git a/client/src/config/user/boilerplate.rs b/client/src/config/user/boilerplate.rs index 205e7f5a64e..39991e1c375 100644 --- a/client/src/config/user/boilerplate.rs +++ b/client/src/config/user/boilerplate.rs @@ -2,9 +2,8 @@ #![allow(missing_docs)] -use std::{error::Error, fs::File, io::Read, path::Path}; +use std::error::Error; -use eyre::{eyre, Context}; use iroha_config::base::{ Emitter, FromEnv, HumanDuration, Merge, ParseEnvResult, UnwrapPartial, UnwrapPartialResult, UserField, @@ -33,26 +32,7 @@ pub struct RootPartial { impl RootPartial { pub fn new() -> Self { // TODO: gen with macro - Default::default() - } - - pub fn from_toml(path: impl AsRef) -> eyre::Result { - let contents = { - let mut contents = String::new(); - File::open(path.as_ref()) - .wrap_err_with(|| { - eyre!("cannot open file at location `{}`", path.as_ref().display()) - })? - .read_to_string(&mut contents)?; - contents - }; - let layer: Self = toml::from_str(&contents).wrap_err("failed to parse toml")?; - Ok(layer) - } - - pub fn merge(mut self, other: Self) -> Self { - Merge::merge(&mut self, other); - self + Self::default() } } diff --git a/config/base/src/lib.rs b/config/base/src/lib.rs index f7403e12519..b022dadc616 100644 --- a/config/base/src/lib.rs +++ b/config/base/src/lib.rs @@ -101,12 +101,18 @@ pub trait ReadEnv { /// part, it might be convenient to parse the string while just borrowing it /// (e.g. with [`FromStr`]), but might be also convenient to own the value. [`Cow`] covers all /// of this. + /// + /// # Errors + /// For any reason an implementor might have. fn read_env(&self, key: impl AsRef) -> Result>, E>; } /// Constructs from environment variables pub trait FromEnv { /// Constructs from environment variables using [`ReadEnv`] + /// + /// # Errors + /// For any reason an implementor might have. // `E: Error` so that it could be wrapped into a Report fn from_env>(env: &R) -> FromEnvResult where @@ -162,6 +168,9 @@ impl Emitter { /// Transform the emitter into a [`Result`], containing an [`ErrorCollection`] if /// any errors were emitted. + /// + /// # Errors + /// If any errors were emitted. pub fn finish(mut self) -> Result<(), ErrorsCollection> { self.bomb.defuse(); @@ -173,6 +182,12 @@ impl Emitter { } } +impl Default for Emitter { + fn default() -> Self { + Self::new() + } +} + impl Emitter { /// Shorthand to emit a [`MissingFieldError`]. pub fn emit_missing_field(&mut self, field_name: impl AsRef) { @@ -449,7 +464,10 @@ pub trait UnwrapPartial { /// The output of unwrapping, i.e. the full layer type Output; - /// Unwraps the partial, emitting multiple [`MissingFieldError`] in case of absense. + /// Unwraps the partial into a structure with all required fields present. + /// + /// # Errors + /// If there are absent fields, returns a bulk of [`MissingFieldError`]s. fn unwrap_partial(self) -> UnwrapPartialResult; } diff --git a/config/src/parameters/user.rs b/config/src/parameters/user.rs index 49e464ae440..c2fe96aac76 100644 --- a/config/src/parameters/user.rs +++ b/config/src/parameters/user.rs @@ -12,8 +12,10 @@ use std::{ error::Error, fmt::Debug, + fs::File, + io::Read, num::{NonZeroU32, NonZeroUsize}, - path::PathBuf, + path::{Path, PathBuf}, time::Duration, }; @@ -57,7 +59,85 @@ pub struct Root { chain_wide: ChainWide, } +impl RootPartial { + /// Read the partial from TOML file + /// + /// # Errors + /// - If file is not found, or not a valid TOML + /// - If failed to parse data into a layer + /// - If failed to read other configurations specified in `extends` + pub fn from_toml(path: impl AsRef) -> eyre::Result { + let contents = { + let mut file = File::open(path.as_ref()).wrap_err_with(|| { + eyre!("cannot open file at location `{}`", path.as_ref().display()) + })?; + let mut contents = String::new(); + file.read_to_string(&mut contents)?; + contents + }; + let mut layer: Self = toml::from_str(&contents).wrap_err("failed to parse toml")?; + + let base_path = path + .as_ref() + .parent() + .expect("the config file path could not be empty or root"); + + layer.normalise_paths(base_path); + + if let Some(paths) = layer.extends.take() { + let base = paths + .iter() + .try_fold(None, |acc: Option, extends_path| { + // extends path is not normalised relative to the config file yet + let full_path = base_path.join(extends_path); + + let base = Self::from_toml(&full_path) + .wrap_err_with(|| eyre!("cannot extend from `{}`", full_path.display()))?; + + match acc { + None => Ok::, Report>(Some(base)), + Some(other_base) => Ok(Some(other_base.merge(base))), + } + })?; + if let Some(base) = base { + layer = base.merge(layer) + }; + } + + Ok(layer) + } + + /// **Note:** this function doesn't affect `extends` + fn normalise_paths(&mut self, relative_to: impl AsRef) { + let path = relative_to.as_ref(); + + macro_rules! patch { + ($value:expr) => { + $value.as_mut().map(|x| { + *x = path.join(&x); + }) + }; + } + + patch!(self.genesis.file); + patch!(self.snapshot.store_path); + patch!(self.kura.block_store_path); + patch!(self.telemetry.dev.file); + } + + // FIXME workaround the inconvenient way `Merge::merge` works + #[must_use] + pub fn merge(mut self, other: Self) -> Self { + Merge::merge(&mut self, other); + self + } +} + impl Root { + /// Parses user configuration view into the internal repr. + /// + /// # Errors + /// If any invalidity found. pub fn parse(self, cli: CliContext) -> Result> { let mut emitter = Emitter::new(); diff --git a/config/src/parameters/user/boilerplate.rs b/config/src/parameters/user/boilerplate.rs index 1e2b502afb0..ae298e6bcf8 100644 --- a/config/src/parameters/user/boilerplate.rs +++ b/config/src/parameters/user/boilerplate.rs @@ -4,10 +4,8 @@ use std::{ error::Error, - fs::File, - io::Read, num::{NonZeroU32, NonZeroUsize}, - path::{Path, PathBuf}, + path::PathBuf, }; use eyre::{eyre, Report, WrapErr}; @@ -63,72 +61,6 @@ impl RootPartial { // TODO: generate this function with macro. For now, use default Self::default() } - - pub fn from_toml(path: impl AsRef) -> eyre::Result { - let contents = { - let mut file = File::open(path.as_ref()).wrap_err_with(|| { - eyre!("cannot open file at location `{}`", path.as_ref().display()) - })?; - let mut contents = String::new(); - file.read_to_string(&mut contents)?; - contents - }; - let mut layer: Self = toml::from_str(&contents).wrap_err("failed to parse toml")?; - - let base_path = path - .as_ref() - .parent() - .expect("the config file path could not be empty or root"); - - layer.normalise_paths(base_path); - - if let Some(paths) = layer.extends.take() { - let base = paths - .iter() - .try_fold(None, |acc: Option, extends_path| { - // extends path is not normalised relative to the config file yet - let full_path = base_path.join(extends_path); - - let base = Self::from_toml(&full_path) - .wrap_err_with(|| eyre!("cannot extend from `{}`", full_path.display()))?; - - match acc { - None => Ok::, Report>(Some(base)), - Some(other_base) => Ok(Some(other_base.merge(base))), - } - })?; - if let Some(base) = base { - layer = base.merge(layer) - }; - } - - Ok(layer) - } - - /// **Note:** this function doesn't affect `extends` - fn normalise_paths(&mut self, relative_to: impl AsRef) { - let path = relative_to.as_ref(); - - macro_rules! patch { - ($value:expr) => { - $value.as_mut().map(|x| { - *x = path.join(&x); - }) - }; - } - - patch!(self.genesis.file); - patch!(self.snapshot.store_path); - patch!(self.kura.block_store_path); - patch!(self.telemetry.dev.file); - } - - // FIXME workaround the inconvenient way `Merge::merge` works - #[must_use] - pub fn merge(mut self, other: Self) -> Self { - Merge::merge(&mut self, other); - self - } } impl UnwrapPartial for RootPartial { diff --git a/config/tests/fixtures.rs b/config/tests/fixtures.rs index 4ea81c7de70..d29199a8116 100644 --- a/config/tests/fixtures.rs +++ b/config/tests/fixtures.rs @@ -1,3 +1,5 @@ +#![allow(clippy::needless_raw_string_hashes)] // triggered by `expect_test` snapshots + use std::{ collections::{HashMap, HashSet}, fs, @@ -38,6 +40,7 @@ fn test_env_from_file(p: impl AsRef) -> TestEnv { /// This test not only asserts that the minimal set of fields is enough; /// it also gives an insight into every single default value #[test] +#[allow(clippy::too_many_lines)] fn minimal_config_snapshot() -> Result<()> { let config = RootPartial::from_toml(fixtures_dir().join("minimal_with_trusted_peers.toml"))? .unwrap_partial()? @@ -270,6 +273,7 @@ fn inconsistent_genesis_config() -> Result<()> { /// Aims the purpose of checking that every single provided env variable is consumed and parsed /// into a valid config. #[test] +#[allow(clippy::too_many_lines)] fn full_envs_set_is_consumed() -> Result<()> { let env = test_env_from_file(fixtures_dir().join("full.env")); diff --git a/core/benches/blocks/common.rs b/core/benches/blocks/common.rs index 4f9018eb168..9249bcfde95 100644 --- a/core/benches/blocks/common.rs +++ b/core/benches/blocks/common.rs @@ -186,7 +186,7 @@ pub fn build_wsv( let mut wsv = WorldStateView::new(World::with([domain], UniqueVec::new()), kura, query_handle); wsv.config.transaction_limits = TransactionLimits::new(u64::MAX, u64::MAX); wsv.config.wasm_runtime.fuel_limit = u64::MAX; - wsv.config.wasm_runtime.max_memory_bytes = u32::MAX.into(); + wsv.config.wasm_runtime.max_memory_bytes = u32::MAX; { let path_to_executor = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) diff --git a/core/src/queue.rs b/core/src/queue.rs index 17362c88fc4..daed2ba96b0 100644 --- a/core/src/queue.rs +++ b/core/src/queue.rs @@ -859,8 +859,10 @@ mod tests { ) .with_executable(tx.0.instructions().clone()); - let creation_time: u64 = tx.0.creation_time().as_millis().try_into().unwrap(); - new_tx.set_creation_time_ms(creation_time + (future_threshold * 2).as_millis() as u64); + new_tx.set_creation_time_ms( + tx.0.payload().creation_time_ms + + u64::try_from((future_threshold * 2).as_millis()).unwrap(), + ); let new_tx = new_tx.sign(&alice_key); let limits = TransactionLimits { diff --git a/core/test_network/src/lib.rs b/core/test_network/src/lib.rs index 3e0bf4ea8b1..fb817bf0884 100644 --- a/core/test_network/src/lib.rs +++ b/core/test_network/src/lib.rs @@ -765,7 +765,7 @@ impl TestConfiguration for Configuration { }; let mut layer = iroha::samples::get_user_config( - UniqueVec::new(), + &UniqueVec::new(), Some(get_chain_id()), Some(get_key_pair()), ) diff --git a/p2p/src/network.rs b/p2p/src/network.rs index c9ccb6dac07..c867746b435 100644 --- a/p2p/src/network.rs +++ b/p2p/src/network.rs @@ -321,7 +321,7 @@ impl NetworkBase { } for public_key in to_disconnect { - self.disconnect_peer(public_key) + self.disconnect_peer(&public_key) } } @@ -344,8 +344,8 @@ impl NetworkBase { ); } - fn disconnect_peer(&mut self, public_key: PublicKey) { - let peer = match self.peers.remove(&public_key) { + fn disconnect_peer(&mut self, public_key: &PublicKey) { + let peer = match self.peers.remove(public_key) { Some(peer) => peer, _ => return iroha_logger::warn!(?public_key, "Not found peer to disconnect"), }; diff --git a/tools/swarm/src/compose.rs b/tools/swarm/src/compose.rs index 60979eae8be..22be1c45ade 100644 --- a/tools/swarm/src/compose.rs +++ b/tools/swarm/src/compose.rs @@ -722,6 +722,7 @@ mod tests { }; let actual = serde_yaml::to_string(&compose).expect("Should be serialisable"); + #[allow(clippy::needless_raw_string_hashes)] let expected = expect_test::expect![[r#" version: '3.8' services: @@ -771,6 +772,7 @@ mod tests { .into(); let actual = serde_yaml::to_string(&env).unwrap(); + #[allow(clippy::needless_raw_string_hashes)] let expected = expect_test::expect![[r#" CHAIN_ID: 00000000-0000-0000-0000-000000000000 PUBLIC_KEY: ed0120415388A90FA238196737746A70565D041CFB32EAA0C89FF8CB244C7F832A6EBD diff --git a/torii/src/routing.rs b/torii/src/routing.rs index d7028451105..2aef903af42 100644 --- a/torii/src/routing.rs +++ b/torii/src/routing.rs @@ -423,7 +423,7 @@ pub mod profiling { { // Create profiler guard let guard = pprof::ProfilerGuardBuilder::default() - .frequency(frequency.get().into()) + .frequency(i32::from(frequency.get())) .blocklist(&["libc", "libgcc", "pthread", "vdso"]) .build() .map_err(|e| { From 65ed561fb3ad60cab430d637698427f60da291c6 Mon Sep 17 00:00:00 2001 From: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> Date: Fri, 9 Feb 2024 06:21:14 +0900 Subject: [PATCH 57/94] [test]: fix pytests Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- client_cli/pytests/src/client_cli/configuration.py | 2 +- client_cli/pytests/src/client_cli/iroha.py | 7 ++++++- scripts/test_env.py | 12 +++++------- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/client_cli/pytests/src/client_cli/configuration.py b/client_cli/pytests/src/client_cli/configuration.py index 4c39c5bcdff..a80b0e50202 100644 --- a/client_cli/pytests/src/client_cli/configuration.py +++ b/client_cli/pytests/src/client_cli/configuration.py @@ -48,7 +48,7 @@ def randomise_torii_url(self): :return: None """ - parsed_url = urlparse(self._config['api']["torii_url"]) + parsed_url = urlparse(self._config["torii_url"]) random_port = random.randint(self.port_min, self.port_max) self._envs["TORII_URL"] = parsed_url._replace(netloc=f"{parsed_url.hostname}:{random_port}").geturl() diff --git a/client_cli/pytests/src/client_cli/iroha.py b/client_cli/pytests/src/client_cli/iroha.py index 38174fefeac..e62e694dc6f 100644 --- a/client_cli/pytests/src/client_cli/iroha.py +++ b/client_cli/pytests/src/client_cli/iroha.py @@ -51,7 +51,12 @@ def domains(self) -> Dict[str, Dict]: :rtype: List[str] """ self._execute_command('domain') - domains = json.loads(self.stdout) + try: + domains = json.loads(self.stdout) + except json.decoder.JSONDecodeError as e: + print(f"JSON decode error occurred with this input:", self.stdout) + print(f"STDERR:", self.stderr) + raise domains_dict = { domain["id"]: domain for domain in domains } return domains_dict diff --git a/scripts/test_env.py b/scripts/test_env.py index bf3f39d16f9..7dad1ff107a 100755 --- a/scripts/test_env.py +++ b/scripts/test_env.py @@ -38,9 +38,7 @@ def __init__(self, args: argparse.Namespace): logging.info("Generating shared configuration...") trusted_peers = [{"address": f"{peer.host_ip}:{peer.p2p_port}", "public_key": peer.public_key} for peer in self.peers] shared_config = { - "iroha": { - "chain_id": "00000000-0000-0000-0000-000000000000", - }, + "chain_id": "00000000-0000-0000-0000-000000000000", "genesis": { "public_key": self.peers[0].public_key }, @@ -116,10 +114,10 @@ def __init__(self, args: argparse.Namespace, nth: int): config = { "extends": f"../{SHARED_CONFIG_FILE_NAME}", - "iroha": { - "public_key": self.public_key, - "private_key": self.private_key, - "p2p_address": f"{self.host_ip}:{self.p2p_port}" + "public_key": self.public_key, + "private_key": self.private_key, + "network": { + "address": f"{self.host_ip}:{self.p2p_port}" }, "torii": { "address": f"{self.host_ip}:{self.api_port}" From c9deae74297e2d93547c71ea5a89a134be6d5a61 Mon Sep 17 00:00:00 2001 From: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> Date: Fri, 9 Feb 2024 06:24:47 +0900 Subject: [PATCH 58/94] [misc]: re-arrange sample configurations - use `configs` dir again - name example configs as _templates_ Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- CONTRIBUTING.md | 2 +- README.md | 2 +- cli/README.md | 8 +++---- client/examples/tutorial.rs | 2 +- config_samples/examples/client.example.toml | 17 --------------- configs/client.template.toml | 20 ++++++++++++++++++ .../peer.template.toml | 2 +- configs/peer/config.json | 0 .../prometheus.template.yml | 0 {config_samples => configs}/swarm/client.toml | 0 .../swarm/docker-compose.local.yml | 0 .../swarm/docker-compose.single.yml | 0 .../swarm/docker-compose.yml | 0 .../swarm/executor.wasm | Bin .../swarm/genesis.json | 0 core/benches/blocks/common.rs | 2 +- core/benches/validation.rs | 2 +- core/test_network/src/lib.rs | 7 +++--- default_executor/README.md | 2 +- scripts/test_env.py | 2 +- scripts/tests/consistency.sh | 18 ++++++++-------- scripts/tests/panic_on_invalid_genesis.sh | 2 +- 22 files changed, 45 insertions(+), 43 deletions(-) delete mode 100644 config_samples/examples/client.example.toml create mode 100644 configs/client.template.toml rename config_samples/examples/peer.example.toml => configs/peer.template.toml (97%) delete mode 100644 configs/peer/config.json rename config_samples/examples/prometheus.example.yml => configs/prometheus.template.yml (100%) rename {config_samples => configs}/swarm/client.toml (100%) rename {config_samples => configs}/swarm/docker-compose.local.yml (100%) rename {config_samples => configs}/swarm/docker-compose.single.yml (100%) rename {config_samples => configs}/swarm/docker-compose.yml (100%) rename {config_samples => configs}/swarm/executor.wasm (100%) rename {config_samples => configs}/swarm/genesis.json (100%) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1d592221fd9..854f2302c4d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -222,7 +222,7 @@ Follow these commit guidelines: - To run the source-code based tests, execute [`cargo test`](https://doc.rust-lang.org/cargo/commands/cargo-test.html) in the Iroha root. Note that this is a long process. - To run benchmarks, execute [`cargo bench`](https://doc.rust-lang.org/cargo/commands/cargo-bench.html) from the Iroha root. To help debug benchmark outputs, set the `debug_assertions` environment variable like so: `RUSTFLAGS="--cfg debug_assertions" cargo bench`. - If you are working on a particular component, be mindful that when you run `cargo test` in a [workspace](https://doc.rust-lang.org/cargo/reference/workspaces.html), it will only run the tests for that workspace, which usually doesn't include any [integration tests](https://www.testingxperts.com/blog/what-is-integration-testing). -- If you want to test your changes on a minimal network, the provided [`docker-compose.yml`](config_samples/swarm/docker-compose.yml) creates a network of 4 Iroha peers in docker containers that can be used to test consensus and asset propagation-related logic. We recommend interacting with that network using either [`iroha-python`](https://github.com/hyperledger/iroha-python), or the included `iroha_client_cli`. +- If you want to test your changes on a minimal network, the provided [`docker-compose.yml`](configs/swarm/docker-compose.yml) creates a network of 4 Iroha peers in docker containers that can be used to test consensus and asset propagation-related logic. We recommend interacting with that network using either [`iroha-python`](https://github.com/hyperledger/iroha-python), or the included `iroha_client_cli`. - Do not remove failing tests. Even tests that are ignored will be run in our pipeline eventually. - If possible, please benchmark your code both before and after making your changes, as a significant performance regression can break existing users' installations. diff --git a/README.md b/README.md index 7fbd2d93091..f83f05ef4b1 100644 --- a/README.md +++ b/README.md @@ -124,7 +124,7 @@ docker compose up With the `docker-compose` instance running, use [Iroha Client CLI](./client_cli/README.md): ```bash -cargo run --bin iroha_client_cli -- --config ./config_samples/swarm/client.toml +cargo run --bin iroha_client_cli -- --config ./configs/swarm/client.toml ``` ## Integration diff --git a/cli/README.md b/cli/README.md index 40499ee4240..5ba8d269b39 100644 --- a/cli/README.md +++ b/cli/README.md @@ -82,7 +82,7 @@ You may deploy Iroha as a [native binary](#native-binary) or by using [Docker](# ### Native binary - + 1. Prepare a deployment environment. @@ -91,8 +91,8 @@ You may deploy Iroha as a [native binary](#native-binary) or by using [Docker](# ```bash # FIXME # cp ./target/release/iroha - # cp ./config_samples/peer/config.json deploy - # cp ./config_samples/peer/genesis.json deploy + # cp ./configs/peer/config.json deploy + # cp ./configs/peer/genesis.json deploy ``` 2. Make the necessary edits to `config.json` and `genesis.json`, such as: @@ -114,7 +114,7 @@ You may deploy Iroha as a [native binary](#native-binary) or by using [Docker](# ### Docker -We provide a sample configuration for Docker in [`docker-compose.yml`](../config_samples/swarm/docker-compose.yml). We highly recommend that you adjust the `config.json` to include a set of new key pairs. +We provide a sample configuration for Docker in [`docker-compose.yml`](../configs/swarm/docker-compose.yml). We highly recommend that you adjust the `config.json` to include a set of new key pairs. [Generate the keys](#generating-keys) and put them into `services.*.environment` in `docker-compose.yml`. Don't forget to update the public keys of `TRUSTED_PEERS`. diff --git a/client/examples/tutorial.rs b/client/examples/tutorial.rs index f5f316d437c..bec8227f6a5 100644 --- a/client/examples/tutorial.rs +++ b/client/examples/tutorial.rs @@ -8,7 +8,7 @@ use iroha_client::config::Config; fn main() { // #region rust_config_load - let config = Config::load("../config_samples/swarm/client.toml").unwrap(); + let config = Config::load("../configs/swarm/client.toml").unwrap(); // #endregion rust_config_load // Your code goes here… diff --git a/config_samples/examples/client.example.toml b/config_samples/examples/client.example.toml deleted file mode 100644 index 7b1939efe77..00000000000 --- a/config_samples/examples/client.example.toml +++ /dev/null @@ -1,17 +0,0 @@ -chain_id = "00000000-0000-0000-0000-000000000000" -# Might be set via `TORII_URL` env var -torii_url = "http://127.0.0.1:8080/" -basic_auth.login = "mad_hatter" -basic_auth.password = "ilovetea" - -[account] -id = "alice@wonderland" -public_key = "ed01207233BFC89DCBD68C19FDE6CE6158225298EC1131B6A130D1AEB454C1AB5183C0" -private_key.digest_function = "ed25519" -private_key.payload = "9ac47abf59b356e0bd7dcbbbb4dec080e302156a48ca907e47cb6aea1d32719e7233bfc89dcbd68c19fde6ce6158225298ec1131b6a130d1aeb454c1ab5183c0" - -[transaction] -time_to_live = 100_000 -status_timeout = 100_000 -nonce = false - diff --git a/configs/client.template.toml b/configs/client.template.toml new file mode 100644 index 00000000000..c0ced15851c --- /dev/null +++ b/configs/client.template.toml @@ -0,0 +1,20 @@ +# chain_id = + +## Might be set via `TORII_URL` env var +# torii_url = + +[basic_auth] +# login = +# password = + +[account] +# id = +# public_key = +# private_key.digest_function = +# private_key.payload = + +[transaction] +# time_to_live = 100_000 +# status_timeout = 100_000 +# nonce = false + diff --git a/config_samples/examples/peer.example.toml b/configs/peer.template.toml similarity index 97% rename from config_samples/examples/peer.example.toml rename to configs/peer.template.toml index 5af4ecf3726..0d96be66f8c 100644 --- a/config_samples/examples/peer.example.toml +++ b/configs/peer.template.toml @@ -3,7 +3,7 @@ ## You can use another TOML file to extend from. ## For a single file extension: # extends = "./base.toml" -## For a chain of extensions: +## Or, for a chain of extensions: # extends = ["base-1.toml", "base-2.toml"] # chain_id = diff --git a/configs/peer/config.json b/configs/peer/config.json deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/config_samples/examples/prometheus.example.yml b/configs/prometheus.template.yml similarity index 100% rename from config_samples/examples/prometheus.example.yml rename to configs/prometheus.template.yml diff --git a/config_samples/swarm/client.toml b/configs/swarm/client.toml similarity index 100% rename from config_samples/swarm/client.toml rename to configs/swarm/client.toml diff --git a/config_samples/swarm/docker-compose.local.yml b/configs/swarm/docker-compose.local.yml similarity index 100% rename from config_samples/swarm/docker-compose.local.yml rename to configs/swarm/docker-compose.local.yml diff --git a/config_samples/swarm/docker-compose.single.yml b/configs/swarm/docker-compose.single.yml similarity index 100% rename from config_samples/swarm/docker-compose.single.yml rename to configs/swarm/docker-compose.single.yml diff --git a/config_samples/swarm/docker-compose.yml b/configs/swarm/docker-compose.yml similarity index 100% rename from config_samples/swarm/docker-compose.yml rename to configs/swarm/docker-compose.yml diff --git a/config_samples/swarm/executor.wasm b/configs/swarm/executor.wasm similarity index 100% rename from config_samples/swarm/executor.wasm rename to configs/swarm/executor.wasm diff --git a/config_samples/swarm/genesis.json b/configs/swarm/genesis.json similarity index 100% rename from config_samples/swarm/genesis.json rename to configs/swarm/genesis.json diff --git a/core/benches/blocks/common.rs b/core/benches/blocks/common.rs index 9249bcfde95..a81cd11a8e3 100644 --- a/core/benches/blocks/common.rs +++ b/core/benches/blocks/common.rs @@ -190,7 +190,7 @@ pub fn build_wsv( { let path_to_executor = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("../config_samples/swarm/executor.wasm"); + .join("../configs/swarm/executor.wasm"); let wasm = std::fs::read(&path_to_executor) .unwrap_or_else(|_| panic!("Failed to read file: {}", path_to_executor.display())); let executor = Executor::new(WasmSmartContract::from_compiled(wasm)); diff --git a/core/benches/validation.rs b/core/benches/validation.rs index 8c69e23ce95..037e031cd12 100644 --- a/core/benches/validation.rs +++ b/core/benches/validation.rs @@ -79,7 +79,7 @@ fn build_test_and_transient_wsv(keys: KeyPair) -> WorldStateView { { let path_to_executor = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("../config_samples/swarm/executor.wasm"); + .join("../configs/swarm/executor.wasm"); let wasm = std::fs::read(&path_to_executor) .unwrap_or_else(|_| panic!("Failed to read file: {}", path_to_executor.display())); let executor = Executor::new(WasmSmartContract::from_compiled(wasm)); diff --git a/core/test_network/src/lib.rs b/core/test_network/src/lib.rs index fb817bf0884..a18fa81822c 100644 --- a/core/test_network/src/lib.rs +++ b/core/test_network/src/lib.rs @@ -80,10 +80,9 @@ impl TestGenesis for GenesisNetwork { // TODO: Fix this somehow. Probably we need to make `kagami` a library (#3253). let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR")); - let mut genesis = RawGenesisBlock::from_path( - manifest_dir.join("../../config_samples/swarm/genesis.json"), - ) - .expect("Failed to deserialize genesis block from file"); + let mut genesis = + RawGenesisBlock::from_path(manifest_dir.join("../../configs/swarm/genesis.json")) + .expect("Failed to deserialize genesis block from file"); let rose_definition_id = AssetDefinitionId::from_str("rose#wonderland").expect("valid names"); diff --git a/default_executor/README.md b/default_executor/README.md index a2db2d88dc1..0fe04b6fe24 100644 --- a/default_executor/README.md +++ b/default_executor/README.md @@ -4,5 +4,5 @@ Use the [Wasm Builder CLI](../tools/wasm_builder_cli) in order to build it: ```bash cargo run --bin iroha_wasm_builder_cli -- \ - build ./default_executor --optimize --outfile ./config_samples/swarm/executor.wasm + build ./default_executor --optimize --outfile ./configs/swarm/executor.wasm ``` \ No newline at end of file diff --git a/scripts/test_env.py b/scripts/test_env.py index 7dad1ff107a..e1e94910cf6 100755 --- a/scripts/test_env.py +++ b/scripts/test_env.py @@ -19,7 +19,7 @@ import urllib.request import tomli_w -SWARM_CONFIGS_DIRECTORY = pathlib.Path("config_samples/swarm") +SWARM_CONFIGS_DIRECTORY = pathlib.Path("configs/swarm") SHARED_CONFIG_FILE_NAME = "config.base.toml" class Network: diff --git a/scripts/tests/consistency.sh b/scripts/tests/consistency.sh index 305151b4f78..ba3a34531f5 100755 --- a/scripts/tests/consistency.sh +++ b/scripts/tests/consistency.sh @@ -3,8 +3,8 @@ set -e case $1 in "genesis") - cargo run --release --bin kagami -- genesis --executor-path-in-genesis ./executor.wasm | diff - config_samples/swarm/genesis.json || { - echo 'Please re-generate the genesis with `cargo run --release --bin kagami -- genesis --executor-path-in-genesis ./executor.wasm > config_samples/swarm/genesis.json`' + cargo run --release --bin kagami -- genesis --executor-path-in-genesis ./executor.wasm | diff - configs/swarm/genesis.json || { + echo 'Please re-generate the genesis with `cargo run --release --bin kagami -- genesis --executor-path-in-genesis ./executor.wasm > configs/swarm/genesis.json`' exit 1 };; "schema") @@ -19,7 +19,7 @@ case $1 in # FIXME: not nice; add an option to `kagami swarm` to print content into stdout? # it is not a default behaviour because Kagami resolves `build` path relative # to the output file location - temp_file="config_samples/swarm/docker-compose.TMP.yml" + temp_file="configs/swarm/docker-compose.TMP.yml" full_cmd="$cmd_base --outfile $temp_file" eval "$full_cmd" @@ -30,19 +30,19 @@ case $1 in } command_base_for_single() { - echo "cargo run --release --bin iroha_swarm -- -p 1 -s Iroha --force --config-dir ./config_samples/swarm --health-check --build ." + echo "cargo run --release --bin iroha_swarm -- -p 1 -s Iroha --force --config-dir ./configs/swarm --health-check --build ." } command_base_for_multiple_local() { - echo "cargo run --release --bin iroha_swarm -- -p 4 -s Iroha --force --config-dir ./config_samples/swarm --health-check --build ." + echo "cargo run --release --bin iroha_swarm -- -p 4 -s Iroha --force --config-dir ./configs/swarm --health-check --build ." } command_base_for_default() { - echo "cargo run --release --bin iroha_swarm -- -p 4 -s Iroha --force --config-dir ./config_samples/swarm --health-check --image hyperledger/iroha2:dev" + echo "cargo run --release --bin iroha_swarm -- -p 4 -s Iroha --force --config-dir ./configs/swarm --health-check --image hyperledger/iroha2:dev" } - do_check "$(command_base_for_single)" "config_samples/swarm/docker-compose.single.yml" - do_check "$(command_base_for_multiple_local)" "config_samples/swarm/docker-compose.local.yml" - do_check "$(command_base_for_default)" "config_samples/swarm/docker-compose.yml" + do_check "$(command_base_for_single)" "configs/swarm/docker-compose.single.yml" + do_check "$(command_base_for_multiple_local)" "configs/swarm/docker-compose.local.yml" + do_check "$(command_base_for_default)" "configs/swarm/docker-compose.yml" esac diff --git a/scripts/tests/panic_on_invalid_genesis.sh b/scripts/tests/panic_on_invalid_genesis.sh index 51d270a3a73..a76e5580f15 100755 --- a/scripts/tests/panic_on_invalid_genesis.sh +++ b/scripts/tests/panic_on_invalid_genesis.sh @@ -18,6 +18,6 @@ trap 'rm -rf -- "$IROHA2_GENESIS_PATH" "$KURA_BLOCK_STORE_PATH"' EXIT # Create invalid genesis # NewAssetDefinition replaced with AssetDefinition -sed 's/NewAssetDefinition/AssetDefinition/' ./config_samples/swarm/genesis.json > $IROHA2_GENESIS_PATH +sed 's/NewAssetDefinition/AssetDefinition/' ./configs/swarm/genesis.json > $IROHA2_GENESIS_PATH timeout 1m target/debug/iroha --submit-genesis 2>&1 | tee /dev/stderr | grep -q 'Transaction validation failed in genesis block' From 5828a08bdbf25a2f06ed3c441d500da57d880620 Mon Sep 17 00:00:00 2001 From: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> Date: Fri, 9 Feb 2024 06:32:48 +0900 Subject: [PATCH 59/94] [docs]: update README Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- README.md | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/README.md b/README.md index f83f05ef4b1..c1306066ac8 100644 --- a/README.md +++ b/README.md @@ -164,11 +164,7 @@ A brief overview on how to configure and maintain an Iroha instance: There is a set of configuration parameters that could be passed either through a configuration file or environment variables. ```shell -# Override default config path through CLI or ENV iroha --config /path/to/config.toml - -# or ENV -IROHA_CONFIG=/path/to/config.toml iroha ``` **Note:** detailed configuration reference is [work in progress](https://github.com/hyperledger/iroha-2-docs/issues/392). @@ -204,11 +200,7 @@ The details of the `Health` endpoint can be found in the [API Reference > Torii Iroha can produce both JSON-formatted as well as `prometheus`-readable metrics at the `status` and `metrics` endpoints respectively. -The [`prometheus`](https://prometheus.io/docs/introduction/overview/) monitoring system is the de-factor standard for monitoring long-running services such as an Iroha peer. In order to get started, [install `prometheus`](https://prometheus.io/docs/introduction/first_steps/) and execute the following in the project root: - -``` -prometheus --config.file=configs/prometheus.yml -``` +The [`prometheus`](https://prometheus.io/docs/introduction/overview/) monitoring system is the de-factor standard for monitoring long-running services such as an Iroha peer. In order to get started, [install `prometheus`](https://prometheus.io/docs/introduction/first_steps/) and use `configs/prometheus.template.yml` for configuration. ### Storage From 90356e6110dfc963ae0214175507db3e0a6c0016 Mon Sep 17 00:00:00 2001 From: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> Date: Fri, 9 Feb 2024 06:57:22 +0900 Subject: [PATCH 60/94] [refactor]: remove `parse-display` from deps Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- Cargo.lock | 1 - Cargo.toml | 1 - data_model/Cargo.toml | 1 - 3 files changed, 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 79b1d9a674f..902307ffb5b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2902,7 +2902,6 @@ dependencies = [ "iroha_version", "once_cell", "parity-scale-codec", - "parse-display", "serde", "serde_json", "serde_with", diff --git a/Cargo.toml b/Cargo.toml index c79a5457fad..b9ec9589dd0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -112,7 +112,6 @@ displaydoc = { version = "0.2.4", default-features = false } cfg-if = "1.0.0" derive_more = { version = "0.99.17", default-features = false } -parse-display = "0.8.2" async-trait = "0.1.73" strum = { version = "0.25.0", default-features = false } getset = "0.1.2" diff --git a/data_model/Cargo.toml b/data_model/Cargo.toml index 6a21cf6d6bd..9b80ee2b6fc 100644 --- a/data_model/Cargo.toml +++ b/data_model/Cargo.toml @@ -43,7 +43,6 @@ iroha_ffi = { workspace = true, optional = true } parity-scale-codec = { workspace = true, features = ["derive"] } derive_more = { workspace = true, features = ["as_ref", "display", "constructor", "from_str", "from", "into"] } -parse-display = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_with = { workspace = true, features = ["macros"] } serde_json = { workspace = true } From abbb48fa137326706e1d70b5cae42f3c8018082c Mon Sep 17 00:00:00 2001 From: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> Date: Fri, 9 Feb 2024 06:58:55 +0900 Subject: [PATCH 61/94] [refactor]: remove extra `iroha_config_base` exposure from the client Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- client/src/config.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/client/src/config.rs b/client/src/config.rs index 5340d8d85a7..b6029de680e 100644 --- a/client/src/config.rs +++ b/client/src/config.rs @@ -5,8 +5,7 @@ use std::{path::Path, time::Duration}; use derive_more::Display; use eyre::Result; -pub use iroha_config::base; -use iroha_config::base::UnwrapPartial; +use iroha_config::{base, base::UnwrapPartial}; use iroha_crypto::prelude::*; use iroha_data_model::{prelude::*, ChainId}; use iroha_primitives::small::SmallStr; From eaaa9585bdb8c2a1adeccc27719b15f931b8698f Mon Sep 17 00:00:00 2001 From: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> Date: Fri, 9 Feb 2024 07:01:22 +0900 Subject: [PATCH 62/94] [fix]: import `Deserialize` in the macro Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- config/base/src/lib.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config/base/src/lib.rs b/config/base/src/lib.rs index b022dadc616..df1527ada32 100644 --- a/config/base/src/lib.rs +++ b/config/base/src/lib.rs @@ -44,6 +44,8 @@ macro_rules! impl_deserialize_from_str { where D: $crate::serde::Deserializer<'de>, { + use $crate::serde::Deserialize; + String::deserialize(deserializer)? .parse() .map_err($crate::serde::de::Error::custom) From 5083524f231ada44143c8e208a0ac5e749d85e0c Mon Sep 17 00:00:00 2001 From: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> Date: Mon, 12 Feb 2024 16:04:46 +0900 Subject: [PATCH 63/94] [refactor]: use `PrivateKey::into_raw` instead Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- crypto/src/lib.rs | 7 ++++++- tools/swarm/src/compose.rs | 13 +++++++------ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/crypto/src/lib.rs b/crypto/src/lib.rs index d547204e7e1..d1ba5ee54c5 100755 --- a/crypto/src/lib.rs +++ b/crypto/src/lib.rs @@ -649,7 +649,7 @@ impl PrivateKey { } /// Key payload - pub fn payload(&self) -> Vec { + fn payload(&self) -> Vec { match self.0.borrow() { PrivateKeyInner::Ed25519(key) => key.to_keypair_bytes().to_vec(), PrivateKeyInner::Secp256k1(key) => key.to_bytes().to_vec(), @@ -657,6 +657,11 @@ impl PrivateKey { PrivateKeyInner::BlsSmall(key) => key.to_bytes(), } } + + /// Split the key into its algorithm and payload. Reverse conversion to [`Self::from_raw`]. + pub fn into_raw(self) -> (Algorithm, Vec) { + (self.algorithm(), self.payload()) + } } #[cfg(not(feature = "ffi_import"))] diff --git a/tools/swarm/src/compose.rs b/tools/swarm/src/compose.rs index 22be1c45ade..32bb9c99aab 100644 --- a/tools/swarm/src/compose.rs +++ b/tools/swarm/src/compose.rs @@ -333,17 +333,18 @@ impl From for FullPeerEnv { let (genesis_private_key_digest, genesis_private_key_payload, genesis_file) = value .genesis_private_key .map_or((None, None, None), |private_key| { + let (algorithm, payload) = private_key.into_raw(); ( - Some(private_key.algorithm()), - Some(SerializeAsHex(private_key.payload())), + Some(algorithm), + Some(SerializeAsHex(payload)), Some(PATH_TO_GENESIS.to_string()), ) }); - let (private_key_digest, private_key_payload) = ( - value.key_pair.private_key().algorithm(), - SerializeAsHex(value.key_pair.private_key().payload()), - ); + let (private_key_digest, private_key_payload) = { + let (algorithm, payload) = value.key_pair.private_key().clone().into_raw(); + (algorithm, SerializeAsHex(payload)) + }; Self { chain_id: value.chain_id, From 7bf07d19aafd8875e0c7d9ccb42534c4b6184176 Mon Sep 17 00:00:00 2001 From: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> Date: Mon, 12 Feb 2024 16:11:10 +0900 Subject: [PATCH 64/94] [ci]: use `--break-system-packages` `pip` flag Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- .github/workflows/iroha2-dev-pr.yml | 5 +---- .github/workflows/iroha2-release-pr.yml | 3 +-- client_cli/pytests/README.md | 1 + 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/.github/workflows/iroha2-dev-pr.yml b/.github/workflows/iroha2-dev-pr.yml index 776c8ca993d..f2a91d0ea28 100644 --- a/.github/workflows/iroha2-dev-pr.yml +++ b/.github/workflows/iroha2-dev-pr.yml @@ -143,10 +143,7 @@ jobs: cargo build --bin iroha - name: Setup test Iroha 2 environment on the bare metal run: | - # FIXME: not sure how to install it in the right way. `requirements.txt` triggers - # "This environment is externally managed" error - pacman -S python-tomli-w - # pip3 install -r scripts/requirements.txt --no-input + pip3 install -r scripts/requirements.txt --no-input --break-system-packages ./scripts/test_env.py setup - name: Mark binaries as executable run: | diff --git a/.github/workflows/iroha2-release-pr.yml b/.github/workflows/iroha2-release-pr.yml index 208627ee56f..99067c687fb 100644 --- a/.github/workflows/iroha2-release-pr.yml +++ b/.github/workflows/iroha2-release-pr.yml @@ -36,8 +36,7 @@ jobs: cargo build --bin iroha - name: Setup test Iroha 2 environment on bare metal run: | - # TODO - # pip3 install -r scripts/requirements.txt --no-input + pip3 install -r scripts/requirements.txt --no-input --break-system-packages ./scripts/test_env.py setup - name: Mark binaries as executable run: | diff --git a/client_cli/pytests/README.md b/client_cli/pytests/README.md index 8a89105933c..f565dc71e26 100644 --- a/client_cli/pytests/README.md +++ b/client_cli/pytests/README.md @@ -54,6 +54,7 @@ The test model has the following structure: ```shell # Must be executed from the repo root: ./scripts/test_env.py setup + # Note: make sure you have installed packages from `./scripts/requirements.txt` ``` By default, this builds `iroha`, `iroha_client_cli`, and `kagami` binaries, and runs four peers with their API exposed through the `8080`-`8083` ports.\ From ddd099b31104ce1c50a8b68e13b19310c8e4a8ee Mon Sep 17 00:00:00 2001 From: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> Date: Mon, 12 Feb 2024 16:19:41 +0900 Subject: [PATCH 65/94] [fix]: regenerate swarms Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- configs/swarm/docker-compose.local.yml | 8 ++++---- configs/swarm/docker-compose.single.yml | 2 +- configs/swarm/docker-compose.yml | 8 ++++---- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/configs/swarm/docker-compose.local.yml b/configs/swarm/docker-compose.local.yml index 3c7b9d8c2eb..fe10bcaabb1 100644 --- a/configs/swarm/docker-compose.local.yml +++ b/configs/swarm/docker-compose.local.yml @@ -22,7 +22,7 @@ services: - 1337:1337 - 8080:8080 volumes: - - ../../configs/peer:/config + - ./:/config init: true command: iroha --submit-genesis healthcheck: @@ -47,7 +47,7 @@ services: - 1338:1338 - 8081:8081 volumes: - - ../../configs/peer:/config + - ./:/config init: true healthcheck: test: test $(curl -s http://127.0.0.1:8081/status/blocks) -gt 0 @@ -71,7 +71,7 @@ services: - 1339:1339 - 8082:8082 volumes: - - ../../configs/peer:/config + - ./:/config init: true healthcheck: test: test $(curl -s http://127.0.0.1:8082/status/blocks) -gt 0 @@ -95,7 +95,7 @@ services: - 1340:1340 - 8083:8083 volumes: - - ../../configs/peer:/config + - ./:/config init: true healthcheck: test: test $(curl -s http://127.0.0.1:8083/status/blocks) -gt 0 diff --git a/configs/swarm/docker-compose.single.yml b/configs/swarm/docker-compose.single.yml index 697c7fe3588..5af2868b817 100644 --- a/configs/swarm/docker-compose.single.yml +++ b/configs/swarm/docker-compose.single.yml @@ -21,7 +21,7 @@ services: - 1337:1337 - 8080:8080 volumes: - - ../../configs/peer:/config + - ./:/config init: true command: iroha --submit-genesis healthcheck: diff --git a/configs/swarm/docker-compose.yml b/configs/swarm/docker-compose.yml index b90fe946091..c21b8300a89 100644 --- a/configs/swarm/docker-compose.yml +++ b/configs/swarm/docker-compose.yml @@ -22,7 +22,7 @@ services: - 1337:1337 - 8080:8080 volumes: - - ../../configs/peer:/config + - ./:/config init: true command: iroha --submit-genesis healthcheck: @@ -47,7 +47,7 @@ services: - 1338:1338 - 8081:8081 volumes: - - ../../configs/peer:/config + - ./:/config init: true healthcheck: test: test $(curl -s http://127.0.0.1:8081/status/blocks) -gt 0 @@ -71,7 +71,7 @@ services: - 1339:1339 - 8082:8082 volumes: - - ../../configs/peer:/config + - ./:/config init: true healthcheck: test: test $(curl -s http://127.0.0.1:8082/status/blocks) -gt 0 @@ -95,7 +95,7 @@ services: - 1340:1340 - 8083:8083 volumes: - - ../../configs/peer:/config + - ./:/config init: true healthcheck: test: test $(curl -s http://127.0.0.1:8083/status/blocks) -gt 0 From aadc5a96902edda46913f69956f609717ff9c9aa Mon Sep 17 00:00:00 2001 From: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> Date: Mon, 12 Feb 2024 16:24:31 +0900 Subject: [PATCH 66/94] [ci]: fix pytests workflow Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- .github/workflows/iroha2-dev-pr.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/iroha2-dev-pr.yml b/.github/workflows/iroha2-dev-pr.yml index f2a91d0ea28..7846ef54715 100644 --- a/.github/workflows/iroha2-dev-pr.yml +++ b/.github/workflows/iroha2-dev-pr.yml @@ -138,9 +138,7 @@ jobs: - uses: Swatinem/rust-cache@v2 - name: Build binaries run: | - cargo build --bin iroha_client_cli - cargo build --bin kagami - cargo build --bin iroha + cargo build -p iroha_client_cli -p kagami -p iroha - name: Setup test Iroha 2 environment on the bare metal run: | pip3 install -r scripts/requirements.txt --no-input --break-system-packages @@ -154,6 +152,10 @@ jobs: poetry install - name: Run client cli tests working-directory: client_cli/pytests + env: + # prepared by `test_env.py` + CLIENT_CLI_BINARY: ../../test/iroha_client_cli + CLIENT_CLI_CONFIG: ../../test/client.toml run: | poetry run pytest - name: Cleanup test environment From c82798b4e7195ddc3bfdbcf6800148e5b17b93dc Mon Sep 17 00:00:00 2001 From: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> Date: Mon, 12 Feb 2024 16:26:06 +0900 Subject: [PATCH 67/94] [fix]: remove `PrivateKey::payload()` access Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- config/src/parameters/user.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/config/src/parameters/user.rs b/config/src/parameters/user.rs index c2fe96aac76..c47a44f7cda 100644 --- a/config/src/parameters/user.rs +++ b/config/src/parameters/user.rs @@ -657,8 +657,9 @@ mod tests { .get() .expect("private key is provided, should not fail"); - assert_eq!(private_key.algorithm(), "ed25519".parse().unwrap()); - assert_eq!(hex::encode( private_key.payload()), "8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb"); + let (algorithm, payload) = private_key.into_raw(); + assert_eq!(algorithm, "ed25519".parse().unwrap()); + assert_eq!(hex::encode(payload), "8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb"); } #[test] From abb7a943d78174510131b3225a80655ea8e6fcb0 Mon Sep 17 00:00:00 2001 From: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> Date: Wed, 14 Feb 2024 06:35:06 +0900 Subject: [PATCH 68/94] [docs]: fix "unable to validate" doc Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- client/src/config.rs | 2 +- config/src/parameters/actual.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/config.rs b/client/src/config.rs index b6029de680e..fbb5607ca30 100644 --- a/client/src/config.rs +++ b/client/src/config.rs @@ -88,7 +88,7 @@ impl Config { /// /// # Errors /// - unable to load config from a TOML file - /// - unable to validate loaded config + /// - the config is invalid pub fn load(path: impl AsRef) -> std::result::Result { Ok(RootPartial::from_toml(path)?.unwrap_partial()?.parse()?) } diff --git a/config/src/parameters/actual.rs b/config/src/parameters/actual.rs index 620fe22dc78..920747251e2 100644 --- a/config/src/parameters/actual.rs +++ b/config/src/parameters/actual.rs @@ -52,7 +52,7 @@ impl Root { /// # Errors /// - unable to load config from a TOML file /// - unable to parse config from envs - /// - unable to validate loaded config + /// - the config is invalid pub fn load(path: impl AsRef, cli: CliContext) -> Result { let config = RootPartial::from_toml(path)?; let config = config.merge(RootPartial::from_env(&StdEnv)?); From 8abb1a6f537b6fddf7d1504374fe03ce1ea3643f Mon Sep 17 00:00:00 2001 From: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> Date: Wed, 14 Feb 2024 06:37:51 +0900 Subject: [PATCH 69/94] [refactor]: accept `Duration` for `set_creation_time` Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- client/src/client.rs | 2 +- core/src/queue.rs | 5 +---- data_model/src/transaction.rs | 5 +++-- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/client/src/client.rs b/client/src/client.rs index ab60013a32e..13f4b7a4ee4 100644 --- a/client/src/client.rs +++ b/client/src/client.rs @@ -1630,7 +1630,7 @@ mod tests { .with_executable(tx1.instructions().clone()) .with_metadata(tx1.metadata().clone()); - tx.set_creation_time_ms(tx1.creation_time().as_millis().try_into().unwrap()); + tx.set_creation_time(tx1.creation_time()); if let Some(nonce) = tx1.nonce() { tx.set_nonce(nonce); } diff --git a/core/src/queue.rs b/core/src/queue.rs index daed2ba96b0..0598e0415f4 100644 --- a/core/src/queue.rs +++ b/core/src/queue.rs @@ -859,10 +859,7 @@ mod tests { ) .with_executable(tx.0.instructions().clone()); - new_tx.set_creation_time_ms( - tx.0.payload().creation_time_ms - + u64::try_from((future_threshold * 2).as_millis()).unwrap(), - ); + new_tx.set_creation_time(tx.0.creation_time() + future_threshold * 2); let new_tx = new_tx.sign(&alice_key); let limits = TransactionLimits { diff --git a/data_model/src/transaction.rs b/data_model/src/transaction.rs index 015343eec9f..bd13a66cf28 100644 --- a/data_model/src/transaction.rs +++ b/data_model/src/transaction.rs @@ -735,8 +735,9 @@ mod http { } /// Set creation time of transaction - pub fn set_creation_time_ms(&mut self, value: u64) -> &mut Self { - self.payload.creation_time_ms = value; + pub fn set_creation_time(&mut self, value: Duration) -> &mut Self { + self.payload.creation_time_ms = + u64::try_from(value.as_millis()).expect("should never exceed u64"); self } From 0ee40dda5ff69ca57b262ab42211ea13a74ac8f0 Mon Sep 17 00:00:00 2001 From: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> Date: Wed, 14 Feb 2024 06:39:53 +0900 Subject: [PATCH 70/94] [refactor]: `PrivateKey::to_raw` Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- config/src/parameters/user.rs | 2 +- crypto/src/lib.rs | 7 +++++-- tools/swarm/src/compose.rs | 4 ++-- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/config/src/parameters/user.rs b/config/src/parameters/user.rs index c47a44f7cda..5c30962cfc9 100644 --- a/config/src/parameters/user.rs +++ b/config/src/parameters/user.rs @@ -657,7 +657,7 @@ mod tests { .get() .expect("private key is provided, should not fail"); - let (algorithm, payload) = private_key.into_raw(); + let (algorithm, payload) = private_key.to_raw(); assert_eq!(algorithm, "ed25519".parse().unwrap()); assert_eq!(hex::encode(payload), "8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb"); } diff --git a/crypto/src/lib.rs b/crypto/src/lib.rs index d1ba5ee54c5..dc0f85a3128 100755 --- a/crypto/src/lib.rs +++ b/crypto/src/lib.rs @@ -658,8 +658,11 @@ impl PrivateKey { } } - /// Split the key into its algorithm and payload. Reverse conversion to [`Self::from_raw`]. - pub fn into_raw(self) -> (Algorithm, Vec) { + /// Extracts the raw bytes from the private key, copying the payload. + /// + /// `into_raw()` without copying is not provided because underlying crypto + /// libraries do not provide move functionality. + pub fn to_raw(self) -> (Algorithm, Vec) { (self.algorithm(), self.payload()) } } diff --git a/tools/swarm/src/compose.rs b/tools/swarm/src/compose.rs index 32bb9c99aab..0c687f9e387 100644 --- a/tools/swarm/src/compose.rs +++ b/tools/swarm/src/compose.rs @@ -333,7 +333,7 @@ impl From for FullPeerEnv { let (genesis_private_key_digest, genesis_private_key_payload, genesis_file) = value .genesis_private_key .map_or((None, None, None), |private_key| { - let (algorithm, payload) = private_key.into_raw(); + let (algorithm, payload) = private_key.to_raw(); ( Some(algorithm), Some(SerializeAsHex(payload)), @@ -342,7 +342,7 @@ impl From for FullPeerEnv { }); let (private_key_digest, private_key_payload) = { - let (algorithm, payload) = value.key_pair.private_key().clone().into_raw(); + let (algorithm, payload) = value.key_pair.private_key().clone().to_raw(); (algorithm, SerializeAsHex(payload)) }; From 34cf89016a77e4b71204d7dc5da579a3af5ff41a Mon Sep 17 00:00:00 2001 From: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> Date: Wed, 14 Feb 2024 06:40:57 +0900 Subject: [PATCH 71/94] [fix]: do not extend trusted peers in config Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- config/src/parameters/user.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/src/parameters/user.rs b/config/src/parameters/user.rs index 5c30962cfc9..4aac86653a3 100644 --- a/config/src/parameters/user.rs +++ b/config/src/parameters/user.rs @@ -418,8 +418,8 @@ impl UnwrapPartial for UserTrustedPeers { } impl Merge for UserTrustedPeers { - fn merge(&mut self, mut other: Self) { - self.peers.append(other.peers.as_mut()) + fn merge(&mut self, other: Self) { + self.peers = other.peers; } } From 2c17ad1d39add9a927d2374e04d91c31ee46e824 Mon Sep 17 00:00:00 2001 From: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> Date: Wed, 14 Feb 2024 06:44:11 +0900 Subject: [PATCH 72/94] [refactor]: hide user view from `iroha_client::config` Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- client/src/config.rs | 11 ++++++++--- client/src/config/user/boilerplate.rs | 1 + 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/client/src/config.rs b/client/src/config.rs index fbb5607ca30..d122435cf01 100644 --- a/client/src/config.rs +++ b/client/src/config.rs @@ -5,7 +5,10 @@ use std::{path::Path, time::Duration}; use derive_more::Display; use eyre::Result; -use iroha_config::{base, base::UnwrapPartial}; +use iroha_config::{ + base, + base::{FromEnv, StdEnv, UnwrapPartial}, +}; use iroha_crypto::prelude::*; use iroha_data_model::{prelude::*, ChainId}; use iroha_primitives::small::SmallStr; @@ -14,7 +17,7 @@ use url::Url; use crate::config::user::RootPartial; -pub mod user; +mod user; #[allow(missing_docs)] pub const DEFAULT_TRANSACTION_TIME_TO_LIVE: Duration = Duration::from_secs(100); @@ -90,6 +93,8 @@ impl Config { /// - unable to load config from a TOML file /// - the config is invalid pub fn load(path: impl AsRef) -> std::result::Result { - Ok(RootPartial::from_toml(path)?.unwrap_partial()?.parse()?) + let config = RootPartial::from_toml(path)?; + let config = config.merge(RootPartial::from_env(&StdEnv)?); + Ok(config.unwrap_partial()?.parse()?) } } diff --git a/client/src/config/user/boilerplate.rs b/client/src/config/user/boilerplate.rs index 39991e1c375..500b13afecb 100644 --- a/client/src/config/user/boilerplate.rs +++ b/client/src/config/user/boilerplate.rs @@ -30,6 +30,7 @@ pub struct RootPartial { } impl RootPartial { + #[allow(unused)] pub fn new() -> Self { // TODO: gen with macro Self::default() From d983a4523bc2376c25558f11d1ed7fdc0145d842 Mon Sep 17 00:00:00 2001 From: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> Date: Wed, 14 Feb 2024 06:58:35 +0900 Subject: [PATCH 73/94] =?UTF-8?q?[refactor]:=20rename=20`*configuration`?= =?UTF-8?q?=20to=20`config`=20everywhere=EF=B9=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ﹡except crypto Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- cli/src/lib.rs | 6 +- client/benches/torii.rs | 4 +- client/examples/million_accounts_genesis.rs | 2 +- client/src/client.rs | 6 +- client/tests/integration/add_account.rs | 4 +- client/tests/integration/add_domain.rs | 4 +- client/tests/integration/asset.rs | 4 +- client/tests/integration/asset_propagation.rs | 4 +- client/tests/integration/connected_peers.rs | 10 +- client/tests/integration/events/pipeline.rs | 4 +- .../integration/multiple_blocks_created.rs | 4 +- .../integration/multisignature_account.rs | 4 +- .../integration/multisignature_transaction.rs | 24 ++-- client/tests/integration/offline_peers.rs | 4 +- client/tests/integration/restart_peer.rs | 4 +- client/tests/integration/tx_history.rs | 4 +- client/tests/integration/unregister_peer.rs | 6 +- client/tests/integration/unstable_network.rs | 8 +- client_cli/src/main.rs | 20 ++-- config/src/client_api.rs | 14 +-- core/src/executor.rs | 8 +- core/src/kiso.rs | 22 ++-- core/src/queue.rs | 6 +- core/src/smartcontracts/wasm.rs | 2 +- core/src/wsv.rs | 4 +- core/test_network/src/lib.rs | 105 ++++++++---------- torii/src/routing.rs | 7 +- 27 files changed, 138 insertions(+), 156 deletions(-) diff --git a/cli/src/lib.rs b/cli/src/lib.rs index 3639ac4724f..20f8c0d9abe 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -314,7 +314,7 @@ impl Iroha { Arc::clone(&kura), ); - Self::spawn_configuration_updates_broadcasting(kiso.clone(), logger.clone()); + Self::spawn_config_updates_broadcasting(kiso.clone(), logger.clone()); Self::start_listening_signal(Arc::clone(¬ify_shutdown))?; @@ -402,7 +402,7 @@ impl Iroha { #[cfg(not(feature = "telemetry"))] async fn start_telemetry( _logger: &LoggerHandle, - _config: &TelemetryConfiguration, + _config: &Config, ) -> Result { Ok(TelemetryStartStatus::NotStarted) } @@ -438,7 +438,7 @@ impl Iroha { /// Spawns a task which subscribes on updates from configuration actor /// and broadcasts them further to interested actors. This way, neither config actor nor other ones know /// about each other, achieving loose coupling of code and system. - fn spawn_configuration_updates_broadcasting( + fn spawn_config_updates_broadcasting( kiso: KisoHandle, logger: LoggerHandle, ) -> task::JoinHandle<()> { diff --git a/client/benches/torii.rs b/client/benches/torii.rs index 4813e7e461c..669fcc0c917 100644 --- a/client/benches/torii.rs +++ b/client/benches/torii.rs @@ -49,7 +49,7 @@ fn query_requests(criterion: &mut Criterion) { .expect("genesis creation failed"); let builder = PeerBuilder::new() - .with_configuration(configuration) + .with_config(configuration) .with_into_genesis(genesis); rt.block_on(builder.start_with_peer(&mut peer)); @@ -156,7 +156,7 @@ fn instruction_submits(criterion: &mut Criterion) { ) .expect("failed to create genesis"); let builder = PeerBuilder::new() - .with_configuration(configuration) + .with_config(configuration) .with_into_genesis(genesis); rt.block_on(builder.start_with_peer(&mut peer)); let mut group = criterion.benchmark_group("instruction-requests"); diff --git a/client/examples/million_accounts_genesis.rs b/client/examples/million_accounts_genesis.rs index de951f7add3..c618caf700b 100644 --- a/client/examples/million_accounts_genesis.rs +++ b/client/examples/million_accounts_genesis.rs @@ -57,7 +57,7 @@ fn main_genesis() { let builder = PeerBuilder::new() .with_into_genesis(genesis) - .with_configuration(configuration); + .with_config(configuration); // This only submits the genesis. It doesn't check if the accounts // are created, because that check is 1) not needed for what the diff --git a/client/src/client.rs b/client/src/client.rs index 13f4b7a4ee4..fadfcafef36 100644 --- a/client/src/client.rs +++ b/client/src/client.rs @@ -13,7 +13,7 @@ use derive_more::{DebugCustom, Display}; use eyre::{eyre, Result, WrapErr}; use futures_util::StreamExt; use http_default::{AsyncWebSocketStream, WebSocketStream}; -pub use iroha_config::client_api::ConfigurationDTO; +pub use iroha_config::client_api::ConfigDTO; use iroha_logger::prelude::*; use iroha_telemetry::metrics::Status; use iroha_torii_const::uri as torii_uri; @@ -1021,7 +1021,7 @@ impl Client { /// /// # Errors /// Fails if sending request or decoding fails - pub fn get_config(&self) -> Result { + pub fn get_config(&self) -> Result { let resp = DefaultRequestBuilder::new( HttpMethod::GET, self.torii_url @@ -1047,7 +1047,7 @@ impl Client { /// /// # Errors /// If sending request or decoding fails - pub fn set_config(&self, dto: ConfigurationDTO) -> Result<()> { + pub fn set_config(&self, dto: ConfigDTO) -> Result<()> { let body = serde_json::to_vec(&dto).wrap_err(format!("Failed to serialize {dto:?}"))?; let url = self .torii_url diff --git a/client/tests/integration/add_account.rs b/client/tests/integration/add_account.rs index e15c185d690..d7de69d5041 100644 --- a/client/tests/integration/add_account.rs +++ b/client/tests/integration/add_account.rs @@ -2,7 +2,7 @@ use std::thread; use eyre::Result; use iroha_client::{client, data_model::prelude::*}; -use iroha_config::parameters::actual::Root as Configuration; +use iroha_config::parameters::actual::Root as Config; use test_network::*; #[test] @@ -11,7 +11,7 @@ fn client_add_account_with_name_length_more_than_limit_should_not_commit_transac let (_rt, _peer, test_client) = ::new().with_port(10_505).start_with_runtime(); wait_for_genesis_committed(&vec![test_client.clone()], 0); - let pipeline_time = Configuration::pipeline_time(); + let pipeline_time = Config::pipeline_time(); let normal_account_id: AccountId = "bob@wonderland".parse().expect("Valid"); let create_account = Register::account(Account::new(normal_account_id.clone(), [])); diff --git a/client/tests/integration/add_domain.rs b/client/tests/integration/add_domain.rs index b1260496a1f..d4cfe89c3b3 100644 --- a/client/tests/integration/add_domain.rs +++ b/client/tests/integration/add_domain.rs @@ -2,7 +2,7 @@ use std::thread; use eyre::Result; use iroha_client::{client, data_model::prelude::*}; -use iroha_config::parameters::actual::Root as Configuration; +use iroha_config::parameters::actual::Root as Config; use test_network::*; #[test] @@ -10,7 +10,7 @@ fn client_add_domain_with_name_length_more_than_limit_should_not_commit_transact { let (_rt, _peer, test_client) = ::new().with_port(10_500).start_with_runtime(); wait_for_genesis_committed(&vec![test_client.clone()], 0); - let pipeline_time = Configuration::pipeline_time(); + let pipeline_time = Config::pipeline_time(); // Given diff --git a/client/tests/integration/asset.rs b/client/tests/integration/asset.rs index 99d536c2f2f..7838742fd71 100644 --- a/client/tests/integration/asset.rs +++ b/client/tests/integration/asset.rs @@ -6,7 +6,7 @@ use iroha_client::{ crypto::{KeyPair, PublicKey}, data_model::prelude::*, }; -use iroha_config::parameters::actual::Root as Configuration; +use iroha_config::parameters::actual::Root as Config; use iroha_primitives::fixed::Fixed; use serde_json::json; use test_network::*; @@ -205,7 +205,7 @@ fn client_add_asset_with_decimal_should_increase_asset_amount() -> Result<()> { fn client_add_asset_with_name_length_more_than_limit_should_not_commit_transaction() -> Result<()> { let (_rt, _peer, test_client) = ::new().with_port(10_520).start_with_runtime(); wait_for_genesis_committed(&[test_client.clone()], 0); - let pipeline_time = Configuration::pipeline_time(); + let pipeline_time = Config::pipeline_time(); // Given let normal_asset_definition_id = AssetDefinitionId::from_str("xor#wonderland").expect("Valid"); diff --git a/client/tests/integration/asset_propagation.rs b/client/tests/integration/asset_propagation.rs index 4886210ca42..99a834017db 100644 --- a/client/tests/integration/asset_propagation.rs +++ b/client/tests/integration/asset_propagation.rs @@ -9,7 +9,7 @@ use iroha_client::{ prelude::*, }, }; -use iroha_config::parameters::actual::Root as Configuration; +use iroha_config::parameters::actual::Root as Config; use test_network::*; #[test] @@ -18,7 +18,7 @@ fn client_add_asset_quantity_to_existing_asset_should_increase_asset_amount_on_a // Given let (_rt, network, client) = Network::start_test_with_runtime(4, Some(10_450)); wait_for_genesis_committed(&network.clients(), 0); - let pipeline_time = Configuration::pipeline_time(); + let pipeline_time = Config::pipeline_time(); client.submit_all_blocking( ParametersBuilder::new() diff --git a/client/tests/integration/connected_peers.rs b/client/tests/integration/connected_peers.rs index 64e46b2aafa..0bf17809514 100644 --- a/client/tests/integration/connected_peers.rs +++ b/client/tests/integration/connected_peers.rs @@ -8,7 +8,7 @@ use iroha_client::{ peer::Peer as DataModelPeer, }, }; -use iroha_config::parameters::actual::Root as Configuration; +use iroha_config::parameters::actual::Root as Config; use iroha_primitives::unique_vec; use rand::{seq::SliceRandom, thread_rng, Rng}; use test_network::*; @@ -29,7 +29,7 @@ fn connected_peers_with_f_1_0_1() -> Result<()> { fn register_new_peer() -> Result<()> { let (_rt, network, _) = Network::start_test_with_runtime(4, Some(11_180)); wait_for_genesis_committed(&network.clients(), 0); - let pipeline_time = Configuration::pipeline_time(); + let pipeline_time = Config::pipeline_time(); let mut peer_clients: Vec<_> = Network::peers(&network) .zip(Network::clients(&network)) @@ -38,13 +38,13 @@ fn register_new_peer() -> Result<()> { check_status(&peer_clients, 1); // Start new peer - let mut configuration = Configuration::test(); + let mut configuration = Config::test(); configuration.sumeragi.trusted_peers = unique_vec![peer_clients.choose(&mut thread_rng()).unwrap().0.id.clone()]; let rt = Runtime::test(); let new_peer = rt.block_on( PeerBuilder::new() - .with_configuration(configuration) + .with_config(configuration) .with_into_genesis(WithGenesis::None) .with_port(11_225) .start(), @@ -75,7 +75,7 @@ fn connected_peers_with_f(faults: u64, start_port: Option) -> Result<()> { start_port, ); wait_for_genesis_committed(&network.clients(), 0); - let pipeline_time = Configuration::pipeline_time(); + let pipeline_time = Config::pipeline_time(); let mut peer_clients: Vec<_> = Network::peers(&network) .zip(Network::clients(&network)) diff --git a/client/tests/integration/events/pipeline.rs b/client/tests/integration/events/pipeline.rs index 5474401d757..aa546b63153 100644 --- a/client/tests/integration/events/pipeline.rs +++ b/client/tests/integration/events/pipeline.rs @@ -8,7 +8,7 @@ use iroha_client::{ prelude::*, }, }; -use iroha_config::parameters::actual::Root as Configuration; +use iroha_config::parameters::actual::Root as Config; use test_network::*; // Needed to re-enable ignored tests. @@ -41,7 +41,7 @@ fn test_with_instruction_and_status_and_port( Network::start_test_with_runtime(PEER_COUNT.try_into().unwrap(), Some(port)); let clients = network.clients(); wait_for_genesis_committed(&clients, 0); - let pipeline_time = Configuration::pipeline_time(); + let pipeline_time = Config::pipeline_time(); client.submit_all_blocking( ParametersBuilder::new() diff --git a/client/tests/integration/multiple_blocks_created.rs b/client/tests/integration/multiple_blocks_created.rs index 47bb1e21912..1b56abe5590 100644 --- a/client/tests/integration/multiple_blocks_created.rs +++ b/client/tests/integration/multiple_blocks_created.rs @@ -9,7 +9,7 @@ use iroha_client::{ prelude::*, }, }; -use iroha_config::parameters::actual::Root as Configuration; +use iroha_config::parameters::actual::Root as Config; use test_network::*; const N_BLOCKS: usize = 510; @@ -20,7 +20,7 @@ fn long_multiple_blocks_created() -> Result<()> { // Given let (_rt, network, client) = Network::start_test_with_runtime(4, Some(10_965)); wait_for_genesis_committed(&network.clients(), 0); - let pipeline_time = Configuration::pipeline_time(); + let pipeline_time = Config::pipeline_time(); client.submit_all_blocking( ParametersBuilder::new() diff --git a/client/tests/integration/multisignature_account.rs b/client/tests/integration/multisignature_account.rs index 9490ddb1f00..c057ef04ea1 100644 --- a/client/tests/integration/multisignature_account.rs +++ b/client/tests/integration/multisignature_account.rs @@ -6,14 +6,14 @@ use iroha_client::{ crypto::KeyPair, data_model::prelude::*, }; -use iroha_config::parameters::actual::Root as Configuration; +use iroha_config::parameters::actual::Root as Config; use test_network::*; #[test] fn transaction_signed_by_new_signatory_of_account_should_pass() -> Result<()> { let (_rt, peer, client) = ::new().with_port(10_605).start_with_runtime(); wait_for_genesis_committed(&[client.clone()], 0); - let pipeline_time = Configuration::pipeline_time(); + let pipeline_time = Config::pipeline_time(); // Given let account_id: AccountId = "alice@wonderland".parse().expect("Valid"); diff --git a/client/tests/integration/multisignature_transaction.rs b/client/tests/integration/multisignature_transaction.rs index 7bcd2134f6e..66c218e32ff 100644 --- a/client/tests/integration/multisignature_transaction.rs +++ b/client/tests/integration/multisignature_transaction.rs @@ -3,14 +3,14 @@ use std::{str::FromStr as _, thread, time::Duration}; use eyre::Result; use iroha_client::{ client::{self, Client, QueryResult}, - config::Config as ClientConfiguration, + config::Config as ClientConfig, crypto::KeyPair, data_model::{ parameter::{default::MAX_TRANSACTIONS_IN_BLOCK, ParametersBuilder}, prelude::*, }, }; -use iroha_config::parameters::actual::Root as Configuration; +use iroha_config::parameters::actual::Root as Config; use test_network::*; #[allow(clippy::too_many_lines)] @@ -18,7 +18,7 @@ use test_network::*; fn multisignature_transactions_should_wait_for_all_signatures() -> Result<()> { let (_rt, network, client) = Network::start_test_with_runtime(4, Some(10_945)); wait_for_genesis_committed(&network.clients(), 0); - let pipeline_time = Configuration::pipeline_time(); + let pipeline_time = Config::pipeline_time(); client.submit_all_blocking( ParametersBuilder::new() @@ -39,8 +39,8 @@ fn multisignature_transactions_should_wait_for_all_signatures() -> Result<()> { alice_id.clone(), ); - let mut client_configuration = ClientConfiguration::test(&network.genesis.api_address); - let client = Client::new(client_configuration.clone()); + let mut client_config = ClientConfig::test(&network.genesis.api_address); + let client = Client::new(client_config.clone()); let instructions: [InstructionBox; 2] = [create_asset.into(), set_signature_condition.into()]; client.submit_all_blocking(instructions)?; @@ -49,22 +49,22 @@ fn multisignature_transactions_should_wait_for_all_signatures() -> Result<()> { let asset_id = AssetId::new(asset_definition_id, alice_id.clone()); let mint_asset = Mint::asset_quantity(quantity, asset_id.clone()); - client_configuration.account_id = alice_id.clone(); - client_configuration.key_pair = alice_key_pair; - let client = Client::new(client_configuration.clone()); + client_config.account_id = alice_id.clone(); + client_config.key_pair = alice_key_pair; + let client = Client::new(client_config.clone()); let instructions = [mint_asset.clone()]; let transaction = client.build_transaction(instructions, UnlimitedMetadata::new()); client.submit_transaction(&client.sign_transaction(transaction))?; thread::sleep(pipeline_time); //Then - client_configuration.torii_api_url = format!( + client_config.torii_api_url = format!( "http://{}", &network.peers.values().last().unwrap().api_address, ) .parse() .unwrap(); - let client_1 = Client::new(client_configuration.clone()); + let client_1 = Client::new(client_config.clone()); let request = client::asset::by_account_id(alice_id); let assets = client_1 .request(request.clone())? @@ -75,8 +75,8 @@ fn multisignature_transactions_should_wait_for_all_signatures() -> Result<()> { "Multisignature transaction was committed before all required signatures were added" ); - client_configuration.key_pair = key_pair_2; - let client_2 = Client::new(client_configuration); + client_config.key_pair = key_pair_2; + let client_2 = Client::new(client_config); let instructions = [mint_asset]; let transaction = client_2.build_transaction(instructions, UnlimitedMetadata::new()); let transaction = client_2 diff --git a/client/tests/integration/offline_peers.rs b/client/tests/integration/offline_peers.rs index 2d9aa5b6375..d7ce1b1fc57 100644 --- a/client/tests/integration/offline_peers.rs +++ b/client/tests/integration/offline_peers.rs @@ -6,7 +6,7 @@ use iroha_client::{ prelude::*, }, }; -use iroha_config::parameters::actual::Root as Configuration; +use iroha_config::parameters::actual::Root as Config; use iroha_crypto::KeyPair; use iroha_primitives::addr::socket_addr; use test_network::*; @@ -47,7 +47,7 @@ fn register_offline_peer() -> Result<()> { let (_rt, network, client) = Network::start_test_with_runtime(n_peers, Some(11_160)); wait_for_genesis_committed(&network.clients(), 0); - let pipeline_time = Configuration::pipeline_time(); + let pipeline_time = Config::pipeline_time(); let peer_clients = Network::clients(&network); check_status(&peer_clients, 1); diff --git a/client/tests/integration/restart_peer.rs b/client/tests/integration/restart_peer.rs index dcf322d543c..00433722636 100644 --- a/client/tests/integration/restart_peer.rs +++ b/client/tests/integration/restart_peer.rs @@ -5,7 +5,7 @@ use iroha_client::{ client::{self, Client, QueryResult}, data_model::prelude::*, }; -use iroha_config::parameters::actual::Root as Configuration; +use iroha_config::parameters::actual::Root as Config; use rand::{seq::SliceRandom, thread_rng, Rng}; use test_network::*; use tokio::runtime::Runtime; @@ -21,7 +21,7 @@ fn restarted_peer_should_have_the_same_asset_amount() -> Result<()> { let (_rt, network, _) = Network::start_test_with_runtime(n_peers, Some(11_205)); wait_for_genesis_committed(&network.clients(), 0); - let pipeline_time = Configuration::pipeline_time(); + let pipeline_time = Config::pipeline_time(); let peer_clients = Network::clients(&network); let create_asset = diff --git a/client/tests/integration/tx_history.rs b/client/tests/integration/tx_history.rs index e7fa1ca2243..c148b3410af 100644 --- a/client/tests/integration/tx_history.rs +++ b/client/tests/integration/tx_history.rs @@ -9,7 +9,7 @@ use iroha_client::{ client::{transaction, QueryResult}, data_model::{prelude::*, query::Pagination}, }; -use iroha_config::parameters::actual::Root as Configuration; +use iroha_config::parameters::actual::Root as Config; use test_network::*; #[ignore = "ignore, more in #2851"] @@ -18,7 +18,7 @@ fn client_has_rejected_and_acepted_txs_should_return_tx_history() -> Result<()> let (_rt, _peer, client) = ::new().with_port(10_715).start_with_runtime(); wait_for_genesis_committed(&vec![client.clone()], 0); - let pipeline_time = Configuration::pipeline_time(); + let pipeline_time = Config::pipeline_time(); // Given let account_id = AccountId::from_str("alice@wonderland")?; diff --git a/client/tests/integration/unregister_peer.rs b/client/tests/integration/unregister_peer.rs index 2f856d38d6a..e73112ae920 100644 --- a/client/tests/integration/unregister_peer.rs +++ b/client/tests/integration/unregister_peer.rs @@ -9,7 +9,7 @@ use iroha_client::{ prelude::*, }, }; -use iroha_config::parameters::actual::Root as Configuration; +use iroha_config::parameters::actual::Root as Config; use test_network::*; // Note the test is marked as `unstable`, not the network. @@ -60,7 +60,7 @@ fn check_assets( iroha_client .poll_request_with_period( client::asset::by_account_id(account_id.clone()), - Configuration::block_sync_gossip_time(), + Config::block_sync_gossip_time(), 15, |result| { let assets = result.collect::>>().expect("Valid"); @@ -100,7 +100,7 @@ fn init() -> Result<( AssetDefinitionId, )> { let (rt, network, client) = Network::start_test_with_runtime(4, Some(10_925)); - let pipeline_time = Configuration::pipeline_time(); + let pipeline_time = Config::pipeline_time(); iroha_logger::info!("Started"); let parameters = ParametersBuilder::new() .add_parameter(MAX_TRANSACTIONS_IN_BLOCK, 1u32)? diff --git a/client/tests/integration/unstable_network.rs b/client/tests/integration/unstable_network.rs index 3fd793a5c8f..bfccf0bbad7 100644 --- a/client/tests/integration/unstable_network.rs +++ b/client/tests/integration/unstable_network.rs @@ -5,7 +5,7 @@ use iroha_client::{ client::{self, Client, QueryResult}, data_model::{prelude::*, Level}, }; -use iroha_config::parameters::actual::Root as Configuration; +use iroha_config::parameters::actual::Root as Config; use rand::seq::SliceRandom; use test_network::*; use tokio::runtime::Runtime; @@ -52,7 +52,7 @@ fn unstable_network( let rt = Runtime::test(); // Given let (network, iroha_client) = rt.block_on(async { - let mut configuration = Configuration::test(); + let mut configuration = Config::test(); configuration.chain_wide.max_transactions_in_block = MAX_TRANSACTIONS_IN_BLOCK.try_into().unwrap(); configuration.logger.level = Level::INFO; @@ -73,7 +73,7 @@ fn unstable_network( }); wait_for_genesis_committed(&network.clients(), n_offline_peers); - let pipeline_time = Configuration::pipeline_time(); + let pipeline_time = Config::pipeline_time(); let account_id: AccountId = "alice@wonderland".parse().expect("Valid"); let asset_definition_id: AssetDefinitionId = "camomile#wonderland".parse().expect("Valid"); @@ -113,7 +113,7 @@ fn unstable_network( iroha_client .poll_request_with_period( client::asset::by_account_id(account_id.clone()), - Configuration::pipeline_time(), + Config::pipeline_time(), 4, |result| { let assets = result.collect::>>().expect("Valid"); diff --git a/client_cli/src/main.rs b/client_cli/src/main.rs index 33725bc449f..25e8eaf93c0 100644 --- a/client_cli/src/main.rs +++ b/client_cli/src/main.rs @@ -135,7 +135,7 @@ trait RunContext { /// Get access to configuration fn configuration(&self) -> &Config; - fn client_from_configuration(&self) -> Client { + fn client_from_config(&self) -> Client { Client::new(self.configuration().clone()) } @@ -238,7 +238,7 @@ fn submit( metadata: UnlimitedMetadata, context: &mut dyn RunContext, ) -> Result<()> { - let iroha_client = context.client_from_configuration(); + let iroha_client = context.client_from_config(); let instructions = instructions.into(); let tx = iroha_client.build_transaction(instructions, metadata); let transactions = if context.skip_mst_check() { @@ -323,7 +323,7 @@ mod events { } fn listen(filter: FilterBox, context: &mut dyn RunContext) -> Result<()> { - let iroha_client = context.client_from_configuration(); + let iroha_client = context.client_from_config(); eprintln!("Listening to events with filter: {filter:?}"); iroha_client .listen_for_events(filter) @@ -353,7 +353,7 @@ mod blocks { } fn listen(height: NonZeroU64, context: &mut dyn RunContext) -> Result<()> { - let iroha_client = context.client_from_configuration(); + let iroha_client = context.client_from_config(); eprintln!("Listening to blocks from height: {height}"); iroha_client .listen_for_blocks(height) @@ -418,7 +418,7 @@ mod domain { impl RunArgs for List { fn run(self, context: &mut dyn RunContext) -> Result<()> { - let client = context.client_from_configuration(); + let client = context.client_from_config(); let vec = match self { Self::All => client @@ -654,7 +654,7 @@ mod account { impl RunArgs for List { fn run(self, context: &mut dyn RunContext) -> Result<()> { - let client = context.client_from_configuration(); + let client = context.client_from_config(); let vec = match self { Self::All => client @@ -724,7 +724,7 @@ mod account { impl RunArgs for ListPermissions { fn run(self, context: &mut dyn RunContext) -> Result<()> { - let client = context.client_from_configuration(); + let client = context.client_from_config(); let find_all_permissions = FindPermissionTokensByAccountId::new(self.id); let permissions = client .request(find_all_permissions) @@ -911,7 +911,7 @@ mod asset { impl RunArgs for Get { fn run(self, context: &mut dyn RunContext) -> Result<()> { let Self { asset_id } = self; - let iroha_client = context.client_from_configuration(); + let iroha_client = context.client_from_config(); let asset = iroha_client .request(asset::by_id(asset_id)) .wrap_err("Failed to get asset.")?; @@ -931,7 +931,7 @@ mod asset { impl RunArgs for List { fn run(self, context: &mut dyn RunContext) -> Result<()> { - let client = context.client_from_configuration(); + let client = context.client_from_config(); let vec = match self { Self::All => client @@ -1005,7 +1005,7 @@ mod asset { impl RunArgs for GetKeyValue { fn run(self, context: &mut dyn RunContext) -> Result<()> { let Self { asset_id, key } = self; - let client = context.client_from_configuration(); + let client = context.client_from_config(); let find_key_value = FindAssetKeyValueByIdAndKey::new(asset_id, key); let asset = client .request(find_key_value) diff --git a/config/src/client_api.rs b/config/src/client_api.rs index 096c4b4b8d2..f87bc5b7a41 100644 --- a/config/src/client_api.rs +++ b/config/src/client_api.rs @@ -2,8 +2,8 @@ //! //! Intended usage: //! -//! - Create [`ConfigurationDTO`] from [`crate::iroha::Configuration`] and serialize it for the client -//! - Deserialize [`ConfigurationDTO`] from the client and use [`ConfigurationDTO::apply_update()`] to update the configuration +//! - Create [`ConfigDTO`] from [`crate::iroha::Configuration`] and serialize it for the client +//! - Deserialize [`ConfigDTO`] from the client and use [`ConfigDTO::apply_update()`] to update the configuration // TODO: Currently logic here is not generalised and handles only `logger.level` parameter. In future, when // other parts of configuration are refactored and there is a solid foundation e.g. as a general // configuration-related crate, this part should be re-written in a clean way. @@ -12,17 +12,17 @@ use iroha_data_model::Level; use serde::{Deserialize, Serialize}; -use crate::parameters::actual::{Logger as BaseLogger, Root as BaseConfiguration}; +use crate::parameters::actual::{Logger as BaseLogger, Root as BaseConfig}; /// Subset of [`super::iroha`] configuration. #[derive(Debug, Serialize, Deserialize, Clone, Copy)] -pub struct ConfigurationDTO { +pub struct ConfigDTO { #[allow(missing_docs)] pub logger: Logger, } -impl From<&'_ BaseConfiguration> for ConfigurationDTO { - fn from(value: &'_ BaseConfiguration) -> Self { +impl From<&'_ BaseConfig> for ConfigDTO { + fn from(value: &'_ BaseConfig) -> Self { Self { logger: (&value.logger).into(), } @@ -48,7 +48,7 @@ mod test { #[test] fn snapshot_serialized_form() { - let value = ConfigurationDTO { + let value = ConfigDTO { logger: Logger { level: Level::TRACE, }, diff --git a/core/src/executor.rs b/core/src/executor.rs index 10a076fdb0e..70eb4c5fc0a 100644 --- a/core/src/executor.rs +++ b/core/src/executor.rs @@ -157,7 +157,7 @@ impl Executor { let runtime = wasm::RuntimeBuilder::::new() .with_engine(wsv.engine.clone()) // Cloning engine is cheap, see [`wasmtime::Engine`] docs - .with_configuration(wsv.config.wasm_runtime) + .with_config(wsv.config.wasm_runtime) .build()?; runtime.execute_executor_validate_transaction( @@ -191,7 +191,7 @@ impl Executor { let runtime = wasm::RuntimeBuilder::::new() .with_engine(wsv.engine.clone()) // Cloning engine is cheap, see [`wasmtime::Engine`] docs - .with_configuration(wsv.config.wasm_runtime) + .with_config(wsv.config.wasm_runtime) .build()?; runtime.execute_executor_validate_instruction( @@ -224,7 +224,7 @@ impl Executor { Self::UserProvided(UserProvidedExecutor(loaded_executor)) => { let runtime = wasm::RuntimeBuilder::::new() .with_engine(wsv.engine.clone()) // Cloning engine is cheap, see [`wasmtime::Engine`] docs - .with_configuration(wsv.config.wasm_runtime) + .with_config(wsv.config.wasm_runtime) .build()?; runtime.execute_executor_validate_query( @@ -259,7 +259,7 @@ impl Executor { let runtime = wasm::RuntimeBuilder::::new() .with_engine(wsv.engine.clone()) // Cloning engine is cheap, see [`wasmtime::Engine`] docs - .with_configuration(wsv.config.wasm_runtime) + .with_config(wsv.config.wasm_runtime) .build()?; runtime diff --git a/core/src/kiso.rs b/core/src/kiso.rs index ca93ae0819c..1f6b0c7204d 100644 --- a/core/src/kiso.rs +++ b/core/src/kiso.rs @@ -1,6 +1,6 @@ //! Actor responsible for configuration state and its dynamic updates. //! -//! Currently the API exposed by [`KisoHandle`] works only with [`ConfigurationDTO`], because +//! Currently the API exposed by [`KisoHandle`] works only with [`ConfigDTO`], because //! no any part of Iroha is interested in the whole state. However, the API could be extended //! in future. //! @@ -9,7 +9,7 @@ use eyre::Result; use iroha_config::{ - client_api::{ConfigurationDTO, Logger as LoggerDTO}, + client_api::{ConfigDTO, Logger as LoggerDTO}, parameters::actual::Root as Config, }; use iroha_logger::Level; @@ -42,11 +42,11 @@ impl KisoHandle { } } - /// Fetch the [`ConfigurationDTO`] from the actor's state. + /// Fetch the [`ConfigDTO`] from the actor's state. /// /// # Errors /// If communication with actor fails. - pub async fn get_dto(&self) -> Result { + pub async fn get_dto(&self) -> Result { let (tx, rx) = oneshot::channel(); let msg = Message::GetDTO { respond_to: tx }; let _ = self.actor.send(msg).await; @@ -61,7 +61,7 @@ impl KisoHandle { /// /// # Errors /// If communication with actor fails. - pub async fn update_with_dto(&self, dto: ConfigurationDTO) -> Result<(), Error> { + pub async fn update_with_dto(&self, dto: ConfigDTO) -> Result<(), Error> { let (tx, rx) = oneshot::channel(); let msg = Message::UpdateWithDTO { dto, @@ -86,10 +86,10 @@ impl KisoHandle { enum Message { GetDTO { - respond_to: oneshot::Sender, + respond_to: oneshot::Sender, }, UpdateWithDTO { - dto: ConfigurationDTO, + dto: ConfigDTO, respond_to: oneshot::Sender>, }, SubscribeOnLogLevel { @@ -124,12 +124,12 @@ impl Actor { fn handle_message(&mut self, msg: Message) { match msg { Message::GetDTO { respond_to } => { - let dto = ConfigurationDTO::from(&self.state); + let dto = ConfigDTO::from(&self.state); let _ = respond_to.send(dto); } Message::UpdateWithDTO { dto: - ConfigurationDTO { + ConfigDTO { logger: LoggerDTO { level: new_level }, }, respond_to, @@ -151,7 +151,7 @@ mod tests { use std::time::Duration; use iroha_config::{ - client_api::{ConfigurationDTO, Logger as LoggerDTO}, + client_api::{ConfigDTO, Logger as LoggerDTO}, parameters::actual::Root, }; @@ -189,7 +189,7 @@ mod tests { .await .expect_err("Watcher should not be active initially"); - kiso.update_with_dto(ConfigurationDTO { + kiso.update_with_dto(ConfigDTO { logger: LoggerDTO { level: NEW_LOG_LEVEL, }, diff --git a/core/src/queue.rs b/core/src/queue.rs index 0598e0415f4..3beab0a9546 100644 --- a/core/src/queue.rs +++ b/core/src/queue.rs @@ -115,11 +115,7 @@ impl Queue { !self.is_expired(tx) && !tx.is_in_blockchain(wsv) } - /// Checks if this transaction is waiting longer than specified in - /// `transaction_time_to_live` from `QueueConfiguration` or - /// `time_to_live_ms` of this transaction. Meaning that the - /// transaction will be expired as soon as the lesser of the - /// specified TTLs was reached. + /// Checks if the transaction is waiting longer than its TTL or than the TTL from [`Config`]. pub fn is_expired(&self, tx: &AcceptedTransaction) -> bool { let tx_creation_time = tx.as_ref().creation_time(); diff --git a/core/src/smartcontracts/wasm.rs b/core/src/smartcontracts/wasm.rs index c7d9c6fa84d..27694c3ca6d 100644 --- a/core/src/smartcontracts/wasm.rs +++ b/core/src/smartcontracts/wasm.rs @@ -1427,7 +1427,7 @@ impl RuntimeBuilder { /// Sets the [`Configuration`] to be used by the [`Runtime`] #[must_use] #[inline] - pub fn with_configuration(mut self, config: IrohaWasmConfig) -> Self { + pub fn with_config(mut self, config: IrohaWasmConfig) -> Self { self.config = Some(config); self } diff --git a/core/src/wsv.rs b/core/src/wsv.rs index cc7086d6a24..17dee33d3df 100644 --- a/core/src/wsv.rs +++ b/core/src/wsv.rs @@ -521,7 +521,7 @@ impl WorldStateView { } Wasm(LoadedWasm { module, .. }) => { let mut wasm_runtime = wasm::RuntimeBuilder::::new() - .with_configuration(self.config.wasm_runtime) + .with_config(self.config.wasm_runtime) .with_engine(self.engine.clone()) // Cloning engine is cheap .build()?; wasm_runtime @@ -584,7 +584,7 @@ impl WorldStateView { } Executable::Wasm(bytes) => { let mut wasm_runtime = wasm::RuntimeBuilder::::new() - .with_configuration(self.config.wasm_runtime) + .with_config(self.config.wasm_runtime) .with_engine(self.engine.clone()) // Cloning engine is cheap .build()?; wasm_runtime diff --git a/core/test_network/src/lib.rs b/core/test_network/src/lib.rs index a18fa81822c..5536d6edca4 100644 --- a/core/test_network/src/lib.rs +++ b/core/test_network/src/lib.rs @@ -9,10 +9,10 @@ use futures::{prelude::*, stream::FuturesUnordered}; use iroha::Iroha; use iroha_client::{ client::{Client, QueryOutput}, - config::Config as ClientConfiguration, + config::Config as ClientConfig, data_model::{isi::Instruction, peer::Peer as DataModelPeer, prelude::*, query::Query, Level}, }; -use iroha_config::parameters::actual::Root as Configuration; +use iroha_config::parameters::actual::Root as Config; use iroha_crypto::prelude::*; use iroha_data_model::ChainId; use iroha_genesis::{GenesisNetwork, RawGenesisBlock}; @@ -76,7 +76,7 @@ pub trait TestGenesis: Sized { impl TestGenesis for GenesisNetwork { fn test_with_instructions(extra_isi: impl IntoIterator) -> Self { - let cfg = Configuration::test(); + let cfg = Config::test(); // TODO: Fix this somehow. Probably we need to make `kagami` a library (#3253). let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR")); @@ -175,16 +175,12 @@ impl Network { offline_peers: u32, start_port: Option, ) -> (Self, Client) { - let mut configuration = Configuration::test(); - configuration.logger.level = Level::INFO; - let network = Network::new_with_offline_peers( - Some(configuration), - n_peers, - offline_peers, - start_port, - ) - .await - .expect("Failed to init peers"); + let mut config = Config::test(); + config.logger.level = Level::INFO; + let network = + Network::new_with_offline_peers(Some(config), n_peers, offline_peers, start_port) + .await + .expect("Failed to init peers"); let client = Client::test( &Network::peers(&network) .choose(&mut thread_rng()) @@ -215,17 +211,17 @@ impl Network { .api_address, ); - let mut config = Configuration::test(); + let mut config = Config::test(); config.sumeragi.trusted_peers = UniqueVec::from_iter(self.peers().map(|peer| &peer.id).cloned()); let peer = PeerBuilder::new() - .with_configuration(config) + .with_config(config) .with_genesis(GenesisNetwork::test()) .start() .await; - time::sleep(Configuration::pipeline_time() + Configuration::block_sync_gossip_time()).await; + time::sleep(Config::pipeline_time() + Config::block_sync_gossip_time()).await; let add_peer = Register::peer(DataModelPeer::new(peer.id.clone())); client.submit(add_peer).expect("Failed to add new peer."); @@ -245,7 +241,7 @@ impl Network { /// - (RARE) Creating new peers and collecting into a [`HashMap`] fails. /// - Creating new [`Peer`] instance fails. pub async fn new_with_offline_peers( - default_configuration: Option, + default_config: Option, n_peers: u32, offline_peers: u32, start_port: Option, @@ -270,12 +266,12 @@ impl Network { .map(PeerBuilder::build) .collect::>>()?; - let mut configuration = default_configuration.unwrap_or_else(Configuration::test); - configuration.sumeragi.trusted_peers = + let mut config = default_config.unwrap_or_else(Config::test); + config.sumeragi.trusted_peers = UniqueVec::from_iter(peers.iter().map(|peer| peer.id.clone())); let mut genesis_peer = peers.remove(0); - let genesis_builder = builders.remove(0).with_configuration(configuration.clone()); + let genesis_builder = builders.remove(0).with_config(config.clone()); // Offset by one to account for genesis let online_peers = n_peers - offline_peers - 1; @@ -289,11 +285,7 @@ impl Network { .zip(peers.iter_mut()) .choose_multiple(rng, online_peers as usize) { - futures.push( - builder - .with_configuration(configuration.clone()) - .start_with_peer(peer), - ); + futures.push(builder.with_config(config.clone()).start_with_peer(peer)); } futures.collect::<()>().await; @@ -397,32 +389,32 @@ impl Drop for Peer { impl Peer { /// Returns per peer config with all addresses, keys, and id set up. - fn get_config(&self, configuration: Configuration) -> Configuration { + fn get_config(&self, config: Config) -> Config { use iroha_config::parameters::actual::{Common, Torii}; - Configuration { + Config { common: Common { key_pair: self.key_pair.clone(), p2p_address: self.p2p_address.clone(), - ..configuration.common + ..config.common }, torii: Torii { address: self.api_address.clone(), - ..configuration.torii + ..config.torii }, - ..configuration + ..config } } /// Starts a peer with arguments. async fn start( &mut self, - configuration: Configuration, + config: Config, genesis: Option, temp_dir: Arc, ) { - let mut configuration = self.get_config(configuration); - configuration.kura.block_store_path = temp_dir.path().to_str().unwrap().into(); + let mut config = self.get_config(config); + config.kura.block_store_path = temp_dir.path().to_str().unwrap().into(); let info_span = iroha_logger::info_span!( "test-peer", p2p_addr = %self.p2p_address, @@ -433,7 +425,7 @@ impl Peer { let handle = task::spawn( async move { - let mut iroha = Iroha::new(configuration, genesis, logger) + let mut iroha = Iroha::new(config, genesis, logger) .await .expect("Failed to start iroha"); let job_handle = iroha.start_as_task().unwrap(); @@ -516,7 +508,7 @@ impl>> From for WithGenesis { /// `PeerBuilder`. #[derive(Default)] pub struct PeerBuilder { - configuration: Option, + config: Option, genesis: WithGenesis, temp_dir: Option>, port: Option, @@ -560,8 +552,8 @@ impl PeerBuilder { /// Set Iroha configuration #[must_use] - pub fn with_configuration(mut self, configuration: Configuration) -> Self { - self.configuration = Some(configuration); + pub fn with_config(mut self, config: Config) -> Self { + self.config = Some(config); self } @@ -595,8 +587,8 @@ impl PeerBuilder { /// Accept a peer and starts it. pub async fn start_with_peer(self, peer: &mut Peer) { - let configuration = self.configuration.unwrap_or_else(|| { - let mut config = Configuration::test(); + let config = self.config.unwrap_or_else(|| { + let mut config = Config::test(); config.sumeragi.trusted_peers = unique_vec![peer.id.clone()]; config }); @@ -609,7 +601,7 @@ impl PeerBuilder { .temp_dir .unwrap_or_else(|| Arc::new(TempDir::new().expect("Failed to create temp dir."))); - peer.start(configuration, genesis, temp_dir).await; + peer.start(config, genesis, temp_dir).await; } /// Create and start a peer with preapplied arguments. @@ -621,16 +613,13 @@ impl PeerBuilder { /// Create and start a peer, create a client and connect it to the peer and return both. pub async fn start_with_client(self) -> (Peer, Client) { - let configuration = self - .configuration - .clone() - .unwrap_or_else(Configuration::test); + let config = self.config.clone().unwrap_or_else(Config::test); let peer = self.start().await; let client = Client::test(&peer.api_address); - time::sleep(configuration.chain_wide.pipeline_time()).await; + time::sleep(config.chain_wide.pipeline_time()).await; (peer, client) } @@ -656,7 +645,7 @@ pub trait TestRuntime { } /// Peer configuration mocking trait. -pub trait TestConfiguration { +pub trait TestConfig { /// Creates test configuration fn test() -> Self; /// Returns default pipeline time. @@ -666,7 +655,7 @@ pub trait TestConfiguration { } /// Client configuration mocking trait. -pub trait TestClientConfiguration { +pub trait TestClientConfig { /// Creates test client configuration fn test(api_address: &SocketAddr) -> Self; } @@ -756,7 +745,7 @@ impl TestRuntime for Runtime { } } -impl TestConfiguration for Configuration { +impl TestConfig for Config { fn test() -> Self { use iroha_config::{ base::{FromEnv as _, StdEnv, UnwrapPartial as _}, @@ -792,7 +781,7 @@ impl TestConfiguration for Configuration { } } -impl TestClientConfiguration for ClientConfiguration { +impl TestClientConfig for ClientConfig { fn test(api_address: &SocketAddr) -> Self { iroha_client::samples::get_client_config( get_chain_id(), @@ -806,20 +795,20 @@ impl TestClientConfiguration for ClientConfiguration { impl TestClient for Client { fn test(api_addr: &SocketAddr) -> Self { - Client::new(ClientConfiguration::test(api_addr)) + Client::new(ClientConfig::test(api_addr)) } fn test_with_key(api_addr: &SocketAddr, keys: KeyPair) -> Self { - let mut configuration = ClientConfiguration::test(api_addr); - configuration.key_pair = keys; - Client::new(configuration) + let mut config = ClientConfig::test(api_addr); + config.key_pair = keys; + Client::new(config) } fn test_with_account(api_addr: &SocketAddr, keys: KeyPair, account_id: &AccountId) -> Self { - let mut configuration = ClientConfiguration::test(api_addr); - configuration.account_id = account_id.clone(); - configuration.key_pair = keys; - Client::new(configuration) + let mut config = ClientConfig::test(api_addr); + config.account_id = account_id.clone(); + config.key_pair = keys; + Client::new(config) } fn for_each_event(self, event_filter: FilterBox, f: impl Fn(Result)) { @@ -896,6 +885,6 @@ impl TestClient for Client { ::Target: core::fmt::Debug, >::Error: Into, { - self.poll_request_with_period(request, Configuration::pipeline_time() / 2, 10, f) + self.poll_request_with_period(request, Config::pipeline_time() / 2, 10, f) } } diff --git a/torii/src/routing.rs b/torii/src/routing.rs index 2aef903af42..fe72ff0e27d 100644 --- a/torii/src/routing.rs +++ b/torii/src/routing.rs @@ -8,7 +8,7 @@ #[cfg(feature = "telemetry")] use eyre::{eyre, WrapErr}; use futures::TryStreamExt; -use iroha_config::client_api::ConfigurationDTO; +use iroha_config::client_api::ConfigDTO; use iroha_core::{ query::store::LiveQueryStoreHandle, smartcontracts::query::ValidQueryRequest, sumeragi::SumeragiHandle, @@ -182,10 +182,7 @@ pub async fn handle_get_configuration(kiso: KisoHandle) -> Result { } #[iroha_futures::telemetry_future] -pub async fn handle_post_configuration( - kiso: KisoHandle, - value: ConfigurationDTO, -) -> Result { +pub async fn handle_post_configuration(kiso: KisoHandle, value: ConfigDTO) -> Result { kiso.update_with_dto(value).await?; Ok(reply::with_status(reply::reply(), StatusCode::ACCEPTED)) } From 8e9ddf0dcb09abbbd67533f12482f9ae462a3731 Mon Sep 17 00:00:00 2001 From: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> Date: Wed, 14 Feb 2024 06:59:07 +0900 Subject: [PATCH 74/94] [fix]: update default wasm fuel limit Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- config/src/parameters/defaults.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/src/parameters/defaults.rs b/config/src/parameters/defaults.rs index c82c13bfb8a..6ccdf2b1e0d 100644 --- a/config/src/parameters/defaults.rs +++ b/config/src/parameters/defaults.rs @@ -58,7 +58,7 @@ pub mod chain_wide { pub const DEFAULT_MAX_TXS: NonZeroU32 = nonzero!(2_u32.pow(9)); pub const DEFAULT_BLOCK_TIME: Duration = Duration::from_secs(2); pub const DEFAULT_COMMIT_TIME: Duration = Duration::from_secs(4); - pub const DEFAULT_WASM_FUEL_LIMIT: u64 = 30_000_000; + pub const DEFAULT_WASM_FUEL_LIMIT: u64 = 55_000_000; // TODO: wrap into a `Bytes` newtype pub const DEFAULT_WASM_MAX_MEMORY_BYTES: u32 = 500 * 2_u32.pow(20); From 881f60c1a87b6e240984ba29bf19da43a504370b Mon Sep 17 00:00:00 2001 From: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> Date: Wed, 14 Feb 2024 07:02:57 +0900 Subject: [PATCH 75/94] [refactor]: change docs and methods of `WebLogin` Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- client/src/config.rs | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/client/src/config.rs b/client/src/config.rs index d122435cf01..92b8d9a4b15 100644 --- a/client/src/config.rs +++ b/client/src/config.rs @@ -26,22 +26,17 @@ pub const DEFAULT_TRANSACTION_STATUS_TIMEOUT: Duration = Duration::from_secs(15) #[allow(missing_docs)] pub const DEFAULT_TRANSACTION_NONCE: bool = false; -/// Wrapper over `SmallStr` to provide basic auth login checking +/// Valid web auth login string. See [`WebLogin::from_str`] #[derive(Debug, Display, Clone, Serialize, PartialEq, Eq)] pub struct WebLogin(SmallStr); -impl WebLogin { - /// Construct new [`Self`] +impl FromStr for WebLogin { + type Err = eyre::ErrReport; + + /// Validates that the string is a valid web login /// /// # Errors /// Fails if `login` contains `:` character, which is the binary representation of the '\0'. - pub fn new(login: &str) -> Result { - Self::from_str(login) - } -} - -impl FromStr for WebLogin { - type Err = eyre::ErrReport; fn from_str(login: &str) -> Result { if login.contains(':') { eyre::bail!("The `:` character, in `{login}` is not allowed"); From e7b66fb5ca9f0e356a43fe4ddd0523a235512418 Mon Sep 17 00:00:00 2001 From: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> Date: Wed, 14 Feb 2024 07:11:48 +0900 Subject: [PATCH 76/94] [fix]: trusted peers and config tests Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- cli/src/samples.rs | 2 +- config/src/parameters/user.rs | 24 ++--------------------- config/src/parameters/user/boilerplate.rs | 22 ++++++++------------- config/tests/fixtures.rs | 6 ++---- 4 files changed, 13 insertions(+), 41 deletions(-) diff --git a/cli/src/samples.rs b/cli/src/samples.rs index 5e20bf61f83..c8561d53831 100644 --- a/cli/src/samples.rs +++ b/cli/src/samples.rs @@ -78,7 +78,7 @@ pub fn get_user_config( .chain_wide .max_transactions_in_block .set(2.try_into().unwrap()); - config.sumeragi.trusted_peers.peers = peers.to_vec(); + config.sumeragi.trusted_peers.set(peers.to_vec()); config.torii.address.set(DEFAULT_TORII_ADDR); config .network diff --git a/config/src/parameters/user.rs b/config/src/parameters/user.rs index 4aac86653a3..e62c418fa0c 100644 --- a/config/src/parameters/user.rs +++ b/config/src/parameters/user.rs @@ -378,7 +378,7 @@ pub struct KuraDebug { #[derive(Debug)] pub struct Sumeragi { - pub trusted_peers: Vec, + pub trusted_peers: Option>, pub debug: SumeragiDebug, } @@ -389,7 +389,7 @@ impl Sumeragi { debug: SumeragiDebug { force_soft_fork }, } = self; - let trusted_peers = construct_unique_vec(trusted_peers)?; + let trusted_peers = construct_unique_vec(trusted_peers.unwrap_or(vec![]))?; Ok(actual::Sumeragi { trusted_peers, @@ -403,26 +403,6 @@ pub struct SumeragiDebug { pub force_soft_fork: bool, } -#[derive(Deserialize, Serialize, Default, PartialEq, Eq, Debug, Clone)] -#[serde(transparent)] -pub struct UserTrustedPeers { - // FIXME: doesn't raise an error on finding duplicates during deserialization - pub peers: Vec, -} - -impl UnwrapPartial for UserTrustedPeers { - type Output = Vec; - fn unwrap_partial(self) -> UnwrapPartialResult { - Ok(self.peers) - } -} - -impl Merge for UserTrustedPeers { - fn merge(&mut self, other: Self) { - self.peers = other.peers; - } -} - // FIXME: handle duplicates properly, not here, and with details fn construct_unique_vec( unchecked: Vec, diff --git a/config/src/parameters/user/boilerplate.rs b/config/src/parameters/user/boilerplate.rs index ae298e6bcf8..dbce1a0ffda 100644 --- a/config/src/parameters/user/boilerplate.rs +++ b/config/src/parameters/user/boilerplate.rs @@ -16,8 +16,10 @@ use iroha_config_base::{ }; use iroha_crypto::{PrivateKey, PublicKey}; use iroha_data_model::{ - metadata::Limits as MetadataLimits, transaction::TransactionLimits, ChainId, LengthLimits, - Level, + metadata::Limits as MetadataLimits, + prelude::{ChainId, PeerId}, + transaction::TransactionLimits, + LengthLimits, Level, }; use iroha_primitives::addr::SocketAddr; use serde::{Deserialize, Serialize}; @@ -31,7 +33,7 @@ use crate::{ user, user::{ ChainWide, Genesis, Kura, KuraDebug, Logger, Network, Queue, Root, Snapshot, Sumeragi, - SumeragiDebug, Telemetry, TelemetryDev, Torii, UserTrustedPeers, + SumeragiDebug, Telemetry, TelemetryDev, Torii, }, }, }; @@ -350,7 +352,7 @@ impl FromEnv for KuraPartial { #[derive(Deserialize, Serialize, Debug, Default, Merge)] #[serde(deny_unknown_fields, default)] pub struct SumeragiPartial { - pub trusted_peers: UserTrustedPeers, + pub trusted_peers: UserField>, pub debug: SumeragiDebugPartial, } @@ -360,14 +362,6 @@ impl UnwrapPartial for SumeragiPartial { fn unwrap_partial(self) -> UnwrapPartialResult { let mut emitter = Emitter::new(); - let trusted_peers = self.trusted_peers.unwrap_partial().map_or_else( - |err| { - emitter.emit_collection(err); - None - }, - Some, - ); - let debug = self.debug.unwrap_partial().map_or_else( |err| { emitter.emit_collection(err); @@ -379,7 +373,7 @@ impl UnwrapPartial for SumeragiPartial { emitter.finish()?; Ok(Sumeragi { - trusted_peers: trusted_peers.unwrap(), + trusted_peers: self.trusted_peers.get(), debug: debug.unwrap(), }) } @@ -573,7 +567,7 @@ impl UnwrapPartial for TelemetryDevPartial { fn unwrap_partial(self) -> UnwrapPartialResult { Ok(TelemetryDev { - file: self.file.get(), + out_file: self.file.get(), }) } } diff --git a/config/tests/fixtures.rs b/config/tests/fixtures.rs index d29199a8116..c3f33e9fa96 100644 --- a/config/tests/fixtures.rs +++ b/config/tests/fixtures.rs @@ -155,7 +155,7 @@ fn minimal_config_snapshot() -> Result<()> { max: 128, }, wasm_runtime: WasmRuntime { - fuel_limit: 30000000, + fuel_limit: 55000000, max_memory_bytes: 524288000, }, }, @@ -330,9 +330,7 @@ fn full_envs_set_is_consumed() -> Result<()> { }, }, sumeragi: SumeragiPartial { - trusted_peers: UserTrustedPeers { - peers: [], - }, + trusted_peers: None, debug: SumeragiDebugPartial { force_soft_fork: None, }, From 5543e43a66e6262f7e76df10b22ccec81ef64b6f Mon Sep 17 00:00:00 2001 From: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> Date: Wed, 14 Feb 2024 07:12:40 +0900 Subject: [PATCH 77/94] [refactor]: rename `telemetry.dev.out_file` Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- cli/src/lib.rs | 2 +- config/src/parameters/actual.rs | 2 +- config/src/parameters/user.rs | 14 ++++++-------- configs/peer.template.toml | 2 +- telemetry/src/dev.rs | 2 +- 5 files changed, 10 insertions(+), 12 deletions(-) diff --git a/cli/src/lib.rs b/cli/src/lib.rs index 20f8c0d9abe..22dfe46850e 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -626,7 +626,7 @@ mod tests { config .dev_telemetry .expect("dev telemetry should be set") - .file + .out_file .absolutize()?, dir.path().join("logs/telemetry") ); diff --git a/config/src/parameters/actual.rs b/config/src/parameters/actual.rs index 920747251e2..fc3c66afea0 100644 --- a/config/src/parameters/actual.rs +++ b/config/src/parameters/actual.rs @@ -243,5 +243,5 @@ pub struct Telemetry { #[derive(Debug, Clone)] #[allow(missing_docs)] pub struct DevTelemetry { - pub file: PathBuf, + pub out_file: PathBuf, } diff --git a/config/src/parameters/user.rs b/config/src/parameters/user.rs index e62c418fa0c..7c906be92b6 100644 --- a/config/src/parameters/user.rs +++ b/config/src/parameters/user.rs @@ -21,17 +21,13 @@ use std::{ pub use boilerplate::*; use eyre::{eyre, Report, WrapErr}; -use iroha_config_base::{ - Emitter, ErrorsCollection, HumanBytes, Merge, ParseEnvResult, ReadEnv, UnwrapPartial, - UnwrapPartialResult, -}; +use iroha_config_base::{Emitter, ErrorsCollection, HumanBytes, Merge, ParseEnvResult, ReadEnv}; use iroha_crypto::{KeyPair, PrivateKey, PublicKey}; use iroha_data_model::{ metadata::Limits as MetadataLimits, peer::PeerId, transaction::TransactionLimits, ChainId, LengthLimits, Level, }; use iroha_primitives::{addr::SocketAddr, unique_vec::UniqueVec}; -use serde::{Deserialize, Serialize}; use url::Url; use crate::{ @@ -504,7 +500,7 @@ pub struct Telemetry { #[derive(Debug)] pub struct TelemetryDev { - pub file: Option, + pub out_file: Option, } impl Telemetry { @@ -514,7 +510,7 @@ impl Telemetry { url, max_retry_delay_exponent, min_retry_period, - dev: TelemetryDev { file }, + dev: TelemetryDev { out_file: file }, } = self; let regular = match (name, url) { @@ -535,7 +531,9 @@ impl Telemetry { } }; - let dev = file.map(|file| actual::DevTelemetry { file: file.clone() }); + let dev = file.map(|file| actual::DevTelemetry { + out_file: file.clone(), + }); Ok((regular, dev)) } diff --git a/configs/peer.template.toml b/configs/peer.template.toml index 0d96be66f8c..b457698925c 100644 --- a/configs/peer.template.toml +++ b/configs/peer.template.toml @@ -69,7 +69,7 @@ [telemetry.dev] ## FIXME: is it JSON5? -# file = "./dev-telemetry.json5" +# out_file = "./dev-telemetry.json5" ## TODO: remove it from the config file entirely # [chain_wide] \ No newline at end of file diff --git a/telemetry/src/dev.rs b/telemetry/src/dev.rs index 9afab4b16c1..257b980165d 100644 --- a/telemetry/src/dev.rs +++ b/telemetry/src/dev.rs @@ -25,7 +25,7 @@ pub async fn start(config: Config, telemetry: Receiver) -> Result Date: Wed, 14 Feb 2024 07:14:48 +0900 Subject: [PATCH 78/94] [docs]: update `GenesisNetwork::new` errors Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- genesis/src/lib.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/genesis/src/lib.rs b/genesis/src/lib.rs index 1b4d2acb403..b9eab5b2b2e 100644 --- a/genesis/src/lib.rs +++ b/genesis/src/lib.rs @@ -43,8 +43,7 @@ impl GenesisNetwork { /// Construct [`GenesisNetwork`] from configuration. /// /// # Errors - /// If fails to sign a transaction (which means that the `key_pair` is malformed rather - /// than anything else) + /// If fails to resolve the executor pub fn new( raw_block: RawGenesisBlock, chain_id: &ChainId, From 36cc10cb207f9c39bfa866090ace56d5b8a4236d Mon Sep 17 00:00:00 2001 From: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> Date: Wed, 14 Feb 2024 07:22:11 +0900 Subject: [PATCH 79/94] [refactor]: use `serde_with` Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- Cargo.lock | 2 ++ client/Cargo.toml | 1 + client/src/config.rs | 29 +++++++++++++++++------------ client/src/config/user.rs | 5 ++--- config/Cargo.toml | 1 + config/base/src/lib.rs | 34 ---------------------------------- config/src/kura.rs | 20 +++++++++++++++----- config/src/logger.rs | 18 +++++++++++++----- 8 files changed, 51 insertions(+), 59 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 902307ffb5b..dc591f7392b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2716,6 +2716,7 @@ dependencies = [ "rand", "serde", "serde_json", + "serde_with", "tempfile", "test_network", "thiserror", @@ -2767,6 +2768,7 @@ dependencies = [ "proptest", "serde", "serde_json", + "serde_with", "stacker", "strum 0.25.0", "thiserror", diff --git a/client/Cargo.toml b/client/Cargo.toml index c747e674069..1d38df505b9 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -63,6 +63,7 @@ http = "0.2.9" url = { workspace = true } rand = { workspace = true } serde = { workspace = true, features = ["derive"] } +serde_with = { workspace = true } serde_json = { workspace = true } base64 = { workspace = true } thiserror = { workspace = true } diff --git a/client/src/config.rs b/client/src/config.rs index 92b8d9a4b15..08b30ee3a83 100644 --- a/client/src/config.rs +++ b/client/src/config.rs @@ -13,6 +13,7 @@ use iroha_crypto::prelude::*; use iroha_data_model::{prelude::*, ChainId}; use iroha_primitives::small::SmallStr; use serde::{Deserialize, Serialize}; +use serde_with::{DeserializeFromStr, SerializeDisplay}; use url::Url; use crate::config::user::RootPartial; @@ -27,7 +28,7 @@ pub const DEFAULT_TRANSACTION_STATUS_TIMEOUT: Duration = Duration::from_secs(15) pub const DEFAULT_TRANSACTION_NONCE: bool = false; /// Valid web auth login string. See [`WebLogin::from_str`] -#[derive(Debug, Display, Clone, Serialize, PartialEq, Eq)] +#[derive(Debug, Display, Clone, PartialEq, Eq, DeserializeFromStr, SerializeDisplay)] pub struct WebLogin(SmallStr); impl FromStr for WebLogin { @@ -46,17 +47,6 @@ impl FromStr for WebLogin { } } -/// Deserializing `WebLogin` with `FromStr` implementation -impl<'de> Deserialize<'de> for WebLogin { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - let s = String::deserialize(deserializer)?; - FromStr::from_str(&s).map_err(serde::de::Error::custom) - } -} - /// Basic Authentication credentials #[derive(Clone, Deserialize, Serialize, Debug, PartialEq, Eq)] pub struct BasicAuth { @@ -93,3 +83,18 @@ impl Config { Ok(config.unwrap_partial()?.parse()?) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn web_login_ok() { + let _ok = WebLogin::from_str("alice").expect("input is valid"); + } + + #[test] + fn web_login_bad() { + let _err = WebLogin::from_str("alice:wonderland").expect_err("input has `:`"); + } +} diff --git a/client/src/config/user.rs b/client/src/config/user.rs index c62f3c2d8ce..30a684e5bac 100644 --- a/client/src/config/user.rs +++ b/client/src/config/user.rs @@ -10,6 +10,7 @@ use iroha_config::base::{Emitter, ErrorsCollection}; use iroha_crypto::{KeyPair, PrivateKey, PublicKey}; use iroha_data_model::{account::AccountId, ChainId}; use merge::Merge; +use serde_with::DeserializeFromStr; use url::Url; use crate::config::BasicAuth; @@ -134,7 +135,7 @@ pub struct Transaction { } /// A [`Url`] that might only have HTTP scheme inside -#[derive(Debug, Clone, Eq, PartialEq)] +#[derive(Debug, Clone, Eq, PartialEq, DeserializeFromStr)] pub struct OnlyHttpUrl(Url); impl FromStr for OnlyHttpUrl { @@ -166,8 +167,6 @@ pub enum ParseHttpUrlError { }, } -iroha_config::base::impl_deserialize_from_str!(OnlyHttpUrl); - #[cfg(test)] mod tests { use std::collections::HashSet; diff --git a/config/Cargo.toml b/config/Cargo.toml index b0d1241da88..6efc37b06ff 100644 --- a/config/Cargo.toml +++ b/config/Cargo.toml @@ -23,6 +23,7 @@ tracing-subscriber = { workspace = true, features = ["fmt", "ansi"] } url = { workspace = true, features = ["serde"] } serde = { workspace = true, features = ["derive"] } +serde_with = { workspace = true } strum = { workspace = true, features = ["derive"] } serde_json = { workspace = true } json5 = { workspace = true } diff --git a/config/base/src/lib.rs b/config/base/src/lib.rs index df1527ada32..490e29b3ccb 100644 --- a/config/base/src/lib.rs +++ b/config/base/src/lib.rs @@ -20,40 +20,6 @@ pub use merge::Merge; pub use serde; use serde::{Deserialize, Serialize}; -/// Implement [`serde::Serialize`] using `Display` for a given type -#[macro_export] -macro_rules! impl_serialize_display { - ($ty:ty) => { - impl $crate::serde::Serialize for $ty { - fn serialize(&self, serializer: S) -> std::result::Result - where - S: serde::Serializer, - { - serializer.collect_str(self) - } - } - }; -} - -/// Implement [`serde::Deserialize`] using [`FromStr`] for a given type -#[macro_export] -macro_rules! impl_deserialize_from_str { - ($ty:ty) => { - impl<'de> $crate::serde::Deserialize<'de> for $ty { - fn deserialize(deserializer: D) -> std::result::Result - where - D: $crate::serde::Deserializer<'de>, - { - use $crate::serde::Deserialize; - - String::deserialize(deserializer)? - .parse() - .map_err($crate::serde::de::Error::custom) - } - } - }; -} - /// [`Duration`], but can parse a human-readable string. /// TODO: currently deserializes just as [`Duration`] #[derive(Debug, Copy, Clone, Deserialize, Serialize, Ord, PartialOrd, Eq, PartialEq)] diff --git a/config/src/kura.rs b/config/src/kura.rs index 655655db656..507e44db3da 100644 --- a/config/src/kura.rs +++ b/config/src/kura.rs @@ -1,9 +1,22 @@ //! Configuration tools related to Kura specifically. -use iroha_config_base::{impl_deserialize_from_str, impl_serialize_display}; +// use iroha_config_base::{impl_deserialize_from_str, impl_serialize_display}; + +use serde_with::{DeserializeFromStr, SerializeDisplay}; /// Kura initialization mode. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, strum::EnumString, strum::Display)] +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Eq, + Default, + strum::EnumString, + strum::Display, + DeserializeFromStr, + SerializeDisplay, +)] #[strum(serialize_all = "snake_case")] pub enum Mode { /// Strict validation of all blocks. @@ -13,9 +26,6 @@ pub enum Mode { Fast, } -impl_serialize_display!(Mode); -impl_deserialize_from_str!(Mode); - #[cfg(test)] mod tests { use crate::kura::Mode; diff --git a/config/src/logger.rs b/config/src/logger.rs index b8a97aa9712..e5038337396 100644 --- a/config/src/logger.rs +++ b/config/src/logger.rs @@ -1,7 +1,7 @@ //! Configuration utils related to Logger specifically. -use iroha_config_base::{impl_deserialize_from_str, impl_serialize_display}; pub use iroha_data_model::Level; +use serde_with::{DeserializeFromStr, SerializeDisplay}; /// Convert [`Level`] into [`tracing::Level`] pub fn into_tracing_level(level: Level) -> tracing::Level { @@ -15,7 +15,18 @@ pub fn into_tracing_level(level: Level) -> tracing::Level { } /// Reflects formatters in [`tracing_subscriber::fmt::format`] -#[derive(Debug, Copy, Clone, Eq, PartialEq, strum::Display, strum::EnumString, Default)] +#[derive( + Debug, + Copy, + Clone, + Eq, + PartialEq, + strum::Display, + strum::EnumString, + Default, + SerializeDisplay, + DeserializeFromStr, +)] #[strum(serialize_all = "snake_case")] pub enum Format { /// See [`tracing_subscriber::fmt::format::Full`] @@ -29,9 +40,6 @@ pub enum Format { Json, } -impl_serialize_display!(Format); -impl_deserialize_from_str!(Format); - #[cfg(test)] pub mod tests { use crate::logger::Format; From 8239f0fb4e0fef29c4843ca2ad5957bdd9ff8358 Mon Sep 17 00:00:00 2001 From: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> Date: Wed, 14 Feb 2024 07:24:47 +0900 Subject: [PATCH 80/94] [misc]: move `nonzero_ext` to workspace level Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- Cargo.toml | 1 + config/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index b9ec9589dd0..7764075fca4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -89,6 +89,7 @@ impls = "1.0.3" base64 = { version = "0.21.4", default-features = false } hex = { version = "0.4.3", default-features = false } +nonzero_ext = "0.3.0" fixnum = { version = "0.9.2", default-features = false } url = "2.4.1" diff --git a/config/Cargo.toml b/config/Cargo.toml index 6efc37b06ff..a976320e776 100644 --- a/config/Cargo.toml +++ b/config/Cargo.toml @@ -32,7 +32,7 @@ displaydoc = { workspace = true } derive_more = { workspace = true } cfg-if = { workspace = true } once_cell = { workspace = true } -nonzero_ext = "0.3.0" +nonzero_ext = { workspace = true } toml = { workspace = true } merge = "0.1.0" From c5d8c709fc2728a354f933334e6310028f7adae2 Mon Sep 17 00:00:00 2001 From: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> Date: Wed, 14 Feb 2024 07:33:56 +0900 Subject: [PATCH 81/94] [fix]: make `--config` optional It is still possible to set full config via env (e.g. `iroha_swarm` case) Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- cli/src/lib.rs | 4 ++-- cli/src/main.rs | 10 +++++----- config/src/parameters/actual.rs | 12 ++++++++---- 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/cli/src/lib.rs b/cli/src/lib.rs index 22dfe46850e..5cd7cb748f3 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -494,8 +494,8 @@ fn genesis_domain(public_key: PublicKey) -> Domain { /// - If failed to read the config /// - If failed to load the genesis block /// - If failed to build a genesis network -pub fn read_config_and_genesis( - path: impl AsRef, +pub fn read_config_and_genesis>( + path: Option

, submit_genesis: bool, ) -> Result<(Config, Option)> { use iroha_config::parameters::actual::Genesis; diff --git a/cli/src/main.rs b/cli/src/main.rs index b76c8cc0467..34c7909ef9d 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -18,7 +18,7 @@ fn default_terminal_colors_str() -> clap::builder::OsStr { struct Args { /// Path to the configuration file #[arg(long, short, value_name("PATH"), value_hint(clap::ValueHint::FilePath))] - config: PathBuf, + config: Option, /// Whether to enable ANSI colored output or not /// /// By default, Iroha determines whether the terminal supports colors or not. @@ -85,7 +85,7 @@ mod tests { #[test] #[allow(clippy::bool_assert_comparison)] // for expressiveness fn default_args() -> Result<()> { - let args = Args::try_parse_from(["test", "--config", "config.toml"])?; + let args = Args::try_parse_from(["test"])?; assert_eq!(args.terminal_colors, is_colouring_supported()); assert_eq!(args.submit_genesis, false); @@ -97,11 +97,11 @@ mod tests { #[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, "--config", "config.toml"])?.terminal_colors) + Ok(Args::try_parse_from(["test", arg])?.terminal_colors) } assert_eq!( - Args::try_parse_from(["test", "--config", "config.toml"])?.terminal_colors, + Args::try_parse_from(["test"])?.terminal_colors, is_colouring_supported() ); assert_eq!(try_with("--terminal-colors")?, true); @@ -116,7 +116,7 @@ mod tests { fn user_provided_config_path_works() -> Result<()> { let args = Args::try_parse_from(["test", "--config", "/home/custom/file.json"])?; - assert_eq!(args.config, PathBuf::from("/home/custom/file.json")); + assert_eq!(args.config, Some(PathBuf::from("/home/custom/file.json"))); Ok(()) } diff --git a/config/src/parameters/actual.rs b/config/src/parameters/actual.rs index fc3c66afea0..0be623cac18 100644 --- a/config/src/parameters/actual.rs +++ b/config/src/parameters/actual.rs @@ -53,10 +53,14 @@ impl Root { /// - unable to load config from a TOML file /// - unable to parse config from envs /// - the config is invalid - pub fn load(path: impl AsRef, cli: CliContext) -> Result { - let config = RootPartial::from_toml(path)?; - let config = config.merge(RootPartial::from_env(&StdEnv)?); - let config = config.unwrap_partial()?.parse(cli)?; + pub fn load>(path: Option

, cli: CliContext) -> Result { + let from_file = path.map(RootPartial::from_toml).transpose()?; + let from_env = RootPartial::from_env(&StdEnv)?; + let merged = match from_file { + Some(x) => x.merge(from_env), + None => from_env, + }; + let config = merged.unwrap_partial()?.parse(cli)?; Ok(config) } } From 4dc67ac2d02ca01820fac864102d3e090cc57d64 Mon Sep 17 00:00:00 2001 From: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> Date: Wed, 14 Feb 2024 07:39:43 +0900 Subject: [PATCH 82/94] [chore]: remove "regular" from telemetry re-export Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- telemetry/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telemetry/src/lib.rs b/telemetry/src/lib.rs index c1d179ebd8a..0fb3ec02ebd 100644 --- a/telemetry/src/lib.rs +++ b/telemetry/src/lib.rs @@ -8,7 +8,7 @@ mod retry_period; pub mod ws; pub use iroha_config::parameters::actual::{ - DevTelemetry as DevTelemetryConfig, Telemetry as RegularTelemetryConfig, + DevTelemetry as DevTelemetryConfig, Telemetry as TelemetryConfig, }; pub use iroha_telemetry_derive::metrics; From 4e8f2e11a3b5a4cfdfa627f31f5978743bfc968d Mon Sep 17 00:00:00 2001 From: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> Date: Wed, 14 Feb 2024 07:44:22 +0900 Subject: [PATCH 83/94] [chore]: rename imports in `wasm.rs` Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- core/src/smartcontracts/wasm.rs | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/core/src/smartcontracts/wasm.rs b/core/src/smartcontracts/wasm.rs index 27694c3ca6d..ec431e8e458 100644 --- a/core/src/smartcontracts/wasm.rs +++ b/core/src/smartcontracts/wasm.rs @@ -6,7 +6,7 @@ use error::*; use import::traits::{ ExecuteOperations as _, GetExecutorPayloads as _, SetPermissionTokenSchema as _, }; -use iroha_config::parameters::actual::WasmRuntime as IrohaWasmConfig; +use iroha_config::parameters::actual::WasmRuntime as Config; use iroha_data_model::{ account::AccountId, executor::{self, MigrationResult}, @@ -25,7 +25,8 @@ use iroha_logger::debug; use iroha_logger::{error_span as wasm_log_span, prelude::tracing::Span}; use iroha_wasm_codec::{self as codec, WasmUsize}; use wasmtime::{ - Caller, Config, Engine, Linker, Module, Store, StoreLimits, StoreLimitsBuilder, TypedFunc, + Caller, Config as WasmtimeConfig, Engine, Linker, Module, Store, StoreLimits, + StoreLimitsBuilder, TypedFunc, }; use crate::{ @@ -265,8 +266,8 @@ pub fn create_engine() -> Engine { .expect("Failed to create WASM engine with a predefined configuration. This is a bug") } -fn create_config() -> Result { - let mut config = Config::new(); +fn create_config() -> Result { + let mut config = WasmtimeConfig::new(); config .consume_fuel(true) .cache_config_load_default() @@ -340,7 +341,7 @@ pub mod state { /// /// Panics if failed to convert `u32` into `usize` which should not happen /// on any supported platform - pub fn store_limits_from_config(config: &IrohaWasmConfig) -> StoreLimits { + pub fn store_limits_from_config(config: &Config) -> StoreLimits { StoreLimitsBuilder::new() .memory_size(config.max_memory_bytes as usize) .instances(1) @@ -367,7 +368,7 @@ pub mod state { /// Create new [`OrdinaryState`] pub fn new( authority: AccountId, - config: IrohaWasmConfig, + config: Config, log_span: Span, wsv: W, specific_state: S, @@ -560,7 +561,7 @@ pub mod state { pub struct Runtime { engine: Engine, linker: Linker, - config: IrohaWasmConfig, + config: Config, } impl Runtime { @@ -588,7 +589,7 @@ impl Runtime { fn get_typed_func( instance: &wasmtime::Instance, - mut store: &mut wasmtime::Store, + mut store: &mut Store, func_name: &'static str, ) -> Result, ExportError> { instance @@ -1402,7 +1403,7 @@ impl<'wrld> import::traits::SetPermissionTokenSchema { engine: Option, - config: Option, + config: Option, linker: Option>, } @@ -1427,7 +1428,7 @@ impl RuntimeBuilder { /// Sets the [`Configuration`] to be used by the [`Runtime`] #[must_use] #[inline] - pub fn with_config(mut self, config: IrohaWasmConfig) -> Self { + pub fn with_config(mut self, config: Config) -> Self { self.config = Some(config); self } From e35ce3c7bd6fd6c47198173f64b8e3207f42137a Mon Sep 17 00:00:00 2001 From: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> Date: Wed, 14 Feb 2024 08:18:11 +0900 Subject: [PATCH 84/94] [refactor]: lints Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- cli/src/lib.rs | 4 ++-- core/src/kiso.rs | 2 +- crypto/src/lib.rs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cli/src/lib.rs b/cli/src/lib.rs index 5cd7cb748f3..f193659aef0 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -607,7 +607,7 @@ mod tests { // When - let (config, genesis) = read_config_and_genesis(config_path, true)?; + let (config, genesis) = read_config_and_genesis(Some(config_path), true)?; // Then @@ -656,7 +656,7 @@ mod tests { // When & Then - let report = read_config_and_genesis(config_path, false).unwrap_err(); + let report = read_config_and_genesis(Some(config_path), false).unwrap_err(); assert_contains!( format!("{report:#}"), diff --git a/core/src/kiso.rs b/core/src/kiso.rs index 1f6b0c7204d..c99add91be0 100644 --- a/core/src/kiso.rs +++ b/core/src/kiso.rs @@ -162,7 +162,7 @@ mod tests { Root::load( // FIXME Specifying path here might break! - "../config/iroha_test_config.toml", + Some("../config/iroha_test_config.toml"), CliContext { submit_genesis: true, }, diff --git a/crypto/src/lib.rs b/crypto/src/lib.rs index dc0f85a3128..715a9603cf8 100755 --- a/crypto/src/lib.rs +++ b/crypto/src/lib.rs @@ -662,7 +662,7 @@ impl PrivateKey { /// /// `into_raw()` without copying is not provided because underlying crypto /// libraries do not provide move functionality. - pub fn to_raw(self) -> (Algorithm, Vec) { + pub fn to_raw(&self) -> (Algorithm, Vec) { (self.algorithm(), self.payload()) } } From 5564d98876a0cef4baef81ffd1f33188288cc3b7 Mon Sep 17 00:00:00 2001 From: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> Date: Fri, 16 Feb 2024 09:42:42 +0900 Subject: [PATCH 85/94] [refactor]: use `serde_with` in swarm Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- Cargo.lock | 8 ++++- tools/swarm/Cargo.toml | 2 +- tools/swarm/src/compose.rs | 61 ++++++++------------------------------ 3 files changed, 20 insertions(+), 51 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index dc591f7392b..c34b4e553f7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -622,6 +622,7 @@ dependencies = [ "iana-time-zone", "js-sys", "num-traits", + "serde", "wasm-bindgen", "windows-targets 0.48.5", ] @@ -3209,7 +3210,6 @@ dependencies = [ "color-eyre", "derive_more", "expect-test", - "hex", "inquire", "iroha_config", "iroha_crypto", @@ -3220,6 +3220,7 @@ dependencies = [ "pathdiff", "serde", "serde_json", + "serde_with", "serde_yaml", ] @@ -4976,8 +4977,13 @@ version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "64cd236ccc1b7a29e7e2739f27c0b2dd199804abc4290e32f59f3b68d6405c23" dependencies = [ + "base64", + "chrono", + "hex", "serde", + "serde_json", "serde_with_macros", + "time", ] [[package]] diff --git a/tools/swarm/Cargo.toml b/tools/swarm/Cargo.toml index c5e9d81acb1..ba26e2d5195 100644 --- a/tools/swarm/Cargo.toml +++ b/tools/swarm/Cargo.toml @@ -21,9 +21,9 @@ serde = { workspace = true, features = ["derive"] } clap = { workspace = true, features = ["derive"] } serde_yaml.workspace = true serde_json.workspace = true +serde_with = { workspace = true, features = ["json", "macros", "hex"] } derive_more.workspace = true inquire.workspace = true -hex.workspace = true [dev-dependencies] iroha_config.workspace = true diff --git a/tools/swarm/src/compose.rs b/tools/swarm/src/compose.rs index 0c687f9e387..5a4f449a2cd 100644 --- a/tools/swarm/src/compose.rs +++ b/tools/swarm/src/compose.rs @@ -15,10 +15,7 @@ use iroha_crypto::{ use iroha_data_model::{prelude::PeerId, ChainId}; use iroha_primitives::addr::{socket_addr, SocketAddr}; use peer_generator::Peer; -use serde::{ - ser::{Error as _, SerializeMap}, - Serialize, Serializer, -}; +use serde::{ser::SerializeMap, Serialize, Serializer}; use crate::{cli::SourceParsed, util::AbsolutePath}; @@ -297,24 +294,25 @@ pub enum ServiceSource { Build(PathBuf), } +#[serde_with::serde_as] +#[serde_with::skip_serializing_none] #[derive(Serialize, Debug)] #[serde(rename_all = "UPPERCASE")] struct FullPeerEnv { chain_id: ChainId, public_key: PublicKey, private_key_digest: Algorithm, - private_key_payload: SerializeAsHex>, + #[serde_as(as = "serde_with::hex::Hex")] + private_key_payload: Vec, p2p_address: SocketAddr, api_address: SocketAddr, genesis_public_key: PublicKey, - #[serde(skip_serializing_if = "Option::is_none")] genesis_private_key_digest: Option, - #[serde(skip_serializing_if = "Option::is_none")] - genesis_private_key_payload: Option>>, - #[serde(skip_serializing_if = "Option::is_none")] + #[serde_as(as = "Option")] + genesis_private_key_payload: Option>, genesis_file: Option, - #[serde(skip_serializing_if = "Option::is_none")] - sumeragi_trusted_peers: Option>>, + #[serde_as(as = "Option")] + sumeragi_trusted_peers: Option>, } struct CompactPeerEnv { @@ -336,14 +334,14 @@ impl From for FullPeerEnv { let (algorithm, payload) = private_key.to_raw(); ( Some(algorithm), - Some(SerializeAsHex(payload)), + Some(payload), Some(PATH_TO_GENESIS.to_string()), ) }); let (private_key_digest, private_key_payload) = { let (algorithm, payload) = value.key_pair.private_key().clone().to_raw(); - (algorithm, SerializeAsHex(payload)) + (algorithm, payload) }; Self { @@ -360,47 +358,12 @@ impl From for FullPeerEnv { sumeragi_trusted_peers: if value.trusted_peers.is_empty() { None } else { - Some(SerializeAsJsonStr(value.trusted_peers)) + Some(value.trusted_peers) }, } } } -#[derive(Debug)] -struct SerializeAsJsonStr(T); - -impl serde::Serialize for SerializeAsJsonStr -where - T: serde::Serialize, -{ - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - let json = serde_json::to_string(&self.0).map_err(|json_err| { - S::Error::custom(format!("failed to serialize as JSON: {json_err}")) - })?; - serializer.serialize_str(&json) - } -} - -#[derive(Debug)] -struct SerializeAsHex(T); - -impl serde::Serialize for SerializeAsHex -where - T: AsRef<[u8]>, -{ - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - // FIXME maybe there is a way to avoid extra allocation, doesn't matter much - let data = hex::encode(&self.0); - serializer.serialize_str(&data) - } -} - #[derive(Debug)] pub struct DockerComposeBuilder<'a> { /// Needed to compute a relative source build path From 6011e4df4666da0476b35e089f39ba2cddfbc88c Mon Sep 17 00:00:00 2001 From: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> Date: Fri, 16 Feb 2024 10:26:45 +0900 Subject: [PATCH 86/94] [refactor]: rename parameters, cover full config in tests, fix bugs Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- Cargo.lock | 5 ++ cli/src/lib.rs | 12 ++-- cli/src/samples.rs | 2 +- config/base/Cargo.toml | 1 + config/base/src/lib.rs | 19 +++++- config/src/parameters/actual.rs | 6 +- config/src/parameters/user.rs | 34 +++++------ config/src/parameters/user/boilerplate.rs | 42 ++++++------- config/tests/fixtures.rs | 16 ++++- config/tests/fixtures/full.toml | 74 +++++++++++++++++++++++ configs/peer.template.toml | 46 ++++++-------- core/benches/kura.rs | 2 +- core/src/block_sync.rs | 6 +- core/src/gossiper.rs | 13 ++-- core/src/kura.rs | 17 +++--- core/src/snapshot.rs | 2 +- core/test_network/src/lib.rs | 2 +- 17 files changed, 201 insertions(+), 98 deletions(-) create mode 100644 config/tests/fixtures/full.toml diff --git a/Cargo.lock b/Cargo.lock index c34b4e553f7..d2bad0789f4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1289,6 +1289,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eb30d70a07a3b04884d2677f06bec33509dc67ca60d92949e5535352d3191dc" dependencies = [ "powerfmt", + "serde", ] [[package]] @@ -2613,6 +2614,7 @@ checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", "hashbrown 0.12.3", + "serde", ] [[package]] @@ -2790,6 +2792,7 @@ dependencies = [ "merge", "num-traits", "serde", + "serde_with", "thiserror", "toml 0.8.8", ] @@ -4980,6 +4983,8 @@ dependencies = [ "base64", "chrono", "hex", + "indexmap 1.9.3", + "indexmap 2.1.0", "serde", "serde_json", "serde_with_macros", diff --git a/cli/src/lib.rs b/cli/src/lib.rs index f193659aef0..5dbf5318efd 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -216,7 +216,7 @@ impl Iroha { let block_count = kura.init()?; let wsv = try_read_snapshot( - &config.snapshot.store_path, + &config.snapshot.store_dir, &kura, live_query_store_handle.clone(), block_count, @@ -587,9 +587,9 @@ mod tests { let config = { let mut cfg = config_factory(); cfg.genesis.file.set("./genesis/gen.json".into()); - cfg.kura.block_store_path.set("../storage".into()); - cfg.snapshot.store_path.set("../snapshots".into()); - cfg.telemetry.dev.file.set("../logs/telemetry".into()); + cfg.kura.store_dir.set("../storage".into()); + cfg.snapshot.store_dir.set("../snapshots".into()); + cfg.telemetry.dev.out_file.set("../logs/telemetry".into()); toml::Value::try_from(cfg)? }; @@ -615,11 +615,11 @@ mod tests { assert!(genesis.is_some()); assert_eq!( - config.kura.block_store_path.absolutize()?, + config.kura.store_dir.absolutize()?, dir.path().join("storage") ); assert_eq!( - config.snapshot.store_path.absolutize()?, + config.snapshot.store_dir.absolutize()?, dir.path().join("snapshots") ); assert_eq!( diff --git a/cli/src/samples.rs b/cli/src/samples.rs index c8561d53831..35fd25da53e 100644 --- a/cli/src/samples.rs +++ b/cli/src/samples.rs @@ -82,7 +82,7 @@ pub fn get_user_config( config.torii.address.set(DEFAULT_TORII_ADDR); config .network - .max_blocks_per_gossip + .block_gossip_max_size .set(1.try_into().unwrap()); config .network diff --git a/config/base/Cargo.toml b/config/base/Cargo.toml index 87f78d0b976..b5b469bc524 100644 --- a/config/base/Cargo.toml +++ b/config/base/Cargo.toml @@ -16,6 +16,7 @@ drop_bomb = { workspace = true } derive_more = { workspace = true, features = ["from", "deref", "deref_mut"] } eyre = { workspace = true } serde = { workspace = true, features = ["derive"] } +serde_with = { workspace = true, features = ["macros", "std"] } thiserror = { workspace = true } num-traits = "0.2.17" diff --git a/config/base/src/lib.rs b/config/base/src/lib.rs index 490e29b3ccb..cf898f6ab57 100644 --- a/config/base/src/lib.rs +++ b/config/base/src/lib.rs @@ -22,8 +22,9 @@ use serde::{Deserialize, Serialize}; /// [`Duration`], but can parse a human-readable string. /// TODO: currently deserializes just as [`Duration`] +#[serde_with::serde_as] #[derive(Debug, Copy, Clone, Deserialize, Serialize, Ord, PartialOrd, Eq, PartialEq)] -pub struct HumanDuration(pub Duration); +pub struct HumanDuration(#[serde_as(as = "serde_with::DurationMilliSeconds")] pub Duration); impl HumanDuration { /// Get the [`Duration`] @@ -612,4 +613,20 @@ mod tests { let multi = ExtendsPaths::Chain(vec!["foo".into(), "bar".into(), "baz".into()]); assert_eq!(multi.as_str_vec(), vec!["foo", "bar", "baz"]); } + + #[test] + fn deserialize_human_duration() { + #[derive(Deserialize)] + struct Test { + value: HumanDuration, + } + + let Test { value } = toml::toml! { + value = 10_500 + } + .try_into() + .expect("input is fine, should parse"); + + assert_eq!(value.get(), Duration::from_millis(10_500)); + } } diff --git a/config/src/parameters/actual.rs b/config/src/parameters/actual.rs index 0be623cac18..9a54da8e990 100644 --- a/config/src/parameters/actual.rs +++ b/config/src/parameters/actual.rs @@ -120,7 +120,7 @@ impl Genesis { #[derive(Debug, Clone)] pub struct Kura { pub init_mode: Mode, - pub block_store_path: PathBuf, + pub store_dir: PathBuf, pub debug_output_new_blocks: bool, } @@ -160,14 +160,14 @@ impl Default for LiveQueryStore { #[derive(Debug, Clone, Copy)] pub struct BlockSync { pub gossip_period: Duration, - pub batch_size: NonZeroU32, + pub gossip_max_size: NonZeroU32, } #[derive(Debug, Clone, Copy)] #[allow(missing_docs)] pub struct TransactionGossiper { pub gossip_period: Duration, - pub batch_size: NonZeroU32, + pub gossip_max_size: NonZeroU32, } #[derive(Debug, Clone, Copy, Serialize, Deserialize)] diff --git a/config/src/parameters/user.rs b/config/src/parameters/user.rs index 7c906be92b6..57238af0aaf 100644 --- a/config/src/parameters/user.rs +++ b/config/src/parameters/user.rs @@ -116,9 +116,9 @@ impl RootPartial { } patch!(self.genesis.file); - patch!(self.snapshot.store_path); - patch!(self.kura.block_store_path); - patch!(self.telemetry.dev.file); + patch!(self.snapshot.store_dir); + patch!(self.kura.store_dir); + patch!(self.telemetry.dev.out_file); } // FIXME workaround the inconvenient way `Merge::merge` works @@ -344,7 +344,7 @@ pub enum GenesisConfigError { #[derive(Debug)] pub struct Kura { pub init_mode: Mode, - pub block_store_path: PathBuf, + pub store_dir: PathBuf, pub debug: KuraDebug, } @@ -352,7 +352,7 @@ impl Kura { fn parse(self) -> actual::Kura { let Self { init_mode, - block_store_path, + store_dir: block_store_path, debug: KuraDebug { output_new_blocks: debug_output_new_blocks, @@ -361,7 +361,7 @@ impl Kura { actual::Kura { init_mode, - block_store_path, + store_dir: block_store_path, debug_output_new_blocks, } } @@ -417,9 +417,9 @@ fn construct_unique_vec( pub struct Network { /// Peer-to-peer address pub address: SocketAddr, + pub block_gossip_max_size: NonZeroU32, pub block_gossip_period: Duration, - pub max_blocks_per_gossip: NonZeroU32, - pub max_transactions_per_gossip: NonZeroU32, + pub transaction_gossip_max_size: NonZeroU32, pub transaction_gossip_period: Duration, } @@ -427,9 +427,9 @@ impl Network { fn parse(self) -> (SocketAddr, actual::BlockSync, actual::TransactionGossiper) { let Self { address, - max_blocks_per_gossip, - max_transactions_per_gossip, + block_gossip_max_size, block_gossip_period, + transaction_gossip_max_size, transaction_gossip_period, } = self; @@ -437,11 +437,11 @@ impl Network { address, actual::BlockSync { gossip_period: block_gossip_period, - batch_size: max_blocks_per_gossip, + gossip_max_size: block_gossip_max_size, }, actual::TransactionGossiper { gossip_period: transaction_gossip_period, - batch_size: max_transactions_per_gossip, + gossip_max_size: transaction_gossip_max_size, }, ) } @@ -472,7 +472,7 @@ pub struct Logger { pub format: Format, #[cfg(feature = "tokio-console")] /// Address of tokio console (only available under "tokio-console" feature) - pub tokio_console_addr: SocketAddr, + pub tokio_console_address: SocketAddr, } #[allow(clippy::derivable_impls)] // triggers in absence of `tokio-console` feature @@ -482,7 +482,7 @@ impl Default for Logger { level: Level::default(), format: Format::default(), #[cfg(feature = "tokio-console")] - tokio_console_addr: super::defaults::logger::DEFAULT_TOKIO_CONSOLE_ADDR, + tokio_console_address: super::defaults::logger::DEFAULT_TOKIO_CONSOLE_ADDR, } } } @@ -542,7 +542,7 @@ impl Telemetry { #[derive(Debug, Clone)] pub struct Snapshot { pub create_every: Duration, - pub store_path: PathBuf, + pub store_dir: PathBuf, pub creation_enabled: bool, } @@ -556,7 +556,7 @@ pub struct ChainWide { pub asset_definition_metadata_limits: MetadataLimits, pub account_metadata_limits: MetadataLimits, pub domain_metadata_limits: MetadataLimits, - pub identifier_length_limits: LengthLimits, + pub ident_length_limits: LengthLimits, pub wasm_fuel_limit: u64, pub wasm_max_memory: HumanBytes, } @@ -572,7 +572,7 @@ impl ChainWide { asset_definition_metadata_limits, account_metadata_limits, domain_metadata_limits, - identifier_length_limits, + ident_length_limits: identifier_length_limits, wasm_fuel_limit, wasm_max_memory, } = self; diff --git a/config/src/parameters/user/boilerplate.rs b/config/src/parameters/user/boilerplate.rs index dbce1a0ffda..9a6e5f0ed1f 100644 --- a/config/src/parameters/user/boilerplate.rs +++ b/config/src/parameters/user/boilerplate.rs @@ -261,7 +261,7 @@ impl FromEnv for GenesisPartial { #[serde(deny_unknown_fields, default)] pub struct KuraPartial { pub init_mode: UserField, - pub block_store_path: UserField, + pub store_dir: UserField, pub debug: KuraDebugPartial, } @@ -274,7 +274,7 @@ impl UnwrapPartial for KuraPartial { let init_mode = self.init_mode.unwrap_or_default(); let block_store_path = self - .block_store_path + .store_dir .get() .unwrap_or_else(|| PathBuf::from(DEFAULT_BLOCK_STORE_PATH)); @@ -290,7 +290,7 @@ impl UnwrapPartial for KuraPartial { Ok(Kura { init_mode, - block_store_path, + store_dir: block_store_path, debug: debug.unwrap(), }) } @@ -341,7 +341,7 @@ impl FromEnv for KuraPartial { Ok(Self { init_mode, - block_store_path, + store_dir: block_store_path, debug: KuraDebugPartial { output_new_blocks: debug_output_new_blocks, }, @@ -401,9 +401,9 @@ impl FromEnvDefaultFallback for SumeragiPartial {} #[serde(deny_unknown_fields, default)] pub struct NetworkPartial { pub address: UserField, + pub block_gossip_max_size: UserField, pub block_gossip_period: UserField, - pub max_blocks_per_gossip: UserField, - pub max_transactions_per_gossip: UserField, + pub transaction_gossip_max_size: UserField, pub transaction_gossip_period: UserField, } @@ -425,12 +425,12 @@ impl UnwrapPartial for NetworkPartial { .transaction_gossip_period .map(HumanDuration::get) .unwrap_or(DEFAULT_TRANSACTION_GOSSIP_PERIOD), - max_transactions_per_gossip: self - .max_transactions_per_gossip + transaction_gossip_max_size: self + .transaction_gossip_max_size .get() .unwrap_or(DEFAULT_MAX_TRANSACTIONS_PER_GOSSIP), - max_blocks_per_gossip: self - .max_blocks_per_gossip + block_gossip_max_size: self + .block_gossip_max_size .get() .unwrap_or(DEFAULT_MAX_BLOCKS_PER_GOSSIP), }) @@ -505,7 +505,7 @@ pub struct LoggerPartial { pub format: UserField, #[cfg(feature = "tokio-console")] /// Address of tokio console (only available under "tokio-console" feature) - pub tokio_console_addr: UserField, + pub tokio_console_address: UserField, } impl UnwrapPartial for LoggerPartial { @@ -516,7 +516,7 @@ impl UnwrapPartial for LoggerPartial { level: self.level.unwrap_or_default(), format: self.format.unwrap_or_default(), #[cfg(feature = "tokio-console")] - tokio_console_addr: self.tokio_console_addr.get().unwrap_or_else(|| { + tokio_console_address: self.tokio_console_address.get().unwrap_or_else(|| { super::super::defaults::logger::DEFAULT_TOKIO_CONSOLE_ADDR.clone() }), }) @@ -559,7 +559,7 @@ pub struct TelemetryPartial { #[derive(Clone, Deserialize, Serialize, Debug, Default, Merge)] #[serde(deny_unknown_fields, default)] pub struct TelemetryDevPartial { - pub file: UserField, + pub out_file: UserField, } impl UnwrapPartial for TelemetryDevPartial { @@ -567,7 +567,7 @@ impl UnwrapPartial for TelemetryDevPartial { fn unwrap_partial(self) -> UnwrapPartialResult { Ok(TelemetryDev { - out_file: self.file.get(), + out_file: self.out_file.get(), }) } } @@ -600,7 +600,7 @@ impl FromEnvDefaultFallback for TelemetryPartial {} #[serde(deny_unknown_fields, default)] pub struct SnapshotPartial { pub create_every: UserField, - pub store_path: UserField, + pub store_dir: UserField, pub creation_enabled: UserField, } @@ -614,8 +614,8 @@ impl UnwrapPartial for SnapshotPartial { .create_every .get() .map_or(DEFAULT_SNAPSHOT_CREATE_EVERY_MS, HumanDuration::get), - store_path: self - .store_path + store_dir: self + .store_dir .get() .unwrap_or_else(|| PathBuf::from(DEFAULT_SNAPSHOT_PATH)), }) @@ -647,7 +647,7 @@ impl FromEnv for SnapshotPartial { emitter.finish()?; Ok(Self { - store_path, + store_dir: store_path, creation_enabled, ..Self::default() }) @@ -665,7 +665,7 @@ pub struct ChainWidePartial { pub asset_definition_metadata_limits: UserField, pub account_metadata_limits: UserField, pub domain_metadata_limits: UserField, - pub identifier_length_limits: UserField, + pub ident_length_limits: UserField, pub wasm_fuel_limit: UserField, pub wasm_max_memory: UserField>, } @@ -697,8 +697,8 @@ impl UnwrapPartial for ChainWidePartial { domain_metadata_limits: self .domain_metadata_limits .unwrap_or(DEFAULT_METADATA_LIMITS), - identifier_length_limits: self - .identifier_length_limits + ident_length_limits: self + .ident_length_limits .unwrap_or(DEFAULT_IDENT_LENGTH_LIMITS), wasm_fuel_limit: self.wasm_fuel_limit.unwrap_or(DEFAULT_WASM_FUEL_LIMIT), wasm_max_memory: self diff --git a/config/tests/fixtures.rs b/config/tests/fixtures.rs index c3f33e9fa96..378dc1063fb 100644 --- a/config/tests/fixtures.rs +++ b/config/tests/fixtures.rs @@ -7,7 +7,10 @@ use std::{ }; use eyre::Result; -use iroha_config::parameters::user::{CliContext, RootPartial}; +use iroha_config::parameters::{ + actual::Root, + user::{CliContext, RootPartial}, +}; use iroha_config_base::{FromEnv, TestEnv, UnwrapPartial as _}; fn fixtures_dir() -> PathBuf { @@ -477,3 +480,14 @@ fn multiple_extends_works() -> Result<()> { Ok(()) } + +#[test] +fn full_config_parses_fine() { + let _cfg = Root::load( + Some(fixtures_dir().join("full.toml")), + CliContext { + submit_genesis: true, + }, + ) + .expect("should be fine"); +} diff --git a/config/tests/fixtures/full.toml b/config/tests/fixtures/full.toml new file mode 100644 index 00000000000..878223301aa --- /dev/null +++ b/config/tests/fixtures/full.toml @@ -0,0 +1,74 @@ +# This config has ALL fields specified (except `extends`) + +chain_id = "0" +public_key = "ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB" +private_key = { digest_function = "ed25519", payload = "8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb" } + +[genesis] +file = "genesis.json" +public_key = "ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB" +private_key = { digest_function = "ed25519", payload = "8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb" } + +[network] +address = "localhost:3840" +block_gossip_period = 10_000 +block_gossip_max_size = 4 +transaction_gossip_period = 1_000 +transaction_gossip_max_size = 500 + +[torii] +address = "localhost:5000" +max_content_len = 16 +query_idle_time = 30_000 + +[kura] +init_mode = "strict" +store_dir = "./storage" + +[kura.debug] +output_new_blocks = true + +[[sumeragi.trusted_peers]] +address = "localhost:8081" +public_key = "ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB" + +[sumeragi.debug] +force_soft_fork = true + +[logger] +level = "TRACE" +format = "compact" +tokio_console_address = "127.0.0.1:5555" + +[queue] +capacity = 65536 +capacity_per_user = 65536 +transaction_time_to_live = 100 +future_threshold = 50 + +[snapshot] +creation_enabled = true +create_every = 60_000 +store_dir = "./storage/snapshot" + +[telemetry] +name = "test" +url = "http://test.com" +min_retry_period = 5_000 +max_retry_delay_exponent = 4 + +[telemetry.dev] +out_file = "./dev-telemetry.json5" + +[chain_wide] +max_transactions_in_block = 512 +block_time = 2_000 +commit_time = 4_000 +transaction_limits = {max_instruction_number = 4096, max_wasm_size_bytes = 4194304 } +asset_metadata_limits = { max_len = 1048576, max_entry_byte_size = 4096 } +asset_definition_metadata_limits = { max_len = 1048576, max_entry_byte_size = 4096 } +account_metadata_limits = { max_len = 1048576, max_entry_byte_size = 4096 } +domain_metadata_limits = { max_len = 1048576, max_entry_byte_size = 4096 } +ident_length_limits = { min = 1, max = 128 } +wasm_fuel_limit = 55000000 +wasm_max_memory = 524288000 diff --git a/configs/peer.template.toml b/configs/peer.template.toml index b457698925c..855e44c6c0b 100644 --- a/configs/peer.template.toml +++ b/configs/peer.template.toml @@ -8,23 +8,22 @@ # chain_id = # public_key = -# private_key.digest_function = -# private_key.payload = +# private_key = { +# algorithm = , +# payload = +# } + +[genesis] +# file = +# public_key = +# private_key = { algorithm = "", payload = "" } [network] # address = # block_gossip_period = "10s" -# max_blocks_per_gossip = 4 -# max_transactions_per_gossip = 500 +# block_gossip_max_size = 4 # transaction_gossip_period = "1s" - -[genesis] -# public_key = - -## Combine these with `--submit-genesis` CLI argument -# file = -# private_key.digest_function = -# private_key.payload = +# transaction_gossip_max_size = 500 [torii] # address = @@ -33,43 +32,36 @@ [kura] # init_mode = "strict" -# block_store_path = "./storage" - -[kura.debug] -# output_new_blocks = false +# store_dir = "./storage" ## Add more of this section for each trusted peer # [[sumeragi.trusted_peers]] # address = # public_key = -[sumeragi.debug] -# force_soft_fork = false - [logger] # level = "INFO" # format = "full" +# tokio_console_address = "127.0.0.1:5555" +## Transactions Queue [queue] # capacity = 65536 # capacity_per_user = 65536 -# transaction_time_to_live = "86400s" +# transaction_time_to_live = "1day" # future_threshold = "1s" [snapshot] -# create_every = "60s" -# store_path = "./storage/snapshot" # creation_enabled = true +# create_every = "1min" +# store_dir = "./storage/snapshot" [telemetry] # name = # url = -# min_retry_period = -# max_retry_delay_exponent = +# min_retry_period = "1s" +# max_retry_delay_exponent = 4 [telemetry.dev] ## FIXME: is it JSON5? # out_file = "./dev-telemetry.json5" - -## TODO: remove it from the config file entirely -# [chain_wide] \ No newline at end of file diff --git a/core/benches/kura.rs b/core/benches/kura.rs index ec267d3df8e..5ee45f62556 100644 --- a/core/benches/kura.rs +++ b/core/benches/kura.rs @@ -43,7 +43,7 @@ async fn measure_block_size_for_n_executors(n_executors: u32) { let cfg = Config { init_mode: iroha_config::kura::Mode::Strict, debug_output_new_blocks: false, - block_store_path: dir.path().to_str().unwrap().into(), + store_dir: dir.path().to_path_buf(), }; let kura = iroha_core::kura::Kura::new(&cfg).unwrap(); let _thread_handle = iroha_core::kura::Kura::start(kura.clone()); diff --git a/core/src/block_sync.rs b/core/src/block_sync.rs index 4919f14f304..e4d71aad0a1 100644 --- a/core/src/block_sync.rs +++ b/core/src/block_sync.rs @@ -36,7 +36,7 @@ pub struct BlockSynchronizer { kura: Arc, peer_id: PeerId, gossip_period: Duration, - block_batch_size: NonZeroU32, + gossip_max_size: NonZeroU32, network: IrohaNetwork, latest_hash: Option>, previous_hash: Option>, @@ -118,7 +118,7 @@ impl BlockSynchronizer { sumeragi, kura, gossip_period: config.gossip_period, - block_batch_size: config.batch_size, + gossip_max_size: config.gossip_max_size, network, latest_hash, previous_hash, @@ -210,7 +210,7 @@ pub mod message { }; let blocks = (start_height..) - .take(1 + block_sync.block_batch_size.get() as usize) + .take(1 + block_sync.gossip_max_size.get() as usize) .map_while(|height| block_sync.kura.get_block_by_height(height)) .skip_while(|block| Some(block.hash()) == *latest_hash) .map(|block| (*block).clone()) diff --git a/core/src/gossiper.rs b/core/src/gossiper.rs index fd158498b36..ddb81dcd310 100644 --- a/core/src/gossiper.rs +++ b/core/src/gossiper.rs @@ -35,7 +35,7 @@ pub struct TransactionGossiper { chain_id: ChainId, /// The size of batch that is being gossiped. Smaller size leads /// to longer time to synchronise, useful if you have high packet loss. - gossip_batch_size: NonZeroU32, + gossip_max_size: NonZeroU32, /// The time between gossiping. More frequent gossiping shortens /// the time to sync, but can overload the network. gossip_period: Duration, @@ -60,7 +60,10 @@ impl TransactionGossiper { /// Construct [`Self`] from configuration pub fn from_config( chain_id: ChainId, - config: Config, + Config { + gossip_period, + gossip_max_size, + }: Config, network: IrohaNetwork, queue: Arc, sumeragi: SumeragiHandle, @@ -71,8 +74,8 @@ impl TransactionGossiper { queue, sumeragi, network, - gossip_batch_size: config.batch_size, - gossip_period: config.gossip_period, + gossip_max_size, + gossip_period, wsv, } } @@ -100,7 +103,7 @@ impl TransactionGossiper { fn gossip_transactions(&self) { let txs = self .queue - .n_random_transactions(self.gossip_batch_size.get(), &self.wsv); + .n_random_transactions(self.gossip_max_size.get(), &self.wsv); if txs.is_empty() { return; diff --git a/core/src/kura.rs b/core/src/kura.rs index 5777a9ff50e..b6319986b0d 100644 --- a/core/src/kura.rs +++ b/core/src/kura.rs @@ -50,15 +50,12 @@ impl Kura { /// to access the block store indicated by the provided /// path. pub fn new(config: &Config) -> Result> { - let block_store_path = Path::new(&config.block_store_path); - let mut block_store = BlockStore::new(block_store_path, LockStatus::Unlocked); + let mut block_store = BlockStore::new(&config.store_dir, LockStatus::Unlocked); block_store.create_files_if_they_do_not_exist()?; - let block_plain_text_path = config.debug_output_new_blocks.then(|| { - let mut path_buf = block_store_path.to_path_buf(); - path_buf.push("blocks.json"); - path_buf - }); + let block_plain_text_path = config + .debug_output_new_blocks + .then(|| config.store_dir.join("blocks.json")); let kura = Arc::new(Self { mode: config.init_mode, @@ -395,9 +392,9 @@ impl BlockStore { /// /// # Panics /// * if you pass in `LockStatus::Unlocked` and it is unable to lock the block store. - pub fn new(store_path: &Path, already_locked: LockStatus) -> Self { + pub fn new(store_path: impl AsRef, already_locked: LockStatus) -> Self { if matches!(already_locked, LockStatus::Unlocked) { - let lock_path = store_path.join(LOCK_FILE_NAME); + let lock_path = store_path.as_ref().join(LOCK_FILE_NAME); if let Err(e) = fs::File::options() .read(true) .write(true) @@ -1051,7 +1048,7 @@ mod tests { let temp_dir = TempDir::new().unwrap(); Kura::new(&Config { init_mode: Mode::Strict, - block_store_path: temp_dir.path().to_str().unwrap().into(), + store_dir: temp_dir.path().to_str().unwrap().into(), debug_output_new_blocks: false, }) .unwrap() diff --git a/core/src/snapshot.rs b/core/src/snapshot.rs index 2256199c158..22e7e3762b9 100644 --- a/core/src/snapshot.rs +++ b/core/src/snapshot.rs @@ -141,7 +141,7 @@ impl SnapshotMaker { Self { sumeragi, snapshot_create_every: config.create_every, - snapshot_dir: config.store_path.clone(), + snapshot_dir: config.store_dir.clone(), snapshot_creation_enabled: config.creation_enabled, new_wsv_available: false, } diff --git a/core/test_network/src/lib.rs b/core/test_network/src/lib.rs index 5536d6edca4..1c187480a74 100644 --- a/core/test_network/src/lib.rs +++ b/core/test_network/src/lib.rs @@ -414,7 +414,7 @@ impl Peer { temp_dir: Arc, ) { let mut config = self.get_config(config); - config.kura.block_store_path = temp_dir.path().to_str().unwrap().into(); + config.kura.store_dir = temp_dir.path().to_str().unwrap().into(); let info_span = iroha_logger::info_span!( "test-peer", p2p_addr = %self.p2p_address, From 9c584a31591a8f30ee7c0cd95602385ec31b5cb2 Mon Sep 17 00:00:00 2001 From: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> Date: Fri, 16 Feb 2024 10:35:06 +0900 Subject: [PATCH 87/94] [test]: cover absolute paths Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- config/tests/fixtures.rs | 24 ++++++++++++++++++++++- config/tests/fixtures/absolute_paths.toml | 14 +++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 config/tests/fixtures/absolute_paths.toml diff --git a/config/tests/fixtures.rs b/config/tests/fixtures.rs index 378dc1063fb..fa901da3d8e 100644 --- a/config/tests/fixtures.rs +++ b/config/tests/fixtures.rs @@ -8,7 +8,7 @@ use std::{ use eyre::Result; use iroha_config::parameters::{ - actual::Root, + actual::{Genesis, Root}, user::{CliContext, RootPartial}, }; use iroha_config_base::{FromEnv, TestEnv, UnwrapPartial as _}; @@ -491,3 +491,25 @@ fn full_config_parses_fine() { ) .expect("should be fine"); } + +#[test] +fn absolute_paths_are_preserved() { + let cfg = Root::load( + Some(fixtures_dir().join("absolute_paths.toml")), + CliContext { + submit_genesis: true, + }, + ) + .expect("should be fine"); + + assert_eq!(cfg.kura.store_dir, PathBuf::from("/kura/store")); + assert_eq!(cfg.snapshot.store_dir, PathBuf::from("/snapshot/store")); + assert_eq!( + cfg.dev_telemetry.unwrap().out_file, + PathBuf::from("/telemetry/file.json") + ); + let Genesis::Full { + file: genesis_file, .. + } = cfg.genesis else { unreachable!() }; + assert_eq!(genesis_file, PathBuf::from("/oh/my/genesis.json")); +} diff --git a/config/tests/fixtures/absolute_paths.toml b/config/tests/fixtures/absolute_paths.toml new file mode 100644 index 00000000000..0d1f3d3f3d5 --- /dev/null +++ b/config/tests/fixtures/absolute_paths.toml @@ -0,0 +1,14 @@ +extends = ["base.toml"] + +[kura] +store_dir = "/kura/store" + +[snapshot] +store_dir = "/snapshot/store" + +[telemetry.dev] +out_file = "/telemetry/file.json" + +[genesis] +file = "/oh/my/genesis.json" +private_key = { digest_function = "ed25519", payload = "8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb" } \ No newline at end of file From 04ee42fd57c103268b72277335d2ad5348b20ce5 Mon Sep 17 00:00:00 2001 From: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> Date: Fri, 16 Feb 2024 11:07:17 +0900 Subject: [PATCH 88/94] [refactor]: update client configs, cover full in tests Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- client/src/config.rs | 22 ++++++++++++++++++++++ configs/client.template.toml | 9 ++++----- configs/swarm/client.toml | 9 +++++---- 3 files changed, 31 insertions(+), 9 deletions(-) diff --git a/client/src/config.rs b/client/src/config.rs index 08b30ee3a83..c6010c834ec 100644 --- a/client/src/config.rs +++ b/client/src/config.rs @@ -97,4 +97,26 @@ mod tests { fn web_login_bad() { let _err = WebLogin::from_str("alice:wonderland").expect_err("input has `:`"); } + + #[test] + fn parse_full_toml_config() { + let _: RootPartial = toml::toml! { + chain_id = "00000000-0000-0000-0000-000000000000" + torii_url = "http://127.0.0.1:8080/" + + [basic_auth] + web_login = "mad_hatter" + password = "ilovetea" + + [account] + id = "alice@wonderland" + public_key = "ed01207233BFC89DCBD68C19FDE6CE6158225298EC1131B6A130D1AEB454C1AB5183C0" + private_key = { digest_function = "ed25519", payload = "9ac47abf59b356e0bd7dcbbbb4dec080e302156a48ca907e47cb6aea1d32719e7233bfc89dcbd68c19fde6ce6158225298ec1131b6a130d1aeb454c1ab5183c0" } + + [transaction] + time_to_live = 100_000 + status_timeout = 100_000 + nonce = false + }.try_into().unwrap(); + } } diff --git a/configs/client.template.toml b/configs/client.template.toml index c0ced15851c..3bad84abcc5 100644 --- a/configs/client.template.toml +++ b/configs/client.template.toml @@ -10,11 +10,10 @@ [account] # id = # public_key = -# private_key.digest_function = -# private_key.payload = +# private_key = { algorithm = "", payload = "" } [transaction] -# time_to_live = 100_000 -# status_timeout = 100_000 +# time_to_live = "100s" +# status_timeout = "100s" +## Nonce is TODO describe what it is # nonce = false - diff --git a/configs/swarm/client.toml b/configs/swarm/client.toml index d0a4836ccb9..bc2a82df05f 100644 --- a/configs/swarm/client.toml +++ b/configs/swarm/client.toml @@ -1,10 +1,11 @@ chain_id = "00000000-0000-0000-0000-000000000000" torii_url = "http://127.0.0.1:8080/" -basic_auth.web_login = "mad_hatter" -basic_auth.password = "ilovetea" + +[basic_auth] +web_login = "mad_hatter" +password = "ilovetea" [account] id = "alice@wonderland" public_key = "ed01207233BFC89DCBD68C19FDE6CE6158225298EC1131B6A130D1AEB454C1AB5183C0" -private_key.digest_function = "ed25519" -private_key.payload = "9ac47abf59b356e0bd7dcbbbb4dec080e302156a48ca907e47cb6aea1d32719e7233bfc89dcbd68c19fde6ce6158225298ec1131b6a130d1aeb454c1ab5183c0" +private_key = { digest_function = "ed25519", payload = "9ac47abf59b356e0bd7dcbbbb4dec080e302156a48ca907e47cb6aea1d32719e7233bfc89dcbd68c19fde6ce6158225298ec1131b6a130d1aeb454c1ab5183c0" } From 9c4384f303c9b5948215475c54e616800199f335 Mon Sep 17 00:00:00 2001 From: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> Date: Fri, 16 Feb 2024 11:11:11 +0900 Subject: [PATCH 89/94] [refactor]: lints Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- core/src/gossiper.rs | 6 +++--- core/src/kura.rs | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/core/src/gossiper.rs b/core/src/gossiper.rs index ddb81dcd310..a67709fbb9a 100644 --- a/core/src/gossiper.rs +++ b/core/src/gossiper.rs @@ -71,11 +71,11 @@ impl TransactionGossiper { let wsv = sumeragi.wsv_clone(); Self { chain_id, - queue, - sumeragi, - network, gossip_max_size, gossip_period, + queue, + network, + sumeragi, wsv, } } diff --git a/core/src/kura.rs b/core/src/kura.rs index b6319986b0d..c70e5557323 100644 --- a/core/src/kura.rs +++ b/core/src/kura.rs @@ -72,7 +72,7 @@ impl Kura { pub fn blank_kura_for_testing() -> Arc { Arc::new(Self { mode: Mode::Strict, - block_store: Mutex::new(BlockStore::new(&PathBuf::new(), LockStatus::Locked)), + block_store: Mutex::new(BlockStore::new(PathBuf::new(), LockStatus::Locked)), block_data: Mutex::new(Vec::new()), block_plain_text_path: None, }) @@ -404,8 +404,8 @@ impl BlockStore { match e.kind() { std::io::ErrorKind::AlreadyExists => Err(Error::Locked(lock_path)), std::io::ErrorKind::NotFound => { - match std::fs::create_dir_all(store_path) - .map_err(|e| Error::MkDir(e, store_path.to_path_buf())) + match std::fs::create_dir_all(store_path.as_ref()) + .map_err(|e| Error::MkDir(e, store_path.as_ref().to_path_buf())) { Err(e) => Err(e), Ok(()) => { @@ -428,7 +428,7 @@ impl BlockStore { } } BlockStore { - path_to_blockchain: store_path.to_path_buf(), + path_to_blockchain: store_path.as_ref().to_path_buf(), } } From 50ba71a8621746e7fa29ba3e597270148456582f Mon Sep 17 00:00:00 2001 From: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> Date: Fri, 16 Feb 2024 17:27:03 +0900 Subject: [PATCH 90/94] [fix]: also rename ENVs Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- config/src/parameters/defaults.rs | 6 ++-- config/src/parameters/user/boilerplate.rs | 36 +++++++++++------------ config/tests/fixtures.rs | 36 +++++++++++++---------- config/tests/fixtures/full.env | 4 +-- 4 files changed, 42 insertions(+), 40 deletions(-) diff --git a/config/src/parameters/defaults.rs b/config/src/parameters/defaults.rs index 6ccdf2b1e0d..a6f779d087b 100644 --- a/config/src/parameters/defaults.rs +++ b/config/src/parameters/defaults.rs @@ -21,7 +21,7 @@ pub mod queue { pub const DEFAULT_FUTURE_THRESHOLD: Duration = Duration::from_secs(1); } pub mod kura { - pub const DEFAULT_BLOCK_STORE_PATH: &str = "./storage"; + pub const DEFAULT_STORE_DIR: &str = "./storage"; } #[cfg(feature = "tokio-console")] @@ -45,9 +45,9 @@ pub mod network { pub mod snapshot { use super::*; - pub const DEFAULT_SNAPSHOT_PATH: &str = "./storage/snapshot"; + pub const DEFAULT_STORE_DIR: &str = "./storage/snapshot"; // Default frequency of making snapshots is 1 minute, need to be adjusted for larger world state view size - pub const DEFAULT_SNAPSHOT_CREATE_EVERY_MS: Duration = Duration::from_secs(60); + pub const DEFAULT_CREATE_EVERY: Duration = Duration::from_secs(60); pub const DEFAULT_ENABLED: bool = true; } diff --git a/config/src/parameters/user/boilerplate.rs b/config/src/parameters/user/boilerplate.rs index 9a6e5f0ed1f..64b81d9d6ff 100644 --- a/config/src/parameters/user/boilerplate.rs +++ b/config/src/parameters/user/boilerplate.rs @@ -29,7 +29,7 @@ use crate::{ kura::Mode, logger::Format, parameters::{ - defaults::{chain_wide::*, kura::*, network::*, queue::*, snapshot::*, torii::*}, + defaults::{self, chain_wide::*, network::*, queue::*, torii::*}, user, user::{ ChainWide, Genesis, Kura, KuraDebug, Logger, Network, Queue, Root, Snapshot, Sumeragi, @@ -273,10 +273,10 @@ impl UnwrapPartial for KuraPartial { let init_mode = self.init_mode.unwrap_or_default(); - let block_store_path = self + let store_dir = self .store_dir .get() - .unwrap_or_else(|| PathBuf::from(DEFAULT_BLOCK_STORE_PATH)); + .unwrap_or_else(|| PathBuf::from(defaults::kura::DEFAULT_STORE_DIR)); let debug = UnwrapPartial::unwrap_partial(self.debug).map_or_else( |err| { @@ -290,7 +290,7 @@ impl UnwrapPartial for KuraPartial { Ok(Kura { init_mode, - store_dir: block_store_path, + store_dir, debug: debug.unwrap(), }) } @@ -322,13 +322,9 @@ impl FromEnv for KuraPartial { let init_mode = ParseEnvResult::parse_simple(&mut emitter, env, "KURA_INIT_MODE", "kura.init_mode") .into(); - let block_store_path = ParseEnvResult::parse_simple( - &mut emitter, - env, - "KURA_BLOCK_STORE", - "kura.block_store_path", - ) - .into(); + let store_dir = + ParseEnvResult::parse_simple(&mut emitter, env, "KURA_STORE_DIR", "kura.store_dir") + .into(); let debug_output_new_blocks = ParseEnvResult::parse_simple( &mut emitter, env, @@ -341,7 +337,7 @@ impl FromEnv for KuraPartial { Ok(Self { init_mode, - store_dir: block_store_path, + store_dir, debug: KuraDebugPartial { output_new_blocks: debug_output_new_blocks, }, @@ -609,15 +605,17 @@ impl UnwrapPartial for SnapshotPartial { fn unwrap_partial(self) -> UnwrapPartialResult { Ok(Snapshot { - creation_enabled: self.creation_enabled.unwrap_or(DEFAULT_ENABLED), + creation_enabled: self + .creation_enabled + .unwrap_or(defaults::snapshot::DEFAULT_ENABLED), create_every: self .create_every .get() - .map_or(DEFAULT_SNAPSHOT_CREATE_EVERY_MS, HumanDuration::get), + .map_or(defaults::snapshot::DEFAULT_CREATE_EVERY, HumanDuration::get), store_dir: self .store_dir .get() - .unwrap_or_else(|| PathBuf::from(DEFAULT_SNAPSHOT_PATH)), + .unwrap_or_else(|| PathBuf::from(defaults::snapshot::DEFAULT_STORE_DIR)), }) } } @@ -629,11 +627,11 @@ impl FromEnv for SnapshotPartial { { let mut emitter = Emitter::new(); - let store_path = ParseEnvResult::parse_simple( + let store_dir = ParseEnvResult::parse_simple( &mut emitter, env, - "SNAPSHOT_STORE", - "snapshot.store_path", + "SNAPSHOT_STORE_DIR", + "snapshot.store_dir", ) .into(); let creation_enabled = ParseEnvResult::parse_simple( @@ -647,7 +645,7 @@ impl FromEnv for SnapshotPartial { emitter.finish()?; Ok(Self { - store_dir: store_path, + store_dir, creation_enabled, ..Self::default() }) diff --git a/config/tests/fixtures.rs b/config/tests/fixtures.rs index fa901da3d8e..214290a1eb2 100644 --- a/config/tests/fixtures.rs +++ b/config/tests/fixtures.rs @@ -82,7 +82,7 @@ fn minimal_config_snapshot() -> Result<()> { }, kura: Kura { init_mode: Strict, - block_store_path: "./storage", + store_dir: "./storage", debug_output_new_blocks: false, }, sumeragi: Sumeragi { @@ -102,11 +102,11 @@ fn minimal_config_snapshot() -> Result<()> { }, block_sync: BlockSync { gossip_period: 10s, - batch_size: 4, + gossip_max_size: 4, }, transaction_gossiper: TransactionGossiper { gossip_period: 1s, - batch_size: 500, + gossip_max_size: 500, }, live_query_store: LiveQueryStore { idle_time: 30s, @@ -114,7 +114,7 @@ fn minimal_config_snapshot() -> Result<()> { logger: Logger { level: INFO, format: Full, - tokio_console_addr: 127.0.0.1:5555, + tokio_console_address: 127.0.0.1:5555, }, queue: Queue { capacity: 65536, @@ -124,7 +124,7 @@ fn minimal_config_snapshot() -> Result<()> { }, snapshot: Snapshot { create_every: 60s, - store_path: "./storage/snapshot", + store_dir: "./storage/snapshot", creation_enabled: true, }, telemetry: None, @@ -323,7 +323,7 @@ fn full_envs_set_is_consumed() -> Result<()> { init_mode: Some( Strict, ), - block_store_path: Some( + store_dir: Some( "/store/path/from/env", ), debug: KuraDebugPartial { @@ -342,9 +342,9 @@ fn full_envs_set_is_consumed() -> Result<()> { address: Some( 127.0.0.1:5432, ), + block_gossip_max_size: None, block_gossip_period: None, - max_blocks_per_gossip: None, - max_transactions_per_gossip: None, + transaction_gossip_max_size: None, transaction_gossip_period: None, }, logger: LoggerPartial { @@ -354,7 +354,7 @@ fn full_envs_set_is_consumed() -> Result<()> { format: Some( Pretty, ), - tokio_console_addr: None, + tokio_console_address: None, }, queue: QueuePartial { capacity: None, @@ -364,7 +364,7 @@ fn full_envs_set_is_consumed() -> Result<()> { }, snapshot: SnapshotPartial { create_every: None, - store_path: Some( + store_dir: Some( "/snapshot/path/from/env", ), creation_enabled: Some( @@ -377,7 +377,7 @@ fn full_envs_set_is_consumed() -> Result<()> { min_retry_period: None, max_retry_delay_exponent: None, dev: TelemetryDevPartial { - file: None, + out_file: None, }, }, torii: ToriiPartial { @@ -396,7 +396,7 @@ fn full_envs_set_is_consumed() -> Result<()> { asset_definition_metadata_limits: None, account_metadata_limits: None, domain_metadata_limits: None, - identifier_length_limits: None, + ident_length_limits: None, wasm_fuel_limit: None, wasm_max_memory: None, }, @@ -474,7 +474,7 @@ fn multiple_extends_works() -> Result<()> { format: Some( Compact, ), - tokio_console_addr: None, + tokio_console_address: None, }"#]]; expected.assert_eq(&format!("{layer:#?}")); @@ -508,8 +508,12 @@ fn absolute_paths_are_preserved() { cfg.dev_telemetry.unwrap().out_file, PathBuf::from("/telemetry/file.json") ); - let Genesis::Full { + if let Genesis::Full { file: genesis_file, .. - } = cfg.genesis else { unreachable!() }; - assert_eq!(genesis_file, PathBuf::from("/oh/my/genesis.json")); + } = cfg.genesis + { + assert_eq!(genesis_file, PathBuf::from("/oh/my/genesis.json")); + } else { + unreachable!() + }; } diff --git a/config/tests/fixtures/full.env b/config/tests/fixtures/full.env index fe21b9dd8ee..e79ed99d747 100644 --- a/config/tests/fixtures/full.env +++ b/config/tests/fixtures/full.env @@ -8,9 +8,9 @@ GENESIS_PRIVATE_KEY_DIGEST=ed25519 GENESIS_PRIVATE_KEY_PAYLOAD=8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb API_ADDRESS=127.0.0.1:8080 KURA_INIT_MODE=strict -KURA_BLOCK_STORE=/store/path/from/env +KURA_STORE_DIR=/store/path/from/env KURA_DEBUG_OUTPUT_NEW_BLOCKS=false LOG_LEVEL=DEBUG LOG_FORMAT=pretty -SNAPSHOT_STORE=/snapshot/path/from/env +SNAPSHOT_STORE_DIR=/snapshot/path/from/env SNAPSHOT_CREATION_ENABLED=false From 838a4cfdbae2a5df9e4d18912c1bcac958fce720 Mon Sep 17 00:00:00 2001 From: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> Date: Fri, 16 Feb 2024 17:31:22 +0900 Subject: [PATCH 91/94] [build]: add notes to Dockerfile Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- Dockerfile | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Dockerfile b/Dockerfile index 5b78c51c074..fbc66fc751c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -29,6 +29,7 @@ ENV CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_LINKER=/x86_64-linux-musl-native/bin/ # builder stage WORKDIR /iroha COPY . . +# FIXME: shouldn't it only build `iroha`, `iroha_client_cli`, and `kagami`? RUN cargo build --target x86_64-unknown-linux-musl --profile deploy @@ -39,9 +40,12 @@ ARG STORAGE=/storage ARG TARGET_DIR=/iroha/target/x86_64-unknown-linux-musl/deploy ENV BIN_PATH=/usr/local/bin/ ENV CONFIG_DIR=/config + +# FIXME: these are obsolete ENV IROHA2_CONFIG_PATH=$CONFIG_DIR/config.json ENV IROHA2_GENESIS_PATH=$CONFIG_DIR/genesis.json ENV KURA_BLOCK_STORE_PATH=$STORAGE + ENV WASM_DIRECTORY=/app/.cache/wasmtime ENV USER=iroha ENV UID=1001 From bc5c420fce2fbbb25778e1d2a260f0a429a8f351 Mon Sep 17 00:00:00 2001 From: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> Date: Fri, 16 Feb 2024 17:33:47 +0900 Subject: [PATCH 92/94] [chore]: update style in `iroha_test_config.toml` Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- config/iroha_test_config.toml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/config/iroha_test_config.toml b/config/iroha_test_config.toml index 4495aa70645..eaade1dbfe4 100644 --- a/config/iroha_test_config.toml +++ b/config/iroha_test_config.toml @@ -1,7 +1,6 @@ chain_id = "00000000-0000-0000-0000-000000000000" public_key = "ed01201C61FAF8FE94E253B93114240394F79A607B7FA55F9E5A41EBEC74B88055768B" -private_key.digest_function = "ed25519" -private_key.payload = "282ED9F3CF92811C3818DBC4AE594ED59DC1A2F78E4241E31924E101D6B1FB831C61FAF8FE94E253B93114240394F79A607B7FA55F9E5A41EBEC74B88055768B" +private_key = { digest_function = "ed25519", payload = "282ED9F3CF92811C3818DBC4AE594ED59DC1A2F78E4241E31924E101D6B1FB831C61FAF8FE94E253B93114240394F79A607B7FA55F9E5A41EBEC74B88055768B" } [network] address = "127.0.0.1:1337" @@ -9,8 +8,7 @@ address = "127.0.0.1:1337" [genesis] public_key = "ed01204CFFD0EE429B1BDD36B3910EC570852B8BB63F18750341772FB46BC856C5CAAF" file = "./genesis.json" -private_key.digest_function = "ed25519" -private_key.payload = "D748E18CE60CB30DEA3E73C9019B7AF45A8D465E3D71BCC9A5EF99A008205E534CFFD0EE429B1BDD36B3910EC570852B8BB63F18750341772FB46BC856C5CAAF" +private_key = { digest_function = "ed25519", payload = "D748E18CE60CB30DEA3E73C9019B7AF45A8D465E3D71BCC9A5EF99A008205E534CFFD0EE429B1BDD36B3910EC570852B8BB63F18750341772FB46BC856C5CAAF" } [torii] address = "127.0.0.1:8080" From 3a6bbc58a844b1f8cb792add5de8a9e436337aa2 Mon Sep 17 00:00:00 2001 From: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> Date: Fri, 16 Feb 2024 17:39:15 +0900 Subject: [PATCH 93/94] [test]: fix old params in `test_env.py` Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- scripts/test_env.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/test_env.py b/scripts/test_env.py index e1e94910cf6..13301c1c8ee 100755 --- a/scripts/test_env.py +++ b/scripts/test_env.py @@ -123,10 +123,10 @@ def __init__(self, args: argparse.Namespace, nth: int): "address": f"{self.host_ip}:{self.api_port}" }, "kura": { - "block_store_path": "storage" + "store_dir": "storage" }, "snapshot": { - "store_path": "storage/snapshot" + "store_dir": "storage/snapshot" }, # it is not available in debug iroha build # "logger": { From 7d54e8783dafb55f1b37d6529758e6d407f11f48 Mon Sep 17 00:00:00 2001 From: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> Date: Fri, 16 Feb 2024 17:39:28 +0900 Subject: [PATCH 94/94] [test]: add a note to `panic_on_invalid_genesis.sh` Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- scripts/tests/panic_on_invalid_genesis.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/tests/panic_on_invalid_genesis.sh b/scripts/tests/panic_on_invalid_genesis.sh index a76e5580f15..6ce79c0476e 100755 --- a/scripts/tests/panic_on_invalid_genesis.sh +++ b/scripts/tests/panic_on_invalid_genesis.sh @@ -1,6 +1,7 @@ #!/bin/bash set -ex # Setup env +# FIXME: these are obsolete export TORII_P2P_ADDR='127.0.0.1:1341' export TORII_API_URL='127.0.0.1:8084' export IROHA_PUBLIC_KEY='ed01201C61FAF8FE94E253B93114240394F79A607B7FA55F9E5A41EBEC74B88055768B'