From 0fef278c09097f9592684dc5efcd53ffe08ae9f5 Mon Sep 17 00:00:00 2001 From: Asem Abdelhady Date: Mon, 11 Dec 2023 04:26:01 +0300 Subject: [PATCH 01/21] [fix] #4090 #3858: Fix having to pass IROHA_SKIP_WASM_CHECKS env variable with true (#4096) * [fix] #4090: Fix having to pass IROHA_SKIP_WASM_CHECKS env variable with true Signed-off-by: Asem-Abdelhady * [fix]: Fix having to pass variable IROHA_SKIP_WASM_CHECKS with checking PORFILE variable to be test Signed-off-by: Asem-Abdelhady * [fix]: delete formating from the src code Signed-off-by: Asem-Abdelhady * [delete] the check needed in building for client and cli Signed-off-by: Asem-Abdelhady * [add] format and checking of smart contracts in CI Signed-off-by: Asem-Abdelhady * [add] check and format in pr for dev Signed-off-by: Asem-Abdelhady * [change] format chekck from dev and dev-pr to dev-pr-static Signed-off-by: Asem-Abdelhady * [revert] paths deletion in dev-pr && [fix] naming issues in dev-pr-static CIs Signed-off-by: Asem-Abdelhady * [change] revert double quotes to single quotes in paths && remove the checking from dev CI Signed-off-by: Asem-Abdelhady * [delete] build script from client && [delete] IROHA_SKIP_WASM_CHECKS from nix Signed-off-by: Asem-Abdelhady * [delete] unecessary print statments and stray comment from cli build.rs Signed-off-by: Asem-Abdelhady --------- Signed-off-by: Asem-Abdelhady --- .github/workflows/iroha2-dev-pr-static.yml | 20 +++++++++- cli/build.rs | 21 +---------- client/build.rs | 29 --------------- flake.nix | 3 -- tools/wasm_builder_cli/src/main.rs | 6 --- wasm_builder/src/lib.rs | 43 +--------------------- 6 files changed, 22 insertions(+), 100 deletions(-) delete mode 100644 client/build.rs diff --git a/.github/workflows/iroha2-dev-pr-static.yml b/.github/workflows/iroha2-dev-pr-static.yml index 7fadced88d1..c89ef6976d8 100644 --- a/.github/workflows/iroha2-dev-pr-static.yml +++ b/.github/workflows/iroha2-dev-pr-static.yml @@ -17,7 +17,25 @@ env: RUSTUP_TOOLCHAIN: nightly-2023-06-25 jobs: - analysis: + smart_contracts_analysis: + runs-on: ubuntu-latest + container: + image: hyperledger/iroha2-ci:nightly-2023-06-25 + steps: + - uses: actions/checkout@v4 + - uses: Swatinem/rust-cache@v2 + + - name: Default executor format + run: | + cd ./default_executor + mold --run cargo fmt --all -- --check + + - name: Integration tests smart contracts format + run: | + cd ./client/tests/integration/smartcontracts + mold --run cargo fmt --all -- --check + + workspace_analysis: runs-on: ubuntu-latest container: image: hyperledger/iroha2-ci:nightly-2023-06-25 diff --git a/cli/build.rs b/cli/build.rs index 23ce6a29d23..699f4559ba8 100644 --- a/cli/build.rs +++ b/cli/build.rs @@ -1,22 +1,10 @@ -//! Build script to extract git hash of iroha build and to check runtime executor +//! Build script to extract git hash of iroha build use eyre::{eyre, Result, WrapErr}; -const DEFAULT_EXECUTOR_PATH: &str = "../default_executor"; - fn main() -> Result<()> { - println!("cargo:rerun-if-changed=build.rs"); - println!("cargo:rerun-if-changed={DEFAULT_EXECUTOR_PATH}"); - extract_git_hash()?; - // HACK: used by Nix, since at the moment - // the checks are a process that's hard to accomodate - // in Nix environment - if std::option_env!("IROHA_SKIP_WASM_CHECKS").is_none() { - check_default_executor()?; - } - Ok(()) } @@ -28,10 +16,3 @@ fn extract_git_hash() -> Result<()> { .map_err(|err| eyre!(Box::new(err))) .wrap_err("Failed to extract git hash") } - -/// Apply `cargo check` to the smartcontract. -fn check_default_executor() -> Result<()> { - iroha_wasm_builder::Builder::new(DEFAULT_EXECUTOR_PATH) - .format() - .check() -} diff --git a/client/build.rs b/client/build.rs deleted file mode 100644 index 4f60d4cf4b0..00000000000 --- a/client/build.rs +++ /dev/null @@ -1,29 +0,0 @@ -//! Build script which checks smartcontracts for tests -//! -//! Technically this script is used only for testing purposes, but current cargo implementation -//! doesn't allow to run build script only for tests or get info about current profile from it. -//! See [cargo issue #4001](https://github.com/rust-lang/cargo/issues/4001) -//! and [#4789](https://github.com/rust-lang/cargo/issues/4789) - -use eyre::Result; - -const TEST_SMARTCONTRACTS_DIR: &str = "tests/integration/smartcontracts"; - -fn main() -> Result<()> { - println!("cargo:rerun-if-changed={TEST_SMARTCONTRACTS_DIR}"); - - // HACK: used by Nix, since at the moment - // the checks are a process that's hard to accomodate - // in Nix environment - if std::option_env!("IROHA_SKIP_WASM_CHECKS").is_none() { - check_all_smartcontracts()?; - } - - Ok(()) -} - -fn check_all_smartcontracts() -> Result<()> { - iroha_wasm_builder::Builder::new(TEST_SMARTCONTRACTS_DIR) - .format() - .check() -} diff --git a/flake.nix b/flake.nix index 3b067fee410..0d7a40f63bf 100755 --- a/flake.nix +++ b/flake.nix @@ -154,8 +154,6 @@ VERGEN_IDEMPOTENT = true; VERGEN_GIT_SHA = self.rev or "?dirty tree?"; - # Temporary workaround - IROHA_SKIP_WASM_CHECKS = true; }; in rec { inherit mkIroha; @@ -216,7 +214,6 @@ fenix'.rust-analyzer ]; - IROHA_SKIP_WASM_CHECKS = true; }; }); } diff --git a/tools/wasm_builder_cli/src/main.rs b/tools/wasm_builder_cli/src/main.rs index 436dc94decb..645b079a451 100644 --- a/tools/wasm_builder_cli/src/main.rs +++ b/tools/wasm_builder_cli/src/main.rs @@ -19,10 +19,6 @@ enum Cli { Build { #[command(flatten)] common: CommonArgs, - /// Enable smartcontract formatting using `cargo fmt`. - // TODO: why it is a part of `build` in wasm_builder? - #[arg(long)] - format: bool, /// Optimize WASM output. #[arg(long)] optimize: bool, @@ -48,12 +44,10 @@ fn main() -> color_eyre::Result<()> { } Cli::Build { common: CommonArgs { path }, - format, optimize, outfile, } => { let builder = Builder::new(&path).show_output(); - let builder = if format { builder.format() } else { builder }; let output = { // not showing the spinner here, cargo does a progress bar for us diff --git a/wasm_builder/src/lib.rs b/wasm_builder/src/lib.rs index 1cc36788b22..e5857900636 100644 --- a/wasm_builder/src/lib.rs +++ b/wasm_builder/src/lib.rs @@ -27,7 +27,6 @@ const TOOLCHAIN: &str = "+nightly-2023-06-25"; /// fn main() -> Result<()> { /// let bytes = Builder::new("relative/path/to/smartcontract/") /// .out_dir("path/to/out/dir") // Optional: Set output directory -/// .format() // Optional: Enable smartcontract formatting /// .build()? // Run build /// .optimize()? // Optimize WASM output /// .into_bytes()?; // Get resulting WASM bytes @@ -44,8 +43,6 @@ pub struct Builder<'path, 'out_dir> { path: &'path Path, /// Build output directory out_dir: Option<&'out_dir Path>, - /// Flag to enable smartcontract formatting - format: bool, /// Flag controlling whether to show output of the build process show_output: bool, } @@ -61,7 +58,6 @@ impl<'path, 'out_dir> Builder<'path, 'out_dir> { Self { path: relative_path.as_ref(), out_dir: None, - format: false, show_output: false, } } @@ -79,14 +75,6 @@ impl<'path, 'out_dir> Builder<'path, 'out_dir> { self } - /// Enable smartcontract formatting using `cargo fmt`. - /// - /// Disabled by default. - pub fn format(mut self) -> Self { - self.format = true; - self - } - /// Enable showing output of the build process. /// /// Disabled by default. @@ -99,7 +87,7 @@ impl<'path, 'out_dir> Builder<'path, 'out_dir> { /// /// # Errors /// - /// Can fail due to multiple reasons like invalid path, failed formatting, failed build, etc. + /// Can fail due to multiple reasons like invalid path, failed build, etc. pub fn check(self) -> Result<()> { self.into_internal()?.check() } @@ -108,8 +96,7 @@ impl<'path, 'out_dir> Builder<'path, 'out_dir> { /// /// # Errors /// - /// Can fail due to multiple reasons like invalid path, failed formatting, - /// failed build, etc. + /// Can fail due to multiple reasons like invalid path, failed build, etc. /// /// Will also return error if ran on workspace and not on the concrete package. pub fn build(self) -> Result { @@ -126,7 +113,6 @@ impl<'path, 'out_dir> Builder<'path, 'out_dir> { || -> Result<_> { Ok(Cow::Owned(Self::default_out_dir()?)) }, |out_dir| Ok(Cow::Borrowed(out_dir)), )?, - format: self.format, show_output: self.show_output, }) } @@ -180,14 +166,11 @@ mod internal { pub struct Builder<'out_dir> { pub absolute_path: PathBuf, pub out_dir: Cow<'out_dir, Path>, - pub format: bool, pub show_output: bool, } impl Builder<'_> { pub fn check(self) -> Result<()> { - self.maybe_format()?; - self.check_smartcontract().wrap_err_with(|| { format!( "Failed to check the smartcontract at path: {}", @@ -197,8 +180,6 @@ mod internal { } pub fn build(self) -> Result { - self.maybe_format()?; - let absolute_path = self.absolute_path.clone(); self.build_smartcontract().wrap_err_with(|| { format!( @@ -208,18 +189,6 @@ mod internal { }) } - fn maybe_format(&self) -> Result<()> { - if self.format { - self.format_smartcontract().wrap_err_with(|| { - format!( - "Failed to format the smartcontract at path: {}", - self.absolute_path.display() - ) - })?; - } - Ok(()) - } - fn build_options() -> impl Iterator { [ "--release", @@ -235,14 +204,6 @@ mod internal { .into_iter() } - fn format_smartcontract(&self) -> Result<()> { - check_command( - self.show_output, - cargo_command().current_dir(&self.absolute_path).arg("fmt"), - "cargo fmt", - ) - } - fn get_base_command(&self, cmd: &'static str) -> std::process::Command { let mut command = cargo_command(); command From 23e7214feb6dbea5d2fc5b963dad488523cb71e1 Mon Sep 17 00:00:00 2001 From: 0x009922 <43530070+0x009922@users.noreply.github.com> Date: Tue, 12 Dec 2023 07:49:51 +0000 Subject: [PATCH 02/21] [fix] #4030, #4063, #4064, #4079: re-architect logger and dynamic configuration (#4100) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: ⭐️NINIKA⭐️ Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- CONTRIBUTING.md | 8 +- Cargo.lock | 43 +-- README.md | 12 +- cli/src/lib.rs | 142 +++++-- cli/src/main.rs | 32 +- cli/src/style.rs | 16 +- cli/src/torii/mod.rs | 17 +- cli/src/torii/routing.rs | 81 ++-- client/benches/torii.rs | 14 +- client/benches/tps/oneshot.rs | 2 +- client/src/client.rs | 40 +- client/tests/integration/config.rs | 56 ++- client/tests/integration/unstable_network.rs | 2 +- config/Cargo.toml | 1 + config/base/src/lib.rs | 2 - config/base/src/runtime_upgrades.rs | 362 ------------------ config/iroha_test_config.json | 7 +- config/src/client_api.rs | 70 ++++ config/src/iroha.rs | 4 +- config/src/kura.rs | 34 +- config/src/lib.rs | 34 +- config/src/logger.rs | 131 +++---- config/src/telemetry.rs | 54 ++- configs/peer/config.json | 9 +- core/benches/blocks/apply_blocks_oneshot.rs | 15 +- .../benches/blocks/validate_blocks_oneshot.rs | 15 +- core/benches/kura.rs | 9 +- core/src/kiso.rs | 206 ++++++++++ core/src/kura.rs | 25 +- core/src/lib.rs | 1 + core/test_network/src/lib.rs | 12 +- docs/source/references/config.md | 84 +--- futures/src/lib.rs | 2 +- futures/tests/basic.rs | 19 +- logger/Cargo.toml | 6 +- logger/src/actor.rs | 145 +++++++ logger/src/layer.rs | 8 +- logger/src/lib.rs | 182 ++++----- logger/src/telemetry.rs | 100 +++-- logger/tests/configuration.rs | 25 +- logger/tests/setting_logger.rs | 24 +- logger/tests/telemetry.rs | 16 +- p2p/tests/integration/p2p.rs | 34 +- scripts/test_env.py | 6 +- telemetry/Cargo.toml | 2 +- telemetry/src/dev.rs | 25 +- telemetry/src/futures.rs | 7 +- telemetry/src/ws.rs | 127 +++--- 48 files changed, 1072 insertions(+), 1196 deletions(-) delete mode 100644 config/base/src/runtime_upgrades.rs create mode 100644 config/src/client_api.rs create mode 100644 core/src/kiso.rs create mode 100644 logger/src/actor.rs diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2fc9c77a9be..4869dd4e4c0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -231,7 +231,7 @@ Follow these commit guidelines:
Expand to learn how to change the log level or write logs to a JSON. -If one of your tests is failing, you may want to decrease the maximum logging level. By default, Iroha only logs `INFO` level messages, but retains the ability to produce both `DEBUG` and `TRACE` level logs. This setting can be changed either using the `MAX_LOG_LEVEL` environment variable for code-based tests, or using the `/configuration` endpoint on one of the peers in a deployed network. +If one of your tests is failing, you may want to decrease the maximum logging level. By default, Iroha only logs `INFO` level messages, but retains the ability to produce both `DEBUG` and `TRACE` level logs. This setting can be changed either using the `LOG_LEVEL` environment variable for code-based tests, or using the `/configuration` endpoint on one of the peers in a deployed network. While logs printed in the `stdout` are sufficient, you may find it more convenient to produce `json`-formatted logs into a separate file and parse them using either [node-bunyan](https://www.npmjs.com/package/bunyan) or [rust-bunyan](https://crates.io/crates/bunyan). @@ -251,8 +251,8 @@ In this case you should compile iroha with support of tokio console like that: RUSTFLAGS="--cfg tokio_unstable" cargo build --features tokio-console ``` -Port for tokio console can by configured through `TOKIO_CONSOLE_ADDR` configuration parameter (or environment variable). -Using tokio console require log level to be `TRACE`, can be enabled through configuration parameter or environment variable `MAX_LOG_LEVEL`. +Port for tokio console can by configured through `LOG_TOKIO_CONSOLE_ADDR` configuration parameter (or environment variable). +Using tokio console require log level to be `TRACE`, can be enabled through configuration parameter or environment variable `LOG_LEVEL`. Example of running iroha with tokio console support using `scripts/test_env.sh`: @@ -260,7 +260,7 @@ Example of running iroha with tokio console support using `scripts/test_env.sh`: # 1. Compile iroha RUSTFLAGS="--cfg tokio_unstable" cargo build --features tokio-console # 2. Run iroha with TRACE log level -MAX_LOG_LEVEL=TRACE ./scripts/test_env.sh setup +LOG_LEVEL=TRACE ./scripts/test_env.sh setup # 3. Access iroha. Peers will be available on ports 5555, 5556, ... tokio-console http://127.0.0.1:5555 ``` diff --git a/Cargo.lock b/Cargo.lock index 9dadaa730bd..dd4bd2c1248 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -40,7 +40,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" dependencies = [ "cfg-if", - "getrandom 0.2.10", "once_cell", "version_check", ] @@ -1722,16 +1721,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "gethostname" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1ebd34e35c46e00bb73e81363248d627782724609fe1b6396f553f68fe3862e" -dependencies = [ - "libc", - "winapi", -] - [[package]] name = "getrandom" version = "0.1.16" @@ -2746,6 +2735,7 @@ dependencies = [ "cfg-if", "derive_more", "displaydoc", + "expect-test", "eyre", "iroha_config_base", "iroha_crypto", @@ -3063,9 +3053,9 @@ dependencies = [ "iroha_data_model", "once_cell", "serde_json", + "thiserror", "tokio", "tracing", - "tracing-bunyan-formatter", "tracing-core", "tracing-error", "tracing-futures", @@ -5556,6 +5546,7 @@ dependencies = [ "futures-core", "pin-project-lite", "tokio", + "tokio-util", ] [[package]] @@ -5699,24 +5690,6 @@ dependencies = [ "syn 2.0.38", ] -[[package]] -name = "tracing-bunyan-formatter" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5c266b9ac83dedf0e0385ad78514949e6d89491269e7065bee51d2bb8ec7373" -dependencies = [ - "ahash", - "gethostname", - "log", - "serde", - "serde_json", - "time", - "tracing", - "tracing-core", - "tracing-log", - "tracing-subscriber", -] - [[package]] name = "tracing-core" version = "0.1.31" @@ -5759,13 +5732,12 @@ dependencies = [ ] [[package]] -name = "tracing-log" +name = "tracing-serde" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922" +checksum = "bc6b213177105856957181934e4920de57730fc69bf42c37ee5bb664d406d9e1" dependencies = [ - "lazy_static", - "log", + "serde", "tracing-core", ] @@ -5779,11 +5751,14 @@ dependencies = [ "nu-ansi-term", "once_cell", "regex", + "serde", + "serde_json", "sharded-slab", "smallvec", "thread_local", "tracing", "tracing-core", + "tracing-serde", ] [[package]] diff --git a/README.md b/README.md index 237481c5a48..5877ff5f40d 100644 --- a/README.md +++ b/README.md @@ -169,7 +169,7 @@ For a list of all endpoints, available operations, and ways to customize them wi By default, Iroha provides logs in a human-readable format and prints them out to `stdout`. -The logging level can be changed either via a [configuration option](./docs/source/references/config.md#loggermax_log_level) or at run-time using the `configuration` endpoint. +The logging level can be changed either via the [`logger.level` configuration parameter](./docs/source/references/config.md#loggerlevel) or at run-time using the `configuration` endpoint.
Example: changing log level @@ -178,17 +178,13 @@ For example, if your Iroha instance is running at `127.0.0.1:8080` and you want curl -X POST \ -H 'content-type: application/json' \ http://127.0.0.1:8080/configuration \ - -d '{"LogLevel": "DEBUG"}' -i + -d '{"logger": {"level": "DEBUG"}}' -i ```
-#### JSON Logging Mode +The log format might be configured via the [`logger.format` configuration parameter](./docs/source/references/config.md#loggerformat). Possible values are: `full` (default), `compact`, `pretty`, and `json`. -Additionally, Iroha supports a JSON logging mode. - -To enable it, provide the [logging file](./docs/source/references/config.md#loggerlog_file_path) to store the logs in. On UNIX, you can also specify `/dev/stdout` or `/dev/stderr` if you prefer to pipe the output to [`bunyan`](https://www.npmjs.com/package/bunyan). - -[Log rotation](https://www.commandlinux.com/man-page/man5/logrotate.conf.5.html) is the responsibility of the peer administrator. +Output goes to `/dev/stdout`. Piping to files or [log rotation](https://www.commandlinux.com/man-page/man5/logrotate.conf.5.html) is the responsibility of the peer administrator. ### Monitoring diff --git a/cli/src/lib.rs b/cli/src/lib.rs index bb72b92c049..2d8a9a9c078 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -13,11 +13,13 @@ use iroha_config::{ base::proxy::{LoadFromDisk, LoadFromEnv, Override}, iroha::{Configuration, ConfigurationProxy}, path::Path as ConfigPath, + telemetry::Configuration as TelemetryConfiguration, }; use iroha_core::{ block_sync::{BlockSynchronizer, BlockSynchronizerHandle}, gossiper::{TransactionGossiper, TransactionGossiperHandle}, handler::ThreadHandler, + kiso::KisoHandle, kura::Kura, prelude::{World, WorldStateView}, query::store::LiveQueryStore, @@ -30,6 +32,7 @@ use iroha_core::{ }; use iroha_data_model::prelude::*; use iroha_genesis::GenesisNetwork; +use iroha_logger::actor::LoggerHandle; use tokio::{ signal, sync::{broadcast, mpsc, Notify}, @@ -74,6 +77,25 @@ impl Default for Arguments { } } +/// Reflects user decision (or its absence) about ANSI colored output +#[derive(Copy, Clone, Debug)] +pub enum TerminalColorsArg { + /// Coloring should be decided automatically + Default, + /// User explicitly specified the value + UserSet(bool), +} + +impl TerminalColorsArg { + /// Transforms the enumeration into flag + pub fn evaluate(self) -> bool { + match self { + Self::Default => supports_color::on(supports_color::Stream::Stdout).is_some(), + Self::UserSet(x) => x, + } + } +} + /// Iroha is an /// [Orchestrator](https://en.wikipedia.org/wiki/Orchestration_%28computing%29) /// of the system. It configures, coordinates and manages transactions @@ -85,6 +107,8 @@ impl Default for Arguments { /// forgot this step. #[must_use = "run `.start().await?` to not immediately stop Iroha"] pub struct Iroha { + /// Actor responsible for the configuration + pub kiso: KisoHandle, /// Queue of transactions pub queue: Arc, /// Sumeragi consensus @@ -225,7 +249,7 @@ impl Iroha { pub async fn with_genesis( genesis: Option, config: Configuration, - telemetry: Option, + logger: LoggerHandle, ) -> Result { let listen_addr = config.torii.p2p_addr.clone(); let network = IrohaNetwork::start(listen_addr, config.sumeragi.key_pair.clone()) @@ -234,15 +258,11 @@ impl Iroha { let (events_sender, _) = broadcast::channel(10000); let world = World::with( - [genesis_domain(&config)], + [genesis_domain(config.genesis.account_public_key.clone())], config.sumeragi.trusted_peers.peers.clone(), ); - let kura = Kura::new( - config.kura.init_mode, - std::path::Path::new(&config.kura.block_store_path), - config.kura.debug_output_new_blocks, - )?; + let kura = Kura::new(&config.kura)?; let live_query_store_handle = LiveQueryStore::from_configuration(config.live_query_store).start(); @@ -273,11 +293,10 @@ impl Iroha { ); let queue = Arc::new(Queue::from_configuration(&config.queue)); - if Self::start_telemetry(telemetry, &config).await? { - iroha_logger::info!("Telemetry started") - } else { - iroha_logger::warn!("Telemetry not started") - } + match Self::start_telemetry(&logger, &config.telemetry).await? { + TelemetryStartStatus::Started => iroha_logger::info!("Telemetry started"), + TelemetryStartStatus::NotStarted => iroha_logger::warn!("Telemetry not started"), + }; let kura_thread_handler = Kura::start(Arc::clone(&kura)); @@ -328,8 +347,11 @@ impl Iroha { let snapshot_maker = SnapshotMaker::from_configuration(&config.snapshot, sumeragi.clone()).start(); - let torii = Torii::from_configuration( - config.clone(), + let kiso = KisoHandle::new(config.clone()); + + let torii = Torii::new( + kiso.clone(), + &config.torii, Arc::clone(&queue), events_sender, Arc::clone(¬ify_shutdown), @@ -338,12 +360,15 @@ impl Iroha { Arc::clone(&kura), ); + Self::spawn_configuration_updates_broadcasting(kiso.clone(), logger.clone()); + Self::start_listening_signal(Arc::clone(¬ify_shutdown))?; Self::prepare_panic_hook(notify_shutdown); let torii = Some(torii); Ok(Self { + kiso, queue, sumeragi, kura, @@ -389,37 +414,46 @@ impl Iroha { #[cfg(feature = "telemetry")] async fn start_telemetry( - telemetry: Option<( - iroha_logger::SubstrateTelemetry, - iroha_logger::FutureTelemetry, - )>, - config: &Configuration, - ) -> Result { + logger: &LoggerHandle, + config: &TelemetryConfiguration, + ) -> Result { #[allow(unused)] - if let Some((substrate_telemetry, telemetry_future)) = telemetry { - #[cfg(feature = "dev-telemetry")] - { - iroha_telemetry::dev::start(&config.telemetry, telemetry_future) + let (config_for_regular, config_for_dev) = config.parse(); + + #[cfg(feature = "dev-telemetry")] + { + if let Some(config) = config_for_dev { + 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) .await .wrap_err("Failed to setup telemetry for futures")?; } - iroha_telemetry::ws::start(&config.telemetry, substrate_telemetry) + } + + if let Some(config) = config_for_regular { + 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) .await - .wrap_err("Failed to setup telemetry for websocket communication") + .wrap_err("Failed to setup telemetry for websocket communication")?; + + Ok(TelemetryStartStatus::Started) } else { - Ok(false) + Ok(TelemetryStartStatus::NotStarted) } } #[cfg(not(feature = "telemetry"))] async fn start_telemetry( - _telemetry: Option<( - iroha_logger::SubstrateTelemetry, - iroha_logger::FutureTelemetry, - )>, - _config: &Configuration, - ) -> Result { - Ok(false) + _logger: &LoggerHandle, + _config: &TelemetryConfiguration, + ) -> Result { + Ok(TelemetryStartStatus::NotStarted) } #[allow(clippy::redundant_pub_crate)] @@ -448,22 +482,52 @@ impl Iroha { Ok(handle) } + + /// 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( + kiso: KisoHandle, + logger: LoggerHandle, + ) -> task::JoinHandle<()> { + tokio::spawn(async move { + let mut log_level_update = kiso + .subscribe_on_log_level() + .await + // FIXME: don't like neither the message nor inability to throw Result to the outside + .expect("Cannot proceed without working subscriptions"); + + loop { + tokio::select! { + Ok(()) = log_level_update.changed() => { + let value = *log_level_update.borrow_and_update(); + if let Err(error) = logger.reload_level(value).await { + iroha_logger::error!("Failed to reload log level: {error}"); + }; + } + }; + } + }) + } } -fn genesis_account(public_key: iroha_crypto::PublicKey) -> Account { +enum TelemetryStartStatus { + Started, + NotStarted, +} + +fn genesis_account(public_key: PublicKey) -> Account { Account::new(iroha_genesis::GENESIS_ACCOUNT_ID.clone(), [public_key]) .build(&iroha_genesis::GENESIS_ACCOUNT_ID) } -fn genesis_domain(configuration: &Configuration) -> Domain { - let account_public_key = &configuration.genesis.account_public_key; - +fn genesis_domain(public_key: PublicKey) -> Domain { let mut domain = Domain::new(iroha_genesis::GENESIS_DOMAIN_ID.clone()) .build(&iroha_genesis::GENESIS_ACCOUNT_ID); domain.accounts.insert( iroha_genesis::GENESIS_ACCOUNT_ID.clone(), - genesis_account(account_public_key.clone()), + genesis_account(public_key), ); domain diff --git a/cli/src/main.rs b/cli/src/main.rs index e2a07e6ae74..16629ea7ea7 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -2,7 +2,7 @@ use std::env; use color_eyre::eyre::WrapErr as _; -use iroha::style::Styling; +use iroha::{style::Styling, TerminalColorsArg}; use iroha_config::path::Path as ConfigPath; use iroha_genesis::{GenesisNetwork, RawGenesisBlock}; use owo_colors::OwoColorize as _; @@ -10,6 +10,8 @@ use owo_colors::OwoColorize as _; const HELP_ARG: [&str; 2] = ["--help", "-h"]; const SUBMIT_ARG: [&str; 2] = ["--submit-genesis", "-s"]; const VERSION_ARG: [&str; 2] = ["--version", "-V"]; +const TERMINAL_COLORS_ARG: &str = "--terminal-colors"; +const NO_TERMINAL_COLORS_ARG: &str = "--no-terminal-colors"; const REQUIRED_ENV_VARS: [(&str, &str); 7] = [ ("IROHA_TORII", "Torii (gateway) endpoint configuration"), @@ -42,11 +44,29 @@ const REQUIRED_ENV_VARS: [(&str, &str); 7] = [ /// - Telemetry setup /// - [`Sumeragi`] init async fn main() -> Result<(), color_eyre::Report> { - let styling = Styling::new(); - if !iroha::style::should_disable_color() { + let mut args = iroha::Arguments::default(); + + let terminal_colors = env::var("TERMINAL_COLORS") + .ok() + .map(|s| !s.as_str().parse().unwrap_or(true)) + .or_else(|| { + if env::args().any(|a| a == TERMINAL_COLORS_ARG) { + Some(true) + } else if env::args().any(|a| a == NO_TERMINAL_COLORS_ARG) { + Some(false) + } else { + None + } + }) + .map_or(TerminalColorsArg::Default, TerminalColorsArg::UserSet) + .evaluate(); + + if terminal_colors { color_eyre::install()?; } - let mut args = iroha::Arguments::default(); + + let styling = Styling::new(terminal_colors); + if env::args().any(|a| HELP_ARG.contains(&a.as_str())) { print_help(&styling)?; return Ok(()); @@ -109,7 +129,7 @@ async fn main() -> Result<(), color_eyre::Report> { } let config = iroha::combine_configs(&args)?; - let telemetry = iroha_logger::init(&config.logger)?; + let logger = iroha_logger::init_global(&config.logger, terminal_colors)?; if !config.disable_panic_terminal_colors { // FIXME: it shouldn't be logged here; it is a part of configuration domain // this message can be very simply broken by the changes in the configuration @@ -140,7 +160,7 @@ async fn main() -> Result<(), color_eyre::Report> { }) .transpose()?; - iroha::Iroha::with_genesis(genesis, config, telemetry) + iroha::Iroha::with_genesis(genesis, config, logger) .await? .start() .await?; diff --git a/cli/src/style.rs b/cli/src/style.rs index dbf64b975e7..393ae591140 100644 --- a/cli/src/style.rs +++ b/cli/src/style.rs @@ -25,22 +25,14 @@ impl Default for Styling { } } -/// Determine if message colourisation is to be enabled -pub fn should_disable_color() -> bool { - supports_color::on(supports_color::Stream::Stdout).is_none() - || std::env::var("TERMINAL_COLORS") - .map(|s| !s.as_str().parse().unwrap_or(true)) - .unwrap_or(false) -} - impl Styling { #[must_use] /// Constructor - pub fn new() -> Self { - if should_disable_color() { - Self::no_color() - } else { + pub fn new(terminal_colors: bool) -> Self { + if terminal_colors { Self::default() + } else { + Self::no_color() } } diff --git a/cli/src/torii/mod.rs b/cli/src/torii/mod.rs index 9594362ab5d..a20f2c3f11a 100644 --- a/cli/src/torii/mod.rs +++ b/cli/src/torii/mod.rs @@ -10,7 +10,9 @@ use std::{ }; use futures::{stream::FuturesUnordered, StreamExt}; +use iroha_config::torii::Configuration as ToriiConfiguration; use iroha_core::{ + kiso::{Error as KisoError, KisoHandle}, kura::Kura, prelude::*, query::store::LiveQueryStoreHandle, @@ -18,6 +20,7 @@ use iroha_core::{ sumeragi::SumeragiHandle, EventsSender, }; +use iroha_primitives::addr::SocketAddr; use tokio::sync::Notify; use utils::*; use warp::{ @@ -33,13 +36,15 @@ mod routing; /// Main network handler and the only entrypoint of the Iroha. pub struct Torii { - iroha_cfg: super::Configuration, + kiso: KisoHandle, queue: Arc, events: EventsSender, notify_shutdown: Arc, sumeragi: SumeragiHandle, query_service: LiveQueryStoreHandle, kura: Arc, + transaction_max_content_length: u64, + address: SocketAddr, } /// Torii errors. @@ -53,13 +58,13 @@ pub enum Error { Config(#[source] eyre::Report), /// Failed to push into queue PushIntoQueue(#[from] Box), - /// Attempt to change configuration failed - ConfigurationReload(#[from] iroha_config::base::runtime_upgrades::ReloadError), #[cfg(feature = "telemetry")] /// Error while getting Prometheus metrics Prometheus(#[source] eyre::Report), /// Internal error while getting status StatusFailure(#[source] eyre::Report), + /// Failure caused by configuration subsystem + ConfigurationFailure(#[from] KisoError), /// Cannot find status segment by provided path StatusSegmentNotFound(#[source] eyre::Report), } @@ -82,7 +87,7 @@ impl Error { match self { Query(e) => Self::query_status_code(e), - AcceptTransaction(_) | ConfigurationReload(_) => StatusCode::BAD_REQUEST, + AcceptTransaction(_) => StatusCode::BAD_REQUEST, Config(_) | StatusSegmentNotFound(_) => StatusCode::NOT_FOUND, PushIntoQueue(err) => match **err { queue::Error::Full => StatusCode::INTERNAL_SERVER_ERROR, @@ -90,7 +95,9 @@ impl Error { _ => StatusCode::BAD_REQUEST, }, #[cfg(feature = "telemetry")] - Prometheus(_) | StatusFailure(_) => StatusCode::INTERNAL_SERVER_ERROR, + Prometheus(_) | StatusFailure(_) | ConfigurationFailure(_) => { + StatusCode::INTERNAL_SERVER_ERROR + } } } diff --git a/cli/src/torii/routing.rs b/cli/src/torii/routing.rs index 8326453d7af..6a9298974ce 100644 --- a/cli/src/torii/routing.rs +++ b/cli/src/torii/routing.rs @@ -7,12 +7,7 @@ use eyre::{eyre, WrapErr}; use futures::TryStreamExt; -use iroha_config::{ - base::proxy::Documented, - iroha::{Configuration, ConfigurationView}, - torii::uri, - GetConfiguration, PostConfiguration, -}; +use iroha_config::{client_api::ConfigurationDTO, torii::uri}; use iroha_core::{ query::{pagination::Paginate, store::LiveQueryStoreHandle}, smartcontracts::query::ValidQueryRequest, @@ -79,7 +74,7 @@ fn fetch_size() -> impl warp::Filter, sumeragi: SumeragiHandle, transaction: SignedTransaction, @@ -169,42 +164,18 @@ async fn handle_pending_transactions( } #[iroha_futures::telemetry_future] -async fn handle_get_configuration( - iroha_cfg: Configuration, - get_cfg: GetConfiguration, -) -> Result { - use GetConfiguration::*; - - match get_cfg { - Docs(field) => ::get_doc_recursive( - field.iter().map(AsRef::as_ref).collect::>(), - ) - .wrap_err("Failed to get docs {:?field}") - .and_then(|doc| serde_json::to_value(doc).wrap_err("Failed to serialize docs")), - // Cast to configuration view to hide private keys. - Value => serde_json::to_value(ConfigurationView::from(iroha_cfg)) - .wrap_err("Failed to serialize value"), - } - .map(|v| reply::json(&v)) - .map_err(Error::Config) +async fn handle_get_configuration(kiso: KisoHandle) -> Result { + let dto = kiso.get_dto().await?; + Ok(reply::json(&dto)) } #[iroha_futures::telemetry_future] async fn handle_post_configuration( - iroha_cfg: Configuration, - cfg: PostConfiguration, -) -> Result { - use iroha_config::base::runtime_upgrades::Reload; - use PostConfiguration::*; - - iroha_logger::debug!(?cfg); - match cfg { - LogLevel(level) => { - iroha_cfg.logger.max_log_level.reload(level)?; - } - }; - - Ok(reply::json(&true)) + kiso: KisoHandle, + value: ConfigurationDTO, +) -> Result { + kiso.update_with_dto(value).await?; + Ok(reply::with_status(reply::reply(), StatusCode::ACCEPTED)) } #[iroha_futures::telemetry_future] @@ -403,8 +374,9 @@ fn handle_status( impl Torii { /// Construct `Torii`. #[allow(clippy::too_many_arguments)] - pub fn from_configuration( - iroha_cfg: Configuration, + pub fn new( + kiso: KisoHandle, + config: &ToriiConfiguration, queue: Arc, events: EventsSender, notify_shutdown: Arc, @@ -413,13 +385,15 @@ impl Torii { kura: Arc, ) -> Self { Self { - iroha_cfg, + kiso, queue, events, notify_shutdown, sumeragi, query_service, kura, + address: config.api_url.clone(), + transaction_max_content_length: config.max_content_len.into(), } } @@ -437,12 +411,11 @@ impl Torii { .and(add_state!(self.queue, self.sumeragi,)) .and(paginate()), ) - .or(endpoint2( - handle_get_configuration, - warp::path(uri::CONFIGURATION) - .and(add_state!(self.iroha_cfg)) - .and(warp::body::json()), - )), + .or(warp::path(uri::CONFIGURATION) + .and(add_state!(self.kiso)) + .and_then(|kiso| async move { + Ok::<_, Infallible>(WarpResult(handle_get_configuration(kiso).await)) + })), ); let get_router_status = warp::path(uri::STATUS) @@ -474,11 +447,11 @@ impl Torii { let post_router = warp::post() .and( endpoint3( - handle_instructions, + handle_transaction, warp::path(uri::TRANSACTION) .and(add_state!(self.queue, self.sumeragi)) .and(warp::body::content_length_limit( - self.iroha_cfg.torii.max_content_len.into(), + self.transaction_max_content_length, )) .and(body::versioned()), ) @@ -491,7 +464,7 @@ impl Torii { .or(endpoint2( handle_post_configuration, warp::path(uri::CONFIGURATION) - .and(add_state!(self.iroha_cfg)) + .and(add_state!(self.kiso)) .and(warp::body::json()), )), ) @@ -549,10 +522,10 @@ impl Torii { /// # Errors /// Can fail due to listening to network or if http server fails fn start_api(self: Arc) -> eyre::Result>> { - let api_url = &self.iroha_cfg.torii.api_url; + let torii_address = &self.address; let mut handles = vec![]; - match api_url.to_socket_addrs() { + match torii_address.to_socket_addrs() { Ok(addrs) => { for addr in addrs { let torii = Arc::clone(&self); @@ -568,7 +541,7 @@ impl Torii { Ok(handles) } Err(error) => { - iroha_logger::error!(%api_url, %error, "API address configuration parse error"); + iroha_logger::error!(%torii_address, %error, "API address configuration parse error"); Err(eyre::Error::new(error)) } } diff --git a/client/benches/torii.rs b/client/benches/torii.rs index 5dc72359570..b8906f52504 100644 --- a/client/benches/torii.rs +++ b/client/benches/torii.rs @@ -8,7 +8,6 @@ use iroha_client::{ client::{asset, Client}, data_model::prelude::*, }; -use iroha_config::base::runtime_upgrades::Reload; use iroha_crypto::KeyPair; use iroha_genesis::{GenesisNetwork, RawGenesisBlockBuilder}; use iroha_primitives::unique_vec; @@ -40,15 +39,16 @@ fn query_requests(criterion: &mut Criterion) { .expect("genesis creation failed"); let builder = PeerBuilder::new() - .with_configuration(configuration.clone()) + .with_configuration(configuration) .with_into_genesis(genesis); rt.block_on(builder.start_with_peer(&mut peer)); - configuration - .logger - .max_log_level - .reload(iroha_client::data_model::Level::ERROR) - .expect("Should not fail"); + rt.block_on(async { + iroha_logger::test_logger() + .reload_level(iroha_client::data_model::Level::ERROR) + .await + .unwrap() + }); let mut group = criterion.benchmark_group("query-requests"); let domain_id: DomainId = "domain".parse().expect("Valid"); let create_domain = RegisterExpr::new(Domain::new(domain_id.clone())); diff --git a/client/benches/tps/oneshot.rs b/client/benches/tps/oneshot.rs index 6fd57cf00ba..99efceac8b2 100644 --- a/client/benches/tps/oneshot.rs +++ b/client/benches/tps/oneshot.rs @@ -20,7 +20,7 @@ fn main() { flush_guard = Some(flame_layer.flush_on_drop()); tracing_subscriber::registry().with(flame_layer).init(); - iroha_logger::disable_logger(); + iroha_logger::disable_global().expect("Logger should not be set yet"); } let config = utils::Config::from_path("benches/tps/config.json").expect("Failed to configure"); diff --git a/client/src/client.rs b/client/src/client.rs index 942e5444a8d..d4f7f383e30 100644 --- a/client/src/client.rs +++ b/client/src/client.rs @@ -13,14 +13,13 @@ use derive_more::{DebugCustom, Display}; use eyre::{eyre, Result, WrapErr}; use futures_util::StreamExt; use http_default::{AsyncWebSocketStream, WebSocketStream}; -use iroha_config::{client::Configuration, torii::uri, GetConfiguration, PostConfiguration}; +use iroha_config::{client::Configuration, client_api::ConfigurationDTO, torii::uri}; use iroha_crypto::{HashOf, KeyPair}; use iroha_logger::prelude::*; use iroha_telemetry::metrics::Status; use iroha_version::prelude::*; use parity_scale_codec::DecodeAll; use rand::Rng; -use serde::de::DeserializeOwned; use url::Url; use self::{blocks_api::AsyncBlockStream, events_api::AsyncEventStream}; @@ -1073,13 +1072,16 @@ impl Client { ) } - fn get_config(&self, get_config: &GetConfiguration) -> Result { + /// Get value of config on peer + /// + /// # Errors + /// Fails if sending request or decoding fails + pub fn get_config(&self) -> Result { let resp = DefaultRequestBuilder::new( HttpMethod::GET, self.torii_url.join(uri::CONFIGURATION).expect("Valid URI"), ) .header(http::header::CONTENT_TYPE, APPLICATION_JSON) - .body(serde_json::to_vec(get_config).wrap_err("Failed to serialize")?) .build()? .send()?; @@ -1097,9 +1099,8 @@ impl Client { /// /// # Errors /// If sending request or decoding fails - pub fn set_config(&self, post_config: PostConfiguration) -> Result { - let body = serde_json::to_vec(&post_config) - .wrap_err(format!("Failed to serialize {post_config:?}"))?; + pub fn set_config(&self, dto: ConfigurationDTO) -> Result<()> { + let body = serde_json::to_vec(&dto).wrap_err(format!("Failed to serialize {dto:?}"))?; let url = self.torii_url.join(uri::CONFIGURATION).expect("Valid URI"); let resp = DefaultRequestBuilder::new(HttpMethod::POST, url) .header(http::header::CONTENT_TYPE, APPLICATION_JSON) @@ -1107,34 +1108,15 @@ impl Client { .build()? .send()?; - if resp.status() != StatusCode::OK { + if resp.status() != StatusCode::ACCEPTED { return Err(eyre!( "Failed to post configuration with HTTP status: {}. {}", resp.status(), std::str::from_utf8(resp.body()).unwrap_or(""), )); - } - serde_json::from_slice(resp.body()) - .wrap_err(format!("Failed to decode body {:?}", resp.body())) - } - - /// Get documentation of some field on config - /// - /// # Errors - /// Fails if sending request or decoding fails - pub fn get_config_docs(&self, field: &[&str]) -> Result> { - let field = field.iter().copied().map(ToOwned::to_owned).collect(); - self.get_config(&GetConfiguration::Docs(field)) - .wrap_err("Failed to get docs for field") - } + }; - /// Get value of config on peer - /// - /// # Errors - /// Fails if sending request or decoding fails - pub fn get_config_value(&self) -> Result { - self.get_config(&GetConfiguration::Value) - .wrap_err("Failed to get configuration value") + Ok(()) } /// Gets network status seen from the peer diff --git a/client/tests/integration/config.rs b/client/tests/integration/config.rs index 7a6470a9087..54c7f4f596b 100644 --- a/client/tests/integration/config.rs +++ b/client/tests/integration/config.rs @@ -1,27 +1,41 @@ +use iroha_data_model::Level; use test_network::*; -use super::{Builder, Configuration, ConfigurationProxy}; - #[test] -fn get_config() { - // The underscored variables must not be dropped until end of closure. - let (_dont_drop, _dont_drop_either, test_client) = - ::new().with_port(10_685).start_with_runtime(); +fn config_endpoints() { + const NEW_LOG_LEVEL: Level = Level::ERROR; + + let (rt, peer, test_client) = ::new().with_port(10_685).start_with_runtime(); wait_for_genesis_committed(&vec![test_client.clone()], 0); - let field = test_client.get_config_docs(&["torii"]).unwrap().unwrap(); - assert!(field.contains("IROHA_TORII")); - - let test = Configuration::test(); - let cfg_proxy: ConfigurationProxy = - serde_json::from_value(test_client.get_config_value().unwrap()).unwrap(); - assert_eq!( - cfg_proxy.block_sync.unwrap().build().unwrap(), - test.block_sync - ); - assert_eq!(cfg_proxy.network.unwrap().build().unwrap(), test.network); - assert_eq!( - cfg_proxy.telemetry.unwrap().build().unwrap(), - *test.telemetry - ); + let init_log_level = rt.block_on(async move { + peer.iroha + .as_ref() + .unwrap() + .kiso + .get_dto() + .await + .unwrap() + .logger + .level + }); + + // Just to be sure this test suite is not useless + assert_ne!(init_log_level, NEW_LOG_LEVEL); + + // Retrieving through API + let mut dto = test_client.get_config().expect("Client can always get it"); + assert_eq!(dto.logger.level, init_log_level); + + // Updating the log level + dto.logger.level = NEW_LOG_LEVEL; + test_client.set_config(dto).expect("New config is valid"); + + // Checking the updated value + dto = test_client.get_config().unwrap(); + assert_eq!(dto.logger.level, NEW_LOG_LEVEL); + + // Restoring value + dto.logger.level = init_log_level; + test_client.set_config(dto).expect("Also valid DTO"); } diff --git a/client/tests/integration/unstable_network.rs b/client/tests/integration/unstable_network.rs index d0df5179a2c..da962f4728d 100644 --- a/client/tests/integration/unstable_network.rs +++ b/client/tests/integration/unstable_network.rs @@ -55,7 +55,7 @@ fn unstable_network( let (network, iroha_client) = rt.block_on(async { let mut configuration = Configuration::test(); configuration.sumeragi.max_transactions_in_block = MAX_TRANSACTIONS_IN_BLOCK; - configuration.logger.max_log_level = Level::INFO.into(); + configuration.logger.level = Level::INFO; #[cfg(debug_assertions)] { configuration.sumeragi.debug_force_soft_fork = force_soft_fork; diff --git a/config/Cargo.toml b/config/Cargo.toml index e4caf3da84d..2f548f60946 100644 --- a/config/Cargo.toml +++ b/config/Cargo.toml @@ -35,6 +35,7 @@ once_cell = { workspace = true } [dev-dependencies] proptest = "1.3.1" stacker = "0.1.15" +expect-test = { workspace = true } [features] tokio-console = [] diff --git a/config/base/src/lib.rs b/config/base/src/lib.rs index bee2b692efc..d8f40d64c21 100644 --- a/config/base/src/lib.rs +++ b/config/base/src/lib.rs @@ -418,8 +418,6 @@ pub mod derive { } } -pub mod runtime_upgrades; - pub mod view { //! Module for view related traits and structs diff --git a/config/base/src/runtime_upgrades.rs b/config/base/src/runtime_upgrades.rs deleted file mode 100644 index 95b69e0e13d..00000000000 --- a/config/base/src/runtime_upgrades.rs +++ /dev/null @@ -1,362 +0,0 @@ -//! Module handling runtime upgrade logic. -pub use serde::{Deserialize, Serialize}; -use thiserror::*; - -type Result = core::result::Result; - -/// Error which occurs when reloading a configuration fails. -#[derive(Clone, Copy, Debug, Error)] -pub enum ReloadError { - /// The resource held by the handle was poisoned by a panic in - /// another thread. - #[error("Resource poisoned.")] - Poisoned, - /// The resource held by the handle was dropped. - #[error("Resource dropped.")] - Dropped, - /// If the reload handle wasn't properly initialized (using - /// [`handle::Singleton::set`]), there's nothing to reload with. - #[error("Cannot reload an uninitialized handle.")] - NotInitialized, - /// Error not specified by the implementer of the [`Reload`] - /// traits. Use as last resort. - #[error("Unspecified reload failure.")] - Other, -} - -/// The field needs to be mutably borrowed to be reloaded. -pub trait ReloadMut { - /// Reload `self` using provided `item`. - /// - /// # Errors - /// Fails with an appropriate variant of - /// [`ReloadError`]. [`ReloadError::Other`] can be used as a - /// **temporary** placeholder. - fn reload(&mut self, item: T) -> Result<()>; -} - -/// The field can be immutably borrowed and reloaded. -pub trait Reload { - /// Reload `self` using provided `item`. - /// - /// # Errors - /// Fails with an appropriate variant of [`ReloadError`]. - /// [`ReloadError::Other`] can be used as a **temporary** placeholder. - fn reload(&self, item: T) -> Result<()>; -} - -/// Contains [`handle`] types: opaque wrappers around a reloadable -/// configuration, used to embed reloading functionality into -/// various [`iroha_config_derive::Documented`] types. -/// -/// # Architecture. -/// -/// ## Desired behaviour -/// -/// Given a value of type (`` in this module), need to -/// -/// - Embed a handle into the configuration options, replacing a Value -/// of type with a handle. -/// -/// - The handle gets (de)serialized as if it were ``: no extra -/// fields, no extra initialisation. -/// -/// - The configuration as a whole is immutable. This is to ensure -/// that you don't accidentally re-assign the handle. -/// -/// - The last object that got instantiated from the configuration -/// file is modified when we call [`Reload::reload`]. -/// -/// - The value used to [`Reload::reload`] the value, must be reflected in the -/// configuration. -/// -/// ## Additional considerations -/// -/// - The handle might have internal mutable state, and be passed -/// along several threads in both a `sync` and `async` context. -/// -/// - The handle's state can be a global mutable static value behind a -/// wrapper. -/// -/// - The handle is almost never read. All interactions with the -/// handle are writes. -/// -/// - The handle can retain a reference to different types, depending -/// on the configuration options. The types might not all be known -/// ahead of time, or be impractically long (both true for -/// `tracting_subscriber::reload::Handle`). -/// -/// # Usage -/// -/// Embed a `SyncValue>`, in your -/// configuration options. When using the configuration to initialise -/// components, call [`handle::SyncValue::set_handle`], on a value that -/// implements [`ReloadMut`] (which you defined earlier). Call -/// [`handle::SyncValue::reload`] to change the configuration at run-time. -/// -/// If the type stored in `H` is a single simple type, it is -/// recommended to use a custom tuple `struct`, and `impl` -/// [`Reload`] for it. -/// -/// If the types are too varied, or generic in arguments that change -/// depending on run-time values, (as in -/// e.g. `tracing_subscriber::reload::Handle`), it is recommended to -/// instead use the provided opaque wrapper [`handle::Singleton`]. -/// -/// **NOTE** you shouldn't normally need to use either -/// [`handle::Singleton`] or [`handle::Value`] directly. -/// -/// # Examples -/// -/// ```ignore -/// use iroha_config_derive::Documented; -/// use serde::{Deserialize, Serialize}; -/// use iroha_config::runtime_upgrades::{handle, Reload, ReloadMut, ReloadError}; -/// use tracing::Level; -/// use tracing_subscriber::{reload::Handle, filter::LevelFilter}; -/// use std::fmt::Debug; -/// -/// struct Logger; -/// -/// #[derive(Clone, Deserialize, Serialize, Debug, Documented)] -/// struct Configuration { -/// pub max_log_level: handle::SyncValue>, -/// pub log_file_path: Option, -/// } -/// -/// fn init(config: &Configuration) -> Logger { -/// let level = config.max_log_level.value(); -/// let level_filter = tracing_subscriber::filter::LevelFilter::from_level(level); -/// let (filter, handle) = reload::Layer::new(level_filter); -/// config.max_log_level.set_handle(iroha_config::logger::ReloadHandle(handle)).unwrap(); -/// } -/// -/// impl ReloadMut for Handle { -/// fn reload(&mut self, level: Level) -> Result<(), ReloadError> { -/// let level_filter = LevelFilter::from_level(level); -/// Handle::reload(self, level_filter).map_err(|_todo| ReloadError::Dropped) -/// } -/// } -/// ``` - -pub mod handle { - use std::{ - fmt::{Debug, Formatter}, - sync::Arc, - }; - - use crossbeam::atomic::AtomicCell; - use parking_lot::Mutex; - use serde::{Deserialize, Serialize}; - - use super::{Reload, ReloadError, ReloadMut, Result}; - // ----------------------------------------------------------------- - - /// An opaque handle for arbitrary [`super::ReloadMut`], useful - /// when it is either impossible or impractical to specify a - /// single `enum` or generic type. You shouldn't embed this into - /// your configuration, and instead use [`SyncValue`]. - #[derive(Clone, Serialize, Deserialize)] - pub struct Singleton { - #[serde(skip)] - inner: Arc + Send + Sync>>>>, - } - - impl Default for Singleton { - fn default() -> Self { - Self { - inner: Arc::new(Mutex::new(None)), - } - } - } - - impl Singleton { - /// Set and/or initialize the [`Self`] to a non-empty value. - /// Reloading before calling this `fn` should cause - /// [`ReloadError::NotInitialized`]. - /// - /// # Errors - /// [`ReloadError::Poisoned`] When the [`Mutex`] storing the reload handle is poisoned. - pub fn set(&self, handle: impl ReloadMut + Send + Sync + 'static) { - *self.inner.lock() = Some(Box::new(handle)); - } - } - - impl Debug for Singleton { - fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { - f.debug_struct("Handle with side effect").finish() - } - } - - impl Reload for Singleton { - fn reload(&self, item: T) -> Result<()> { - match &mut *self.inner.lock() { - Some(handle) => { - handle.reload(item)?; - Ok(()) - } - None => Err(ReloadError::NotInitialized), - } - } - } - - // --------------------------------------------------------------- - - /// A run-time reloadable configuration option with - /// value-semantics. This means that reloading a [`Value`] only - /// affects the [`Value`] itself. It's useful when you want to - /// keep a configuration immutable, but retain thread-safe - /// interior mutability, which is preferable to making the entire - /// configuration `mut`. - /// - /// # Examples - /// - /// ```ignore - /// use serde::{Serialize, Deserialize}; - /// use iroha_config_base::runtime_upgrades::{handle::Value, Reload}; - /// - /// #[derive(iroha_config_base::derive::Combine, Serialize, Deserialize)] - /// pub struct Config { option: Value } - /// - /// fn main() { - /// let c = Config { option: true.into() }; - /// - /// c.option.reload(false); - /// } - /// ``` - /// - /// If you wish to perform validation on the value, consider using - /// a thin wrapper `tuple` struct. - /// - #[derive(Debug)] - pub struct Value(pub AtomicCell); - - impl Clone for Value { - fn clone(&self) -> Self { - Self(AtomicCell::new(self.0.load())) - } - } - - impl From for Value { - fn from(value: T) -> Self { - Self(AtomicCell::new(value)) - } - } - - impl Default for Value { - fn default() -> Self { - Self(AtomicCell::default()) - } - } - - impl Reload for Value { - fn reload(&self, item: T) -> Result<()> { - self.0.swap(item); - Ok(()) - } - } - - impl<'de, T: Deserialize<'de> + Copy + Clone> Deserialize<'de> for Value { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - Ok(Self(AtomicCell::new(T::deserialize(deserializer)?))) - } - } - - impl Serialize for Value { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - (self.0.load()).serialize(serializer) - } - } - - // ----------------------------------------------------------------------- - - /// Structure that encapsulates a configuration value as well as a - /// handle for reloading other parts of the program. This is the - /// `struct` that you want to use 99% of the time. - /// - /// It handles automatic synchronisation of the current value from - /// the reload, as well as proper (de)serialization: namely the - /// handle doesn't pollute your configuration options. - pub struct SyncValue>(Value, H); - - impl> SyncValue { - /// Getter for the wrapped [`Value`] - pub fn value(&self) -> T { - self.0 .0.load() - } - } - - impl SyncValue> { - /// Set the handle - /// - /// # Errors - /// If [`Singleton::set`] fails. - pub fn set_handle(&self, other: impl ReloadMut + Send + Sync + 'static) { - self.1.set(other); - } - } - - impl + Clone> Clone for SyncValue { - fn clone(&self) -> Self { - Self(self.0.clone(), self.1.clone()) - } - } - - impl + Debug> Debug for SyncValue { - fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { - f.debug_tuple("Reconfigure") - .field(&self.0) - .field(&self.1) - .finish() - } - } - - impl Default for SyncValue> - where - T: Default + Clone + Copy + Send + Sync + Debug, - { - fn default() -> Self { - Self(Value::default(), Singleton::default()) - } - } - - impl + Default> From for SyncValue { - fn from(value: T) -> Self { - Self(Value(AtomicCell::new(value)), H::default()) - } - } - - impl> Serialize for SyncValue { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - // We only want the actual (simple) value to be part of the serializing - self.0.serialize(serializer) - } - } - - impl<'de, T: Deserialize<'de> + Copy + Clone, H: Reload + Default> Deserialize<'de> - for SyncValue - { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - Ok(Self(Value::::deserialize(deserializer)?, H::default())) - } - } - - impl> Reload for SyncValue { - fn reload(&self, item: T) -> Result<()> { - self.1.reload(item)?; - self.0.reload(item) - } - } -} diff --git a/config/iroha_test_config.json b/config/iroha_test_config.json index 80f61607c38..6ebbf417a26 100644 --- a/config/iroha_test_config.json +++ b/config/iroha_test_config.json @@ -57,11 +57,8 @@ "FUTURE_THRESHOLD_MS": 1000 }, "LOGGER": { - "MAX_LOG_LEVEL": "INFO", - "TELEMETRY_CAPACITY": 1000, - "COMPACT_MODE": false, - "LOG_FILE_PATH": null, - "TERMINAL_COLORS": true, + "LEVEL": "INFO", + "FORMAT": "full", "TOKIO_CONSOLE_ADDR": "127.0.0.1:5555" }, "GENESIS": { diff --git a/config/src/client_api.rs b/config/src/client_api.rs new file mode 100644 index 00000000000..030edb8523a --- /dev/null +++ b/config/src/client_api.rs @@ -0,0 +1,70 @@ +//! Functionality related to working with the configuration through client API. +//! +//! 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 +// 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. +// Track configuration refactoring here: https://github.com/hyperledger/iroha/issues/2585 + +use iroha_data_model::Level; +use serde::{Deserialize, Serialize}; + +use super::{iroha::Configuration as BaseConfiguration, logger::Configuration as BaseLogger}; + +/// Subset of [`super::iroha`] configuration. +#[derive(Debug, Serialize, Deserialize, Clone, Copy)] +pub struct ConfigurationDTO { + #[allow(missing_docs)] + pub logger: Logger, +} + +impl From<&'_ BaseConfiguration> for ConfigurationDTO { + fn from(value: &'_ BaseConfiguration) -> Self { + Self { + logger: value.logger.as_ref().into(), + } + } +} + +/// Subset of [`super::logger`] configuration. +#[derive(Debug, Serialize, Deserialize, Clone, Copy)] +pub struct Logger { + #[allow(missing_docs)] + pub level: Level, +} + +impl From<&'_ BaseLogger> for Logger { + fn from(value: &'_ BaseLogger) -> Self { + Self { level: value.level } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn snapshot_serialized_form() { + let value = ConfigurationDTO { + logger: Logger { + level: Level::TRACE, + }, + }; + + let actual = serde_json::to_string_pretty(&value).expect("The value is a valid JSON"); + + // NOTE: whenever this is updated, make sure to update the documentation accordingly: + // https://hyperledger.github.io/iroha-2-docs/reference/torii-endpoints.html + // -> Configuration endpoints + let expected = expect_test::expect![[r#" + { + "logger": { + "level": "TRACE" + } + }"#]]; + expected.assert_eq(&actual); + } +} diff --git a/config/src/iroha.rs b/config/src/iroha.rs index 6ed054b6593..0ade3128196 100644 --- a/config/src/iroha.rs +++ b/config/src/iroha.rs @@ -262,9 +262,9 @@ mod tests { #[test] fn example_json_proxy_builds() { - ConfigurationProxy::from_path(CONFIGURATION_PATH).build().unwrap_or_else(|_| panic!("`ConfigurationProxy` specified in {CONFIGURATION_PATH} \ + 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.")); + properly with new changes. Error: {err}")); } #[test] diff --git a/config/src/kura.rs b/config/src/kura.rs index 9eaed6f19d3..03c0cb4fb74 100644 --- a/config/src/kura.rs +++ b/config/src/kura.rs @@ -1,13 +1,10 @@ //! Module for kura-related configuration and structs -use std::{num::NonZeroU64, path::Path}; -use eyre::{eyre, Result}; +use eyre::Result; use iroha_config_base::derive::{Documented, Proxy}; use serde::{Deserialize, Serialize}; -const DEFAULT_BLOCKS_PER_STORAGE_FILE: u64 = 1000_u64; const DEFAULT_BLOCK_STORE_PATH: &str = "./storage"; -const DEFAULT_ACTOR_CHANNEL_CAPACITY: u32 = 100; /// `Kura` configuration. #[derive(Clone, Deserialize, Serialize, Debug, Documented, Proxy, PartialEq, Eq)] @@ -18,10 +15,6 @@ pub struct Configuration { pub init_mode: Mode, /// Path to the existing block store folder or path to create new folder. pub block_store_path: String, - /// Maximum number of blocks to write into a single storage file. - pub blocks_per_storage_file: NonZeroU64, - /// Default buffer capacity of actor's MPSC channel. - pub actor_channel_capacity: u32, /// Whether or not new blocks be outputted to a file called blocks.json. pub debug_output_new_blocks: bool, } @@ -30,31 +23,12 @@ impl Default for ConfigurationProxy { fn default() -> Self { Self { init_mode: Some(Mode::default()), - block_store_path: Some(DEFAULT_BLOCK_STORE_PATH.to_owned()), - blocks_per_storage_file: Some( - NonZeroU64::new(DEFAULT_BLOCKS_PER_STORAGE_FILE) - .expect("BLOCKS_PER_STORAGE cannot be set to a non-positive value."), - ), - actor_channel_capacity: Some(DEFAULT_ACTOR_CHANNEL_CAPACITY), + block_store_path: Some(DEFAULT_BLOCK_STORE_PATH.into()), debug_output_new_blocks: Some(false), } } } -impl Configuration { - /// Set `block_store_path` configuration parameter. Will overwrite the existing one. - /// - /// # Errors - /// Fails if the path is not valid - pub fn block_store_path(&mut self, path: &Path) -> Result<()> { - self.block_store_path = path - .to_str() - .ok_or_else(|| eyre!("Failed to yield slice from path"))? - .to_owned(); - Ok(()) - } -} - /// Kura initialization mode. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)] #[serde(rename_all = "snake_case")] @@ -77,12 +51,10 @@ pub mod tests { ( init_mode in prop::option::of(Just(Mode::default())), block_store_path in prop::option::of(Just(DEFAULT_BLOCK_STORE_PATH.into())), - blocks_per_storage_file in prop::option::of(Just(NonZeroU64::new(DEFAULT_BLOCKS_PER_STORAGE_FILE).expect("Cannot be set to a negative value"))), - actor_channel_capacity in prop::option::of(Just(DEFAULT_ACTOR_CHANNEL_CAPACITY)), debug_output_new_blocks in prop::option::of(Just(false)) ) -> ConfigurationProxy { - ConfigurationProxy { init_mode, block_store_path, blocks_per_storage_file, actor_channel_capacity, debug_output_new_blocks } + ConfigurationProxy { init_mode, block_store_path, debug_output_new_blocks } } } } diff --git a/config/src/lib.rs b/config/src/lib.rs index 6e80c5e1c88..423e5a8dd19 100644 --- a/config/src/lib.rs +++ b/config/src/lib.rs @@ -1,9 +1,9 @@ //! Aggregate configuration for different Iroha modules. pub use iroha_config_base as base; -use serde::{Deserialize, Serialize}; pub mod block_sync; pub mod client; +pub mod client_api; pub mod genesis; pub mod iroha; pub mod kura; @@ -18,35 +18,3 @@ pub mod telemetry; pub mod torii; pub mod wasm; pub mod wsv; - -/// Json config for getting configuration -#[derive(Clone, Debug, Deserialize, Serialize)] -pub enum GetConfiguration { - /// Getting docs of specific field - /// - /// Top-level fields must be enclosed in an array (of strings). This array - /// provides the fully qualified path to the fields. - /// - /// # Examples - /// - /// To get the top-level configuration docs for `iroha_core::Torii` - /// `curl -X GET -H 'content-type: application/json' http://127.0.0.1:8080/configuration -d '{"Docs" : ["torii"]} ' -i` - /// - /// To get the documentation on the [`Logger::config::Configuration.max_log_level`] - /// `curl -X GET -H 'content-type: application/json' http://127.0.0.1:8080/configuration -d '{"Docs" : ["logger", "max_log_level"]}' -i` - Docs(Vec), - /// Get the original Value of the full configuration. - Value, -} - -/// Message acceptable for `POST` requests to the configuration endpoint. -#[derive(Clone, Debug, Deserialize, Serialize, Copy)] -pub enum PostConfiguration { - /// Change the maximum logging level of logger. - /// - /// # Examples - /// - /// To silence all logging events that aren't `ERROR`s - /// `curl -X POST -H 'content-type: application/json' http://127.0.0.1:8080/configuration -d '{"LogLevel": "ERROR"}' -i` - LogLevel(iroha_data_model::Level), -} diff --git a/config/src/logger.rs b/config/src/logger.rs index bee27fda3df..d3c8e79472a 100644 --- a/config/src/logger.rs +++ b/config/src/logger.rs @@ -2,21 +2,14 @@ //! configuration, as well as run-time reloading of the log-level. use core::fmt::Debug; -use derive_more::{Deref, DerefMut, From}; -use iroha_config_base::{ - derive::{Documented, Proxy}, - runtime_upgrades::{handle, ReloadError, ReloadMut}, -}; -use iroha_data_model::Level; +use iroha_config_base::derive::{Documented, Proxy}; +pub use iroha_data_model::Level; +#[cfg(feature = "tokio-console")] +use iroha_primitives::addr::{socket_addr, SocketAddr}; use serde::{Deserialize, Serialize}; -use tracing::Subscriber; -use tracing_subscriber::{filter::LevelFilter, reload::Handle}; -const TELEMETRY_CAPACITY: u32 = 1000; -const DEFAULT_COMPACT_MODE: bool = false; -const DEFAULT_TERMINAL_COLORS: bool = true; #[cfg(feature = "tokio-console")] -const DEFAULT_TOKIO_CONSOLE_ADDR: &str = "127.0.0.1:5555"; +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 { @@ -29,77 +22,50 @@ pub fn into_tracing_level(level: Level) -> tracing::Level { } } -/// Wrapper for [`Handle`] to implement [`ReloadMut`] -#[derive(From)] -pub struct ReloadHandle(pub Handle); - -impl ReloadMut for ReloadHandle { - fn reload(&mut self, level: Level) -> Result<(), ReloadError> { - let level_filter = - tracing_subscriber::filter::LevelFilter::from_level(into_tracing_level(level)); - - Handle::reload(&self.0, level_filter).map_err(|err| { - if err.is_dropped() { - ReloadError::Dropped - } else { - ReloadError::Poisoned - } - }) - } -} - -/// Wrapper around [`Level`] for runtime upgrades. -#[derive(Debug, Clone, Default, Deref, DerefMut, Deserialize, Serialize)] -#[repr(transparent)] -#[serde(transparent)] -pub struct SyncLevel(handle::SyncValue>); - -impl From for SyncLevel { - fn from(level: Level) -> Self { - Self(level.into()) - } -} - -impl PartialEq for SyncLevel { - fn eq(&self, other: &Self) -> bool { - self.0.value() == other.0.value() - } -} - -impl Eq for SyncLevel {} - /// 'Logger' configuration. #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Proxy, Documented)] #[serde(rename_all = "UPPERCASE")] +#[config(env_prefix = "LOG_")] +// `tokio_console_addr` is not `Copy`, but warning appears without `tokio-console` feature +#[allow(missing_copy_implementations)] pub struct Configuration { - /// Maximum log level + /// Level of logging verbosity #[config(serde_as_str)] - pub max_log_level: SyncLevel, - /// Capacity (or batch size) for telemetry channel - pub telemetry_capacity: u32, - /// Compact mode (no spans from telemetry) - pub compact_mode: bool, - /// If provided, logs will be copied to said file in the - /// format readable by [bunyan](https://lib.rs/crates/bunyan) - #[config(serde_as_str)] - pub log_file_path: Option, - /// Enable ANSI terminal colors for formatted output. - pub terminal_colors: bool, + 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: String, + pub tokio_console_addr: SocketAddr, +} + +/// Reflects formatters in [`tracing_subscriber::fmt::format`] +#[derive(Debug, Copy, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "lowercase")] +pub enum Format { + /// See [`tracing_subscriber::fmt::format::Full`] + Full, + /// See [`tracing_subscriber::fmt::format::Compact`] + Compact, + /// See [`tracing_subscriber::fmt::format::Pretty`] + Pretty, + /// See [`tracing_subscriber::fmt::format::Json`] + Json, +} + +impl Default for Format { + fn default() -> Self { + Self::Full + } } impl Default for ConfigurationProxy { fn default() -> Self { Self { - max_log_level: Some(SyncLevel::default()), - telemetry_capacity: Some(TELEMETRY_CAPACITY), - compact_mode: Some(DEFAULT_COMPACT_MODE), - log_file_path: Some(None), - terminal_colors: Some(DEFAULT_TERMINAL_COLORS), + level: Some(Level::default()), + format: Some(Format::default()), #[cfg(feature = "tokio-console")] - tokio_console_addr: Some(DEFAULT_TOKIO_CONSOLE_ADDR.into()), + tokio_console_addr: Some(DEFAULT_TOKIO_CONSOLE_ADDR), } } } @@ -113,22 +79,23 @@ pub mod tests { #[must_use = "strategies do nothing unless used"] pub fn arb_proxy() -> impl proptest::strategy::Strategy { let strat = ( - (prop::option::of(Just(SyncLevel::default()))), - (prop::option::of(Just(TELEMETRY_CAPACITY))), - (prop::option::of(Just(DEFAULT_COMPACT_MODE))), - (prop::option::of(Just(None))), - (prop::option::of(Just(DEFAULT_TERMINAL_COLORS))), + (prop::option::of(Just(Level::default()))), + (prop::option::of(Just(Format::default()))), #[cfg(feature = "tokio-console")] - (prop::option::of(Just(DEFAULT_TOKIO_CONSOLE_ADDR.to_string()))), + (prop::option::of(Just(DEFAULT_TOKIO_CONSOLE_ADDR))), ); proptest::strategy::Strategy::prop_map(strat, move |strat| ConfigurationProxy { - max_log_level: strat.0, - telemetry_capacity: strat.1, - compact_mode: strat.2, - log_file_path: strat.3, - terminal_colors: strat.4, + level: strat.0, + format: strat.1, #[cfg(feature = "tokio-console")] - tokio_console_addr: strat.5, + tokio_console_addr: strat.2, }) } + + #[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/telemetry.rs b/config/src/telemetry.rs index d347df8b050..3b04f79c483 100644 --- a/config/src/telemetry.rs +++ b/config/src/telemetry.rs @@ -1,4 +1,6 @@ //! Module for telemetry-related configuration and structs. +use std::path::PathBuf; + use iroha_config_base::derive::{Documented, Proxy}; use serde::{Deserialize, Serialize}; use url::Url; @@ -20,7 +22,57 @@ pub struct Configuration { pub max_retry_delay_exponent: u8, /// The filepath that to write dev-telemetry to #[config(serde_as_str)] - pub file: Option, + 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 { diff --git a/configs/peer/config.json b/configs/peer/config.json index 51cc9c5a45a..11d5b354ce8 100644 --- a/configs/peer/config.json +++ b/configs/peer/config.json @@ -5,8 +5,6 @@ "KURA": { "INIT_MODE": "strict", "BLOCK_STORE_PATH": "./storage", - "BLOCKS_PER_STORAGE_FILE": 1000, - "ACTOR_CHANNEL_CAPACITY": 100, "DEBUG_OUTPUT_NEW_BLOCKS": false }, "SUMERAGI": { @@ -38,11 +36,8 @@ "FUTURE_THRESHOLD_MS": 1000 }, "LOGGER": { - "MAX_LOG_LEVEL": "INFO", - "TELEMETRY_CAPACITY": 1000, - "COMPACT_MODE": false, - "LOG_FILE_PATH": null, - "TERMINAL_COLORS": true + "LEVEL": "INFO", + "FORMAT": "full" }, "GENESIS": { "ACCOUNT_PUBLIC_KEY": null, diff --git a/core/benches/blocks/apply_blocks_oneshot.rs b/core/benches/blocks/apply_blocks_oneshot.rs index 4c8bdd6e389..f16a5bf5e57 100644 --- a/core/benches/blocks/apply_blocks_oneshot.rs +++ b/core/benches/blocks/apply_blocks_oneshot.rs @@ -8,23 +8,10 @@ mod apply_blocks; use apply_blocks::WsvApplyBlocks; -use iroha_config::base::proxy::Builder; -use iroha_data_model::Level; -use iroha_logger::{Configuration, ConfigurationProxy}; #[tokio::main] async fn main() { - let log_config = Configuration { - max_log_level: Level::INFO.into(), - compact_mode: false, - ..ConfigurationProxy::default() - .build() - .expect("Default logger config should always build") - }; - // Can't use logger because it's failed to initialize. - if let Err(err) = iroha_logger::init(&log_config) { - eprintln!("Failed to initialize logger: {err}"); - } + iroha_logger::test_logger(); iroha_logger::info!("Starting..."); let bench = WsvApplyBlocks::setup().expect("Failed to setup benchmark"); WsvApplyBlocks::measure(&bench).expect("Failed to execute benchmark"); diff --git a/core/benches/blocks/validate_blocks_oneshot.rs b/core/benches/blocks/validate_blocks_oneshot.rs index bcdeb20a519..403adbd0a22 100644 --- a/core/benches/blocks/validate_blocks_oneshot.rs +++ b/core/benches/blocks/validate_blocks_oneshot.rs @@ -7,23 +7,10 @@ mod validate_blocks; -use iroha_config::base::proxy::Builder; -use iroha_data_model::Level; -use iroha_logger::{Configuration, ConfigurationProxy}; use validate_blocks::WsvValidateBlocks; fn main() { - let log_config = Configuration { - max_log_level: Level::INFO.into(), - compact_mode: false, - ..ConfigurationProxy::default() - .build() - .expect("Default logger config should always build") - }; - // Can't use logger because it's failed to initialize. - if let Err(err) = iroha_logger::init(&log_config) { - eprintln!("Failed to initialize logger: {err}"); - } + iroha_logger::test_logger(); iroha_logger::info!("Starting..."); let bench = WsvValidateBlocks::setup().expect("Failed to setup benchmark"); WsvValidateBlocks::measure(bench).expect("Failed to execute bnechmark"); diff --git a/core/benches/kura.rs b/core/benches/kura.rs index c0371201191..279f8d97528 100644 --- a/core/benches/kura.rs +++ b/core/benches/kura.rs @@ -4,6 +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_core::{ block::*, kura::{BlockStore, LockStatus}, @@ -39,8 +40,12 @@ async fn measure_block_size_for_n_executors(n_executors: u32) { let tx = AcceptedTransaction::accept(tx, &transaction_limits) .expect("Failed to accept Transaction."); let dir = tempfile::tempdir().expect("Could not create tempfile."); - let kura = - iroha_core::kura::Kura::new(iroha_config::kura::Mode::Strict, dir.path(), false).unwrap(); + let cfg = Configuration { + init_mode: iroha_config::kura::Mode::Strict, + debug_output_new_blocks: false, + block_store_path: dir.path().to_str().unwrap().into(), + }; + let kura = iroha_core::kura::Kura::new(&cfg).unwrap(); let _thread_handle = iroha_core::kura::Kura::start(kura.clone()); let query_handle = LiveQueryStore::test().start(); diff --git a/core/src/kiso.rs b/core/src/kiso.rs new file mode 100644 index 00000000000..464b01acb0e --- /dev/null +++ b/core/src/kiso.rs @@ -0,0 +1,206 @@ +//! Actor responsible for configuration state and its dynamic updates. +//! +//! Currently the API exposed by [`KisoHandle`] works only with [`ConfigurationDTO`], because +//! no any part of Iroha is interested in the whole state. However, the API could be extended +//! in future. +//! +//! Updates mechanism is implemented via subscriptions to [`tokio::sync::watch`] channels. For now, +//! only `logger.level` field is dynamic, which might be tracked with [`KisoHandle::subscribe_on_log_level()`]. + +use eyre::Result; +use iroha_config::{ + client_api::{ConfigurationDTO, Logger as LoggerDTO}, + iroha::Configuration, +}; +use iroha_logger::Level; +use tokio::sync::{mpsc, oneshot, watch}; + +const DEFAULT_CHANNEL_SIZE: usize = 32; + +/// Handle to work with the actor. +/// +/// The actor will shutdown when all its handles are dropped. +#[derive(Clone)] +pub struct KisoHandle { + actor: mpsc::Sender, +} + +impl KisoHandle { + /// Spawn a new actor + pub fn new(state: Configuration) -> Self { + let (actor_sender, actor_receiver) = mpsc::channel(DEFAULT_CHANNEL_SIZE); + let (log_level_update, _) = watch::channel(state.logger.level); + let mut actor = Actor { + handle: actor_receiver, + state, + log_level_update, + }; + tokio::spawn(async move { actor.run().await }); + + Self { + actor: actor_sender, + } + } + + /// Fetch the [`ConfigurationDTO`] from the actor's state. + /// + /// # Errors + /// If communication with actor fails. + 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; + let dto = rx.await?; + Ok(dto) + } + + /// Update the configuration state and notify subscribers. + /// + /// Works in a fire-and-forget way, i.e. completion of this task doesn't mean that updates are applied. However, + /// subsequent call of [`Self::get_dto()`] will return an updated state. + /// + /// # Errors + /// If communication with actor fails. + pub async fn update_with_dto(&self, dto: ConfigurationDTO) -> Result<(), Error> { + let (tx, rx) = oneshot::channel(); + let msg = Message::UpdateWithDTO { + dto, + respond_to: tx, + }; + let _ = self.actor.send(msg).await; + rx.await? + } + + /// Subscribe on updates of `logger.level` parameter. + /// + /// # Errors + /// If communication with actor fails. + pub async fn subscribe_on_log_level(&self) -> Result, Error> { + let (tx, rx) = oneshot::channel(); + let msg = Message::SubscribeOnLogLevel { respond_to: tx }; + let _ = self.actor.send(msg).await; + let receiver = rx.await?; + Ok(receiver) + } +} + +enum Message { + GetDTO { + respond_to: oneshot::Sender, + }, + UpdateWithDTO { + dto: ConfigurationDTO, + respond_to: oneshot::Sender>, + }, + SubscribeOnLogLevel { + respond_to: oneshot::Sender>, + }, +} + +/// Possible errors might occur while working with [`KisoHandle`] +#[derive(thiserror::Error, displaydoc::Display, Debug)] +pub enum Error { + /// Failed to get actor's response + Communication(#[from] oneshot::error::RecvError), +} + +struct Actor { + handle: mpsc::Receiver, + state: Configuration, + // 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 + // required. However, as of now a single manually written implementation seems optimal. + log_level_update: watch::Sender, +} + +impl Actor { + async fn run(&mut self) { + while let Some(msg) = self.handle.recv().await { + self.handle_message(msg).await + } + } + + async fn handle_message(&mut self, msg: Message) { + match msg { + Message::GetDTO { respond_to } => { + let dto = ConfigurationDTO::from(&self.state); + let _ = respond_to.send(dto); + } + Message::UpdateWithDTO { + dto: + ConfigurationDTO { + logger: LoggerDTO { level: new_level }, + }, + respond_to, + } => { + let _ = self.log_level_update.send(new_level); + self.state.logger.level = new_level; + + let _ = respond_to.send(Ok(())); + } + Message::SubscribeOnLogLevel { respond_to } => { + let _ = respond_to.send(self.log_level_update.subscribe()); + } + } + } +} + +#[cfg(test)] +#[allow(unused)] +mod tests { + use std::time::Duration; + + use iroha_config::{ + base::proxy::LoadFromDisk, + client_api::{ConfigurationDTO, Logger as LoggerDTO}, + iroha::{Configuration, ConfigurationProxy}, + }; + + 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() + } + + #[tokio::test] + async fn subscription_on_log_level_works() { + const INIT_LOG_LEVEL: Level = Level::WARN; + const NEW_LOG_LEVEL: Level = Level::DEBUG; + const WATCH_LAG_MILLIS: u64 = 30; + + let mut config = test_config(); + config.logger.level = INIT_LOG_LEVEL; + let kiso = KisoHandle::new(config); + + let mut recv = kiso + .subscribe_on_log_level() + .await + .expect("Subscription should be fine"); + + let _err = tokio::time::timeout(Duration::from_millis(WATCH_LAG_MILLIS), recv.changed()) + .await + .expect_err("Watcher should not be active initially"); + + kiso.update_with_dto(ConfigurationDTO { + logger: LoggerDTO { + level: NEW_LOG_LEVEL, + }, + }) + .await + .expect("Update should work fine"); + + let () = tokio::time::timeout(Duration::from_millis(WATCH_LAG_MILLIS), recv.changed()) + .await + .expect("Watcher should resolve within timeout") + .expect("Watcher should not be closed"); + + let value = *recv.borrow_and_update(); + assert_eq!(value, NEW_LOG_LEVEL); + } +} diff --git a/core/src/kura.rs b/core/src/kura.rs index cede4d491b3..11dbf2c5192 100644 --- a/core/src/kura.rs +++ b/core/src/kura.rs @@ -10,7 +10,7 @@ use std::{ sync::Arc, }; -use iroha_config::kura::Mode; +use iroha_config::kura::{Configuration, Mode}; use iroha_crypto::{Hash, HashOf}; use iroha_data_model::block::SignedBlock; use iroha_logger::prelude::*; @@ -50,22 +50,19 @@ impl Kura { /// Fails if there are filesystem errors when trying /// to access the block store indicated by the provided /// path. - pub fn new( - mode: Mode, - block_store_path: &Path, - debug_output_new_blocks: bool, - ) -> Result> { + pub fn new(config: &Configuration) -> 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()?; - let block_plain_text_path = debug_output_new_blocks.then(|| { + 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 kura = Arc::new(Self { - mode, + mode: config.init_mode, block_store: Mutex::new(block_store), block_data: Mutex::new(Vec::new()), block_plain_text_path, @@ -1054,9 +1051,13 @@ mod tests { #[tokio::test] async fn strict_init_kura() { let temp_dir = TempDir::new().unwrap(); - Kura::new(Mode::Strict, temp_dir.path(), false) - .unwrap() - .init() - .unwrap(); + Kura::new(&Configuration { + init_mode: Mode::Strict, + block_store_path: temp_dir.path().to_str().unwrap().into(), + debug_output_new_blocks: false, + }) + .unwrap() + .init() + .unwrap(); } } diff --git a/core/src/lib.rs b/core/src/lib.rs index e0c6109e31f..c032e5fda37 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -4,6 +4,7 @@ pub mod block; pub mod block_sync; pub mod executor; pub mod gossiper; +pub mod kiso; pub mod kura; pub mod modules; pub mod query; diff --git a/core/test_network/src/lib.rs b/core/test_network/src/lib.rs index a5fbb690c6f..96c4210fd08 100644 --- a/core/test_network/src/lib.rs +++ b/core/test_network/src/lib.rs @@ -170,7 +170,7 @@ impl Network { start_port: Option, ) -> (Self, Client) { let mut configuration = Configuration::test(); - configuration.logger.max_log_level = Level::INFO.into(); + configuration.logger.level = Level::INFO; let network = Network::new_with_offline_peers( Some(configuration), n_peers, @@ -414,22 +414,18 @@ impl Peer { temp_dir: Arc, ) { let mut configuration = self.get_config(configuration); - configuration - .kura - .block_store_path(temp_dir.path()) - .expect("block store path not readable"); + configuration.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, api_addr = %self.api_address, ); - let telemetry = - iroha_logger::init(&configuration.logger).expect("Failed to initialize telemetry"); + let logger = iroha_logger::test_logger(); let (sender, receiver) = std::sync::mpsc::sync_channel(1); let handle = task::spawn( async move { - let mut iroha = Iroha::with_genesis(genesis, configuration, telemetry) + let mut iroha = Iroha::with_genesis(genesis, configuration, logger) .await .expect("Failed to start iroha"); let job_handle = iroha.start_as_task().unwrap(); diff --git a/docs/source/references/config.md b/docs/source/references/config.md index 4a8288df05c..82b00f0bc54 100644 --- a/docs/source/references/config.md +++ b/docs/source/references/config.md @@ -36,8 +36,6 @@ The following is the default configuration used by Iroha. "KURA": { "INIT_MODE": "strict", "BLOCK_STORE_PATH": "./storage", - "BLOCKS_PER_STORAGE_FILE": 1000, - "ACTOR_CHANNEL_CAPACITY": 100, "DEBUG_OUTPUT_NEW_BLOCKS": false }, "SUMERAGI": { @@ -69,11 +67,8 @@ The following is the default configuration used by Iroha. "FUTURE_THRESHOLD_MS": 1000 }, "LOGGER": { - "MAX_LOG_LEVEL": "INFO", - "TELEMETRY_CAPACITY": 1000, - "COMPACT_MODE": false, - "LOG_FILE_PATH": null, - "TERMINAL_COLORS": true + "LEVEL": "INFO", + "FORMAT": "full" }, "GENESIS": { "ACCOUNT_PUBLIC_KEY": null, @@ -225,24 +220,12 @@ Has type `Option>`[^1]. Can be configured via envi ```json { - "ACTOR_CHANNEL_CAPACITY": 100, - "BLOCKS_PER_STORAGE_FILE": 1000, "BLOCK_STORE_PATH": "./storage", "DEBUG_OUTPUT_NEW_BLOCKS": false, "INIT_MODE": "strict" } ``` -### `kura.actor_channel_capacity` - -Default buffer capacity of actor's MPSC channel. - -Has type `Option`[^1]. Can be configured via environment variable `KURA_ACTOR_CHANNEL_CAPACITY` - -```json -100 -``` - ### `kura.block_store_path` Path to the existing block store folder or path to create new folder. @@ -253,16 +236,6 @@ Has type `Option`[^1]. Can be configured via environment variable `KURA_ "./storage" ``` -### `kura.blocks_per_storage_file` - -Maximum number of blocks to write into a single storage file. - -Has type `Option`[^1]. Can be configured via environment variable `KURA_BLOCKS_PER_STORAGE_FILE` - -```json -1000 -``` - ### `kura.debug_output_new_blocks` Whether or not new blocks be outputted to a file called blocks.json. @@ -313,64 +286,31 @@ Has type `Option>`[^1]. Can be configured via en ```json { - "COMPACT_MODE": false, - "LOG_FILE_PATH": null, - "MAX_LOG_LEVEL": "INFO", - "TELEMETRY_CAPACITY": 1000, - "TERMINAL_COLORS": true + "FORMAT": "full", + "LEVEL": "INFO" } ``` -### `logger.compact_mode` - -Compact mode (no spans from telemetry) - -Has type `Option`[^1]. Can be configured via environment variable `COMPACT_MODE` - -```json -false -``` - -### `logger.log_file_path` +### `logger.format` -If provided, logs will be copied to said file in the +Output format -Has type `Option>`[^1]. Can be configured via environment variable `LOG_FILE_PATH` +Has type `Option`[^1]. Can be configured via environment variable `LOG_FORMAT` ```json -null +"full" ``` -### `logger.max_log_level` +### `logger.level` -Maximum log level +Level of logging verbosity -Has type `Option`[^1]. Can be configured via environment variable `MAX_LOG_LEVEL` +Has type `Option`[^1]. Can be configured via environment variable `LOG_LEVEL` ```json "INFO" ``` -### `logger.telemetry_capacity` - -Capacity (or batch size) for telemetry channel - -Has type `Option`[^1]. Can be configured via environment variable `TELEMETRY_CAPACITY` - -```json -1000 -``` - -### `logger.terminal_colors` - -Enable ANSI terminal colors for formatted output. - -Has type `Option`[^1]. Can be configured via environment variable `TERMINAL_COLORS` - -```json -true -``` - ## `network` Network configuration @@ -642,7 +582,7 @@ Has type `Option>`[^1]. Can be configured via The filepath that to write dev-telemetry to -Has type `Option>`[^1]. Can be configured via environment variable `TELEMETRY_FILE` +Has type `Option>`[^1]. Can be configured via environment variable `TELEMETRY_FILE` ```json null diff --git a/futures/src/lib.rs b/futures/src/lib.rs index f45fa002b71..3865cc4e6fe 100644 --- a/futures/src/lib.rs +++ b/futures/src/lib.rs @@ -7,7 +7,7 @@ use std::{ }; pub use iroha_futures_derive::*; -use iroha_logger::telemetry::{Telemetry, TelemetryFields}; +use iroha_logger::telemetry::{Event as Telemetry, Fields as TelemetryFields}; use serde::{Deserialize, Serialize}; use serde_json::Value; diff --git a/futures/tests/basic.rs b/futures/tests/basic.rs index a1514e01ab1..190d6201000 100644 --- a/futures/tests/basic.rs +++ b/futures/tests/basic.rs @@ -1,10 +1,9 @@ use std::{thread, time::Duration}; -use iroha_config::base::proxy::Builder; use iroha_futures::FuturePollTelemetry; -use iroha_logger::ConfigurationProxy; +use iroha_logger::telemetry::Channel; use tokio::task; -use tokio_stream::{wrappers::ReceiverStream, StreamExt}; +use tokio_stream::{wrappers::BroadcastStream, StreamExt}; #[iroha_futures::telemetry_future] async fn sleep(times: Vec) -> i32 { @@ -32,15 +31,13 @@ async fn test_sleep() { Duration::from_nanos(80_000_000), ]; - let (_, telemetry_future) = iroha_logger::init( - &ConfigurationProxy::default() - .build() - .expect("Default logger config always builds"), - ) - .unwrap() - .unwrap(); + let future_telemetry = iroha_logger::test_logger() + .subscribe_on_telemetry(Channel::Future) + .await + .unwrap(); assert_eq!(sleep(sleep_times.clone()).await, 10_i32); - let telemetry = ReceiverStream::new(telemetry_future) + let telemetry = BroadcastStream::new(future_telemetry) + .filter_map(Result::ok) .map(FuturePollTelemetry::try_from) .filter_map(Result::ok) .take(3) diff --git a/logger/Cargo.toml b/logger/Cargo.toml index 9d3ce2ac20c..83aba591aea 100644 --- a/logger/Cargo.toml +++ b/logger/Cargo.toml @@ -19,13 +19,13 @@ serde_json = { workspace = true } tracing = { workspace = true } tracing-core = "0.1.31" tracing-futures = { version = "0.2.5", default-features = false, features = ["std-future", "std"] } -tracing-subscriber = { workspace = true, features = ["fmt", "ansi"] } -tracing-bunyan-formatter = { version = "0.3.9", default-features = false } -tokio = { workspace = true, features = ["sync"] } +tracing-subscriber = { workspace = true, features = ["fmt", "ansi", "json"] } +tokio = { workspace = true, features = ["sync", "rt", "macros"] } console-subscriber = { version = "0.2.0", optional = true } once_cell = { workspace = true } derive_more = { workspace = true } tracing-error = "0.2.0" +thiserror = { workspace = true } [dev-dependencies] tokio = { workspace = true, features = ["macros", "time", "rt"] } diff --git a/logger/src/actor.rs b/logger/src/actor.rs new file mode 100644 index 00000000000..e9e2d91280e --- /dev/null +++ b/logger/src/actor.rs @@ -0,0 +1,145 @@ +//! Actor encapsulating interaction with logger & telemetry subsystems. + +use iroha_config::logger::into_tracing_level; +use iroha_data_model::Level; +use tokio::sync::{broadcast, mpsc, oneshot}; +use tracing_core::Subscriber; +use tracing_subscriber::{reload, reload::Error as ReloadError}; + +use crate::telemetry; + +/// TODO +#[derive(Clone)] +pub struct LoggerHandle { + sender: mpsc::Sender, +} + +impl LoggerHandle { + pub(crate) fn new( + handle: reload::Handle, + telemetry_receiver: mpsc::Receiver, + ) -> Self { + let (tx, rx) = mpsc::channel(32); + let (regular, _) = broadcast::channel(32); + let (future_forward, _) = broadcast::channel(32); + let mut actor = LoggerActor { + message_receiver: rx, + level_handle: handle, + telemetry_receiver, + telemetry_forwarder_regular: regular, + telemetry_forwarder_future: future_forward, + }; + tokio::spawn(async move { actor.run().await }); + + Self { sender: tx } + } + + /// Reload the log level filter. + /// + /// # Errors + /// - If reloading on the side of [`reload::Handle`] fails + /// - If actor communication fails + pub async fn reload_level(&self, new_value: Level) -> color_eyre::Result<(), Error> { + let (tx, rx) = oneshot::channel(); + let _ = self + .sender + .send(Message::ReloadLevel { + value: new_value, + respond_to: tx, + }) + .await; + Ok(rx.await??) + } + + /// Subscribe to the telemetry events broadcasting. + /// + /// # Errors + /// If actor communication fails + pub async fn subscribe_on_telemetry( + &self, + channel: telemetry::Channel, + ) -> color_eyre::Result, Error> { + let (tx, rx) = oneshot::channel(); + let _ = self + .sender + .send(Message::SubscribeOnTelemetry { + channel, + respond_to: tx, + }) + .await; + Ok(rx.await?) + } +} + +enum Message { + ReloadLevel { + value: Level, + respond_to: oneshot::Sender>, + }, + SubscribeOnTelemetry { + channel: telemetry::Channel, + respond_to: oneshot::Sender>, + }, +} + +/// Possible errors that might occur while interacting with the actor. +#[derive(thiserror::Error, Debug)] +pub enum Error { + /// If dynamic log level reloading failed + #[error("cannot dynamically reload the log level")] + LevelReload(#[from] ReloadError), + /// If actor communication is broken + #[error("failed to communicate with the actor")] + Communication(#[from] oneshot::error::RecvError), +} + +struct LoggerActor { + message_receiver: mpsc::Receiver, + telemetry_receiver: mpsc::Receiver, + telemetry_forwarder_regular: broadcast::Sender, + telemetry_forwarder_future: broadcast::Sender, + level_handle: reload::Handle, +} + +impl LoggerActor { + async fn run(&mut self) { + loop { + tokio::select! { + Some(msg) = self.message_receiver.recv() => { + self.handle_message(msg); + }, + Some(telemetry::ChannelEvent(channel, event)) = self.telemetry_receiver.recv() => { + let forward_to = match channel { + telemetry::Channel::Regular => &self.telemetry_forwarder_regular, + telemetry::Channel::Future => &self.telemetry_forwarder_future, + }; + + let _ = forward_to.send(event); + }, + else => break + } + tokio::task::yield_now().await; + } + } + + fn handle_message(&mut self, msg: Message) { + match msg { + Message::ReloadLevel { value, respond_to } => { + let level = into_tracing_level(value); + let filter = tracing_subscriber::filter::LevelFilter::from_level(level); + let result = self.level_handle.reload(filter); + let _ = respond_to.send(result); + } + Message::SubscribeOnTelemetry { + channel: kind, + respond_to, + } => { + let receiver = match kind { + telemetry::Channel::Regular => self.telemetry_forwarder_regular.subscribe(), + telemetry::Channel::Future => self.telemetry_forwarder_future.subscribe(), + }; + let _ = respond_to.send(receiver); + } + } + } +} diff --git a/logger/src/layer.rs b/logger/src/layer.rs index 5b17d93cfe4..6da758b35e9 100644 --- a/logger/src/layer.rs +++ b/logger/src/layer.rs @@ -111,7 +111,7 @@ pub struct LevelFilter { static CURRENT_LEVEL: AtomicU8 = AtomicU8::new(0); /// Return max log level -pub fn max_log_level() -> u8 { +pub fn current_level() -> u8 { CURRENT_LEVEL.load(Ordering::Relaxed) } @@ -129,12 +129,12 @@ impl LevelFilter { /// Constructor of level filter #[allow(clippy::new_ret_no_self)] pub fn new(level: Level, subscriber: S) -> impl Subscriber { - Self::update_max_log_level(level); + Self::update_log_level(level); EventSubscriber(Self { subscriber }) } /// Updater of max level - fn update_max_log_level(level: Level) { + fn update_log_level(level: Level) { CURRENT_LEVEL.store(Self::level_as_u8(level), Ordering::SeqCst); } } @@ -148,7 +148,7 @@ impl EventInspectorTrait for LevelFilter { fn event(&self, event: &Event<'_>) { let level = Self::level_as_u8(*event.metadata().level()); - if level >= max_log_level() { + if level >= current_level() { self.subscriber.event(event) } } diff --git a/logger/src/lib.rs b/logger/src/lib.rs index fceecc1d26c..f84ddc6a7d8 100644 --- a/logger/src/lib.rs +++ b/logger/src/lib.rs @@ -1,159 +1,139 @@ //! Iroha's logging utilities. +pub mod actor; pub mod layer; pub mod telemetry; use std::{ fmt::Debug, - fs::OpenOptions, - path::PathBuf, sync::{ atomic::{AtomicBool, Ordering}, - Arc, + OnceLock, }, }; -use color_eyre::{eyre::WrapErr, Report, Result}; -use iroha_config::logger::into_tracing_level; -pub use iroha_config::logger::{Configuration, ConfigurationProxy}; -pub use telemetry::{Telemetry, TelemetryFields, TelemetryLayer}; -use tokio::sync::mpsc::Receiver; +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 tracing::subscriber::set_global_default; pub use tracing::{ debug, debug_span, error, error_span, info, info_span, instrument as log, trace, trace_span, warn, warn_span, Instrument, }; -use tracing::{subscriber::set_global_default, Subscriber}; -use tracing_bunyan_formatter::{BunyanFormattingLayer, JsonStorageLayer}; pub use tracing_futures::Instrument as InstrumentFutures; +pub use tracing_subscriber::reload::Error as ReloadError; use tracing_subscriber::{layer::SubscriberExt, registry::Registry, reload}; -/// Substrate telemetry -pub type SubstrateTelemetry = Receiver; - -/// Future telemetry -pub type FutureTelemetry = Receiver; - -/// Convenience wrapper for Telemetry types. -pub type Telemetries = (SubstrateTelemetry, FutureTelemetry); +const TELEMETRY_CAPACITY: usize = 1000; static LOGGER_SET: AtomicBool = AtomicBool::new(false); -/// Initializes `Logger` with given [`Configuration`]. -/// After the initialization `log` macros will print with the use of this `Logger`. -/// Returns the receiving side of telemetry channels (regular telemetry, future telemetry) -/// -/// # Errors -/// If the logger is already set, raises a generic error. -pub fn init(configuration: &Configuration) -> Result> { +fn try_set_logger() -> Result<()> { if LOGGER_SET .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst) .is_err() { - return Ok(None); + return Err(eyre!("Logger is already set.")); } - Ok(Some(setup_logger(configuration)?)) + Ok(()) } -/// Disables the logger by setting `LOGGER_SET` to true. Will fail -/// if the logger has already been initialized. This function is -/// required in order to generate flamegraphs and flamecharts. +/// Initializes the logger globally with given [`Configuration`]. /// -/// Returns true on success. -pub fn disable_logger() -> bool { - LOGGER_SET - .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst) - .is_ok() -} +/// Returns [`LoggerHandle`] to interact with the logger instance +/// +/// Works only once per process, all subsequent invocations will fail. +/// +/// For usage in tests consider [`test_logger`]. +/// +/// # Errors +/// 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 { + try_set_logger()?; -fn setup_logger(configuration: &Configuration) -> Result { let layer = tracing_subscriber::fmt::layer() - .with_ansi(configuration.terminal_colors) + .with_ansi(terminal_colors) .with_test_writer(); - if configuration.compact_mode { - add_bunyan(configuration, layer.compact()) - } else { - add_bunyan(configuration, layer) + match configuration.format { + Format::Full => step2(configuration, layer), + Format::Compact => step2(configuration, layer.compact()), + Format::Pretty => step2(configuration, layer.pretty()), + Format::Json => step2(configuration, layer.json()), } } -fn bunyan_writer_create(destination: PathBuf) -> Result> { - OpenOptions::new() - .create(true) - .append(true) - .open(destination) - .wrap_err("Failed to create or open bunyan logs file") - .map(Arc::new) +/// Returns once lazily initialised global logger for testing purposes. +/// +/// # Panics +/// If [`init_global`] or [`disable_global`] were called first. +#[allow(clippy::needless_update)] // `tokio-console` feature adds additional fields to Configuration +pub fn test_logger() -> LoggerHandle { + static LOGGER: OnceLock = OnceLock::new(); + + LOGGER + .get_or_init(|| { + // 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() + }; + + init_global(&config, true).expect( + "`init_global()` or `disable_global()` should not be called before `test_logger()`", + ) + }) + .clone() +} + +/// Disables the logger globally, so that subsequent calls to [`init_global`] will fail. +/// +/// Disabling logger is required in order to generate flamegraphs and flamecharts. +/// +/// # Errors +/// If global logger was already initialised/disabled. +pub fn disable_global() -> Result<()> { + try_set_logger() } -fn add_bunyan(configuration: &Configuration, layer: L) -> Result +fn step2(configuration: &Configuration, layer: L) -> Result where L: tracing_subscriber::Layer + Debug + Send + Sync + 'static, { - let level: tracing::Level = into_tracing_level(configuration.max_log_level.value()); + let level: tracing::Level = into_tracing_level(configuration.level); let level_filter = tracing_subscriber::filter::LevelFilter::from_level(level); - let (filter, handle) = reload::Layer::new(level_filter); - configuration - .max_log_level - .set_handle(iroha_config::logger::ReloadHandle(handle)); - let (bunyan_layer, storage_layer) = match configuration.log_file_path.clone() { - Some(path) => ( - Some(BunyanFormattingLayer::new( - "bunyan_layer".into(), - bunyan_writer_create(path)?, - )), - Some(JsonStorageLayer), - ), - None => (None, None), - }; + let (level_filter, level_filter_handle) = reload::Layer::new(level_filter); let subscriber = Registry::default() .with(layer) - .with(filter) - .with(storage_layer) - .with(tracing_error::ErrorLayer::default()) - .with(bunyan_layer); - - add_tokio_console_subscriber(configuration, subscriber) -} + .with(level_filter) + .with(tracing_error::ErrorLayer::default()); -fn add_tokio_console_subscriber< - S: Subscriber + Send + Sync + 'static + for<'a> tracing_subscriber::registry::LookupSpan<'a>, ->( - configuration: &Configuration, - subscriber: S, -) -> Result { #[cfg(all(feature = "tokio-console", not(feature = "no-tokio-console")))] - { + let subscriber = { let console_subscriber = console_subscriber::ConsoleLayer::builder() .server_addr( configuration .tokio_console_addr - .parse::() + .into() .expect("Invalid address for tokio console"), ) .spawn(); - add_telemetry_and_set_default(configuration, subscriber.with(console_subscriber)) - } - #[cfg(any(not(feature = "tokio-console"), feature = "no-tokio-console"))] - { - add_telemetry_and_set_default(configuration, subscriber) - } -} - -fn add_telemetry_and_set_default( - configuration: &Configuration, - subscriber: S, -) -> Result { - // static global_subscriber: dyn Subscriber = once_cell::new; - let (subscriber, receiver, receiver_future) = TelemetryLayer::from_capacity( - subscriber, - configuration - .telemetry_capacity - .try_into() - .expect("u32 should always fit in usize"), - ); + subscriber.with(console_subscriber) + }; + let (subscriber, receiver) = telemetry::Layer::with_capacity(subscriber, TELEMETRY_CAPACITY); set_global_default(subscriber)?; - Ok((receiver, receiver_future)) + + let handle = LoggerHandle::new(level_filter_handle, receiver); + + Ok(handle) } /// Macro for sending telemetry info diff --git a/logger/src/telemetry.rs b/logger/src/telemetry.rs index 526209daa60..3e65e5914bb 100644 --- a/logger/src/telemetry.rs +++ b/logger/src/telemetry.rs @@ -4,25 +4,25 @@ use std::{error::Error, fmt::Debug}; use derive_more::{Deref, DerefMut}; use serde_json::Value; -use tokio::sync::mpsc::{self, Receiver, Sender}; +use tokio::sync::mpsc; use tracing::{ field::{Field, Visit}, - Event, Subscriber, + Event as TracingEvent, Subscriber, }; use crate::layer::{EventInspectorTrait, EventSubscriber}; /// Target for telemetry in `tracing` -pub const TELEMETRY_TARGET_PREFIX: &str = "telemetry::"; +pub const TARGET_PREFIX: &str = "telemetry::"; /// Target for telemetry future in `tracing` -pub const TELEMETRY_FUTURE_TARGET_PREFIX: &str = "telemetry_future::"; +pub const FUTURE_TARGET_PREFIX: &str = "telemetry_future::"; /// Fields for telemetry (type for efficient saving) #[derive(Clone, Debug, PartialEq, Eq, Default, Deref, DerefMut)] -pub struct TelemetryFields(pub Vec<(&'static str, Value)>); +pub struct Fields(pub Vec<(&'static str, Value)>); -impl From for Value { - fn from(TelemetryFields(fields): TelemetryFields) -> Self { +impl From for Value { + fn from(Fields(fields): Fields) -> Self { fields .into_iter() .map(|(key, value)| (key.to_owned(), value)) @@ -32,14 +32,14 @@ impl From for Value { /// Telemetry which can be received from telemetry layer #[derive(Clone, Debug, PartialEq, Eq)] -pub struct Telemetry { +pub struct Event { /// Subsystem from which telemetry was received pub target: &'static str, /// Fields which was recorded - pub fields: TelemetryFields, + pub fields: Fields, } -impl Visit for Telemetry { +impl Visit for Event { fn record_debug(&mut self, field: &Field, value: &dyn Debug) { self.fields .push((field.name(), format!("{:?}", &value).into())) @@ -71,9 +71,9 @@ impl Visit for Telemetry { } } -impl Telemetry { - fn from_event(target: &'static str, event: &Event<'_>) -> Self { - let fields = TelemetryFields::default(); +impl Event { + fn from_event(target: &'static str, event: &TracingEvent<'_>) -> Self { + let fields = Fields::default(); let mut telemetry = Self { target, fields }; event.record(&mut telemetry); telemetry @@ -82,70 +82,58 @@ impl Telemetry { /// Telemetry layer #[derive(Debug, Clone)] -pub struct TelemetryLayer { - telemetry_sender: Sender, - telemetry_future_sender: Sender, +pub struct Layer { + sender: mpsc::Sender, subscriber: S, } -impl TelemetryLayer { - /// Create telemetry from channel sender - pub fn from_senders( - subscriber: S, - telemetry_sender: Sender, - telemetry_future_sender: Sender, - ) -> impl Subscriber { - EventSubscriber(Self { - telemetry_sender, - telemetry_future_sender, - subscriber, - }) - } - - /// Create new telemetry layer with specific channel size (via const generic) - #[allow(clippy::new_ret_no_self)] - pub fn new( - subscriber: S, - ) -> (impl Subscriber, Receiver, Receiver) { - let (sender, receiver) = mpsc::channel(CHANNEL_SIZE); - let (sender_future, receiver_future) = mpsc::channel(CHANNEL_SIZE); - let telemetry = Self::from_senders(subscriber, sender, sender_future); - (telemetry, receiver, receiver_future) - } - +impl Layer { /// Create new telemetry layer with specific channel size #[allow(clippy::new_ret_no_self)] - pub fn from_capacity( + pub fn with_capacity( subscriber: S, channel_size: usize, - ) -> (impl Subscriber, Receiver, Receiver) { + ) -> (impl Subscriber, mpsc::Receiver) { let (sender, receiver) = mpsc::channel(channel_size); - let (sender_future, receiver_future) = mpsc::channel(channel_size); - let telemetry = Self::from_senders(subscriber, sender, sender_future); - (telemetry, receiver, receiver_future) + let telemetry = EventSubscriber(Self { sender, subscriber }); + (telemetry, receiver) + } + + fn send_event(&self, channel: Channel, target: &'static str, event: &TracingEvent<'_>) { + let _ = self + .sender + .try_send(ChannelEvent(channel, Event::from_event(target, event))); } } -impl EventInspectorTrait for TelemetryLayer { +impl EventInspectorTrait for Layer { type Subscriber = S; fn inner_subscriber(&self) -> &Self::Subscriber { &self.subscriber } - fn event(&self, event: &Event<'_>) { + fn event(&self, event: &TracingEvent<'_>) { let target = event.metadata().target(); #[allow(clippy::option_if_let_else)] // This is actually more readable. - if let Some(telemetry_target) = target.strip_prefix(TELEMETRY_TARGET_PREFIX) { - let _result = self - .telemetry_sender - .try_send(Telemetry::from_event(telemetry_target, event)); - } else if let Some(future_target) = target.strip_prefix(TELEMETRY_FUTURE_TARGET_PREFIX) { - let _result = self - .telemetry_future_sender - .try_send(Telemetry::from_event(future_target, event)); + if let Some(target) = target.strip_prefix(TARGET_PREFIX) { + self.send_event(Channel::Regular, target, event); + } else if let Some(target) = target.strip_prefix(FUTURE_TARGET_PREFIX) { + self.send_event(Channel::Future, target, event); } else { self.subscriber.event(event) } } } + +/// A pair of [`Channel`] associated with [`Event`] +pub struct ChannelEvent(pub Channel, pub Event); + +/// Supported telemetry channels +#[derive(Copy, Clone)] +pub enum Channel { + /// Regular telemetry + Regular, + /// Telemetry collected from futures instrumented with `iroha_futures::TelemetryFuture`. + Future, +} diff --git a/logger/tests/configuration.rs b/logger/tests/configuration.rs index 661443ed256..d63d837200c 100644 --- a/logger/tests/configuration.rs +++ b/logger/tests/configuration.rs @@ -1,26 +1,23 @@ use std::time::Duration; -use iroha_data_model::Level; -use iroha_logger::{info, init, Configuration, Telemetry, TelemetryFields}; +use iroha_logger::{ + info, + telemetry::{Channel, Event, Fields}, + test_logger, +}; use tokio::time; #[tokio::test] async fn telemetry_separation_custom() { - let config = Configuration { - max_log_level: Level::TRACE.into(), - telemetry_capacity: 100, - compact_mode: true, - log_file_path: Some("/dev/stdout".into()), - terminal_colors: true, - #[cfg(feature = "tokio-console")] - tokio_console_addr: "127.0.0.1:5555".into(), - }; - let (mut receiver, _) = init(&config).unwrap().unwrap(); + let mut receiver = test_logger() + .subscribe_on_telemetry(Channel::Regular) + .await + .unwrap(); info!(target: "telemetry::test", a = 2, c = true, d = "this won't be logged"); info!("This will be logged in bunyan-readable format"); - let telemetry = Telemetry { + let telemetry = Event { target: "test", - fields: TelemetryFields(vec![ + fields: Fields(vec![ ("a", serde_json::json!(2)), ("c", serde_json::json!(true)), ("d", serde_json::json!("this won't be logged")), diff --git a/logger/tests/setting_logger.rs b/logger/tests/setting_logger.rs index 6d204f7abca..209a6b45928 100644 --- a/logger/tests/setting_logger.rs +++ b/logger/tests/setting_logger.rs @@ -1,21 +1,17 @@ use iroha_config::base::proxy::Builder; -use iroha_logger::{init, ConfigurationProxy}; +use iroha_logger::{init_global, ConfigurationProxy}; #[tokio::test] async fn setting_logger_twice_fails() { - assert!(init( - &ConfigurationProxy::default() - .build() - .expect("Default logger config always builds") - ) - .is_ok()); - let second_init = init( - &ConfigurationProxy::default() - .build() - .expect("Default logger config always builds"), - ); - assert!(second_init.is_ok()); - assert!(second_init.unwrap().is_none()); + let cfg = ConfigurationProxy::default() + .build() + .expect("Default logger config always builds"); + + let first = init_global(&cfg, false); + assert!(first.is_ok()); + + let second = init_global(&cfg, false); + assert!(second.is_err()); } #[test] diff --git a/logger/tests/telemetry.rs b/logger/tests/telemetry.rs index bfab41332eb..64b8985ca0d 100644 --- a/logger/tests/telemetry.rs +++ b/logger/tests/telemetry.rs @@ -1,19 +1,23 @@ use std::time::Duration; -use iroha_config::base::proxy::Builder; -use iroha_logger::{info, init, ConfigurationProxy, Telemetry, TelemetryFields}; +use iroha_logger::{ + info, + telemetry::{Channel, Event, Fields}, + test_logger, +}; use tokio::time; #[tokio::test] async fn telemetry_separation_default() { - let (mut receiver, _) = init(&ConfigurationProxy::default().build().unwrap()) - .unwrap() + let mut receiver = test_logger() + .subscribe_on_telemetry(Channel::Regular) + .await .unwrap(); info!(target: "telemetry::test", a = 2, c = true, d = "this won't be logged"); info!("This will be logged"); - let telemetry = Telemetry { + let telemetry = Event { target: "test", - fields: TelemetryFields(vec![ + fields: Fields(vec![ ("a", serde_json::json!(2)), ("c", serde_json::json!(true)), ("d", serde_json::json!("this won't be logged")), diff --git a/p2p/tests/integration/p2p.rs b/p2p/tests/integration/p2p.rs index 93f9a391765..b23faff5036 100644 --- a/p2p/tests/integration/p2p.rs +++ b/p2p/tests/integration/p2p.rs @@ -10,8 +10,8 @@ use std::{ use futures::{prelude::*, stream::FuturesUnordered, task::AtomicWaker}; use iroha_config_base::proxy::Builder; use iroha_crypto::KeyPair; -use iroha_data_model::{prelude::PeerId, Level}; -use iroha_logger::{prelude::*, Configuration, ConfigurationProxy}; +use iroha_data_model::prelude::PeerId; +use iroha_logger::{prelude::*, ConfigurationProxy}; use iroha_p2p::{network::message::*, NetworkHandle}; use iroha_primitives::addr::socket_addr; use parity_scale_codec::{Decode, Encode}; @@ -23,18 +23,16 @@ use tokio::{ #[derive(Clone, Debug, Decode, Encode)] struct TestMessage(String); -static INIT: Once = Once::new(); - fn setup_logger() { + static INIT: Once = Once::new(); + INIT.call_once(|| { - let log_config = Configuration { - max_log_level: Level::TRACE.into(), - compact_mode: false, - ..ConfigurationProxy::default() - .build() - .expect("Default logger config failed to build. This is a programmer error") - }; - iroha_logger::init(&log_config).expect("Failed to start logger"); + 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(); }) } @@ -230,17 +228,7 @@ async fn two_networks() { #[tokio::test(flavor = "multi_thread", worker_threads = 8)] async fn multiple_networks() { - let log_config = Configuration { - max_log_level: Level::TRACE.into(), - compact_mode: false, - ..ConfigurationProxy::default() - .build() - .expect("Default logger config should always build") - }; - // Can't use logger because it's failed to initialize. - if let Err(err) = iroha_logger::init(&log_config) { - eprintln!("Failed to initialize logger: {err}"); - } + setup_logger(); info!("Starting..."); let mut peers = Vec::new(); diff --git a/scripts/test_env.py b/scripts/test_env.py index 65b2266c3ca..458fc0ae6c1 100755 --- a/scripts/test_env.py +++ b/scripts/test_env.py @@ -118,14 +118,14 @@ def run(self, is_genesis: bool = False): os.environ["KURA_BLOCK_STORE_PATH"] = str(peer_dir.joinpath("storage")) os.environ["SNAPSHOT_DIR_PATH"] = str(peer_dir.joinpath("storage")) - os.environ["LOG_FILE_PATH"] = str(peer_dir.joinpath("log.json")) - os.environ["MAX_LOG_LEVEL"] = "TRACE" + os.environ["LOG_LEVEL"] = "TRACE" + os.environ["LOG_FORMAT"] = "\"pretty\"" + os.environ["LOG_TOKIO_CONSOLE_ADDR"] = f"{self.host_ip}:{self.tokio_console_port}" os.environ["IROHA_PUBLIC_KEY"] = self.public_key os.environ["IROHA_PRIVATE_KEY"] = self.private_key os.environ["SUMERAGI_DEBUG_FORCE_SOFT_FORK"] = "false" os.environ["TORII_P2P_ADDR"] = f"{self.host_ip}:{self.p2p_port}" os.environ["TORII_API_URL"] = f"{self.host_ip}:{self.api_port}" - os.environ["TOKIO_CONSOLE_ADDR"] = f"{self.host_ip}:{self.tokio_console_port}" genesis_arg = "--submit-genesis" if is_genesis else "" # FD never gets closed diff --git a/telemetry/Cargo.toml b/telemetry/Cargo.toml index 4289c80e87d..e1fb573541b 100644 --- a/telemetry/Cargo.toml +++ b/telemetry/Cargo.toml @@ -31,7 +31,7 @@ serde_json = { workspace = true } streaming-stats = "0.2.3" serde = { workspace = true, features = ["derive"] } tokio = { workspace = true, features = ["rt", "rt-multi-thread", "macros"] } -tokio-stream = { workspace = true, features = ["fs"] } +tokio-stream = { workspace = true, features = ["fs", "sync"] } tokio-tungstenite = { workspace = true } url = { workspace = true, features = ["serde"] } prometheus = { workspace = true } diff --git a/telemetry/src/dev.rs b/telemetry/src/dev.rs index d4970f7c653..674b7bca748 100644 --- a/telemetry/src/dev.rs +++ b/telemetry/src/dev.rs @@ -1,31 +1,26 @@ //! Module with development telemetry use eyre::{Result, WrapErr}; -use iroha_logger::telemetry::Telemetry; +use iroha_config::telemetry::DevTelemetryConfig; +use iroha_logger::telemetry::Event as Telemetry; use tokio::{ fs::OpenOptions, io::AsyncWriteExt, - sync::mpsc::Receiver, + sync::broadcast::Receiver, task::{self, JoinHandle}, }; -use tokio_stream::{wrappers::ReceiverStream, StreamExt}; - -use crate::Configuration; +use tokio_stream::{wrappers::BroadcastStream, StreamExt}; /// Starts telemetry writing to a file /// # Errors /// Fails if unable to open the file pub async fn start( - config: &Configuration, + DevTelemetryConfig { + file: telemetry_file, + }: DevTelemetryConfig, telemetry: Receiver, ) -> Result> { - let mut telemetry = crate::futures::get_stream(ReceiverStream::new(telemetry)); - - let Some(telemetry_file) = &config.file else { - return Ok(task::spawn(async move { - while telemetry.next().await.is_some() {} - })); - }; + let mut stream = crate::futures::get_stream(BroadcastStream::new(telemetry).fuse()); let mut file = OpenOptions::new() .write(true) @@ -40,11 +35,11 @@ pub async fn start( .wrap_err("Failed to create and open file for telemetry")?; // Serde doesn't support async Read Write traits. - // So let synchonous synchronous code be here. + // So let synchronous code be here. // // TODO: After migration to tokio move to https://docs.rs/tokio-serde let join_handle = task::spawn(async move { - while let Some(item) = telemetry.next().await { + while let Some(item) = stream.next().await { let telemetry_json = match serde_json::to_string(&item) { Ok(json) => json, Err(error) => { diff --git a/telemetry/src/futures.rs b/telemetry/src/futures.rs index df723f38406..5f4cb834d22 100644 --- a/telemetry/src/futures.rs +++ b/telemetry/src/futures.rs @@ -2,10 +2,10 @@ use std::{collections::HashMap, marker::Unpin, time::Duration}; use iroha_futures::FuturePollTelemetry; -use iroha_logger::telemetry::Telemetry; +use iroha_logger::telemetry::Event as Telemetry; use serde::{Deserialize, Serialize}; use tokio::time; -use tokio_stream::{Stream, StreamExt}; +use tokio_stream::{wrappers::errors::BroadcastStreamRecvError, Stream, StreamExt}; pub mod post_process { //! Module with telemetry post processing @@ -80,9 +80,10 @@ pub mod post_process { /// Gets stream of future poll telemetry out of general telemetry stream pub fn get_stream( - receiver: impl Stream + Unpin, + receiver: impl Stream> + Unpin, ) -> impl Stream + Unpin { receiver + .filter_map(Result::ok) .map(FuturePollTelemetry::try_from) .filter_map(Result::ok) .map( diff --git a/telemetry/src/ws.rs b/telemetry/src/ws.rs index 700860f2de8..c8f1486e76c 100644 --- a/telemetry/src/ws.rs +++ b/telemetry/src/ws.rs @@ -4,13 +4,15 @@ use std::time::Duration; use chrono::Local; use eyre::{eyre, Result}; use futures::{stream::SplitSink, Sink, SinkExt, StreamExt}; -use iroha_logger::Telemetry; +use iroha_config::telemetry::RegularTelemetryConfig; +use iroha_logger::telemetry::Event as Telemetry; use serde_json::Map; use tokio::{ net::TcpStream, - sync::mpsc::{self, Receiver, Sender}, + sync::{broadcast, mpsc}, + task::JoinHandle, }; -use tokio_stream::wrappers::ReceiverStream; +use tokio_stream::wrappers::{BroadcastStream, ReceiverStream}; use tokio_tungstenite::{ tungstenite::{Error, Message}, MaybeTlsStream, WebSocketStream, @@ -21,36 +23,43 @@ use crate::retry_period::RetryPeriod; type WebSocketSplitSink = SplitSink>, Message>; +const INTERNAL_CHANNEL_CAPACITY: usize = 10; + /// Starts telemetry sending data to a server /// # Errors /// Fails if unable to connect to the server -pub async fn start(config: &crate::Configuration, telemetry: Receiver) -> Result { - if let (Some(name), Some(url)) = (&config.name, &config.url) { - iroha_logger::info!(%url, "Starting telemetry"); - let (ws, _) = tokio_tungstenite::connect_async(url).await?; - let (write, _read) = ws.split(); - let (internal_sender, internal_receiver) = mpsc::channel(10); - let client = Client::new( - name.clone(), - write, - WebsocketSinkFactory::new(url.clone()), - RetryPeriod::new(config.min_retry_period, config.max_retry_delay_exponent), - internal_sender, - ); - tokio::task::spawn(async move { - client.run(telemetry, internal_receiver).await; - }); - Ok(true) - } else { - Ok(false) - } +pub async fn start( + RegularTelemetryConfig { + name, + url, + max_retry_delay_exponent, + min_retry_period, + }: RegularTelemetryConfig, + telemetry: broadcast::Receiver, +) -> Result> { + iroha_logger::info!(%url, "Starting telemetry"); + let (ws, _) = tokio_tungstenite::connect_async(&url).await?; + let (write, _read) = ws.split(); + let (internal_sender, internal_receiver) = mpsc::channel(INTERNAL_CHANNEL_CAPACITY); + let client = Client::new( + name, + write, + WebsocketSinkFactory::new(url), + RetryPeriod::new(min_retry_period, max_retry_delay_exponent), + internal_sender, + ); + let handle = tokio::task::spawn(async move { + client.run(telemetry, internal_receiver).await; + }); + + Ok(handle) } struct Client { name: String, sink_factory: F, retry_period: RetryPeriod, - internal_sender: Sender, + internal_sender: mpsc::Sender, sink: Option, init_msg: Option, } @@ -65,7 +74,7 @@ where sink: S, sink_factory: F, retry_period: RetryPeriod, - internal_sender: Sender, + internal_sender: mpsc::Sender, ) -> Self { Self { name, @@ -79,15 +88,15 @@ where pub async fn run( mut self, - receiver: Receiver, - internal_receiver: Receiver, + receiver: broadcast::Receiver, + internal_receiver: mpsc::Receiver, ) { - let mut stream = ReceiverStream::new(receiver).fuse(); + let mut stream = BroadcastStream::new(receiver).fuse(); let mut internal_stream = ReceiverStream::new(internal_receiver).fuse(); loop { tokio::select! { msg = stream.next() => { - if let Some(msg) = msg { + if let Some(Ok(msg)) = msg { self.on_telemetry(msg).await; } else { break; @@ -272,8 +281,7 @@ mod tests { use eyre::{eyre, Result}; use futures::{Sink, StreamExt}; - use iroha_config::base::proxy::Builder; - use iroha_logger::telemetry::{Telemetry, TelemetryFields}; + use iroha_logger::telemetry::{Event, Fields}; use serde_json::{Map, Value}; use tokio::task::JoinHandle; use tokio_tungstenite::tungstenite::{Error, Message}; @@ -356,13 +364,13 @@ mod tests { struct Suite { fail_send: Arc, fail_factory_create: Arc, - telemetry_sender: tokio::sync::mpsc::Sender, + telemetry_sender: tokio::sync::broadcast::Sender, message_receiver: futures::channel::mpsc::Receiver, } impl Suite { pub fn new() -> (Self, JoinHandle<()>) { - let (telemetry_sender, telemetry_receiver) = tokio::sync::mpsc::channel(100); + let (telemetry_sender, telemetry_receiver) = tokio::sync::broadcast::channel(100); let (message_sender, message_receiver) = futures::channel::mpsc::channel(100); let fail_send = Arc::new(AtomicBool::new(false)); let message_sender = { @@ -402,10 +410,10 @@ mod tests { } } - fn system_connected_telemetry() -> Telemetry { - Telemetry { + fn system_connected_telemetry() -> Event { + Event { target: "telemetry::test", - fields: TelemetryFields(vec![ + fields: Fields(vec![ ("msg", Value::String("system.connected".to_owned())), ( "genesis_hash", @@ -415,10 +423,10 @@ mod tests { } } - fn system_interval_telemetry(peers: u64) -> Telemetry { - Telemetry { + fn system_interval_telemetry(peers: u64) -> Event { + Event { target: "telemetry::test", - fields: TelemetryFields(vec![ + fields: Fields(vec![ ("msg", Value::String("system.interval".to_owned())), ("peers", Value::Number(peers.into())), ]), @@ -433,10 +441,7 @@ mod tests { } = suite; // The first message is `initialization` - telemetry_sender - .send(system_connected_telemetry()) - .await - .unwrap(); + telemetry_sender.send(system_connected_telemetry()).unwrap(); tokio::time::sleep(Duration::from_millis(100)).await; { let msg = message_receiver.next().await.unwrap(); @@ -467,10 +472,7 @@ mod tests { } // The second message is `update` - telemetry_sender - .send(system_interval_telemetry(2)) - .await - .unwrap(); + telemetry_sender.send(system_interval_telemetry(2)).unwrap(); tokio::time::sleep(Duration::from_millis(100)).await; { let msg = message_receiver.next().await.unwrap(); @@ -500,19 +502,13 @@ mod tests { // Fail sending the first message fail_send.store(true, Ordering::SeqCst); - telemetry_sender - .send(system_connected_telemetry()) - .await - .unwrap(); + telemetry_sender.send(system_connected_telemetry()).unwrap(); message_receiver.try_next().unwrap_err(); tokio::time::sleep(Duration::from_millis(100)).await; // The second message is not sent because the sink is reset fail_send.store(false, Ordering::SeqCst); - telemetry_sender - .send(system_interval_telemetry(1)) - .await - .unwrap(); + telemetry_sender.send(system_interval_telemetry(1)).unwrap(); message_receiver.try_next().unwrap_err(); tokio::time::sleep(Duration::from_millis(100)).await; @@ -521,10 +517,7 @@ mod tests { tokio::time::sleep(Duration::from_secs(1)).await; // The third message is not sent because the sink is not created yet - telemetry_sender - .send(system_interval_telemetry(1)) - .await - .unwrap(); + telemetry_sender.send(system_interval_telemetry(1)).unwrap(); message_receiver.try_next().unwrap_err(); } @@ -538,19 +531,13 @@ mod tests { // Fail sending the first message fail_send.store(true, Ordering::SeqCst); - telemetry_sender - .send(system_connected_telemetry()) - .await - .unwrap(); + telemetry_sender.send(system_connected_telemetry()).unwrap(); message_receiver.try_next().unwrap_err(); tokio::time::sleep(Duration::from_millis(100)).await; // The second message is not sent because the sink is reset fail_send.store(false, Ordering::SeqCst); - telemetry_sender - .send(system_interval_telemetry(1)) - .await - .unwrap(); + telemetry_sender.send(system_interval_telemetry(1)).unwrap(); message_receiver.try_next().unwrap_err(); tokio::time::sleep(Duration::from_millis(100)).await; @@ -569,12 +556,6 @@ mod tests { ($ident:ident, $future:ident) => { #[tokio::test] async fn $ident() { - iroha_logger::init( - &iroha_logger::ConfigurationProxy::default() - .build() - .expect("Default logger config should always build"), - ) - .unwrap(); let (suite, run_handle) = Suite::new(); $future(suite).await; run_handle.await.unwrap(); From df0a704bbe45df717586bb2863ebe1c16ff57f86 Mon Sep 17 00:00:00 2001 From: sorabot Date: Tue, 12 Dec 2023 08:33:52 +0000 Subject: [PATCH 03/21] [documentation]: Update lts/stable configs following a release Signed-off-by: sorabot <> --- configs/peer/lts/executor.wasm | Bin 501156 -> 501157 bytes configs/peer/stable/executor.wasm | Bin 501156 -> 501157 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/configs/peer/lts/executor.wasm b/configs/peer/lts/executor.wasm index b74e020ea156800dff0626fd603722315e764215..544c9e29dfa4cb65d7bdae1f6ccd1e2cbb3e8fb2 100644 GIT binary patch delta 41804 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

=d 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 YXu>VU=#%Ni=$&l~M3`%5u{ad-KdsyPZU6uP diff --git a/configs/peer/stable/executor.wasm b/configs/peer/stable/executor.wasm index b74e020ea156800dff0626fd603722315e764215..544c9e29dfa4cb65d7bdae1f6ccd1e2cbb3e8fb2 100644 GIT binary patch delta 41804 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
=d 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 YXu>VU=#%Ni=$&l~M3`%5u{ad-KdsyPZU6uP From 86fe3fd3e49190d23cb008c050613549a144ea90 Mon Sep 17 00:00:00 2001 From: Nikita Strygin Date: Thu, 28 Sep 2023 17:34:48 +0300 Subject: [PATCH 04/21] [refactor] #3934: Migrate `iroha_wasm_derive` and `iroha_validator_derive` to syn 2.0 Signed-off-by: Nikita Strygin --- Cargo.lock | 10 +- smart_contract/derive/Cargo.toml | 9 +- smart_contract/derive/src/entrypoint.rs | 28 ++- smart_contract/derive/src/lib.rs | 22 +- smart_contract/executor/derive/Cargo.toml | 12 +- .../executor/derive/src/conversion.rs | 54 ++--- .../executor/derive/src/entrypoint.rs | 62 +++--- smart_contract/executor/derive/src/lib.rs | 78 +++++-- smart_contract/executor/derive/src/token.rs | 14 +- .../executor/derive/src/validate.rs | 196 ++++++++++-------- smart_contract/trigger/derive/Cargo.toml | 10 +- .../trigger/derive/src/entrypoint.rs | 28 ++- smart_contract/trigger/derive/src/lib.rs | 21 +- 13 files changed, 318 insertions(+), 226 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index dd4bd2c1248..0123f2c9830 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2968,7 +2968,6 @@ dependencies = [ "manyhow", "proc-macro2", "quote", - "syn 1.0.109", "syn 2.0.38", ] @@ -3189,9 +3188,11 @@ dependencies = [ name = "iroha_smart_contract_derive" version = "2.0.0-pre-rc.20" dependencies = [ + "iroha_macro_utils", + "manyhow", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.38", ] [[package]] @@ -3279,9 +3280,12 @@ dependencies = [ name = "iroha_trigger_derive" version = "2.0.0-pre-rc.20" dependencies = [ + "darling", + "iroha_macro_utils", + "manyhow", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.38", ] [[package]] diff --git a/smart_contract/derive/Cargo.toml b/smart_contract/derive/Cargo.toml index 72658aa9aa8..8ecc878acec 100644 --- a/smart_contract/derive/Cargo.toml +++ b/smart_contract/derive/Cargo.toml @@ -14,6 +14,9 @@ workspace = true proc-macro = true [dependencies] -syn.workspace = true -quote.workspace = true -proc-macro2.workspace = true +iroha_macro_utils = { workspace = true } + +syn2 = { workspace = true } +manyhow = { workspace = true } +quote = { workspace = true } +proc-macro2 = { workspace = true } diff --git a/smart_contract/derive/src/entrypoint.rs b/smart_contract/derive/src/entrypoint.rs index 4970b406ea3..426c2ab091f 100644 --- a/smart_contract/derive/src/entrypoint.rs +++ b/smart_contract/derive/src/entrypoint.rs @@ -1,26 +1,32 @@ //! Macro for writing smart contract entrypoint -use proc_macro::TokenStream; +#![allow(clippy::str_to_string)] + +use iroha_macro_utils::Emitter; +use manyhow::emit; +use proc_macro2::TokenStream; use quote::quote; -use syn::{parse_macro_input, parse_quote}; +use syn2::parse_quote; mod export { pub const SMART_CONTRACT_MAIN: &str = "_iroha_smart_contract_main"; } #[allow(clippy::needless_pass_by_value)] -pub fn impl_entrypoint(_attr: TokenStream, item: TokenStream) -> TokenStream { - let syn::ItemFn { +pub fn impl_entrypoint(emitter: &mut Emitter, item: syn2::ItemFn) -> TokenStream { + let syn2::ItemFn { attrs, vis, sig, mut block, - } = parse_macro_input!(item); + } = item; - assert!( - syn::ReturnType::Default == sig.output, - "Smart contract `main()` function must not have a return type" - ); + if sig.output != syn2::ReturnType::Default { + emit!( + emitter, + "Smart contract entrypoint must not have a return type" + ); + } let fn_name = &sig.ident; @@ -33,7 +39,8 @@ pub fn impl_entrypoint(_attr: TokenStream, item: TokenStream) -> TokenStream { ), ); - let main_fn_name = syn::Ident::new(export::SMART_CONTRACT_MAIN, proc_macro2::Span::call_site()); + let main_fn_name = + syn2::Ident::new(export::SMART_CONTRACT_MAIN, proc_macro2::Span::call_site()); quote! { /// Smart contract entrypoint @@ -51,5 +58,4 @@ pub fn impl_entrypoint(_attr: TokenStream, item: TokenStream) -> TokenStream { #vis #sig #block } - .into() } diff --git a/smart_contract/derive/src/lib.rs b/smart_contract/derive/src/lib.rs index af82cd24fbe..da3faa41190 100644 --- a/smart_contract/derive/src/lib.rs +++ b/smart_contract/derive/src/lib.rs @@ -1,6 +1,8 @@ //! Macros for writing smart contracts. -use proc_macro::TokenStream; +use iroha_macro_utils::Emitter; +use manyhow::{emit, manyhow}; +use proc_macro2::TokenStream; mod entrypoint; @@ -23,7 +25,23 @@ mod entrypoint; /// todo!() /// } /// ``` +#[manyhow] #[proc_macro_attribute] pub fn main(attr: TokenStream, item: TokenStream) -> TokenStream { - entrypoint::impl_entrypoint(attr, item) + let mut emitter = Emitter::new(); + + if !attr.is_empty() { + emit!( + emitter, + "Smart contract entrypoint does not accept attributes" + ); + } + + let Some(item) = emitter.handle(syn2::parse2(item)) else { + return emitter.finish_token_stream(); + }; + + let result = entrypoint::impl_entrypoint(&mut emitter, item); + + emitter.finish_token_stream_with(result) } diff --git a/smart_contract/executor/derive/Cargo.toml b/smart_contract/executor/derive/Cargo.toml index 652714e3625..43e093218fd 100644 --- a/smart_contract/executor/derive/Cargo.toml +++ b/smart_contract/executor/derive/Cargo.toml @@ -15,10 +15,10 @@ workspace = true proc-macro = true [dependencies] -iroha_macro_utils.workspace = true -syn = { workspace = true, features = ["full", "derive"] } +iroha_macro_utils = { workspace = true } + syn2 = { workspace = true, features = ["full", "derive"] } -quote.workspace = true -proc-macro2.workspace = true -manyhow.workspace = true -darling.workspace = true +quote = { workspace = true } +proc-macro2 = { workspace = true } +manyhow = { workspace = true } +darling = { workspace = true } diff --git a/smart_contract/executor/derive/src/conversion.rs b/smart_contract/executor/derive/src/conversion.rs index 87b27becbb5..009ace5a426 100644 --- a/smart_contract/executor/derive/src/conversion.rs +++ b/smart_contract/executor/derive/src/conversion.rs @@ -1,76 +1,66 @@ //! Module with conversion derive macros implementation -use super::*; +use proc_macro2::TokenStream; +use quote::quote; +use syn2::DeriveInput; /// [`derive_ref_into_asset_owner`](crate::derive_ref_into_asset_owner) macro implementation -pub fn impl_derive_ref_into_asset_owner(input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as DeriveInput); - +pub fn impl_derive_ref_into_asset_owner(input: &DeriveInput) -> TokenStream { impl_from( &input.ident, &input.generics, - &syn::parse_quote!(::iroha_executor::permission::asset::Owner), - &syn::parse_quote!(asset_id), + &syn2::parse_quote!(::iroha_executor::permission::asset::Owner), + &syn2::parse_quote!(asset_id), ) - .into() } /// [`derive_ref_into_asset_definition_creator`](crate::derive_ref_into_asset_definition_creator) /// macro implementation -pub fn impl_derive_ref_into_asset_definition_owner(input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as DeriveInput); - +pub fn impl_derive_ref_into_asset_definition_owner(input: &DeriveInput) -> TokenStream { impl_from( &input.ident, &input.generics, - &syn::parse_quote!(::iroha_executor::permission::asset_definition::Owner), - &syn::parse_quote!(asset_definition_id), + &syn2::parse_quote!(::iroha_executor::permission::asset_definition::Owner), + &syn2::parse_quote!(asset_definition_id), ) - .into() } /// [`derive_ref_into_account_owner`](crate::derive_ref_into_account_owner) macro implementation -pub fn impl_derive_ref_into_account_owner(input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as DeriveInput); - +pub fn impl_derive_ref_into_account_owner(input: &DeriveInput) -> TokenStream { impl_from( &input.ident, &input.generics, - &syn::parse_quote!(::iroha_executor::permission::account::Owner), - &syn::parse_quote!(account_id), + &syn2::parse_quote!(::iroha_executor::permission::account::Owner), + &syn2::parse_quote!(account_id), ) - .into() } /// [`derive_ref_into_domain_owner`](crate::derive_ref_into_domain_owner) macro implementation -pub fn impl_derive_ref_into_domain_owner(input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as DeriveInput); - +pub fn impl_derive_ref_into_domain_owner(input: &DeriveInput) -> TokenStream { impl_from( &input.ident, &input.generics, - &syn::parse_quote!(::iroha_executor::permission::domain::Owner), - &syn::parse_quote!(domain_id), + &syn2::parse_quote!(::iroha_executor::permission::domain::Owner), + &syn2::parse_quote!(domain_id), ) - .into() } fn impl_from( - ident: &syn::Ident, - generics: &syn::Generics, - pass_condition_type: &syn::Type, - field: &syn::Ident, -) -> proc_macro2::TokenStream { + ident: &syn2::Ident, + generics: &syn2::Generics, + pass_condition_type: &syn2::Type, + field: &syn2::Ident, +) -> TokenStream { use quote::ToTokens; let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); - let mut generics: proc_macro2::TokenStream = syn::parse_str("<'token, ").unwrap(); + let mut generics: TokenStream = syn2::parse_str("<'token, ").unwrap(); let impl_generics_tokens = impl_generics.into_token_stream(); if impl_generics_tokens.is_empty() { generics.extend(core::iter::once(proc_macro2::TokenTree::Punct( - syn::parse_str(">").unwrap(), + syn2::parse_str(">").unwrap(), ))); } else { generics.extend(impl_generics_tokens.into_iter().skip(1)); diff --git a/smart_contract/executor/derive/src/entrypoint.rs b/smart_contract/executor/derive/src/entrypoint.rs index 60b6cd9ad65..01c69e99c03 100644 --- a/smart_contract/executor/derive/src/entrypoint.rs +++ b/smart_contract/executor/derive/src/entrypoint.rs @@ -1,6 +1,10 @@ //! Module [`executor_entrypoint`](crate::executor_entrypoint) macro implementation -use super::*; +use iroha_macro_utils::Emitter; +use manyhow::emit; +use proc_macro2::TokenStream; +use quote::quote; +use syn2::parse_quote; mod export { pub const EXECUTOR_VALIDATE_TRANSACTION: &str = "_iroha_executor_validate_transaction"; @@ -17,14 +21,7 @@ mod import { /// [`executor_entrypoint`](crate::executor_entrypoint()) macro implementation #[allow(clippy::needless_pass_by_value)] -pub fn impl_entrypoint(attr: TokenStream, item: TokenStream) -> TokenStream { - let fn_item = parse_macro_input!(item as syn::ItemFn); - - assert!( - attr.is_empty(), - "`#[entrypoint]` macro for Executor entrypoints accepts no attributes" - ); - +pub fn impl_entrypoint(emitter: &mut Emitter, item: syn2::ItemFn) -> TokenStream { macro_rules! match_entrypoints { (validate: { $($user_entrypoint_name:ident => @@ -33,23 +30,27 @@ pub fn impl_entrypoint(attr: TokenStream, item: TokenStream) -> TokenStream { other: { $($other_user_entrypoint_name:ident => $branch:block),* $(,)? }) => { - match &fn_item.sig.ident { + match &item.sig.ident { $(fn_name if fn_name == stringify!($user_entrypoint_name) => { impl_validate_entrypoint( - fn_item, + item, stringify!($user_entrypoint_name), export::$generated_entrypoint_name, import::$query_validating_object_fn_name, ) })* $(fn_name if fn_name == stringify!($other_user_entrypoint_name) => $branch),* - _ => panic!( - "Executor entrypoint name must be one of: {:?}", - [ - $(stringify!($user_entrypoint_name),)* - $(stringify!($other_user_entrypoint_name),)* - ] - ), + _ => { + emit!( + emitter, + "Executor entrypoint name must be one of: {:?}", + [ + $(stringify!($user_entrypoint_name),)* + $(stringify!($other_user_entrypoint_name),)* + ] + ); + return quote!(); + }, } }; } @@ -61,18 +62,18 @@ pub fn impl_entrypoint(attr: TokenStream, item: TokenStream) -> TokenStream { validate_query => EXECUTOR_VALIDATE_QUERY(GET_VALIDATE_QUERY_PAYLOAD), } other: { - migrate => { impl_migrate_entrypoint(fn_item) } + migrate => { impl_migrate_entrypoint(item) } } } } fn impl_validate_entrypoint( - fn_item: syn::ItemFn, + fn_item: syn2::ItemFn, user_entrypoint_name: &'static str, generated_entrypoint_name: &'static str, get_validation_payload_fn_name: &'static str, ) -> TokenStream { - let syn::ItemFn { + let syn2::ItemFn { attrs, vis, sig, @@ -81,7 +82,7 @@ fn impl_validate_entrypoint( let fn_name = &sig.ident; assert!( - matches!(sig.output, syn::ReturnType::Type(_, _)), + matches!(sig.output, syn2::ReturnType::Type(_, _)), "Executor `{user_entrypoint_name}` entrypoint must have `Result` return type" ); @@ -92,11 +93,11 @@ fn impl_validate_entrypoint( ), ); - let generated_entrypoint_ident: syn::Ident = syn::parse_str(generated_entrypoint_name) + let generated_entrypoint_ident: syn2::Ident = syn2::parse_str(generated_entrypoint_name) .expect("Provided entrypoint name to generate is not a valid Ident, this is a bug"); - let get_validation_payload_fn_ident: syn::Ident = - syn::parse_str(get_validation_payload_fn_name).expect( + let get_validation_payload_fn_ident: syn2::Ident = + syn2::parse_str(get_validation_payload_fn_name).expect( "Provided function name to query validating object is not a valid Ident, this is a bug", ); @@ -125,11 +126,10 @@ fn impl_validate_entrypoint( #vis #sig #block } - .into() } -fn impl_migrate_entrypoint(fn_item: syn::ItemFn) -> TokenStream { - let syn::ItemFn { +fn impl_migrate_entrypoint(fn_item: syn2::ItemFn) -> TokenStream { + let syn2::ItemFn { attrs, vis, sig, @@ -138,11 +138,12 @@ fn impl_migrate_entrypoint(fn_item: syn::ItemFn) -> TokenStream { let fn_name = &sig.ident; assert!( - matches!(sig.output, syn::ReturnType::Type(_, _)), + matches!(sig.output, syn2::ReturnType::Type(_, _)), "Executor `migrate()` entrypoint must have `MigrationResult` return type" ); - let migrate_fn_name = syn::Ident::new(export::EXECUTOR_MIGRATE, proc_macro2::Span::call_site()); + let migrate_fn_name = + syn2::Ident::new(export::EXECUTOR_MIGRATE, proc_macro2::Span::call_site()); quote! { /// Executor `permission_token_schema` entrypoint @@ -167,5 +168,4 @@ fn impl_migrate_entrypoint(fn_item: syn::ItemFn) -> TokenStream { #vis #sig #block } - .into() } diff --git a/smart_contract/executor/derive/src/lib.rs b/smart_contract/executor/derive/src/lib.rs index b5795582d44..228357ca7ef 100644 --- a/smart_contract/executor/derive/src/lib.rs +++ b/smart_contract/executor/derive/src/lib.rs @@ -1,11 +1,8 @@ //! Crate with executor-related derive macros. use iroha_macro_utils::Emitter; -use manyhow::manyhow; -use proc_macro::TokenStream; -use proc_macro2::TokenStream as TokenStream2; -use quote::quote; -use syn::{parse_macro_input, parse_quote, DeriveInput}; +use manyhow::{emit, manyhow, Result}; +use proc_macro2::TokenStream; mod conversion; mod default; @@ -46,9 +43,25 @@ mod validate; /// todo!() /// } /// ``` +#[manyhow] #[proc_macro_attribute] pub fn entrypoint(attr: TokenStream, item: TokenStream) -> TokenStream { - entrypoint::impl_entrypoint(attr, item) + let mut emitter = Emitter::new(); + + if !attr.is_empty() { + emit!( + emitter, + "`#[entrypoint]` macro for Executor entrypoints accepts no attributes" + ); + } + + let Some(item) = emitter.handle(syn2::parse2(item)) else { + return emitter.finish_token_stream(); + }; + + let result = entrypoint::impl_entrypoint(&mut emitter, item); + + emitter.finish_token_stream_with(result) } /// Derive macro for `Token` trait. @@ -79,9 +92,12 @@ pub fn entrypoint(attr: TokenStream, item: TokenStream) -> TokenStream { /// }.is_owned_by(&authority) /// } /// ``` +#[manyhow] #[proc_macro_derive(Token)] -pub fn derive_token(input: TokenStream) -> TokenStream { - token::impl_derive_token(input) +pub fn derive_token(input: TokenStream) -> Result { + let input = syn2::parse2(input)?; + + Ok(token::impl_derive_token(&input)) } /// Derive macro for `ValidateGrantRevoke` trait. @@ -144,12 +160,14 @@ pub fn derive_token(input: TokenStream) -> TokenStream { // ... // } // ``` +#[manyhow] #[proc_macro_derive( ValidateGrantRevoke, attributes(validate, validate_grant, validate_revoke) )] -pub fn derive_validate_grant_revoke(input: TokenStream) -> TokenStream { - validate::impl_derive_validate_grant_revoke(input) +pub fn derive_validate_grant_revoke(input: TokenStream) -> Result { + let input = syn2::parse2(input)?; + validate::impl_derive_validate_grant_revoke(&input) } /// Should be used together with [`ValidateGrantRevoke`] derive macro to derive a conversion @@ -159,9 +177,14 @@ pub fn derive_validate_grant_revoke(input: TokenStream) -> TokenStream { /// /// Implements [`From`] for `permission::asset_definition::Owner` /// and not [`Into`] for your type. [`Into`] will be implemented automatically. +#[manyhow] #[proc_macro_derive(RefIntoAssetDefinitionOwner)] -pub fn derive_ref_into_asset_definition_owner(input: TokenStream) -> TokenStream { - conversion::impl_derive_ref_into_asset_definition_owner(input) +pub fn derive_ref_into_asset_definition_owner(input: TokenStream) -> Result { + let input = syn2::parse2(input)?; + + Ok(conversion::impl_derive_ref_into_asset_definition_owner( + &input, + )) } /// Should be used together with [`ValidateGrantRevoke`] derive macro to derive a conversion @@ -171,9 +194,12 @@ pub fn derive_ref_into_asset_definition_owner(input: TokenStream) -> TokenStream /// /// Implements [`From`] for `permission::asset::Owner` /// and not [`Into`] for your type. [`Into`] will be implemented automatically. +#[manyhow] #[proc_macro_derive(RefIntoAssetOwner)] -pub fn derive_ref_into_asset_owner(input: TokenStream) -> TokenStream { - conversion::impl_derive_ref_into_asset_owner(input) +pub fn derive_ref_into_asset_owner(input: TokenStream) -> Result { + let input = syn2::parse2(input)?; + + Ok(conversion::impl_derive_ref_into_asset_owner(&input)) } /// Should be used together with [`ValidateGrantRevoke`] derive macro to derive a conversion @@ -183,9 +209,12 @@ pub fn derive_ref_into_asset_owner(input: TokenStream) -> TokenStream { /// /// Implements [`From`] for `permission::asset::Owner` /// and not [`Into`] for your type. [`Into`] will be implemented automatically. +#[manyhow] #[proc_macro_derive(RefIntoAccountOwner)] -pub fn derive_ref_into_account_owner(input: TokenStream) -> TokenStream { - conversion::impl_derive_ref_into_account_owner(input) +pub fn derive_ref_into_account_owner(input: TokenStream) -> Result { + let input = syn2::parse2(input)?; + + Ok(conversion::impl_derive_ref_into_account_owner(&input)) } /// Should be used together with [`ValidateGrantRevoke`] derive macro to derive a conversion @@ -195,9 +224,12 @@ pub fn derive_ref_into_account_owner(input: TokenStream) -> TokenStream { /// /// Implements [`From`] for `permission::domain::Owner` /// and not [`Into`] for your type. [`Into`] will be implemented automatically. +#[manyhow] #[proc_macro_derive(RefIntoDomainOwner)] -pub fn derive_ref_into_domain_owner(input: TokenStream) -> TokenStream { - conversion::impl_derive_ref_into_domain_owner(input) +pub fn derive_ref_into_domain_owner(input: TokenStream) -> Result { + let input = syn2::parse2(input)?; + + Ok(conversion::impl_derive_ref_into_domain_owner(&input)) } /// Implements the `iroha_executor::Validate` trait for the given `Executor` struct. As @@ -211,7 +243,7 @@ pub fn derive_ref_into_domain_owner(input: TokenStream) -> TokenStream { /// `block_height` are needed. The types can be unqualified, but not aliased. #[manyhow] #[proc_macro_derive(Validate)] -pub fn derive_validate(input: TokenStream2) -> TokenStream2 { +pub fn derive_validate(input: TokenStream) -> TokenStream { let mut emitter = Emitter::new(); let Some(input) = emitter.handle(syn2::parse2(input)) else { @@ -256,7 +288,7 @@ pub fn derive_validate(input: TokenStream2) -> TokenStream2 { /// ``` #[manyhow] #[proc_macro_derive(Visit, attributes(visit))] -pub fn derive_visit(input: TokenStream2) -> TokenStream2 { +pub fn derive_visit(input: TokenStream) -> TokenStream { let mut emitter = Emitter::new(); let Some(input) = emitter.handle(syn2::parse2(input)) else { @@ -297,7 +329,7 @@ pub fn derive_visit(input: TokenStream2) -> TokenStream2 { /// ``` #[manyhow] #[proc_macro_derive(ValidateEntrypoints, attributes(entrypoints))] -pub fn derive_entrypoints(input: TokenStream2) -> TokenStream2 { +pub fn derive_entrypoints(input: TokenStream) -> TokenStream { let mut emitter = Emitter::new(); let Some(input) = emitter.handle(syn2::parse2(input)) else { @@ -318,7 +350,7 @@ pub fn derive_entrypoints(input: TokenStream2) -> TokenStream2 { /// The types can be unqualified, but not aliased. #[manyhow] #[proc_macro_derive(ExpressionEvaluator)] -pub fn derive_expression_evaluator(input: TokenStream2) -> TokenStream2 { +pub fn derive_expression_evaluator(input: TokenStream) -> TokenStream { let mut emitter = Emitter::new(); let Some(input) = emitter.handle(syn2::parse2(input)) else { @@ -340,7 +372,7 @@ pub fn derive_expression_evaluator(input: TokenStream2) -> TokenStream2 { /// `host`: `iroha_executor::smart_contract::Host`. The types can be unqualified, but not aliased. #[manyhow] #[proc_macro_derive(Constructor)] -pub fn derive_constructor(input: TokenStream2) -> TokenStream2 { +pub fn derive_constructor(input: TokenStream) -> TokenStream { let mut emitter = Emitter::new(); let Some(input) = emitter.handle(syn2::parse2(input)) else { diff --git a/smart_contract/executor/derive/src/token.rs b/smart_contract/executor/derive/src/token.rs index 69f7915d65e..6d961c1c3ad 100644 --- a/smart_contract/executor/derive/src/token.rs +++ b/smart_contract/executor/derive/src/token.rs @@ -1,10 +1,10 @@ //! Module with [`derive_token`](crate::derive_token) macro implementation -use super::*; +use proc_macro2::TokenStream; +use quote::quote; /// [`derive_token`](crate::derive_token()) macro implementation -pub fn impl_derive_token(input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as DeriveInput); +pub fn impl_derive_token(input: &syn2::DeriveInput) -> TokenStream { let generics = &input.generics; let ident = &input.ident; @@ -15,10 +15,9 @@ pub fn impl_derive_token(input: TokenStream) -> TokenStream { #impl_token #impl_try_from_permission_token } - .into() } -fn impl_token(ident: &syn::Ident, generics: &syn::Generics) -> proc_macro2::TokenStream { +fn impl_token(ident: &syn2::Ident, generics: &syn2::Generics) -> proc_macro2::TokenStream { let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); quote! { @@ -46,10 +45,7 @@ fn impl_token(ident: &syn::Ident, generics: &syn::Generics) -> proc_macro2::Toke } } -fn impl_try_from_permission_token( - ident: &syn::Ident, - generics: &syn::Generics, -) -> proc_macro2::TokenStream { +fn impl_try_from_permission_token(ident: &syn2::Ident, generics: &syn2::Generics) -> TokenStream { let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); let token_id = quote! { ::name() }; diff --git a/smart_contract/executor/derive/src/validate.rs b/smart_contract/executor/derive/src/validate.rs index de6cc982cf1..8a6a890b695 100644 --- a/smart_contract/executor/derive/src/validate.rs +++ b/smart_contract/executor/derive/src/validate.rs @@ -1,28 +1,27 @@ //! Module with [`derive_validate`](crate::derive_validate) macro implementation -use proc_macro2::Span; -use syn::{Attribute, Ident, Path, Type}; - -use super::*; +use darling::FromAttributes; +use manyhow::Result; +use proc_macro2::{Span, TokenStream}; +use quote::quote; +use syn2::{Attribute, Ident, Type}; /// [`derive_validate`](crate::derive_validate()) macro implementation -pub fn impl_derive_validate_grant_revoke(input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as DeriveInput); - let ident = input.ident; +pub fn impl_derive_validate_grant_revoke(input: &syn2::DeriveInput) -> Result { + let ident = &input.ident; - let (validate_grant_impl, validate_revoke_impl) = gen_validate_impls(&input.attrs); + let (validate_grant_impl, validate_revoke_impl) = gen_validate_impls(&input.attrs)?; let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); - quote! { + Ok(quote! { impl #impl_generics ::iroha_executor::permission::ValidateGrantRevoke for #ident #ty_generics #where_clause { #validate_grant_impl #validate_revoke_impl } - } - .into() + }) } /// Enum representing possible attributes. @@ -37,119 +36,140 @@ enum ValidateAttribute { }, } -impl ValidateAttribute { - fn from_attributes<'attr, A>(attributes: A) -> Self - where - A: IntoIterator, - { +impl FromAttributes for ValidateAttribute { + // we use `Option::or` to select the first specified condition in case of duplicates + // but we still _want_ to validate that each attribute parses successfully + // this is to ensure that we provide the user with as much validation as possible, instead of bailing out early + // `Option::or_else` would NOT work here, as it would not validate conditions after the first valid one + #[allow(clippy::or_fun_call)] + fn from_attributes(attrs: &[Attribute]) -> darling::Result { + let mut accumulator = darling::error::Accumulator::default(); + let mut general_condition: Option = None; let mut grant_condition: Option = None; let mut revoke_condition: Option = None; - let general_path: Path = syn::parse_str("validate").unwrap(); - let grant_path: Path = syn::parse_str("validate_grant").unwrap(); - let revoke_path: Path = syn::parse_str("validate_revoke").unwrap(); - - for attribute in attributes { - let path = &attribute.path; - - // Skip if it's not our attribute - if path != &general_path && path != &grant_path && path != &revoke_path { + for attr in attrs { + let path = attr.path(); + if !path.is_ident("validate") + && !path.is_ident("validate_grant") + && !path.is_ident("validate_revoke") + { continue; } - let Some(proc_macro2::TokenTree::Group(group)) = - attribute.tokens.clone().into_iter().next() - else { - panic!("Expected parentheses group"); - }; - assert!( - group.delimiter() == proc_macro2::Delimiter::Parenthesis, - "Expected parentheses" - ); - let tokens = group.stream().into(); - - match path { - _general if path == &general_path => { - assert!(grant_condition.is_none() && revoke_condition.is_none(), - "`validate` attribute can't be used with `validate_grant` or `validate_revoke` attributes"); - assert!( - general_condition.is_none(), - "`validate` attribute duplication is not allowed" - ); - - general_condition.replace(syn::parse(tokens).unwrap()); + let Some(list) = accumulator.handle(attr.meta.require_list().map_err(darling::Error::from)) else { continue; }; + let tokens = &list.tokens; + + if path.is_ident("validate") { + if grant_condition.is_some() || revoke_condition.is_some() { + accumulator.push(darling::Error::custom( + "`validate` attribute can't be used with `validate_grant` or `validate_revoke` attributes" + ).with_span(&attr)) } - _grant if path == &grant_path => { - assert!( - general_condition.is_none(), - "`validate_grant` attribute can't be used with `validate` attribute" - ); - assert!( - grant_condition.is_none(), - "`validate_grant` attribute duplication is not allowed" - ); - - grant_condition.replace(syn::parse(tokens).unwrap()); + if general_condition.is_some() { + accumulator.push( + darling::Error::custom("`validate` attribute duplication is not allowed") + .with_span(&attr), + ) } - _revoke if path == &revoke_path => { - assert!( - general_condition.is_none(), - "`validate_revoke` attribute can't be used with `validate` attribute" - ); - assert!( - revoke_condition.is_none(), - "`validate_revoke` attribute duplication is not allowed" - ); - - revoke_condition.replace(syn::parse(tokens).unwrap()); + + general_condition = general_condition + .or(accumulator + .handle(syn2::parse2(tokens.clone()).map_err(darling::Error::from))); + } else if path.is_ident("grant") { + if general_condition.is_some() { + accumulator.push( + darling::Error::custom( + "`validate_grant` attribute can't be used with `validate` attribute", + ) + .with_span(&attr), + ) } - path => { - panic!( - "Unexpected attribute: `{}`. Expected `validate`, `validate_grant` or `validate_revoke`", - path.get_ident().map_or_else(|| "".to_owned(), ToString::to_string) + if grant_condition.is_some() { + accumulator.push( + darling::Error::custom( + "`validate_grant` attribute duplication is not allowed", + ) + .with_span(&attr), ) } + + grant_condition = grant_condition + .or(accumulator + .handle(syn2::parse2(tokens.clone()).map_err(darling::Error::from))); + } else if path.is_ident("revoke") { + if general_condition.is_some() { + accumulator.push( + darling::Error::custom( + "`validate_revoke` attribute can't be used with `validate` attribute", + ) + .with_span(&attr), + ) + } + if revoke_condition.is_some() { + accumulator.push( + darling::Error::custom( + "`validate_revoke` attribute duplication is not allowed", + ) + .with_span(&attr), + ) + } + + revoke_condition = revoke_condition + .or(accumulator + .handle(syn2::parse2(tokens.clone()).map_err(darling::Error::from))); + } else { + unreachable!() } } - match (general_condition, grant_condition, revoke_condition) { - (Some(condition), None, None) => ValidateAttribute::General(condition), + let result = match (general_condition, grant_condition, revoke_condition) { + (Some(condition), None, None) => Ok(ValidateAttribute::General(condition)), (None, Some(grant_condition), Some(revoke_condition)) => { - ValidateAttribute::Separate { + Ok(ValidateAttribute::Separate { grant_condition, revoke_condition, - } + }) } (None, Some(_grant_condition), None) => { - panic!("`validate_grant` attribute should be used together with `validate_revoke` attribute") + Err(darling::Error::custom( + "`validate_grant` attribute should be used together with `validate_revoke` attribute" + )) } (None, None, Some(_revoke_condition)) => { - panic!("`validate_revoke` attribute should be used together with `validate_grant` attribute") + Err(darling::Error::custom( + "`validate_revoke` attribute should be used together with `validate_grant` attribute" + )) } - (None, None, None) => panic!("`validate` attribute or combination of `validate_grant` and `validate_revoke` attributes is required"), - _ => unreachable!(), - } + (None, None, None) => Err(darling::Error::custom( + "`validate` attribute or combination of `validate_grant` and `validate_revoke` attributes is required", + )), + _ => Err(darling::Error::custom("Invalid combination of attributes")), + }; + + let res = accumulator.handle(result); + + accumulator.finish().map(|_| res.unwrap()) } } fn gen_validate_impls( attributes: &[Attribute], -) -> (proc_macro2::TokenStream, proc_macro2::TokenStream) { - let validate_attribute = ValidateAttribute::from_attributes(attributes); - +) -> Result<(proc_macro2::TokenStream, proc_macro2::TokenStream)> { + let validate_attribute = ValidateAttribute::from_attributes(attributes)?; match validate_attribute { - ValidateAttribute::General(pass_condition) => ( + ValidateAttribute::General(pass_condition) => Ok(( gen_validate_impl(IsiName::Grant, &pass_condition), gen_validate_impl(IsiName::Revoke, &pass_condition), - ), + )), ValidateAttribute::Separate { grant_condition, revoke_condition, - } => ( + } => Ok(( gen_validate_impl(IsiName::Grant, &grant_condition), gen_validate_impl(IsiName::Revoke, &revoke_condition), - ), + )), } } diff --git a/smart_contract/trigger/derive/Cargo.toml b/smart_contract/trigger/derive/Cargo.toml index 486eaa75ad7..b2c4d84a6b4 100644 --- a/smart_contract/trigger/derive/Cargo.toml +++ b/smart_contract/trigger/derive/Cargo.toml @@ -15,6 +15,10 @@ workspace = true proc-macro = true [dependencies] -syn.workspace = true -quote.workspace = true -proc-macro2.workspace = true +iroha_macro_utils = { workspace = true } + +syn2 = { workspace = true } +manyhow = { workspace = true } +quote = { workspace = true } +proc-macro2 = { workspace = true } +darling = { workspace = true } diff --git a/smart_contract/trigger/derive/src/entrypoint.rs b/smart_contract/trigger/derive/src/entrypoint.rs index e045d262b5a..57a5b467e93 100644 --- a/smart_contract/trigger/derive/src/entrypoint.rs +++ b/smart_contract/trigger/derive/src/entrypoint.rs @@ -1,6 +1,10 @@ //! Module wht [`main`](super::main) macro implementation -use super::*; +use iroha_macro_utils::Emitter; +use manyhow::emit; +use proc_macro2::TokenStream; +use quote::quote; +use syn2::parse_quote; mod export { pub const TRIGGER_MAIN: &str = "_iroha_trigger_main"; @@ -8,18 +12,21 @@ mod export { /// [`main`](super::main()) macro implementation #[allow(clippy::needless_pass_by_value)] -pub fn impl_entrypoint(_attr: TokenStream, item: TokenStream) -> TokenStream { - let syn::ItemFn { +pub fn impl_entrypoint(emitter: &mut Emitter, item: syn2::ItemFn) -> TokenStream { + let syn2::ItemFn { attrs, vis, sig, mut block, - } = parse_macro_input!(item); - - assert!( - syn::ReturnType::Default == sig.output, - "Trigger `main()` function must not have a return type" - ); + } = item; + + if sig.output != syn2::ReturnType::Default { + emit!( + emitter, + sig.output, + "Trigger `main()` function must not have a return type" + ) + } let fn_name = &sig.ident; @@ -32,7 +39,7 @@ pub fn impl_entrypoint(_attr: TokenStream, item: TokenStream) -> TokenStream { ), ); - let main_fn_name = syn::Ident::new(export::TRIGGER_MAIN, proc_macro2::Span::call_site()); + let main_fn_name = syn2::Ident::new(export::TRIGGER_MAIN, proc_macro2::Span::call_site()); quote! { /// Smart contract entrypoint @@ -50,5 +57,4 @@ pub fn impl_entrypoint(_attr: TokenStream, item: TokenStream) -> TokenStream { #vis #sig #block } - .into() } diff --git a/smart_contract/trigger/derive/src/lib.rs b/smart_contract/trigger/derive/src/lib.rs index 71a0fb417c6..01701a708e9 100644 --- a/smart_contract/trigger/derive/src/lib.rs +++ b/smart_contract/trigger/derive/src/lib.rs @@ -1,8 +1,8 @@ //! Crate with trigger procedural macros. -use proc_macro::TokenStream; -use quote::quote; -use syn::{parse_macro_input, parse_quote}; +use iroha_macro_utils::Emitter; +use manyhow::{emit, manyhow}; +use proc_macro2::TokenStream; mod entrypoint; @@ -22,7 +22,20 @@ mod entrypoint; /// todo!() /// } /// ``` +#[manyhow] #[proc_macro_attribute] pub fn main(attr: TokenStream, item: TokenStream) -> TokenStream { - entrypoint::impl_entrypoint(attr, item) + let mut emitter = Emitter::new(); + + if !attr.is_empty() { + emit!(emitter, "#[main] attribute does not accept arguments"); + } + + let Some(item) = emitter.handle(syn2::parse2(item)) else { + return emitter.finish_token_stream(); + }; + + let result = entrypoint::impl_entrypoint(&mut emitter, item); + + emitter.finish_token_stream_with(result) } From 49e837d83d423febfa6fbc42714d08d05c543d5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marin=20Ver=C5=A1i=C4=87?= Date: Tue, 12 Dec 2023 14:50:30 +0300 Subject: [PATCH 05/21] [fix] #0000: On-chain predictable iteration order (#4130) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Marin Veršić --- Cargo.lock | 27 +++++++++-------- core/Cargo.toml | 2 ++ core/clippy.toml | 1 + core/src/block_sync.rs | 1 + core/src/lib.rs | 11 +++---- core/src/query/store.rs | 6 ++-- core/src/queue.rs | 4 +-- core/src/smartcontracts/isi/triggers/set.rs | 33 +++++++++++---------- core/src/smartcontracts/wasm.rs | 9 +++--- core/src/sumeragi/mod.rs | 5 +++- core/src/sumeragi/network_topology.rs | 4 +-- core/src/sumeragi/view_change.rs | 4 +-- core/src/wsv.rs | 11 +++---- data_model/clippy.toml | 1 + 14 files changed, 63 insertions(+), 56 deletions(-) create mode 100644 core/clippy.toml create mode 100644 data_model/clippy.toml diff --git a/Cargo.lock b/Cargo.lock index 0123f2c9830..d71bbe9df90 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1762,7 +1762,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" dependencies = [ "fallible-iterator", - "indexmap 2.0.2", + "indexmap 2.1.0", "stable_deref_trait", ] @@ -2584,9 +2584,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.0.2" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8adf3ddd720272c6ea8bf59463c04e0f93d0bbf7c5439b691bca2987e0270897" +checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" dependencies = [ "equivalent", "hashbrown 0.14.1", @@ -2796,6 +2796,7 @@ dependencies = [ "eyre", "futures", "hex", + "indexmap 2.1.0", "iroha_config", "iroha_crypto", "iroha_data_model", @@ -3845,7 +3846,7 @@ checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" dependencies = [ "crc32fast", "hashbrown 0.14.1", - "indexmap 2.0.2", + "indexmap 2.1.0", "memchr", ] @@ -4133,7 +4134,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9" dependencies = [ "fixedbitset", - "indexmap 2.0.2", + "indexmap 2.1.0", ] [[package]] @@ -4904,7 +4905,7 @@ version = "0.9.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a49e178e4452f45cb61d0cd8cebc1b0fafd3e41929e996cef79aa3aca91f574" dependencies = [ - "indexmap 2.0.2", + "indexmap 2.1.0", "itoa", "ryu", "serde", @@ -5606,7 +5607,7 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.0.2", + "indexmap 2.1.0", "toml_datetime", "winnow", ] @@ -6185,7 +6186,7 @@ version = "0.116.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a58e28b80dd8340cb07b8242ae654756161f6fc8d0038123d679b7b99964fa50" dependencies = [ - "indexmap 2.0.2", + "indexmap 2.1.0", "semver", ] @@ -6201,7 +6202,7 @@ dependencies = [ "bumpalo", "cfg-if", "fxprof-processed-profile", - "indexmap 2.0.2", + "indexmap 2.1.0", "libc", "log", "object", @@ -6326,7 +6327,7 @@ dependencies = [ "anyhow", "cranelift-entity", "gimli", - "indexmap 2.0.2", + "indexmap 2.1.0", "log", "object", "serde", @@ -6411,7 +6412,7 @@ dependencies = [ "anyhow", "cc", "cfg-if", - "indexmap 2.0.2", + "indexmap 2.1.0", "libc", "log", "mach", @@ -6463,7 +6464,7 @@ checksum = "41786c7bbbf250c0e685b291323b50c6bb65f0505a2c0b4f0b598c740f13f185" dependencies = [ "anyhow", "heck", - "indexmap 2.0.2", + "indexmap 2.1.0", "wit-parser", ] @@ -6718,7 +6719,7 @@ checksum = "15df6b7b28ce94b8be39d8df5cb21a08a4f3b9f33b631aedb4aa5776f785ead3" dependencies = [ "anyhow", "id-arena", - "indexmap 2.0.2", + "indexmap 2.1.0", "log", "semver", "serde", diff --git a/core/Cargo.toml b/core/Cargo.toml index 73a9f5c63b8..def91180650 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -68,7 +68,9 @@ displaydoc = { workspace = true } wasmtime = { workspace = true } parking_lot = { workspace = true, features = ["deadlock_detection"] } derive_more = { workspace = true } + uuid = { version = "1.4.1", features = ["v4"] } +indexmap = "2.1.0" [dev-dependencies] criterion = { workspace = true } diff --git a/core/clippy.toml b/core/clippy.toml new file mode 100644 index 00000000000..ad9bd114bed --- /dev/null +++ b/core/clippy.toml @@ -0,0 +1 @@ +disallowed-types = ["std::collections::HashMap", "std::collections::HashSet"] diff --git a/core/src/block_sync.rs b/core/src/block_sync.rs index 22adcfc2ef8..bf74dfcbbfd 100644 --- a/core/src/block_sync.rs +++ b/core/src/block_sync.rs @@ -84,6 +84,7 @@ impl BlockSynchronizer { } /// Get a random online peer. + #[allow(clippy::disallowed_types)] pub fn random_peer(peers: &std::collections::HashSet) -> Option { use rand::{seq::IteratorRandom, SeedableRng}; diff --git a/core/src/lib.rs b/core/src/lib.rs index c032e5fda37..3d17d7a16e7 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -16,9 +16,10 @@ pub mod tx; pub mod wsv; use core::time::Duration; -use std::collections::{BTreeSet, HashMap, HashSet}; +use std::collections::BTreeSet; use gossiper::TransactionGossip; +use indexmap::{IndexMap, IndexSet}; use iroha_data_model::{permission::Permissions, prelude::*}; use iroha_primitives::unique_vec::UniqueVec; use parity_scale_codec::{Decode, Encode}; @@ -39,16 +40,16 @@ pub type IrohaNetwork = iroha_p2p::NetworkHandle; pub type PeersIds = UniqueVec; /// Parameters set. -pub type Parameters = HashSet; +pub type Parameters = IndexSet; /// API to work with collections of [`DomainId`] : [`Domain`] mappings. -pub type DomainsMap = HashMap; +pub type DomainsMap = IndexMap; /// API to work with a collections of [`RoleId`]: [`Role`] mappings. -pub type RolesMap = HashMap; +pub type RolesMap = IndexMap; /// API to work with a collections of [`AccountId`] [`Permissions`] mappings. -pub type PermissionTokensMap = HashMap; +pub type PermissionTokensMap = IndexMap; /// API to work with a collections of [`AccountId`] to [`RoleId`] mappings. pub type AccountRolesSet = BTreeSet; diff --git a/core/src/query/store.rs b/core/src/query/store.rs index 92684de1e09..ae1957da793 100644 --- a/core/src/query/store.rs +++ b/core/src/query/store.rs @@ -2,11 +2,11 @@ use std::{ cmp::Ordering, - collections::HashMap, num::NonZeroU64, time::{Duration, Instant}, }; +use indexmap::IndexMap; use iroha_config::live_query_store::Configuration; use iroha_data_model::{ asset::AssetValue, @@ -67,7 +67,7 @@ type LiveQuery = Batched>; /// Clients can handle their queries using [`LiveQueryStoreHandle`] #[derive(Debug)] pub struct LiveQueryStore { - queries: HashMap, + queries: IndexMap, query_idle_time: Duration, } @@ -75,7 +75,7 @@ impl LiveQueryStore { /// Construct [`LiveQueryStore`] from configuration. pub fn from_configuration(cfg: Configuration) -> Self { Self { - queries: HashMap::default(), + queries: IndexMap::new(), query_idle_time: Duration::from_millis(cfg.query_idle_time_ms.into()), } } diff --git a/core/src/queue.rs b/core/src/queue.rs index bc3d860cd1f..e19e46f2ba5 100644 --- a/core/src/queue.rs +++ b/core/src/queue.rs @@ -1,10 +1,10 @@ //! Module with queue actor use core::time::Duration; -use std::collections::HashSet; use crossbeam_queue::ArrayQueue; use dashmap::{mapref::entry::Entry, DashMap}; use eyre::{Report, Result}; +use indexmap::IndexSet; use iroha_config::queue::Configuration; use iroha_crypto::HashOf; use iroha_data_model::{account::AccountId, transaction::prelude::*}; @@ -326,7 +326,7 @@ impl Queue { self.pop_from_queue(&mut seen_queue, wsv, &mut expired_transactions_queue) }); - let transactions_hashes: HashSet> = + let transactions_hashes: IndexSet> = transactions.iter().map(|tx| tx.payload().hash()).collect(); let txs = txs_from_queue .filter(|tx| !transactions_hashes.contains(&tx.payload().hash())) diff --git a/core/src/smartcontracts/isi/triggers/set.rs b/core/src/smartcontracts/isi/triggers/set.rs index 624fc2a6acd..b32e49190e1 100644 --- a/core/src/smartcontracts/isi/triggers/set.rs +++ b/core/src/smartcontracts/isi/triggers/set.rs @@ -9,8 +9,9 @@ //! trigger hooks. use core::cmp::min; -use std::{collections::HashMap, fmt}; +use std::fmt; +use indexmap::IndexMap; use iroha_crypto::HashOf; use iroha_data_model::{ events::Filter as EventFilter, @@ -138,17 +139,17 @@ impl + Clone> LoadedActionTrait for Loaded #[derive(Debug, Default)] pub struct Set { /// Triggers using [`DataEventFilter`] - data_triggers: HashMap>, + data_triggers: IndexMap>, /// Triggers using [`PipelineEventFilter`] - pipeline_triggers: HashMap>, + pipeline_triggers: IndexMap>, /// Triggers using [`TimeEventFilter`] - time_triggers: HashMap>, + time_triggers: IndexMap>, /// Triggers using [`ExecuteTriggerEventFilter`] - by_call_triggers: HashMap>, + by_call_triggers: IndexMap>, /// Trigger ids with type of events they process - ids: HashMap, + ids: IndexMap, /// Original [`WasmSmartContract`]s by [`TriggerId`] for querying purposes. - original_contracts: HashMap, WasmSmartContract>, + original_contracts: IndexMap, WasmSmartContract>, /// List of actions that should be triggered by events provided by `handle_*` methods. /// Vector is used to save the exact triggers order. matched_ids: Vec<(Event, TriggerId)>, @@ -157,14 +158,14 @@ pub struct Set { /// Helper struct for serializing triggers. struct TriggersWithContext<'s, F> { /// Triggers being serialized - triggers: &'s HashMap>, + triggers: &'s IndexMap>, /// Containing Set, used for looking up origignal [`WasmSmartContract`]s /// during serialization. set: &'s Set, } impl<'s, F> TriggersWithContext<'s, F> { - fn new(triggers: &'s HashMap>, set: &'s Set) -> Self { + fn new(triggers: &'s IndexMap>, set: &'s Set) -> Self { Self { triggers, set } } } @@ -236,7 +237,7 @@ impl<'de> DeserializeSeed<'de> for WasmSeed<'_, Set> { while let Some(key) = map.next_key::()? { match key.as_str() { "data_triggers" => { - let triggers: HashMap> = + let triggers: IndexMap> = map.next_value()?; for (id, action) in triggers { set.add_data_trigger(self.loader.engine, Trigger::new(id, action)) @@ -244,7 +245,7 @@ impl<'de> DeserializeSeed<'de> for WasmSeed<'_, Set> { } } "pipeline_triggers" => { - let triggers: HashMap> = + let triggers: IndexMap> = map.next_value()?; for (id, action) in triggers { set.add_pipeline_trigger( @@ -255,7 +256,7 @@ impl<'de> DeserializeSeed<'de> for WasmSeed<'_, Set> { } } "time_triggers" => { - let triggers: HashMap> = + let triggers: IndexMap> = map.next_value()?; for (id, action) in triggers { set.add_time_trigger(self.loader.engine, Trigger::new(id, action)) @@ -263,7 +264,7 @@ impl<'de> DeserializeSeed<'de> for WasmSeed<'_, Set> { } } "by_call_triggers" => { - let triggers: HashMap> = + let triggers: IndexMap> = map.next_value()?; for (id, action) in triggers { set.add_by_call_trigger( @@ -387,7 +388,7 @@ impl Set { engine: &wasmtime::Engine, trigger: Trigger, event_type: TriggeringEventType, - map: impl FnOnce(&mut Self) -> &mut HashMap>, + map: impl FnOnce(&mut Self) -> &mut IndexMap>, ) -> Result { if self.contains(trigger.id()) { return Ok(false); @@ -816,8 +817,8 @@ impl Set { /// Remove actions with zero execution count from `triggers` fn remove_zeros( - ids: &mut HashMap, - triggers: &mut HashMap>, + ids: &mut IndexMap, + triggers: &mut IndexMap>, ) { let to_remove: Vec = triggers .iter() diff --git a/core/src/smartcontracts/wasm.rs b/core/src/smartcontracts/wasm.rs index 0d0116dba2f..a7c0b8b757e 100644 --- a/core/src/smartcontracts/wasm.rs +++ b/core/src/smartcontracts/wasm.rs @@ -328,9 +328,8 @@ impl LimitsExecutor { pub mod state { //! All supported states for [`Runtime`](super::Runtime) - use std::collections::HashSet; - use derive_more::Constructor; + use indexmap::IndexSet; use super::*; @@ -360,7 +359,7 @@ pub mod state { pub(super) store_limits: StoreLimits, /// Span inside of which all logs are recorded for this smart contract pub(super) log_span: Span, - pub(super) executed_queries: HashSet, + pub(super) executed_queries: IndexSet, /// Borrowed [`WorldStateView`] kind pub(super) wsv: W, /// Concrete state for specific executable @@ -380,14 +379,14 @@ pub mod state { authority, store_limits: store_limits_from_config(&config), log_span, - executed_queries: HashSet::new(), + executed_queries: IndexSet::new(), wsv, specific_state, } } /// Take executed queries leaving an empty set - pub fn take_executed_queries(&mut self) -> HashSet { + pub fn take_executed_queries(&mut self) -> IndexSet { std::mem::take(&mut self.executed_queries) } } diff --git a/core/src/sumeragi/mod.rs b/core/src/sumeragi/mod.rs index b6f3c7391f0..7066d5e9e05 100644 --- a/core/src/sumeragi/mod.rs +++ b/core/src/sumeragi/mod.rs @@ -110,7 +110,10 @@ impl SumeragiHandle { pub fn update_metrics(&self) -> Result<()> { let online_peers_count: u64 = self .network - .online_peers(std::collections::HashSet::len) + .online_peers( + #[allow(clippy::disallowed_types)] + std::collections::HashSet::len, + ) .try_into() .expect("casting usize to u64"); diff --git a/core/src/sumeragi/network_topology.rs b/core/src/sumeragi/network_topology.rs index 05e92157d3e..4ba77806e45 100644 --- a/core/src/sumeragi/network_topology.rs +++ b/core/src/sumeragi/network_topology.rs @@ -1,7 +1,7 @@ //! Structures formalising the peer topology (e.g. which peers have which predefined roles). -use std::collections::HashSet; use derive_more::Display; +use indexmap::IndexSet; use iroha_crypto::{PublicKey, SignatureOf}; use iroha_data_model::{block::SignedBlock, prelude::PeerId}; use iroha_logger::trace; @@ -88,7 +88,7 @@ impl Topology { roles: &[Role], signatures: I, ) -> Vec> { - let mut public_keys: HashSet<&PublicKey> = HashSet::with_capacity(self.ordered_peers.len()); + let mut public_keys = IndexSet::with_capacity(self.ordered_peers.len()); for role in roles { match (role, self.is_non_empty(), self.is_consensus_required()) { (Role::Leader, Some(topology), _) => { diff --git a/core/src/sumeragi/view_change.rs b/core/src/sumeragi/view_change.rs index 11f24b90cfd..0b0ed73032c 100644 --- a/core/src/sumeragi/view_change.rs +++ b/core/src/sumeragi/view_change.rs @@ -1,9 +1,9 @@ //! Structures related to proofs and reasons of view changes. //! Where view change is a process of changing topology due to some faulty network behavior. -use std::collections::HashSet; use derive_more::{Deref, DerefMut}; use eyre::Result; +use indexmap::IndexSet; use iroha_crypto::{HashOf, KeyPair, PublicKey, SignatureOf, SignaturesOf}; use iroha_data_model::{block::SignedBlock, prelude::PeerId}; use parity_scale_codec::{Decode, Encode}; @@ -76,7 +76,7 @@ impl SignedProof { /// Verify if the proof is valid, given the peers in `topology`. fn verify(&self, peers: &[PeerId], max_faults: usize) -> bool { - let peer_public_keys: HashSet<&PublicKey> = + let peer_public_keys: IndexSet<&PublicKey> = peers.iter().map(|peer_id| &peer_id.public_key).collect(); let valid_count = self diff --git a/core/src/wsv.rs b/core/src/wsv.rs index 13a899845e9..29274a3323e 100644 --- a/core/src/wsv.rs +++ b/core/src/wsv.rs @@ -1,15 +1,12 @@ //! This module provides the [`WorldStateView`] — an in-memory representation of the current blockchain //! state. use std::{ - borrow::Borrow, - collections::{BTreeSet, HashMap}, - fmt::Debug, - marker::PhantomData, - sync::Arc, + borrow::Borrow, collections::BTreeSet, fmt::Debug, marker::PhantomData, sync::Arc, time::Duration, }; use eyre::Result; +use indexmap::IndexMap; use iroha_config::{ base::proxy::Builder, wsv::{Configuration, ConfigurationProxy}, @@ -277,7 +274,7 @@ pub struct WorldStateView { /// Blockchain. pub block_hashes: Vec>, /// Hashes of transactions mapped onto block height where they stored - pub transactions: HashMap, u64>, + pub transactions: IndexMap, u64>, /// Buffer containing events generated during `WorldStateView::apply`. Renewed on every block commit. #[serde(skip)] pub events_buffer: Vec, @@ -942,7 +939,7 @@ impl WorldStateView { Self { world, config, - transactions: HashMap::new(), + transactions: IndexMap::new(), block_hashes: Vec::new(), events_buffer: Vec::new(), new_tx_amounts: Arc::new(Mutex::new(Vec::new())), diff --git a/data_model/clippy.toml b/data_model/clippy.toml new file mode 100644 index 00000000000..ad9bd114bed --- /dev/null +++ b/data_model/clippy.toml @@ -0,0 +1 @@ +disallowed-types = ["std::collections::HashMap", "std::collections::HashSet"] From a2ff9a0716162925dea9fec1405a533fe55c69ea Mon Sep 17 00:00:00 2001 From: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> Date: Fri, 8 Dec 2023 13:58:13 +0700 Subject: [PATCH 06/21] [docs]: remove auto-generated config reference Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- .github/workflows/iroha2-dev-pr-label.yml | 3 +- .github/workflows/iroha2-dev-pr.yml | 3 - CONTRIBUTING.md | 15 +- README.md | 10 +- cli/README.md | 22 +- config/base/derive/src/documented.rs | 263 ------- config/base/derive/src/lib.rs | 12 +- config/base/derive/src/proxy.rs | 4 +- config/base/src/lib.rs | 147 +--- config/base/tests/simple.rs | 206 ------ config/src/block_sync.rs | 4 +- config/src/client.rs | 6 +- config/src/genesis.rs | 4 +- config/src/iroha.rs | 4 +- config/src/kura.rs | 4 +- config/src/live_query_store.rs | 4 +- config/src/logger.rs | 4 +- config/src/network.rs | 4 +- config/src/queue.rs | 4 +- config/src/snapshot.rs | 4 +- config/src/sumeragi.rs | 4 +- config/src/telemetry.rs | 4 +- config/src/torii.rs | 4 +- config/src/wasm.rs | 4 +- config/src/wsv.rs | 4 +- docs/README.md | 3 +- docs/source/references/config.md | 835 ---------------------- hooks/pre-commit.sample | 3 +- scripts/tests/consistency.sh | 5 - tools/kagami/src/docs.rs | 130 ---- tools/kagami/src/main.rs | 4 - 31 files changed, 51 insertions(+), 1676 deletions(-) delete mode 100644 config/base/derive/src/documented.rs delete mode 100644 config/base/tests/simple.rs delete mode 100644 docs/source/references/config.md delete mode 100644 tools/kagami/src/docs.rs diff --git a/.github/workflows/iroha2-dev-pr-label.yml b/.github/workflows/iroha2-dev-pr-label.yml index cbf581bd692..4eb18953a92 100644 --- a/.github/workflows/iroha2-dev-pr-label.yml +++ b/.github/workflows/iroha2-dev-pr-label.yml @@ -5,7 +5,6 @@ on: branches: [iroha-dev] paths: - 'docs/source/references/schema.json' - - 'docs/source/references/config.md' jobs: api-changes: @@ -30,7 +29,7 @@ jobs: continue-on-error: true id: config_label - uses: actions-ecosystem/action-add-labels@v1 - if: contains(steps.config_label.outputs.added_modified, 'docs/source/references/config.md') + if: contains(steps.config_label.outputs.added_modified) with: github_token: ${{ secrets.github_token }} labels: | diff --git a/.github/workflows/iroha2-dev-pr.yml b/.github/workflows/iroha2-dev-pr.yml index b78ede1a64a..9ba479df65d 100644 --- a/.github/workflows/iroha2-dev-pr.yml +++ b/.github/workflows/iroha2-dev-pr.yml @@ -25,9 +25,6 @@ jobs: steps: - uses: actions/checkout@v4 - uses: Swatinem/rust-cache@v2 - - name: Check config.md - if: always() - run: ./scripts/tests/consistency.sh docs - name: Check genesis.json if: always() run: ./scripts/tests/consistency.sh genesis diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4869dd4e4c0..74bafed0f92 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -18,14 +18,13 @@ New to our project? [Make your first contribution](#your-first-code-contribution ### TL;DR -* Find [ZenHub](https://app.zenhub.com/workspaces/iroha-v2-60ddb820813b9100181fc060/board?repos=181739240). -* Fork [Iroha](https://github.com/hyperledger/iroha/tree/iroha2-dev). -* Fix your issue of choice. -* Ensure you follow our [style guides](#style-guides) for code and documentation. -* Write [tests](https://doc.rust-lang.org/cargo/commands/cargo-test.html). Ensure they all pass (`cargo test`). -* Fix [`clippy`](https://lib.rs/crates/cargo-lints) warnings: `cargo lints clippy --workspace --benches --tests --examples --all-features`. -* Format code `cargo +nightly fmt --all` and generate docs `cargo run --bin kagami -- docs >"docs/source/references/config.md" && git add "docs/source/references/config.md"`. -* With the `upstream` set to track [Hyperledger Iroha repository](https://github.com/hyperledger/iroha), `git pull -r upstream iroha2-dev`, `git commit -s`, `git push `, and [create a pull request](https://github.com/hyperledger/iroha/compare) to the `iroha2-dev` branch. Ensure the PR has the `[type] #: Description` [title](#pull-request-titles). +- Find [ZenHub](https://app.zenhub.com/workspaces/iroha-v2-60ddb820813b9100181fc060/board?repos=181739240). +- Fork [Iroha](https://github.com/hyperledger/iroha/tree/iroha2-dev). +- Fix your issue of choice. +- Ensure you follow our [style guides](#style-guides) for code and documentation. +- Write [tests](https://doc.rust-lang.org/cargo/commands/cargo-test.html). Ensure they all pass (`cargo test --workspace`). +- Perform pre-commit routine like formatting & artifacts regeneration (see [`pre-commit.sample`](./hooks/pre-commit.sample)) +- With the `upstream` set to track [Hyperledger Iroha repository](https://github.com/hyperledger/iroha), `git pull -r upstream iroha2-dev`, `git commit -s`, `git push `, and [create a pull request](https://github.com/hyperledger/iroha/compare) to the `iroha2-dev` branch. Ensure the PR has the `[type] #: Description` [title](#pull-request-titles). ### Reporting Bugs diff --git a/README.md b/README.md index 5877ff5f40d..d93882849bc 100644 --- a/README.md +++ b/README.md @@ -157,9 +157,7 @@ A brief overview on how to configure and maintain an Iroha instance: ### Configuration -You can provide configuration parameters either as a `config.json` or using environment variables. Refer to the [detailed list](./docs/source/references/config.md) of all available configuration parameters. - -Configuration example you may use as a reference point: [cli/src/samples.rs](./cli/src/samples.rs) +**Note:** this section is under development. You can track it in the [issue](https://github.com/hyperledger/iroha-2-docs/issues/392). ### Endpoints @@ -169,7 +167,7 @@ For a list of all endpoints, available operations, and ways to customize them wi By default, Iroha provides logs in a human-readable format and prints them out to `stdout`. -The logging level can be changed either via the [`logger.level` configuration parameter](./docs/source/references/config.md#loggerlevel) or at run-time using the `configuration` endpoint. +The logging level can be changed either via the `logger.level` configuration parameter or at run-time using the `configuration` endpoint.
Example: changing log level @@ -182,7 +180,7 @@ curl -X POST \ ```
-The log format might be configured via the [`logger.format` configuration parameter](./docs/source/references/config.md#loggerformat). Possible values are: `full` (default), `compact`, `pretty`, and `json`. +The log format might be configured via the `logger.format` configuration parameter. Possible values are: `full` (default), `compact`, `pretty`, and `json`. Output goes to `/dev/stdout`. Piping to files or [log rotation](https://www.commandlinux.com/man-page/man5/logrotate.conf.5.html) is the responsibility of the peer administrator. @@ -218,7 +216,7 @@ We encourage you to check out our [Iroha 2 Tutorial](https://hyperledger.github. * [Glossary](https://hyperledger.github.io/iroha-2-docs/guide/glossary) * [Iroha Special Instructions](https://hyperledger.github.io/iroha-2-docs/guide/blockchain/instructions) * [API Reference](https://hyperledger.github.io/iroha-2-docs/api/torii-endpoints) -* [Configuration Reference](./docs/source/references/config.md) + * [Iroha 2 Whitepaper](./docs/source/iroha_2_whitepaper.md) Iroha SDKs: diff --git a/cli/README.md b/cli/README.md index 9da57cc943e..c5912212253 100644 --- a/cli/README.md +++ b/cli/README.md @@ -74,27 +74,7 @@ Refer to [generating key pairs with `kagami`](../tools/kagami#crypto) for more d ### Configuration file -You must provide a configuration file to run the Iroha peer binary. Iroha will not run with defaults if the configuration file is not available. - -The Iroha binary looks for either a `config.json` file in the current directory or a JSON file in `IROHA2_CONFIG_PATH`. If the configuration file is not valid, the Iroha peer binary exits and does nothing. If neither of these files is provided, all the fields from the default `config.json` should be specified as environment variables. Note that environment variables override the variables in their respective fields provided via `config.json`. - -The environment variables replacing `config.json` should be passed as JSON strings, meaning that any inner quotes should be properly escaped in the command line as shown in the example below. - -
Expand to see the example - -``` bash -IROHA_TORII="{\"P2P_ADDR\": \"127.0.0.1:1339\", \"API_URL\": \"127.0.0.1:8080\"}" IROHA_SUMERAGI="{\"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\"}]}" IROHA_KURA="{\"INIT_MODE\": \"strict\",\"BLOCK_STORE_PATH\": \"./storage\"}" IROHA_BLOCK_SYNC="{\"GOSSIP_PERIOD_MS\": 10000,\"BATCH_SIZE\": 2}" IROHA_PUBLIC_KEY="ed01201C61FAF8FE94E253B93114240394F79A607B7FA55F9E5A41EBEC74B88055768B" IROHA_PRIVATE_KEY="{\"digest_function\": \"ed25519\",\"payload\": \"282ED9F3CF92811C3818DBC4AE594ED59DC1A2F78E4241E31924E101D6B1FB831C61FAF8FE94E253B93114240394F79A607B7FA55F9E5A41EBEC74B88055768B\"}" IROHA_GENESIS="{\"ACCOUNT_PUBLIC_KEY\": \"ed01204CFFD0EE429B1BDD36B3910EC570852B8BB63F18750341772FB46BC856C5CAAF\",\"ACCOUNT_PRIVATE_KEY\": {\"digest_function\": \"ed25519\",\"payload\": \"D748E18CE60CB30DEA3E73C9019B7AF45A8D465E3D71BCC9A5EF99A008205E534CFFD0EE429B1BDD36B3910EC570852B8BB63F18750341772FB46BC856C5CAAF\"}}" ./iroha -``` - -
- -:grey_exclamation: We do not recommend using environment variables for configuration outside docker-compose and Kubernetes deployments. Please change the values in the configuration file instead. That would also help us debug the problems that you might be having. - -The [configuration options reference](../docs/source/references/config.md) provides detailed explanations of each configuration variable. You may use the [sample configuration file](../configs/peer/config.json) for quick testing. - -One of the peers on your network must be provided with the genesis block, which is either `IROHA2_GENESIS_PATH` or `genesis.json` in the working directory. -Check [configuration options](https://github.com/hyperledger/iroha/blob/iroha2-dev/docs/source/references/config.md#genesis) for details. -Learn more about the genesis block in [our tutorial](https://hyperledger.github.io/iroha-2-docs/guide/configure/genesis.html). +**Note:** this section is under development. You can track it in the [issue](https://github.com/hyperledger/iroha-2-docs/issues/392). ## Deployment diff --git a/config/base/derive/src/documented.rs b/config/base/derive/src/documented.rs deleted file mode 100644 index 604399061fa..00000000000 --- a/config/base/derive/src/documented.rs +++ /dev/null @@ -1,263 +0,0 @@ -use proc_macro::TokenStream; -use proc_macro2::Span; -use quote::quote; -use syn::{parse_quote, Lit, LitStr, Meta, Path}; - -use super::utils::{get_inner_type, StructWithFields}; - -pub fn impl_documented(ast: &StructWithFields) -> TokenStream { - let name = &ast.ident; - let docs = gen_docs(ast); - - let get_docs = impl_get_docs(docs.clone(), ast); - let get_inner_docs = impl_get_inner_docs(docs.clone(), ast); - let get_doc_recursive = impl_get_doc_recursive(docs, ast); - - let get_recursive = impl_get_recursive(ast); - - let out = quote! { - impl ::iroha_config_base::proxy::Documented for #name { - type Error = ::iroha_config_base::derive::Error; - - #get_recursive - #get_doc_recursive - #get_docs - #get_inner_docs - } - }; - out.into() -} - -fn impl_get_doc_recursive(docs: Vec, ast: &StructWithFields) -> proc_macro2::TokenStream { - if ast.fields.is_empty() { - return quote! { - fn get_doc_recursive<'a>( - inner_field: impl AsRef<[&'a str]>, - ) -> core::result::Result, ::iroha_config_base::derive::Error> - { - Err(::iroha_config_base::derive::Error::UnknownField( - ::iroha_config_base::derive::Field( - inner_field.as_ref().iter().map(ToString::to_string).collect() - ))) - } - }; - } - - let variants = ast.fields - .iter() - .zip(docs) - .map(|(field, documentation)| { - let ty = &field.ty; - let ident = &field.ident; - let documented_trait: Path = parse_quote! { iroha_config_base::proxy::Documented }; - if field.has_inner && field.has_option { - let inner_ty = get_inner_type("Option", &field.ty); - quote! { - [stringify!(#ident)] => { - let curr_doc = #documentation; - let inner_docs = <#inner_ty as #documented_trait>::get_inner_docs(); - let total_docs = format!("{}\n\nHas following fields:\n\n{}\n", curr_doc, inner_docs); - Some(total_docs) - }, - [stringify!(#ident), rest @ ..] => <#inner_ty as #documented_trait>::get_doc_recursive(rest)?, - } - } else if field.has_inner { - quote! { - [stringify!(#ident)] => { - let curr_doc = #documentation; - let inner_docs = <#ty as #documented_trait>::get_inner_docs(); - let total_docs = format!("{}\n\nHas following fields:\n\n{}\n", curr_doc, inner_docs); - Some(total_docs) - }, - [stringify!(#ident), rest @ ..] => <#ty as #documented_trait>::get_doc_recursive(rest)?, - } - } else { - quote! { [stringify!(#ident)] => Some(#documentation.to_owned()), } - } - }); - - quote! { - fn get_doc_recursive<'a>( - inner_field: impl AsRef<[&'a str]>, - ) -> core::result::Result, ::iroha_config_base::derive::Error> - { - let inner_field = inner_field.as_ref(); - let doc = match inner_field { - #(#variants)* - field => return Err(::iroha_config_base::derive::Error::UnknownField( - ::iroha_config_base::derive::Field( - field.iter().map(ToString::to_string).collect() - ))), - }; - Ok(doc) - } - } -} - -fn impl_get_inner_docs(docs: Vec, ast: &StructWithFields) -> proc_macro2::TokenStream { - let inserts = ast.fields.iter().zip(docs).map(|(field, documentation)| { - let ty = &field.ty; - let ident = &field.ident; - let documented_trait: Path = parse_quote! { ::iroha_config_base::proxy::Documented }; - let doc = if field.has_inner && field.has_option { - let inner_ty = get_inner_type("Option", &field.ty); - quote! { - <#inner_ty as #documented_trait>::get_inner_docs().as_str() - } - } else if field.has_inner { - quote! { <#ty as #documented_trait>::get_inner_docs().as_str() } - } else { - quote! { #documentation.into() } - }; - - quote! { - inner_docs.push_str(stringify!(#ident)); - inner_docs.push_str(": "); - inner_docs.push_str(#doc); - inner_docs.push_str("\n\n"); - } - }); - - quote! { - fn get_inner_docs() -> String { - let mut inner_docs = String::new(); - #(#inserts)* - inner_docs - } - } -} - -fn impl_get_docs(docs: Vec, ast: &StructWithFields) -> proc_macro2::TokenStream { - let inserts = ast.fields.iter().zip(docs).map(|(field, documentation)| { - let ident = &field.ident; - let ty = &field.ty; - let documented_trait: Path = parse_quote! { iroha_config_base::proxy::Documented }; - let doc = if field.has_inner && field.has_option { - let inner_ty = get_inner_type("Option", &field.ty); - quote! { <#inner_ty as #documented_trait>::get_docs().into() } - } else if field.has_inner { - quote! { <#ty as #documented_trait>::get_docs().into() } - } else { - quote! { #documentation.into() } - }; - - quote! { map.insert(stringify!(#ident).to_owned(), #doc); } - }); - - quote! { - fn get_docs() -> serde_json::Value { - let mut map = serde_json::Map::new(); - #(#inserts)* - map.into() - } - } -} - -fn impl_get_recursive(ast: &StructWithFields) -> proc_macro2::TokenStream { - if ast.fields.is_empty() { - return quote! { - fn get_recursive<'a, T>( - &self, - inner_field: T, - ) -> ::iroha_config_base::BoxedFuture<'a, core::result::Result> - where - T: AsRef<[&'a str]> + Send + 'a, - { - Err(::iroha_config_base::derive::Error::UnknownField( - ::iroha_config_base::derive::Field( - inner_field.as_ref().iter().map(ToString::to_string).collect() - ))) - } - }; - } - - let variants = ast.fields - .iter() - .map(|field | { - let ident = &field.ident; - let l_value = &field.lvalue_read; - let inner_thing2 = if field.has_inner && field.has_option { - let inner_ty = get_inner_type("Option", &field.ty); - let documented_trait: Path = parse_quote! { iroha_config_base::proxy::Documented }; - quote! { - [stringify!(#ident), rest @ ..] => { - <#inner_ty as #documented_trait>::get_recursive(#l_value.as_ref().expect("Should be instantiated"), rest)? - }, - } - } else if field.has_inner { - quote! { - [stringify!(#ident), rest @ ..] => { - #l_value.get_recursive(rest)? - }, - } - } else { - quote! {} - }; - quote! { - [stringify!(#ident)] => { - serde_json::to_value(&#l_value) - .map_err( - |error| - ::iroha_config_base::derive::Error::field_deserialization_from_json( - stringify!(#ident), - &error - ) - )? - } - #inner_thing2 - } - }); - - quote! { - fn get_recursive<'a, T>( - &self, - inner_field: T, - ) -> core::result::Result - where - T: AsRef<[&'a str]> + Send + 'a, - { - let inner_field = inner_field.as_ref(); - let value = match inner_field { - #(#variants)* - field => return Err(::iroha_config_base::derive::Error::UnknownField( - ::iroha_config_base::derive::Field( - field.iter().map(ToString::to_string).collect() - ))), - }; - Ok(value) - } - } -} - -/// Generate documentation for all fields based on their type and already existing documentation -pub fn gen_docs(ast: &StructWithFields) -> Vec { - ast.fields - .iter() - .map(|field| { - let field_ty = &field.ty; - let env = &field.env_str; - let real_doc = field - .attrs - .iter() - .filter_map(|attr| attr.parse_meta().ok()) - .find_map(|metadata| { - if let Meta::NameValue(meta) = metadata { - if meta.path.is_ident("doc") { - if let Lit::Str(s) = meta.lit { - return Some(s); - } - } - } - None - }); - let real_doc = real_doc.map(|doc| doc.value() + "\n\n").unwrap_or_default(); - let docs = format!( - "{}Has type `{}`[^1]. Can be configured via environment variable `{}`", - real_doc, - quote! { #field_ty }.to_string().replace(' ', ""), - env - ); - LitStr::new(&docs, Span::mixed_site()) - }) - .collect::>() -} diff --git a/config/base/derive/src/lib.rs b/config/base/derive/src/lib.rs index f86d6af896b..0cd24e4e345 100644 --- a/config/base/derive/src/lib.rs +++ b/config/base/derive/src/lib.rs @@ -2,13 +2,12 @@ use proc_macro::TokenStream; -pub(crate) mod documented; 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)] +#[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) @@ -37,19 +36,12 @@ pub fn load_from_disk_derive(input: TokenStream) -> TokenStream { } /// Derive for config querying and setting. More details in `iroha_config_base` reexport -#[proc_macro_derive(Proxy)] +#[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) } -/// Derive for config querying and setting. More details in `iroha_config_base` reexport -#[proc_macro_derive(Documented, attributes(config))] -pub fn documented_derive(input: TokenStream) -> TokenStream { - let ast = syn::parse_macro_input!(input as utils::StructWithFields); - documented::impl_documented(&ast) -} - /// Generate view for given struct and convert from type to its view. /// More details in `iroha_config_base` reexport. #[proc_macro] diff --git a/config/base/derive/src/proxy.rs b/config/base/derive/src/proxy.rs index 7a1e170f2e5..dafef4c6145 100644 --- a/config/base/derive/src/proxy.rs +++ b/config/base/derive/src/proxy.rs @@ -14,7 +14,6 @@ pub fn impl_proxy(ast: StructWithFields) -> TokenStream { 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 }; - let documented_derive = quote! { ::iroha_config_base::derive::Documented }; quote! { /// Proxy configuration structure to be used as an intermediate /// for configuration loading. Both loading from disk and @@ -24,8 +23,7 @@ pub fn impl_proxy(ast: StructWithFields) -> TokenStream { #builder_derive, #loadenv_derive, #disk_derive, - #override_derive, - #documented_derive + #override_derive )] #[builder(parent = #parent_ty)] #proxy_struct diff --git a/config/base/src/lib.rs b/config/base/src/lib.rs index d8f40d64c21..7ea61d35ddb 100644 --- a/config/base/src/lib.rs +++ b/config/base/src/lib.rs @@ -2,7 +2,6 @@ use std::{fmt::Debug, path::Path}; use serde::{de::DeserializeOwned, Deserialize, Deserializer, Serialize}; -use serde_json::Value; pub mod derive { //! Derives for configuration entities @@ -100,34 +99,6 @@ pub mod derive { /// ``` pub use iroha_config_derive::Builder; /// Derive macro for implementing the trait - /// [`iroha_config::base::proxy::Documented`](`crate::proxy::Documented`) - /// for config structures. - /// - /// Even though this macro doesn't own any attributes, as of now - /// it relies on the `#[config]` attribute defined by the - /// [`iroha_config::base::derive::Override`](`crate::derive::Override`) - /// macro. As such, `#[config(env_prefix = ...)]` is required for - /// generating documentation, and `#[config(inner)]` for getting - /// inner fields recursively. - /// - /// # Examples - /// - /// ```rust - /// use iroha_config_base::derive::Documented; - /// use iroha_config_base::proxy::Documented as _; - /// - /// #[derive(serde::Deserialize, serde::Serialize, Documented)] - /// struct Outer { #[config(inner)] inner: Inner } - /// - /// #[derive(serde::Deserialize, serde::Serialize, Documented)] - /// struct Inner { b: String } - /// - /// let outer = Outer { inner: Inner { b: "a".to_owned() }}; - /// - /// assert_eq!(outer.get_recursive(["inner", "b"]).unwrap(), "a"); - /// ``` - pub use iroha_config_derive::Documented; - /// Derive macro for implementing the trait /// [`iroha_config::base::proxy::LoadFromDisk`](`crate::proxy::LoadFromDisk`) /// trait for config structures. /// @@ -272,38 +243,10 @@ pub mod derive { /// (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`)). - /// - /// # Examples - /// - /// ```rust - /// use iroha_config_base::derive::{Documented, Proxy}; - /// - /// // Need `Documented` here as it owns the `#[config]` attribute - /// #[derive(serde::Deserialize, serde::Serialize, Documented, Proxy)] - /// struct Outer { #[config(inner)] inner: Inner } - /// - /// #[derive(serde::Deserialize, serde::Serialize, Documented, Proxy)] - /// struct Inner { b: String } - /// - /// // Will generate something like this - /// // #[derive(Debug, Clone, serde::Deserialize, serde::Serialize, - /// // Builder, Override, Documented, LoadFromEnv, LoadFromDisk)] - /// // #[builder(parent = Outer)] - /// // struct OuterProxy { #[config(inner)] inner: Option } - /// - /// // #[derive(Debug, PartialEq, serde::Deserialize, serde::Serialize, - /// // Builder, Override, Documented, LoadFromEnv, LoadFromDisk)] - /// // struct InnerProxy { b: Option } - /// ``` pub use iroha_config_derive::Proxy; use serde::Deserialize; use thiserror::Error; - // TODO: use VERGEN to point to LTS reference on LTS branch - /// Reference to the current Dev branch configuration - pub static CONFIG_REFERENCE: &str = - "https://github.com/hyperledger/iroha/blob/iroha2-dev/docs/source/references/config.md"; - /// Represents a path to a nested field in a config structure #[derive(Debug, Deserialize)] #[serde(transparent)] @@ -322,14 +265,9 @@ pub mod derive { #[ignore_extra_doc_attributes] #[allow(clippy::enum_variant_names)] pub enum Error { - /// Got unknown field: `{0}` - /// - /// Used in [`Documented`] trait for wrong query errors - UnknownField(Field), - /// Failed to deserialize the field `{field}` /// - /// Used in [`Documented`] and [`super::proxy::LoadFromEnv`] trait for deserialization + /// Used in [`super::proxy::LoadFromEnv`] trait for deserialization /// errors #[serde(skip)] FieldDeserialization { @@ -408,14 +346,6 @@ pub mod derive { } } } - - #[test] - fn unknown_field_fmt() { - assert_eq!( - Error::UnknownField(Field(vec!["a".into(), "b".into()])).to_string(), - "Got unknown field: `a.b`" - ); - } } pub mod view { @@ -450,81 +380,6 @@ pub mod proxy { use super::*; - /// Trait for dynamic and asynchronous configuration via - /// maintenance endpoint for Rust structures - pub trait Documented: Serialize + DeserializeOwned { - /// Error type returned by methods of this trait - type Error; - - /// Return documentation for all fields in a form of a JSON object - fn get_docs() -> Value; - - /// Get inner documentation for non-leaf fields - fn get_inner_docs() -> String; - - /// Return the JSON value of a given field - /// - /// # Errors - /// Fails if field was unknown - #[inline] - fn get(&self, field: &'_ str) -> Result { - self.get_recursive([field]) - } - - /// Get documentation of a given field - /// - /// # Errors - /// Fails if field was unknown - #[inline] - fn get_doc(field: &str) -> Result, Self::Error> { - Self::get_doc_recursive([field]) - } - - /// Return the JSON value of a given inner field of arbitrary - /// inner depth - /// - /// # Errors - /// Fails if field was unknown - fn get_recursive<'tl, T>(&self, inner_field: T) -> Result - where - T: AsRef<[&'tl str]> + Send + 'tl; - - #[allow(single_use_lifetimes)] // Unstable - /// Get documentation of a given inner field of arbitrary depth - /// - /// # Errors - /// Fails if field was unknown - fn get_doc_recursive<'tl>( - field: impl AsRef<[&'tl str]>, - ) -> Result, Self::Error>; - } - - impl Documented for Box { - type Error = T::Error; - - fn get_docs() -> Value { - T::get_docs() - } - - fn get_inner_docs() -> String { - T::get_inner_docs() - } - - fn get_recursive<'tl, U>(&self, inner_field: U) -> Result - where - U: AsRef<[&'tl str]> + Send + 'tl, - { - T::get_recursive(self, inner_field) - } - - #[allow(single_use_lifetimes)] // False-positive - fn get_doc_recursive<'tl>( - field: impl AsRef<[&'tl str]>, - ) -> Result, Self::Error> { - T::get_doc_recursive(field) - } - } - /// Trait for combining two configuration instances pub trait Override: Serialize + DeserializeOwned + Sized { /// If any of the fields in `other` are filled, they diff --git a/config/base/tests/simple.rs b/config/base/tests/simple.rs deleted file mode 100644 index 4de93ca3632..00000000000 --- a/config/base/tests/simple.rs +++ /dev/null @@ -1,206 +0,0 @@ -use std::{collections::HashMap, env::VarError, ffi::OsStr}; - -use iroha_config_base::{ - derive::{Documented, LoadFromEnv, Override}, - proxy::{Documented as _, FetchEnv, LoadFromEnv as _, Override as _}, -}; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, Deserialize, Serialize, LoadFromEnv, Override)] -#[config(env_prefix = "CONF_")] -struct ConfigurationProxy { - /// Inner structure - #[config(inner)] - inner: Option, - #[config(serde_as_str)] - pub string_wrapper: Option, - pub string: Option, - pub data: Option, -} - -#[derive(Clone, Debug, Deserialize, Serialize, Documented)] -#[config(env_prefix = "CONF_")] -struct Configuration { - /// Inner structure - #[config(inner)] - inner: InnerConfiguration, - #[config(serde_as_str)] - pub string_wrapper: StringWrapper, - pub string: String, - pub data: Data, -} - -impl ConfigurationProxy { - fn new_with_placeholders() -> Self { - Self { - inner: Some(InnerConfigurationProxy { - a: Some("string".to_owned()), - b: Some(42), - }), - string_wrapper: Some(StringWrapper("string".to_owned())), - string: Some("cool string".to_owned()), - data: Some(Data { - key: "key".to_owned(), - value: 34, - }), - } - } - - fn new_with_none() -> Self { - Self { - inner: None, - string_wrapper: None, - string: None, - data: None, - } - } -} - -#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, LoadFromEnv, Override)] -#[config(env_prefix = "CONF_INNER_")] -struct InnerConfigurationProxy { - pub a: Option, - // From expression - /// Docs from b - pub b: Option, -} - -#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Documented)] -#[config(env_prefix = "CONF_INNER_")] -struct InnerConfiguration { - pub a: String, - // From expression - /// Docs from b - pub b: i32, -} -#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] -struct Data { - key: String, - value: u64, -} - -#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] -struct StringWrapper(String); - -#[test] -fn test_docs() { - assert_eq!( - Configuration::get_doc_recursive(["inner", "b"]).unwrap(), - Some(" Docs from b\n\nHas type `i32`[^1]. Can be configured via environment variable `CONF_INNER_B`".to_owned()) - ); - assert_eq!( - Configuration::get_doc_recursive(["inner", "a"]).unwrap(), - Some( - "Has type `String`[^1]. Can be configured via environment variable `CONF_INNER_A`" - .to_owned() - ) - ); - assert_eq!( - Configuration::get_doc_recursive(["inner"]).unwrap(), - Some(" Inner structure\n\nHas type `InnerConfiguration`[^1]. Can be configured via environment variable `CONF_INNER`\n\nHas following fields:\n\na: Has type `String`[^1]. Can be configured via environment variable `CONF_INNER_A`\n\nb: Docs from b\n\nHas type `i32`[^1]. Can be configured via environment variable `CONF_INNER_B`\n\n\n".to_owned()) - ); -} - -struct TestEnv { - map: HashMap, -} - -impl TestEnv { - fn new() -> Self { - Self { - map: HashMap::new(), - } - } - - fn set_var(&mut self, key: impl AsRef, value: impl AsRef) { - self.map - .insert(key.as_ref().to_owned(), value.as_ref().to_owned()); - } - - fn remove_var(&mut self, key: impl AsRef) { - self.map.remove(key.as_ref()); - } -} - -impl FetchEnv for TestEnv { - fn fetch>(&self, key: K) -> Result { - self.map - .get( - key.as_ref() - .to_str() - .ok_or_else(|| VarError::NotUnicode(key.as_ref().to_owned()))?, - ) - .ok_or(VarError::NotPresent) - .map(Clone::clone) - } -} - -fn test_env_factory() -> TestEnv { - let string_wrapper_json = "string"; - let string = "cool string"; - let data_json = r#"{"key": "key", "value": 34}"#; - let inner_json = r#"{"a": "", "b": 0}"#; - let mut env = TestEnv::new(); - env.set_var("CONF_STRING_WRAPPER", string_wrapper_json); - env.set_var("CONF_STRING", string); - env.set_var("CONF_DATA", data_json); - env.set_var("CONF_OPTIONAL_STRING_WRAPPER", string_wrapper_json); - env.set_var("CONF_OPTIONAL_STRING", string); - env.set_var("CONF_OPTIONAL_DATA", data_json); - env.set_var("CONF_OPTIONAL_INNER", inner_json); - env.set_var("CONF_INNER_A", "string"); - env.set_var("CONF_INNER_B", "42"); - env -} - -#[test] -fn test_proxy_load_from_env() { - let config = ConfigurationProxy::new_with_placeholders(); - let env_config = ConfigurationProxy::from_env(&test_env_factory()).expect("valid env"); - assert_eq!(&env_config.data, &config.data); - assert_eq!(&env_config.string_wrapper, &config.string_wrapper); - assert_eq!(&env_config.string, &config.string); - assert_eq!(&env_config.inner, &config.inner); -} - -#[test] -fn test_can_load_inner_without_the_wrapping_config() { - let mut env = test_env_factory(); - env.remove_var("CONF_OPTIONAL_INNER"); - let config = ConfigurationProxy::new_with_placeholders(); - let env_config = ConfigurationProxy::from_env(&env).expect("valid env"); - assert_eq!(&env_config.inner, &config.inner); -} - -#[test] -fn test_proxy_combine_does_not_overload_with_none() { - let config = ConfigurationProxy::new_with_none(); - let env_config = ConfigurationProxy::from_env(&test_env_factory()).expect("valid env"); - let combine_config = env_config.clone().override_with(config); - assert_eq!(&env_config.data, &combine_config.data); -} - -#[test] -fn configuration_proxy_from_env_returns_err_on_parsing_error() { - #[derive(LoadFromEnv, Debug)] - #[config(env_prefix = "")] - struct Target { - #[allow(dead_code)] - foo: Option, - } - - struct Env; - - impl FetchEnv for Env { - fn fetch>(&self, key: K) -> Result { - match key.as_ref().to_str().unwrap() { - "FOO" => Ok("not u64 for sure".to_owned()), - _ => Err(VarError::NotPresent), - } - } - } - - let err = Target::from_env(&Env).expect_err("Must not be parsed"); - let err = eyre::Report::new(err); - assert_eq!(format!("{err:?}"), "Failed to deserialize the field `FOO`\n\nCaused by:\n JSON5: --> 1:1\n |\n 1 | not u64 for sure\n | ^---\n |\n = expected array, boolean, null, number, object, or string\n\nLocation:\n config/base/tests/simple.rs:204:15"); -} diff --git a/config/src/block_sync.rs b/config/src/block_sync.rs index 6802fcce9c9..dd927df3ece 100644 --- a/config/src/block_sync.rs +++ b/config/src/block_sync.rs @@ -1,5 +1,5 @@ //! Module for `BlockSynchronizer`-related configuration and structs. -use iroha_config_base::derive::{Documented, Proxy}; +use iroha_config_base::derive::Proxy; use serde::{Deserialize, Serialize}; const DEFAULT_BLOCK_BATCH_SIZE: u32 = 4; @@ -7,7 +7,7 @@ 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, Documented, Proxy)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, Proxy)] #[serde(rename_all = "UPPERCASE")] #[config(env_prefix = "BLOCK_SYNC_")] pub struct Configuration { diff --git a/config/src/client.rs b/config/src/client.rs index 312bb2a1737..a9238879cac 100644 --- a/config/src/client.rs +++ b/config/src/client.rs @@ -4,7 +4,7 @@ use std::num::NonZeroU64; use derive_more::Display; use eyre::{Result, WrapErr}; -use iroha_config_base::derive::{Documented, Error as ConfigError, Proxy}; +use iroha_config_base::derive::{Error as ConfigError, Proxy}; use iroha_crypto::prelude::*; use iroha_data_model::{prelude::*, transaction::TransactionLimits}; use iroha_primitives::small::SmallStr; @@ -56,7 +56,7 @@ impl<'de> Deserialize<'de> for WebLogin { } /// Basic Authentication credentials -#[derive(Clone, Deserialize, Serialize, Debug, Documented, PartialEq, Eq)] +#[derive(Clone, Deserialize, Serialize, Debug, PartialEq, Eq)] pub struct BasicAuth { /// Login for Basic Authentication pub web_login: WebLogin, @@ -65,7 +65,7 @@ pub struct BasicAuth { } /// `Configuration` provides an ability to define client parameters such as `TORII_URL`. -#[derive(Debug, Clone, Deserialize, Serialize, Proxy, Documented, PartialEq, Eq)] +#[derive(Debug, Clone, Deserialize, Serialize, Proxy, PartialEq, Eq)] #[serde(rename_all = "UPPERCASE")] #[config(env_prefix = "IROHA_")] pub struct Configuration { diff --git a/config/src/genesis.rs b/config/src/genesis.rs index fe51c5e33a3..2bb9e8d892b 100644 --- a/config/src/genesis.rs +++ b/config/src/genesis.rs @@ -1,12 +1,12 @@ //! Module with genesis configuration logic. -use iroha_config_base::derive::{view, Documented, Proxy}; +use iroha_config_base::derive::{view, Proxy}; use iroha_crypto::{PrivateKey, PublicKey}; 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, Documented, Proxy)] + #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Proxy)] #[serde(rename_all = "UPPERCASE")] #[config(env_prefix = "IROHA_GENESIS_")] pub struct Configuration { diff --git a/config/src/iroha.rs b/config/src/iroha.rs index 0ade3128196..6fc69f5069b 100644 --- a/config/src/iroha.rs +++ b/config/src/iroha.rs @@ -1,7 +1,7 @@ //! This module contains [`struct@Configuration`] structure and related implementation. use std::fmt::Debug; -use iroha_config_base::derive::{view, Documented, Error as ConfigError, Proxy}; +use iroha_config_base::derive::{view, Error as ConfigError, Proxy}; use iroha_crypto::prelude::*; use serde::{Deserialize, Serialize}; @@ -10,7 +10,7 @@ use super::*; // Generate `ConfigurationView` without the private key view! { /// Configuration parameters for a peer - #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Proxy, Documented)] + #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Proxy)] #[serde(rename_all = "UPPERCASE")] #[config(env_prefix = "IROHA_")] pub struct Configuration { diff --git a/config/src/kura.rs b/config/src/kura.rs index 03c0cb4fb74..5ce29c4ce95 100644 --- a/config/src/kura.rs +++ b/config/src/kura.rs @@ -1,13 +1,13 @@ //! Module for kura-related configuration and structs use eyre::Result; -use iroha_config_base::derive::{Documented, Proxy}; +use iroha_config_base::derive::Proxy; use serde::{Deserialize, Serialize}; const DEFAULT_BLOCK_STORE_PATH: &str = "./storage"; /// `Kura` configuration. -#[derive(Clone, Deserialize, Serialize, Debug, Documented, Proxy, PartialEq, Eq)] +#[derive(Clone, Deserialize, Serialize, Debug, Proxy, PartialEq, Eq)] #[serde(rename_all = "UPPERCASE")] #[config(env_prefix = "KURA_")] pub struct Configuration { diff --git a/config/src/live_query_store.rs b/config/src/live_query_store.rs index 79382fee2ca..de8b2a31ec2 100644 --- a/config/src/live_query_store.rs +++ b/config/src/live_query_store.rs @@ -2,7 +2,7 @@ use std::num::NonZeroU64; -use iroha_config_base::derive::{Documented, Proxy}; +use iroha_config_base::derive::Proxy; use serde::{Deserialize, Serialize}; /// Default max time a query can remain in the store unaccessed @@ -10,7 +10,7 @@ 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, Documented, Proxy)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize, Serialize, Proxy)] #[serde(rename_all = "UPPERCASE")] #[config(env_prefix = "LIVE_QUERY_STORE_")] pub struct Configuration { diff --git a/config/src/logger.rs b/config/src/logger.rs index d3c8e79472a..6d5e4e9d5e6 100644 --- a/config/src/logger.rs +++ b/config/src/logger.rs @@ -2,7 +2,7 @@ //! configuration, as well as run-time reloading of the log-level. use core::fmt::Debug; -use iroha_config_base::derive::{Documented, Proxy}; +use iroha_config_base::derive::Proxy; pub use iroha_data_model::Level; #[cfg(feature = "tokio-console")] use iroha_primitives::addr::{socket_addr, SocketAddr}; @@ -23,7 +23,7 @@ pub fn into_tracing_level(level: Level) -> tracing::Level { } /// 'Logger' configuration. -#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Proxy, Documented)] +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Proxy)] #[serde(rename_all = "UPPERCASE")] #[config(env_prefix = "LOG_")] // `tokio_console_addr` is not `Copy`, but warning appears without `tokio-console` feature diff --git a/config/src/network.rs b/config/src/network.rs index e5c5ec48e41..845743fac42 100644 --- a/config/src/network.rs +++ b/config/src/network.rs @@ -1,11 +1,11 @@ //! Module for network-related configuration and structs -use iroha_config_base::derive::{Documented, Proxy}; +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, Documented, Proxy)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, Proxy)] #[serde(rename_all = "UPPERCASE")] #[config(env_prefix = "IROHA_NETWORK_")] pub struct Configuration { diff --git a/config/src/queue.rs b/config/src/queue.rs index 3dde85d60d1..5803e90ed7c 100644 --- a/config/src/queue.rs +++ b/config/src/queue.rs @@ -1,5 +1,5 @@ //! Module for `Queue`-related configuration and structs. -use iroha_config_base::derive::{Documented, Proxy}; +use iroha_config_base::derive::Proxy; use serde::{Deserialize, Serialize}; const DEFAULT_MAX_TRANSACTIONS_IN_QUEUE: u32 = 2_u32.pow(16); @@ -8,7 +8,7 @@ const DEFAULT_TRANSACTION_TIME_TO_LIVE_MS: u64 = 24 * 60 * 60 * 1000; // 24 hour const DEFAULT_FUTURE_THRESHOLD_MS: u64 = 1000; /// `Queue` configuration. -#[derive(Copy, Clone, Deserialize, Serialize, Debug, Documented, Proxy, PartialEq, Eq)] +#[derive(Copy, Clone, Deserialize, Serialize, Debug, Proxy, PartialEq, Eq)] #[serde(rename_all = "UPPERCASE")] #[config(env_prefix = "QUEUE_")] pub struct Configuration { diff --git a/config/src/snapshot.rs b/config/src/snapshot.rs index 484dfb7de3e..ea949340767 100644 --- a/config/src/snapshot.rs +++ b/config/src/snapshot.rs @@ -1,6 +1,6 @@ //! Module for `SnapshotMaker`-related configuration and structs. -use iroha_config_base::derive::{Documented, Proxy}; +use iroha_config_base::derive::Proxy; use serde::{Deserialize, Serialize}; const DEFAULT_SNAPSHOT_PATH: &str = "./storage"; @@ -9,7 +9,7 @@ const DEFAULT_SNAPSHOT_CREATE_EVERY_MS: u64 = 1000 * 60; const DEFAULT_ENABLED: bool = true; /// Configuration for `SnapshotMaker`. -#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Documented, Proxy)] +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Proxy)] #[serde(rename_all = "UPPERCASE")] #[config(env_prefix = "SNAPSHOT_")] pub struct Configuration { diff --git a/config/src/sumeragi.rs b/config/src/sumeragi.rs index c6929d441d6..18698d634c6 100644 --- a/config/src/sumeragi.rs +++ b/config/src/sumeragi.rs @@ -2,7 +2,7 @@ use std::{fmt::Debug, fs::File, io::BufReader, path::Path}; use eyre::{Result, WrapErr}; -use iroha_config_base::derive::{view, Documented, Proxy}; +use iroha_config_base::derive::{view, Proxy}; use iroha_crypto::prelude::*; use iroha_data_model::prelude::*; use iroha_primitives::{unique_vec, unique_vec::UniqueVec}; @@ -36,7 +36,7 @@ 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, Documented)] + #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Proxy)] #[serde(rename_all = "UPPERCASE")] #[config(env_prefix = "SUMERAGI_")] pub struct Configuration { diff --git a/config/src/telemetry.rs b/config/src/telemetry.rs index 3b04f79c483..b7ce10f9ee4 100644 --- a/config/src/telemetry.rs +++ b/config/src/telemetry.rs @@ -1,12 +1,12 @@ //! Module for telemetry-related configuration and structs. use std::path::PathBuf; -use iroha_config_base::derive::{Documented, Proxy}; +use iroha_config_base::derive::Proxy; use serde::{Deserialize, Serialize}; use url::Url; /// Configuration parameters container -#[derive(Clone, Deserialize, Serialize, Debug, Proxy, Documented, PartialEq, Eq)] +#[derive(Clone, Deserialize, Serialize, Debug, Proxy, PartialEq, Eq)] #[serde(rename_all = "UPPERCASE")] #[config(env_prefix = "TELEMETRY_")] pub struct Configuration { diff --git a/config/src/torii.rs b/config/src/torii.rs index 1c2b801e981..7dea529aa54 100644 --- a/config/src/torii.rs +++ b/config/src/torii.rs @@ -1,6 +1,6 @@ //! `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::{Documented, Proxy}; +use iroha_config_base::derive::Proxy; use iroha_primitives::addr::{socket_addr, SocketAddr}; use serde::{Deserialize, Serialize}; @@ -14,7 +14,7 @@ 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, Documented, Proxy)] +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Proxy)] #[serde(rename_all = "UPPERCASE")] #[config(env_prefix = "TORII_")] pub struct Configuration { diff --git a/config/src/wasm.rs b/config/src/wasm.rs index 0528da996ed..9e49f8d9391 100644 --- a/config/src/wasm.rs +++ b/config/src/wasm.rs @@ -1,5 +1,5 @@ //! Module for wasm-related configuration and structs. -use iroha_config_base::derive::{Documented, Proxy}; +use iroha_config_base::derive::Proxy; use serde::{Deserialize, Serialize}; use self::default::*; @@ -13,7 +13,7 @@ pub mod default { } /// `WebAssembly Runtime` configuration. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, Documented, Proxy)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, Proxy)] #[config(env_prefix = "WASM_")] #[serde(rename_all = "UPPERCASE")] pub struct Configuration { diff --git a/config/src/wsv.rs b/config/src/wsv.rs index aacc58734be..dcb23b23d85 100644 --- a/config/src/wsv.rs +++ b/config/src/wsv.rs @@ -1,6 +1,6 @@ //! Module for `WorldStateView`-related configuration and structs. use default::*; -use iroha_config_base::derive::{Documented, Proxy}; +use iroha_config_base::derive::Proxy; use iroha_data_model::{prelude::*, transaction::TransactionLimits}; use serde::{Deserialize, Serialize}; @@ -26,7 +26,7 @@ pub mod default { } /// `WorldStateView` configuration. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, Proxy, Documented)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, Proxy)] #[config(env_prefix = "WSV_")] #[serde(rename_all = "UPPERCASE")] pub struct Configuration { diff --git a/docs/README.md b/docs/README.md index 11e941c5e8f..b8aa69d85a4 100644 --- a/docs/README.md +++ b/docs/README.md @@ -4,7 +4,7 @@ This is the main Iroha 2 documentation that you will find useful: - [Tutorial](https://hyperledger.github.io/iroha-2-docs/) - [API Reference](https://hyperledger.github.io/iroha-2-docs/api/torii-endpoints) -- [Configuration Reference](./source/references/config.md) + - [Iroha 2 Whitepaper](./source/iroha_2_whitepaper.md) ## Tools @@ -18,5 +18,6 @@ Documentation for Iroha 2 tools: ## Development The following is useful for development: + - [Hot reload Iroha in a Docker container](./source/guides/hot-reload.md) - [Benchmark your code](../client/benches/tps/README.md) diff --git a/docs/source/references/config.md b/docs/source/references/config.md deleted file mode 100644 index 82b00f0bc54..00000000000 --- a/docs/source/references/config.md +++ /dev/null @@ -1,835 +0,0 @@ -# Iroha Configuration reference - -In this document we provide a reference and detailed descriptions of Iroha's configuration options. The options have different underlying types and default values, which are denoted in code as types wrapped in a single `Option<..>` or in a double `Option>`. For the detailed explanation, please refer to this [section](#configuration-types). - -## Configuration types - -### `Option<..>` - -A type wrapped in a single `Option<..>` signifies that in the corresponding `json` block there is a fallback value for this type, and that it only serves as a reference. If a default for such a type has a `null` value, it means that there is no meaningful fallback available for this particular value. - -All the default values can be freely obtained from a provided [sample configuration file](../../../configs/peer/config.json), but it should only serve as a starting point. If left unchanged, the sample configuration file would still fail to build due to it having `null` in place of [public](#public_key) and [private](#private_key) keys as well as [API endpoint URL](#torii.api_url). These should be provided either by modifying the sample config file or as environment variables. No other overloading of configuration values happens besides reading them from a file and capturing the environment variables. - -For both types of configuration options wrapped in a single `Option<..>` (i.e. both those that have meaningful defaults and those that have `null`), failure to provide them in any of the above two ways results in an error. - -### `Option>` - -`Option>` types should be distinguished from types wrapped in a single `Option<..>`. Only the double option ones are allowed to stay `null`, meaning that **not** providing them in an environment variable or a file will **not** result in an error. - -Thus, only these types are truly optional in the mundane sense of the word. An example of this distinction is genesis [public](#genesis.account_public_key) and [private](#genesis.account_private_key) key. While the first one is a single `Option<..>` wrapped type, the latter is wrapped in `Option>`. This means that the genesis *public* key should always be provided by the user, be it via a file config or an environment variable, whereas the *private* key is only needed for the peer that submits the genesis block, and can be omitted for all others. The same logic goes for other double option fields such as logger file path. - -### Sumeragi: default `null` values - -A special note about sumeragi fields with `null` as default: only the [`trusted_peers`](#sumeragi.trusted_peers) field out of the three can be initialized via a provided file or an environment variable. - -The other two fields, namely [`key_pair`](#sumeragi.key_pair) and [`peer_id`](#sumeragi.peer_id), go through a process of finalization where their values are derived from the corresponding ones in the uppermost Iroha config (using its [`public_key`](#public_key) and [`private_key`](#private_key) fields) or the Torii config (via its [`p2p_addr`](#torii.p2p_addr)). This ensures that these linked fields stay in sync, and prevents the programmer error when different values are provided to these field pairs. Providing either `sumeragi.key_pair` or `sumeragi.peer_id` by hand will result in an error, as it should never be done directly. - -## Default configuration - -The following is the default configuration used by Iroha. - -```json -{ - "PUBLIC_KEY": null, - "PRIVATE_KEY": null, - "DISABLE_PANIC_TERMINAL_COLORS": false, - "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": { - "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 - }, - "LIVE_QUERY_STORE": { - "QUERY_IDLE_TIME_MS": 30000 - } -} -``` - -## `block_sync` - -`BlockSynchronizer` configuration - -Has type `Option`[^1]. Can be configured via environment variable `IROHA_BLOCK_SYNC` - -```json -{ - "ACTOR_CHANNEL_CAPACITY": 100, - "BLOCK_BATCH_SIZE": 4, - "GOSSIP_PERIOD_MS": 10000 -} -``` - -### `block_sync.actor_channel_capacity` - -Buffer capacity of actor's MPSC channel - -Has type `Option`[^1]. Can be configured via environment variable `BLOCK_SYNC_ACTOR_CHANNEL_CAPACITY` - -```json -100 -``` - -### `block_sync.block_batch_size` - -The number of blocks that can be sent in one message. - -Has type `Option`[^1]. Can be configured via environment variable `BLOCK_SYNC_BLOCK_BATCH_SIZE` - -```json -4 -``` - -### `block_sync.gossip_period_ms` - -The period of time to wait between sending requests for the latest block. - -Has type `Option`[^1]. Can be configured via environment variable `BLOCK_SYNC_GOSSIP_PERIOD_MS` - -```json -10000 -``` - -## `disable_panic_terminal_colors` - -Disable coloring of the backtrace and error report on panic - -Has type `Option`[^1]. Can be configured via environment variable `IROHA_DISABLE_PANIC_TERMINAL_COLORS` - -```json -false -``` - -## `genesis` - -`GenesisBlock` configuration - -Has type `Option>`[^1]. Can be configured via environment variable `IROHA_GENESIS` - -```json -{ - "ACCOUNT_PRIVATE_KEY": null, - "ACCOUNT_PUBLIC_KEY": null -} -``` - -### `genesis.account_private_key` - -The private key of the genesis account, only needed for the peer that submits the genesis block. - -Has type `Option>`[^1]. Can be configured via environment variable `IROHA_GENESIS_ACCOUNT_PRIVATE_KEY` - -```json -null -``` - -### `genesis.account_public_key` - -The public key of the genesis account, should be supplied to all peers. - -Has type `Option`[^1]. Can be configured via environment variable `IROHA_GENESIS_ACCOUNT_PUBLIC_KEY` - -```json -null -``` - -## `kura` - -`Kura` configuration - -Has type `Option>`[^1]. Can be configured via environment variable `IROHA_KURA` - -```json -{ - "BLOCK_STORE_PATH": "./storage", - "DEBUG_OUTPUT_NEW_BLOCKS": false, - "INIT_MODE": "strict" -} -``` - -### `kura.block_store_path` - -Path to the existing block store folder or path to create new folder. - -Has type `Option`[^1]. Can be configured via environment variable `KURA_BLOCK_STORE_PATH` - -```json -"./storage" -``` - -### `kura.debug_output_new_blocks` - -Whether or not new blocks be outputted to a file called blocks.json. - -Has type `Option`[^1]. Can be configured via environment variable `KURA_DEBUG_OUTPUT_NEW_BLOCKS` - -```json -false -``` - -### `kura.init_mode` - -Initialization mode: `strict` or `fast`. - -Has type `Option`[^1]. Can be configured via environment variable `KURA_INIT_MODE` - -```json -"strict" -``` - -## `live_query_store` - -LiveQueryStore configuration - -Has type `Option`[^1]. Can be configured via environment variable `IROHA_LIVE_QUERY_STORE` - -```json -{ - "QUERY_IDLE_TIME_MS": 30000 -} -``` - -### `live_query_store.query_idle_time_ms` - -Time query can remain in the store if unaccessed - -Has type `Option`[^1]. Can be configured via environment variable `LIVE_QUERY_STORE_QUERY_IDLE_TIME_MS` - -```json -30000 -``` - -## `logger` - -`Logger` configuration - -Has type `Option>`[^1]. Can be configured via environment variable `IROHA_LOGGER` - -```json -{ - "FORMAT": "full", - "LEVEL": "INFO" -} -``` - -### `logger.format` - -Output format - -Has type `Option`[^1]. Can be configured via environment variable `LOG_FORMAT` - -```json -"full" -``` - -### `logger.level` - -Level of logging verbosity - -Has type `Option`[^1]. Can be configured via environment variable `LOG_LEVEL` - -```json -"INFO" -``` - -## `network` - -Network configuration - -Has type `Option`[^1]. Can be configured via environment variable `IROHA_NETWORK` - -```json -{ - "ACTOR_CHANNEL_CAPACITY": 100 -} -``` - -### `network.actor_channel_capacity` - -Buffer capacity of actor's MPSC channel - -Has type `Option`[^1]. Can be configured via environment variable `IROHA_NETWORK_ACTOR_CHANNEL_CAPACITY` - -```json -100 -``` - -## `private_key` - -Private key of this peer - -Has type `Option`[^1]. Can be configured via environment variable `IROHA_PRIVATE_KEY` - -```json -null -``` - -## `public_key` - -Public key of this peer - -Has type `Option`[^1]. Can be configured via environment variable `IROHA_PUBLIC_KEY` - -```json -null -``` - -## `queue` - -`Queue` configuration - -Has type `Option`[^1]. Can be configured via environment variable `IROHA_QUEUE` - -```json -{ - "FUTURE_THRESHOLD_MS": 1000, - "MAX_TRANSACTIONS_IN_QUEUE": 65536, - "MAX_TRANSACTIONS_IN_QUEUE_PER_USER": 65536, - "TRANSACTION_TIME_TO_LIVE_MS": 86400000 -} -``` - -### `queue.future_threshold_ms` - -The threshold to determine if a transaction has been tampered to have a future timestamp. - -Has type `Option`[^1]. Can be configured via environment variable `QUEUE_FUTURE_THRESHOLD_MS` - -```json -1000 -``` - -### `queue.max_transactions_in_queue` - -The upper limit of the number of transactions waiting in the queue. - -Has type `Option`[^1]. Can be configured via environment variable `QUEUE_MAX_TRANSACTIONS_IN_QUEUE` - -```json -65536 -``` - -### `queue.max_transactions_in_queue_per_user` - -The upper limit of the number of transactions waiting in the queue for single user. - -Has type `Option`[^1]. Can be configured via environment variable `QUEUE_MAX_TRANSACTIONS_IN_QUEUE_PER_USER` - -```json -65536 -``` - -### `queue.transaction_time_to_live_ms` - -The transaction will be dropped after this time if it is still in the queue. - -Has type `Option`[^1]. Can be configured via environment variable `QUEUE_TRANSACTION_TIME_TO_LIVE_MS` - -```json -86400000 -``` - -## `snapshot` - -SnapshotMaker configuration - -Has type `Option>`[^1]. Can be configured via environment variable `IROHA_SNAPSHOT` - -```json -{ - "CREATE_EVERY_MS": 60000, - "CREATION_ENABLED": true, - "DIR_PATH": "./storage" -} -``` - -### `snapshot.create_every_ms` - -The period of time to wait between attempts to create new snapshot. - -Has type `Option`[^1]. Can be configured via environment variable `SNAPSHOT_CREATE_EVERY_MS` - -```json -60000 -``` - -### `snapshot.creation_enabled` - -Flag to enable or disable snapshot creation - -Has type `Option`[^1]. Can be configured via environment variable `SNAPSHOT_CREATION_ENABLED` - -```json -true -``` - -### `snapshot.dir_path` - -Path to the directory where snapshots should be stored - -Has type `Option`[^1]. Can be configured via environment variable `SNAPSHOT_DIR_PATH` - -```json -"./storage" -``` - -## `sumeragi` - -`Sumeragi` configuration - -Has type `Option>`[^1]. Can be configured via environment variable `IROHA_SUMERAGI` - -```json -{ - "ACTOR_CHANNEL_CAPACITY": 100, - "BLOCK_TIME_MS": 2000, - "COMMIT_TIME_LIMIT_MS": 4000, - "GOSSIP_BATCH_SIZE": 500, - "GOSSIP_PERIOD_MS": 1000, - "KEY_PAIR": null, - "MAX_TRANSACTIONS_IN_BLOCK": 512, - "PEER_ID": null, - "TRUSTED_PEERS": null -} -``` - -### `sumeragi.actor_channel_capacity` - -Buffer capacity of actor's MPSC channel - -Has type `Option`[^1]. Can be configured via environment variable `SUMERAGI_ACTOR_CHANNEL_CAPACITY` - -```json -100 -``` - -### `sumeragi.block_time_ms` - -The period of time a peer waits for the `CreatedBlock` message after getting a `TransactionReceipt` - -Has type `Option`[^1]. Can be configured via environment variable `SUMERAGI_BLOCK_TIME_MS` - -```json -2000 -``` - -### `sumeragi.commit_time_limit_ms` - -The period of time a peer waits for `CommitMessage` from the proxy tail. - -Has type `Option`[^1]. Can be configured via environment variable `SUMERAGI_COMMIT_TIME_LIMIT_MS` - -```json -4000 -``` - -### `sumeragi.gossip_batch_size` - -max number of transactions in tx gossip batch message. While configuring this, pay attention to `p2p` max message size. - -Has type `Option`[^1]. Can be configured via environment variable `SUMERAGI_GOSSIP_BATCH_SIZE` - -```json -500 -``` - -### `sumeragi.gossip_period_ms` - -Period in milliseconds for pending transaction gossiping between peers. - -Has type `Option`[^1]. Can be configured via environment variable `SUMERAGI_GOSSIP_PERIOD_MS` - -```json -1000 -``` - -### `sumeragi.key_pair` - -The key pair consisting of a private and a public key. - -Has type `Option`[^1]. Can be configured via environment variable `SUMERAGI_KEY_PAIR` - -```json -null -``` - -### `sumeragi.max_transactions_in_block` - -The upper limit of the number of transactions per block. - -Has type `Option`[^1]. Can be configured via environment variable `SUMERAGI_MAX_TRANSACTIONS_IN_BLOCK` - -```json -512 -``` - -### `sumeragi.peer_id` - -Current Peer Identification. - -Has type `Option`[^1]. Can be configured via environment variable `SUMERAGI_PEER_ID` - -```json -null -``` - -### `sumeragi.trusted_peers` - -Optional list of predefined trusted peers. - -Has type `Option`[^1]. Can be configured via environment variable `SUMERAGI_TRUSTED_PEERS` - -```json -null -``` - -## `telemetry` - -Telemetry configuration - -Has type `Option>`[^1]. Can be configured via environment variable `IROHA_TELEMETRY` - -```json -{ - "FILE": null, - "MAX_RETRY_DELAY_EXPONENT": 4, - "MIN_RETRY_PERIOD": 1, - "NAME": null, - "URL": null -} -``` - -### `telemetry.file` - -The filepath that to write dev-telemetry to - -Has type `Option>`[^1]. Can be configured via environment variable `TELEMETRY_FILE` - -```json -null -``` - -### `telemetry.max_retry_delay_exponent` - -The maximum exponent of 2 that is used for increasing delay between reconnections - -Has type `Option`[^1]. Can be configured via environment variable `TELEMETRY_MAX_RETRY_DELAY_EXPONENT` - -```json -4 -``` - -### `telemetry.min_retry_period` - -The minimum period of time in seconds to wait before reconnecting - -Has type `Option`[^1]. Can be configured via environment variable `TELEMETRY_MIN_RETRY_PERIOD` - -```json -1 -``` - -### `telemetry.name` - -The node's name to be seen on the telemetry - -Has type `Option>`[^1]. Can be configured via environment variable `TELEMETRY_NAME` - -```json -null -``` - -### `telemetry.url` - -The url of the telemetry, e.g., ws://127.0.0.1:8001/submit - -Has type `Option>`[^1]. Can be configured via environment variable `TELEMETRY_URL` - -```json -null -``` - -## `torii` - -`Torii` configuration - -Has type `Option>`[^1]. Can be configured via environment variable `IROHA_TORII` - -```json -{ - "API_URL": null, - "MAX_CONTENT_LEN": 16384000, - "MAX_TRANSACTION_SIZE": 32768, - "P2P_ADDR": null -} -``` - -### `torii.api_url` - -Torii address for client API. - -Has type `Option`[^1]. Can be configured via environment variable `TORII_API_URL` - -```json -null -``` - -### `torii.max_content_len` - -Maximum number of bytes in raw message. Used to prevent from DOS attacks. - -Has type `Option`[^1]. Can be configured via environment variable `TORII_MAX_CONTENT_LEN` - -```json -16384000 -``` - -### `torii.max_transaction_size` - -Maximum number of bytes in raw transaction. Used to prevent from DOS attacks. - -Has type `Option`[^1]. Can be configured via environment variable `TORII_MAX_TRANSACTION_SIZE` - -```json -32768 -``` - -### `torii.p2p_addr` - -Torii address for p2p communication for consensus and block synchronization purposes. - -Has type `Option`[^1]. Can be configured via environment variable `TORII_P2P_ADDR` - -```json -null -``` - -## `wsv` - -`WorldStateView` configuration - -Has type `Option>`[^1]. Can be configured via environment variable `IROHA_WSV` - -```json -{ - "ACCOUNT_METADATA_LIMITS": { - "max_entry_byte_size": 4096, - "max_len": 1048576 - }, - "ASSET_DEFINITION_METADATA_LIMITS": { - "max_entry_byte_size": 4096, - "max_len": 1048576 - }, - "ASSET_METADATA_LIMITS": { - "max_entry_byte_size": 4096, - "max_len": 1048576 - }, - "DOMAIN_METADATA_LIMITS": { - "max_entry_byte_size": 4096, - "max_len": 1048576 - }, - "IDENT_LENGTH_LIMITS": { - "max": 128, - "min": 1 - }, - "TRANSACTION_LIMITS": { - "max_instruction_number": 4096, - "max_wasm_size_bytes": 4194304 - }, - "WASM_RUNTIME_CONFIG": { - "FUEL_LIMIT": 23000000, - "MAX_MEMORY": 524288000 - } -} -``` - -### `wsv.account_metadata_limits` - -[`MetadataLimits`] of any account metadata. - -Has type `Option`[^1]. Can be configured via environment variable `WSV_ACCOUNT_METADATA_LIMITS` - -```json -{ - "max_entry_byte_size": 4096, - "max_len": 1048576 -} -``` - -### `wsv.asset_definition_metadata_limits` - -[`MetadataLimits`] of any asset definition metadata. - -Has type `Option`[^1]. Can be configured via environment variable `WSV_ASSET_DEFINITION_METADATA_LIMITS` - -```json -{ - "max_entry_byte_size": 4096, - "max_len": 1048576 -} -``` - -### `wsv.asset_metadata_limits` - -[`MetadataLimits`] for every asset with store. - -Has type `Option`[^1]. Can be configured via environment variable `WSV_ASSET_METADATA_LIMITS` - -```json -{ - "max_entry_byte_size": 4096, - "max_len": 1048576 -} -``` - -### `wsv.domain_metadata_limits` - -[`MetadataLimits`] of any domain metadata. - -Has type `Option`[^1]. Can be configured via environment variable `WSV_DOMAIN_METADATA_LIMITS` - -```json -{ - "max_entry_byte_size": 4096, - "max_len": 1048576 -} -``` - -### `wsv.ident_length_limits` - -[`LengthLimits`] for the number of chars in identifiers that can be stored in the WSV. - -Has type `Option`[^1]. Can be configured via environment variable `WSV_IDENT_LENGTH_LIMITS` - -```json -{ - "max": 128, - "min": 1 -} -``` - -### `wsv.transaction_limits` - -Limits that all transactions need to obey, in terms of size - -Has type `Option`[^1]. Can be configured via environment variable `WSV_TRANSACTION_LIMITS` - -```json -{ - "max_instruction_number": 4096, - "max_wasm_size_bytes": 4194304 -} -``` - -### `wsv.wasm_runtime_config` - -WASM runtime configuration - -Has type `Option`[^1]. Can be configured via environment variable `WSV_WASM_RUNTIME_CONFIG` - -```json -{ - "FUEL_LIMIT": 23000000, - "MAX_MEMORY": 524288000 -} -``` - -#### `wsv.wasm_runtime_config.fuel_limit` - -The fuel limit determines the maximum number of instructions that can be executed within a smart contract. - -Has type `Option`[^1]. Can be configured via environment variable `WASM_FUEL_LIMIT` - -```json -23000000 -``` - -#### `wsv.wasm_runtime_config.max_memory` - -Maximum amount of linear memory a given smart contract can allocate. - -Has type `Option`[^1]. Can be configured via environment variable `WASM_MAX_MEMORY` - -```json -524288000 -``` - diff --git a/hooks/pre-commit.sample b/hooks/pre-commit.sample index fdf09848d91..72addb9c6b5 100755 --- a/hooks/pre-commit.sample +++ b/hooks/pre-commit.sample @@ -2,7 +2,6 @@ set -e cargo +nightly fmt --all -- --check cargo +nightly lints clippy --workspace --benches --tests --examples --all-features -cargo run --bin kagami -- docs >docs/source/references/config.md cargo run --bin kagami -- genesis >configs/peer/genesis.json cargo run --bin kagami -- schema >docs/source/references/schema.json -git add docs/source/references/config.md configs/peer/genesis.json docs/source/references/schema.json +git add configs/peer/genesis.json docs/source/references/schema.json diff --git a/scripts/tests/consistency.sh b/scripts/tests/consistency.sh index dd5a5291a5c..bf5873f2f81 100755 --- a/scripts/tests/consistency.sh +++ b/scripts/tests/consistency.sh @@ -2,11 +2,6 @@ set -e case $1 in - "docs") - cargo run --release --bin kagami -- docs | diff - docs/source/references/config.md || { - echo 'Please re-generate docs using `cargo run --release --bin kagami -- docs > docs/source/references/config.md`' - exit 1 - };; "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`' diff --git a/tools/kagami/src/docs.rs b/tools/kagami/src/docs.rs deleted file mode 100644 index 737959c5aef..00000000000 --- a/tools/kagami/src/docs.rs +++ /dev/null @@ -1,130 +0,0 @@ -use std::{fmt::Debug, io::Write}; - -use color_eyre::eyre::WrapErr as _; -use iroha_config::{base::proxy::Documented, iroha::ConfigurationProxy}; -use serde_json::Value; - -use super::*; - -impl + Send + Sync + Default> PrintDocs for C {} - -#[derive(ClapArgs, Debug, Clone, Copy)] -pub struct Args; - -impl RunArgs for Args { - fn run(self, writer: &mut BufWriter) -> crate::Outcome { - ConfigurationProxy::get_markdown(writer).wrap_err("Failed to generate documentation") - } -} - -pub trait PrintDocs: Documented + Send + Sync + Default -where - Self::Error: Debug, -{ - fn get_markdown(writer: &mut W) -> color_eyre::Result<()> { - let Value::Object(docs) = Self::get_docs() else { - unreachable!("Top level structure is always an object") - }; - let mut vec = Vec::new(); - let defaults = serde_json::to_string_pretty(&Self::default())?; - - writeln!(writer, "# Iroha Configuration reference\n")?; - writeln!(writer, "In this document we provide a reference and detailed descriptions of Iroha's configuration options. \ - The options have different underlying types and default values, which are denoted in code as types wrapped in a single \ - `Option<..>` or in a double `Option>`. For the detailed explanation, please refer to \ - this [section](#configuration-types).\n")?; - writeln!( - writer, - "## Configuration types\n\n\ - ### `Option<..>`\n\n\ - A type wrapped in a single `Option<..>` signifies that in the corresponding `json` block there is a fallback value for this type, \ - and that it only serves as a reference. If a default for such a type has a `null` value, it means that there is no meaningful fallback \ - available for this particular value.\n\nAll the default values can be freely obtained from a provided [sample configuration file](../../../configs/peer/config.json), \ - but it should only serve as a starting point. If left unchanged, the sample configuration file would still fail to build due to it having `null` in place of \ - [public](#public_key) and [private](#private_key) keys as well as [API endpoint URL](#torii.api_url). \ - These should be provided either by modifying the sample config file or as environment variables. \ - No other overloading of configuration values happens besides reading them from a file and capturing the environment variables.\n\n\ - For both types of configuration options wrapped in a single `Option<..>` (i.e. both those that have meaningful defaults and those that have `null`), \ - failure to provide them in any of the above two ways results in an error.\n\n\ - ### `Option>`\n\n\ - `Option>` types should be distinguished from types wrapped in a single `Option<..>`. Only the double option ones are allowed to stay `null`, \ - meaning that **not** providing them in an environment variable or a file will **not** result in an error.\n\n\ - Thus, only these types are truly optional in the mundane sense of the word. \ - An example of this distinction is genesis [public](#genesis.account_public_key) and [private](#genesis.account_private_key) key. \ - While the first one is a single `Option<..>` wrapped type, the latter is wrapped in `Option>`. This means that the genesis *public* key should always be \ - provided by the user, be it via a file config or an environment variable, whereas the *private* key is only needed for the peer that submits the genesis block, \ - and can be omitted for all others. The same logic goes for other double option fields such as logger file path.\n\n\ - ### Sumeragi: default `null` values\n\n\ - A special note about sumeragi fields with `null` as default: only the [`trusted_peers`](#sumeragi.trusted_peers) field out of the three can be initialized via a \ - provided file or an environment variable.\n\n\ - The other two fields, namely [`key_pair`](#sumeragi.key_pair) and [`peer_id`](#sumeragi.peer_id), go through a process of finalization where their values \ - are derived from the corresponding ones in the uppermost Iroha config (using its [`public_key`](#public_key) and [`private_key`](#private_key) fields) \ - or the Torii config (via its [`p2p_addr`](#torii.p2p_addr)). \ - This ensures that these linked fields stay in sync, and prevents the programmer error when different values are provided to these field pairs. \ - Providing either `sumeragi.key_pair` or `sumeragi.peer_id` by hand will result in an error, as it should never be done directly.\n" - )?; - writeln!(writer, "## Default configuration\n")?; - writeln!( - writer, - "The following is the default configuration used by Iroha.\n" - )?; - writeln!(writer, "```json\n{defaults}\n```\n")?; - Self::get_markdown_with_depth(writer, &docs, &mut vec, 2)?; - Ok(()) - } - - fn get_markdown_with_depth( - writer: &mut W, - docs: &serde_json::Map, - field: &mut Vec, - depth: usize, - ) -> color_eyre::Result<()> { - let current_field = { - let mut docs = docs; - for f in &*field { - docs = match &docs[f] { - Value::Object(obj) => obj, - _ => unreachable!(), - }; - } - docs - }; - - for (f, value) in current_field { - field.push(f.clone()); - let get_field = field.iter().map(AsRef::as_ref).collect::>(); - let (doc, inner) = match value { - Value::Object(_) => { - let doc = Self::get_doc_recursive(&get_field) - .expect("Should be there, as already in docs"); - (doc.unwrap_or_default(), true) - } - Value::String(s) => (s.clone(), false), - _ => unreachable!("Only strings and objects in docs"), - }; - // Hacky workaround to avoid duplicating inner fields docs in the reference - let doc = doc.lines().take(3).collect::>().join("\n"); - let doc = doc.strip_prefix(' ').unwrap_or(&doc); - let defaults = Self::default() - .get_recursive(get_field) - .expect("Failed to get defaults."); - let defaults = serde_json::to_string_pretty(&defaults)?; - let field_str = field - .join(".") - .chars() - .filter(|&chr| chr != ' ') - .collect::(); - - write!(writer, "{} `{}`\n\n", "#".repeat(depth), field_str)?; - write!(writer, "{doc}\n\n")?; - write!(writer, "```json\n{defaults}\n```\n\n")?; - - if inner { - Self::get_markdown_with_depth(writer, docs, field, depth + 1)?; - } - - field.pop(); - } - Ok(()) - } -} diff --git a/tools/kagami/src/main.rs b/tools/kagami/src/main.rs index f4ed9d40733..87ef8774f12 100644 --- a/tools/kagami/src/main.rs +++ b/tools/kagami/src/main.rs @@ -12,7 +12,6 @@ use iroha_data_model::prelude::*; mod config; mod crypto; -mod docs; mod genesis; mod schema; @@ -54,8 +53,6 @@ enum Args { Genesis(genesis::Args), /// Generate the default client/peer configuration Config(config::Args), - /// Generate a Markdown reference of configuration parameters - Docs(Box), } impl RunArgs for Args { @@ -67,7 +64,6 @@ impl RunArgs for Args { Schema(args) => args.run(writer), Genesis(args) => args.run(writer), Config(args) => args.run(writer), - Docs(args) => args.run(writer), } } } From 248760a3c4c99bf463ae157a673093b9966eb544 Mon Sep 17 00:00:00 2001 From: Daniil Polyakov Date: Thu, 14 Dec 2023 11:38:44 +0300 Subject: [PATCH 07/21] [feature] #3941, #3612: Remove expressions (#4089) Signed-off-by: Daniil Polyakov --- Cargo.lock | 23 - Cargo.toml | 1 - cli/src/torii/mod.rs | 4 +- client/benches/torii.rs | 29 +- client/benches/tps/utils.rs | 76 +- client/examples/million_accounts_genesis.rs | 5 +- client/examples/tutorial.rs | 57 +- client/src/client.rs | 49 +- client/tests/integration/add_account.rs | 4 +- client/tests/integration/add_domain.rs | 4 +- client/tests/integration/asset.rs | 139 +- client/tests/integration/asset_propagation.rs | 20 +- client/tests/integration/burn_public_keys.rs | 9 +- client/tests/integration/connected_peers.rs | 6 +- client/tests/integration/domain_owner.rs | 121 +- client/tests/integration/events/data.rs | 40 +- .../tests/integration/events/notification.rs | 16 +- client/tests/integration/events/pipeline.rs | 8 +- .../integration/multiple_blocks_created.rs | 19 +- .../integration/multisignature_account.rs | 19 +- .../integration/multisignature_transaction.rs | 13 +- client/tests/integration/non_mintable.rs | 39 +- client/tests/integration/offline_peers.rs | 2 +- client/tests/integration/pagination.rs | 4 +- client/tests/integration/permissions.rs | 63 +- client/tests/integration/queries/account.rs | 6 +- client/tests/integration/queries/asset.rs | 259 +-- client/tests/integration/queries/role.rs | 10 +- client/tests/integration/restart_peer.rs | 12 +- client/tests/integration/roles.rs | 22 +- client/tests/integration/set_parameter.rs | 6 +- .../src/lib.rs | 6 +- .../executor_with_admin/src/lib.rs | 9 +- .../executor_with_custom_token/src/lib.rs | 22 +- .../executor_with_migration_fail/src/lib.rs | 7 +- .../mint_rose_trigger/src/lib.rs | 2 +- .../query_assets_and_save_cursor/src/lib.rs | 10 +- client/tests/integration/sorting.rs | 21 +- client/tests/integration/transfer_asset.rs | 58 +- .../integration/triggers/by_call_trigger.rs | 86 +- .../integration/triggers/data_trigger.rs | 27 +- .../integration/triggers/event_trigger.rs | 7 +- .../integration/triggers/time_trigger.rs | 24 +- .../integration/triggers/trigger_rollback.rs | 12 +- client/tests/integration/tx_history.rs | 15 +- client/tests/integration/tx_rollback.rs | 13 +- client/tests/integration/unregister_peer.rs | 32 +- client/tests/integration/unstable_network.rs | 12 +- client/tests/integration/upgrade.rs | 8 +- client_cli/src/main.rs | 45 +- configs/peer/executor.wasm | Bin 495847 -> 389245 bytes configs/peer/genesis.json | 130 +- core/benches/blocks/common.rs | 42 +- core/benches/blocks/validate_blocks.rs | 4 +- core/benches/kura.rs | 6 +- core/benches/validation.rs | 14 +- core/src/block.rs | 27 +- core/src/executor.rs | 4 +- core/src/queue.rs | 2 +- core/src/smartcontracts/isi/account.rs | 57 +- core/src/smartcontracts/isi/asset.rs | 80 +- core/src/smartcontracts/isi/block.rs | 8 +- core/src/smartcontracts/isi/domain.rs | 39 +- core/src/smartcontracts/isi/mod.rs | 506 ++--- core/src/smartcontracts/isi/query.rs | 10 +- core/src/smartcontracts/isi/triggers/mod.rs | 40 +- core/src/smartcontracts/isi/triggers/set.rs | 2 +- core/src/smartcontracts/isi/tx.rs | 18 +- core/src/smartcontracts/isi/world.rs | 12 +- core/src/smartcontracts/mod.rs | 54 +- core/src/smartcontracts/wasm.rs | 40 +- core/src/sumeragi/main_loop.rs | 6 +- core/src/tx.rs | 249 +-- core/src/wsv.rs | 2 +- core/test_network/src/lib.rs | 8 +- data_model/src/evaluate.rs | 869 -------- data_model/src/events/data/events.rs | 18 + data_model/src/expression.rs | 703 ------- data_model/src/isi.rs | 1645 +++++++++------ data_model/src/lib.rs | 132 +- data_model/src/query/mod.rs | 339 +-- data_model/src/transaction.rs | 15 +- data_model/src/trigger.rs | 2 +- data_model/src/visit.rs | 752 +++---- data_model/tests/data_model.rs | 57 +- data_model/tests/ui.rs | 7 - data_model/tests/ui_fail/evaluates_to.rs | 12 - data_model/tests/ui_fail/evaluates_to.stderr | 17 - default_executor/src/lib.rs | 5 +- docs/source/references/schema.json | 1825 +++++++++-------- dsl/Cargo.toml | 28 - dsl/src/lib.rs | 208 -- ffi/src/lib.rs | 1 + genesis/src/lib.rs | 30 +- p2p/src/peer.rs | 2 + schema/derive/src/lib.rs | 26 +- .../tests/ui_pass/derive_into_schema.rs | 10 + schema/gen/src/lib.rs | 114 +- smart_contract/executor/derive/src/default.rs | 122 +- smart_contract/executor/derive/src/lib.rs | 29 +- smart_contract/executor/src/default.rs | 868 ++++---- smart_contract/executor/src/lib.rs | 26 +- smart_contract/src/lib.rs | 71 +- smart_contract/utils/src/lib.rs | 2 +- tools/kagami/src/genesis.rs | 37 +- .../parity_scale_decoder/samples/trigger.bin | Bin 80 -> 76 bytes .../parity_scale_decoder/samples/trigger.json | 8 +- tools/parity_scale_decoder/src/main.rs | 2 +- 108 files changed, 4000 insertions(+), 6843 deletions(-) delete mode 100644 data_model/src/evaluate.rs delete mode 100644 data_model/src/expression.rs delete mode 100644 data_model/tests/ui.rs delete mode 100644 data_model/tests/ui_fail/evaluates_to.rs delete mode 100644 data_model/tests/ui_fail/evaluates_to.stderr delete mode 100755 dsl/Cargo.toml delete mode 100755 dsl/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index d71bbe9df90..ff9e1f49ce9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2934,20 +2934,6 @@ dependencies = [ "trybuild", ] -[[package]] -name = "iroha_dsl" -version = "2.0.0-pre-rc.20" -dependencies = [ - "iroha_client", - "iroha_config", - "iroha_crypto", - "iroha_data_model", - "litrs", - "proc-macro2", - "quote", - "serde_json", -] - [[package]] name = "iroha_executor" version = "2.0.0-pre-rc.20" @@ -3582,15 +3568,6 @@ version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "969488b55f8ac402214f3f5fd243ebb7206cf82de60d3172994707a4bcc2b829" -[[package]] -name = "litrs" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f17c3668f3cc1132437cdadc93dab05e52d592f06948d3f64828430c36e4a70" -dependencies = [ - "proc-macro2", -] - [[package]] name = "lock_api" version = "0.4.10" diff --git a/Cargo.toml b/Cargo.toml index 73b53e5a8bc..3da58f35d73 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -218,7 +218,6 @@ members = [ "genesis", "primitives", "primitives/derive", - "dsl", "ffi", "ffi/derive", "futures", diff --git a/cli/src/torii/mod.rs b/cli/src/torii/mod.rs index a20f2c3f11a..7780d4e5b74 100644 --- a/cli/src/torii/mod.rs +++ b/cli/src/torii/mod.rs @@ -112,9 +112,7 @@ impl Error { QueryFailed(query_error) | InstructionFailed(InstructionExecutionError::Query(query_error)) => match query_error { - Evaluate(_) | Conversion(_) | UnknownCursor | FetchSizeTooBig => { - StatusCode::BAD_REQUEST - } + Conversion(_) | UnknownCursor | FetchSizeTooBig => StatusCode::BAD_REQUEST, Signature(_) => StatusCode::UNAUTHORIZED, Find(_) => StatusCode::NOT_FOUND, }, diff --git a/client/benches/torii.rs b/client/benches/torii.rs index b8906f52504..95b6be46304 100644 --- a/client/benches/torii.rs +++ b/client/benches/torii.rs @@ -9,6 +9,7 @@ use iroha_client::{ data_model::prelude::*, }; use iroha_crypto::KeyPair; +use iroha_data_model::isi::InstructionBox; use iroha_genesis::{GenesisNetwork, RawGenesisBlockBuilder}; use iroha_primitives::unique_vec; use iroha_version::Encode; @@ -51,18 +52,19 @@ fn query_requests(criterion: &mut Criterion) { }); let mut group = criterion.benchmark_group("query-requests"); let domain_id: DomainId = "domain".parse().expect("Valid"); - let create_domain = RegisterExpr::new(Domain::new(domain_id.clone())); + let create_domain = Register::domain(Domain::new(domain_id.clone())); let account_id = AccountId::new("account".parse().expect("Valid"), domain_id.clone()); let (public_key, _) = KeyPair::generate() .expect("Failed to generate KeyPair") .into(); - let create_account = RegisterExpr::new(Account::new(account_id.clone(), [public_key])); + let create_account = Register::account(Account::new(account_id.clone(), [public_key])); let asset_definition_id = AssetDefinitionId::new("xor".parse().expect("Valid"), domain_id); - let create_asset = RegisterExpr::new(AssetDefinition::quantity(asset_definition_id.clone())); + let create_asset = + Register::asset_definition(AssetDefinition::quantity(asset_definition_id.clone())); let quantity: u32 = 200; - let mint_asset = MintExpr::new( - quantity.to_value(), - IdBox::AssetId(AssetId::new(asset_definition_id, account_id.clone())), + let mint_asset = Mint::asset_quantity( + quantity, + AssetId::new(asset_definition_id, account_id.clone()), ); let mut client_config = iroha_client::samples::get_client_config(&get_key_pair()); @@ -71,7 +73,7 @@ fn query_requests(criterion: &mut Criterion) { let iroha_client = Client::new(&client_config).expect("Invalid client configuration"); thread::sleep(std::time::Duration::from_millis(5000)); - let instructions: [InstructionExpr; 4] = [ + let instructions: [InstructionBox; 4] = [ create_domain.into(), create_account.into(), create_asset.into(), @@ -140,12 +142,12 @@ fn instruction_submits(criterion: &mut Criterion) { rt.block_on(builder.start_with_peer(&mut peer)); let mut group = criterion.benchmark_group("instruction-requests"); let domain_id: DomainId = "domain".parse().expect("Valid"); - let create_domain = RegisterExpr::new(Domain::new(domain_id.clone())); + let create_domain: InstructionBox = Register::domain(Domain::new(domain_id.clone())).into(); let account_id = AccountId::new("account".parse().expect("Valid"), domain_id.clone()); let (public_key, _) = KeyPair::generate() .expect("Failed to generate Key-pair.") .into(); - let create_account = RegisterExpr::new(Account::new(account_id.clone(), [public_key])); + let create_account = Register::account(Account::new(account_id.clone(), [public_key])).into(); let asset_definition_id = AssetDefinitionId::new("xor".parse().expect("Valid"), domain_id); let mut client_config = iroha_client::samples::get_client_config(&get_key_pair()); client_config.torii_api_url = format!("http://{}", peer.api_address).parse().unwrap(); @@ -160,12 +162,9 @@ fn instruction_submits(criterion: &mut Criterion) { let _dropable = group.bench_function("instructions", |b| { b.iter(|| { let quantity: u32 = 200; - let mint_asset = MintExpr::new( - quantity.to_value(), - IdBox::AssetId(AssetId::new( - asset_definition_id.clone(), - account_id.clone(), - )), + let mint_asset = Mint::asset_quantity( + quantity, + AssetId::new(asset_definition_id.clone(), account_id.clone()), ); match iroha_client.submit(mint_asset) { Ok(_) => success_count += 1, diff --git a/client/benches/tps/utils.rs b/client/benches/tps/utils.rs index d21611f7d53..5c37499ce51 100644 --- a/client/benches/tps/utils.rs +++ b/client/benches/tps/utils.rs @@ -19,7 +19,6 @@ use iroha_client::{ }, }; use serde::Deserialize; -use serde_json::json; use test_network::*; pub type Tps = f64; @@ -61,7 +60,7 @@ impl Config { let clients = network.clients(); wait_for_genesis_committed(&clients, 0); - client.submit_blocking( + client.submit_all_blocking( ParametersBuilder::new() .add_parameter(MAX_TRANSACTIONS_IN_BLOCK, self.max_txs_per_block)? .into_set_parameters(), @@ -70,13 +69,12 @@ impl Config { let unit_names = (UnitName::MIN..).take(self.peers as usize); let units = clients .into_iter() - .zip(unit_names.clone().zip(unit_names.cycle().skip(1))) - .map(|(client, pair)| { + .zip(unit_names) + .map(|(client, name)| { let unit = MeasurerUnit { config: self, client, - name: pair.0, - next_name: pair.1, + name, }; unit.ready() }) @@ -155,7 +153,6 @@ struct MeasurerUnit { pub config: Config, pub client: Client, pub name: UnitName, - pub next_name: UnitName, } type UnitName = u32; @@ -169,34 +166,13 @@ impl MeasurerUnit { let keypair = iroha_crypto::KeyPair::generate().expect("Failed to generate KeyPair."); let account_id = account_id(self.name); - let alice_id = AccountId::from_str("alice@wonderland")?; let asset_id = asset_id(self.name); - let register_me = RegisterExpr::new(Account::new( - account_id.clone(), - [keypair.public_key().clone()], - )); + let register_me = + Register::account(Account::new(account_id, [keypair.public_key().clone()])); self.client.submit_blocking(register_me)?; - let can_burn_my_asset = PermissionToken::new( - "CanBurnUserAsset".parse().unwrap(), - &json!({ "asset_id": asset_id }), - ); - let allow_alice_to_burn_my_asset = GrantExpr::new(can_burn_my_asset, alice_id.clone()); - let can_transfer_my_asset = PermissionToken::new( - "CanTransferUserAsset".parse().unwrap(), - &json!({ "asset_id": asset_id }), - ); - let allow_alice_to_transfer_my_asset = GrantExpr::new(can_transfer_my_asset, alice_id); - let grant_tx = TransactionBuilder::new(account_id) - .with_instructions([ - allow_alice_to_burn_my_asset, - allow_alice_to_transfer_my_asset, - ]) - .sign(keypair)?; - self.client.submit_transaction_blocking(&grant_tx)?; - - let mint_a_rose = MintExpr::new(1_u32, asset_id); + let mint_a_rose = Mint::asset_quantity(1_u32, asset_id); self.client.submit_blocking(mint_a_rose)?; Ok(self) @@ -267,42 +243,12 @@ impl MeasurerUnit { }) } - fn instructions(&self) -> impl Iterator { - [self.mint_or_burn(), self.relay_a_rose()] - .into_iter() - .cycle() - } - - fn mint_or_burn(&self) -> InstructionExpr { - let is_running_out = Less::new( - EvaluatesTo::new_unchecked(Expression::Query( - FindAssetQuantityById::new(asset_id(self.name)).into(), - )), - 100_u32, - ); - let supply_roses = MintExpr::new(100_u32.to_value(), asset_id(self.name)); - let burn_a_rose = BurnExpr::new(1_u32.to_value(), asset_id(self.name)); - - ConditionalExpr::with_otherwise(is_running_out, supply_roses, burn_a_rose).into() + fn instructions(&self) -> impl Iterator { + std::iter::once(self.mint()).cycle() } - fn relay_a_rose(&self) -> InstructionExpr { - // Save at least one rose - // because if asset value hits 0 it's automatically deleted from account - // and query `FindAssetQuantityById` return error - let enough_to_transfer = Greater::new( - EvaluatesTo::new_unchecked(Expression::Query( - FindAssetQuantityById::new(asset_id(self.name)).into(), - )), - 1_u32, - ); - let transfer_rose = TransferExpr::new( - asset_id(self.name), - 1_u32.to_value(), - account_id(self.next_name), - ); - - ConditionalExpr::new(enough_to_transfer, transfer_rose).into() + fn mint(&self) -> InstructionBox { + Mint::asset_quantity(1_u32, asset_id(self.name)).into() } } diff --git a/client/examples/million_accounts_genesis.rs b/client/examples/million_accounts_genesis.rs index 079c0ecc7e5..57993c1a972 100644 --- a/client/examples/million_accounts_genesis.rs +++ b/client/examples/million_accounts_genesis.rs @@ -3,6 +3,7 @@ use std::{thread, time::Duration}; use iroha::samples::{construct_executor, get_config}; use iroha_client::data_model::prelude::*; +use iroha_data_model::isi::InstructionBox; use iroha_genesis::{GenesisNetwork, RawGenesisBlock, RawGenesisBlockBuilder}; use iroha_primitives::unique_vec; use test_network::{ @@ -64,8 +65,8 @@ fn create_million_accounts_directly() { format!("bob-{i}").parse().expect("Valid"), domain_id.clone(), ); - let create_domain = RegisterExpr::new(Domain::new(domain_id)); - let create_account = RegisterExpr::new(Account::new(normal_account_id.clone(), [])); + let create_domain: InstructionBox = Register::domain(Domain::new(domain_id)).into(); + let create_account = Register::account(Account::new(normal_account_id.clone(), [])).into(); if test_client .submit_all([create_domain, create_account]) .is_err() diff --git a/client/examples/tutorial.rs b/client/examples/tutorial.rs index a961a1932e7..5cc86cd8495 100644 --- a/client/examples/tutorial.rs +++ b/client/examples/tutorial.rs @@ -3,7 +3,6 @@ use std::fs::File; use eyre::{Error, WrapErr}; -use iroha_client::data_model::TryToValue; // #region rust_config_crates use iroha_config::client::Configuration; // #endregion rust_config_crates @@ -51,7 +50,7 @@ fn domain_registration_test(config: &Configuration) -> Result<(), Error> { client::Client, data_model::{ metadata::UnlimitedMetadata, - prelude::{Domain, DomainId, InstructionExpr, RegisterExpr}, + prelude::{Domain, DomainId, InstructionBox, Register}, }, }; // #endregion domain_register_example_crates @@ -63,7 +62,7 @@ fn domain_registration_test(config: &Configuration) -> Result<(), Error> { // #region domain_register_example_create_isi // Create an ISI - let create_looking_glass = RegisterExpr::new(Domain::new(looking_glass)); + let create_looking_glass = Register::domain(Domain::new(looking_glass)); // #endregion domain_register_example_create_isi // #region rust_client_create @@ -74,7 +73,7 @@ fn domain_registration_test(config: &Configuration) -> Result<(), Error> { // #region domain_register_example_prepare_tx // Prepare a transaction let metadata = UnlimitedMetadata::default(); - let instructions: Vec = vec![create_looking_glass.into()]; + let instructions: Vec = vec![create_looking_glass.into()]; let tx = iroha_client .build_transaction(instructions, metadata) .wrap_err("Error building a domain registration transaction")?; @@ -117,7 +116,7 @@ fn account_registration_test(config: &Configuration) -> Result<(), Error> { client::Client, data_model::{ metadata::UnlimitedMetadata, - prelude::{Account, AccountId, InstructionExpr, RegisterExpr}, + prelude::{Account, AccountId, InstructionBox, Register}, }, }; use iroha_crypto::KeyPair; @@ -141,14 +140,14 @@ fn account_registration_test(config: &Configuration) -> Result<(), Error> { // #region register_account_generate // Generate a new account - let create_account = RegisterExpr::new(Account::new(account_id, [public_key])); + let create_account = Register::account(Account::new(account_id, [public_key])); // #endregion register_account_generate // #region register_account_prepare_tx // Prepare a transaction using the - // Account's RegisterExpr + // Account's RegisterBox let metadata = UnlimitedMetadata::new(); - let instructions: Vec = vec![create_account.into()]; + let instructions: Vec = vec![create_account.into()]; let tx = iroha_client.build_transaction(instructions, metadata)?; // #endregion register_account_prepare_tx @@ -168,7 +167,7 @@ fn asset_registration_test(config: &Configuration) -> Result<(), Error> { use iroha_client::{ client::Client, data_model::prelude::{ - AccountId, AssetDefinition, AssetDefinitionId, AssetId, IdBox, MintExpr, RegisterExpr, + AccountId, AssetDefinition, AssetDefinitionId, AssetId, Mint, Register, }, }; // #endregion register_asset_crates @@ -185,7 +184,7 @@ fn asset_registration_test(config: &Configuration) -> Result<(), Error> { // #region register_asset_init_submit // Initialise the registration time let register_time = - RegisterExpr::new(AssetDefinition::fixed(asset_def_id.clone()).mintable_once()); + Register::asset_definition(AssetDefinition::fixed(asset_def_id.clone()).mintable_once()); // Submit a registration time iroha_client.submit(register_time)?; @@ -197,10 +196,10 @@ fn asset_registration_test(config: &Configuration) -> Result<(), Error> { .expect("Valid, because the string contains no whitespace, has a single '@' character and is not empty after"); // #region register_asset_mint_submit - // Create a MintExpr using a previous asset and account - let mint = MintExpr::new( - 12.34_f64.try_to_value()?, - IdBox::AssetId(AssetId::new(asset_def_id, account_id)), + // Create a MintBox using a previous asset and account + let mint = Mint::asset_fixed( + 12.34_f64.try_into()?, + AssetId::new(asset_def_id, account_id), ); // Submit a minting transaction @@ -217,10 +216,7 @@ fn asset_minting_test(config: &Configuration) -> Result<(), Error> { use iroha_client::{ client::Client, - data_model::{ - prelude::{AccountId, AssetDefinitionId, AssetId, MintExpr, ToValue}, - IdBox, - }, + data_model::prelude::{AccountId, AssetDefinitionId, AssetId, Mint}, }; // #endregion mint_asset_crates @@ -237,10 +233,7 @@ fn asset_minting_test(config: &Configuration) -> Result<(), Error> { // Mint the Asset instance // #region mint_asset_mint - let mint_roses = MintExpr::new( - 42_u32.to_value(), - IdBox::AssetId(AssetId::new(roses, alice)), - ); + let mint_roses = Mint::asset_quantity(42_u32, AssetId::new(roses, alice)); // #endregion mint_asset_mint // #region mint_asset_submit_tx @@ -255,10 +248,7 @@ fn asset_minting_test(config: &Configuration) -> Result<(), Error> { // or `roses.to_string() + "#" + alice.to_string()`. // The `##` is a short-hand for the rose `which belongs to the same domain as the account // to which it belongs to. - let mint_roses_alt = MintExpr::new( - 10_u32.to_value(), - IdBox::AssetId("rose##alice@wonderland".parse()?), - ); + let mint_roses_alt = Mint::asset_quantity(10_u32, "rose##alice@wonderland".parse()?); // #endregion mint_asset_mint_alt // #region mint_asset_submit_tx_alt @@ -277,10 +267,7 @@ fn asset_burning_test(config: &Configuration) -> Result<(), Error> { use iroha_client::{ client::Client, - data_model::{ - prelude::{AccountId, AssetDefinitionId, AssetId, BurnExpr, ToValue}, - IdBox, - }, + data_model::prelude::{AccountId, AssetDefinitionId, AssetId, Burn}, }; // #endregion burn_asset_crates @@ -297,10 +284,7 @@ fn asset_burning_test(config: &Configuration) -> Result<(), Error> { // #region burn_asset_burn // Burn the Asset instance - let burn_roses = BurnExpr::new( - 10_u32.to_value(), - IdBox::AssetId(AssetId::new(roses, alice)), - ); + let burn_roses = Burn::asset_quantity(10_u32, AssetId::new(roses, alice)); // #endregion burn_asset_burn // #region burn_asset_submit_tx @@ -315,10 +299,7 @@ fn asset_burning_test(config: &Configuration) -> Result<(), Error> { // or `roses.to_string() + "#" + alice.to_string()`. // The `##` is a short-hand for the rose `which belongs to the same domain as the account // to which it belongs to. - let burn_roses_alt = BurnExpr::new( - 10_u32.to_value(), - IdBox::AssetId("rose##alice@wonderland".parse()?), - ); + let burn_roses_alt = Burn::asset_quantity(10_u32, "rose##alice@wonderland".parse()?); // #endregion burn_asset_burn_alt // #region burn_asset_submit_tx_alt diff --git a/client/src/client.rs b/client/src/client.rs index d4f7f383e30..e057a8c7c58 100644 --- a/client/src/client.rs +++ b/client/src/client.rs @@ -1472,14 +1472,12 @@ pub mod account { } /// Construct a query to get account by id - pub fn by_id(account_id: impl Into>) -> FindAccountById { + pub fn by_id(account_id: AccountId) -> FindAccountById { FindAccountById::new(account_id) } /// Construct a query to get all accounts containing specified asset - pub fn all_with_asset( - asset_definition_id: impl Into>, - ) -> FindAccountsWithAsset { + pub fn all_with_asset(asset_definition_id: AssetDefinitionId) -> FindAccountsWithAsset { FindAccountsWithAsset::new(asset_definition_id) } } @@ -1499,19 +1497,17 @@ pub mod asset { } /// Construct a query to get asset definition by its id - pub fn definition_by_id( - asset_definition_id: impl Into>, - ) -> FindAssetDefinitionById { + pub fn definition_by_id(asset_definition_id: AssetDefinitionId) -> FindAssetDefinitionById { FindAssetDefinitionById::new(asset_definition_id) } /// Construct a query to get all assets by account id - pub fn by_account_id(account_id: impl Into>) -> FindAssetsByAccountId { + pub fn by_account_id(account_id: AccountId) -> FindAssetsByAccountId { FindAssetsByAccountId::new(account_id) } /// Construct a query to get an asset by its id - pub fn by_id(asset_id: impl Into>) -> FindAssetById { + pub fn by_id(asset_id: AssetId) -> FindAssetById { FindAssetById::new(asset_id) } } @@ -1532,9 +1528,7 @@ pub mod block { } /// Construct a query to find block header by hash - pub fn header_by_hash( - hash: impl Into>>, - ) -> FindBlockHeaderByHash { + pub fn header_by_hash(hash: HashOf) -> FindBlockHeaderByHash { FindBlockHeaderByHash::new(hash) } } @@ -1549,7 +1543,7 @@ pub mod domain { } /// Construct a query to get all domain by id - pub fn by_id(domain_id: impl Into>) -> FindDomainById { + pub fn by_id(domain_id: DomainId) -> FindDomainById { FindDomainById::new(domain_id) } } @@ -1565,16 +1559,12 @@ pub mod transaction { } /// Construct a query to retrieve transactions for account - pub fn by_account_id( - account_id: impl Into>, - ) -> FindTransactionsByAccountId { + pub fn by_account_id(account_id: AccountId) -> FindTransactionsByAccountId { FindTransactionsByAccountId::new(account_id) } /// Construct a query to retrieve transaction by hash - pub fn by_hash( - hash: impl Into>>, - ) -> FindTransactionByHash { + pub fn by_hash(hash: HashOf) -> FindTransactionByHash { FindTransactionByHash::new(hash) } } @@ -1584,7 +1574,7 @@ pub mod trigger { use super::*; /// Construct a query to get triggers by domain id - pub fn by_domain_id(domain_id: impl Into>) -> FindTriggersByDomainId { + pub fn by_domain_id(domain_id: DomainId) -> FindTriggersByDomainId { FindTriggersByDomainId::new(domain_id) } } @@ -1600,10 +1590,8 @@ pub mod permission { /// Construct a query to get all [`PermissionToken`] granted /// to account with given [`Id`][AccountId] - pub fn by_account_id( - account_id: impl Into>, - ) -> FindPermissionTokensByAccountId { - FindPermissionTokensByAccountId::new(account_id.into()) + pub fn by_account_id(account_id: AccountId) -> FindPermissionTokensByAccountId { + FindPermissionTokensByAccountId::new(account_id) } } @@ -1622,12 +1610,12 @@ pub mod role { } /// Construct a query to retrieve a role by its id - pub fn by_id(role_id: impl Into>) -> FindRoleByRoleId { + pub fn by_id(role_id: RoleId) -> FindRoleByRoleId { FindRoleByRoleId::new(role_id) } /// Construct a query to retrieve all roles for an account - pub fn by_account_id(account_id: impl Into>) -> FindRolesByAccountId { + pub fn by_account_id(account_id: AccountId) -> FindRolesByAccountId { FindRolesByAccountId::new(account_id) } } @@ -1681,7 +1669,7 @@ mod tests { let build_transaction = || { client - .build_transaction(Vec::::new(), UnlimitedMetadata::new()) + .build_transaction(Vec::::new(), UnlimitedMetadata::new()) .unwrap() }; let tx1 = build_transaction(); @@ -1754,13 +1742,6 @@ mod tests { )), ), (StatusCode::UNPROCESSABLE_ENTITY, ValidationFail::TooComplex), - ( - StatusCode::NOT_FOUND, - // Here should be `Find`, but actually handler doesn't care - ValidationFail::QueryFailed(QueryExecutionFail::Evaluate( - "whatever".to_owned(), - )), - ), ]; for (status_code, err) in responses { let resp = Response::builder().status(status_code).body(err.encode())?; diff --git a/client/tests/integration/add_account.rs b/client/tests/integration/add_account.rs index f463266399e..bdec9441783 100644 --- a/client/tests/integration/add_account.rs +++ b/client/tests/integration/add_account.rs @@ -13,14 +13,14 @@ fn client_add_account_with_name_length_more_than_limit_should_not_commit_transac let pipeline_time = super::Configuration::pipeline_time(); let normal_account_id: AccountId = "bob@wonderland".parse().expect("Valid"); - let create_account = RegisterExpr::new(Account::new(normal_account_id.clone(), [])); + let create_account = Register::account(Account::new(normal_account_id.clone(), [])); test_client.submit(create_account)?; let too_long_account_name = "0".repeat(2_usize.pow(14)); let incorrect_account_id: AccountId = (too_long_account_name + "@wonderland") .parse() .expect("Valid"); - let create_account = RegisterExpr::new(Account::new(incorrect_account_id.clone(), [])); + let create_account = Register::account(Account::new(incorrect_account_id.clone(), [])); test_client.submit(create_account)?; thread::sleep(pipeline_time * 2); diff --git a/client/tests/integration/add_domain.rs b/client/tests/integration/add_domain.rs index f963fe31d10..09bf95bb90d 100644 --- a/client/tests/integration/add_domain.rs +++ b/client/tests/integration/add_domain.rs @@ -16,11 +16,11 @@ fn client_add_domain_with_name_length_more_than_limit_should_not_commit_transact // Given let normal_domain_id: DomainId = "sora".parse()?; - let create_domain = RegisterExpr::new(Domain::new(normal_domain_id.clone())); + let create_domain = Register::domain(Domain::new(normal_domain_id.clone())); test_client.submit(create_domain)?; let too_long_domain_name: DomainId = "0".repeat(2_usize.pow(14)).parse()?; - let create_domain = RegisterExpr::new(Domain::new(too_long_domain_name.clone())); + let create_domain = Register::domain(Domain::new(too_long_domain_name.clone())); test_client.submit(create_domain)?; thread::sleep(pipeline_time * 2); diff --git a/client/tests/integration/asset.rs b/client/tests/integration/asset.rs index 26e672fc857..2cd3cab768f 100644 --- a/client/tests/integration/asset.rs +++ b/client/tests/integration/asset.rs @@ -6,6 +6,7 @@ use iroha_client::{ data_model::prelude::*, }; use iroha_crypto::{KeyPair, PublicKey}; +use iroha_data_model::isi::InstructionBox; use iroha_primitives::fixed::Fixed; use serde_json::json; use test_network::*; @@ -21,11 +22,13 @@ fn client_register_asset_should_add_asset_once_but_not_twice() -> Result<()> { let account_id = AccountId::from_str("alice@wonderland").expect("Valid"); let asset_definition_id = AssetDefinitionId::from_str("test_asset#wonderland").expect("Valid"); - let create_asset = RegisterExpr::new(AssetDefinition::quantity(asset_definition_id.clone())); - let register_asset = RegisterExpr::new(Asset::new( + let create_asset: InstructionBox = + Register::asset_definition(AssetDefinition::quantity(asset_definition_id.clone())).into(); + let register_asset: InstructionBox = Register::asset(Asset::new( AssetId::new(asset_definition_id.clone(), account_id.clone()), AssetValue::Quantity(0), - )); + )) + .into(); test_client.submit_all([create_asset, register_asset.clone()])?; @@ -56,9 +59,11 @@ fn unregister_asset_should_remove_asset_from_account() -> Result<()> { let asset_definition_id = AssetDefinitionId::from_str("test_asset#wonderland").expect("Valid"); let asset_id = AssetId::new(asset_definition_id.clone(), account_id.clone()); - let create_asset = RegisterExpr::new(AssetDefinition::quantity(asset_definition_id.clone())); - let register_asset = RegisterExpr::new(Asset::new(asset_id.clone(), AssetValue::Quantity(0))); - let unregister_asset = UnregisterExpr::new(asset_id); + let create_asset: InstructionBox = + Register::asset_definition(AssetDefinition::quantity(asset_definition_id.clone())).into(); + let register_asset = + Register::asset(Asset::new(asset_id.clone(), AssetValue::Quantity(0))).into(); + let unregister_asset = Unregister::asset(asset_id); test_client.submit_all([create_asset, register_asset])?; @@ -93,18 +98,16 @@ fn client_add_asset_quantity_to_existing_asset_should_increase_asset_amount() -> // Given let account_id = AccountId::from_str("alice@wonderland").expect("Valid"); let asset_definition_id = AssetDefinitionId::from_str("xor#wonderland").expect("Valid"); - let create_asset = RegisterExpr::new(AssetDefinition::quantity(asset_definition_id.clone())); + let create_asset = + Register::asset_definition(AssetDefinition::quantity(asset_definition_id.clone())); let metadata = iroha_client::data_model::metadata::UnlimitedMetadata::default(); //When let quantity: u32 = 200; - let mint = MintExpr::new( - quantity.to_value(), - IdBox::AssetId(AssetId::new( - asset_definition_id.clone(), - account_id.clone(), - )), + let mint = Mint::asset_quantity( + quantity, + AssetId::new(asset_definition_id.clone(), account_id.clone()), ); - let instructions: [InstructionExpr; 2] = [create_asset.into(), mint.into()]; + let instructions: [InstructionBox; 2] = [create_asset.into(), mint.into()]; let tx = test_client.build_transaction(instructions, metadata)?; test_client.submit_transaction(&tx)?; test_client.poll_request(client::asset::by_account_id(account_id), |result| { @@ -127,18 +130,15 @@ fn client_add_big_asset_quantity_to_existing_asset_should_increase_asset_amount( let account_id = AccountId::from_str("alice@wonderland").expect("Valid"); let asset_definition_id = AssetDefinitionId::from_str("xor#wonderland").expect("Valid"); let create_asset = - RegisterExpr::new(AssetDefinition::big_quantity(asset_definition_id.clone())); + Register::asset_definition(AssetDefinition::big_quantity(asset_definition_id.clone())); let metadata = iroha_client::data_model::metadata::UnlimitedMetadata::default(); //When let quantity: u128 = 2_u128.pow(65); - let mint = MintExpr::new( - quantity.to_value(), - IdBox::AssetId(AssetId::new( - asset_definition_id.clone(), - account_id.clone(), - )), + let mint = Mint::asset_big_quantity( + quantity, + AssetId::new(asset_definition_id.clone(), account_id.clone()), ); - let instructions: [InstructionExpr; 2] = [create_asset.into(), mint.into()]; + let instructions: [InstructionBox; 2] = [create_asset.into(), mint.into()]; let tx = test_client.build_transaction(instructions, metadata)?; test_client.submit_transaction(&tx)?; test_client.poll_request(client::asset::by_account_id(account_id), |result| { @@ -160,20 +160,17 @@ fn client_add_asset_with_decimal_should_increase_asset_amount() -> Result<()> { // Given let account_id = AccountId::from_str("alice@wonderland").expect("Valid"); let asset_definition_id = AssetDefinitionId::from_str("xor#wonderland").expect("Valid"); - let identifiable_box = AssetDefinition::fixed(asset_definition_id.clone()); - let create_asset = RegisterExpr::new(identifiable_box); + let asset_definition = AssetDefinition::fixed(asset_definition_id.clone()); + let create_asset = Register::asset_definition(asset_definition); let metadata = iroha_client::data_model::metadata::UnlimitedMetadata::default(); //When let quantity: Fixed = Fixed::try_from(123.456_f64).unwrap(); - let mint = MintExpr::new( - quantity.to_value(), - IdBox::AssetId(AssetId::new( - asset_definition_id.clone(), - account_id.clone(), - )), + let mint = Mint::asset_fixed( + quantity, + AssetId::new(asset_definition_id.clone(), account_id.clone()), ); - let instructions: [InstructionExpr; 2] = [create_asset.into(), mint.into()]; + let instructions: [InstructionBox; 2] = [create_asset.into(), mint.into()]; let tx = test_client.build_transaction(instructions, metadata)?; test_client.submit_transaction(&tx)?; test_client.poll_request(client::asset::by_account_id(account_id.clone()), |result| { @@ -187,12 +184,9 @@ fn client_add_asset_with_decimal_should_increase_asset_amount() -> Result<()> { // Add some fractional part let quantity2: Fixed = Fixed::try_from(0.55_f64).unwrap(); - let mint = MintExpr::new( - quantity2.to_value(), - IdBox::AssetId(AssetId::new( - asset_definition_id.clone(), - account_id.clone(), - )), + let mint = Mint::asset_fixed( + quantity2, + AssetId::new(asset_definition_id.clone(), account_id.clone()), ); // and check that it is added without errors let sum = quantity @@ -217,7 +211,7 @@ fn client_add_asset_with_name_length_more_than_limit_should_not_commit_transacti // Given let normal_asset_definition_id = AssetDefinitionId::from_str("xor#wonderland").expect("Valid"); - let create_asset = RegisterExpr::new(AssetDefinition::quantity( + let create_asset = Register::asset_definition(AssetDefinition::quantity( normal_asset_definition_id.clone(), )); test_client.submit(create_asset)?; @@ -226,7 +220,7 @@ fn client_add_asset_with_name_length_more_than_limit_should_not_commit_transacti let too_long_asset_name = "0".repeat(2_usize.pow(14)); let incorrect_asset_definition_id = AssetDefinitionId::from_str(&(too_long_asset_name + "#wonderland")).expect("Valid"); - let create_asset = RegisterExpr::new(AssetDefinition::quantity( + let create_asset = Register::asset_definition(AssetDefinition::quantity( incorrect_asset_definition_id.clone(), )); @@ -273,11 +267,11 @@ fn find_rate_and_make_exchange_isi_should_succeed() { let buyer_keypair = KeyPair::generate().expect("Failed to generate seller KeyPair."); let register_account = |account_id: AccountId, signature: PublicKey| { - RegisterExpr::new(Account::new(account_id, [signature])) + Register::account(Account::new(account_id, [signature])) }; let grant_alice_asset_transfer_permission = |asset_id: AssetId, owner_keypair: KeyPair| { - let allow_alice_to_transfer_asset = GrantExpr::new( + let allow_alice_to_transfer_asset = Grant::permission_token( PermissionToken::new( "CanTransferUserAsset".parse().unwrap(), &json!({ "asset_id": asset_id }), @@ -304,7 +298,7 @@ fn find_rate_and_make_exchange_isi_should_succeed() { "exchange", account_id_new("dex", "exchange"), ); - let instructions: [InstructionExpr; 12] = [ + let instructions: [InstructionBox; 12] = [ register::domain("exchange").into(), register::domain("company").into(), register::domain("crypto").into(), @@ -314,17 +308,17 @@ fn find_rate_and_make_exchange_isi_should_succeed() { register::asset_definition("btc", "crypto").into(), register::asset_definition("eth", "crypto").into(), register::asset_definition("btc2eth_rate", "exchange").into(), - MintExpr::new( - 200_u32.to_value(), - IdBox::AssetId(asset_id_new("eth", "crypto", buyer_account_id.clone())), + Mint::asset_quantity( + 200_u32, + asset_id_new("eth", "crypto", buyer_account_id.clone()), ) .into(), - MintExpr::new( - 20_u32.to_value(), - IdBox::AssetId(asset_id_new("btc", "crypto", seller_account_id.clone())), + Mint::asset_quantity( + 20_u32, + asset_id_new("btc", "crypto", seller_account_id.clone()), ) .into(), - MintExpr::new(20_u32.to_value(), IdBox::AssetId(asset_id.clone())).into(), + Mint::asset_quantity(20_u32, asset_id.clone()).into(), ]; test_client .submit_all_blocking(instructions) @@ -333,23 +327,26 @@ fn find_rate_and_make_exchange_isi_should_succeed() { grant_alice_asset_transfer_permission(seller_btc, seller_keypair); grant_alice_asset_transfer_permission(buyer_eth, buyer_keypair); + let to_transfer = test_client + .request(FindAssetQuantityById::new(asset_id)) + .expect("Failed to execute query to find asset quantity by id."); + let to_transfer = match to_transfer { + NumericValue::U32(value) => value, + _ => panic!("Wrong asset quantity type."), + }; test_client - .submit_all_blocking([PairExpr::new( - TransferExpr::new( - IdBox::AssetId(asset_id_new("btc", "crypto", seller_account_id.clone())), - EvaluatesTo::new_evaluates_to_value(Expression::Query( - FindAssetQuantityById::new(asset_id.clone()).into(), - )), - IdBox::AccountId(buyer_account_id.clone()), + .submit_all_blocking([ + Transfer::asset_quantity( + asset_id_new("btc", "crypto", seller_account_id.clone()), + to_transfer, + buyer_account_id.clone(), ), - TransferExpr::new( - IdBox::AssetId(asset_id_new("eth", "crypto", buyer_account_id)), - EvaluatesTo::new_evaluates_to_value(Expression::Query( - FindAssetQuantityById::new(asset_id).into(), - )), - IdBox::AccountId(seller_account_id), + Transfer::asset_quantity( + asset_id_new("eth", "crypto", buyer_account_id), + to_transfer, + seller_account_id, ), - )]) + ]) .expect("Failed to exchange eth for btc."); let expected_seller_eth = NumericValue::U32(20); @@ -403,7 +400,7 @@ fn transfer_asset_definition() { let asset_definition_id: AssetDefinitionId = "asset#wonderland".parse().expect("Valid"); test_client - .submit_blocking(RegisterExpr::new(AssetDefinition::quantity( + .submit_blocking(Register::asset_definition(AssetDefinition::quantity( asset_definition_id.clone(), ))) .expect("Failed to submit transaction"); @@ -414,7 +411,7 @@ fn transfer_asset_definition() { assert_eq!(asset_definition.owned_by(), &alice_id); test_client - .submit_blocking(TransferExpr::new( + .submit_blocking(Transfer::asset_definition( alice_id, asset_definition_id.clone(), bob_id.clone(), @@ -447,12 +444,12 @@ fn asset_id_new(definition_name: &str, definition_domain: &str, account_id: Acco mod register { use super::*; - pub fn domain(name: &str) -> RegisterExpr { - RegisterExpr::new(Domain::new(DomainId::from_str(name).expect("Valid"))) + pub fn domain(name: &str) -> Register { + Register::domain(Domain::new(DomainId::from_str(name).expect("Valid"))) } - pub fn account(account_name: &str, domain_name: &str) -> RegisterExpr { - RegisterExpr::new(Account::new( + pub fn account(account_name: &str, domain_name: &str) -> Register { + Register::account(Account::new( AccountId::new( account_name.parse().expect("Valid"), domain_name.parse().expect("Valid"), @@ -461,8 +458,8 @@ mod register { )) } - pub fn asset_definition(asset_name: &str, domain_name: &str) -> RegisterExpr { - RegisterExpr::new(AssetDefinition::quantity(AssetDefinitionId::new( + pub fn asset_definition(asset_name: &str, domain_name: &str) -> Register { + Register::asset_definition(AssetDefinition::quantity(AssetDefinitionId::new( asset_name.parse().expect("Valid"), domain_name.parse().expect("Valid"), ))) diff --git a/client/tests/integration/asset_propagation.rs b/client/tests/integration/asset_propagation.rs index fb50c5b3f2d..de7a5238418 100644 --- a/client/tests/integration/asset_propagation.rs +++ b/client/tests/integration/asset_propagation.rs @@ -9,6 +9,7 @@ use iroha_client::{ }, }; use iroha_crypto::KeyPair; +use iroha_data_model::isi::InstructionBox; use test_network::*; use super::Configuration; @@ -21,28 +22,27 @@ fn client_add_asset_quantity_to_existing_asset_should_increase_asset_amount_on_a wait_for_genesis_committed(&network.clients(), 0); let pipeline_time = Configuration::pipeline_time(); - client.submit_blocking( + client.submit_all_blocking( ParametersBuilder::new() .add_parameter(MAX_TRANSACTIONS_IN_BLOCK, 1u32)? .into_set_parameters(), )?; - let create_domain = RegisterExpr::new(Domain::new(DomainId::from_str("domain")?)); + let create_domain: InstructionBox = + Register::domain(Domain::new(DomainId::from_str("domain")?)).into(); let account_id = AccountId::from_str("account@domain")?; let (public_key, _) = KeyPair::generate()?.into(); - let create_account = RegisterExpr::new(Account::new(account_id.clone(), [public_key])); + let create_account = Register::account(Account::new(account_id.clone(), [public_key])).into(); let asset_definition_id = AssetDefinitionId::from_str("xor#domain")?; - let create_asset = RegisterExpr::new(AssetDefinition::quantity(asset_definition_id.clone())); + let create_asset = + Register::asset_definition(AssetDefinition::quantity(asset_definition_id.clone())).into(); client.submit_all([create_domain, create_account, create_asset])?; thread::sleep(pipeline_time * 3); //When let quantity: u32 = 200; - client.submit(MintExpr::new( - quantity.to_value(), - IdBox::AssetId(AssetId::new( - asset_definition_id.clone(), - account_id.clone(), - )), + client.submit(Mint::asset_quantity( + quantity, + AssetId::new(asset_definition_id.clone(), account_id.clone()), ))?; thread::sleep(pipeline_time); diff --git a/client/tests/integration/burn_public_keys.rs b/client/tests/integration/burn_public_keys.rs index 4c1431c0639..fb4c54b2251 100644 --- a/client/tests/integration/burn_public_keys.rs +++ b/client/tests/integration/burn_public_keys.rs @@ -29,7 +29,7 @@ fn submit( } fn get(client: &Client, hash: HashOf) -> TransactionValue { - client + *client .request(transaction::by_hash(hash)) .unwrap() .transaction @@ -51,7 +51,7 @@ fn public_keys_cannot_be_burned_to_nothing() { wait_for_genesis_committed(&vec![client.clone()], 0); let charlie_initial_keypair = KeyPair::generate().unwrap(); - let register_charlie = RegisterExpr::new(Account::new( + let register_charlie = Register::account(Account::new( charlie_id.clone(), [charlie_initial_keypair.public_key().clone()], )); @@ -64,7 +64,7 @@ fn public_keys_cannot_be_burned_to_nothing() { let mint_keys = (0..KEYS_COUNT - 1).map(|_| { let (public_key, _) = KeyPair::generate().unwrap().into(); - MintExpr::new(public_key, charlie_id.clone()) + Mint::account_public_key(public_key, charlie_id.clone()) }); let (tx_hash, res) = submit( @@ -79,7 +79,8 @@ fn public_keys_cannot_be_burned_to_nothing() { let charlie = client.request(account::by_id(charlie_id.clone())).unwrap(); let mut keys = charlie.signatories(); - let burn = |key: PublicKey| InstructionExpr::from(BurnExpr::new(key, charlie_id.clone())); + let burn = + |key: PublicKey| InstructionBox::from(Burn::account_public_key(key, charlie_id.clone())); let burn_keys_leaving_one = keys .by_ref() .filter(|pub_key| pub_key != &charlie_initial_keypair.public_key()) diff --git a/client/tests/integration/connected_peers.rs b/client/tests/integration/connected_peers.rs index 0ad808b20c2..91745b15ff8 100644 --- a/client/tests/integration/connected_peers.rs +++ b/client/tests/integration/connected_peers.rs @@ -37,7 +37,7 @@ fn connected_peers_with_f(faults: u64, start_port: Option) -> Result<()> { wait_for_genesis_committed(&network.clients(), 0); let pipeline_time = Configuration::pipeline_time(); - client.submit_blocking( + client.submit_all_blocking( ParametersBuilder::new() .add_parameter(MAX_TRANSACTIONS_IN_BLOCK, 1u32)? .into_set_parameters(), @@ -52,7 +52,7 @@ fn connected_peers_with_f(faults: u64, start_port: Option) -> Result<()> { // then `status.peers` decrements let peer = network.peers.values().last().unwrap(); let peer_client = Client::test(&peer.api_address); - let unregister_peer = UnregisterExpr::new(IdBox::PeerId(peer.id.clone())); + let unregister_peer = Unregister::peer(peer.id.clone()); client.submit_blocking(unregister_peer)?; thread::sleep(pipeline_time * 2); // Wait for some time to allow peers to connect status = client.get_status()?; @@ -63,7 +63,7 @@ fn connected_peers_with_f(faults: u64, start_port: Option) -> Result<()> { // Re-register the peer: committed with f = `faults` - 1 then // `status.peers` increments - let register_peer = RegisterExpr::new(DataModelPeer::new(peer.id.clone())); + let register_peer = Register::peer(DataModelPeer::new(peer.id.clone())); client.submit_blocking(register_peer)?; thread::sleep(pipeline_time * 4); // Wait for some time to allow peers to connect status = client.get_status()?; diff --git a/client/tests/integration/domain_owner.rs b/client/tests/integration/domain_owner.rs index 608eb38bdba..b420d5e81c2 100644 --- a/client/tests/integration/domain_owner.rs +++ b/client/tests/integration/domain_owner.rs @@ -13,13 +13,13 @@ fn domain_owner_domain_permissions() -> Result<()> { // "alice@wonderland" is owner of "kingdom" domain let kingdom = Domain::new(kingdom_id.clone()); - test_client.submit_blocking(RegisterExpr::new(kingdom))?; + test_client.submit_blocking(Register::domain(kingdom))?; // check that "alice@wonderland" as owner of domain can edit metadata in her domain let key: Name = "key".parse()?; let value: Name = "value".parse()?; - test_client.submit_blocking(SetKeyValueExpr::new(kingdom_id.clone(), key.clone(), value))?; - test_client.submit_blocking(RemoveKeyValueExpr::new(kingdom_id.clone(), key))?; + test_client.submit_blocking(SetKeyValue::domain(kingdom_id.clone(), key.clone(), value))?; + test_client.submit_blocking(RemoveKeyValue::domain(kingdom_id.clone(), key))?; // check that "alice@wonderland" as owner of domain can grant and revoke domain related permission tokens let bob_id: AccountId = "bob@wonderland".parse()?; @@ -27,11 +27,11 @@ fn domain_owner_domain_permissions() -> Result<()> { "CanUnregisterDomain".parse().unwrap(), &json!({ "domain_id": kingdom_id }), ); - test_client.submit_blocking(GrantExpr::new(token.clone(), bob_id.clone()))?; - test_client.submit_blocking(RevokeExpr::new(token, bob_id))?; + test_client.submit_blocking(Grant::permission_token(token.clone(), bob_id.clone()))?; + test_client.submit_blocking(Revoke::permission_token(token, bob_id))?; // check that "alice@wonderland" as owner of domain can unregister her domain - test_client.submit_blocking(UnregisterExpr::new(kingdom_id))?; + test_client.submit_blocking(Unregister::domain(kingdom_id))?; Ok(()) } @@ -46,28 +46,28 @@ fn domain_owner_account_permissions() -> Result<()> { // "alice@wonderland" is owner of "kingdom" domain let kingdom = Domain::new(kingdom_id); - test_client.submit_blocking(RegisterExpr::new(kingdom))?; + test_client.submit_blocking(Register::domain(kingdom))?; let mad_hatter_keypair = KeyPair::generate()?; let mad_hatter = Account::new( mad_hatter_id.clone(), [mad_hatter_keypair.public_key().clone()], ); - test_client.submit_blocking(RegisterExpr::new(mad_hatter))?; + test_client.submit_blocking(Register::account(mad_hatter))?; // check that "alice@wonderland" as owner of domain can burn and mint public keys for accounts in her domain let mad_hatter_new_keypair = KeyPair::generate()?; - test_client.submit_blocking(MintExpr::new( + test_client.submit_blocking(Mint::account_public_key( mad_hatter_new_keypair.public_key().clone(), mad_hatter_id.clone(), ))?; - test_client.submit_blocking(BurnExpr::new( + test_client.submit_blocking(Burn::account_public_key( mad_hatter_new_keypair.public_key().clone(), mad_hatter_id.clone(), ))?; // check that "alice@wonderland" as owner of domain can change signature check condition for accounts in her domain - test_client.submit_blocking(MintExpr::new( + test_client.submit_blocking(Mint::account_signature_check_condition( SignatureCheckCondition::AnyAccountSignatureOr(Vec::new().into()), mad_hatter_id.clone(), ))?; @@ -75,12 +75,12 @@ fn domain_owner_account_permissions() -> Result<()> { // check that "alice@wonderland" as owner of domain can edit metadata of account in her domain let key: Name = "key".parse()?; let value: Name = "value".parse()?; - test_client.submit_blocking(SetKeyValueExpr::new( + test_client.submit_blocking(SetKeyValue::account( mad_hatter_id.clone(), key.clone(), value, ))?; - test_client.submit_blocking(RemoveKeyValueExpr::new(mad_hatter_id.clone(), key))?; + test_client.submit_blocking(RemoveKeyValue::account(mad_hatter_id.clone(), key))?; // check that "alice@wonderland" as owner of domain can grant and revoke account related permission tokens in her domain let bob_id: AccountId = "bob@wonderland".parse()?; @@ -88,11 +88,11 @@ fn domain_owner_account_permissions() -> Result<()> { "CanUnregisterAccount".parse().unwrap(), &json!({ "account_id": mad_hatter_id }), ); - test_client.submit_blocking(GrantExpr::new(token.clone(), bob_id.clone()))?; - test_client.submit_blocking(RevokeExpr::new(token, bob_id))?; + test_client.submit_blocking(Grant::permission_token(token.clone(), bob_id.clone()))?; + test_client.submit_blocking(Revoke::permission_token(token, bob_id))?; // check that "alice@wonderland" as owner of domain can unregister accounts in her domain - test_client.submit_blocking(UnregisterExpr::new(mad_hatter_id))?; + test_client.submit_blocking(Unregister::account(mad_hatter_id))?; Ok(()) } @@ -109,30 +109,38 @@ fn domain_owner_asset_definition_permissions() -> Result<()> { // "alice@wonderland" is owner of "kingdom" domain let kingdom = Domain::new(kingdom_id); - test_client.submit_blocking(RegisterExpr::new(kingdom))?; + test_client.submit_blocking(Register::domain(kingdom))?; let bob_keypair = KeyPair::generate()?; let bob = Account::new(bob_id.clone(), [bob_keypair.public_key().clone()]); - test_client.submit_blocking(RegisterExpr::new(bob))?; + test_client.submit_blocking(Register::account(bob))?; let rabbit = Account::new(rabbit_id.clone(), []); - test_client.submit_blocking(RegisterExpr::new(rabbit))?; + test_client.submit_blocking(Register::account(rabbit))?; // register asset definitions by "bob@kingdom" so he is owner of it let coin = AssetDefinition::quantity(coin_id.clone()); let transaction = TransactionBuilder::new(bob_id.clone()) - .with_instructions([RegisterExpr::new(coin)]) + .with_instructions([Register::asset_definition(coin)]) .sign(bob_keypair)?; test_client.submit_transaction_blocking(&transaction)?; // check that "alice@wonderland" as owner of domain can transfer asset definitions in her domain - test_client.submit_blocking(TransferExpr::new(bob_id, coin_id.clone(), rabbit_id))?; + test_client.submit_blocking(Transfer::asset_definition( + bob_id, + coin_id.clone(), + rabbit_id, + ))?; // check that "alice@wonderland" as owner of domain can edit metadata of asset definition in her domain let key: Name = "key".parse()?; let value: Name = "value".parse()?; - test_client.submit_blocking(SetKeyValueExpr::new(coin_id.clone(), key.clone(), value))?; - test_client.submit_blocking(RemoveKeyValueExpr::new(coin_id.clone(), key))?; + test_client.submit_blocking(SetKeyValue::asset_definition( + coin_id.clone(), + key.clone(), + value, + ))?; + test_client.submit_blocking(RemoveKeyValue::asset_definition(coin_id.clone(), key))?; // check that "alice@wonderland" as owner of domain can grant and revoke asset definition related permission tokens in her domain let bob_id: AccountId = "bob@wonderland".parse()?; @@ -140,11 +148,11 @@ fn domain_owner_asset_definition_permissions() -> Result<()> { "CanUnregisterAssetDefinition".parse().unwrap(), &json!({ "asset_definition_id": coin_id }), ); - test_client.submit_blocking(GrantExpr::new(token.clone(), bob_id.clone()))?; - test_client.submit_blocking(RevokeExpr::new(token, bob_id))?; + test_client.submit_blocking(Grant::permission_token(token.clone(), bob_id.clone()))?; + test_client.submit_blocking(Revoke::permission_token(token, bob_id))?; // check that "alice@wonderland" as owner of domain can unregister asset definitions in her domain - test_client.submit_blocking(UnregisterExpr::new(coin_id))?; + test_client.submit_blocking(Unregister::asset_definition(coin_id))?; Ok(()) } @@ -162,41 +170,40 @@ fn domain_owner_asset_permissions() -> Result<()> { // "alice@wonderland" is owner of "kingdom" domain let kingdom = Domain::new(kingdom_id); - test_client.submit_blocking(RegisterExpr::new(kingdom))?; + test_client.submit_blocking(Register::domain(kingdom))?; let bob_keypair = KeyPair::generate()?; let bob = Account::new(bob_id.clone(), [bob_keypair.public_key().clone()]); - test_client.submit_blocking(RegisterExpr::new(bob))?; + test_client.submit_blocking(Register::account(bob))?; // register asset definitions by "bob@kingdom" so he is owner of it let coin = AssetDefinition::quantity(coin_id.clone()); let store = AssetDefinition::store(store_id.clone()); let transaction = TransactionBuilder::new(bob_id.clone()) - .with_instructions([RegisterExpr::new(coin), RegisterExpr::new(store)]) + .with_instructions([ + Register::asset_definition(coin), + Register::asset_definition(store), + ]) .sign(bob_keypair)?; test_client.submit_transaction_blocking(&transaction)?; // check that "alice@wonderland" as owner of domain can register and unregister assets in her domain let bob_coin_id = AssetId::new(coin_id, bob_id.clone()); let bob_coin = Asset::new(bob_coin_id.clone(), 30u32); - test_client.submit_blocking(RegisterExpr::new(bob_coin))?; - test_client.submit_blocking(UnregisterExpr::new(bob_coin_id.clone()))?; + test_client.submit_blocking(Register::asset(bob_coin))?; + test_client.submit_blocking(Unregister::asset(bob_coin_id.clone()))?; // check that "alice@wonderland" as owner of domain can burn, mint and transfer assets in her domain - test_client.submit_blocking(MintExpr::new(10u32.to_value(), bob_coin_id.clone()))?; - test_client.submit_blocking(BurnExpr::new(5u32.to_value(), bob_coin_id.clone()))?; - test_client.submit_blocking(TransferExpr::new(bob_coin_id, 5u32.to_value(), alice_id))?; + test_client.submit_blocking(Mint::asset_quantity(10u32, bob_coin_id.clone()))?; + test_client.submit_blocking(Burn::asset_quantity(5u32, bob_coin_id.clone()))?; + test_client.submit_blocking(Transfer::asset_quantity(bob_coin_id, 5u32, alice_id))?; // check that "alice@wonderland" as owner of domain can edit metadata of store asset in her domain let key: Name = "key".parse()?; let value: Name = "value".parse()?; let bob_store_id = AssetId::new(store_id, bob_id); - test_client.submit_blocking(SetKeyValueExpr::new( - bob_store_id.clone(), - key.clone(), - value, - ))?; - test_client.submit_blocking(RemoveKeyValueExpr::new(bob_store_id.clone(), key))?; + test_client.submit_blocking(SetKeyValue::asset(bob_store_id.clone(), key.clone(), value))?; + test_client.submit_blocking(RemoveKeyValue::asset(bob_store_id.clone(), key))?; // check that "alice@wonderland" as owner of domain can grant and revoke asset related permission tokens in her domain let bob_id: AccountId = "bob@wonderland".parse()?; @@ -204,8 +211,8 @@ fn domain_owner_asset_permissions() -> Result<()> { "CanUnregisterUserAsset".parse().unwrap(), &json!({ "asset_id": bob_store_id }), ); - test_client.submit_blocking(GrantExpr::new(token.clone(), bob_id.clone()))?; - test_client.submit_blocking(RevokeExpr::new(token, bob_id))?; + test_client.submit_blocking(Grant::permission_token(token.clone(), bob_id.clone()))?; + test_client.submit_blocking(Revoke::permission_token(token, bob_id))?; Ok(()) } @@ -221,18 +228,18 @@ fn domain_owner_trigger_permissions() -> Result<()> { // "alice@wonderland" is owner of "kingdom" domain let kingdom = Domain::new(kingdom_id); - test_client.submit_blocking(RegisterExpr::new(kingdom))?; + test_client.submit_blocking(Register::domain(kingdom))?; let bob_keypair = KeyPair::generate()?; let bob = Account::new(bob_id.clone(), [bob_keypair.public_key().clone()]); - test_client.submit_blocking(RegisterExpr::new(bob))?; + test_client.submit_blocking(Register::account(bob))?; let asset_definition_id = "rose#wonderland".parse()?; let asset_id = AssetId::new(asset_definition_id, alice_id.clone()); let trigger_id: TriggerId = "trigger$kingdom".parse()?; - let trigger_instructions = vec![MintExpr::new(1_u32, asset_id)]; - let register_trigger = RegisterExpr::new(Trigger::new( + let trigger_instructions = vec![Mint::asset_quantity(1_u32, asset_id)]; + let register_trigger = Register::trigger(Trigger::new( trigger_id.clone(), Action::new( trigger_instructions, @@ -248,11 +255,11 @@ fn domain_owner_trigger_permissions() -> Result<()> { test_client.submit_blocking(register_trigger)?; // check that "alice@wonderland" as owner of domain can edit repetitions of triggers in her domain - test_client.submit_blocking(MintExpr::new(1_u32, trigger_id.clone()))?; - test_client.submit_blocking(BurnExpr::new(1_u32, trigger_id.clone()))?; + test_client.submit_blocking(Mint::trigger_repetitions(1_u32, trigger_id.clone()))?; + test_client.submit_blocking(Burn::trigger_repetitions(1_u32, trigger_id.clone()))?; // check that "alice@wonderland" as owner of domain can call triggers in her domain - let execute_trigger = ExecuteTriggerExpr::new(trigger_id.clone()); + let execute_trigger = ExecuteTrigger::new(trigger_id.clone()); let _result = test_client.submit_blocking(execute_trigger)?; // check that "alice@wonderland" as owner of domain can grant and revoke trigger related permission tokens in her domain @@ -261,19 +268,15 @@ fn domain_owner_trigger_permissions() -> Result<()> { "CanUnregisterUserTrigger".parse().unwrap(), &json!({ "trigger_id": trigger_id }), ); - test_client.submit_blocking(GrantExpr::new(token.clone(), bob_id.clone()))?; - test_client.submit_blocking(RevokeExpr::new(token, bob_id))?; + test_client.submit_blocking(Grant::permission_token(token.clone(), bob_id.clone()))?; + test_client.submit_blocking(Revoke::permission_token(token, bob_id))?; // check that "alice@wonderland" as owner of domain can unregister triggers in her domain - test_client.submit_blocking(UnregisterExpr::new(trigger_id))?; + test_client.submit_blocking(Unregister::trigger(trigger_id))?; Ok(()) } -#[deprecated( - since = "2.0.0-pre-rc.20", - note = "This test suite is deprecated, use test_transfer_domains.py instead" -)] #[ignore = "migrated to client cli python tests"] #[test] fn domain_owner_transfer() -> Result<()> { @@ -286,17 +289,17 @@ fn domain_owner_transfer() -> Result<()> { // "alice@wonderland" is owner of "kingdom" domain let kingdom = Domain::new(kingdom_id.clone()); - test_client.submit_blocking(RegisterExpr::new(kingdom))?; + test_client.submit_blocking(Register::domain(kingdom))?; let bob_keypair = KeyPair::generate()?; let bob = Account::new(bob_id.clone(), [bob_keypair.public_key().clone()]); - test_client.submit_blocking(RegisterExpr::new(bob))?; + test_client.submit_blocking(Register::account(bob))?; let domain = test_client.request(FindDomainById::new(kingdom_id.clone()))?; assert_eq!(domain.owned_by(), &alice_id); test_client - .submit_blocking(TransferExpr::new( + .submit_blocking(Transfer::domain( alice_id, kingdom_id.clone(), bob_id.clone(), diff --git a/client/tests/integration/events/data.rs b/client/tests/integration/events/data.rs index 161ec489ac7..d3aa2a96834 100644 --- a/client/tests/integration/events/data.rs +++ b/client/tests/integration/events/data.rs @@ -8,37 +8,15 @@ use test_network::*; use crate::wasm::utils::wasm_template; -fn produce_instructions() -> Vec { +fn produce_instructions() -> Vec { let domains = (0..4) .map(|domain_index: usize| Domain::new(domain_index.to_string().parse().expect("Valid"))); - let registers: [InstructionExpr; 4] = domains + domains .into_iter() - .map(RegisterExpr::new) - .map(InstructionExpr::from) + .map(Register::domain) + .map(InstructionBox::from) .collect::>() - .try_into() - .unwrap(); - - // TODO: should we re-introduce the DSL? - vec![ - // domain "0" - // pair - // domain "1" - // if false fail else sequence - // domain "2" - // domain "3" - registers[0].clone(), - PairExpr::new( - registers[1].clone(), - ConditionalExpr::with_otherwise( - false, - Fail::new("unreachable"), - SequenceExpr::new([registers[2].clone(), registers[3].clone()]), - ), - ) - .into(), - ] } #[test] @@ -69,7 +47,7 @@ fn wasm_execution_should_produce_events() -> Result<()> { ptr_len = ptr_len / 2, )?; - ptr_offset = ptr_len; + ptr_offset += ptr_len; } let wat = format!( @@ -121,8 +99,10 @@ fn transaction_execution_should_produce_events( client.submit_transaction_blocking(&transaction)?; // assertion + iroha_logger::info!("Listening for events"); for i in 0..4_usize { let event: DataEvent = event_receiver.recv()??.try_into()?; + iroha_logger::info!("Event: {:?}", event); assert!(matches!(event, DataEvent::Domain(_))); if let DataEvent::Domain(domain_event) = event { assert!(matches!(domain_event, DomainEvent::Created(_))); @@ -174,16 +154,16 @@ fn produce_multiple_events() -> Result<()> { let role = iroha_client::data_model::role::Role::new(role_id.clone()) .add_permission(token_1.clone()) .add_permission(token_2.clone()); - let instructions = [RegisterExpr::new(role.clone())]; + let instructions = [Register::role(role.clone())]; client.submit_all_blocking(instructions)?; // Grants role to Bob let bob_id = AccountId::from_str("bob@wonderland")?; - let grant_role = GrantExpr::new(role_id.clone(), bob_id.clone()); + let grant_role = Grant::role(role_id.clone(), bob_id.clone()); client.submit_blocking(grant_role)?; // Unregister role - let unregister_role = UnregisterExpr::new(role_id.clone()); + let unregister_role = Unregister::role(role_id.clone()); client.submit_blocking(unregister_role)?; // Inspect produced events diff --git a/client/tests/integration/events/notification.rs b/client/tests/integration/events/notification.rs index 2c5cf522fd2..2cd033e2b7c 100644 --- a/client/tests/integration/events/notification.rs +++ b/client/tests/integration/events/notification.rs @@ -14,11 +14,11 @@ fn trigger_completion_success_should_produce_event() -> Result<()> { let asset_id = AssetId::new(asset_definition_id, account_id); let trigger_id = TriggerId::from_str("mint_rose")?; - let instruction = MintExpr::new(1_u32, asset_id.clone()); - let register_trigger = RegisterExpr::new(Trigger::new( + let instruction = Mint::asset_quantity(1_u32, asset_id.clone()); + let register_trigger = Register::trigger(Trigger::new( trigger_id.clone(), Action::new( - vec![InstructionExpr::from(instruction)], + vec![InstructionBox::from(instruction)], Repeats::Indefinitely, asset_id.account_id.clone(), TriggeringFilterBox::ExecuteTrigger(ExecuteTriggerEventFilter::new( @@ -29,7 +29,7 @@ fn trigger_completion_success_should_produce_event() -> Result<()> { )); test_client.submit_blocking(register_trigger)?; - let call_trigger = ExecuteTriggerExpr::new(trigger_id.clone()); + let call_trigger = ExecuteTrigger::new(trigger_id.clone()); let thread_client = test_client.clone(); let (sender, receiver) = mpsc::channel(); @@ -63,11 +63,11 @@ fn trigger_completion_failure_should_produce_event() -> Result<()> { let account_id: AccountId = "alice@wonderland".parse()?; let trigger_id = TriggerId::from_str("fail_box")?; - let instruction = Fail::new("Fail box"); - let register_trigger = RegisterExpr::new(Trigger::new( + let instruction = Fail::new("Fail box".to_owned()); + let register_trigger = Register::trigger(Trigger::new( trigger_id.clone(), Action::new( - vec![InstructionExpr::from(instruction)], + vec![InstructionBox::from(instruction)], Repeats::Indefinitely, account_id.clone(), TriggeringFilterBox::ExecuteTrigger(ExecuteTriggerEventFilter::new( @@ -78,7 +78,7 @@ fn trigger_completion_failure_should_produce_event() -> Result<()> { )); test_client.submit_blocking(register_trigger)?; - let call_trigger = ExecuteTriggerExpr::new(trigger_id.clone()); + let call_trigger = ExecuteTrigger::new(trigger_id.clone()); let thread_client = test_client.clone(); let (sender, receiver) = mpsc::channel(); diff --git a/client/tests/integration/events/pipeline.rs b/client/tests/integration/events/pipeline.rs index 8a9ce281d06..966a922f6e0 100644 --- a/client/tests/integration/events/pipeline.rs +++ b/client/tests/integration/events/pipeline.rs @@ -24,7 +24,7 @@ fn transaction_with_no_instructions_should_be_committed() -> Result<()> { // #[ignore = "Experiment"] #[test] fn transaction_with_fail_instruction_should_be_rejected() -> Result<()> { - let fail = Fail::new("Should be rejected"); + let fail = Fail::new("Should be rejected".to_owned()); test_with_instruction_and_status_and_port( Some(fail.into()), PipelineStatusKind::Rejected, @@ -34,7 +34,7 @@ fn transaction_with_fail_instruction_should_be_rejected() -> Result<()> { #[allow(dead_code, clippy::needless_range_loop, clippy::needless_pass_by_value)] fn test_with_instruction_and_status_and_port( - instruction: Option, + instruction: Option, should_be: PipelineStatusKind, port: u16, ) -> Result<()> { @@ -44,7 +44,7 @@ fn test_with_instruction_and_status_and_port( wait_for_genesis_committed(&clients, 0); let pipeline_time = Configuration::pipeline_time(); - client.submit_blocking( + client.submit_all_blocking( ParametersBuilder::new() .add_parameter(MAX_TRANSACTIONS_IN_BLOCK, 1u32)? .into_set_parameters(), @@ -110,7 +110,7 @@ fn committed_block_must_be_available_in_kura() { .expect("Failed to subscribe for events"); client - .submit(Fail::new("Dummy instruction")) + .submit(Fail::new("Dummy instruction".to_owned())) .expect("Failed to submit transaction"); let event = event_iter.next().expect("Block must be committed"); diff --git a/client/tests/integration/multiple_blocks_created.rs b/client/tests/integration/multiple_blocks_created.rs index f2a13a8089b..aa3f3a551ba 100644 --- a/client/tests/integration/multiple_blocks_created.rs +++ b/client/tests/integration/multiple_blocks_created.rs @@ -9,6 +9,7 @@ use iroha_client::{ }, }; use iroha_crypto::KeyPair; +use iroha_data_model::isi::InstructionBox; use test_network::*; use super::Configuration; @@ -23,18 +24,19 @@ fn long_multiple_blocks_created() -> Result<()> { wait_for_genesis_committed(&network.clients(), 0); let pipeline_time = Configuration::pipeline_time(); - client.submit_blocking( + client.submit_all_blocking( ParametersBuilder::new() .add_parameter(MAX_TRANSACTIONS_IN_BLOCK, 1u32)? .into_set_parameters(), )?; - let create_domain = RegisterExpr::new(Domain::new("domain".parse()?)); + let create_domain: InstructionBox = Register::domain(Domain::new("domain".parse()?)).into(); let account_id: AccountId = "account@domain".parse()?; let (public_key, _) = KeyPair::generate()?.into(); - let create_account = RegisterExpr::new(Account::new(account_id.clone(), [public_key])); + let create_account = Register::account(Account::new(account_id.clone(), [public_key])).into(); let asset_definition_id: AssetDefinitionId = "xor#domain".parse()?; - let create_asset = RegisterExpr::new(AssetDefinition::quantity(asset_definition_id.clone())); + let create_asset = + Register::asset_definition(AssetDefinition::quantity(asset_definition_id.clone())).into(); client.submit_all([create_domain, create_account, create_asset])?; @@ -44,12 +46,9 @@ fn long_multiple_blocks_created() -> Result<()> { //When for _ in 0..N_BLOCKS { let quantity: u32 = 1; - let mint_asset = MintExpr::new( - quantity.to_value(), - IdBox::AssetId(AssetId::new( - asset_definition_id.clone(), - account_id.clone(), - )), + let mint_asset = Mint::asset_quantity( + quantity, + AssetId::new(asset_definition_id.clone(), account_id.clone()), ); client.submit(mint_asset)?; account_has_quantity += quantity; diff --git a/client/tests/integration/multisignature_account.rs b/client/tests/integration/multisignature_account.rs index 44f289dcfb1..9f35fb9bbfa 100644 --- a/client/tests/integration/multisignature_account.rs +++ b/client/tests/integration/multisignature_account.rs @@ -19,24 +19,19 @@ fn transaction_signed_by_new_signatory_of_account_should_pass() -> Result<()> { // Given let account_id: AccountId = "alice@wonderland".parse().expect("Valid"); let asset_definition_id: AssetDefinitionId = "xor#wonderland".parse().expect("Valid"); - let create_asset = RegisterExpr::new(AssetDefinition::quantity(asset_definition_id.clone())); + let create_asset = + Register::asset_definition(AssetDefinition::quantity(asset_definition_id.clone())); let key_pair = KeyPair::generate()?; - let add_signatory = MintExpr::new( - key_pair.public_key().clone(), - IdBox::AccountId(account_id.clone()), - ); + let add_signatory = Mint::account_public_key(key_pair.public_key().clone(), account_id.clone()); - let instructions: [InstructionExpr; 2] = [create_asset.into(), add_signatory.into()]; + let instructions: [InstructionBox; 2] = [create_asset.into(), add_signatory.into()]; client.submit_all(instructions)?; thread::sleep(pipeline_time * 2); //When let quantity: u32 = 200; - let mint_asset = MintExpr::new( - quantity.to_value(), - IdBox::AssetId(AssetId::new( - asset_definition_id.clone(), - account_id.clone(), - )), + let mint_asset = Mint::asset_quantity( + quantity, + AssetId::new(asset_definition_id.clone(), account_id.clone()), ); Client::test_with_key(&peer.api_address, key_pair).submit_till( mint_asset, diff --git a/client/tests/integration/multisignature_transaction.rs b/client/tests/integration/multisignature_transaction.rs index 4d999d5c920..280a07751f3 100644 --- a/client/tests/integration/multisignature_transaction.rs +++ b/client/tests/integration/multisignature_transaction.rs @@ -21,7 +21,7 @@ fn multisignature_transactions_should_wait_for_all_signatures() -> Result<()> { wait_for_genesis_committed(&network.clients(), 0); let pipeline_time = Configuration::pipeline_time(); - client.submit_blocking( + client.submit_all_blocking( ParametersBuilder::new() .add_parameter(MAX_TRANSACTIONS_IN_BLOCK, 1u32)? .into_set_parameters(), @@ -31,23 +31,24 @@ fn multisignature_transactions_should_wait_for_all_signatures() -> Result<()> { let alice_key_pair = get_key_pair(); let key_pair_2 = KeyPair::generate()?; let asset_definition_id = AssetDefinitionId::from_str("camomile#wonderland")?; - let create_asset = RegisterExpr::new(AssetDefinition::quantity(asset_definition_id.clone())); - let set_signature_condition = MintExpr::new( + let create_asset = + Register::asset_definition(AssetDefinition::quantity(asset_definition_id.clone())); + let set_signature_condition = Mint::account_signature_check_condition( SignatureCheckCondition::AllAccountSignaturesAnd( vec![key_pair_2.public_key().clone()].into(), ), - IdBox::AccountId(alice_id.clone()), + alice_id.clone(), ); let mut client_configuration = ClientConfiguration::test(&network.genesis.api_address); let client = Client::new(&client_configuration)?; - let instructions: [InstructionExpr; 2] = [create_asset.into(), set_signature_condition.into()]; + let instructions: [InstructionBox; 2] = [create_asset.into(), set_signature_condition.into()]; client.submit_all_blocking(instructions)?; //When let quantity: u32 = 200; let asset_id = AssetId::new(asset_definition_id, alice_id.clone()); - let mint_asset = MintExpr::new(quantity.to_value(), IdBox::AssetId(asset_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(); diff --git a/client/tests/integration/non_mintable.rs b/client/tests/integration/non_mintable.rs index 1dca25910a2..c80be2ca4d9 100644 --- a/client/tests/integration/non_mintable.rs +++ b/client/tests/integration/non_mintable.rs @@ -5,6 +5,7 @@ use iroha_client::{ client::{self, QueryResult}, data_model::{metadata::UnlimitedMetadata, prelude::*}, }; +use iroha_data_model::isi::InstructionBox; use test_network::*; #[test] @@ -15,20 +16,18 @@ fn non_mintable_asset_can_be_minted_once_but_not_twice() -> Result<()> { // Given let account_id = AccountId::from_str("alice@wonderland").expect("Valid"); let asset_definition_id = AssetDefinitionId::from_str("xor#wonderland").expect("Valid"); - let create_asset = - RegisterExpr::new(AssetDefinition::quantity(asset_definition_id.clone()).mintable_once()); + let create_asset = Register::asset_definition( + AssetDefinition::quantity(asset_definition_id.clone()).mintable_once(), + ); let metadata = UnlimitedMetadata::default(); - let mint = MintExpr::new( - 200_u32.to_value(), - IdBox::AssetId(AssetId::new( - asset_definition_id.clone(), - account_id.clone(), - )), + let mint = Mint::asset_quantity( + 200_u32, + AssetId::new(asset_definition_id.clone(), account_id.clone()), ); - let instructions: [InstructionExpr; 2] = [create_asset.into(), mint.clone().into()]; + let instructions: [InstructionBox; 2] = [create_asset.into(), mint.clone().into()]; let tx = test_client.build_transaction(instructions, metadata)?; // We can register and mint the non-mintable token @@ -65,11 +64,14 @@ fn non_mintable_asset_cannot_be_minted_if_registered_with_non_zero_value() -> Re // Given let account_id = AccountId::from_str("alice@wonderland").expect("Valid"); let asset_definition_id = AssetDefinitionId::from_str("xor#wonderland").expect("Valid"); - let create_asset = - RegisterExpr::new(AssetDefinition::quantity(asset_definition_id.clone()).mintable_once()); + let create_asset: InstructionBox = Register::asset_definition( + AssetDefinition::quantity(asset_definition_id.clone()).mintable_once(), + ) + .into(); let asset_id = AssetId::new(asset_definition_id.clone(), account_id.clone()); - let register_asset = RegisterExpr::new(Asset::new(asset_id.clone(), 1_u32)); + let register_asset: InstructionBox = + Register::asset(Asset::new(asset_id.clone(), 1_u32)).into(); // We can register the non-mintable token test_client.submit_all([create_asset, register_asset.clone()])?; @@ -85,7 +87,7 @@ fn non_mintable_asset_cannot_be_minted_if_registered_with_non_zero_value() -> Re assert!(test_client.submit_blocking(register_asset).is_err()); // And can't be minted - let mint = MintExpr::new(1_u32.to_value(), IdBox::AssetId(asset_id)); + let mint = Mint::asset_quantity(1_u32, asset_id); assert!(test_client.submit_blocking(mint).is_err()); Ok(()) @@ -99,15 +101,16 @@ fn non_mintable_asset_can_be_minted_if_registered_with_zero_value() -> Result<() // Given let account_id = AccountId::from_str("alice@wonderland").expect("Valid"); let asset_definition_id = AssetDefinitionId::from_str("xor#wonderland").expect("Valid"); - let create_asset = - RegisterExpr::new(AssetDefinition::quantity(asset_definition_id.clone()).mintable_once()); + let create_asset = Register::asset_definition( + AssetDefinition::quantity(asset_definition_id.clone()).mintable_once(), + ); let asset_id = AssetId::new(asset_definition_id.clone(), account_id.clone()); - let register_asset = RegisterExpr::new(Asset::new(asset_id.clone(), 0_u32)); - let mint = MintExpr::new(1_u32.to_value(), IdBox::AssetId(asset_id)); + let register_asset = Register::asset(Asset::new(asset_id.clone(), 0_u32)); + let mint = Mint::asset_quantity(1_u32, asset_id); // We can register the non-mintable token wih zero value and then mint it - let instructions: [InstructionExpr; 3] = + let instructions: [InstructionBox; 3] = [create_asset.into(), register_asset.into(), mint.into()]; test_client.submit_all(instructions)?; test_client.poll_request(client::asset::by_account_id(account_id), |result| { diff --git a/client/tests/integration/offline_peers.rs b/client/tests/integration/offline_peers.rs index 86838146000..193ff34afaf 100644 --- a/client/tests/integration/offline_peers.rs +++ b/client/tests/integration/offline_peers.rs @@ -21,7 +21,7 @@ fn genesis_block_is_committed_with_some_offline_peers() -> Result<()> { )); wait_for_genesis_committed(&network.clients(), 1); - client.submit_blocking( + client.submit_all_blocking( ParametersBuilder::new() .add_parameter(MAX_TRANSACTIONS_IN_BLOCK, 1u32)? .into_set_parameters(), diff --git a/client/tests/integration/pagination.rs b/client/tests/integration/pagination.rs index 1ab7ab4c14d..6e0ed462ec2 100644 --- a/client/tests/integration/pagination.rs +++ b/client/tests/integration/pagination.rs @@ -46,11 +46,11 @@ fn fetch_size_should_work() -> Result<()> { } fn register_assets(client: &Client) -> Result<()> { - let register: Vec = ('a'..='z') + let register: Vec = ('a'..='z') .map(|c| c.to_string()) .map(|name| (name + "#wonderland").parse().expect("Valid")) .map(|asset_definition_id| { - RegisterExpr::new(AssetDefinition::quantity(asset_definition_id)).into() + Register::asset_definition(AssetDefinition::quantity(asset_definition_id)).into() }) .collect(); let _ = client.submit_all_blocking(register)?; diff --git a/client/tests/integration/permissions.rs b/client/tests/integration/permissions.rs index 11234a6b16a..fb829ed2ce3 100644 --- a/client/tests/integration/permissions.rs +++ b/client/tests/integration/permissions.rs @@ -18,7 +18,7 @@ fn genesis_transactions_are_validated() { let mut genesis = GenesisNetwork::test(true).expect("Expected genesis"); - let grant_invalid_token = GrantExpr::new( + let grant_invalid_token = Grant::permission_token( PermissionToken::new("InvalidToken".parse().unwrap(), &json!(null)), AccountId::from_str("alice@wonderland").unwrap(), ); @@ -78,7 +78,8 @@ fn permissions_disallow_asset_transfer() { let bob_id: AccountId = "bob@wonderland".parse().expect("Valid"); let mouse_id: AccountId = "mouse@wonderland".parse().expect("Valid"); let asset_definition_id: AssetDefinitionId = "xor#wonderland".parse().expect("Valid"); - let create_asset = RegisterExpr::new(AssetDefinition::quantity(asset_definition_id.clone())); + let create_asset = + Register::asset_definition(AssetDefinition::quantity(asset_definition_id.clone())); let mouse_keypair = iroha_crypto::KeyPair::generate().expect("Failed to generate KeyPair."); let alice_start_assets = get_assets(&iroha_client, &alice_id); @@ -87,19 +88,19 @@ fn permissions_disallow_asset_transfer() { .expect("Failed to prepare state."); let quantity: u32 = 200; - let mint_asset = MintExpr::new( - quantity.to_value(), - IdBox::AssetId(AssetId::new(asset_definition_id.clone(), bob_id.clone())), + let mint_asset = Mint::asset_quantity( + quantity, + AssetId::new(asset_definition_id.clone(), bob_id.clone()), ); iroha_client .submit_blocking(mint_asset) .expect("Failed to create asset."); //When - let transfer_asset = TransferExpr::new( - IdBox::AssetId(AssetId::new(asset_definition_id, bob_id)), - quantity.to_value(), - IdBox::AccountId(alice_id.clone()), + let transfer_asset = Transfer::asset_quantity( + AssetId::new(asset_definition_id, bob_id), + quantity, + alice_id.clone(), ); let transfer_tx = TransactionBuilder::new(mouse_id) .with_instructions([transfer_asset]) @@ -131,7 +132,8 @@ fn permissions_disallow_asset_burn() { let bob_id: AccountId = "bob@wonderland".parse().expect("Valid"); let mouse_id: AccountId = "mouse@wonderland".parse().expect("Valid"); let asset_definition_id = AssetDefinitionId::from_str("xor#wonderland").expect("Valid"); - let create_asset = RegisterExpr::new(AssetDefinition::quantity(asset_definition_id.clone())); + let create_asset = + Register::asset_definition(AssetDefinition::quantity(asset_definition_id.clone())); let mouse_keypair = iroha_crypto::KeyPair::generate().expect("Failed to generate KeyPair."); let alice_start_assets = get_assets(&iroha_client, &alice_id); @@ -141,16 +143,14 @@ fn permissions_disallow_asset_burn() { .expect("Failed to prepare state."); let quantity: u32 = 200; - let mint_asset = MintExpr::new( - quantity.to_value(), - IdBox::AssetId(AssetId::new(asset_definition_id.clone(), bob_id)), - ); + let mint_asset = + Mint::asset_quantity(quantity, AssetId::new(asset_definition_id.clone(), bob_id)); iroha_client .submit_blocking(mint_asset) .expect("Failed to create asset."); - let burn_asset = BurnExpr::new( - quantity.to_value(), - IdBox::AssetId(AssetId::new(asset_definition_id, mouse_id.clone())), + let burn_asset = Burn::asset_quantity( + quantity, + AssetId::new(asset_definition_id, mouse_id.clone()), ); let burn_tx = TransactionBuilder::new(mouse_id) .with_instructions([burn_asset]) @@ -184,7 +184,7 @@ fn account_can_query_only_its_own_domain() -> Result<()> { // Given let domain_id: DomainId = "wonderland".parse()?; let new_domain_id: DomainId = "wonderland2".parse()?; - let register_domain = RegisterExpr::new(Domain::new(new_domain_id.clone())); + let register_domain = Register::domain(Domain::new(new_domain_id.clone())); client.submit_blocking(register_domain)?; @@ -213,20 +213,20 @@ fn permissions_differ_not_only_by_names() { let new_shoes_definition = AssetDefinition::store(shoes_definition_id.clone()); client .submit_all_blocking([ - RegisterExpr::new(new_hat_definition), - RegisterExpr::new(new_shoes_definition), + Register::asset_definition(new_hat_definition), + Register::asset_definition(new_shoes_definition), ]) .expect("Failed to register new asset definitions"); // Registering mouse let new_mouse_account = Account::new(mouse_id.clone(), [mouse_keypair.public_key().clone()]); client - .submit_blocking(RegisterExpr::new(new_mouse_account)) + .submit_blocking(Register::account(new_mouse_account)) .expect("Failed to register mouse"); // Granting permission to Alice to modify metadata in Mouse's hats let mouse_hat_id = AssetId::new(hat_definition_id, mouse_id.clone()); - let allow_alice_to_set_key_value_in_hats = GrantExpr::new( + let allow_alice_to_set_key_value_in_hats = Grant::permission_token( PermissionToken::new( "CanSetKeyValueInUserAsset".parse().unwrap(), &json!({ "asset_id": mouse_hat_id }), @@ -244,7 +244,7 @@ fn permissions_differ_not_only_by_names() { // Checking that Alice can modify Mouse's hats ... client - .submit_blocking(SetKeyValueExpr::new( + .submit_blocking(SetKeyValue::asset( mouse_hat_id, Name::from_str("color").expect("Valid"), "red".to_owned(), @@ -253,7 +253,7 @@ fn permissions_differ_not_only_by_names() { // ... but not shoes let mouse_shoes_id = AssetId::new(shoes_definition_id, mouse_id.clone()); - let set_shoes_color = SetKeyValueExpr::new( + let set_shoes_color = SetKeyValue::asset( mouse_shoes_id.clone(), Name::from_str("color").expect("Valid"), "yellow".to_owned(), @@ -263,7 +263,7 @@ fn permissions_differ_not_only_by_names() { .expect_err("Expected Alice to fail to modify Mouse's shoes"); // Granting permission to Alice to modify metadata in Mouse's shoes - let allow_alice_to_set_key_value_in_shoes = GrantExpr::new( + let allow_alice_to_set_key_value_in_shoes = Grant::permission_token( PermissionToken::new( "CanSetKeyValueInUserAsset".parse().unwrap(), &json!({ "asset_id": mouse_shoes_id }), @@ -287,6 +287,7 @@ fn permissions_differ_not_only_by_names() { } #[test] +#[allow(deprecated)] fn stored_vs_granted_token_payload() -> Result<()> { let (_rt, _peer, iroha_client) = ::new().with_port(10_730).start_with_runtime(); wait_for_genesis_committed(&[iroha_client.clone()], 0); @@ -296,12 +297,13 @@ fn stored_vs_granted_token_payload() -> Result<()> { // Registering mouse and asset definition let asset_definition_id: AssetDefinitionId = "xor#wonderland".parse().expect("Valid"); - let create_asset = RegisterExpr::new(AssetDefinition::store(asset_definition_id.clone())); + let create_asset = + Register::asset_definition(AssetDefinition::store(asset_definition_id.clone())); let mouse_id: AccountId = "mouse@wonderland".parse().expect("Valid"); let mouse_keypair = iroha_crypto::KeyPair::generate().expect("Failed to generate KeyPair."); let new_mouse_account = Account::new(mouse_id.clone(), [mouse_keypair.public_key().clone()]); - let instructions: [InstructionExpr; 2] = [ - RegisterExpr::new(new_mouse_account).into(), + let instructions: [InstructionBox; 2] = [ + Register::account(new_mouse_account).into(), create_asset.into(), ]; iroha_client @@ -310,7 +312,7 @@ fn stored_vs_granted_token_payload() -> Result<()> { // Allow alice to mint mouse asset and mint initial value let mouse_asset = AssetId::new(asset_definition_id, mouse_id.clone()); - let allow_alice_to_set_key_value_in_mouse_asset = GrantExpr::new( + let allow_alice_to_set_key_value_in_mouse_asset = Grant::permission_token( PermissionToken::from_str_unchecked( "CanSetKeyValueInUserAsset".parse().unwrap(), // NOTE: Introduced additional whitespaces in the serialized form @@ -328,8 +330,7 @@ fn stored_vs_granted_token_payload() -> Result<()> { .expect("Failed to grant permission to alice."); // Check that alice can indeed mint mouse asset - let set_key_value = - SetKeyValueExpr::new(mouse_asset, Name::from_str("color")?, "red".to_owned()); + let set_key_value = SetKeyValue::asset(mouse_asset, Name::from_str("color")?, "red".to_owned()); iroha_client .submit_blocking(set_key_value) .expect("Failed to mint asset for mouse."); diff --git a/client/tests/integration/queries/account.rs b/client/tests/integration/queries/account.rs index d19a4498361..69d28c66e6f 100644 --- a/client/tests/integration/queries/account.rs +++ b/client/tests/integration/queries/account.rs @@ -15,7 +15,7 @@ fn find_accounts_with_asset() -> Result<()> { // Registering new asset definition let definition_id = AssetDefinitionId::from_str("test_coin#wonderland").expect("Valid"); let asset_definition = AssetDefinition::quantity(definition_id.clone()); - test_client.submit_blocking(RegisterExpr::new(asset_definition.clone()))?; + test_client.submit_blocking(Register::asset_definition(asset_definition.clone()))?; // Checking results before all let received_asset_definition = @@ -40,7 +40,7 @@ fn find_accounts_with_asset() -> Result<()> { .iter() .skip(1) // Alice has already been registered in genesis .cloned() - .map(|account_id| RegisterExpr::new(Account::new(account_id, []))) + .map(|account_id| Register::account(Account::new(account_id, []))) .collect::>(); test_client.submit_all_blocking(register_accounts)?; @@ -48,7 +48,7 @@ fn find_accounts_with_asset() -> Result<()> { .iter() .cloned() .map(|account_id| AssetId::new(definition_id.clone(), account_id)) - .map(|asset_id| MintExpr::new(1_u32, asset_id)) + .map(|asset_id| Mint::asset_quantity(1_u32, asset_id)) .collect::>(); test_client.submit_all_blocking(mint_asset)?; diff --git a/client/tests/integration/queries/asset.rs b/client/tests/integration/queries/asset.rs index 4748cd6d7c2..bb34d302158 100644 --- a/client/tests/integration/queries/asset.rs +++ b/client/tests/integration/queries/asset.rs @@ -1,12 +1,14 @@ use eyre::Result; use iroha_client::{ - client::ClientQueryError, + client::{Client, ClientQueryError}, data_model::{ + asset::AssetValue, prelude::*, query::{asset::FindTotalAssetQuantityByAssetDefinitionId, error::QueryExecutionFail}, }, }; use iroha_crypto::KeyPair; +use iroha_data_model::isi::Instruction; use iroha_primitives::fixed::Fixed; use test_network::*; @@ -19,7 +21,7 @@ fn find_asset_total_quantity() -> Result<()> { // Register new domain let domain_id: DomainId = "looking_glass".parse()?; let domain = Domain::new(domain_id); - test_client.submit_blocking(RegisterExpr::new(domain))?; + test_client.submit_blocking(Register::domain(domain))?; let accounts: [AccountId; 5] = [ "alice@wonderland".parse()?, @@ -40,121 +42,52 @@ fn find_asset_total_quantity() -> Result<()> { .skip(1) // Alice has already been registered in genesis .cloned() .zip(keys.iter().map(KeyPair::public_key).cloned()) - .map(|(account_id, public_key)| RegisterExpr::new(Account::new(account_id, [public_key]))) + .map(|(account_id, public_key)| Register::account(Account::new(account_id, [public_key]))) .collect::>(); test_client.submit_all_blocking(register_accounts)?; // Test for numeric assets value types - for ( - definition, - asset_value_type, - initial_value, - to_mint, - to_burn, - expected_total_asset_quantity, - ) in [ - ( - "quantity#wonderland", - AssetValueType::Quantity, - AssetValue::Quantity(1_u32), - 10_u32.to_value(), - 5_u32.to_value(), - NumericValue::U32(30_u32), - ), - ( - "big-quantity#wonderland", - AssetValueType::BigQuantity, - AssetValue::BigQuantity(1_u128), - 10_u128.to_value(), - 5_u128.to_value(), - NumericValue::U128(30_u128), - ), - ( - "fixed#wonderland", - AssetValueType::Fixed, - AssetValue::Fixed(Fixed::try_from(1.0)?), - 10.0_f64.try_to_value()?, - 5.0_f64.try_to_value()?, - NumericValue::Fixed(Fixed::try_from(30.0)?), - ), - ] { - // Registering new asset definition - let definition_id: AssetDefinitionId = - definition.parse().expect("Failed to parse `definition_id`"); - let asset_definition = AssetDefinition::new(definition_id.clone(), asset_value_type); - test_client.submit_blocking(RegisterExpr::new(asset_definition.clone()))?; - - let asset_ids = accounts - .iter() - .cloned() - .map(|account_id| AssetId::new(definition_id.clone(), account_id)) - .collect::>(); - - // Assert that initial total quantity before any burns and mints is zero - let initial_total_asset_quantity = test_client.request( - FindTotalAssetQuantityByAssetDefinitionId::new(definition_id.clone()), - )?; - assert!(initial_total_asset_quantity.is_zero_value()); - - let register_asset = asset_ids - .iter() - .cloned() - .map(|asset_id| Asset::new(asset_id, initial_value.clone())) - .map(RegisterExpr::new) - .collect::>(); - test_client.submit_all_blocking(register_asset)?; - - let mint_asset = asset_ids - .iter() - .cloned() - .map(|asset_id| MintExpr::new(to_mint.clone(), asset_id)); - test_client.submit_all_blocking(mint_asset)?; - - let burn_asset = asset_ids - .iter() - .cloned() - .map(|asset_id| BurnExpr::new(to_burn.clone(), asset_id)) - .collect::>(); - test_client.submit_all_blocking(burn_asset)?; - - // Assert that total asset quantity is equal to: `n_accounts * (initial_value + to_mint - to_burn)` - let total_asset_quantity = test_client.request( - FindTotalAssetQuantityByAssetDefinitionId::new(definition_id.clone()), - )?; - assert_eq!(expected_total_asset_quantity, total_asset_quantity); - - let unregister_asset = asset_ids - .iter() - .cloned() - .map(UnregisterExpr::new) - .collect::>(); - test_client.submit_all_blocking(unregister_asset)?; - - // Assert that total asset quantity is zero after unregistering asset from all accounts - let total_asset_quantity = test_client.request( - FindTotalAssetQuantityByAssetDefinitionId::new(definition_id.clone()), - )?; - assert!(total_asset_quantity.is_zero_value()); - - // Unregister asset definition - test_client.submit_blocking(UnregisterExpr::new(definition_id.clone()))?; - - // Assert that total asset quantity cleared with unregistering of asset definition - let result = test_client.request(FindTotalAssetQuantityByAssetDefinitionId::new( - definition_id.clone(), - )); - assert!(matches!( - result, - Err(ClientQueryError::Validation(ValidationFail::QueryFailed( - QueryExecutionFail::Find(_) - ))) - )); - } + test_total_quantity( + &test_client, + &accounts, + "quantity#wonderland", + AssetValueType::Quantity, + 1_u32, + 10_u32, + 5_u32, + NumericValue::U32(30_u32), + Mint::asset_quantity, + Burn::asset_quantity, + )?; + test_total_quantity( + &test_client, + &accounts, + "big-quantity#wonderland", + AssetValueType::BigQuantity, + 1_u128, + 10_u128, + 5_u128, + NumericValue::U128(30_u128), + Mint::asset_big_quantity, + Burn::asset_big_quantity, + )?; + test_total_quantity( + &test_client, + &accounts, + "fixed#wonderland", + AssetValueType::Fixed, + Fixed::try_from(1.0)?, + Fixed::try_from(10.0)?, + Fixed::try_from(5.0)?, + NumericValue::Fixed(Fixed::try_from(30.0)?), + Mint::asset_fixed, + Burn::asset_fixed, + )?; // Test for `Store` asset value type let definition_id: AssetDefinitionId = "store#wonderland".parse().expect("Valid"); let asset_definition = AssetDefinition::store(definition_id.clone()); - test_client.submit_blocking(RegisterExpr::new(asset_definition))?; + test_client.submit_blocking(Register::asset_definition(asset_definition))?; let asset_ids = accounts .iter() @@ -168,13 +101,13 @@ fn find_asset_total_quantity() -> Result<()> { )?; assert!(initial_total_asset_quantity.is_zero_value()); - let register_asset = asset_ids + let register_assets = asset_ids .iter() .cloned() .map(|asset_id| Asset::new(asset_id, Metadata::default())) - .map(RegisterExpr::new) + .map(Register::asset) .collect::>(); - test_client.submit_all_blocking(register_asset)?; + test_client.submit_all_blocking(register_assets)?; // Assert that total quantity is equal to number of registrations let result = test_client.request(FindTotalAssetQuantityByAssetDefinitionId::new( @@ -182,12 +115,106 @@ fn find_asset_total_quantity() -> Result<()> { ))?; assert_eq!(NumericValue::U32(5), result); - let unregister_asset = asset_ids + let unregister_assets = asset_ids + .iter() + .cloned() + .map(Unregister::asset) + .collect::>(); + test_client.submit_all_blocking(unregister_assets)?; + + // Assert that total asset quantity is zero after unregistering asset from all accounts + let total_asset_quantity = test_client.request( + FindTotalAssetQuantityByAssetDefinitionId::new(definition_id.clone()), + )?; + assert!(total_asset_quantity.is_zero_value()); + + // Unregister asset definition + test_client.submit_blocking(Unregister::asset_definition(definition_id.clone()))?; + + // Assert that total asset quantity cleared with unregistering of asset definition + let result = test_client.request(FindTotalAssetQuantityByAssetDefinitionId::new( + definition_id, + )); + assert!(matches!( + result, + Err(ClientQueryError::Validation(ValidationFail::QueryFailed( + QueryExecutionFail::Find(_) + ))) + )); + + Ok(()) +} + +#[allow(clippy::too_many_arguments)] +fn test_total_quantity( + test_client: &Client, + accounts: &[AccountId; 5], + definition: &str, + asset_value_type: AssetValueType, + initial_value: T, + to_mint: T, + to_burn: T, + expected_total_asset_quantity: NumericValue, + mint_ctr: impl Fn(T, AssetId) -> Mint, + burn_ctr: impl Fn(T, AssetId) -> Burn, +) -> Result<()> +where + T: Copy + Into, + Value: From, + Mint: Instruction, + Burn: Instruction, +{ + // Registering new asset definition + let definition_id: AssetDefinitionId = + definition.parse().expect("Failed to parse `definition_id`"); + let asset_definition = AssetDefinition::new(definition_id.clone(), asset_value_type); + test_client.submit_blocking(Register::asset_definition(asset_definition))?; + + let asset_ids = accounts + .iter() + .cloned() + .map(|account_id| AssetId::new(definition_id.clone(), account_id)) + .collect::>(); + + // Assert that initial total quantity before any burns and mints is zero + let initial_total_asset_quantity = test_client.request( + FindTotalAssetQuantityByAssetDefinitionId::new(definition_id.clone()), + )?; + assert!(initial_total_asset_quantity.is_zero_value()); + + let register_assets = asset_ids + .iter() + .cloned() + .map(|asset_id| Asset::new(asset_id, initial_value)) + .map(Register::asset) + .collect::>(); + test_client.submit_all_blocking(register_assets)?; + + let mint_assets = asset_ids + .iter() + .cloned() + .map(|asset_id| mint_ctr(to_mint, asset_id)); + test_client.submit_all_blocking(mint_assets)?; + + let burn_assets = asset_ids + .iter() + .cloned() + .map(|asset_id| burn_ctr(to_burn, asset_id)) + .collect::>(); + test_client.submit_all_blocking(burn_assets)?; + + // Assert that total asset quantity is equal to: `n_accounts * (initial_value + to_mint - to_burn)` + let total_asset_quantity = test_client.request( + FindTotalAssetQuantityByAssetDefinitionId::new(definition_id.clone()), + )?; + assert_eq!(expected_total_asset_quantity, total_asset_quantity); + + let unregister_assets = asset_ids .iter() .cloned() - .map(UnregisterExpr::new) + .map(Unregister::asset) .collect::>(); - test_client.submit_all_blocking(unregister_asset)?; + test_client.submit_all_blocking(unregister_assets)?; // Assert that total asset quantity is zero after unregistering asset from all accounts let total_asset_quantity = test_client.request( @@ -196,7 +223,7 @@ fn find_asset_total_quantity() -> Result<()> { assert!(total_asset_quantity.is_zero_value()); // Unregister asset definition - test_client.submit_blocking(UnregisterExpr::new(definition_id.clone()))?; + test_client.submit_blocking(Unregister::asset_definition(definition_id.clone()))?; // Assert that total asset quantity cleared with unregistering of asset definition let result = test_client.request(FindTotalAssetQuantityByAssetDefinitionId::new( diff --git a/client/tests/integration/queries/role.rs b/client/tests/integration/queries/role.rs index 5432dc2e547..9d18b523910 100644 --- a/client/tests/integration/queries/role.rs +++ b/client/tests/integration/queries/role.rs @@ -29,7 +29,7 @@ fn find_roles() -> Result<()> { let register_roles = role_ids .iter() .cloned() - .map(|role_id| RegisterExpr::new(Role::new(role_id))) + .map(|role_id| Register::role(Role::new(role_id))) .collect::>(); test_client.submit_all_blocking(register_roles)?; @@ -61,7 +61,7 @@ fn find_role_ids() -> Result<()> { let register_roles = role_ids .iter() .cloned() - .map(|role_id| RegisterExpr::new(Role::new(role_id))) + .map(|role_id| Register::role(Role::new(role_id))) .collect::>(); test_client.submit_all_blocking(register_roles)?; @@ -87,7 +87,7 @@ fn find_role_by_id() -> Result<()> { let new_role = Role::new(role_id.clone()); // Registering role - let register_role = RegisterExpr::new(new_role.clone()); + let register_role = Register::role(new_role.clone()); test_client.submit_blocking(register_role)?; let found_role = test_client.request(client::role::by_id(role_id))?; @@ -130,7 +130,7 @@ fn find_roles_by_account_id() -> Result<()> { .iter() .cloned() .map(|role_id| { - RegisterExpr::new(Role::new(role_id).add_permission(PermissionToken::new( + Register::role(Role::new(role_id).add_permission(PermissionToken::new( "CanSetKeyValueInUserAccount".parse().unwrap(), &json!({ "account_id": alice_id }), ))) @@ -142,7 +142,7 @@ fn find_roles_by_account_id() -> Result<()> { let grant_roles = role_ids .iter() .cloned() - .map(|role_id| GrantExpr::new(role_id, alice_id.clone())) + .map(|role_id| Grant::role(role_id, alice_id.clone())) .collect::>(); test_client.submit_all_blocking(grant_roles)?; diff --git a/client/tests/integration/restart_peer.rs b/client/tests/integration/restart_peer.rs index cfe153e3c9d..1699ae9763e 100644 --- a/client/tests/integration/restart_peer.rs +++ b/client/tests/integration/restart_peer.rs @@ -22,7 +22,8 @@ fn restarted_peer_should_have_the_same_asset_amount() -> Result<()> { let account_id = AccountId::from_str("alice@wonderland").unwrap(); let asset_definition_id = AssetDefinitionId::from_str("xor#wonderland").unwrap(); - let create_asset = RegisterExpr::new(AssetDefinition::quantity(asset_definition_id.clone())); + let create_asset = + Register::asset_definition(AssetDefinition::quantity(asset_definition_id.clone())); let quantity: u32 = 200; let iroha_client = client::Client::test(&peer.api_address); @@ -38,12 +39,9 @@ fn restarted_peer_should_have_the_same_asset_amount() -> Result<()> { wait_for_genesis_committed(&vec![iroha_client.clone()], 0); iroha_client.submit_blocking(create_asset)?; - let mint_asset = MintExpr::new( - quantity.to_value(), - IdBox::AssetId(AssetId::new( - asset_definition_id.clone(), - account_id.clone(), - )), + let mint_asset = Mint::asset_quantity( + quantity, + AssetId::new(asset_definition_id.clone(), account_id.clone()), ); iroha_client.submit_blocking(mint_asset)?; diff --git a/client/tests/integration/roles.rs b/client/tests/integration/roles.rs index a89a939c69d..8523cfb39b6 100644 --- a/client/tests/integration/roles.rs +++ b/client/tests/integration/roles.rs @@ -14,7 +14,7 @@ fn register_empty_role() -> Result<()> { wait_for_genesis_committed(&vec![test_client.clone()], 0); let role_id = "root".parse().expect("Valid"); - let register_role = RegisterExpr::new(Role::new(role_id)); + let register_role = Register::role(Role::new(role_id)); test_client.submit(register_role)?; Ok(()) @@ -29,7 +29,7 @@ fn register_role_with_empty_token_params() -> Result<()> { let token = PermissionToken::new("token".parse()?, &json!(null)); let role = Role::new(role_id).add_permission(token); - test_client.submit(RegisterExpr::new(role))?; + test_client.submit(Register::role(role))?; Ok(()) } @@ -53,7 +53,7 @@ fn register_and_grant_role_for_metadata_access() -> Result<()> { // Registering Mouse let mouse_key_pair = iroha_crypto::KeyPair::generate()?; - let register_mouse = RegisterExpr::new(Account::new( + let register_mouse = Register::account(Account::new( mouse_id.clone(), [mouse_key_pair.public_key().clone()], )); @@ -70,18 +70,18 @@ fn register_and_grant_role_for_metadata_access() -> Result<()> { "CanRemoveKeyValueInUserAccount".parse()?, &json!({ "account_id": mouse_id }), )); - let register_role = RegisterExpr::new(role); + let register_role = Register::role(role); test_client.submit_blocking(register_role)?; // Mouse grants role to Alice - let grant_role = GrantExpr::new(role_id.clone(), alice_id.clone()); + let grant_role = Grant::role(role_id.clone(), alice_id.clone()); let grant_role_tx = TransactionBuilder::new(mouse_id.clone()) .with_instructions([grant_role]) .sign(mouse_key_pair)?; test_client.submit_transaction_blocking(&grant_role_tx)?; // Alice modifies Mouse's metadata - let set_key_value = SetKeyValueExpr::new( + let set_key_value = SetKeyValue::account( mouse_id, Name::from_str("key").expect("Valid"), Value::String("value".to_owned()), @@ -107,11 +107,11 @@ fn unregistered_role_removed_from_account() -> Result<()> { let mouse_id: AccountId = "mouse@wonderland".parse().expect("Valid"); // Registering Mouse - let register_mouse = RegisterExpr::new(Account::new(mouse_id.clone(), [])); + let register_mouse = Register::account(Account::new(mouse_id.clone(), [])); test_client.submit_blocking(register_mouse)?; // Register root role - let register_role = RegisterExpr::new(Role::new(role_id.clone()).add_permission( + let register_role = Register::role(Role::new(role_id.clone()).add_permission( PermissionToken::new( "CanSetKeyValueInUserAccount".parse()?, &json!({ "account_id": alice_id }), @@ -120,7 +120,7 @@ fn unregistered_role_removed_from_account() -> Result<()> { test_client.submit_blocking(register_role)?; // Grant root role to Mouse - let grant_role = GrantExpr::new(role_id.clone(), mouse_id.clone()); + let grant_role = Grant::role(role_id.clone(), mouse_id.clone()); test_client.submit_blocking(grant_role)?; // Check that Mouse has root role @@ -130,7 +130,7 @@ fn unregistered_role_removed_from_account() -> Result<()> { assert!(found_mouse_roles.contains(&role_id)); // Unregister root role - let unregister_role = UnregisterExpr::new(role_id.clone()); + let unregister_role = Unregister::role(role_id.clone()); test_client.submit_blocking(unregister_role)?; // Check that Mouse doesn't have the root role @@ -155,7 +155,7 @@ fn role_with_invalid_permissions_is_not_accepted() -> Result<()> { )); let err = test_client - .submit_blocking(RegisterExpr::new(role)) + .submit_blocking(Register::role(role)) .expect_err("Submitting role with invalid permission token should fail"); let rejection_reason = err diff --git a/client/tests/integration/set_parameter.rs b/client/tests/integration/set_parameter.rs index 1308c7ea141..08012429e01 100644 --- a/client/tests/integration/set_parameter.rs +++ b/client/tests/integration/set_parameter.rs @@ -14,7 +14,7 @@ fn can_change_parameter_value() -> Result<()> { let parameter = Parameter::from_str("?BlockTime=4000")?; let parameter_id = ParameterId::from_str("BlockTime")?; - let param_box = SetParameterExpr::new(parameter); + let param_box = SetParameter::new(parameter); let old_params = test_client .request(client::parameter::all())? @@ -46,13 +46,13 @@ fn parameter_propagated() -> Result<()> { wait_for_genesis_committed(&vec![test_client.clone()], 0); let too_long_domain_name: DomainId = "0".repeat(2_usize.pow(8)).parse()?; - let create_domain = RegisterExpr::new(Domain::new(too_long_domain_name)); + let create_domain = Register::domain(Domain::new(too_long_domain_name)); let _ = test_client .submit_blocking(create_domain.clone()) .expect_err("Should fail before ident length limits update"); let parameter = Parameter::from_str("?WSVIdentLengthLimits=1,256_LL")?; - let param_box = SetParameterExpr::new(parameter); + let param_box = SetParameter::new(parameter); test_client.submit_blocking(param_box)?; test_client diff --git a/client/tests/integration/smartcontracts/create_nft_for_every_user_trigger/src/lib.rs b/client/tests/integration/smartcontracts/create_nft_for_every_user_trigger/src/lib.rs index 5fa18856b48..bce2802adcb 100644 --- a/client/tests/integration/smartcontracts/create_nft_for_every_user_trigger/src/lib.rs +++ b/client/tests/integration/smartcontracts/create_nft_for_every_user_trigger/src/lib.rs @@ -43,8 +43,10 @@ fn main(_owner: AccountId, _event: Event) { let account_nft_id = AssetId::new(nft_id, account.id().clone()); let account_nft = Asset::new(account_nft_id, Metadata::new()); - RegisterExpr::new(nft_definition).execute().dbg_unwrap(); - RegisterExpr::new(account_nft).execute().dbg_unwrap(); + Register::asset_definition(nft_definition) + .execute() + .dbg_unwrap(); + Register::asset(account_nft).execute().dbg_unwrap(); } iroha_trigger::log::info!("Smart contract executed successfully"); diff --git a/client/tests/integration/smartcontracts/executor_with_admin/src/lib.rs b/client/tests/integration/smartcontracts/executor_with_admin/src/lib.rs index 1b3b6443ef8..8a950ee38cd 100644 --- a/client/tests/integration/smartcontracts/executor_with_admin/src/lib.rs +++ b/client/tests/integration/smartcontracts/executor_with_admin/src/lib.rs @@ -6,23 +6,22 @@ #[cfg(not(test))] extern crate panic_halt; -use iroha_executor::{parse, prelude::*, smart_contract}; +use iroha_executor::{parse, prelude::*}; use lol_alloc::{FreeListAllocator, LockedAllocator}; #[global_allocator] static ALLOC: LockedAllocator = LockedAllocator::new(FreeListAllocator::new()); -#[derive(Constructor, ValidateEntrypoints, ExpressionEvaluator, Validate, Visit)] +#[derive(Constructor, ValidateEntrypoints, Validate, Visit)] #[visit(custom(visit_instruction))] struct Executor { verdict: Result, block_height: u64, - host: smart_contract::Host, } -fn visit_instruction(executor: &mut Executor, authority: &AccountId, isi: &InstructionExpr) { +fn visit_instruction(executor: &mut Executor, authority: &AccountId, isi: &InstructionBox) { if parse!("admin@admin" as AccountId) == *authority { - pass!(executor); + execute!(executor, isi); } iroha_executor::default::visit_instruction(executor, authority, isi); diff --git a/client/tests/integration/smartcontracts/executor_with_custom_token/src/lib.rs b/client/tests/integration/smartcontracts/executor_with_custom_token/src/lib.rs index f75d0e43fed..bd9322a7f87 100644 --- a/client/tests/integration/smartcontracts/executor_with_custom_token/src/lib.rs +++ b/client/tests/integration/smartcontracts/executor_with_custom_token/src/lib.rs @@ -20,7 +20,7 @@ use alloc::{borrow::ToOwned, string::String}; use anyhow::anyhow; use iroha_executor::{ - default::default_permission_token_schema, permission::Token as _, prelude::*, smart_contract, + default::default_permission_token_schema, permission::Token as _, prelude::*, }; use iroha_schema::IntoSchema; use lol_alloc::{FreeListAllocator, LockedAllocator}; @@ -54,12 +54,11 @@ mod token { pub struct CanControlDomainLives; } -#[derive(Constructor, ValidateEntrypoints, ExpressionEvaluator, Validate, Visit)] +#[derive(Constructor, ValidateEntrypoints, Validate, Visit)] #[visit(custom(visit_register_domain, visit_unregister_domain))] struct Executor { verdict: Result, block_height: u64, - host: smart_contract::Host, } impl Executor { @@ -119,7 +118,7 @@ impl Executor { accounts .iter() .try_for_each(|(account, domain_id)| { - RevokeExpr::new( + Revoke::permission_token( PermissionToken::new( can_unregister_domain_definition_id.clone(), &json!({ "domain_id": domain_id }), @@ -138,7 +137,7 @@ impl Executor { ) })?; - GrantExpr::new( + Grant::permission_token( PermissionToken::new( can_control_domain_lives_definition_id.clone(), &json!(null), @@ -170,13 +169,12 @@ impl Executor { } } -// TODO (#4049): Fix unused `visit_register_domain()` -fn visit_register_domain(executor: &mut Executor, authority: &AccountId, _isi: Register) { +fn visit_register_domain(executor: &mut Executor, authority: &AccountId, isi: &Register) { if executor.block_height() == 0 { - pass!(executor) + execute!(executor, isi); } if token::CanControlDomainLives.is_owned_by(authority) { - pass!(executor); + execute!(executor, isi); } deny!( @@ -188,13 +186,13 @@ fn visit_register_domain(executor: &mut Executor, authority: &AccountId, _isi: R fn visit_unregister_domain( executor: &mut Executor, authority: &AccountId, - _isi: Unregister, + isi: &Unregister, ) { if executor.block_height() == 0 { - pass!(executor); + execute!(executor, isi); } if token::CanControlDomainLives.is_owned_by(authority) { - pass!(executor); + execute!(executor, isi); } deny!(executor, "You don't have permission to unregister domain"); diff --git a/client/tests/integration/smartcontracts/executor_with_migration_fail/src/lib.rs b/client/tests/integration/smartcontracts/executor_with_migration_fail/src/lib.rs index 437e01b9bdd..e603758dd1d 100644 --- a/client/tests/integration/smartcontracts/executor_with_migration_fail/src/lib.rs +++ b/client/tests/integration/smartcontracts/executor_with_migration_fail/src/lib.rs @@ -9,17 +9,16 @@ extern crate panic_halt; use alloc::{borrow::ToOwned as _, format}; use anyhow::anyhow; -use iroha_executor::{parse, prelude::*, smart_contract}; +use iroha_executor::{parse, prelude::*}; use lol_alloc::{FreeListAllocator, LockedAllocator}; #[global_allocator] static ALLOC: LockedAllocator = LockedAllocator::new(FreeListAllocator::new()); -#[derive(Constructor, ValidateEntrypoints, ExpressionEvaluator, Validate, Visit)] +#[derive(Constructor, ValidateEntrypoints, Validate, Visit)] struct Executor { verdict: Result, block_height: u64, - host: smart_contract::Host, } #[entrypoint] @@ -28,7 +27,7 @@ pub fn migrate(_block_height: u64) -> MigrationResult { // Registering a new domain (using ISI) let domain_id = parse!("failed_migration_test_domain" as DomainId); - RegisterExpr::new(Domain::new(domain_id)) + Register::domain(Domain::new(domain_id)) .execute() .map_err(|error| { format!( diff --git a/client/tests/integration/smartcontracts/mint_rose_trigger/src/lib.rs b/client/tests/integration/smartcontracts/mint_rose_trigger/src/lib.rs index 7dd2d5c7c0d..f794772bebd 100644 --- a/client/tests/integration/smartcontracts/mint_rose_trigger/src/lib.rs +++ b/client/tests/integration/smartcontracts/mint_rose_trigger/src/lib.rs @@ -20,7 +20,7 @@ fn main(owner: AccountId, _event: Event) { .dbg_expect("Failed to parse `rose#wonderland` asset definition id"); let rose_id = AssetId::new(rose_definition_id, owner); - MintExpr::new(1_u32, rose_id) + Mint::asset_quantity(1_u32, rose_id) .execute() .dbg_expect("Failed to mint rose"); } diff --git a/client/tests/integration/smartcontracts/query_assets_and_save_cursor/src/lib.rs b/client/tests/integration/smartcontracts/query_assets_and_save_cursor/src/lib.rs index c86e452e693..87137474596 100644 --- a/client/tests/integration/smartcontracts/query_assets_and_save_cursor/src/lib.rs +++ b/client/tests/integration/smartcontracts/query_assets_and_save_cursor/src/lib.rs @@ -26,12 +26,14 @@ fn main(owner: AccountId) { let (_batch, cursor) = asset_cursor.into_raw_parts(); - SetKeyValueExpr::new( + SetKeyValue::account( owner, parse!("cursor" as Name), - serde_json::to_value(cursor) - .dbg_expect("Failed to convert cursor to JSON") - .to_string(), + Value::String( + serde_json::to_value(cursor) + .dbg_expect("Failed to convert cursor to JSON") + .to_string(), + ), ) .execute() .dbg_expect("Failed to save cursor to the owner's metadata"); diff --git a/client/tests/integration/sorting.rs b/client/tests/integration/sorting.rs index 31b974d2b05..19f69f3b86e 100644 --- a/client/tests/integration/sorting.rs +++ b/client/tests/integration/sorting.rs @@ -14,6 +14,7 @@ use iroha_client::{ query::{Pagination, Sorting}, }, }; +use iroha_data_model::isi::InstructionBox; use test_network::*; #[test] @@ -46,8 +47,9 @@ fn correct_pagination_assets_after_creating_new_one() { assets.push(asset.clone()); - let create_asset_definition = RegisterExpr::new(asset_definition); - let create_asset = RegisterExpr::new(asset); + let create_asset_definition: InstructionBox = + Register::asset_definition(asset_definition).into(); + let create_asset = Register::asset(asset).into(); instructions.push(create_asset_definition); instructions.push(create_asset); @@ -94,8 +96,9 @@ fn correct_pagination_assets_after_creating_new_one() { AssetValue::Store(new_asset_metadata), ); - let create_asset_definition = RegisterExpr::new(new_asset_definition); - let create_asset = RegisterExpr::new(new_asset.clone()); + let create_asset_definition: InstructionBox = + Register::asset_definition(new_asset_definition).into(); + let create_asset = Register::asset(new_asset.clone()).into(); test_client .submit_all_blocking([create_asset_definition, create_asset]) @@ -153,7 +156,7 @@ fn correct_sorting_of_entities() { metadata_of_assets.push(asset_metadata); asset_definitions.push(asset_definition_id); - let create_asset_definition = RegisterExpr::new(asset_definition); + let create_asset_definition = Register::asset_definition(asset_definition); instructions.push(create_asset_definition); } @@ -203,7 +206,7 @@ fn correct_sorting_of_entities() { accounts.push(account_id); metadata_of_accounts.push(account_metadata); - let create_account = RegisterExpr::new(account); + let create_account = Register::account(account); instructions.push(create_account); } @@ -249,7 +252,7 @@ fn correct_sorting_of_entities() { domains.push(domain_id); metadata_of_domains.push(domain_metadata); - let create_account = RegisterExpr::new(domain); + let create_account = Register::domain(domain); instructions.push(create_account); } @@ -294,7 +297,7 @@ fn correct_sorting_of_entities() { domains.push(domain_id); metadata_of_domains.push(domain_metadata); - let create_account = RegisterExpr::new(domain); + let create_account = Register::domain(domain); instructions.push(create_account); } test_client @@ -356,7 +359,7 @@ fn sort_only_elements_which_have_sorting_key() -> Result<()> { account }; - let create_account = RegisterExpr::new(account); + let create_account = Register::account(account); instructions.push(create_account); } diff --git a/client/tests/integration/transfer_asset.rs b/client/tests/integration/transfer_asset.rs index b62ba7dc216..cbc23b190fa 100644 --- a/client/tests/integration/transfer_asset.rs +++ b/client/tests/integration/transfer_asset.rs @@ -3,17 +3,32 @@ use iroha_client::{ data_model::{prelude::*, Registered}, }; use iroha_crypto::KeyPair; +use iroha_data_model::isi::Instruction; use iroha_primitives::fixed::Fixed; use test_network::*; #[test] fn simulate_transfer_quantity() { - simulate_transfer(200_u32, &20_u32, AssetDefinition::quantity, 10_710) + simulate_transfer( + 200_u32, + &20_u32, + AssetDefinition::quantity, + Mint::asset_quantity, + Transfer::asset_quantity, + 10_710, + ) } #[test] fn simulate_transfer_big_quantity() { - simulate_transfer(200_u128, &20_u128, AssetDefinition::big_quantity, 10_785) + simulate_transfer( + 200_u128, + &20_u128, + AssetDefinition::big_quantity, + Mint::asset_big_quantity, + Transfer::asset_big_quantity, + 10_785, + ) } #[test] @@ -22,6 +37,8 @@ fn simulate_transfer_fixed() { Fixed::try_from(200_f64).expect("Valid"), &Fixed::try_from(20_f64).expect("Valid"), AssetDefinition::fixed, + Mint::asset_fixed, + Transfer::asset_fixed, 10_790, ) } @@ -34,22 +51,24 @@ fn simulate_insufficient_funds() { Fixed::try_from(20_f64).expect("Valid"), &Fixed::try_from(200_f64).expect("Valid"), AssetDefinition::fixed, + Mint::asset_fixed, + Transfer::asset_fixed, 10_800, ) } -// TODO add tests when the transfer uses the wrong AssetId. - -fn simulate_transfer< - T: Into + Clone, - D: FnOnce(AssetDefinitionId) -> ::With, ->( +fn simulate_transfer( starting_amount: T, amount_to_transfer: &T, - value_type: D, + asset_definition_ctr: impl FnOnce(AssetDefinitionId) -> ::With, + mint_ctr: impl FnOnce(T, AssetId) -> Mint, + transfer_ctr: impl FnOnce(AssetId, T, AccountId) -> Transfer, port_number: u16, ) where + T: std::fmt::Debug + Clone + Into, Value: From, + Mint: Instruction, + Transfer: Instruction, { let (_rt, _peer, iroha_client) = ::new() .with_port(port_number) @@ -61,15 +80,16 @@ fn simulate_transfer< let (bob_public_key, _) = KeyPair::generate() .expect("Failed to generate KeyPair") .into(); - let create_mouse = RegisterExpr::new(Account::new(mouse_id.clone(), [bob_public_key])); + let create_mouse = Register::account(Account::new(mouse_id.clone(), [bob_public_key])); let asset_definition_id: AssetDefinitionId = "camomile#wonderland".parse().expect("Valid"); - let create_asset = RegisterExpr::new(value_type(asset_definition_id.clone())); - let mint_asset = MintExpr::new( - starting_amount.to_value(), - IdBox::AssetId(AssetId::new(asset_definition_id.clone(), alice_id.clone())), + let create_asset = + Register::asset_definition(asset_definition_ctr(asset_definition_id.clone())); + let mint_asset = mint_ctr( + starting_amount, + AssetId::new(asset_definition_id.clone(), alice_id.clone()), ); - let instructions: [InstructionExpr; 3] = [ + let instructions: [InstructionBox; 3] = [ // create_alice.into(), We don't need to register Alice, because she is created in genesis create_mouse.into(), create_asset.into(), @@ -80,10 +100,10 @@ fn simulate_transfer< .expect("Failed to prepare state."); //When - let transfer_asset = TransferExpr::new( - IdBox::AssetId(AssetId::new(asset_definition_id.clone(), alice_id)), - amount_to_transfer.clone().to_value(), - IdBox::AccountId(mouse_id.clone()), + let transfer_asset = transfer_ctr( + AssetId::new(asset_definition_id.clone(), alice_id), + amount_to_transfer.clone(), + mouse_id.clone(), ); iroha_client .submit_till( diff --git a/client/tests/integration/triggers/by_call_trigger.rs b/client/tests/integration/triggers/by_call_trigger.rs index a2b03df24d7..41644169a73 100644 --- a/client/tests/integration/triggers/by_call_trigger.rs +++ b/client/tests/integration/triggers/by_call_trigger.rs @@ -9,6 +9,7 @@ use iroha_client::{ transaction::Executable, }, }; +use iroha_data_model::events::TriggeringFilterBox; use iroha_genesis::GenesisNetwork; use iroha_logger::info; use test_network::*; @@ -25,12 +26,12 @@ fn call_execute_trigger() -> Result<()> { let asset_id = AssetId::new(asset_definition_id, account_id); let prev_value = get_asset_value(&mut test_client, asset_id.clone())?; - let instruction = MintExpr::new(1_u32, asset_id.clone()); + let instruction = Mint::asset_quantity(1_u32, asset_id.clone()); let register_trigger = build_register_trigger_isi(asset_id.clone(), vec![instruction.into()]); test_client.submit_blocking(register_trigger)?; let trigger_id = TriggerId::from_str(TRIGGER_NAME)?; - let call_trigger = ExecuteTriggerExpr::new(trigger_id); + let call_trigger = ExecuteTrigger::new(trigger_id); test_client.submit_blocking(call_trigger)?; let new_value = get_asset_value(&mut test_client, asset_id)?; @@ -48,12 +49,12 @@ fn execute_trigger_should_produce_event() -> Result<()> { let account_id: AccountId = "alice@wonderland".parse()?; let asset_id = AssetId::new(asset_definition_id, account_id.clone()); - let instruction = MintExpr::new(1_u32, asset_id.clone()); + let instruction = Mint::asset_quantity(1_u32, asset_id.clone()); let register_trigger = build_register_trigger_isi(asset_id, vec![instruction.into()]); test_client.submit_blocking(register_trigger)?; let trigger_id = TriggerId::from_str(TRIGGER_NAME)?; - let call_trigger = ExecuteTriggerExpr::new(trigger_id.clone()); + let call_trigger = ExecuteTrigger::new(trigger_id.clone()); let thread_client = test_client.clone(); let (sender, receiver) = mpsc::channel(); @@ -83,11 +84,11 @@ fn infinite_recursion_should_produce_one_call_per_block() -> Result<()> { let account_id = "alice@wonderland".parse()?; let asset_id = AssetId::new(asset_definition_id, account_id); let trigger_id = TriggerId::from_str(TRIGGER_NAME)?; - let call_trigger = ExecuteTriggerExpr::new(trigger_id); + let call_trigger = ExecuteTrigger::new(trigger_id); let prev_value = get_asset_value(&mut test_client, asset_id.clone())?; let instructions = vec![ - MintExpr::new(1_u32, asset_id.clone()).into(), + Mint::asset_quantity(1_u32, asset_id.clone()).into(), call_trigger.clone().into(), ]; let register_trigger = build_register_trigger_isi(asset_id.clone(), instructions); @@ -113,8 +114,8 @@ fn trigger_failure_should_not_cancel_other_triggers_execution() -> Result<()> { // Registering trigger that should fail on execution let bad_trigger_id = TriggerId::from_str("bad_trigger")?; // Invalid instruction - let bad_trigger_instructions = vec![MintExpr::new(1_u32, account_id.clone())]; - let register_bad_trigger = RegisterExpr::new(Trigger::new( + let bad_trigger_instructions = vec![Fail::new("Bad trigger".to_owned())]; + let register_bad_trigger = Register::trigger(Trigger::new( bad_trigger_id.clone(), Action::new( bad_trigger_instructions, @@ -130,8 +131,8 @@ fn trigger_failure_should_not_cancel_other_triggers_execution() -> Result<()> { // Registering normal trigger let trigger_id = TriggerId::from_str(TRIGGER_NAME)?; - let trigger_instructions = vec![MintExpr::new(1_u32, asset_id.clone())]; - let register_trigger = RegisterExpr::new(Trigger::new( + let trigger_instructions = vec![Mint::asset_quantity(1_u32, asset_id.clone())]; + let register_trigger = Register::trigger(Trigger::new( trigger_id, Action::new( trigger_instructions, @@ -147,7 +148,7 @@ fn trigger_failure_should_not_cancel_other_triggers_execution() -> Result<()> { let prev_asset_value = get_asset_value(&mut test_client, asset_id.clone())?; // Executing bad trigger - test_client.submit_blocking(ExecuteTriggerExpr::new(bad_trigger_id))?; + test_client.submit_blocking(ExecuteTrigger::new(bad_trigger_id))?; // Checking results let new_asset_value = get_asset_value(&mut test_client, asset_id)?; @@ -165,8 +166,8 @@ fn trigger_should_not_be_executed_with_zero_repeats_count() -> Result<()> { let asset_id = AssetId::new(asset_definition_id, account_id.clone()); let trigger_id = TriggerId::from_str("self_modifying_trigger")?; - let trigger_instructions = vec![MintExpr::new(1_u32, asset_id.clone())]; - let register_trigger = RegisterExpr::new(Trigger::new( + let trigger_instructions = vec![Mint::asset_quantity(1_u32, asset_id.clone())]; + let register_trigger = Register::trigger(Trigger::new( trigger_id.clone(), Action::new( trigger_instructions, @@ -184,7 +185,7 @@ fn trigger_should_not_be_executed_with_zero_repeats_count() -> Result<()> { let prev_asset_value = get_asset_value(&mut test_client, asset_id.clone())?; // Executing trigger first time - let execute_trigger = ExecuteTriggerExpr::new(trigger_id.clone()); + let execute_trigger = ExecuteTrigger::new(trigger_id.clone()); test_client.submit_blocking(execute_trigger.clone())?; // Executing trigger second time @@ -224,10 +225,10 @@ fn trigger_should_be_able_to_modify_its_own_repeats_count() -> Result<()> { let trigger_id = TriggerId::from_str("self_modifying_trigger")?; let trigger_instructions = vec![ - MintExpr::new(1_u32, trigger_id.clone()), - MintExpr::new(1_u32, asset_id.clone()), + InstructionBox::from(Mint::trigger_repetitions(1_u32, trigger_id.clone())), + InstructionBox::from(Mint::asset_quantity(1_u32, asset_id.clone())), ]; - let register_trigger = RegisterExpr::new(Trigger::new( + let register_trigger = Register::trigger(Trigger::new( trigger_id.clone(), Action::new( trigger_instructions, @@ -245,7 +246,7 @@ fn trigger_should_be_able_to_modify_its_own_repeats_count() -> Result<()> { let prev_asset_value = get_asset_value(&mut test_client, asset_id.clone())?; // Executing trigger first time - let execute_trigger = ExecuteTriggerExpr::new(trigger_id); + let execute_trigger = ExecuteTrigger::new(trigger_id); test_client.submit_blocking(execute_trigger.clone())?; // Executing trigger second time @@ -270,7 +271,7 @@ fn unregister_trigger() -> Result<()> { let trigger = Trigger::new( trigger_id.clone(), Action::new( - Vec::::new(), + Vec::::new(), Repeats::Indefinitely, account_id.clone(), TriggeringFilterBox::ExecuteTrigger(ExecuteTriggerEventFilter::new( @@ -279,12 +280,12 @@ fn unregister_trigger() -> Result<()> { )), ), ); - let register_trigger = RegisterExpr::new(trigger.clone()); + let register_trigger = Register::trigger(trigger.clone()); test_client.submit_blocking(register_trigger)?; // Finding trigger let find_trigger = FindTriggerById { - id: trigger_id.clone().into(), + id: trigger_id.clone(), }; let found_trigger = test_client.request(find_trigger.clone())?; let found_action = found_trigger.action; @@ -303,7 +304,7 @@ fn unregister_trigger() -> Result<()> { assert_eq!(found_trigger, trigger); // Unregistering trigger - let unregister_trigger = UnregisterExpr::new(trigger_id); + let unregister_trigger = Unregister::trigger(trigger_id); test_client.submit_blocking(unregister_trigger)?; // Checking result @@ -362,7 +363,7 @@ fn trigger_in_genesis_using_base64() -> Result<()> { let tx_ref = &mut genesis.transactions[0].0; match &mut tx_ref.payload_mut().instructions { Executable::Instructions(instructions) => { - instructions.push(RegisterExpr::new(trigger).into()); + instructions.push(Register::trigger(trigger).into()); } Executable::Wasm(_) => panic!("Expected instructions"), } @@ -378,7 +379,7 @@ fn trigger_in_genesis_using_base64() -> Result<()> { let prev_value = get_asset_value(&mut test_client, asset_id.clone())?; // Executing trigger - let call_trigger = ExecuteTriggerExpr::new(trigger_id); + let call_trigger = ExecuteTrigger::new(trigger_id); test_client.submit_blocking(call_trigger)?; // Checking result @@ -397,12 +398,11 @@ fn trigger_should_be_able_to_modify_other_trigger() -> Result<()> { let account_id = AccountId::from_str("alice@wonderland")?; let asset_id = AssetId::new(asset_definition_id, account_id.clone()); let trigger_id_unregister = TriggerId::from_str("unregister_other_trigger")?; - let trigger_id_should_be_unregistered = TriggerId::from_str("should_be_unregistered_trigger")?; + let trigger_id_to_be_unregistered = TriggerId::from_str("should_be_unregistered_trigger")?; - let trigger_unregister_instructions = vec![UnregisterExpr::new( - trigger_id_should_be_unregistered.clone(), - )]; - let register_trigger = RegisterExpr::new(Trigger::new( + let trigger_unregister_instructions = + vec![Unregister::trigger(trigger_id_to_be_unregistered.clone())]; + let register_trigger = Register::trigger(Trigger::new( trigger_id_unregister.clone(), Action::new( trigger_unregister_instructions, @@ -416,15 +416,16 @@ fn trigger_should_be_able_to_modify_other_trigger() -> Result<()> { )); test_client.submit_blocking(register_trigger)?; - let trigger_should_be_unregistered_instructions = vec![MintExpr::new(1_u32, asset_id.clone())]; - let register_trigger = RegisterExpr::new(Trigger::new( - trigger_id_should_be_unregistered.clone(), + let trigger_should_be_unregistered_instructions = + vec![Mint::asset_quantity(1_u32, asset_id.clone())]; + let register_trigger = Register::trigger(Trigger::new( + trigger_id_to_be_unregistered.clone(), Action::new( trigger_should_be_unregistered_instructions, Repeats::from(1_u32), account_id.clone(), TriggeringFilterBox::ExecuteTrigger(ExecuteTriggerEventFilter::new( - trigger_id_should_be_unregistered.clone(), + trigger_id_to_be_unregistered.clone(), account_id, )), ), @@ -435,9 +436,8 @@ fn trigger_should_be_able_to_modify_other_trigger() -> Result<()> { let prev_asset_value = get_asset_value(&mut test_client, asset_id.clone())?; // Executing triggers - let execute_trigger_unregister = ExecuteTriggerExpr::new(trigger_id_unregister); - let execute_trigger_should_be_unregistered = - ExecuteTriggerExpr::new(trigger_id_should_be_unregistered); + let execute_trigger_unregister = ExecuteTrigger::new(trigger_id_unregister); + let execute_trigger_should_be_unregistered = ExecuteTrigger::new(trigger_id_to_be_unregistered); test_client.submit_all_blocking([ execute_trigger_unregister, execute_trigger_should_be_unregistered, @@ -461,8 +461,8 @@ fn trigger_burn_repetitions() -> Result<()> { let asset_id = AssetId::new(asset_definition_id, account_id.clone()); let trigger_id = TriggerId::from_str("trigger")?; - let trigger_instructions = vec![MintExpr::new(1_u32, asset_id)]; - let register_trigger = RegisterExpr::new(Trigger::new( + let trigger_instructions = vec![Mint::asset_quantity(1_u32, asset_id)]; + let register_trigger = Register::trigger(Trigger::new( trigger_id.clone(), Action::new( trigger_instructions, @@ -476,10 +476,10 @@ fn trigger_burn_repetitions() -> Result<()> { )); test_client.submit_blocking(register_trigger)?; - test_client.submit_blocking(BurnExpr::new(1_u32, trigger_id.clone()))?; + test_client.submit_blocking(Burn::trigger_repetitions(1_u32, trigger_id.clone()))?; // Executing trigger - let execute_trigger = ExecuteTriggerExpr::new(trigger_id); + let execute_trigger = ExecuteTrigger::new(trigger_id); let _err = test_client .submit_blocking(execute_trigger) .expect_err("Should fail without repetitions"); @@ -494,11 +494,11 @@ fn get_asset_value(client: &mut Client, asset_id: AssetId) -> Result { fn build_register_trigger_isi( asset_id: AssetId, - trigger_instructions: Vec, -) -> RegisterExpr { + trigger_instructions: Vec, +) -> Register> { let trigger_id: TriggerId = TRIGGER_NAME.parse().expect("Valid"); - RegisterExpr::new(Trigger::new( + Register::trigger(Trigger::new( trigger_id.clone(), Action::new( trigger_instructions, diff --git a/client/tests/integration/triggers/data_trigger.rs b/client/tests/integration/triggers/data_trigger.rs index 46744151559..e7096f7b024 100644 --- a/client/tests/integration/triggers/data_trigger.rs +++ b/client/tests/integration/triggers/data_trigger.rs @@ -13,8 +13,8 @@ fn must_execute_both_triggers() -> Result<()> { let prev_value = get_asset_value(&test_client, asset_id.clone())?; - let instruction = MintExpr::new(1_u32, asset_id.clone()); - let register_trigger = RegisterExpr::new(Trigger::new( + let instruction = Mint::asset_quantity(1_u32, asset_id.clone()); + let register_trigger = Register::trigger(Trigger::new( "mint_rose_1".parse()?, Action::new( [instruction.clone()], @@ -27,7 +27,7 @@ fn must_execute_both_triggers() -> Result<()> { )); test_client.submit_blocking(register_trigger)?; - let register_trigger = RegisterExpr::new(Trigger::new( + let register_trigger = Register::trigger(Trigger::new( "mint_rose_2".parse()?, Action::new( [instruction], @@ -40,11 +40,11 @@ fn must_execute_both_triggers() -> Result<()> { )); test_client.submit_blocking(register_trigger)?; - test_client.submit_blocking(RegisterExpr::new(Account::new( + test_client.submit_blocking(Register::account(Account::new( "bunny@wonderland".parse()?, [], )))?; - test_client.submit_blocking(RegisterExpr::new(Domain::new("neverland".parse()?)))?; + test_client.submit_blocking(Register::domain(Domain::new("neverland".parse()?)))?; let new_value = get_asset_value(&test_client, asset_id)?; assert_eq!(new_value, prev_value + 2); @@ -57,18 +57,19 @@ fn domain_scoped_trigger_must_be_executed_only_on_events_in_its_domain() -> Resu let (_rt, _peer, test_client) = ::new().with_port(10_655).start_with_runtime(); wait_for_genesis_committed(&[test_client.clone()], 0); - let create_neverland_domain = RegisterExpr::new(Domain::new("neverland".parse()?)); + let create_neverland_domain: InstructionBox = + Register::domain(Domain::new("neverland".parse()?)).into(); let account_id: AccountId = "sapporo@neverland".parse()?; - let create_sapporo_account = RegisterExpr::new(Account::new(account_id.clone(), [])); + let create_sapporo_account = Register::account(Account::new(account_id.clone(), [])).into(); let asset_definition_id: AssetDefinitionId = "sakura#neverland".parse()?; let create_sakura_asset_definition = - RegisterExpr::new(AssetDefinition::quantity(asset_definition_id.clone())); + Register::asset_definition(AssetDefinition::quantity(asset_definition_id.clone())).into(); let asset_id = AssetId::new(asset_definition_id, account_id.clone()); let create_sakura_asset = - RegisterExpr::new(Asset::new(asset_id.clone(), AssetValue::Quantity(0))); + Register::asset(Asset::new(asset_id.clone(), AssetValue::Quantity(0))).into(); test_client.submit_all_blocking([ create_neverland_domain, @@ -79,10 +80,10 @@ fn domain_scoped_trigger_must_be_executed_only_on_events_in_its_domain() -> Resu let prev_value = get_asset_value(&test_client, asset_id.clone())?; - let register_trigger = RegisterExpr::new(Trigger::new( + let register_trigger = Register::trigger(Trigger::new( "mint_sakura$neverland".parse()?, Action::new( - [MintExpr::new(1_u32, asset_id.clone())], + [Mint::asset_quantity(1_u32, asset_id.clone())], Repeats::Indefinitely, account_id, TriggeringFilterBox::Data(BySome(DataEntityFilter::ByAccount(BySome( @@ -92,12 +93,12 @@ fn domain_scoped_trigger_must_be_executed_only_on_events_in_its_domain() -> Resu )); test_client.submit_blocking(register_trigger)?; - test_client.submit_blocking(RegisterExpr::new(Account::new( + test_client.submit_blocking(Register::account(Account::new( "asahi@wonderland".parse()?, [], )))?; - test_client.submit_blocking(RegisterExpr::new(Account::new( + test_client.submit_blocking(Register::account(Account::new( "asahi@neverland".parse()?, [], )))?; diff --git a/client/tests/integration/triggers/event_trigger.rs b/client/tests/integration/triggers/event_trigger.rs index 8b438d47fb9..8269a244ad4 100644 --- a/client/tests/integration/triggers/event_trigger.rs +++ b/client/tests/integration/triggers/event_trigger.rs @@ -17,8 +17,8 @@ fn test_mint_asset_when_new_asset_definition_created() -> Result<()> { let asset_id = AssetId::new(asset_definition_id, account_id.clone()); let prev_value = get_asset_value(&mut test_client, asset_id.clone())?; - let instruction = MintExpr::new(1_u32, asset_id.clone()); - let register_trigger = RegisterExpr::new(Trigger::new( + let instruction = Mint::asset_quantity(1_u32, asset_id.clone()); + let register_trigger = Register::trigger(Trigger::new( "mint_rose".parse()?, Action::new( vec![instruction], @@ -35,7 +35,8 @@ fn test_mint_asset_when_new_asset_definition_created() -> Result<()> { test_client.submit(register_trigger)?; let tea_definition_id = "tea#wonderland".parse()?; - let register_tea_definition = RegisterExpr::new(AssetDefinition::quantity(tea_definition_id)); + let register_tea_definition = + Register::asset_definition(AssetDefinition::quantity(tea_definition_id)); test_client.submit_blocking(register_tea_definition)?; let new_value = get_asset_value(&mut test_client, asset_id)?; diff --git a/client/tests/integration/triggers/time_trigger.rs b/client/tests/integration/triggers/time_trigger.rs index 2a150f336c4..9b9c76d3fe6 100644 --- a/client/tests/integration/triggers/time_trigger.rs +++ b/client/tests/integration/triggers/time_trigger.rs @@ -44,8 +44,8 @@ fn time_trigger_execution_count_error_should_be_less_than_15_percent() -> Result let schedule = TimeSchedule::starting_at(start_time).with_period(Duration::from_millis(PERIOD_MS)); - let instruction = MintExpr::new(1_u32, asset_id.clone()); - let register_trigger = RegisterExpr::new(Trigger::new( + let instruction = Mint::asset_quantity(1_u32, asset_id.clone()); + let register_trigger = Register::trigger(Trigger::new( "mint_rose".parse()?, Action::new( vec![instruction], @@ -98,8 +98,8 @@ fn change_asset_metadata_after_1_sec() -> Result<()> { let schedule = TimeSchedule::starting_at(start_time + Duration::from_millis(PERIOD_MS)); let instruction = - SetKeyValueExpr::new(asset_definition_id.clone(), key.clone(), 3_u32.to_value()); - let register_trigger = RegisterExpr::new(Trigger::new( + SetKeyValue::asset_definition(asset_definition_id.clone(), key.clone(), 3_u32.to_value()); + let register_trigger = Register::trigger(Trigger::new( "change_rose_metadata".parse().expect("Valid"), Action::new( vec![instruction], @@ -119,8 +119,8 @@ fn change_asset_metadata_after_1_sec() -> Result<()> { let value = test_client .request(FindAssetDefinitionKeyValueByIdAndKey { - id: asset_definition_id.into(), - key: key.into(), + id: asset_definition_id, + key, })? .into(); assert!(matches!(value, Value::Numeric(NumericValue::U32(3_u32)))); @@ -144,8 +144,8 @@ fn pre_commit_trigger_should_be_executed() -> Result<()> { // Start listening BEFORE submitting any transaction not to miss any block committed event let event_listener = get_block_committed_event_listener(&test_client)?; - let instruction = MintExpr::new(1_u32, asset_id.clone()); - let register_trigger = RegisterExpr::new(Trigger::new( + let instruction = Mint::asset_quantity(1_u32, asset_id.clone()); + let register_trigger = Register::trigger(Trigger::new( "mint_rose".parse()?, Action::new( vec![instruction], @@ -162,7 +162,7 @@ fn pre_commit_trigger_should_be_executed() -> Result<()> { prev_value = new_value; // ISI just to create a new block - let sample_isi = SetKeyValueExpr::new( + let sample_isi = SetKeyValue::account( account_id.clone(), "key".parse::()?, String::from("value"), @@ -196,7 +196,7 @@ fn mint_nft_for_every_user_every_1_sec() -> Result<()> { .iter() .skip(1) // Alice has already been registered in genesis .cloned() - .map(|account_id| RegisterExpr::new(Account::new(account_id, []))) + .map(|account_id| Register::account(Account::new(account_id, []))) .collect::>(); test_client.submit_all_blocking(register_accounts)?; @@ -220,7 +220,7 @@ fn mint_nft_for_every_user_every_1_sec() -> Result<()> { let start_time = current_time(); let schedule = TimeSchedule::starting_at(start_time).with_period(Duration::from_millis(TRIGGER_PERIOD_MS)); - let register_trigger = RegisterExpr::new(Trigger::new( + let register_trigger = Register::trigger(Trigger::new( "mint_nft_for_all".parse()?, Action::new( WasmSmartContract::from_compiled(wasm), @@ -295,7 +295,7 @@ fn submit_sample_isi_on_every_block_commit( for _ in block_committed_event_listener.take(times) { std::thread::sleep(timeout); // ISI just to create a new block - let sample_isi = SetKeyValueExpr::new( + let sample_isi = SetKeyValue::account( account_id.clone(), "key".parse::()?, String::from("value"), diff --git a/client/tests/integration/triggers/trigger_rollback.rs b/client/tests/integration/triggers/trigger_rollback.rs index 67861a9f7b2..182045a2c7e 100644 --- a/client/tests/integration/triggers/trigger_rollback.rs +++ b/client/tests/integration/triggers/trigger_rollback.rs @@ -14,9 +14,13 @@ fn failed_trigger_revert() -> Result<()> { let trigger_id = TriggerId::from_str("trigger")?; let account_id = AccountId::from_str("alice@wonderland")?; let asset_definition_id = AssetDefinitionId::from_str("xor#wonderland")?; - let create_asset = RegisterExpr::new(AssetDefinition::quantity(asset_definition_id.clone())); - let instructions: [InstructionExpr; 2] = [create_asset.into(), Fail::new("Always fail").into()]; - let register_trigger = RegisterExpr::new(Trigger::new( + let create_asset = + Register::asset_definition(AssetDefinition::quantity(asset_definition_id.clone())); + let instructions: [InstructionBox; 2] = [ + create_asset.into(), + Fail::new("Always fail".to_owned()).into(), + ]; + let register_trigger = Register::trigger(Trigger::new( trigger_id.clone(), Action::new( instructions, @@ -30,7 +34,7 @@ fn failed_trigger_revert() -> Result<()> { )); let _ = client.submit_blocking(register_trigger); - let call_trigger = ExecuteTriggerExpr::new(trigger_id); + let call_trigger = ExecuteTrigger::new(trigger_id); client.submit_blocking(call_trigger)?; //Then diff --git a/client/tests/integration/tx_history.rs b/client/tests/integration/tx_history.rs index 15c2ef3c3e1..8cbf9a3f5cc 100644 --- a/client/tests/integration/tx_history.rs +++ b/client/tests/integration/tx_history.rs @@ -24,19 +24,20 @@ fn client_has_rejected_and_acepted_txs_should_return_tx_history() -> Result<()> // Given let account_id = AccountId::from_str("alice@wonderland")?; let asset_definition_id = AssetDefinitionId::from_str("xor#wonderland")?; - let create_asset = RegisterExpr::new(AssetDefinition::quantity(asset_definition_id.clone())); + let create_asset = + Register::asset_definition(AssetDefinition::quantity(asset_definition_id.clone())); client.submit_blocking(create_asset)?; //When let quantity: u32 = 200; let asset_id = AssetId::new(asset_definition_id, account_id.clone()); - let mint_existed_asset = MintExpr::new(quantity.to_value(), IdBox::AssetId(asset_id)); - let mint_not_existed_asset = MintExpr::new( - quantity.to_value(), - IdBox::AssetId(AssetId::new( + let mint_existed_asset = Mint::asset_quantity(quantity, asset_id); + let mint_not_existed_asset = Mint::asset_quantity( + quantity, + AssetId::new( AssetDefinitionId::from_str("foo#wonderland")?, account_id.clone(), - )), + ), ); let transactions_count = 100; @@ -47,7 +48,7 @@ fn client_has_rejected_and_acepted_txs_should_return_tx_history() -> Result<()> } else { &mint_not_existed_asset }; - let instructions: Vec = vec![mint_asset.clone().into()]; + let instructions: Vec = vec![mint_asset.clone().into()]; let transaction = client.build_transaction(instructions, UnlimitedMetadata::new())?; client.submit_transaction(&transaction)?; } diff --git a/client/tests/integration/tx_rollback.rs b/client/tests/integration/tx_rollback.rs index a8b1b918dc9..0c04bbec3a8 100644 --- a/client/tests/integration/tx_rollback.rs +++ b/client/tests/integration/tx_rollback.rs @@ -16,16 +16,13 @@ fn client_sends_transaction_with_invalid_instruction_should_not_see_any_changes( let account_id = AccountId::from_str("alice@wonderland")?; let asset_definition_id = AssetDefinitionId::from_str("xor#wonderland")?; let wrong_asset_definition_id = AssetDefinitionId::from_str("ksor#wonderland")?; - let create_asset = RegisterExpr::new(AssetDefinition::quantity(asset_definition_id)); + let create_asset = Register::asset_definition(AssetDefinition::quantity(asset_definition_id)); let quantity: u32 = 200; - let mint_asset = MintExpr::new( - quantity.to_value(), - IdBox::AssetId(AssetId::new( - wrong_asset_definition_id.clone(), - account_id.clone(), - )), + let mint_asset = Mint::asset_quantity( + quantity, + AssetId::new(wrong_asset_definition_id.clone(), account_id.clone()), ); - let instructions: [InstructionExpr; 2] = [create_asset.into(), mint_asset.into()]; + let instructions: [InstructionBox; 2] = [create_asset.into(), mint_asset.into()]; let _ = client.submit_all_blocking(instructions); //Then diff --git a/client/tests/integration/unregister_peer.rs b/client/tests/integration/unregister_peer.rs index 845f938de16..84a2e4fa5b3 100644 --- a/client/tests/integration/unregister_peer.rs +++ b/client/tests/integration/unregister_peer.rs @@ -34,7 +34,7 @@ fn unstable_network_stable_after_add_and_after_remove_peer() -> Result<()> { // Then the new peer should already have the mint result. check_assets(&peer_client, &account_id, &asset_definition_id, 100); // Also, when a peer is unregistered - let remove_peer = UnregisterExpr::new(IdBox::PeerId(peer.id.clone())); + let remove_peer = Unregister::peer(peer.id.clone()); genesis_client.submit(remove_peer)?; thread::sleep(pipeline_time * 2); // We can mint without error. @@ -82,12 +82,9 @@ fn mint( pipeline_time: std::time::Duration, quantity: u32, ) -> Result { - let mint_asset = MintExpr::new( - quantity.to_value(), - IdBox::AssetId(AssetId::new( - asset_definition_id.clone(), - account_id.clone(), - )), + let mint_asset = Mint::asset_quantity( + quantity, + AssetId::new(asset_definition_id.clone(), account_id.clone()), ); client.submit(mint_asset)?; thread::sleep(pipeline_time * 5); @@ -109,18 +106,21 @@ fn init() -> Result<( let parameters = ParametersBuilder::new() .add_parameter(MAX_TRANSACTIONS_IN_BLOCK, 1u32)? .into_set_parameters(); - let create_domain = RegisterExpr::new(Domain::new("domain".parse()?)); + let create_domain = Register::domain(Domain::new("domain".parse()?)); let account_id: AccountId = "account@domain".parse()?; let (public_key, _) = KeyPair::generate()?.into(); - let create_account = RegisterExpr::new(Account::new(account_id.clone(), [public_key])); + let create_account = Register::account(Account::new(account_id.clone(), [public_key])); let asset_definition_id: AssetDefinitionId = "xor#domain".parse()?; - let create_asset = RegisterExpr::new(AssetDefinition::quantity(asset_definition_id.clone())); - let instructions: [InstructionExpr; 4] = [ - parameters.into(), - create_domain.into(), - create_account.into(), - create_asset.into(), - ]; + let create_asset = + Register::asset_definition(AssetDefinition::quantity(asset_definition_id.clone())); + let instructions = parameters.into_iter().chain( + [ + create_domain.into(), + create_account.into(), + create_asset.into(), + ] + .into_iter(), + ); client.submit_all_blocking(instructions)?; iroha_logger::info!("Init"); Ok(( diff --git a/client/tests/integration/unstable_network.rs b/client/tests/integration/unstable_network.rs index da962f4728d..d0cd9ce186b 100644 --- a/client/tests/integration/unstable_network.rs +++ b/client/tests/integration/unstable_network.rs @@ -77,7 +77,8 @@ fn unstable_network( let account_id: AccountId = "alice@wonderland".parse().expect("Valid"); let asset_definition_id: AssetDefinitionId = "camomile#wonderland".parse().expect("Valid"); - let register_asset = RegisterExpr::new(AssetDefinition::quantity(asset_definition_id.clone())); + let register_asset = + Register::asset_definition(AssetDefinition::quantity(asset_definition_id.clone())); iroha_client .submit_blocking(register_asset) .expect("Failed to register asset"); @@ -99,12 +100,9 @@ fn unstable_network( } let quantity = 1; - let mint_asset = MintExpr::new( - quantity.to_value(), - IdBox::AssetId(AssetId::new( - asset_definition_id.clone(), - account_id.clone(), - )), + let mint_asset = Mint::asset_quantity( + quantity, + AssetId::new(asset_definition_id.clone(), account_id.clone()), ); iroha_client .submit(mint_asset) diff --git a/client/tests/integration/upgrade.rs b/client/tests/integration/upgrade.rs index 5c1de0736c4..cfa822046c2 100644 --- a/client/tests/integration/upgrade.rs +++ b/client/tests/integration/upgrade.rs @@ -17,19 +17,19 @@ fn executor_upgrade_should_work() -> Result<()> { // Register `admin` domain and account let admin_domain = Domain::new("admin".parse()?); - let register_admin_domain = RegisterExpr::new(admin_domain); + let register_admin_domain = Register::domain(admin_domain); client.submit_blocking(register_admin_domain)?; let admin_id: AccountId = "admin@admin".parse()?; let admin_keypair = KeyPair::generate()?; let admin_account = Account::new(admin_id.clone(), [admin_keypair.public_key().clone()]); - let register_admin_account = RegisterExpr::new(admin_account); + let register_admin_account = Register::account(admin_account); client.submit_blocking(register_admin_account)?; // Check that admin isn't allowed to transfer alice's rose by default let alice_rose: AssetId = "rose##alice@wonderland".parse()?; let admin_rose: AccountId = "admin@admin".parse()?; - let transfer_alice_rose = TransferExpr::new(alice_rose, NumericValue::U32(1), admin_rose); + let transfer_alice_rose = Transfer::asset_quantity(alice_rose, 1_u32, admin_rose); let transfer_rose_tx = TransactionBuilder::new(admin_id.clone()) .with_instructions([transfer_alice_rose.clone()]) .sign(admin_keypair.clone())?; @@ -152,7 +152,7 @@ fn upgrade_executor(client: &Client, executor: impl AsRef) -> Result<()> { info!("WASM size is {} bytes", wasm.len()); - let upgrade_executor = UpgradeExpr::new(Executor::new(WasmSmartContract::from_compiled(wasm))); + let upgrade_executor = Upgrade::new(Executor::new(WasmSmartContract::from_compiled(wasm))); client.submit_blocking(upgrade_executor)?; Ok(()) diff --git a/client_cli/src/main.rs b/client_cli/src/main.rs index c16c633fd56..8aca7cef98c 100644 --- a/client_cli/src/main.rs +++ b/client_cli/src/main.rs @@ -391,7 +391,7 @@ mod domain { id, metadata: Metadata(metadata), } = self; - let create_domain = RegisterExpr::new(Domain::new(id)); + let create_domain = iroha_client::data_model::isi::Register::domain(Domain::new(id)); submit([create_domain], metadata, context).wrap_err("Failed to create domain") } } @@ -449,7 +449,7 @@ mod domain { to, metadata: Metadata(metadata), } = self; - let transfer_domain = TransferExpr::new(from, id, to); + let transfer_domain = iroha_client::data_model::isi::Transfer::domain(from, id, to); submit([transfer_domain], metadata, context).wrap_err("Failed to transfer domain") } } @@ -512,7 +512,8 @@ mod account { key, metadata: Metadata(metadata), } = self; - let create_account = RegisterExpr::new(Account::new(id, [key])); + let create_account = + iroha_client::data_model::isi::Register::account(Account::new(id, [key])); submit([create_account], metadata, context).wrap_err("Failed to register account") } } @@ -558,12 +559,12 @@ mod account { impl RunArgs for SignatureCondition { fn run(self, context: &mut dyn RunContext) -> Result<()> { - let account = Account::new(context.configuration().account_id.clone(), []); + let account_id = context.configuration().account_id.clone(); let Self { condition: Signature(condition), metadata: Metadata(metadata), } = self; - let mint_box = MintExpr::new(account, EvaluatesTo::new_unchecked(condition)); + let mint_box = Mint::account_signature_check_condition(condition, account_id); submit([mint_box], metadata, context).wrap_err("Failed to set signature condition") } } @@ -634,7 +635,7 @@ mod account { permission, metadata: Metadata(metadata), } = self; - let grant = GrantExpr::new(permission.0, id); + let grant = iroha_client::data_model::isi::Grant::permission_token(permission.0, id); submit([grant], metadata, context) .wrap_err("Failed to grant the permission to the account") } @@ -727,7 +728,8 @@ mod asset { if unmintable { asset_definition = asset_definition.mintable_once(); } - let create_asset_definition = RegisterExpr::new(asset_definition); + let create_asset_definition = + iroha_client::data_model::isi::Register::asset_definition(asset_definition); submit([create_asset_definition], metadata, context) .wrap_err("Failed to register asset") } @@ -758,9 +760,9 @@ mod asset { quantity, metadata: Metadata(metadata), } = self; - let mint_asset = MintExpr::new( - quantity.to_value(), - IdBox::AssetId(AssetId::new(asset, account)), + let mint_asset = iroha_client::data_model::isi::Mint::asset_quantity( + quantity, + AssetId::new(asset, account), ); submit([mint_asset], metadata, context) .wrap_err("Failed to mint asset of type `NumericValue::U32`") @@ -792,9 +794,9 @@ mod asset { quantity, metadata: Metadata(metadata), } = self; - let burn_asset = BurnExpr::new( - quantity.to_value(), - IdBox::AssetId(AssetId::new(asset, account)), + let burn_asset = iroha_client::data_model::isi::Burn::asset_quantity( + quantity, + AssetId::new(asset, account), ); submit([burn_asset], metadata, context) .wrap_err("Failed to burn asset of type `NumericValue::U32`") @@ -830,10 +832,10 @@ mod asset { quantity, metadata: Metadata(metadata), } = self; - let transfer_asset = TransferExpr::new( - IdBox::AssetId(AssetId::new(asset_id, from)), - quantity.to_value(), - IdBox::AccountId(to), + let transfer_asset = iroha_client::data_model::isi::Transfer::asset_quantity( + AssetId::new(asset_id, from), + quantity, + to, ); submit([transfer_asset], metadata, context).wrap_err("Failed to transfer asset") } @@ -934,7 +936,9 @@ mod peer { key, metadata: Metadata(metadata), } = self; - let register_peer = RegisterExpr::new(Peer::new(PeerId::new(&address, &key))); + let register_peer = iroha_client::data_model::isi::Register::peer(Peer::new( + PeerId::new(&address, &key), + )); submit([register_peer], metadata, context).wrap_err("Failed to register peer") } } @@ -960,7 +964,8 @@ mod peer { key, metadata: Metadata(metadata), } = self; - let unregister_peer = UnregisterExpr::new(IdBox::PeerId(PeerId::new(&address, &key))); + let unregister_peer = + iroha_client::data_model::isi::Unregister::peer(PeerId::new(&address, &key)); submit([unregister_peer], metadata, context).wrap_err("Failed to unregister peer") } } @@ -1017,7 +1022,7 @@ mod json { reader.read_to_end(&mut raw_content)?; let string_content = String::from_utf8(raw_content)?; - let instructions: Vec = json5::from_str(&string_content)?; + let instructions: Vec = json5::from_str(&string_content)?; submit(instructions, UnlimitedMetadata::new(), context) .wrap_err("Failed to submit parsed instructions") } diff --git a/configs/peer/executor.wasm b/configs/peer/executor.wasm index 2ef54f969b642947ac965f14c877ddb8a7b17bb0..48c0f25b41cc2e2c2b2ef6a26a9d75b19fd5ad4d 100644 GIT binary patch literal 389245 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

wma#Zkdo&zd)&zZ0a-aA&o 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

4J9bVM8&+eTF#)55 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%0AaU{5IG7$c&2EFpe7TARgz5v8;s}l6Dp5knLtTE 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%0Dmo&h)O1CxgExU60R+*eS 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 Px`AOjvVk0dGe!Rg!(0e9 diff --git a/configs/peer/genesis.json b/configs/peer/genesis.json index 2ca5d0365ed..a915d22f4f8 100644 --- a/configs/peer/genesis.json +++ b/configs/peer/genesis.json @@ -3,7 +3,7 @@ [ { "Register": { - "NewDomain": { + "Domain": { "id": "wonderland", "logo": null, "metadata": { @@ -16,7 +16,7 @@ }, { "Register": { - "NewAccount": { + "Account": { "id": "alice@wonderland", "signatories": [ "ed01207233BFC89DCBD68C19FDE6CE6158225298EC1131B6A130D1AEB454C1AB5183C0" @@ -31,7 +31,7 @@ }, { "Register": { - "NewAccount": { + "Account": { "id": "bob@wonderland", "signatories": [ "ed01207233BFC89DCBD68C19FDE6CE6158225298EC1131B6A130D1AEB454C1AB5183C0" @@ -46,7 +46,7 @@ }, { "Register": { - "NewAssetDefinition": { + "AssetDefinition": { "id": "rose#wonderland", "value_type": "Quantity", "mintable": "Infinitely", @@ -57,7 +57,7 @@ }, { "Register": { - "NewDomain": { + "Domain": { "id": "garden_of_live_flowers", "logo": null, "metadata": {} @@ -66,7 +66,7 @@ }, { "Register": { - "NewAccount": { + "Account": { "id": "carpenter@garden_of_live_flowers", "signatories": [ "ed01207233BFC89DCBD68C19FDE6CE6158225298EC1131B6A130D1AEB454C1AB5183C0" @@ -77,7 +77,7 @@ }, { "Register": { - "NewAssetDefinition": { + "AssetDefinition": { "id": "cabbage#garden_of_live_flowers", "value_type": "Quantity", "mintable": "Infinitely", @@ -88,95 +88,71 @@ }, { "Mint": { - "object": "13_u32", - "destination_id": { - "AssetId": "rose##alice@wonderland" + "Asset": { + "Quantity": { + "object": 13, + "destination_id": "rose##alice@wonderland" + } } } }, { "Mint": { - "object": "44_u32", - "destination_id": { - "AssetId": "cabbage#garden_of_live_flowers#alice@wonderland" + "Asset": { + "Quantity": { + "object": 44, + "destination_id": "cabbage#garden_of_live_flowers#alice@wonderland" + } } } }, { "Grant": { - "object": { - "PermissionToken": { + "PermissionToken": { + "object": { "definition_id": "CanSetParameters", "payload": null - } - }, - "destination_id": { - "AccountId": "alice@wonderland" + }, + "destination_id": "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" - } - } - ] + "NewParameter": "?MaxTransactionsInBlock=512" + }, + { + "NewParameter": "?BlockTime=2000" + }, + { + "NewParameter": "?CommitTimeLimit=4000" + }, + { + "NewParameter": "?TransactionLimits=4096,4194304_TL" + }, + { + "NewParameter": "?WSVAssetMetadataLimits=1048576,4096_ML" + }, + { + "NewParameter": "?WSVAssetDefinitionMetadataLimits=1048576,4096_ML" + }, + { + "NewParameter": "?WSVAccountMetadataLimits=1048576,4096_ML" + }, + { + "NewParameter": "?WSVDomainMetadataLimits=1048576,4096_ML" + }, + { + "NewParameter": "?WSVIdentLengthLimits=1,128_LL" + }, + { + "NewParameter": "?WASMFuelLimit=23000000" + }, + { + "NewParameter": "?WASMMaxMemory=524288000" }, { "Register": { - "NewRole": { + "Role": { "id": "ALICE_METADATA_ACCESS", "permissions": [ { diff --git a/core/benches/blocks/common.rs b/core/benches/blocks/common.rs index f4f412eb633..7aef12edd2d 100644 --- a/core/benches/blocks/common.rs +++ b/core/benches/blocks/common.rs @@ -12,7 +12,7 @@ use iroha_data_model::{ account::Account, asset::{AssetDefinition, AssetDefinitionId}, domain::Domain, - isi::InstructionExpr, + isi::InstructionBox, prelude::*, transaction::TransactionLimits, }; @@ -22,7 +22,7 @@ use serde_json::json; /// Create block pub fn create_block( wsv: &mut WorldStateView, - instructions: Vec, + instructions: Vec, account_id: AccountId, key_pair: KeyPair, ) -> CommittedBlock { @@ -57,13 +57,13 @@ pub fn populate_wsv( accounts_per_domain: usize, assets_per_domain: usize, owner_id: &AccountId, -) -> Vec { - let mut instructions: Vec = Vec::new(); +) -> Vec { + let mut instructions: Vec = Vec::new(); for i in 0..domains { let domain_id = construct_domain_id(i); let domain = Domain::new(domain_id.clone()); - instructions.push(RegisterExpr::new(domain).into()); - let can_unregister_domain = GrantExpr::new( + instructions.push(Register::domain(domain).into()); + let can_unregister_domain = Grant::permission_token( PermissionToken::new( "CanUnregisterDomain".parse().unwrap(), &json!({ "domain_id": domain_id.clone() }), @@ -74,8 +74,8 @@ pub fn populate_wsv( for j in 0..accounts_per_domain { let account_id = construct_account_id(j, domain_id.clone()); let account = Account::new(account_id.clone(), []); - instructions.push(RegisterExpr::new(account).into()); - let can_unregister_account = GrantExpr::new( + instructions.push(Register::account(account).into()); + let can_unregister_account = Grant::permission_token( PermissionToken::new( "CanUnregisterAccount".parse().unwrap(), &json!({ "account_id": account_id.clone() }), @@ -90,8 +90,8 @@ pub fn populate_wsv( asset_definition_id.clone(), iroha_data_model::asset::AssetValueType::Quantity, ); - instructions.push(RegisterExpr::new(asset_definition).into()); - let can_unregister_asset_definition = GrantExpr::new( + instructions.push(Register::asset_definition(asset_definition).into()); + let can_unregister_asset_definition = Grant::permission_token( PermissionToken::new( "CanUnregisterAssetDefinition".parse().unwrap(), &json!({ "asset_definition_id": asset_definition_id }), @@ -109,23 +109,23 @@ pub fn delete_every_nth( accounts_per_domain: usize, assets_per_domain: usize, nth: usize, -) -> Vec { - let mut instructions: Vec = Vec::new(); +) -> Vec { + let mut instructions: Vec = Vec::new(); for i in 0..domains { let domain_id = construct_domain_id(i); if i % nth == 0 { - instructions.push(UnregisterExpr::new(domain_id.clone()).into()); + instructions.push(Unregister::domain(domain_id.clone()).into()); } else { for j in 0..accounts_per_domain { if j % nth == 0 { let account_id = construct_account_id(j, domain_id.clone()); - instructions.push(UnregisterExpr::new(account_id.clone()).into()); + instructions.push(Unregister::account(account_id.clone()).into()); } } for k in 0..assets_per_domain { if k % nth == 0 { let asset_definition_id = construct_asset_definition_id(k, domain_id.clone()); - instructions.push(UnregisterExpr::new(asset_definition_id).into()); + instructions.push(Unregister::asset_definition(asset_definition_id).into()); } } } @@ -138,19 +138,19 @@ pub fn restore_every_nth( accounts_per_domain: usize, assets_per_domain: usize, nth: usize, -) -> Vec { - let mut instructions: Vec = Vec::new(); +) -> Vec { + let mut instructions: Vec = Vec::new(); for i in 0..domains { let domain_id = construct_domain_id(i); if i % nth == 0 { let domain = Domain::new(domain_id.clone()); - instructions.push(RegisterExpr::new(domain).into()); + instructions.push(Register::domain(domain).into()); } for j in 0..accounts_per_domain { if j % nth == 0 || i % nth == 0 { let account_id = construct_account_id(j, domain_id.clone()); let account = Account::new(account_id.clone(), []); - instructions.push(RegisterExpr::new(account).into()); + instructions.push(Register::account(account).into()); } } for k in 0..assets_per_domain { @@ -160,7 +160,7 @@ pub fn restore_every_nth( asset_definition_id, iroha_data_model::asset::AssetValueType::Quantity, ); - instructions.push(RegisterExpr::new(asset_definition).into()); + instructions.push(Register::asset_definition(asset_definition).into()); } } } @@ -186,7 +186,7 @@ pub fn build_wsv(account_id: &AccountId, key_pair: &KeyPair) -> WorldStateView { 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)); - UpgradeExpr::new(executor) + Upgrade::new(executor) .execute(account_id, &mut wsv) .expect("Failed to load executor"); } diff --git a/core/benches/blocks/validate_blocks.rs b/core/benches/blocks/validate_blocks.rs index 6a6d0bc585d..f39e7eb288e 100644 --- a/core/benches/blocks/validate_blocks.rs +++ b/core/benches/blocks/validate_blocks.rs @@ -1,6 +1,6 @@ use eyre::Result; use iroha_core::prelude::*; -use iroha_data_model::{isi::InstructionExpr, prelude::*}; +use iroha_data_model::{isi::InstructionBox, prelude::*}; #[path = "./common.rs"] mod common; @@ -10,7 +10,7 @@ use common::*; #[derive(Clone)] pub struct WsvValidateBlocks { wsv: WorldStateView, - instructions: Vec>, + instructions: Vec>, key_pair: KeyPair, account_id: AccountId, } diff --git a/core/benches/kura.rs b/core/benches/kura.rs index 279f8d97528..a47f731e31d 100644 --- a/core/benches/kura.rs +++ b/core/benches/kura.rs @@ -23,11 +23,7 @@ async fn measure_block_size_for_n_executors(n_executors: u32) { let bob_id = AccountId::from_str("bob@test").expect("tested"); let xor_id = AssetDefinitionId::from_str("xor#test").expect("tested"); let alice_xor_id = AssetId::new(xor_id, alice_id); - let transfer = TransferExpr::new( - IdBox::AssetId(alice_xor_id), - 10_u32.to_value(), - IdBox::AccountId(bob_id), - ); + let transfer = Transfer::asset_quantity(alice_xor_id, 10_u32, bob_id); let keypair = KeyPair::generate().expect("Failed to generate KeyPair."); let tx = TransactionBuilder::new(AccountId::from_str("alice@wonderland").expect("checked")) .with_instructions([transfer]) diff --git a/core/benches/validation.rs b/core/benches/validation.rs index 0a474ab3ea0..3a5bcaefe23 100644 --- a/core/benches/validation.rs +++ b/core/benches/validation.rs @@ -12,7 +12,7 @@ use iroha_core::{ tx::TransactionExecutor, wsv::World, }; -use iroha_data_model::{prelude::*, transaction::TransactionLimits}; +use iroha_data_model::{isi::InstructionBox, prelude::*, transaction::TransactionLimits}; use iroha_primitives::unique_vec::UniqueVec; const START_DOMAIN: &str = "start"; @@ -26,23 +26,25 @@ const TRANSACTION_LIMITS: TransactionLimits = TransactionLimits { fn build_test_transaction(keys: KeyPair) -> SignedTransaction { let domain_name = "domain"; let domain_id = DomainId::from_str(domain_name).expect("does not panic"); - let create_domain = RegisterExpr::new(Domain::new(domain_id)); + let create_domain: InstructionBox = Register::domain(Domain::new(domain_id)).into(); let account_name = "account"; let (public_key, _) = KeyPair::generate() .expect("Failed to generate KeyPair.") .into(); - let create_account = RegisterExpr::new(Account::new( + let create_account = Register::account(Account::new( AccountId::new( account_name.parse().expect("Valid"), domain_name.parse().expect("Valid"), ), [public_key], - )); + )) + .into(); let asset_definition_id = AssetDefinitionId::new( "xor".parse().expect("Valid"), domain_name.parse().expect("Valid"), ); - let create_asset = RegisterExpr::new(AssetDefinition::quantity(asset_definition_id)); + let create_asset = + Register::asset_definition(AssetDefinition::quantity(asset_definition_id)).into(); let instructions = [create_domain, create_account, create_asset]; TransactionBuilder::new(AccountId::new( @@ -82,7 +84,7 @@ fn build_test_and_transient_wsv(keys: KeyPair) -> WorldStateView { .unwrap_or_else(|_| panic!("Failed to read file: {}", path_to_executor.display())); let executor = Executor::new(WasmSmartContract::from_compiled(wasm)); let authority = "genesis@genesis".parse().expect("Valid"); - UpgradeExpr::new(executor) + Upgrade::new(executor) .execute(&authority, &mut wsv) .expect("Failed to load executor"); } diff --git a/core/src/block.rs b/core/src/block.rs index 9322d16400d..21a7405ef42 100644 --- a/core/src/block.rs +++ b/core/src/block.rs @@ -735,7 +735,7 @@ mod tests { // Creating an instruction let asset_definition_id = AssetDefinitionId::from_str("xor#wonderland").expect("Valid"); let create_asset_definition = - RegisterExpr::new(AssetDefinition::quantity(asset_definition_id)); + Register::asset_definition(AssetDefinition::quantity(asset_definition_id)); // Making two transactions that have the same instruction let transaction_limits = &wsv.transaction_executor().transaction_limits; @@ -778,7 +778,7 @@ mod tests { // Creating an instruction let asset_definition_id = AssetDefinitionId::from_str("xor#wonderland").expect("Valid"); let create_asset_definition = - RegisterExpr::new(AssetDefinition::quantity(asset_definition_id.clone())); + Register::asset_definition(AssetDefinition::quantity(asset_definition_id.clone())); // Making two transactions that have the same instruction let transaction_limits = &wsv.transaction_executor().transaction_limits; @@ -791,14 +791,14 @@ mod tests { let quantity: u32 = 200; let fail_quantity: u32 = 20; - let fail_mint = MintExpr::new( - fail_quantity.to_value(), - IdBox::AssetId(AssetId::new(asset_definition_id.clone(), alice_id.clone())), + let fail_mint = Mint::asset_quantity( + fail_quantity, + AssetId::new(asset_definition_id.clone(), alice_id.clone()), ); - let succeed_mint = MintExpr::new( - quantity.to_value(), - IdBox::AssetId(AssetId::new(asset_definition_id, alice_id.clone())), + let succeed_mint = Mint::asset_quantity( + quantity, + AssetId::new(asset_definition_id, alice_id.clone()), ); let tx0 = TransactionBuilder::new(alice_id.clone()) @@ -848,14 +848,15 @@ mod tests { let transaction_limits = &wsv.transaction_executor().transaction_limits; let domain_id = DomainId::from_str("domain").expect("Valid"); - let create_domain = RegisterExpr::new(Domain::new(domain_id)); + let create_domain = Register::domain(Domain::new(domain_id)); let asset_definition_id = AssetDefinitionId::from_str("coin#domain").expect("Valid"); - let create_asset = RegisterExpr::new(AssetDefinition::quantity(asset_definition_id)); - let instructions_fail: [InstructionExpr; 2] = [ + let create_asset = + Register::asset_definition(AssetDefinition::quantity(asset_definition_id)); + let instructions_fail: [InstructionBox; 2] = [ create_domain.clone().into(), - Fail::new("Always fail").into(), + Fail::new("Always fail".to_owned()).into(), ]; - let instructions_accept: [InstructionExpr; 2] = [create_domain.into(), create_asset.into()]; + let instructions_accept: [InstructionBox; 2] = [create_domain.into(), create_asset.into()]; let tx_fail = TransactionBuilder::new(alice_id.clone()) .with_instructions(instructions_fail) .sign(alice_keys.clone()) diff --git a/core/src/executor.rs b/core/src/executor.rs index 971f7dd7867..62af571fe49 100644 --- a/core/src/executor.rs +++ b/core/src/executor.rs @@ -4,7 +4,7 @@ use derive_more::DebugCustom; use iroha_data_model::{ account::AccountId, executor as data_model_executor, - isi::InstructionExpr, + isi::InstructionBox, query::QueryBox, transaction::{Executable, SignedTransaction}, ValidationFail, @@ -181,7 +181,7 @@ impl Executor { &self, wsv: &mut WorldStateView, authority: &AccountId, - instruction: InstructionExpr, + instruction: InstructionBox, ) -> Result<(), ValidationFail> { trace!("Running instruction validation"); diff --git a/core/src/queue.rs b/core/src/queue.rs index e19e46f2ba5..2872ebc9365 100644 --- a/core/src/queue.rs +++ b/core/src/queue.rs @@ -512,7 +512,7 @@ mod tests { .build() .expect("Default queue config should always build") }); - let instructions: [InstructionExpr; 0] = []; + let instructions: [InstructionBox; 0] = []; let tx = TransactionBuilder::new("alice@wonderland".parse().expect("Valid")) .with_instructions(instructions); let tx_limits = TransactionLimits { diff --git a/core/src/smartcontracts/isi/account.rs b/core/src/smartcontracts/isi/account.rs index 2c1e67ca25c..eff4dbaebbc 100644 --- a/core/src/smartcontracts/isi/account.rs +++ b/core/src/smartcontracts/isi/account.rs @@ -478,10 +478,9 @@ pub mod isi { /// Account-related [`Query`] instructions. pub mod query { - use eyre::{Result, WrapErr}; + use eyre::Result; use iroha_data_model::{ account::Account, - evaluate::ExpressionEvaluator, permission::PermissionToken, query::{error::QueryExecutionFail as Error, MetadataValue}, }; @@ -494,13 +493,10 @@ pub mod query { &self, wsv: &'wsv WorldStateView, ) -> Result + 'wsv>, Error> { - let account_id = wsv - .evaluate(&self.id) - .wrap_err("Failed to evaluate account id") - .map_err(|e| Error::Evaluate(e.to_string()))?; + let account_id = &self.id; iroha_logger::trace!(%account_id, roles=?wsv.world.roles); - wsv.account(&account_id)?; - Ok(Box::new(wsv.account_roles(&account_id).cloned())) + wsv.account(account_id)?; + Ok(Box::new(wsv.account_roles(account_id).cloned())) } } @@ -510,13 +506,10 @@ pub mod query { &self, wsv: &'wsv WorldStateView, ) -> Result + 'wsv>, Error> { - let account_id = wsv - .evaluate(&self.id) - .wrap_err("Failed to evaluate account id") - .map_err(|e| Error::Evaluate(e.to_string()))?; + let account_id = &self.id; iroha_logger::trace!(%account_id, accounts=?wsv.world.domains); Ok(Box::new( - wsv.account_permission_tokens(&account_id)?.cloned(), + wsv.account_permission_tokens(account_id)?.cloned(), )) } } @@ -539,12 +532,9 @@ pub mod query { impl ValidQuery for FindAccountById { #[metrics(+"find_account_by_id")] fn execute(&self, wsv: &WorldStateView) -> Result { - let id = wsv - .evaluate(&self.id) - .wrap_err("Failed to evaluate id") - .map_err(|e| Error::Evaluate(e.to_string()))?; + let id = &self.id; iroha_logger::trace!(%id); - wsv.map_account(&id, Clone::clone).map_err(Into::into) + wsv.map_account(id, Clone::clone).map_err(Into::into) } } @@ -554,10 +544,7 @@ pub mod query { &self, wsv: &'wsv WorldStateView, ) -> Result + 'wsv>, Error> { - let name = wsv - .evaluate(&self.name) - .wrap_err("Failed to evaluate account name") - .map_err(|e| Error::Evaluate(e.to_string()))?; + let name = self.name.clone(); iroha_logger::trace!(%name); Ok(Box::new( wsv.domains() @@ -581,30 +568,21 @@ pub mod query { &self, wsv: &'wsv WorldStateView, ) -> Result + 'wsv>, Error> { - let id = wsv - .evaluate(&self.domain_id) - .wrap_err("Failed to evaluate domain id") - .map_err(|e| Error::Evaluate(e.to_string()))?; + let id = &self.domain_id; iroha_logger::trace!(%id); - Ok(Box::new(wsv.domain(&id)?.accounts.values().cloned())) + Ok(Box::new(wsv.domain(id)?.accounts.values().cloned())) } } impl ValidQuery for FindAccountKeyValueByIdAndKey { #[metrics(+"find_account_key_value_by_id_and_key")] fn execute(&self, wsv: &WorldStateView) -> Result { - let id = wsv - .evaluate(&self.id) - .wrap_err("Failed to evaluate account id") - .map_err(|e| Error::Evaluate(e.to_string()))?; - let key = wsv - .evaluate(&self.key) - .wrap_err("Failed to evaluate key") - .map_err(|e| Error::Evaluate(e.to_string()))?; + let id = &self.id; + let key = &self.key; iroha_logger::trace!(%id, %key); - wsv.map_account(&id, |account| account.metadata.get(&key).map(Clone::clone))? - .ok_or_else(|| FindError::MetadataKey(key).into()) + wsv.map_account(id, |account| account.metadata.get(key).map(Clone::clone))? + .ok_or_else(|| FindError::MetadataKey(key.clone()).into()) .map(Into::into) } } @@ -615,10 +593,7 @@ pub mod query { &self, wsv: &'wsv WorldStateView, ) -> Result + 'wsv>, Error> { - let asset_definition_id = wsv - .evaluate(&self.asset_definition_id) - .wrap_err("Failed to evaluate asset id") - .map_err(|e| Error::Evaluate(e.to_string()))?; + let asset_definition_id = self.asset_definition_id.clone(); iroha_logger::trace!(%asset_definition_id); Ok(Box::new( diff --git a/core/src/smartcontracts/isi/asset.rs b/core/src/smartcontracts/isi/asset.rs index 6d5fd0ccda3..4aaf3e19168 100644 --- a/core/src/smartcontracts/isi/asset.rs +++ b/core/src/smartcontracts/isi/asset.rs @@ -416,7 +416,7 @@ pub mod isi { /// Asset-related query implementations. pub mod query { - use eyre::{Result, WrapErr as _}; + use eyre::Result; use iroha_data_model::{ asset::{Asset, AssetDefinition}, query::{ @@ -464,12 +464,9 @@ pub mod query { impl ValidQuery for FindAssetById { #[metrics(+"find_asset_by_id")] fn execute(&self, wsv: &WorldStateView) -> Result { - let id = wsv - .evaluate(&self.id) - .wrap_err("Failed to get asset id") - .map_err(|e| Error::Evaluate(e.to_string()))?; + let id = &self.id; iroha_logger::trace!(%id); - wsv.asset(&id).map_err(|asset_err| { + wsv.asset(id).map_err(|asset_err| { if let Err(definition_err) = wsv.asset_definition(&id.definition_id) { definition_err.into() } else { @@ -482,12 +479,9 @@ pub mod query { impl ValidQuery for FindAssetDefinitionById { #[metrics(+"find_asset_defintion_by_id")] fn execute(&self, wsv: &WorldStateView) -> Result { - let id = wsv - .evaluate(&self.id) - .wrap_err("Failed to get asset definition id") - .map_err(|e| Error::Evaluate(e.to_string()))?; + let id = &self.id; - let entry = wsv.asset_definition(&id).map_err(Error::from)?; + let entry = wsv.asset_definition(id).map_err(Error::from)?; Ok(entry) } @@ -499,10 +493,7 @@ pub mod query { &self, wsv: &'wsv WorldStateView, ) -> Result + 'wsv>, Error> { - let name = wsv - .evaluate(&self.name) - .wrap_err("Failed to get asset name") - .map_err(|e| Error::Evaluate(e.to_string()))?; + let name = self.name.clone(); iroha_logger::trace!(%name); Ok(Box::new( wsv.domains() @@ -530,12 +521,9 @@ pub mod query { &self, wsv: &'wsv WorldStateView, ) -> Result + 'wsv>, Error> { - let id = wsv - .evaluate(&self.account_id) - .wrap_err("Failed to get account id") - .map_err(|e| Error::Evaluate(e.to_string()))?; + let id = &self.account_id; iroha_logger::trace!(%id); - Ok(Box::new(wsv.account_assets(&id)?.cloned())) + Ok(Box::new(wsv.account_assets(id)?.cloned())) } } @@ -545,10 +533,7 @@ pub mod query { &self, wsv: &'wsv WorldStateView, ) -> Result + 'wsv>, Error> { - let id = wsv - .evaluate(&self.asset_definition_id) - .wrap_err("Failed to get asset definition id") - .map_err(|e| Error::Evaluate(e.to_string()))?; + let id = self.asset_definition_id.clone(); iroha_logger::trace!(%id); Ok(Box::new( wsv.domains() @@ -576,13 +561,10 @@ pub mod query { &self, wsv: &'wsv WorldStateView, ) -> Result + 'wsv>, Error> { - let id = wsv - .evaluate(&self.domain_id) - .wrap_err("Failed to get domain id") - .map_err(|e| Error::Evaluate(e.to_string()))?; + let id = &self.domain_id; iroha_logger::trace!(%id); Ok(Box::new( - wsv.domain(&id)? + wsv.domain(id)? .accounts .values() .flat_map(|account| account.assets.values()) @@ -597,14 +579,8 @@ pub mod query { &self, wsv: &'wsv WorldStateView, ) -> Result + 'wsv>, Error> { - let domain_id = wsv - .evaluate(&self.domain_id) - .wrap_err("Failed to get domain id") - .map_err(|e| Error::Evaluate(e.to_string()))?; - let asset_definition_id = wsv - .evaluate(&self.asset_definition_id) - .wrap_err("Failed to get asset definition id") - .map_err(|e| Error::Evaluate(e.to_string()))?; + let domain_id = self.domain_id.clone(); + let asset_definition_id = self.asset_definition_id.clone(); let domain = wsv.domain(&domain_id)?; let _definition = domain .asset_definitions @@ -632,13 +608,10 @@ pub mod query { impl ValidQuery for FindAssetQuantityById { #[metrics(+"find_asset_quantity_by_id")] fn execute(&self, wsv: &WorldStateView) -> Result { - let id = wsv - .evaluate(&self.id) - .wrap_err("Failed to get asset id") - .map_err(|e| Error::Evaluate(e.to_string()))?; + let id = &self.id; iroha_logger::trace!(%id); let value = wsv - .asset(&id) + .asset(id) .map_err(|asset_err| { if let Err(definition_err) = wsv.asset_definition(&id.definition_id) { Error::Find(definition_err) @@ -656,12 +629,9 @@ pub mod query { impl ValidQuery for FindTotalAssetQuantityByAssetDefinitionId { #[metrics(+"find_total_asset_quantity_by_asset_definition_id")] fn execute(&self, wsv: &WorldStateView) -> Result { - let id = wsv - .evaluate(&self.id) - .wrap_err("Failed to get asset definition id") - .map_err(|e| Error::Evaluate(e.to_string()))?; + let id = &self.id; iroha_logger::trace!(%id); - let asset_value = wsv.asset_total_amount(&id)?; + let asset_value = wsv.asset_total_amount(id)?; Ok(asset_value) } } @@ -669,15 +639,9 @@ pub mod query { impl ValidQuery for FindAssetKeyValueByIdAndKey { #[metrics(+"find_asset_key_value_by_id_and_key")] fn execute(&self, wsv: &WorldStateView) -> Result { - let id = wsv - .evaluate(&self.id) - .wrap_err("Failed to get asset id") - .map_err(|e| Error::Evaluate(e.to_string()))?; - let key = wsv - .evaluate(&self.key) - .wrap_err("Failed to get key") - .map_err(|e| Error::Evaluate(e.to_string()))?; - let asset = wsv.asset(&id).map_err(|asset_err| { + let id = &self.id; + let key = &self.key; + let asset = wsv.asset(id).map_err(|asset_err| { if let Err(definition_err) = wsv.asset_definition(&id.definition_id) { Error::Find(definition_err) } else { @@ -691,8 +655,8 @@ pub mod query { .map_err(eyre::Error::from) .map_err(|e| Error::Conversion(e.to_string()))?; store - .get(&key) - .ok_or_else(|| Error::Find(FindError::MetadataKey(key))) + .get(key) + .ok_or_else(|| Error::Find(FindError::MetadataKey(key.clone()))) .cloned() .map(Into::into) } diff --git a/core/src/smartcontracts/isi/block.rs b/core/src/smartcontracts/isi/block.rs index 4f241372ef1..08f4af0fb6e 100644 --- a/core/src/smartcontracts/isi/block.rs +++ b/core/src/smartcontracts/isi/block.rs @@ -1,8 +1,7 @@ //! This module contains trait implementations related to block queries -use eyre::{Result, WrapErr}; +use eyre::Result; use iroha_data_model::{ block::{BlockHeader, SignedBlock}, - evaluate::ExpressionEvaluator, query::{ block::FindBlockHeaderByHash, error::{FindError, QueryExecutionFail}, @@ -43,10 +42,7 @@ impl ValidQuery for FindAllBlockHeaders { impl ValidQuery for FindBlockHeaderByHash { #[metrics(+"find_block_header")] fn execute(&self, wsv: &WorldStateView) -> Result { - let hash = wsv - .evaluate(&self.hash) - .wrap_err("Failed to evaluate hash") - .map_err(|e| QueryExecutionFail::Evaluate(e.to_string()))?; + let hash = self.hash; let block = wsv .all_blocks() diff --git a/core/src/smartcontracts/isi/domain.rs b/core/src/smartcontracts/isi/domain.rs index b7930106a04..81855321e7c 100644 --- a/core/src/smartcontracts/isi/domain.rs +++ b/core/src/smartcontracts/isi/domain.rs @@ -301,7 +301,7 @@ pub mod isi { /// Query module provides [`Query`] Domain related implementations. pub mod query { - use eyre::{Result, WrapErr}; + use eyre::Result; use iroha_data_model::{ domain::Domain, query::{error::QueryExecutionFail as Error, MetadataValue}, @@ -322,29 +322,20 @@ pub mod query { impl ValidQuery for FindDomainById { #[metrics(+"find_domain_by_id")] fn execute(&self, wsv: &WorldStateView) -> Result { - let id = wsv - .evaluate(&self.id) - .wrap_err("Failed to get domain id") - .map_err(|e| Error::Evaluate(e.to_string()))?; + let id = &self.id; iroha_logger::trace!(%id); - Ok(wsv.domain(&id)?.clone()) + Ok(wsv.domain(id)?.clone()) } } impl ValidQuery for FindDomainKeyValueByIdAndKey { #[metrics(+"find_domain_key_value_by_id_and_key")] fn execute(&self, wsv: &WorldStateView) -> Result { - let id = wsv - .evaluate(&self.id) - .wrap_err("Failed to get domain id") - .map_err(|e| Error::Evaluate(e.to_string()))?; - let key = wsv - .evaluate(&self.key) - .wrap_err("Failed to get key") - .map_err(|e| Error::Evaluate(e.to_string()))?; + let id = &self.id; + let key = &self.key; iroha_logger::trace!(%id, %key); - wsv.map_domain(&id, |domain| domain.metadata.get(&key).map(Clone::clone))? - .ok_or_else(|| FindError::MetadataKey(key).into()) + wsv.map_domain(id, |domain| domain.metadata.get(key).map(Clone::clone))? + .ok_or_else(|| FindError::MetadataKey(key.clone()).into()) .map(Into::into) } } @@ -352,20 +343,14 @@ pub mod query { impl ValidQuery for FindAssetDefinitionKeyValueByIdAndKey { #[metrics(+"find_asset_definition_key_value_by_id_and_key")] fn execute(&self, wsv: &WorldStateView) -> Result { - let id = wsv - .evaluate(&self.id) - .wrap_err("Failed to get asset definition id") - .map_err(|e| Error::Evaluate(e.to_string()))?; - let key = wsv - .evaluate(&self.key) - .wrap_err("Failed to get key") - .map_err(|e| Error::Evaluate(e.to_string()))?; + let id = &self.id; + let key = &self.key; iroha_logger::trace!(%id, %key); Ok(wsv - .asset_definition(&id)? + .asset_definition(id)? .metadata - .get(&key) - .ok_or(FindError::MetadataKey(key)) + .get(key) + .ok_or(FindError::MetadataKey(key.clone())) .cloned() .map(Into::into)?) } diff --git a/core/src/smartcontracts/isi/mod.rs b/core/src/smartcontracts/isi/mod.rs index 7f80bbfade1..a0bf424f1ef 100644 --- a/core/src/smartcontracts/isi/mod.rs +++ b/core/src/smartcontracts/isi/mod.rs @@ -12,12 +12,10 @@ pub mod world; use eyre::Result; use iroha_data_model::{ - evaluate::ExpressionEvaluator, isi::{error::InstructionExecutionError as Error, *}, prelude::*, }; -use iroha_logger::prelude::{Span, *}; -use iroha_primitives::fixed::Fixed; +use iroha_logger::prelude::*; use super::Execute; use crate::{prelude::*, wsv::WorldStateView}; @@ -31,343 +29,167 @@ pub trait Registrable { fn build(self, authority: &AccountId) -> Self::Target; } -impl Execute for InstructionExpr { +impl Execute for InstructionBox { fn execute(self, authority: &AccountId, wsv: &mut WorldStateView) -> Result<(), Error> { iroha_logger::debug!(isi=%self, "Executing"); - macro_rules! match_all { - ($($isi:ident),+ $(,)?) => { - - match self { $( - InstructionExpr::$isi(isi) => isi.execute(authority, wsv), )+ - } - }; - } - - match_all! { - Register, - Unregister, - Mint, - Burn, - Transfer, - If, - Pair, - Sequence, - Fail, - SetKeyValue, - RemoveKeyValue, - Grant, - Revoke, - ExecuteTrigger, - SetParameter, - NewParameter, - Upgrade, - Log, + match self { + Self::Register(isi) => isi.execute(authority, wsv), + Self::Unregister(isi) => isi.execute(authority, wsv), + Self::Mint(isi) => isi.execute(authority, wsv), + Self::Burn(isi) => isi.execute(authority, wsv), + Self::Transfer(isi) => isi.execute(authority, wsv), + Self::Fail(isi) => isi.execute(authority, wsv), + Self::SetKeyValue(isi) => isi.execute(authority, wsv), + Self::RemoveKeyValue(isi) => isi.execute(authority, wsv), + Self::Grant(isi) => isi.execute(authority, wsv), + Self::Revoke(isi) => isi.execute(authority, wsv), + Self::ExecuteTrigger(isi) => isi.execute(authority, wsv), + Self::SetParameter(isi) => isi.execute(authority, wsv), + Self::NewParameter(isi) => isi.execute(authority, wsv), + Self::Upgrade(isi) => isi.execute(authority, wsv), + Self::Log(isi) => isi.execute(authority, wsv), } } } -impl Execute for RegisterExpr { +impl Execute for RegisterBox { #[iroha_logger::log(name = "register", skip_all, fields(id))] fn execute(self, authority: &AccountId, wsv: &mut WorldStateView) -> Result<(), Error> { - let object_id = wsv.evaluate(&self.object)?; - Span::current().record("id", &object_id.to_string()); - match object_id { - RegistrableBox::Peer(object) => Register:: { object }.execute(authority, wsv), - RegistrableBox::Domain(object) => Register:: { object }.execute(authority, wsv), - RegistrableBox::Account(object) => { - Register:: { object }.execute(authority, wsv) - } - RegistrableBox::AssetDefinition(object) => { - Register:: { object }.execute(authority, wsv) - } - RegistrableBox::Asset(object) => Register:: { object }.execute(authority, wsv), - RegistrableBox::Trigger(object) => { - Register::> { object }.execute(authority, wsv) - } - RegistrableBox::Role(object) => Register:: { object }.execute(authority, wsv), + match self { + Self::Peer(isi) => isi.execute(authority, wsv), + Self::Domain(isi) => isi.execute(authority, wsv), + Self::Account(isi) => isi.execute(authority, wsv), + Self::AssetDefinition(isi) => isi.execute(authority, wsv), + Self::Asset(isi) => isi.execute(authority, wsv), + Self::Role(isi) => isi.execute(authority, wsv), + Self::Trigger(isi) => isi.execute(authority, wsv), } } } -impl Execute for UnregisterExpr { +impl Execute for UnregisterBox { #[iroha_logger::log(name = "unregister", skip_all, fields(id))] fn execute(self, authority: &AccountId, wsv: &mut WorldStateView) -> Result<(), Error> { - let object_id = wsv.evaluate(&self.object_id)?; - Span::current().record("id", &object_id.to_string()); - match object_id { - IdBox::AccountId(object_id) => { - Unregister:: { object_id }.execute(authority, wsv) - } - IdBox::AssetId(object_id) => Unregister:: { object_id }.execute(authority, wsv), - IdBox::AssetDefinitionId(object_id) => { - Unregister:: { object_id }.execute(authority, wsv) - } - IdBox::DomainId(object_id) => { - Unregister:: { object_id }.execute(authority, wsv) - } - IdBox::PeerId(object_id) => Unregister:: { object_id }.execute(authority, wsv), - IdBox::RoleId(object_id) => Unregister:: { object_id }.execute(authority, wsv), - IdBox::TriggerId(object_id) => { - Unregister::> { object_id }.execute(authority, wsv) - } - IdBox::PermissionTokenId(_) | IdBox::ParameterId(_) => { - Err(Error::Evaluate(InstructionType::Unregister.into())) - } + match self { + Self::Peer(isi) => isi.execute(authority, wsv), + Self::Domain(isi) => isi.execute(authority, wsv), + Self::Account(isi) => isi.execute(authority, wsv), + Self::AssetDefinition(isi) => isi.execute(authority, wsv), + Self::Asset(isi) => isi.execute(authority, wsv), + Self::Role(isi) => isi.execute(authority, wsv), + Self::Trigger(isi) => isi.execute(authority, wsv), } } } -impl Execute for MintExpr { +impl Execute for MintBox { #[iroha_logger::log(name = "Mint", skip_all, fields(destination))] fn execute(self, authority: &AccountId, wsv: &mut WorldStateView) -> Result<(), Error> { - let destination_id = wsv.evaluate(&self.destination_id)?; - let object = wsv.evaluate(&self.object)?; - Span::current().record("destination", &destination_id.to_string()); - iroha_logger::trace!(?object, %authority); - match (destination_id, object) { - (IdBox::AssetId(destination_id), Value::Numeric(NumericValue::U32(object))) => { - Mint:: { - object, - destination_id, - } - .execute(authority, wsv) - } - (IdBox::AssetId(destination_id), Value::Numeric(NumericValue::U128(object))) => { - Mint:: { - object, - destination_id, - } - .execute(authority, wsv) - } - (IdBox::AssetId(destination_id), Value::Numeric(NumericValue::Fixed(object))) => { - Mint:: { - object, - destination_id, - } - .execute(authority, wsv) - } - (IdBox::AccountId(destination_id), Value::PublicKey(object)) => { - Mint:: { - object, - destination_id, - } - .execute(authority, wsv) - } - (IdBox::AccountId(destination_id), Value::SignatureCheckCondition(object)) => { - Mint:: { - object, - destination_id, - } - .execute(authority, wsv) - } - (IdBox::TriggerId(destination_id), Value::Numeric(NumericValue::U32(object))) => { - Mint::> { - object, - destination_id, - } - .execute(authority, wsv) - } - _ => Err(Error::Evaluate(InstructionType::Mint.into())), + match self { + Self::Account(isi) => isi.execute(authority, wsv), + Self::Asset(isi) => isi.execute(authority, wsv), + Self::TriggerRepetitions(isi) => isi.execute(authority, wsv), } } } -impl Execute for BurnExpr { - #[iroha_logger::log(name = "burn", skip_all, fields(destination))] - fn execute(self, authority: &AccountId, wsv: &mut WorldStateView) -> Result<(), Error> { - let destination_id = wsv.evaluate(&self.destination_id)?; - let object = wsv.evaluate(&self.object)?; - Span::current().record("destination", &destination_id.to_string()); - iroha_logger::trace!(?object, %authority); - match (destination_id, object) { - (IdBox::AssetId(destination_id), Value::Numeric(NumericValue::U32(object))) => { - Burn:: { - object, - destination_id, - } - .execute(authority, wsv) - } - (IdBox::AssetId(destination_id), Value::Numeric(NumericValue::U128(object))) => Burn { - object, - destination_id, - } - .execute(authority, wsv), - (IdBox::AssetId(destination_id), Value::Numeric(NumericValue::Fixed(object))) => Burn { - object, - destination_id, - } - .execute(authority, wsv), - (IdBox::AccountId(destination_id), Value::PublicKey(object)) => Burn { - object, - destination_id, - } - .execute(authority, wsv), - (IdBox::TriggerId(destination_id), Value::Numeric(NumericValue::U32(object))) => { - Burn::> { - object, - destination_id, - } - .execute(authority, wsv) - } - // TODO: Not implemented yet. - // (IdBox::AccountId(account_id), Value::SignatureCheckCondition(condition)) => { - // Burn::{condition, account_id}.execute(authority, wsv) - // } - _ => Err(Error::Evaluate(InstructionType::Burn.into())), +impl Execute for AccountMintBox { + fn execute( + self, + authority: &AccountId, + wsv: &mut WorldStateView, + ) -> std::prelude::v1::Result<(), Error> { + match self { + Self::PublicKey(isi) => isi.execute(authority, wsv), + Self::SignatureCheckCondition(isi) => isi.execute(authority, wsv), } } } -impl Execute for TransferExpr { - #[iroha_logger::log(name = "transfer", skip_all, fields(from, to))] - fn execute(self, authority: &AccountId, wsv: &mut WorldStateView) -> Result<(), Error> { - let source_id = wsv.evaluate(&self.source_id)?; - let destination_id = wsv.evaluate(&self.destination_id)?; - let object = wsv.evaluate(&self.object)?; - iroha_logger::trace!(%object, %authority); - Span::current().record("from", source_id.to_string()); - Span::current().record("to", destination_id.to_string()); - - match (source_id, object, destination_id) { - ( - IdBox::AssetId(source_id), - Value::Numeric(value), - IdBox::AccountId(destination_id), - ) => match value { - NumericValue::U32(object) => Transfer { - source_id, - object, - destination_id, - } - .execute(authority, wsv), - NumericValue::U128(object) => Transfer { - source_id, - object, - destination_id, - } - .execute(authority, wsv), - NumericValue::Fixed(object) => Transfer { - source_id, - object, - destination_id, - } - .execute(authority, wsv), - _ => Err(Error::Evaluate(InstructionType::Transfer.into())), - }, - ( - IdBox::AccountId(source_id), - Value::Id(IdBox::AssetDefinitionId(object)), - IdBox::AccountId(destination_id), - ) => Transfer { - source_id, - object, - destination_id, - } - .execute(authority, wsv), - ( - IdBox::AccountId(source_id), - Value::Id(IdBox::DomainId(object)), - IdBox::AccountId(destination_id), - ) => Transfer { - source_id, - object, - destination_id, - } - .execute(authority, wsv), - _ => Err(Error::Evaluate(InstructionType::Transfer.into())), +impl Execute for AssetMintBox { + fn execute( + self, + authority: &AccountId, + wsv: &mut WorldStateView, + ) -> std::prelude::v1::Result<(), Error> { + match self { + Self::Quantity(isi) => isi.execute(authority, wsv), + Self::BigQuantity(isi) => isi.execute(authority, wsv), + Self::Fixed(isi) => isi.execute(authority, wsv), } } } -impl Execute for SetKeyValueExpr { +impl Execute for BurnBox { + #[iroha_logger::log(name = "burn", skip_all, fields(destination))] fn execute(self, authority: &AccountId, wsv: &mut WorldStateView) -> Result<(), Error> { - let key = wsv.evaluate(&self.key)?; - let value = wsv.evaluate(&self.value)?; - iroha_logger::trace!(?key, ?value, %authority); - match wsv.evaluate(&self.object_id)? { - IdBox::AssetId(object_id) => SetKeyValue:: { - object_id, - key, - value, - } - .execute(authority, wsv), - IdBox::AssetDefinitionId(object_id) => SetKeyValue:: { - object_id, - key, - value, - } - .execute(authority, wsv), - IdBox::AccountId(object_id) => SetKeyValue:: { - object_id, - key, - value, - } - .execute(authority, wsv), - IdBox::DomainId(object_id) => SetKeyValue:: { - object_id, - key, - value, - } - .execute(authority, wsv), - _ => Err(Error::Evaluate(InstructionType::SetKeyValue.into())), + match self { + Self::AccountPublicKey(isi) => isi.execute(authority, wsv), + Self::Asset(isi) => isi.execute(authority, wsv), + Self::TriggerRepetitions(isi) => isi.execute(authority, wsv), } } } -impl Execute for RemoveKeyValueExpr { - fn execute(self, authority: &AccountId, wsv: &mut WorldStateView) -> Result<(), Error> { - let key = wsv.evaluate(&self.key)?; - iroha_logger::trace!(?key, %authority); - match wsv.evaluate(&self.object_id)? { - IdBox::AssetId(object_id) => { - RemoveKeyValue:: { object_id, key }.execute(authority, wsv) - } - IdBox::AssetDefinitionId(object_id) => { - RemoveKeyValue:: { object_id, key }.execute(authority, wsv) - } - IdBox::AccountId(object_id) => { - RemoveKeyValue:: { object_id, key }.execute(authority, wsv) - } - IdBox::DomainId(object_id) => { - RemoveKeyValue:: { object_id, key }.execute(authority, wsv) - } - _ => Err(Error::Evaluate(InstructionType::RemoveKeyValue.into())), +impl Execute for AssetBurnBox { + fn execute( + self, + authority: &AccountId, + wsv: &mut WorldStateView, + ) -> std::prelude::v1::Result<(), Error> { + match self { + Self::Quantity(isi) => isi.execute(authority, wsv), + Self::BigQuantity(isi) => isi.execute(authority, wsv), + Self::Fixed(isi) => isi.execute(authority, wsv), } } } -impl Execute for ConditionalExpr { +impl Execute for TransferBox { + #[iroha_logger::log(name = "transfer", skip_all, fields(from, to))] fn execute(self, authority: &AccountId, wsv: &mut WorldStateView) -> Result<(), Error> { - iroha_logger::trace!(?self); - if wsv.evaluate(&self.condition)? { - self.then.execute(authority, wsv)?; - } else if let Some(otherwise) = self.otherwise { - otherwise.execute(authority, wsv)?; + match self { + Self::Domain(isi) => isi.execute(authority, wsv), + Self::AssetDefinition(isi) => isi.execute(authority, wsv), + Self::Asset(isi) => isi.execute(authority, wsv), } - Ok(()) } } -impl Execute for PairExpr { - fn execute(self, authority: &AccountId, wsv: &mut WorldStateView) -> Result<(), Error> { - iroha_logger::trace!(?self); +impl Execute for AssetTransferBox { + fn execute( + self, + authority: &AccountId, + wsv: &mut WorldStateView, + ) -> std::prelude::v1::Result<(), Error> { + match self { + Self::Quantity(isi) => isi.execute(authority, wsv), + Self::BigQuantity(isi) => isi.execute(authority, wsv), + Self::Fixed(isi) => isi.execute(authority, wsv), + } + } +} - self.left_instruction.execute(authority, wsv)?; - self.right_instruction.execute(authority, wsv)?; - Ok(()) +impl Execute for SetKeyValueBox { + fn execute(self, authority: &AccountId, wsv: &mut WorldStateView) -> Result<(), Error> { + match self { + Self::Domain(isi) => isi.execute(authority, wsv), + Self::Account(isi) => isi.execute(authority, wsv), + Self::AssetDefinition(isi) => isi.execute(authority, wsv), + Self::Asset(isi) => isi.execute(authority, wsv), + } } } -impl Execute for SequenceExpr { - #[iroha_logger::log(skip_all, name = "Sequence", fields(count))] +impl Execute for RemoveKeyValueBox { fn execute(self, authority: &AccountId, wsv: &mut WorldStateView) -> Result<(), Error> { - Span::current().record("count", self.instructions.len()); - for instruction in self.instructions { - iroha_logger::trace!(%instruction); - instruction.execute(authority, wsv)?; + match self { + Self::Domain(isi) => isi.execute(authority, wsv), + Self::Account(isi) => isi.execute(authority, wsv), + Self::AssetDefinition(isi) => isi.execute(authority, wsv), + Self::Asset(isi) => isi.execute(authority, wsv), } - Ok(()) } } @@ -379,86 +201,26 @@ impl Execute for Fail { } } -impl Execute for GrantExpr { +impl Execute for GrantBox { #[iroha_logger::log(name = "grant", skip_all, fields(object))] fn execute(self, authority: &AccountId, wsv: &mut WorldStateView) -> Result<(), Error> { - let destination_id = wsv.evaluate(&self.destination_id)?; - let object = wsv.evaluate(&self.object)?; - Span::current().record("object", &object.to_string()); - iroha_logger::trace!(%destination_id, %authority); - match object { - Value::PermissionToken(object) => Grant:: { - object, - destination_id, - } - .execute(authority, wsv), - Value::Id(IdBox::RoleId(object)) => Grant:: { - object, - destination_id, - } - .execute(authority, wsv), - _ => Err(Error::Evaluate(InstructionType::Grant.into())), + match self { + Self::PermissionToken(sub_isi) => sub_isi.execute(authority, wsv), + Self::Role(sub_isi) => sub_isi.execute(authority, wsv), } } } -impl Execute for RevokeExpr { +impl Execute for RevokeBox { #[iroha_logger::log(name = "revoke", skip_all, fields(object))] fn execute(self, authority: &AccountId, wsv: &mut WorldStateView) -> Result<(), Error> { - let destination_id = wsv.evaluate(&self.destination_id)?; - let object = wsv.evaluate(&self.object)?; - Span::current().record("object", &object.to_string()); - iroha_logger::trace!(?destination_id, ?object, %authority); - match object { - Value::PermissionToken(object) => Revoke:: { - object, - destination_id, - } - .execute(authority, wsv), - Value::Id(IdBox::RoleId(object)) => Revoke:: { - object, - destination_id, - } - .execute(authority, wsv), - _ => Err(Error::Evaluate(InstructionType::Revoke.into())), + match self { + Self::PermissionToken(sub_isi) => sub_isi.execute(authority, wsv), + Self::Role(sub_isi) => sub_isi.execute(authority, wsv), } } } -impl Execute for SetParameterExpr { - fn execute(self, authority: &AccountId, wsv: &mut WorldStateView) -> Result<(), Error> { - let parameter = wsv.evaluate(&self.parameter)?; - SetParameter { parameter }.execute(authority, wsv) - } -} - -impl Execute for NewParameterExpr { - fn execute(self, authority: &AccountId, wsv: &mut WorldStateView) -> Result<(), Error> { - let parameter = wsv.evaluate(&self.parameter)?; - NewParameter { parameter }.execute(authority, wsv) - } -} - -impl Execute for UpgradeExpr { - fn execute(self, authority: &AccountId, wsv: &mut WorldStateView) -> Result<(), Error> { - let object = wsv.evaluate(&self.object)?; - match object { - UpgradableBox::Executor(object) => { - Upgrade:: { object }.execute(authority, wsv) - } - } - } -} - -impl Execute for LogExpr { - fn execute(self, authority: &AccountId, wsv: &mut WorldStateView) -> Result<(), Error> { - let level = wsv.evaluate(&self.level)?; - let msg = wsv.evaluate(&self.msg)?; - - Log { level, msg }.execute(authority, wsv) - } -} - pub mod prelude { //! Re-export important traits and types for glob import `(::*)` pub use super::*; @@ -483,11 +245,11 @@ mod tests { let account_id = AccountId::from_str("alice@wonderland")?; let (public_key, _) = KeyPair::generate()?.into(); let asset_definition_id = AssetDefinitionId::from_str("rose#wonderland")?; - RegisterExpr::new(Domain::new(DomainId::from_str("wonderland")?)) + Register::domain(Domain::new(DomainId::from_str("wonderland")?)) .execute(&genesis_account_id, &mut wsv)?; - RegisterExpr::new(Account::new(account_id, [public_key])) + Register::account(Account::new(account_id, [public_key])) .execute(&genesis_account_id, &mut wsv)?; - RegisterExpr::new(AssetDefinition::store(asset_definition_id)) + Register::asset_definition(AssetDefinition::store(asset_definition_id)) .execute(&genesis_account_id, &mut wsv)?; Ok(wsv) } @@ -499,8 +261,8 @@ mod tests { let account_id = AccountId::from_str("alice@wonderland")?; let asset_definition_id = AssetDefinitionId::from_str("rose#wonderland")?; let asset_id = AssetId::new(asset_definition_id, account_id.clone()); - SetKeyValueExpr::new( - IdBox::from(asset_id.clone()), + SetKeyValue::asset( + asset_id.clone(), Name::from_str("Bytes")?, vec![1_u32, 2_u32, 3_u32], ) @@ -526,8 +288,8 @@ mod tests { let kura = Kura::blank_kura_for_testing(); let mut wsv = wsv_with_test_domains(&kura)?; let account_id = AccountId::from_str("alice@wonderland")?; - SetKeyValueExpr::new( - IdBox::from(account_id.clone()), + SetKeyValue::account( + account_id.clone(), Name::from_str("Bytes")?, vec![1_u32, 2_u32, 3_u32], ) @@ -555,8 +317,8 @@ mod tests { let mut wsv = wsv_with_test_domains(&kura)?; let definition_id = AssetDefinitionId::from_str("rose#wonderland")?; let account_id = AccountId::from_str("alice@wonderland")?; - SetKeyValueExpr::new( - IdBox::from(definition_id.clone()), + SetKeyValue::asset_definition( + definition_id.clone(), Name::from_str("Bytes")?, vec![1_u32, 2_u32, 3_u32], ) @@ -583,8 +345,8 @@ mod tests { let mut wsv = wsv_with_test_domains(&kura)?; let domain_id = DomainId::from_str("wonderland")?; let account_id = AccountId::from_str("alice@wonderland")?; - SetKeyValueExpr::new( - IdBox::from(domain_id.clone()), + SetKeyValue::domain( + domain_id.clone(), Name::from_str("Bytes")?, vec![1_u32, 2_u32, 3_u32], ) @@ -613,7 +375,7 @@ mod tests { let trigger_id = TriggerId::from_str("test_trigger_id")?; assert!(matches!( - ExecuteTriggerExpr::new(trigger_id) + ExecuteTrigger::new(trigger_id) .execute(&account_id, &mut wsv) .expect_err("Error expected"), Error::Find(_) @@ -635,14 +397,14 @@ mod tests { .expect("Failed to generate KeyPair") .into(); let register_account = - RegisterExpr::new(Account::new(fake_account_id.clone(), [public_key])); + Register::account(Account::new(fake_account_id.clone(), [public_key])); register_account.execute(&account_id, &mut wsv)?; // register the trigger - let register_trigger = RegisterExpr::new(Trigger::new( + let register_trigger = Register::trigger(Trigger::new( trigger_id.clone(), Action::new( - Vec::::new(), + Vec::::new(), Repeats::Indefinitely, account_id.clone(), TriggeringFilterBox::ExecuteTrigger(ExecuteTriggerEventFilter::new( @@ -655,11 +417,11 @@ mod tests { register_trigger.execute(&account_id, &mut wsv)?; // execute with the valid account - ExecuteTriggerExpr::new(trigger_id.clone()).execute(&account_id, &mut wsv)?; + ExecuteTrigger::new(trigger_id.clone()).execute(&account_id, &mut wsv)?; // execute with the fake account assert!(matches!( - ExecuteTriggerExpr::new(trigger_id) + ExecuteTrigger::new(trigger_id) .execute(&fake_account_id, &mut wsv) .expect_err("Error expected"), Error::InvariantViolation(_) diff --git a/core/src/smartcontracts/isi/query.rs b/core/src/smartcontracts/isi/query.rs index 19671e06587..d14ed740d0b 100644 --- a/core/src/smartcontracts/isi/query.rs +++ b/core/src/smartcontracts/isi/query.rs @@ -265,14 +265,14 @@ mod tests { wsv.config.transaction_limits = limits; let valid_tx = { - let instructions: [InstructionExpr; 0] = []; + let instructions: [InstructionBox; 0] = []; let tx = TransactionBuilder::new(ALICE_ID.clone()) .with_instructions(instructions) .sign(ALICE_KEYS.clone())?; AcceptedTransaction::accept(tx, &limits)? }; let invalid_tx = { - let isi = Fail::new("fail"); + let isi = Fail::new("fail".to_owned()); let tx = TransactionBuilder::new(ALICE_ID.clone()) .with_instructions([isi.clone(), isi]) .sign(ALICE_KEYS.clone())?; @@ -413,7 +413,7 @@ mod tests { let query_handle = LiveQueryStore::test().start(); let mut wsv = WorldStateView::new(world_with_test_domains(), kura.clone(), query_handle); - let instructions: [InstructionExpr; 0] = []; + let instructions: [InstructionBox; 0] = []; let tx = TransactionBuilder::new(ALICE_ID.clone()) .with_instructions(instructions) .sign(ALICE_KEYS.clone())?; @@ -432,9 +432,7 @@ mod tests { kura.store_block(vcb); let unapplied_tx = TransactionBuilder::new(ALICE_ID.clone()) - .with_instructions([UnregisterExpr::new( - "account@domain".parse::().unwrap(), - )]) + .with_instructions([Unregister::account("account@domain".parse().unwrap())]) .sign(ALICE_KEYS.clone())?; let wrong_hash = unapplied_tx.hash(); let not_found = FindTransactionByHash::new(wrong_hash).execute(&wsv); diff --git a/core/src/smartcontracts/isi/triggers/mod.rs b/core/src/smartcontracts/isi/triggers/mod.rs index 7c814b6fe47..2b8da8ed19f 100644 --- a/core/src/smartcontracts/isi/triggers/mod.rs +++ b/core/src/smartcontracts/isi/triggers/mod.rs @@ -1,9 +1,7 @@ //! This module contains implementations of smart-contract traits and //! instructions for triggers in Iroha. -use iroha_data_model::{ - evaluate::ExpressionEvaluator, isi::error::MathError, prelude::*, query::error::FindError, -}; +use iroha_data_model::{isi::error::MathError, prelude::*, query::error::FindError}; use iroha_telemetry::metrics; pub mod set; @@ -154,13 +152,13 @@ pub mod isi { } } - impl Execute for ExecuteTriggerExpr { + impl Execute for ExecuteTrigger { #[metrics(+"execute_trigger")] fn execute(self, authority: &AccountId, wsv: &mut WorldStateView) -> Result<(), Error> { - let id = wsv.evaluate(&self.trigger_id)?; + let id = &self.trigger_id; wsv.triggers() - .inspect_by_id(&id, |action| -> Result<(), Error> { + .inspect_by_id(id, |action| -> Result<(), Error> { let allow_execute = if let TriggeringFilterBox::ExecuteTrigger(filter) = action.clone_and_box().filter { @@ -186,7 +184,7 @@ pub mod isi { .ok_or_else(|| Error::Find(Box::new(FindError::Trigger(id.clone())))) .and_then(core::convert::identity)?; - wsv.execute_trigger(id, authority); + wsv.execute_trigger(id.clone(), authority); Ok(()) } @@ -217,43 +215,37 @@ pub mod query { impl ValidQuery for FindTriggerById { #[metrics(+"find_trigger_by_id")] fn execute(&self, wsv: &WorldStateView) -> Result, Error> { - let id = wsv - .evaluate(&self.id) - .map_err(|e| Error::Evaluate(format!("Failed to evaluate trigger id. {e}")))?; + let id = &self.id; iroha_logger::trace!(%id); // Can't use just `LoadedActionTrait::clone_and_box` cause this will trigger lifetime mismatch #[allow(clippy::redundant_closure_for_method_calls)] let loaded_action = wsv .triggers() - .inspect_by_id(&id, |action| action.clone_and_box()) + .inspect_by_id(id, |action| action.clone_and_box()) .ok_or_else(|| Error::Find(FindError::Trigger(id.clone())))?; let action = wsv.triggers().get_original_action(loaded_action); // TODO: Should we redact the metadata if the account is not the authority/owner? - Ok(Trigger::new(id, action)) + Ok(Trigger::new(id.clone(), action)) } } impl ValidQuery for FindTriggerKeyValueByIdAndKey { #[metrics(+"find_trigger_key_value_by_id_and_key")] fn execute(&self, wsv: &WorldStateView) -> Result { - let id = wsv - .evaluate(&self.id) - .map_err(|e| Error::Evaluate(format!("Failed to evaluate trigger id. {e}")))?; - let key = wsv - .evaluate(&self.key) - .map_err(|e| Error::Evaluate(format!("Failed to evaluate key. {e}")))?; + let id = &self.id; + let key = &self.key; iroha_logger::trace!(%id, %key); wsv.triggers() - .inspect_by_id(&id, |action| { + .inspect_by_id(id, |action| { action .metadata() - .get(&key) + .get(key) .cloned() .ok_or_else(|| FindError::MetadataKey(key.clone()).into()) }) - .ok_or_else(|| Error::Find(FindError::Trigger(id)))? + .ok_or_else(|| Error::Find(FindError::Trigger(id.clone())))? .map(Into::into) } } @@ -265,13 +257,11 @@ pub mod query { wsv: &'wsv WorldStateView, ) -> eyre::Result> + 'wsv>, Error> { - let domain_id = wsv - .evaluate(&self.domain_id) - .map_err(|e| Error::Evaluate(format!("Failed to evaluate domain id. {e}")))?; + let domain_id = &self.domain_id; Ok(Box::new( wsv.triggers() - .inspect_by_domain_id(&domain_id, |trigger_id, action| { + .inspect_by_domain_id(domain_id, |trigger_id, action| { (trigger_id.clone(), action.clone_and_box()) }) .map(|(trigger_id, action)| { diff --git a/core/src/smartcontracts/isi/triggers/set.rs b/core/src/smartcontracts/isi/triggers/set.rs index b32e49190e1..57e32ae1406 100644 --- a/core/src/smartcontracts/isi/triggers/set.rs +++ b/core/src/smartcontracts/isi/triggers/set.rs @@ -862,7 +862,7 @@ pub enum LoadedExecutable { /// Loaded WASM Wasm(LoadedWasm), /// Vector of ISI - Instructions(Vec), + Instructions(Vec), } impl core::fmt::Debug for LoadedExecutable { diff --git a/core/src/smartcontracts/isi/tx.rs b/core/src/smartcontracts/isi/tx.rs index b33fa69f7f5..f103853a9d5 100644 --- a/core/src/smartcontracts/isi/tx.rs +++ b/core/src/smartcontracts/isi/tx.rs @@ -2,11 +2,10 @@ use std::sync::Arc; -use eyre::{Result, WrapErr}; +use eyre::Result; use iroha_crypto::HashOf; use iroha_data_model::{ block::SignedBlock, - evaluate::ExpressionEvaluator, prelude::*, query::{ error::{FindError, QueryExecutionFail}, @@ -66,7 +65,7 @@ impl ValidQuery for FindAllTransactions { .flat_map(BlockTransactionIter::new) .map(|tx| TransactionQueryOutput { block_hash: tx.block_hash(), - transaction: tx.value(), + transaction: Box::new(tx.value()), }), )) } @@ -78,10 +77,7 @@ impl ValidQuery for FindTransactionsByAccountId { &self, wsv: &'wsv WorldStateView, ) -> Result + 'wsv>, QueryExecutionFail> { - let account_id = wsv - .evaluate(&self.account_id) - .wrap_err("Failed to get account id") - .map_err(|e| QueryExecutionFail::Evaluate(e.to_string()))?; + let account_id = self.account_id.clone(); Ok(Box::new( wsv.all_blocks() @@ -89,7 +85,7 @@ impl ValidQuery for FindTransactionsByAccountId { .filter(move |tx| *tx.authority() == account_id) .map(|tx| TransactionQueryOutput { block_hash: tx.block_hash(), - transaction: tx.value(), + transaction: Box::new(tx.value()), }), )) } @@ -98,10 +94,7 @@ impl ValidQuery for FindTransactionsByAccountId { impl ValidQuery for FindTransactionByHash { #[metrics(+"find_transaction_by_hash")] fn execute(&self, wsv: &WorldStateView) -> Result { - let tx_hash = wsv - .evaluate(&self.hash) - .wrap_err("Failed to get hash") - .map_err(|e| QueryExecutionFail::Evaluate(e.to_string()))?; + let tx_hash = self.hash; iroha_logger::trace!(%tx_hash); if !wsv.has_transaction(tx_hash) { return Err(FindError::Transaction(tx_hash).into()); @@ -118,6 +111,7 @@ impl ValidQuery for FindTransactionByHash { .iter() .find(|transaction| transaction.value.hash() == tx_hash) .cloned() + .map(Box::new) .map(|transaction| TransactionQueryOutput { block_hash, transaction, diff --git a/core/src/smartcontracts/isi/world.rs b/core/src/smartcontracts/isi/world.rs index 64199fd9eb8..44ae2f2eb2e 100644 --- a/core/src/smartcontracts/isi/world.rs +++ b/core/src/smartcontracts/isi/world.rs @@ -214,10 +214,10 @@ pub mod isi { } } - impl Execute for Upgrade { + impl Execute for Upgrade { #[metrics(+"upgrade_executor")] fn execute(self, authority: &AccountId, wsv: &mut WorldStateView) -> Result<(), Error> { - let raw_executor = self.object; + let raw_executor = self.executor; // Cloning executor to avoid multiple mutable borrows of `wsv`. // Also it's a cheap operation. @@ -303,13 +303,11 @@ pub mod query { impl ValidQuery for FindRoleByRoleId { #[metrics(+"find_role_by_role_id")] fn execute(&self, wsv: &WorldStateView) -> Result { - let role_id = wsv - .evaluate(&self.id) - .map_err(|e| Error::Evaluate(e.to_string()))?; + let role_id = &self.id; iroha_logger::trace!(%role_id); - wsv.world.roles.get(&role_id).map_or_else( - || Err(Error::Find(FindError::Role(role_id))), + wsv.world.roles.get(role_id).map_or_else( + || Err(Error::Find(FindError::Role(role_id.clone()))), |role_ref| Ok(role_ref.clone()), ) } diff --git a/core/src/smartcontracts/mod.rs b/core/src/smartcontracts/mod.rs index 05d0195defd..35587c6580b 100644 --- a/core/src/smartcontracts/mod.rs +++ b/core/src/smartcontracts/mod.rs @@ -7,15 +7,12 @@ pub mod isi; pub mod wasm; -use std::collections::BTreeMap; - use iroha_data_model::{ - evaluate::ExpressionEvaluator, isi::error::InstructionExecutionError as Error, prelude::*, - query::error::QueryExecutionFail, + isi::error::InstructionExecutionError as Error, prelude::*, query::error::QueryExecutionFail, }; pub use isi::*; -use self::query::{Lazy, LazyValue}; +use self::query::Lazy; use crate::wsv::WorldStateView; /// Trait implementations should provide actions to apply changes on [`WorldStateView`]. @@ -44,50 +41,3 @@ where wsv: &'wsv WorldStateView, ) -> Result<::Lazy<'wsv>, QueryExecutionFail>; } - -impl ExpressionEvaluator for WorldStateView { - fn evaluate( - &self, - expression: &E, - ) -> Result { - expression.evaluate(&Context::new(self)) - } -} - -#[derive(Clone)] -pub(crate) struct Context<'wsv> { - values: BTreeMap, - wsv: &'wsv WorldStateView, -} - -impl<'a> Context<'a> { - /// Create new [`Self`] - pub fn new(wsv: &'a WorldStateView) -> Self { - Self { - values: BTreeMap::new(), - wsv, - } - } -} - -impl iroha_data_model::evaluate::Context for Context<'_> { - fn query(&self, query: &QueryBox) -> Result { - query - .execute(self.wsv) - .map(|value| match value { - LazyValue::Value(value) => value, - // NOTE: This will only be executed when evaluating an expression for an - // instruction, i.e. it will only be executed from the executor. - LazyValue::Iter(iter) => Value::Vec(iter.collect()), - }) - .map_err(Into::into) - } - - fn get(&self, name: &Name) -> Option<&Value> { - self.values.get(name) - } - - fn update(&mut self, other: impl IntoIterator) { - self.values.extend(other) - } -} diff --git a/core/src/smartcontracts/wasm.rs b/core/src/smartcontracts/wasm.rs index a7c0b8b757e..cde7fe6a624 100644 --- a/core/src/smartcontracts/wasm.rs +++ b/core/src/smartcontracts/wasm.rs @@ -13,7 +13,7 @@ use iroha_config::{ use iroha_data_model::{ account::AccountId, executor::{self, MigrationResult}, - isi::InstructionExpr, + isi::InstructionBox, permission::PermissionTokenSchema, prelude::*, query::{QueryBox, QueryId, QueryRequest, QueryWithParameters}, @@ -87,7 +87,7 @@ mod import { /// Execute `instruction` on host #[codec::wrap_trait_fn] fn execute_instruction( - instruction: InstructionExpr, + instruction: InstructionBox, state: &mut S, ) -> Result<(), ValidationFail>; } @@ -100,7 +100,7 @@ mod import { fn get_validate_transaction_payload(state: &S) -> Validate; #[codec::wrap_trait_fn] - fn get_validate_instruction_payload(state: &S) -> Validate; + fn get_validate_instruction_payload(state: &S) -> Validate; #[codec::wrap_trait_fn] fn get_validate_query_payload(state: &S) -> Validate; @@ -480,7 +480,7 @@ pub mod state { pub type ValidateQuery = Validate; /// State kind for executing `validate_instruction()` entrypoint of executor - pub type ValidateInstruction = Validate; + pub type ValidateInstruction = Validate; /// State kind for executing `migrate()` entrypoint of executor #[derive(Copy, Clone)] @@ -802,7 +802,7 @@ where impl<'wrld, S> Runtime, S>> { fn default_execute_instruction( - instruction: InstructionExpr, + instruction: InstructionBox, state: &mut state::CommonState, S>, ) -> Result<(), ValidationFail> { debug!(%instruction, "Executing"); @@ -912,7 +912,7 @@ impl<'wrld> import::traits::ExecuteOperations> #[codec::wrap] fn execute_instruction( - instruction: InstructionExpr, + instruction: InstructionBox, state: &mut state::SmartContract<'wrld>, ) -> Result<(), ValidationFail> { if let Some(limits_executor) = state.specific_state.limits_executor.as_mut() { @@ -984,7 +984,7 @@ impl<'wrld> import::traits::ExecuteOperations> #[codec::wrap] fn execute_instruction( - instruction: InstructionExpr, + instruction: InstructionBox, state: &mut state::Trigger<'wrld>, ) -> Result<(), ValidationFail> { Self::default_execute_instruction(instruction, state) @@ -1015,7 +1015,7 @@ where #[codec::wrap] fn execute_instruction( - instruction: InstructionExpr, + instruction: InstructionBox, state: &mut state::CommonState, S>, ) -> Result<(), ValidationFail> { debug!(%instruction, "Executing as executor"); @@ -1109,7 +1109,7 @@ impl<'wrld> import::traits::GetExecutorPayloads, - ) -> Validate { + ) -> Validate { panic!("Executor `validate_transaction()` entrypoint should not query payload for `validate_instruction()` entrypoint") } @@ -1141,7 +1141,7 @@ impl<'wrld> Runtime> { wsv: &'wrld mut WorldStateView, authority: &AccountId, module: &wasmtime::Module, - instruction: InstructionExpr, + instruction: InstructionBox, ) -> Result { let span = wasm_log_span!("Running `validate_instruction()`"); @@ -1184,7 +1184,7 @@ impl<'wrld> import::traits::GetExecutorPayloads, - ) -> Validate { + ) -> Validate { Validate { authority: state.authority.clone(), block_height: state.wsv.0.height(), @@ -1253,7 +1253,7 @@ impl<'wrld> import::traits::ExecuteOperations, ) -> Result<(), ValidationFail> { panic!("Executor `validate_query()` entrypoint should not execute instructions") @@ -1278,7 +1278,7 @@ impl<'wrld> import::traits::GetExecutorPayloads, - ) -> Validate { + ) -> Validate { panic!("Executor `validate_query()` entrypoint should not query payload for `validate_instruction()` entrypoint") } @@ -1377,7 +1377,7 @@ impl<'wrld> import::traits::GetExecutorPayloads> #[codec::wrap] fn get_validate_instruction_payload( _state: &state::executor::Migrate<'wrld>, - ) -> Validate { + ) -> Validate { panic!("Executor `migrate()` entrypoint should not query payload for `validate_instruction()` entrypoint") } @@ -1715,8 +1715,8 @@ mod tests { let isi_hex = { let new_authority = AccountId::from_str("mad_hatter@wonderland").expect("Valid"); - let register_isi = RegisterExpr::new(Account::new(new_authority, [])); - encode_hex(InstructionExpr::from(register_isi)) + let register_isi = Register::account(Account::new(new_authority, [])); + encode_hex(InstructionBox::from(register_isi)) }; let wat = format!( @@ -1801,8 +1801,8 @@ mod tests { let isi_hex = { let new_authority = AccountId::from_str("mad_hatter@wonderland").expect("Valid"); - let register_isi = RegisterExpr::new(Account::new(new_authority, [])); - encode_hex(InstructionExpr::from(register_isi)) + let register_isi = Register::account(Account::new(new_authority, [])); + encode_hex(InstructionBox::from(register_isi)) }; let wat = format!( @@ -1850,8 +1850,8 @@ mod tests { let isi_hex = { let new_authority = AccountId::from_str("mad_hatter@wonderland").expect("Valid"); - let register_isi = RegisterExpr::new(Account::new(new_authority, [])); - encode_hex(InstructionExpr::from(register_isi)) + let register_isi = Register::account(Account::new(new_authority, [])); + encode_hex(InstructionBox::from(register_isi)) }; let wat = format!( diff --git a/core/src/sumeragi/main_loop.rs b/core/src/sumeragi/main_loop.rs index 441c0946b2d..b24b263188a 100644 --- a/core/src/sumeragi/main_loop.rs +++ b/core/src/sumeragi/main_loop.rs @@ -1210,7 +1210,7 @@ mod tests { // Create "genesis" block // Creating an instruction - let fail_box: InstructionExpr = Fail::new("Dummy isi").into(); + let fail_box: InstructionBox = Fail::new("Dummy isi".to_owned()).into(); // Making two transactions that have the same instruction let tx = TransactionBuilder::new(alice_id.clone()) @@ -1231,10 +1231,10 @@ mod tests { kura.store_block(genesis); // Making two transactions that have the same instruction - let create_asset_definition1 = RegisterExpr::new(AssetDefinition::quantity( + let create_asset_definition1 = Register::asset_definition(AssetDefinition::quantity( "xor1#wonderland".parse().expect("Valid"), )); - let create_asset_definition2 = RegisterExpr::new(AssetDefinition::quantity( + let create_asset_definition2 = Register::asset_definition(AssetDefinition::quantity( "xor2#wonderland".parse().expect("Valid"), )); diff --git a/core/src/tx.rs b/core/src/tx.rs index 790d8942326..01ee688edcf 100644 --- a/core/src/tx.rs +++ b/core/src/tx.rs @@ -36,179 +36,6 @@ pub enum AcceptTransactionFail { UnexpectedGenesisAccountSignature, } -mod len { - use iroha_data_model::{expression::*, query::QueryBox, Value}; - - pub trait ExprLen { - fn len(&self) -> usize; - } - - impl> ExprLen for EvaluatesTo { - fn len(&self) -> usize { - self.expression.len() - } - } - - impl ExprLen for Expression { - fn len(&self) -> usize { - use Expression::*; - - match self { - Add(add) => add.len(), - Subtract(subtract) => subtract.len(), - Greater(greater) => greater.len(), - Less(less) => less.len(), - Equal(equal) => equal.len(), - Not(not) => not.len(), - And(and) => and.len(), - Or(or) => or.len(), - If(if_expression) => if_expression.len(), - Raw(raw) => raw.len(), - Query(query) => query.len(), - Contains(contains) => contains.len(), - ContainsAll(contains_all) => contains_all.len(), - ContainsAny(contains_any) => contains_any.len(), - Where(where_expression) => where_expression.len(), - ContextValue(context_value) => context_value.len(), - Multiply(multiply) => multiply.len(), - Divide(divide) => divide.len(), - Mod(modulus) => modulus.len(), - RaiseTo(raise_to) => raise_to.len(), - } - } - } - impl ExprLen for ContextValue { - fn len(&self) -> usize { - 1 - } - } - impl ExprLen for QueryBox { - fn len(&self) -> usize { - 1 - } - } - - impl ExprLen for Add { - fn len(&self) -> usize { - self.left.len() + self.right.len() + 1 - } - } - impl ExprLen for Subtract { - fn len(&self) -> usize { - self.left.len() + self.right.len() + 1 - } - } - impl ExprLen for Multiply { - fn len(&self) -> usize { - self.left.len() + self.right.len() + 1 - } - } - impl ExprLen for RaiseTo { - fn len(&self) -> usize { - self.left.len() + self.right.len() + 1 - } - } - impl ExprLen for Divide { - fn len(&self) -> usize { - self.left.len() + self.right.len() + 1 - } - } - impl ExprLen for Mod { - fn len(&self) -> usize { - self.left.len() + self.right.len() + 1 - } - } - impl ExprLen for Greater { - fn len(&self) -> usize { - self.left.len() + self.right.len() + 1 - } - } - impl ExprLen for Less { - fn len(&self) -> usize { - self.left.len() + self.right.len() + 1 - } - } - impl ExprLen for Equal { - fn len(&self) -> usize { - self.left.len() + self.right.len() + 1 - } - } - impl ExprLen for And { - fn len(&self) -> usize { - self.left.len() + self.right.len() + 1 - } - } - impl ExprLen for Or { - fn len(&self) -> usize { - self.left.len() + self.right.len() + 1 - } - } - - impl ExprLen for Not { - fn len(&self) -> usize { - self.expression.len() + 1 - } - } - - impl ExprLen for Contains { - fn len(&self) -> usize { - self.collection.len() + self.element.len() + 1 - } - } - impl ExprLen for ContainsAll { - fn len(&self) -> usize { - self.collection.len() + self.elements.len() + 1 - } - } - impl ExprLen for ContainsAny { - fn len(&self) -> usize { - self.collection.len() + self.elements.len() + 1 - } - } - - impl ExprLen for If { - fn len(&self) -> usize { - // TODO: This is wrong because we don't evaluate both branches - self.condition.len() + self.then.len() + self.otherwise.len() + 1 - } - } - impl ExprLen for Where { - fn len(&self) -> usize { - self.expression.len() + self.values.values().map(EvaluatesTo::len).sum::() + 1 - } - } -} - -fn instruction_size(isi: &InstructionExpr) -> usize { - use len::ExprLen as _; - use InstructionExpr::*; - - match isi { - Register(isi) => isi.object.len() + 1, - Unregister(isi) => isi.object_id.len() + 1, - Mint(isi) => isi.destination_id.len() + isi.object.len() + 1, - Burn(isi) => isi.destination_id.len() + isi.object.len() + 1, - Transfer(isi) => isi.destination_id.len() + isi.object.len() + isi.source_id.len() + 1, - If(isi) => { - let otherwise = isi.otherwise.as_ref().map_or(0, instruction_size); - isi.condition.len() + instruction_size(&isi.then) + otherwise + 1 - } - Pair(isi) => { - instruction_size(&isi.left_instruction) + instruction_size(&isi.right_instruction) + 1 - } - Sequence(isi) => isi.instructions.iter().map(instruction_size).sum::() + 1, - SetKeyValue(isi) => isi.object_id.len() + isi.key.len() + isi.value.len() + 1, - RemoveKeyValue(isi) => isi.object_id.len() + isi.key.len() + 1, - Grant(isi) => isi.object.len() + isi.destination_id.len() + 1, - Revoke(isi) => isi.object.len() + isi.destination_id.len() + 1, - SetParameter(isi) => isi.parameter.len() + 1, - NewParameter(isi) => isi.parameter.len() + 1, - Upgrade(isi) => isi.object.len() + 1, - Log(isi) => isi.msg.len() + isi.msg.len() + 1, - Fail(_) | ExecuteTrigger(_) => 1, - } -} - impl AcceptedTransaction { /// Accept genesis transaction. Transition from [`GenesisTransaction`] to [`AcceptedTransaction`]. pub fn accept_genesis(tx: GenesisTransaction) -> Self { @@ -230,14 +57,10 @@ impl AcceptedTransaction { match &transaction.payload().instructions { Executable::Instructions(instructions) => { - let instruction_count: u64 = instructions - .iter() - .map(instruction_size) - .sum::() - .try_into() - .expect("`usize` should always fit in `u64`"); - - if instruction_count > limits.max_instruction_number { + let instruction_count = instructions.len(); + if u64::try_from(instruction_count).expect("`usize` should always fit into `u64`") + > limits.max_instruction_number + { return Err(AcceptTransactionFail::TransactionLimit( TransactionLimitError { reason: format!( @@ -426,67 +249,3 @@ impl TransactionExecutor { }) } } - -#[cfg(test)] -mod tests { - use core::str::FromStr as _; - - use super::*; - - fn if_instruction( - c: impl Into, - then: InstructionExpr, - otherwise: Option, - ) -> InstructionExpr { - let condition: Expression = c.into(); - let condition = EvaluatesTo::new_unchecked(condition); - ConditionalExpr { - condition, - then, - otherwise, - } - .into() - } - - fn fail() -> InstructionExpr { - Fail { - message: String::default(), - } - .into() - } - - #[test] - fn len_empty_sequence() { - assert_eq!(instruction_size(&SequenceExpr::new(vec![]).into()), 1); - } - - #[test] - fn len_if_one_branch() { - let instructions = vec![if_instruction( - ContextValue { - value_name: Name::from_str("a").expect("Cannot fail."), - }, - fail(), - None, - )]; - - assert_eq!(instruction_size(&SequenceExpr::new(instructions).into()), 4); - } - - #[test] - fn len_sequence_if() { - let instructions = vec![ - fail(), - if_instruction( - ContextValue { - value_name: Name::from_str("b").expect("Cannot fail."), - }, - fail(), - Some(fail()), - ), - fail(), - ]; - - assert_eq!(instruction_size(&SequenceExpr::new(instructions).into()), 7); - } -} diff --git a/core/src/wsv.rs b/core/src/wsv.rs index 29274a3323e..01c4e8e8d30 100644 --- a/core/src/wsv.rs +++ b/core/src/wsv.rs @@ -602,7 +602,7 @@ impl WorldStateView { fn process_instructions( &mut self, - instructions: impl IntoIterator, + instructions: impl IntoIterator, authority: &AccountId, ) -> Result<()> { instructions.into_iter().try_for_each(|instruction| { diff --git a/core/test_network/src/lib.rs b/core/test_network/src/lib.rs index 96c4210fd08..b51e1726e47 100644 --- a/core/test_network/src/lib.rs +++ b/core/test_network/src/lib.rs @@ -118,7 +118,7 @@ impl TestGenesis for GenesisNetwork { upgrade_executor_permission, ] { first_transaction - .append_instruction(GrantExpr::new(permission, alice_id.clone()).into()); + .append_instruction(Grant::permission_token(permission, alice_id.clone()).into()); } if submit_genesis { @@ -211,7 +211,7 @@ impl Network { time::sleep(Configuration::pipeline_time() + Configuration::block_sync_gossip_time()).await; - let add_peer = RegisterExpr::new(DataModelPeer::new(peer.id.clone())); + let add_peer = Register::peer(DataModelPeer::new(peer.id.clone())); genesis_client .submit(add_peer) .expect("Failed to add new peer."); @@ -704,7 +704,7 @@ pub trait TestClient: Sized { /// If predicate is not satisfied, after maximum retries. fn submit_all_till( &self, - instructions: Vec, + instructions: Vec, request: R, f: impl Fn(::Target) -> bool, ) -> eyre::Result<()> @@ -836,7 +836,7 @@ impl TestClient for Client { fn submit_all_till( &self, - instructions: Vec, + instructions: Vec, request: R, f: impl Fn(::Target) -> bool, ) -> eyre::Result<()> diff --git a/data_model/src/evaluate.rs b/data_model/src/evaluate.rs deleted file mode 100644 index b566bf4e0b9..00000000000 --- a/data_model/src/evaluate.rs +++ /dev/null @@ -1,869 +0,0 @@ -//! Implementations for Expression evaluation for different expressions. - -#[cfg(not(feature = "std"))] -use alloc::{ - boxed::Box, - collections::BTreeMap, - format, - string::{String, ToString}, - vec::Vec, -}; -#[cfg(feature = "std")] -use std::collections::BTreeMap; - -use iroha_data_model_derive::model; -use iroha_macro::FromVariant; -use iroha_schema::IntoSchema; - -pub use self::model::*; -use crate::{ - expression::{prelude::*, Expression}, - isi::error::{BinaryOpIncompatibleNumericValueTypesError, MathError}, - prelude::*, -}; - -/// Expression evaluator -pub trait ExpressionEvaluator { - /// Evaluates expression against current state of the underlying system - /// - /// # Errors - /// - /// - if expression is malformed - fn evaluate(&self, expression: &E) -> Result; -} - -/// Context of expression evaluation, holding (name, value) pairs for resolving identifiers. -/// Context comes into play because of [`Where`] and [`Query`] expressions. -/// -/// # Example -/// -/// Say you have an expression such as: `SELECT name FROM table WHERE name = "alice"`. This -/// compound expression is made up of two basic expressions, namely `SELECT FROM` and `WHERE`. -/// To evaluate any expression you have to substitute concrete values for variable names. -/// In this case, `WHERE` should be evaluated first which would place `name = "alice"` -/// inside the context. This context will then be used to evaluate `SELECT FROM`. -/// Starting expression would then be evaluated to `SELECT "alice" FROM table` -pub trait Context: Clone { - /// Execute query against the current state of `Iroha` - /// - /// # Errors - /// - /// If query execution fails - fn query(&self, query: &QueryBox) -> Result; - - /// Return a reference to the [`Value`] corresponding to the [`Name`]. - fn get(&self, name: &Name) -> Option<&Value>; - - /// Update this context with given values. - fn update(&mut self, other: impl IntoIterator); -} - -/// Calculate the result of the expression without mutating the state. -#[allow(clippy::len_without_is_empty)] // NOTE: Evaluate cannot be empty -pub trait Evaluate { - /// The resulting type of the expression. - type Value; - - /// Calculate result. - /// - /// # Errors - /// Concrete to each implementer. - fn evaluate(&self, context: &C) -> Result; -} - -impl> Evaluate for EvaluatesTo -where - V::Error: ToString, -{ - type Value = V; - - fn evaluate(&self, context: &C) -> Result { - let expr = self.expression.evaluate(context)?; - - V::try_from(expr).map_err(|error| EvaluationError::Conversion(error.to_string())) - } -} - -impl Evaluate for Expression { - type Value = Value; - - fn evaluate(&self, context: &C) -> Result { - macro_rules! match_evals { - ($($non_value: ident),+ $(,)?) => { - match self { $( - $non_value(expr) => expr.evaluate(context).map(Into::into)?, )+ - Raw(value) => value.clone(), - } - }; - } - - use Expression::*; - let result = match_evals!( - // numeric - Add, - Subtract, - Greater, - Less, - Multiply, - Divide, - Mod, - RaiseTo, - // logical - Equal, - Not, - And, - Or, - Contains, - ContainsAll, - ContainsAny, - // value - If, - Where, - Query, - ContextValue, - ); - - Ok(result) - } -} - -impl Evaluate for ContextValue { - type Value = Value; - - fn evaluate(&self, context: &C) -> Result { - context - .get(&self.value_name) - .cloned() - .ok_or_else(|| EvaluationError::Find(self.value_name.to_string())) - } -} - -mod numeric { - use super::*; - - impl Evaluate for Add { - type Value = NumericValue; - - fn evaluate(&self, context: &C) -> Result { - use NumericValue::*; - let left = self.left.evaluate(context)?; - let right = self.right.evaluate(context)?; - - let result = match (left, right) { - (U32(left), U32(right)) => left - .checked_add(right) - .ok_or(MathError::Overflow) - .map(NumericValue::from)?, - (U128(left), U128(right)) => left - .checked_add(right) - .ok_or(MathError::Overflow) - .map(NumericValue::from)?, - (Fixed(left), Fixed(right)) => left - .checked_add(right) - .map(NumericValue::from) - .map_err(MathError::from)?, - (left, right) => Err(MathError::from( - BinaryOpIncompatibleNumericValueTypesError { left, right }, - ))?, - }; - - Ok(result) - } - } - - impl Evaluate for Subtract { - type Value = NumericValue; - - fn evaluate(&self, context: &C) -> Result { - use NumericValue::*; - let left = self.left.evaluate(context)?; - let right = self.right.evaluate(context)?; - - let result = match (left, right) { - (U32(left), U32(right)) => left - .checked_sub(right) - .ok_or(MathError::NotEnoughQuantity) - .map(NumericValue::from)?, - (U128(left), U128(right)) => left - .checked_sub(right) - .ok_or(MathError::NotEnoughQuantity) - .map(NumericValue::from)?, - (Fixed(left), Fixed(right)) => left - .checked_sub(right) - .map(NumericValue::from) - .map_err(MathError::from)?, - (left, right) => Err(MathError::from( - BinaryOpIncompatibleNumericValueTypesError { left, right }, - ))?, - }; - - Ok(result) - } - } - - impl Evaluate for Multiply { - type Value = NumericValue; - - fn evaluate(&self, context: &C) -> Result { - use NumericValue::*; - let left = self.left.evaluate(context)?; - let right = self.right.evaluate(context)?; - - let result = match (left, right) { - (U32(left), U32(right)) => left - .checked_mul(right) - .ok_or(MathError::Overflow) - .map(NumericValue::from)?, - (U128(left), U128(right)) => left - .checked_mul(right) - .ok_or(MathError::Overflow) - .map(NumericValue::from)?, - (Fixed(left), Fixed(right)) => left - .checked_mul(right) - .map(NumericValue::from) - .map_err(MathError::from)?, - (left, right) => Err(MathError::from( - BinaryOpIncompatibleNumericValueTypesError { left, right }, - ))?, - }; - - Ok(result) - } - } - - impl Evaluate for RaiseTo { - type Value = NumericValue; - - fn evaluate(&self, context: &C) -> Result { - use NumericValue::*; - let value = self.left.evaluate(context)?; - let exp = self.right.evaluate(context)?; - - let result = match (value, exp) { - (U32(value), U32(exp)) => value - .checked_pow(exp) - .ok_or(MathError::Overflow) - .map(NumericValue::from)?, - (U128(value), U32(exp)) => value - .checked_pow(exp) - .ok_or(MathError::Overflow) - .map(NumericValue::from)?, - // TODO (#2945): Extend `RaiseTo` to support `Fixed` - (left, right) => Err(MathError::from( - BinaryOpIncompatibleNumericValueTypesError { left, right }, - ))?, - }; - - Ok(result) - } - } - - impl Evaluate for Divide { - type Value = NumericValue; - - fn evaluate(&self, context: &C) -> Result { - use NumericValue::*; - let left = self.left.evaluate(context)?; - let right = self.right.evaluate(context)?; - - let result = match (left, right) { - (U32(left), U32(right)) => left - .checked_div(right) - .ok_or(MathError::DivideByZero) - .map(NumericValue::from)?, - (U128(left), U128(right)) => left - .checked_div(right) - .ok_or(MathError::DivideByZero) - .map(NumericValue::from)?, - (Fixed(left), Fixed(right)) => left - .checked_div(right) - .map(NumericValue::from) - .map_err(MathError::from)?, - (left, right) => Err(MathError::from( - BinaryOpIncompatibleNumericValueTypesError { left, right }, - ))?, - }; - - Ok(result) - } - } - - impl Evaluate for Mod { - type Value = NumericValue; - - fn evaluate(&self, context: &C) -> Result { - use NumericValue::*; - let left = self.left.evaluate(context)?; - let right = self.right.evaluate(context)?; - - let result = match (left, right) { - (U32(left), U32(right)) => left - .checked_rem(right) - .ok_or(MathError::DivideByZero) - .map(NumericValue::from)?, - (U128(left), U128(right)) => left - .checked_rem(right) - .ok_or(MathError::DivideByZero) - .map(NumericValue::from)?, - (left, right) => Err(MathError::from( - BinaryOpIncompatibleNumericValueTypesError { left, right }, - ))?, - }; - - Ok(result) - } - } -} - -mod logical { - use super::*; - - impl Evaluate for Greater { - type Value = bool; - - fn evaluate(&self, context: &C) -> Result { - use NumericValue::*; - let left = self.left.evaluate(context)?; - let right = self.right.evaluate(context)?; - - let result = match (left, right) { - (U32(left), U32(right)) => left > right, - (U128(left), U128(right)) => left > right, - (Fixed(left), Fixed(right)) => left > right, - (left, right) => Err(MathError::from( - BinaryOpIncompatibleNumericValueTypesError { left, right }, - ))?, - }; - - Ok(result) - } - } - - impl Evaluate for Less { - type Value = bool; - - fn evaluate(&self, context: &C) -> Result { - use NumericValue::*; - let left = self.left.evaluate(context)?; - let right = self.right.evaluate(context)?; - - let result = match (left, right) { - (U32(left), U32(right)) => left < right, - (U128(left), U128(right)) => left < right, - (Fixed(left), Fixed(right)) => left < right, - (left, right) => Err(MathError::from( - BinaryOpIncompatibleNumericValueTypesError { left, right }, - ))?, - }; - - Ok(result) - } - } - - impl Evaluate for Equal { - type Value = bool; - - fn evaluate(&self, context: &C) -> Result { - let left = self.left.evaluate(context)?; - let right = self.right.evaluate(context)?; - Ok(left == right) - } - } - - impl Evaluate for And { - type Value = bool; - - fn evaluate(&self, context: &C) -> Result { - let left = self.left.evaluate(context)?; - let right = self.right.evaluate(context)?; - Ok(left && right) - } - } - - impl Evaluate for Or { - type Value = bool; - - fn evaluate(&self, context: &C) -> Result { - let left = self.left.evaluate(context)?; - let right = self.right.evaluate(context)?; - Ok(left || right) - } - } - - impl Evaluate for Not { - type Value = bool; - - fn evaluate(&self, context: &C) -> Result { - let expression = self.expression.evaluate(context)?; - Ok(!expression) - } - } - - impl Evaluate for Contains { - type Value = bool; - - fn evaluate(&self, context: &C) -> Result { - let collection = self.collection.evaluate(context)?; - let element = self.element.evaluate(context)?; - Ok(collection.contains(&element)) - } - } - - impl Evaluate for ContainsAll { - type Value = bool; - - fn evaluate(&self, context: &C) -> Result { - let collection = self.collection.evaluate(context)?; - let elements = self.elements.evaluate(context)?; - Ok(elements.iter().all(|element| collection.contains(element))) - } - } - - impl Evaluate for ContainsAny { - type Value = bool; - - fn evaluate(&self, context: &C) -> Result { - let collection = self.collection.evaluate(context)?; - let elements = self.elements.evaluate(context)?; - Ok(elements.iter().any(|element| collection.contains(element))) - } - } -} - -impl Evaluate for If { - type Value = Value; - - fn evaluate(&self, context: &C) -> Result { - let condition = self.condition.evaluate(context)?; - if condition { - self.then.evaluate(context) - } else { - self.otherwise.evaluate(context) - } - } -} - -impl Evaluate for Where { - type Value = Value; - - fn evaluate(&self, context: &C) -> Result { - let additional_context: Result, EvaluationError> = self - .values - .clone() - .into_iter() - .map(|(value_name, expression)| { - expression - .evaluate(context) - .map(|expression_result| (value_name, expression_result)) - }) - .collect(); - - let mut combined_context = context.clone(); - combined_context.update(additional_context?); - self.expression.evaluate(&combined_context) - } -} - -impl Evaluate for QueryBox { - type Value = Value; - - fn evaluate(&self, context: &C) -> Result { - context - .query(self) - .map_err(|err| EvaluationError::Validation(Box::new(err))) - } -} - -#[model] -pub mod model { - #[cfg(not(feature = "std"))] - use alloc::boxed::Box; - - use parity_scale_codec::{Decode, Encode}; - use serde::{Deserialize, Serialize}; - - use super::*; - - /// Expression evaluation error - #[derive( - Debug, - displaydoc::Display, - Clone, - PartialEq, - Eq, - PartialOrd, - Ord, - FromVariant, - Deserialize, - Serialize, - Decode, - Encode, - IntoSchema, - )] - #[cfg_attr(feature = "std", derive(thiserror::Error))] - // TODO: Only temporarily opaque because of problems with FFI - #[ffi_type(opaque)] - pub enum EvaluationError { - /// Failed due to math exception - Math(#[cfg_attr(feature = "std", source)] MathError), - /// Validation failed - Validation(#[cfg_attr(feature = "std", source)] Box), - /// `{0}`: Value not found in the context - Find( - #[skip_from] - #[skip_try_from] - String, - ), - /// Conversion evaluation error: {0} - Conversion( - #[skip_from] - #[skip_try_from] - String, - ), - } -} - -pub mod prelude { - //! Prelude: re-export of most commonly used traits, structs and macros in this crate. - - pub use super::Evaluate; -} - -#[cfg(test)] -mod tests { - use core::{fmt::Debug, str::FromStr as _}; - - use iroha_crypto::KeyPair; - use iroha_primitives::fixed::Fixed; - use parity_scale_codec::{DecodeAll, Encode}; - - use super::*; - use crate::val_vec; - - /// Context of expression evaluation - #[derive(Clone)] - #[repr(transparent)] - struct TestContext { - values: BTreeMap, - } - - impl TestContext { - fn new() -> Self { - Self { - values: BTreeMap::new(), - } - } - } - - impl super::Context for TestContext { - fn query(&self, _: &QueryBox) -> Result { - unimplemented!("This has to be tested on iroha_core") - } - - fn get(&self, name: &Name) -> Option<&Value> { - self.values.get(name) - } - - fn update(&mut self, other: impl IntoIterator) { - self.values.extend(other) - } - } - - /// Example taken from [whitepaper](https://github.com/hyperledger/iroha/blob/iroha2-dev/docs/source/iroha_2_whitepaper.md#261-multisignature-transactions) - #[test] - #[allow(clippy::too_many_lines)] - fn conditional_multisignature_quorum() -> Result<(), EvaluationError> { - let asset_quantity_high = 750_u32.to_value(); - let asset_quantity_low = 300_u32.to_value(); - let (public_key_teller_1, _) = KeyPair::generate().expect("Valid").into(); - let (public_key_teller_2, _) = KeyPair::generate().expect("Valid").into(); - let (manager_public_key, _) = KeyPair::generate().expect("Valid").into(); - let teller_signatory_set = vec![ - Value::PublicKey(public_key_teller_1.clone()), - Value::PublicKey(public_key_teller_2), - ]; - let one_teller_set = Value::Vec(vec![Value::PublicKey(public_key_teller_1)]); - let manager_signatory = Value::PublicKey(manager_public_key); - let manager_signatory_set = Value::Vec(vec![manager_signatory.clone()]); - let condition = If::new( - And::new( - Greater::new( - EvaluatesTo::new_unchecked(ContextValue::new( - Name::from_str("usd_quantity").expect("Can't fail."), - )), - 500_u32, - ), - Less::new( - EvaluatesTo::new_unchecked(ContextValue::new( - Name::from_str("usd_quantity").expect("Can't fail."), - )), - 1000_u32, - ), - ), - EvaluatesTo::new_evaluates_to_value(Or::new( - ContainsAll::new( - EvaluatesTo::new_unchecked(ContextValue::new( - Name::from_str("signatories").expect("Can't fail."), - )), - teller_signatory_set.clone(), - ), - Contains::new( - EvaluatesTo::new_unchecked(ContextValue::new( - Name::from_str("signatories").expect("Can't fail."), - )), - manager_signatory, - ), - )), - true, - ); - // Signed by all tellers - let expression = Where::new(EvaluatesTo::new_evaluates_to_value(condition.clone())) - .with_value( - //TODO: use query to get the actual quantity of an asset from WSV - // in that case this test should be moved to iroha_core - Name::from_str("usd_quantity").expect("Can't fail."), - asset_quantity_high.clone(), - ) - .with_value( - Name::from_str("signatories").expect("Can't fail."), - teller_signatory_set, - ); - assert_eq!(expression.evaluate(&TestContext::new())?, Value::Bool(true)); - // Signed by manager - let expression = Where::new(EvaluatesTo::new_evaluates_to_value(condition.clone())) - .with_value( - Name::from_str("usd_quantity").expect("Can't fail."), - asset_quantity_high.clone(), - ) - .with_value( - Name::from_str("signatories").expect("Can't fail."), - manager_signatory_set, - ); - assert_eq!(expression.evaluate(&TestContext::new())?, Value::Bool(true)); - // Signed by one teller - let expression = Where::new(EvaluatesTo::new_evaluates_to_value(condition.clone())) - .with_value( - Name::from_str("usd_quantity").expect("Can't fail."), - asset_quantity_high, - ) - .with_value( - Name::from_str("signatories").expect("Can't fail."), - one_teller_set.clone(), - ); - assert_eq!( - expression.evaluate(&TestContext::new())?, - Value::Bool(false) - ); - // Signed by one teller with less value - let expression = Where::new(EvaluatesTo::new_evaluates_to_value(condition)) - .with_value( - Name::from_str("usd_quantity").expect("Can't fail."), - asset_quantity_low, - ) - .with_value( - Name::from_str("signatories").expect("Can't fail."), - one_teller_set, - ); - assert_eq!(expression.evaluate(&TestContext::new())?, Value::Bool(true)); - Ok(()) - } - - #[test] - fn where_expression() -> Result<(), EvaluationError> { - assert_eq!( - Where::new(EvaluatesTo::new_unchecked(ContextValue::new( - Name::from_str("test_value").expect("Can't fail.") - ))) - .with_value( - Name::from_str("test_value").expect("Can't fail."), - EvaluatesTo::new_evaluates_to_value(Add::new(2_u32, 3_u32)) - ) - .evaluate(&TestContext::new())?, - 5_u32.to_value() - ); - Ok(()) - } - - #[test] - fn nested_where_expression() -> Result<(), EvaluationError> { - let expression = Where::new(EvaluatesTo::new_unchecked(ContextValue::new( - Name::from_str("a").expect("Can't fail."), - ))) - .with_value(Name::from_str("a").expect("Can't fail."), 2_u32); - let outer_expression = Where::new(EvaluatesTo::new_evaluates_to_value(Add::new( - EvaluatesTo::new_unchecked(expression), - EvaluatesTo::new_unchecked(ContextValue::new( - Name::from_str("b").expect("Can't fail."), - )), - ))) - .with_value(Name::from_str("b").expect("Can't fail."), 4_u32); - assert_eq!( - outer_expression.evaluate(&TestContext::new())?, - 6_u32.to_value() - ); - Ok(()) - } - - #[test] - fn if_condition_branches_correctly() -> Result<(), EvaluationError> { - assert_eq!( - If::new(true, 1_u32, 2_u32).evaluate(&TestContext::new())?, - 1_u32.to_value() - ); - assert_eq!( - If::new(false, 1_u32, 2_u32).evaluate(&TestContext::new())?, - 2_u32.to_value() - ); - Ok(()) - } - - #[test] - #[allow(clippy::unnecessary_wraps)] - fn incorrect_types_are_caught() -> Result<(), EvaluationError> { - fn assert_eval(inst: &I, err_msg: &str) - where - I: Evaluate + Debug, - I::Value: Debug, - { - let result: Result<_, _> = inst.evaluate(&TestContext::new()); - let _err = result.expect_err(err_msg); - } - - assert_eval( - &And::new( - EvaluatesTo::new_unchecked(1_u32), - EvaluatesTo::new_unchecked(Vec::::new()), - ), - "Should not be possible to apply logical and to int and vec.", - ); - assert_eval( - &Or::new( - EvaluatesTo::new_unchecked(1_u32), - EvaluatesTo::new_unchecked(Vec::::new()), - ), - "Should not be possible to apply logical or to int and vec.", - ); - assert_eval( - &Greater::new( - EvaluatesTo::new_unchecked(1_u32), - EvaluatesTo::new_unchecked(Vec::::new()), - ), - "Should not be possible to apply greater sign to int and vec.", - ); - assert_eval( - &Less::new( - EvaluatesTo::new_unchecked(1_u32), - EvaluatesTo::new_unchecked(Vec::::new()), - ), - "Should not be possible to apply greater sign to int and vec.", - ); - assert_eval( - &If::new(EvaluatesTo::new_unchecked(1_u32), 2_u32, 3_u32), - "If condition should be bool", - ); - assert_eval( - &Add::new(10_u32, 1_u128), - "Should not be possible to add `u32` and `u128`", - ); - assert_eval( - &Subtract::new(Fixed::try_from(10.2_f64).map_err(MathError::from)?, 1_u128), - "Should not be possible to subtract `Fixed` and `u128`", - ); - assert_eval( - &Multiply::new(0_u32, Fixed::try_from(1.0_f64).map_err(MathError::from)?), - "Should not be possible to multiply `u32` and `Fixed`", - ); - Ok(()) - } - - #[test] - fn operations_are_correctly_calculated() -> Result<(), EvaluationError> { - assert_eq!( - Add::new(1_u32, 2_u32).evaluate(&TestContext::new())?, - 3_u32.into() - ); - assert_eq!( - Add::new(1_u128, 2_u128).evaluate(&TestContext::new())?, - 3_u128.into(), - ); - assert_eq!( - Add::new( - Fixed::try_from(1.17_f64).map_err(MathError::from)?, - Fixed::try_from(2.13_f64).map_err(MathError::from)? - ) - .evaluate(&TestContext::new())?, - 3.30_f64.try_into().map_err(MathError::from)? - ); - assert_eq!( - Subtract::new(7_u32, 2_u32).evaluate(&TestContext::new())?, - 5_u32.into() - ); - assert_eq!( - Subtract::new(7_u128, 2_u128).evaluate(&TestContext::new())?, - 5_u128.into() - ); - assert_eq!( - Subtract::new( - Fixed::try_from(7.250_f64).map_err(MathError::from)?, - Fixed::try_from(2.125_f64).map_err(MathError::from)? - ) - .evaluate(&TestContext::new())?, - 5.125_f64.try_into().map_err(MathError::from)? - ); - assert!(!Greater::new(1_u32, 2_u32).evaluate(&TestContext::new())?); - assert!(Greater::new(2_u32, 1_u32).evaluate(&TestContext::new())?); - assert!(Less::new(1_u32, 2_u32).evaluate(&TestContext::new())?); - assert!(!Less::new(2_u32, 1_u32).evaluate(&TestContext::new())?); - assert!(!Equal::new(1_u32, 2_u32).evaluate(&TestContext::new())?); - assert!( - Equal::new(vec![1_u32, 3_u32, 5_u32], vec![1_u32, 3_u32, 5_u32]) - .evaluate(&TestContext::new())? - ); - assert!(Contains::new(val_vec![1_u32, 3_u32, 5_u32], 3_u32).evaluate(&TestContext::new())?); - assert!(!Contains::new(val_vec![1_u32, 3_u32, 5_u32], 7_u32).evaluate(&TestContext::new())?); - assert!( - ContainsAll::new(val_vec![1_u32, 3_u32, 5_u32], val_vec![1_u32, 5_u32]) - .evaluate(&TestContext::new())? - ); - assert!( - !ContainsAll::new(val_vec![1_u32, 3_u32, 5_u32], val_vec![1_u32, 5_u32, 7_u32]) - .evaluate(&TestContext::new())? - ); - Ok(()) - } - - #[test] - #[ignore = "Stack overflow"] - fn serde_serialization_works() { - let expression: Expression = Add::new(1_u32, Subtract::new(7_u32, 4_u32)).into(); - let serialized_expression = - serde_json::to_string(&expression).expect("Failed to serialize."); - let deserialized_expression: Expression = - serde_json::from_str(&serialized_expression).expect("Failed to de-serialize."); - assert_eq!( - expression - .evaluate(&TestContext::new()) - .expect("Failed to calculate."), - deserialized_expression - .evaluate(&TestContext::new()) - .expect("Failed to calculate.") - ) - } - - #[test] - fn scale_codec_serialization_works() { - let expression: Expression = Add::new(1_u32, Subtract::new(7_u32, 4_u32)).into(); - let serialized_expression: Vec = expression.encode(); - let deserialized_expression: Expression = - DecodeAll::decode_all(&mut serialized_expression.as_slice()) - .expect("Failed to decode."); - assert_eq!( - expression - .evaluate(&TestContext::new()) - .expect("Failed to calculate."), - deserialized_expression - .evaluate(&TestContext::new()) - .expect("Failed to calculate.") - ) - } -} diff --git a/data_model/src/events/data/events.rs b/data_model/src/events/data/events.rs index 8bf07a8e49b..90f1caf2cf8 100644 --- a/data_model/src/events/data/events.rs +++ b/data_model/src/events/data/events.rs @@ -566,6 +566,7 @@ mod executor { #[derive( Debug, + Copy, Clone, PartialEq, Eq, @@ -586,6 +587,23 @@ mod executor { } /// Filter for [`ExecutorEvent`]. + #[derive( + Debug, + Copy, + Clone, + PartialEq, + Eq, + PartialOrd, + Ord, + parity_scale_codec::Decode, + parity_scale_codec::Encode, + serde::Deserialize, + serde::Serialize, + iroha_schema::IntoSchema, + )] + #[non_exhaustive] + #[serde(untagged)] // Unaffected by #3330, as single unit variant + #[repr(transparent)] pub enum ExecutorFilter { Upgraded, } diff --git a/data_model/src/expression.rs b/data_model/src/expression.rs deleted file mode 100644 index b5e75ce26bc..00000000000 --- a/data_model/src/expression.rs +++ /dev/null @@ -1,703 +0,0 @@ -//! Expressions to use inside of ISIs. - -#![allow( - // Because of `codec(skip)` - clippy::default_trait_access, - // Because of length on instructions and expressions (can't be 0) - clippy::len_without_is_empty, - // Because of length on instructions and expressions (XXX: Should it be trait?) - clippy::unused_self -)] - -#[cfg(not(feature = "std"))] -use alloc::{boxed::Box, collections::btree_map, format, string::String, vec, vec::Vec}; -use core::marker::PhantomData; -#[cfg(feature = "std")] -use std::collections::btree_map; - -use derive_more::{Constructor, DebugCustom, Display}; -use getset::Getters; -use iroha_data_model_derive::{model, PartiallyTaggedDeserialize, PartiallyTaggedSerialize}; -use iroha_macro::FromVariant; -use iroha_schema::{IntoSchema, TypeId}; -use operation::*; -use parity_scale_codec::{Decode, Encode}; -use serde::{Deserialize, Serialize}; - -pub use self::model::*; -use super::{query::QueryBox, Name, Value}; -use crate::NumericValue; - -/// Generate expression structure and basic impls for it. -/// -/// # Syntax -/// -/// Basic syntax: -/// -/// ```ignore -/// gen_expr_and_impls! { -/// /// Comment -/// #[derive(Derives)] -/// pub Expr(param1: Type1, param2: Type2, param3: Type3, ...) -> OutputType -/// } -/// ``` -/// -/// The macro has three syntax forms to specify parameters: -/// - One unnamed parameter. In that case, the parameter name will be `expression`. -/// - Two unnamed parameters. -/// In that case, the parameter names will be `left` and `right` respectively. -/// - Any number of named parameters. -/// -/// The macro has two syntax forms to specify result: -/// - With the actual result type after the arrow (`->`). -/// In that case, `impl From<$i> for EvaluatesTo<$result_type>` will be generated. -/// - With `?` sign as a result type. -/// In that case `impl From<$i> for EvaluatesTo<$result_type>` **won't** be generated. -/// -/// See the example and further usage for more details. -/// -/// # Example -/// -/// ```ignore -/// gen_expr_and_impls! { -/// /// Evaluates to the sum of left and right expressions. -/// #[derive(Debug)] -/// pub Add(u32, u32) -> u32 -/// } -/// -/// // Will generate the following code: -/// -/// /// Evaluates to the sum of left and right expressions. -/// iroha_data_model_derive::model_single! { -/// #[derive(Debug)] -/// pub struct Add { -/// #[allow(missing_docs)] -/// pub left: EvaluatesTo, -/// #[allow(missing_docs)] -/// pub right: EvaluatesTo, -/// } -/// } -/// -/// impl Add { -/// /// Construct new [`Add`] expression -/// pub fn new(left: impl Into>, right: impl Into>) -> Self { -/// Self { -/// left: left.into(), -/// right: right.into(), -/// } -/// } -/// } -/// -/// impl From for EvaluatesTo { -/// fn from(expression: Add) -> Self { -/// EvaluatesTo::new_unchecked(expression) -/// } -/// } -/// ``` -macro_rules! gen_expr_and_impls { - // Case: one unnamed parameter - ($(#[$me:meta])* $v:vis $i:ident($first_type:ty $(,)?) -> $($result:tt)*) => { - gen_expr_and_impls!($(#[$me])* $v $i(expression: $first_type) -> $($result)*); - }; - // Case: two unnamed parameters - ($(#[$me:meta])* $v:vis $i:ident($first_type:ty, $second_type:ty $(,)?) -> $($result:tt)*) => { - gen_expr_and_impls!($(#[$me])* $v $i(left: $first_type, right: $second_type) -> $($result)*); - }; - // Case: any number of named parameters - ($(#[$me:meta])* $v:vis $i:ident($($param_name:ident: $param_type:ty),* $(,)?) -> $($result:tt)*) => { - gen_expr_and_impls!(impl_basic $(#[$me])* $v $i($($param_name: $param_type),*)); - gen_expr_and_impls!(impl_extra_convert $i $($result)*); - }; - // Internal usage: generate basic code for the expression - (impl_basic $(#[$me:meta])* $v:vis $i:ident($($param_name:ident: $param_type:ty),* $(,)?)) => { - iroha_data_model_derive::model_single! { - #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Getters, Decode, Encode, Deserialize, Serialize, IntoSchema)] - #[getset(get = "pub")] - $(#[$me])* - $v struct $i { $( - /// - #[allow(missing_docs)] - pub $param_name: EvaluatesTo<$param_type>, )* - } - } - - impl $i { - #[doc = concat!(" Construct new [`", stringify!($i), "`] expression")] - pub fn new( - $($param_name: impl Into>),* - ) -> Self { - Self { - $($param_name: $param_name.into()),* - } - } - } - }; - // Internal usage: do nothing for expressions with unknown result type - (impl_extra_convert $i:ident ?) => { - }; - // Internal usage: generate extra `From` impl for expressions with known result type - (impl_extra_convert $i:ident $result_type:ty) => { - impl From<$i> for EvaluatesTo<$result_type> { - fn from(expression: $i) -> Self { - EvaluatesTo::new_unchecked(expression) - } - } - }; -} - -#[model] -pub mod model { - use super::*; - - /// Struct for type checking and converting expression results. - #[derive( - DebugCustom, - Display, - Clone, - PartialEq, - Eq, - PartialOrd, - Ord, - Decode, - Encode, - Deserialize, - Serialize, - TypeId, - )] - // As this structure exists only for type checking - // it makes sense to display `expression` directly - #[display(fmt = "{expression}")] - #[debug(fmt = "{expression:?}")] - #[serde(transparent)] - #[repr(transparent)] - // SAFETY: `EvaluatesTo` has no trap representation in `Box` - #[ffi_type(unsafe {robust})] - pub struct EvaluatesTo { - /// Expression. - #[serde(flatten)] - pub expression: Box, - #[codec(skip)] - pub(super) _value_type: PhantomData, - } - - /// Represents all possible expressions. - #[derive( - DebugCustom, - Display, - Clone, - PartialEq, - Eq, - PartialOrd, - Ord, - FromVariant, - Decode, - Encode, - PartiallyTaggedDeserialize, - PartiallyTaggedSerialize, - IntoSchema, - )] - #[ffi_type(opaque)] - pub enum Expression { - /// Add expression. - Add(Add), - /// Subtract expression. - Subtract(Subtract), - /// Multiply expression. - Multiply(Multiply), - /// Divide expression. - Divide(Divide), - /// Module expression. - Mod(Mod), - /// Raise to power expression. - RaiseTo(RaiseTo), - /// Greater expression. - Greater(Greater), - /// Less expression. - Less(Less), - /// Equal expression. - Equal(Equal), - /// Not expression. - Not(Not), - /// And expression. - And(And), - /// Or expression. - Or(Or), - /// If expression. - If(If), - /// Raw value. - #[serde_partially_tagged(untagged)] - #[debug(fmt = "{_0:?}")] - Raw(#[skip_from] Value), - /// Query to Iroha state. - Query(QueryBox), - /// Contains expression for vectors. - Contains(Contains), - /// Contains all expression for vectors. - ContainsAll(ContainsAll), - /// Contains any expression for vectors. - ContainsAny(ContainsAny), - /// Where expression to supply temporary values to local context. - Where(Where), - /// Get a temporary value by name - ContextValue(ContextValue), - } - - /// Get a temporary value by name. The values are brought into [`Context`] by [`Where`] expression. - // NOTE: Can't use `gen_expr_and_impls!` here because we need special type for `value_name` - #[derive( - Debug, - Display, - Clone, - PartialEq, - Eq, - PartialOrd, - Ord, - Getters, - Constructor, - Decode, - Encode, - Deserialize, - Serialize, - IntoSchema, - )] - #[display(fmt = "CONTEXT `{value_name}`")] - #[getset(get = "pub")] - #[serde(transparent)] - #[repr(transparent)] - // SAFETY: `ContextValue` has no trap representation in `Name` - #[ffi_type(unsafe {robust})] - pub struct ContextValue { - /// Name bound to the value. - pub value_name: Name, - } - - gen_expr_and_impls! { - /// Evaluates to the multiplication of left and right expressions. - #[derive(Display)] - #[display(fmt = "{}*{}", // Keep without spaces - "self.left.parenthesise(Operation::Multiply)", - "self.right.parenthesise(Operation::Multiply)" - )] - #[ffi_type] - pub Multiply(NumericValue, NumericValue) -> NumericValue - } - - gen_expr_and_impls! { - /// Evaluates to the left expression divided by the right expression. - #[derive(Display)] - #[display(fmt = "{}/{}", // Keep without spaces - "self.left.parenthesise(Operation::Divide)", - "self.right.parenthesise(Operation::Divide)" - )] - #[ffi_type] - pub Divide(NumericValue, NumericValue) -> NumericValue - } - - gen_expr_and_impls! { - /// Evaluates to the left expression modulo the right expression. - #[derive(Display)] - #[display(fmt = "{} % {}", - "self.left.parenthesise(Operation::Mod)", - "self.right.parenthesise(Operation::Mod)" - )] - #[ffi_type] - pub Mod(NumericValue, NumericValue) -> NumericValue - } - - gen_expr_and_impls! { - /// Evaluates to the left expression in the power of right expression. - /// Currently does not support [`NumericValue::Fixed`]. - #[derive(Display)] - #[display(fmt = "{}**{}", - "self.left.parenthesise(Operation::RaiseTo)", - "self.right.parenthesise(Operation::RaiseTo)" - )] - #[ffi_type] - pub RaiseTo(NumericValue, NumericValue) -> NumericValue - } - - gen_expr_and_impls! { - /// Evaluates to the sum of left and right expressions. - #[derive(Display)] - #[display(fmt = "{}+{}", - "self.left.parenthesise(Operation::Add)", - "self.right.parenthesise(Operation::Add)" - )] - #[ffi_type] - pub Add(NumericValue, NumericValue) -> NumericValue - } - - gen_expr_and_impls! { - /// Evaluates to the left expression minus the right expression. - #[derive(Display)] - #[display(fmt = "{}-{}", - "self.left.parenthesise(Operation::Subtract)", - "self.right.parenthesise(Operation::Subtract)" - )] - #[ffi_type] - pub Subtract(NumericValue, NumericValue) -> NumericValue - } - - gen_expr_and_impls! { - /// Returns whether the `left` expression is greater than the `right`. - #[derive(Display)] - #[display(fmt = "{} > {}", - "self.left.parenthesise(Operation::Greater)", - "self.right.parenthesise(Operation::Greater)" - )] - #[ffi_type] - pub Greater(NumericValue, NumericValue) -> bool - } - - gen_expr_and_impls! { - /// Returns whether the `left` expression is less than the `right`. - #[derive(Display)] - #[display(fmt = "{} < {}", - "self.left.parenthesise(Operation::Less)", - "self.right.parenthesise(Operation::Less)" - )] - #[ffi_type] - pub Less(NumericValue, NumericValue) -> bool - } - - gen_expr_and_impls! { - /// Negates the result of the `expression`. - /// Works only for `Value::Bool`. - #[derive(Display)] - #[display(fmt = "!{}", "self.expression.parenthesise(Operation::Not)")] - #[serde(transparent)] - #[repr(transparent)] - // SAFETY: `Not` has no trap representation in `bool` - #[ffi_type(unsafe {robust})] - pub Not(bool) -> bool - } - - gen_expr_and_impls! { - /// Applies the logical `and` to two `Value::Bool` operands. - #[derive(Display)] - #[display(fmt = "{} && {}", - "self.left.parenthesise(Operation::And)", - "self.right.parenthesise(Operation::And)" - )] - #[ffi_type] - pub And(bool, bool) -> bool - } - - gen_expr_and_impls! { - /// Applies the logical `or` to two `Value::Bool` operands. - #[derive(Display)] - #[display(fmt = "{} || {}", - "self.left.parenthesise(Operation::Or)", - "self.right.parenthesise(Operation::Or)" - )] - #[ffi_type] - pub Or(bool, bool) -> bool - } - - gen_expr_and_impls! { - /// If expression. Based on the `condition`, returns the result of either `then` or `otherwise`. - #[derive(Display)] - #[display(fmt = "if {condition} {{ {then} }} else {{ {otherwise} }}")] - #[ffi_type] - pub If(condition: bool, then: Value, otherwise: Value) -> ? - } - - gen_expr_and_impls! { - /// `Contains` expression. - /// Returns `true` if `collection` contains an `element`, `false` otherwise. - #[derive(Display)] - #[display(fmt = "{}.contains({})", "collection.parenthesise(Operation::MethodCall)", "element")] - #[ffi_type] - pub Contains(collection: Vec, element: Value) -> bool - } - - gen_expr_and_impls! { - /// `ContainsAll` expression. - /// Returns `true` if `collection` contains all `elements`, `false` otherwise. - #[derive(Display)] - #[display(fmt = "{}.contains_all({})", "collection.parenthesise(Operation::MethodCall)", "elements")] - #[ffi_type] - pub ContainsAll(collection: Vec, elements: Vec) -> bool - } - - gen_expr_and_impls! { - /// `ContainsAny` expression. - /// Returns `true` if `collection` contains any element out of the `elements`, `false` otherwise. - #[derive(Display)] - #[display(fmt = "{}.contains_any({})", "collection.parenthesise(Operation::MethodCall)", "elements")] - #[ffi_type] - pub ContainsAny(collection: Vec, elements: Vec) -> bool - } - - gen_expr_and_impls! { - /// Returns `true` if `left` operand is equal to the `right` operand. - #[derive(Display)] - #[display(fmt = "{} == {}", - "self.left.parenthesise(Operation::Equal)", - "self.right.parenthesise(Operation::Equal)" - )] - #[ffi_type] - pub Equal(Value, Value) -> bool - } - - /// Adds a local context of `values` for the `expression`. - /// It is similar to **where** syntax in *Haskell* although evaluated eagerly. - // NOTE: Can't use `gen_expr_and_impls!` here because we need special type for `values` - #[derive( - Debug, - Clone, - PartialEq, - Eq, - PartialOrd, - Ord, - Getters, - Decode, - Encode, - Deserialize, - Serialize, - IntoSchema, - )] - #[ffi_type] - pub struct Where { - /// Expression to be evaluated. - #[getset(get = "pub")] - pub expression: EvaluatesTo, - /// Context values for the context bonded to their `String` names. - pub values: btree_map::BTreeMap>, - } -} - -impl> From for Expression { - fn from(value: V) -> Self { - Self::Raw(value.into()) - } -} - -impl, E: Into + Into> From for EvaluatesTo { - fn from(expression: E) -> Self { - Self::new_unchecked(expression) - } -} - -impl EvaluatesTo { - /// Expression - #[inline] - // NOTE: getset would return &Box - pub fn expression(&self) -> &Expression { - &self.expression - } - - /// Construct new [`EvaluatesTo`] from [`Expression`] without type checking. - /// - /// # Warning - /// Prefer using [`Into`] conversions rather than this method, - /// because it does not check the value type at compile-time. - #[inline] - pub fn new_unchecked(expression: impl Into) -> Self { - Self { - expression: Box::new(expression.into()), - _value_type: PhantomData, - } - } - - fn operation(&self) -> Operation { - use Expression::*; - - match self.expression.as_ref() { - Add(_) => Operation::Add, - Subtract(_) => Operation::Subtract, - Multiply(_) => Operation::Multiply, - Divide(_) => Operation::Divide, - Mod(_) => Operation::Mod, - RaiseTo(_) => Operation::RaiseTo, - Greater(_) => Operation::Greater, - Less(_) => Operation::Less, - Equal(_) => Operation::Equal, - Not(_) => Operation::Not, - And(_) => Operation::And, - Or(_) => Operation::Or, - Contains(_) | ContainsAll(_) | ContainsAny(_) => Operation::MethodCall, - If(_) | Raw(_) | Query(_) | Where(_) | ContextValue(_) => Operation::Other, - } - } - - /// Wrap expression into parentheses depending on `operation` and get the resulting string. - fn parenthesise(&self, operation: Operation) -> String { - if self.operation().priority() < operation.priority() - && !matches!(self.expression.as_ref(), Expression::Raw(_)) - { - format!("({})", self.expression) - } else { - format!("{}", self.expression) - } - } -} - -impl EvaluatesTo { - /// Construct `EvaluatesTo` from any `expression` - /// because all of them evaluate to [`Value`]. - #[inline] - pub fn new_evaluates_to_value(expression: impl Into) -> Self { - Self::new_unchecked(expression) - } -} - -impl + IntoSchema> IntoSchema for EvaluatesTo { - fn type_name() -> String { - format!("EvaluatesTo<{}>", V::type_name()) - } - fn update_schema_map(map: &mut iroha_schema::MetaMap) { - const EXPRESSION: &str = "expression"; - - if !map.contains_key::() { - map.insert::(iroha_schema::Metadata::Struct( - iroha_schema::NamedFieldsMeta { - declarations: vec![iroha_schema::Declaration { - name: String::from(EXPRESSION), - ty: core::any::TypeId::of::(), - }], - }, - )); - - Expression::update_schema_map(map); - } - } -} - -impl core::fmt::Display for Where { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - write!(f, "`{} where: [", self.expression)?; - - let mut first = true; - for (key, value) in &self.values { - if !first { - write!(f, ", ")?; - } - first = false; - write!(f, "`{key}` : `{value}`")?; - } - - write!(f, "]") - } -} - -impl Where { - /// Construct [`Where`] expression - #[must_use] - pub fn new(expression: impl Into>) -> Self { - Self { - expression: expression.into(), - values: Default::default(), - } - } - - /// Get an iterator over the values of [`Where`] clause - #[inline] - pub fn values(&self) -> impl ExactSizeIterator)> { - self.values.iter() - } - - /// Binds `expression` result to a `value_name`, by which it will be reachable from the main expression. - #[must_use] - pub fn with_value>>( - mut self, - value_name: Name, - expression: E, - ) -> Self { - self.values.insert(value_name, expression.into()); - self - } -} - -mod operation { - //! Module containing operations and their priorities. - - /// Type of expression operation. - #[derive(Clone, Copy, PartialEq, Eq)] - pub enum Operation { - MethodCall, - RaiseTo, - Multiply, - Divide, - Mod, - Add, - Subtract, - Greater, - Less, - Equal, - Not, - And, - Or, - Other, - } - - /// Priority of operation. - /// - /// [`First`](Operation::First) is the highest priority - /// and [`Ninth`](Operation::Ninth) is the lowest. - #[derive(Debug, Clone, Copy, PartialEq, Eq)] - pub enum Priority { - First = 1, - Second = 2, - Third = 3, - Fourth = 4, - Fifth = 5, - Sixth = 6, - Seventh = 7, - Eighth = 8, - Ninth = 9, - } - - impl Operation { - /// Get the priority of the operation. - /// - /// Ordering is the same as in Python code. - /// See [`here`](https://docs.python.org/3/reference/expressions.html#operator-precedence) - /// for more details. - pub fn priority(self) -> Priority { - use Operation::*; - - match self { - MethodCall => Priority::First, - RaiseTo => Priority::Second, - Multiply | Divide | Mod => Priority::Third, - Add | Subtract => Priority::Fourth, - Greater | Less | Equal => Priority::Fifth, - Not => Priority::Sixth, - And => Priority::Seventh, - Or => Priority::Eighth, - Other => Priority::Ninth, - } - } - } - - impl PartialOrd for Priority { - #[inline] - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } - } - - impl Ord for Priority { - fn cmp(&self, other: &Self) -> core::cmp::Ordering { - use core::cmp::Ordering::*; - - let lhs = *self as u8; - let rhs = *other as u8; - - match lhs.cmp(&rhs) { - Less => Greater, - Equal => Equal, - Greater => Less, - } - } - } -} - -/// The prelude re-exports most commonly used traits, structs and macros from this crate. -pub mod prelude { - pub use super::{ - Add, And, Contains, ContainsAll, ContainsAny, ContextValue, Divide, Equal, EvaluatesTo, - Expression, Greater, If, Less, Mod, Multiply, Not, Or, RaiseTo, Subtract, Where, - }; -} diff --git a/data_model/src/isi.rs b/data_model/src/isi.rs index c0b2fd41da1..0dd3b178d9e 100644 --- a/data_model/src/isi.rs +++ b/data_model/src/isi.rs @@ -4,36 +4,25 @@ #[cfg(not(feature = "std"))] use alloc::{boxed::Box, format, string::String, vec::Vec}; -use core::fmt::Debug; +use core::fmt::{Debug, Display}; -use derive_more::{DebugCustom, Display}; +use derive_more::{Constructor, DebugCustom, Display}; use iroha_data_model_derive::model; -use iroha_macro::FromVariant; use iroha_schema::IntoSchema; use parity_scale_codec::{Decode, Encode}; use serde::{Deserialize, Serialize}; use strum::EnumDiscriminants; -pub use self::model::*; -use super::{expression::EvaluatesTo, prelude::*, IdBox, RegistrableBox, Value}; +pub use self::{model::*, transparent::*}; +use super::{prelude::*, Value}; use crate::{seal, Level, Registered}; -/// Marker trait designating instruction -pub trait Instruction: Into + seal::Sealed {} - -macro_rules! isi { - ($($meta:meta)* $item:item) => { - iroha_data_model_derive::model_single! { - #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, getset::Getters)] - #[derive(parity_scale_codec::Decode, parity_scale_codec::Encode)] - #[derive(serde::Deserialize, serde::Serialize)] - #[derive(iroha_schema::IntoSchema)] - #[getset(get = "pub")] - $($meta)* - $item - } - }; -} +/// Marker trait designating instruction. +/// +/// Instructions allows to change the state of `Iroha`. +/// All possible instructions are implementors of this trait, excluding +/// [`InstructionBox`] which is just a wrapper. +pub trait Instruction: Into + seal::Sealed {} #[model] pub mod model { @@ -42,6 +31,10 @@ pub mod model { use super::*; /// Sized structure for all possible Instructions. + /// + /// Note that [`InstructionBox`] is not a self-sufficient instruction, + /// but just a wrapper to pass instructions back and forth. + /// If you are a client SDK user then you likely don't need to use this type directly. #[derive( DebugCustom, Display, @@ -50,7 +43,6 @@ pub mod model { Eq, PartialOrd, Ord, - FromVariant, EnumDiscriminants, Decode, Encode, @@ -79,790 +71,1151 @@ pub mod model { )] #[ffi_type(opaque)] #[allow(missing_docs)] - pub enum InstructionExpr { - #[debug(fmt = "{_0:?}")] - Register(RegisterExpr), + pub enum InstructionBox { #[debug(fmt = "{_0:?}")] - Unregister(UnregisterExpr), + Register(RegisterBox), #[debug(fmt = "{_0:?}")] - Mint(MintExpr), + Unregister(UnregisterBox), #[debug(fmt = "{_0:?}")] - Burn(BurnExpr), + Mint(MintBox), #[debug(fmt = "{_0:?}")] - Transfer(TransferExpr), + Burn(BurnBox), #[debug(fmt = "{_0:?}")] - If(Box), + Transfer(TransferBox), #[debug(fmt = "{_0:?}")] - Pair(Box), + SetKeyValue(SetKeyValueBox), #[debug(fmt = "{_0:?}")] - Sequence(SequenceExpr), + RemoveKeyValue(RemoveKeyValueBox), #[debug(fmt = "{_0:?}")] - SetKeyValue(SetKeyValueExpr), + Grant(GrantBox), #[debug(fmt = "{_0:?}")] - RemoveKeyValue(RemoveKeyValueExpr), + Revoke(RevokeBox), #[debug(fmt = "{_0:?}")] - Grant(GrantExpr), + ExecuteTrigger(ExecuteTrigger), #[debug(fmt = "{_0:?}")] - Revoke(RevokeExpr), + SetParameter(SetParameter), #[debug(fmt = "{_0:?}")] - ExecuteTrigger(ExecuteTriggerExpr), + NewParameter(NewParameter), #[debug(fmt = "{_0:?}")] - SetParameter(SetParameterExpr), + Upgrade(Upgrade), #[debug(fmt = "{_0:?}")] - NewParameter(NewParameterExpr), - Upgrade(UpgradeExpr), - /// `Log` variant. - Log(LogExpr), + Log(Log), #[debug(fmt = "{_0:?}")] Fail(Fail), } - impl Instruction for InstructionExpr {} - - impl Instruction for SetKeyValueExpr {} - impl Instruction for RemoveKeyValueExpr {} - impl Instruction for RegisterExpr {} - impl Instruction for UnregisterExpr {} - impl Instruction for MintExpr {} - impl Instruction for BurnExpr {} - impl Instruction for TransferExpr {} - impl Instruction for GrantExpr {} - impl Instruction for RevokeExpr {} - impl Instruction for SetParameterExpr {} - impl Instruction for NewParameterExpr {} - impl Instruction for UpgradeExpr {} - impl Instruction for ExecuteTriggerExpr {} - impl Instruction for LogExpr {} + impl Instruction for InstructionBox {} + + impl Instruction for SetKeyValue {} + impl Instruction for SetKeyValue {} + impl Instruction for SetKeyValue {} + impl Instruction for SetKeyValue {} + + impl Instruction for RemoveKeyValue {} + impl Instruction for RemoveKeyValue {} + impl Instruction for RemoveKeyValue {} + impl Instruction for RemoveKeyValue {} + + impl Instruction for Register {} + impl Instruction for Register {} + impl Instruction for Register {} + impl Instruction for Register {} + impl Instruction for Register {} + impl Instruction for Register {} + impl Instruction for Register> {} + + impl Instruction for Unregister {} + impl Instruction for Unregister {} + impl Instruction for Unregister {} + impl Instruction for Unregister {} + impl Instruction for Unregister {} + impl Instruction for Unregister {} + impl Instruction for Unregister> {} + + impl Instruction for Mint {} + impl Instruction for Mint {} + impl Instruction for Mint {} + impl Instruction for Mint {} + impl Instruction for Mint {} + impl Instruction for Mint> {} + + impl Instruction for Burn {} + impl Instruction for Burn {} + impl Instruction for Burn {} + impl Instruction for Burn {} + impl Instruction for Burn> {} + + impl Instruction for Transfer {} + impl Instruction for Transfer {} + impl Instruction for Transfer {} + impl Instruction for Transfer {} + impl Instruction for Transfer {} + + impl Instruction for Grant {} + impl Instruction for Grant {} + + impl Instruction for Revoke {} + impl Instruction for Revoke {} + + impl Instruction for SetParameter {} + impl Instruction for NewParameter {} + impl Instruction for Upgrade {} + impl Instruction for ExecuteTrigger {} + impl Instruction for Log {} impl Instruction for Fail {} - - // Composite instructions - impl Instruction for ConditionalExpr {} - impl Instruction for SequenceExpr {} - impl Instruction for PairExpr {} } mod transparent { - // NOTE: instructions in this module don't have to be made opaque with `model!` - // because they are never shared between client and server(http)/host(wasm) - use super::*; - use crate::executor::Executor; - - /// Generic instruction to set key value at the object. - #[derive(Debug, Clone)] - pub struct SetKeyValue { - /// Where to set key value. - pub object_id: O::Id, - /// Key. - pub key: Name, - /// Value. - pub value: Value, - } - - /// Generic instruction to remove key value at the object. - #[derive(Debug, Clone)] - pub struct RemoveKeyValue { - /// From where to remove key value. - pub object_id: O::Id, - /// Key of the pair to remove. - pub key: Name, - } - - /// Generic instruction for a registration of an object to the identifiable destination. - #[derive(Debug, Clone)] - pub struct Register { - /// The object that should be registered, should be uniquely identifiable by its id. - pub object: O::With, + use crate::{account::NewAccount, domain::NewDomain}; + + macro_rules! isi { + ($($meta:meta)* $item:item) => { + iroha_data_model_derive::model_single! { + #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] + #[derive(getset::Getters)] + #[derive(parity_scale_codec::Decode, parity_scale_codec::Encode)] + #[derive(serde::Deserialize, serde::Serialize)] + #[derive(iroha_schema::IntoSchema)] + #[getset(get = "pub")] + $($meta)* + $item + } + }; + } + + macro_rules! impl_display { + ( + $ty:ident $(< $($generic:tt),+ >)? + $(where + $( $lt:path $( : $clt:tt $(< $inner_generic:tt >)? $(+ $dlt:tt )* )? ),+ $(,)?)? + => $fmt:literal, $($args:ident),* $(,)? + ) => { + impl $(< $($generic),+ >)? ::core::fmt::Display for $ty $(< $($generic),+ >)? + $(where + $( $lt $( : $clt $(< $inner_generic >)? $(+ $dlt )* )? ),+)? + { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + write!( + f, + $fmt, + $(self.$args),* + ) + } + } + } } - /// Generic instruction for an unregistration of an object from the identifiable destination. - #[derive(Debug, Clone)] - pub struct Unregister { - /// [`Identifiable::Id`] of the object which should be unregistered. - pub object_id: O::Id, + macro_rules! impl_into_box { + ( + $($isi:ident $(< $($generic:ident $(< $nested_generic:ident >)?),+ >)?)|* + ==> $boxed:ident :: $variant:ident + ) => {$( + impl From<$isi $(< $($generic $(< $nested_generic >)?),+ >)? > for $boxed { + fn from(instruction: $isi $(< $($generic $(< $nested_generic >)?),+ >)?) -> Self { + Self::$variant(instruction) + } + } + )*}; + ( + $($isi:ident $(< $($generic:ident $(< $nested_generic:ident >)?),+ >)?)|* + => $middle:ident ==> $boxed:ident :: $variant:ident + ) => {$( + impl From<$isi $(< $($generic $(< $nested_generic >)?),+ >)? > for $boxed { + fn from(instruction: $isi $(< $($generic $(< $nested_generic >)?),+ >)?) -> Self { + Self::$variant($middle::from(instruction)) + } + } + )*}; + } + + isi! { + /// Generic instruction for setting a chain-wide config parameter. + #[derive(Constructor, Display)] + #[display(fmt = "SET `{parameter}`")] + #[serde(transparent)] + #[repr(transparent)] + pub struct SetParameter { + /// The configuration parameter being changed. + #[serde(flatten)] + pub parameter: Parameter, + } } - /// Generic instruction for a mint of an object to the identifiable destination. - #[derive(Debug, Clone)] - pub struct Mint, D: Identifiable> { - /// Object which should be minted. - pub object: O, - /// Destination object [`Identifiable::Id`]. - pub destination_id: D::Id, + impl_into_box!(SetParameter ==> InstructionBox::SetParameter); + + isi! { + /// Sized structure for all possible on-chain configuration parameters when they are first created. + /// Generic instruction for setting a chain-wide config parameter. + #[derive(Constructor, Display)] + #[display(fmt = "SET `{parameter}`")] + #[serde(transparent)] + #[repr(transparent)] + pub struct NewParameter { + /// Parameter to be changed. + #[serde(flatten)] + pub parameter: Parameter, + } } - /// Generic instruction for a burn of an object to the identifiable destination. - #[derive(Debug, Clone)] - pub struct Burn, D: Identifiable> { - /// Object which should be burned. - pub object: O, - /// Destination object [`Identifiable::Id`]. - pub destination_id: D::Id, + impl_into_box!(NewParameter ==> InstructionBox::NewParameter); + + isi! { + /// Generic instruction to set key value at the object. + #[schema(bounds = "O: Identifiable, O::Id: IntoSchema")] + pub struct SetKeyValue { + /// Where to set key value. + #[serde(flatten)] + pub object_id: O::Id, + /// Key. + pub key: Name, + /// Value. + pub value: Value, + } } - /// Generic instruction for a transfer of an object from the identifiable source to the identifiable destination. - #[derive(Debug, Clone)] - pub struct Transfer, D: Identifiable> { - /// Source object `Id`. - pub source_id: S::Id, - /// Object which should be transferred. - pub object: O, - /// Destination object `Id`. - pub destination_id: D::Id, + impl SetKeyValue { + /// Constructs a new [`SetKeyValue`] for a [`Domain`] with the given `key` and `value`. + pub fn domain(domain_id: DomainId, key: Name, value: impl Into) -> Self { + Self { + object_id: domain_id, + key, + value: value.into(), + } + } } - /// Generic instruction for granting permission to an entity. - #[derive(Debug, Clone)] - pub struct Grant> { - /// Object to grant. - pub object: O, - /// Entity to which to grant this token. - pub destination_id: AccountId, + impl SetKeyValue { + /// Constructs a new [`SetKeyValue`] for an [`Account`] with the given `key` and `value`. + pub fn account(account_id: AccountId, key: Name, value: impl Into) -> Self { + Self { + object_id: account_id, + key, + value: value.into(), + } + } } - /// Generic instruction for revoking permission from an entity. - #[derive(Debug, Clone)] - pub struct Revoke> { - /// Object to revoke. - pub object: O, - /// Entity which is being revoked this token from. - pub destination_id: AccountId, + impl SetKeyValue { + /// Constructs a new [`SetKeyValue`] for an [`AssetDefinition`] with the given `key` and `value`. + pub fn asset_definition( + asset_definition_id: AssetDefinitionId, + key: Name, + value: impl Into, + ) -> Self { + Self { + object_id: asset_definition_id, + key, + value: value.into(), + } + } } - /// Generic instruction for setting a chain-wide config parameter. - #[derive(Debug, Clone)] - pub struct SetParameter { - /// Parameter to be changed. - pub parameter: Parameter, + impl SetKeyValue { + /// Constructs a new [`SetKeyValue`] for an [`Asset`] with the given `key` and `value`. + pub fn asset(asset_id: AssetId, key: Name, value: impl Into) -> Self { + Self { + object_id: asset_id, + key, + value: value.into(), + } + } } - /// Generic instruction for setting a chain-wide config parameter. - #[derive(Debug, Clone)] - pub struct NewParameter { - /// Parameter to be changed. - pub parameter: Parameter, + impl_display! { + SetKeyValue + where + O: Identifiable, + O::Id: Display, + => + "SET `{}` = `{}` IN `{}`", + key, value, object_id, + } + + impl_into_box! { + SetKeyValue | + SetKeyValue | + SetKeyValue | + SetKeyValue => SetKeyValueBox ==> InstructionBox::SetKeyValue + } + + isi! { + /// Generic instruction to remove key value at the object. + #[schema(bounds = "O: Identifiable, O::Id: IntoSchema")] + pub struct RemoveKeyValue { + /// From where to remove key value. + #[serde(flatten)] + pub object_id: O::Id, + /// Key of the pair to remove. + pub key: Name, + } } - /// Generic instruction for upgrading runtime objects. - #[derive(Debug, Clone)] - pub struct Upgrade> { - /// Object to upgrade. - pub object: O, + impl RemoveKeyValue { + /// Constructs a new [`RemoveKeyValue`] for a [`Domain`] with the given `key`. + pub fn domain(domain_id: DomainId, key: Name) -> Self { + Self { + object_id: domain_id, + key, + } + } + } + + impl RemoveKeyValue { + /// Constructs a new [`RemoveKeyValue`] for an [`Account`] with the given `key`. + pub fn account(account_id: AccountId, key: Name) -> Self { + Self { + object_id: account_id, + key, + } + } + } + + impl RemoveKeyValue { + /// Constructs a new [`RemoveKeyValue`] for an [`AssetDefinition`] with the given `key`. + pub fn asset_definition(asset_definition_id: AssetDefinitionId, key: Name) -> Self { + Self { + object_id: asset_definition_id, + key, + } + } + } + + impl RemoveKeyValue { + /// Constructs a new [`RemoveKeyValue`] for an [`Asset`] with the given `key`. + pub fn asset(asset_id: AssetId, key: Name) -> Self { + Self { + object_id: asset_id, + key, + } + } + } + + impl_display! { + RemoveKeyValue + where + O: Identifiable, + O::Id: Display, + => + "REMOVE `{}` from `{}`", + key, object_id, + } + + impl_into_box! { + RemoveKeyValue | + RemoveKeyValue | + RemoveKeyValue | + RemoveKeyValue => RemoveKeyValueBox ==> InstructionBox::RemoveKeyValue + } + + isi! { + /// Generic instruction for a registration of an object to the identifiable destination. + #[schema(bounds = "O: Registered, O::With: IntoSchema")] + #[serde(transparent)] + pub struct Register { + /// The object that should be registered, should be uniquely identifiable by its id. + pub object: O::With, + } } - /// Generic instruction for executing specified trigger - #[derive(Debug, Clone)] - pub struct ExecuteTrigger { - /// Id of a trigger to execute - pub trigger_id: TriggerId, + impl Register { + /// Constructs a new [`Register`] for a [`Peer`]. + pub fn peer(new_peer: Peer) -> Self { + Self { object: new_peer } + } } - /// Generic instruction for logging messages - #[derive(Debug, Clone)] - pub struct Log { - /// Log level of the message - pub level: Level, - /// Message to be logged - pub msg: String, + impl Register { + /// Constructs a new [`Register`] for a [`Domain`]. + pub fn domain(new_domain: NewDomain) -> Self { + Self { object: new_domain } + } } - impl From> for SetKeyValueExpr { - fn from(source: SetKeyValue) -> Self { - Self::new(source.object_id.into(), source.key, source.value) + impl Register { + /// Constructs a new [`Register`] for an [`Account`]. + pub fn account(new_account: NewAccount) -> Self { + Self { + object: new_account, + } } } - impl From> for RemoveKeyValueExpr { - fn from(source: RemoveKeyValue) -> Self { - Self::new(source.object_id.into(), source.key) + impl Register { + /// Constructs a new [`Register`] for an [`AssetDefinition`]. + pub fn asset_definition(new_asset_definition: NewAssetDefinition) -> Self { + Self { + object: new_asset_definition, + } } } - impl From> for RegisterExpr { - fn from(source: Register) -> Self { - Self::new(source.object.into()) + impl Register { + /// Constructs a new [`Register`] for an [`Asset`]. + pub fn asset(new_asset: Asset) -> Self { + Self { object: new_asset } } } - impl From> for UnregisterExpr { - fn from(source: Unregister) -> Self { - Self::new(source.object_id.into()) + impl Register { + /// Constructs a new [`Register`] for a [`Role`]. + pub fn role(new_role: NewRole) -> Self { + Self { object: new_role } } } - impl, D: Identifiable> From> for MintExpr { - fn from(source: Mint) -> Self { - Self::new(source.object, source.destination_id.into()) + impl Register> { + /// Constructs a new [`Register`] for a [`Trigger`]. + pub fn trigger(new_trigger: Trigger) -> Self { + Self { + object: new_trigger, + } } } - impl, D: Identifiable> From> for BurnExpr { - fn from(source: Burn) -> Self { - Self::new(source.object, source.destination_id.into()) + impl_display! { + Register + where + O: Registered, + O::With: Display, + => + "REGISTER `{}`", + object, + } + + impl_into_box! { + Register | + Register | + Register | + Register | + Register | + Register | + Register > => RegisterBox ==> InstructionBox::Register + } + + isi! { + /// Generic instruction for an unregistration of an object from the identifiable destination. + #[schema(bounds = "O: Identifiable, O::Id: IntoSchema")] + pub struct Unregister { + /// [`Identifiable::Id`] of the object which should be unregistered. + pub object_id: O::Id, } } - impl, D: Identifiable> From> for TransferExpr { - fn from(source: Transfer) -> Self { - Self::new( - source.source_id.into(), - source.object, - source.destination_id.into(), - ) + impl_display! { + Unregister + where + O: Identifiable, + O::Id: Display, + => + "UNREGISTER `{}`", + object_id, + } + + impl_into_box! { + Unregister | + Unregister | + Unregister | + Unregister | + Unregister | + Unregister | + Unregister > => UnregisterBox ==> InstructionBox::Unregister + } + + impl Unregister { + /// Constructs a new [`Unregister`] for a [`Peer`]. + pub fn peer(peer_id: PeerId) -> Self { + Self { object_id: peer_id } } } - impl> From> for GrantExpr { - fn from(source: Grant) -> Self { - Self::new(source.object, source.destination_id) + impl Unregister { + /// Constructs a new [`Unregister`] for a [`Domain`]. + pub fn domain(domain_id: DomainId) -> Self { + Self { + object_id: domain_id, + } } } - impl> From> for RevokeExpr { - fn from(source: Revoke) -> Self { - Self::new(source.object, source.destination_id) + impl Unregister { + /// Constructs a new [`Unregister`] for an [`Account`]. + pub fn account(account_id: AccountId) -> Self { + Self { + object_id: account_id, + } } } - impl From for SetParameterExpr { - fn from(source: SetParameter) -> Self { - Self::new(source.parameter) + impl Unregister { + /// Constructs a new [`Unregister`] for an [`AssetDefinition`]. + pub fn asset_definition(asset_definition_id: AssetDefinitionId) -> Self { + Self { + object_id: asset_definition_id, + } } } - impl From for NewParameterExpr { - fn from(source: NewParameter) -> Self { - Self::new(source.parameter) + impl Unregister { + /// Constructs a new [`Unregister`] for an [`Asset`]. + pub fn asset(asset_id: AssetId) -> Self { + Self { + object_id: asset_id, + } } } - impl From> for UpgradeExpr { - fn from(source: Upgrade) -> Self { - Self::new(source.object) + impl Unregister { + /// Constructs a new [`Unregister`] for a [`Role`]. + pub fn role(role_id: RoleId) -> Self { + Self { object_id: role_id } } } - impl From for ExecuteTriggerExpr { - fn from(source: ExecuteTrigger) -> Self { - Self::new(source.trigger_id) + impl Unregister> { + /// Constructs a new [`Unregister`] for a [`Trigger`]. + pub fn trigger(trigger_id: TriggerId) -> Self { + Self { + object_id: trigger_id, + } } } - impl From for LogExpr { - fn from(source: Log) -> Self { - Self::new(source.level, source.msg) + isi! { + /// Generic instruction for a mint of an object to the identifiable destination. + #[schema(bounds = "O: Into + IntoSchema, D: Identifiable, D::Id: IntoSchema")] + pub struct Mint, D: Identifiable> { + /// Object which should be minted. + pub object: O, + /// Destination object [`Identifiable::Id`]. + pub destination_id: D::Id, } } -} -isi! { - /// Sized structure for all possible on-chain configuration parameters. - #[derive(Display)] - #[display(fmt = "SET `{parameter}`")] - #[serde(transparent)] - #[repr(transparent)] - // SAFETY: `SetParameterExpr` has no trap representation in `EvaluatesTo` - #[ffi_type(unsafe {robust})] - pub struct SetParameterExpr { - /// The configuration parameter being changed. - #[serde(flatten)] - pub parameter: EvaluatesTo, + impl Mint { + /// Constructs a new [`Mint`] for a [`PublicKey`] for [`Account`]. + pub fn account_public_key(public_key: PublicKey, account_id: AccountId) -> Self { + Self { + object: public_key, + destination_id: account_id, + } + } } -} -isi! { - /// Sized structure for all possible on-chain configuration parameters when they are first created. - #[derive(Display)] - #[display(fmt = "SET `{parameter}`")] - #[serde(transparent)] - #[repr(transparent)] - // SAFETY: `NewParameterExpr` has no trap representation in `EvaluatesTo` - #[ffi_type(unsafe {robust})] - pub struct NewParameterExpr { - /// The configuration parameter being created. - #[serde(flatten)] - pub parameter: EvaluatesTo, + impl Mint { + /// Constructs a new [`Mint`] for a [`SignatureCheckCondition`] for [`Account`]. + pub fn account_signature_check_condition( + signature_check_condition: SignatureCheckCondition, + account_id: AccountId, + ) -> Self { + Self { + object: signature_check_condition, + destination_id: account_id, + } + } } -} -isi! { - /// Sized structure for all possible key value set instructions. - #[derive(Display)] - #[display(fmt = "SET `{key}` = `{value}` IN `{object_id}`")] - #[ffi_type] - pub struct SetKeyValueExpr { - /// Where to set this key value. - #[serde(flatten)] - pub object_id: EvaluatesTo, - /// Key string. - pub key: EvaluatesTo, - /// Object to set as a value. - pub value: EvaluatesTo, + impl Mint { + /// Constructs a new [`Mint`] for an [`Asset`] of [`Quantity`] type. + pub fn asset_quantity(quantity: u32, asset_id: AssetId) -> Self { + Self { + object: quantity, + destination_id: asset_id, + } + } } -} -isi! { - /// Sized structure for all possible key value pair remove instructions. - #[derive(Display)] - #[display(fmt = "REMOVE `{key}` from `{object_id}`")] - #[ffi_type] - pub struct RemoveKeyValueExpr { - /// From where to remove this key value. - #[serde(flatten)] - pub object_id: EvaluatesTo, - /// Key string. - pub key: EvaluatesTo, + impl Mint { + /// Constructs a new [`Mint`] for an [`Asset`] of [`BigQuantity`] type. + pub fn asset_big_quantity(big_quantity: u128, asset_id: AssetId) -> Self { + Self { + object: big_quantity, + destination_id: asset_id, + } + } } -} -isi! { - /// Sized structure for all possible Registers. - #[derive(Display)] - #[display(fmt = "REGISTER `{object}`")] - #[serde(transparent)] - #[repr(transparent)] - // SAFETY: `RegisterExpr` has no trap representation in `EvaluatesTo` - #[ffi_type(unsafe {robust})] - pub struct RegisterExpr { - /// The object that should be registered, should be uniquely identifiable by its id. - pub object: EvaluatesTo, + impl Mint { + /// Constructs a new [`Mint`] for an [`Asset`] of [`Fixed`] type. + pub fn asset_fixed(fixed: Fixed, asset_id: AssetId) -> Self { + Self { + object: fixed, + destination_id: asset_id, + } + } } -} -isi! { - /// Sized structure for all possible Unregisters. - #[derive(Display)] - #[display(fmt = "UNREGISTER `{object_id}`")] - #[serde(transparent)] - #[repr(transparent)] - // SAFETY: `UnregisterExpr` has no trap representation in `EvaluatesTo` - #[ffi_type(unsafe {robust})] - pub struct UnregisterExpr { - /// The id of the object that should be unregistered. - pub object_id: EvaluatesTo, + impl Mint> { + /// Constructs a new [`Mint`] for repetition count of [`Trigger`]. + pub fn trigger_repetitions(repetitions: u32, trigger_id: TriggerId) -> Self { + Self { + object: repetitions, + destination_id: trigger_id, + } + } } -} -isi! { - /// Sized structure for all possible Mints. - #[derive(Display)] - #[display(fmt = "MINT `{object}` TO `{destination_id}`")] - #[ffi_type] - pub struct MintExpr { - /// Object to mint. - pub object: EvaluatesTo, - /// Entity to mint to. - pub destination_id: EvaluatesTo, + impl_display! { + Mint + where + O: Into + Display, + D: Identifiable, + D::Id: Display, + => + "MINT `{}` TO `{}`", + object, + destination_id, } -} -isi! { - /// Sized structure for all possible Burns. - #[derive(Display)] - #[display(fmt = "BURN `{object}` FROM `{destination_id}`")] - #[ffi_type] - pub struct BurnExpr { - /// Object to burn. - pub object: EvaluatesTo, - /// Entity to burn from. - pub destination_id: EvaluatesTo, + impl_into_box! { + Mint | + Mint => AccountMintBox ==> MintBox::Account } -} -isi! { - /// Sized structure for all possible Transfers. - #[derive(Display)] - #[display(fmt = "TRANSFER `{object}` FROM `{source_id}` TO `{destination_id}`")] - #[ffi_type] - pub struct TransferExpr { - /// Entity to transfer from. - pub source_id: EvaluatesTo, - /// Object to transfer. - pub object: EvaluatesTo, - /// Entity to transfer to. - pub destination_id: EvaluatesTo, + impl_into_box! { + Mint | + Mint | + Mint => AssetMintBox ==> MintBox::Asset } -} -isi! { - /// Composite instruction for a pair of instructions. - #[derive(Display)] - #[display(fmt = "(`{left_instruction}`, `{right_instruction}`)")] - #[ffi_type] - pub struct PairExpr { - /// Left instruction - pub left_instruction: InstructionExpr, - /// Right instruction - pub right_instruction: InstructionExpr, + impl_into_box! { + Mint | + Mint | + Mint | + Mint | + Mint | + Mint > => MintBox ==> InstructionBox::Mint } -} -isi! { - /// Composite instruction for a sequence of instructions. - #[serde(transparent)] - #[repr(transparent)] - // SAFETY: `SequenceExpr` has no trap representation in `Vec` - #[ffi_type(unsafe {robust})] - pub struct SequenceExpr { - /// Sequence of Iroha Special Instructions to execute. - pub instructions: Vec, + isi! { + /// Generic instruction for a burn of an object to the identifiable destination. + #[schema(bounds = "O: Into + IntoSchema, D: Identifiable, D::Id: IntoSchema")] + pub struct Burn, D: Identifiable> { + /// Object which should be burned. + pub object: O, + /// Destination object [`Identifiable::Id`]. + pub destination_id: D::Id, + } } -} -impl core::fmt::Display for SequenceExpr { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - write!(f, "SEQUENCE [")?; - let mut first = true; - for instruction in &self.instructions { - if !first { - write!(f, ", ")?; + impl Burn { + /// Constructs a new [`Burn`] for a [`PublicKey`] for [`Account`]. + pub fn account_public_key(public_key: PublicKey, account_id: AccountId) -> Self { + Self { + object: public_key, + destination_id: account_id, } - first = false; + } + } - write!(f, "`{instruction}`")?; + impl Burn { + /// Constructs a new [`Burn`] for an [`Asset`] of [`Quantity`] type. + pub fn asset_quantity(quantity: u32, asset_id: AssetId) -> Self { + Self { + object: quantity, + destination_id: asset_id, + } } - write!(f, "]") } -} -isi! { - /// Composite instruction for a conditional execution of other instructions. - #[ffi_type] - pub struct ConditionalExpr { - /// Condition to be checked. - pub condition: EvaluatesTo, - /// Instruction to be executed if condition pass. - pub then: InstructionExpr, - /// Optional instruction to be executed if condition fail. - pub otherwise: Option, + impl Burn { + /// Constructs a new [`Burn`] for an [`Asset`] of [`BigQuantity`] type. + pub fn asset_big_quantity(big_quantity: u128, asset_id: AssetId) -> Self { + Self { + object: big_quantity, + destination_id: asset_id, + } + } } -} -impl core::fmt::Display for ConditionalExpr { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - write!(f, "IF `{}` THEN `{}`", self.condition, self.then)?; - if let Some(otherwise) = &self.otherwise { - write!(f, " ELSE `{otherwise}`")?; + impl Burn { + /// Constructs a new [`Burn`] for an [`Asset`] of [`Fixed`] type. + pub fn asset_fixed(fixed: Fixed, asset_id: AssetId) -> Self { + Self { + object: fixed, + destination_id: asset_id, + } } + } - Ok(()) + impl Burn> { + /// Constructs a new [`Burn`] for repetition count of [`Trigger`]. + pub fn trigger_repetitions(repetitions: u32, trigger_id: TriggerId) -> Self { + Self { + object: repetitions, + destination_id: trigger_id, + } + } } -} -isi! { - /// Utilitary instruction to fail execution and submit an error `message`. - #[derive(Display)] - #[display(fmt = "FAIL `{message}`")] - #[serde(transparent)] - #[repr(transparent)] - // SAFETY: `Fail` has no trap representation in `String` - #[ffi_type(unsafe {robust})] - pub struct Fail { - /// Message to submit. - pub message: String, + impl_display! { + Burn + where + O: Into + Display, + D: Identifiable, + D::Id: Display, + => + "BURN `{}` FROM `{}`", + object, + destination_id, + } + + impl_into_box! { + Burn | + Burn | + Burn => AssetBurnBox ==> BurnBox::Asset + } + + impl_into_box! { + Burn | + Burn | + Burn | + Burn | + Burn > => BurnBox ==> InstructionBox::Burn + } + + isi! { + /// Generic instruction for a transfer of an object from the identifiable source to the identifiable destination. + #[schema(bounds = "S: Identifiable, S::Id: IntoSchema, \ + O: Into + IntoSchema, \ + D: Identifiable, D::Id: IntoSchema")] + pub struct Transfer, D: Identifiable> { + /// Source object `Id`. + pub source_id: S::Id, + /// Object which should be transferred. + pub object: O, + /// Destination object `Id`. + pub destination_id: D::Id, + } } -} -isi! { - /// Sized structure for all possible Grants. - #[derive(Display)] - #[display(fmt = "GRANT `{object}` TO `{destination_id}`")] - #[ffi_type] - pub struct GrantExpr { - /// Object to grant. - pub object: EvaluatesTo, - /// Account to which to grant this object. - pub destination_id: EvaluatesTo, + impl Transfer { + /// Constructs a new [`Transfer`] for a [`Domain`]. + pub fn domain(from: AccountId, domain_id: DomainId, to: AccountId) -> Self { + Self { + source_id: from, + object: domain_id, + destination_id: to, + } + } } -} -isi! { - /// Sized structure for all possible Grants. - #[derive(Display)] - #[display(fmt = "REVOKE `{object}` FROM `{destination_id}`")] - #[ffi_type] - pub struct RevokeExpr { - /// Object to revoke. - pub object: EvaluatesTo, - /// Account to which to revoke this object from. - pub destination_id: EvaluatesTo, + impl Transfer { + /// Constructs a new [`Transfer`] for an [`AssetDefinition`]. + pub fn asset_definition( + from: AccountId, + asset_definition_id: AssetDefinitionId, + to: AccountId, + ) -> Self { + Self { + source_id: from, + object: asset_definition_id, + destination_id: to, + } + } } -} -isi! { - /// Instruction to execute specified trigger - #[derive(Display)] - #[display(fmt = "EXECUTE `{trigger_id}`")] - #[serde(transparent)] - #[repr(transparent)] - // SAFETY: `ExecuteTriggerExpr` has no trap representation in `TriggerId` - #[ffi_type(unsafe {robust})] - pub struct ExecuteTriggerExpr { - /// Id of a trigger to execute - pub trigger_id: EvaluatesTo, + impl Transfer { + /// Constructs a new [`Transfer`] for an [`Asset`] of [`Quantity`] type. + pub fn asset_quantity(asset_id: AssetId, quantity: u32, to: AccountId) -> Self { + Self { + source_id: asset_id, + object: quantity, + destination_id: to, + } + } } -} -isi! { - /// Sized structure for all possible Upgrades. - #[derive(Display)] - #[display(fmt = "UPGRADE `{object}`")] - #[serde(transparent)] - #[repr(transparent)] - // SAFETY: `UpgradeExpr` has no trap representation in `EvaluatesTo` - #[ffi_type(unsafe {robust})] - pub struct UpgradeExpr { - /// The object to upgrade. - pub object: EvaluatesTo, + impl Transfer { + /// Constructs a new [`Transfer`] for an [`Asset`] of [`BigQuantity`] type. + pub fn asset_big_quantity(asset_id: AssetId, big_quantity: u128, to: AccountId) -> Self { + Self { + source_id: asset_id, + object: big_quantity, + destination_id: to, + } + } } -} -isi! { - /// Instruction to print logs - #[derive(Display)] - #[display(fmt = "LOG({level}): {msg}")] - #[ffi_type] - pub struct LogExpr { - /// Message log level - #[serde(flatten)] - pub level: EvaluatesTo, - /// Msg to be logged - pub msg: EvaluatesTo, + impl Transfer { + /// Constructs a new [`Transfer`] for an [`Asset`] of [`Fixed`] type. + pub fn asset_fixed(asset_id: AssetId, fixed: Fixed, to: AccountId) -> Self { + Self { + source_id: asset_id, + object: fixed, + destination_id: to, + } + } } -} -impl ExecuteTriggerExpr { - /// Construct [`ExecuteTriggerExpr`] - pub fn new(trigger_id: I) -> Self - where - I: Into>, - { - Self { - trigger_id: trigger_id.into(), + impl_display! { + Transfer + where + S: Identifiable, + S::Id: Display, + O: Into + Display, + D: Identifiable, + D::Id: Display, + => + "TRANSFER `{}` FROM `{}` TO `{}`", + object, + source_id, + destination_id, + } + + impl_into_box! { + Transfer | + Transfer | + Transfer => AssetTransferBox ==> TransferBox::Asset + } + + impl_into_box! { + Transfer | + Transfer | + Transfer | + Transfer | + Transfer => TransferBox ==> InstructionBox::Transfer + } + + isi! { + /// Utilitary instruction to fail execution and submit an error `message`. + #[derive(Constructor, Display)] + #[display(fmt = "FAIL `{message}`")] + #[serde(transparent)] + #[repr(transparent)] + pub struct Fail { + /// Message to submit. + pub message: String, } } -} -impl RevokeExpr { - /// Generic constructor. - pub fn new>, I: Into>>( - object: P, - destination_id: I, - ) -> Self { - Self { - destination_id: destination_id.into(), - object: object.into(), + impl_into_box!(Fail ==> InstructionBox::Fail); + + isi! { + /// Generic instruction for granting permission to an entity. + pub struct Grant> { + /// Object to grant. + pub object: O, + /// Entity to which to grant this token. + pub destination_id: AccountId, } } -} -impl GrantExpr { - /// Constructor. - pub fn new>, I: Into>>( - object: P, - destination_id: I, - ) -> Self { - Self { - destination_id: destination_id.into(), - object: object.into(), + impl Grant { + /// Constructs a new [`Grant`] for a [`PermissionToken`]. + pub fn permission_token(permission_token: PermissionToken, to: AccountId) -> Self { + Self { + object: permission_token, + destination_id: to, + } } } -} -impl SetKeyValueExpr { - /// Construct [`SetKeyValueExpr`]. - pub fn new< - I: Into>, - K: Into>, - V: Into>, - >( - object_id: I, - key: K, - value: V, - ) -> Self { - Self { - object_id: object_id.into(), - key: key.into(), - value: value.into(), + impl Grant { + /// Constructs a new [`Grant`] for a [`Role`]. + pub fn role(role_id: RoleId, to: AccountId) -> Self { + Self { + object: role_id, + destination_id: to, + } } } -} -impl RemoveKeyValueExpr { - /// Construct [`RemoveKeyValueExpr`]. - pub fn new>, K: Into>>( - object_id: I, - key: K, - ) -> Self { - Self { - object_id: object_id.into(), - key: key.into(), + impl_display! { + Grant + where + O: Into + Display, + => + "GRANT `{}` TO `{}`", + object, + destination_id, + } + + impl_into_box! { + Grant | + Grant => GrantBox ==> InstructionBox::Grant + } + + isi! { + /// Generic instruction for revoking permission from an entity. + pub struct Revoke> { + /// Object to revoke. + pub object: O, + /// Entity which is being revoked this token from. + pub destination_id: AccountId, } } -} -impl RegisterExpr { - /// Construct [`Register`]. - pub fn new>>(object: O) -> Self { - Self { - object: object.into(), + impl Revoke { + /// Constructs a new [`Revoke`] for a [`PermissionToken`]. + pub fn permission_token(permission_token: PermissionToken, from: AccountId) -> Self { + Self { + object: permission_token, + destination_id: from, + } } } -} -impl UnregisterExpr { - /// Construct [`Unregister`]. - pub fn new>>(object_id: O) -> Self { - Self { - object_id: object_id.into(), + impl Revoke { + /// Constructs a new [`Revoke`] for a [`Role`]. + pub fn role(role_id: RoleId, from: AccountId) -> Self { + Self { + object: role_id, + destination_id: from, + } } } -} -impl MintExpr { - /// Construct [`Mint`]. - pub fn new>, D: Into>>( - object: O, - destination_id: D, - ) -> Self { - Self { - object: object.into(), - destination_id: destination_id.into(), + impl_display! { + Revoke + where + O: Into + Display, + => + "REVOKE `{}` FROM `{}`", + object, + destination_id, + } + + impl_into_box! { + Revoke | + Revoke => RevokeBox ==> InstructionBox::Revoke + } + + isi! { + /// Instruction to execute specified trigger + #[derive(Constructor, Display)] + #[display(fmt = "EXECUTE `{trigger_id}`")] + #[serde(transparent)] + #[repr(transparent)] + pub struct ExecuteTrigger { + /// Id of a trigger to execute + pub trigger_id: TriggerId, } } -} -impl BurnExpr { - /// Construct [`Burn`]. - pub fn new>, D: Into>>( - object: O, - destination_id: D, - ) -> Self { - Self { - object: object.into(), - destination_id: destination_id.into(), + impl_into_box!(ExecuteTrigger ==> InstructionBox::ExecuteTrigger); + + isi! { + /// Generic instruction for upgrading runtime objects. + #[derive(Constructor, Display)] + #[display(fmt = "UPGRADE")] + #[serde(transparent)] + #[repr(transparent)] + pub struct Upgrade { + /// Object to upgrade. + pub executor: Executor, } } -} -impl TransferExpr { - /// Construct [`Transfer`]. - pub fn new< - S: Into>, - O: Into>, - D: Into>, - >( - source_id: S, - object: O, - destination_id: D, - ) -> Self { - Self { - source_id: source_id.into(), - object: object.into(), - destination_id: destination_id.into(), + impl_into_box!(Upgrade ==> InstructionBox::Upgrade); + + isi! { + /// Instruction to print logs + #[derive(Constructor, Display)] + #[display(fmt = "LOG({level}): {msg}")] + pub struct Log { + /// Message log level + #[serde(flatten)] + pub level: Level, + #[getset(skip)] // TODO: Fix this by addressing ffi issues + /// Msg to be logged + pub msg: String, } } + + impl_into_box!(Log ==> InstructionBox::Log); } -impl PairExpr { - /// Construct [`Pair`]. - pub fn new, RI: Into>( - left_instruction: LI, - right_instruction: RI, - ) -> Self { - PairExpr { - left_instruction: left_instruction.into(), - right_instruction: right_instruction.into(), - } +macro_rules! isi_box { + ($($meta:meta)* $item:item) => { + #[derive( + Debug, + Clone, + PartialEq, + Eq, + PartialOrd, + Ord, + Display, + parity_scale_codec::Decode, + parity_scale_codec::Encode, + serde::Deserialize, + serde::Serialize, + iroha_schema::IntoSchema, + derive_more::From, + )] + $($meta)* + $item + }; +} + +isi_box! { + /// Enum with all supported [`SetKeyValue`] instructions. + pub enum SetKeyValueBox { + /// Set key value for [`Domain`]. + Domain(SetKeyValue), + /// Set key value for [`Account`]. + Account(SetKeyValue), + /// Set key value for [`AssetDefinition`]. + AssetDefinition(SetKeyValue), + /// Set key value for [`Asset`]. + Asset(SetKeyValue), } } -impl SequenceExpr { - /// Construct [`SequenceExpr`]. - pub fn new(instructions: impl IntoIterator) -> Self { - Self { - instructions: instructions.into_iter().collect(), - } +isi_box! { + /// Enum with all supported [`RemoveKeyValue`] instructions. + pub enum RemoveKeyValueBox { + /// Remove key value from [`Domain`]. + Domain(RemoveKeyValue), + /// Remove key value from [`Account`]. + Account(RemoveKeyValue), + /// Remove key value from [`AssetDefinition`]. + AssetDefinition(RemoveKeyValue), + /// Remove key value from [`Asset`]. + Asset(RemoveKeyValue), } } -impl ConditionalExpr { - /// Construct [`If`]. - pub fn new>, T: Into>( - condition: C, - then: T, - ) -> Self { - Self { - condition: condition.into(), - then: then.into(), - otherwise: None, - } - } - /// [`If`] constructor with `Otherwise` instruction. - pub fn with_otherwise< - C: Into>, - T: Into, - O: Into, - >( - condition: C, - then: T, - otherwise: O, - ) -> Self { - Self { - condition: condition.into(), - then: then.into(), - otherwise: Some(otherwise.into()), - } +isi_box! { + /// Enum with all supported [`Register`] instructions. + pub enum RegisterBox { + /// Register [`Peer`]. + Peer(Register), + /// Register [`Domain`]. + Domain(Register), + /// Register [`Account`]. + Account(Register), + /// Register [`AssetDefinition`]. + AssetDefinition(Register), + /// Register [`Asset`]. + Asset(Register), + /// Register [`Role`]. + Role(Register), + /// Register [`Trigger`]. + Trigger(Register>) } } -impl Fail { - /// Construct [`Fail`]. - pub fn new(message: &str) -> Self { - Self { - message: String::from(message), - } +isi_box! { + /// Enum with all supported [`Unregister`] instructions. + pub enum UnregisterBox { + /// Unregister [`Peer`]. + Peer(Unregister), + /// Unregister [`Domain`]. + Domain(Unregister), + /// Unregister [`Account`]. + Account(Unregister), + /// Unregister [`AssetDefinition`]. + AssetDefinition(Unregister), + /// Unregister [`Asset`]. + Asset(Unregister), + /// Unregister [`Role`]. + Role(Unregister), + /// Unregister [`Trigger`]. + Trigger(Unregister>) } } -impl SetParameterExpr { - /// Construct [`SetParameterExpr`]. - pub fn new>>(parameter: P) -> Self { - Self { - parameter: parameter.into(), - } +isi_box! { + /// Enum with all supported [`Mint`] instructions. + pub enum MintBox { + /// Mint for [`Account`]. + Account(AccountMintBox), + /// Mint for [`Asset`]. + Asset(AssetMintBox), + /// Mint [`Trigger`] repetitions. + TriggerRepetitions(Mint>), } } -impl NewParameterExpr { - /// Construct [`NewParameterExpr`]. - pub fn new>>(parameter: P) -> Self { - Self { - parameter: parameter.into(), - } +isi_box! { + /// Enum with all supported [`Mint`] instructions related to [`Account`]. + pub enum AccountMintBox { + /// Mint [`PublicKey`]. + PublicKey(Mint), + /// Mint [`SignatureCheckCondition`]. + SignatureCheckCondition(Mint), } } -impl UpgradeExpr { - /// Construct [`UpgradeExpr`]. - pub fn new>>(object: O) -> Self { - Self { - object: object.into(), - } +isi_box! { + /// Enum with all supported [`Mint`] instructions related to [`Asset`]. + pub enum AssetMintBox { + /// Mint [`Asset`] of [`Quantity`] type. + Quantity(Mint), + /// Mint [`Asset`] of [`BigQuantity`] type. + BigQuantity(Mint), + /// Mint [`Asset`] of [`Fixed`] type. + Fixed(Mint), } } -impl LogExpr { - /// Construct [`LogExpr`] - pub fn new>, M: Into>>( - level: L, - msg: M, - ) -> Self { - Self { - level: level.into(), - msg: msg.into(), - } +isi_box! { + /// Enum with all supported [`Burn`] instructions. + pub enum BurnBox { + /// Burn [`PublicKey`] for [`Account`]. + AccountPublicKey(Burn), + /// Burn [`Asset`]. + Asset(AssetBurnBox), + /// Burn [`Trigger`] repetitions. + TriggerRepetitions(Burn>), + } +} + +isi_box! { + /// Enum with all supported [`Burn`] instructions related to [`Asset`]. + pub enum AssetBurnBox { + /// Burn [`Asset`] of [`Quantity`] type. + Quantity(Burn), + /// Burn [`Asset`] of [`BigQuantity`] type. + BigQuantity(Burn), + /// Burn [`Asset`] of [`Fixed`] type. + Fixed(Burn), + } +} + +isi_box! { + /// Enum with all supported [`Transfer`] instructions. + pub enum TransferBox { + /// Transfer [`Domain`] to another [`Account`]. + Domain(Transfer), + /// Transfer [`AssetDefinition`] to another [`Account`]. + AssetDefinition(Transfer), + /// Transfer [`Asset`] to another [`Account`]. + Asset(AssetTransferBox), + } +} + +isi_box! { + /// Enum with all supported [`Transfer`] instructions related to [`Asset`]. + pub enum AssetTransferBox { + /// Transfer [`Asset`] of [`Quantity`] type. + Quantity(Transfer), + /// Transfer [`Asset`] of [`BigQuantity`] type. + BigQuantity(Transfer), + /// Transfer [`Asset`] of [`Fixed`] type. + Fixed(Transfer), + } +} + +isi_box! { + /// Enum with all supported [`Grant`] instructions. + pub enum GrantBox { + /// Grant [`PermissionToken`] to [`Account`]. + PermissionToken(Grant), + /// Grant [`Role`] to [`Account`]. + Role(Grant), + } +} + +isi_box! { + /// Enum with all supported [`Revoke`] instructions. + pub enum RevokeBox { + /// Revoke [`PermissionToken`] from [`Account`]. + PermissionToken(Revoke), + /// Revoke [`Role`] from [`Account`]. + Role(Revoke), } } @@ -884,9 +1237,9 @@ pub mod error { use super::InstructionType; use crate::{ asset::AssetValueType, - evaluate, metadata, + metadata, query::error::{FindError, QueryExecutionFail}, - IdBox, NumericValue, Value, + IdBox, Value, }; #[model] @@ -975,8 +1328,6 @@ pub mod error { // TODO: Only temporarily opaque because of problems with FFI #[ffi_type(opaque)] pub enum InstructionEvaluationError { - /// Failed to evaluate expression - Expression(#[cfg_attr(feature = "std", source)] evaluate::EvaluationError), /// Unsupported parameter type for instruction of type `{0}` Unsupported(InstructionType), /// Failed to find parameter in a permission: {0} @@ -1074,38 +1425,10 @@ pub mod error { /// /// No actual function should ever return this if possible Unknown, - /// Encountered incompatible type of arguments - BinaryOpIncompatibleNumericValueTypes( - #[cfg_attr(feature = "std", source)] BinaryOpIncompatibleNumericValueTypesError, - ), /// Conversion failed: {0} FixedPointConversion(String), } - #[derive( - Debug, - Display, - Clone, - PartialEq, - Eq, - PartialOrd, - Ord, - Deserialize, - Serialize, - Decode, - Encode, - IntoSchema, - )] - #[display( - fmt = "Binary operation does not support provided combination of arguments ({left}, {right})" - )] - #[cfg_attr(feature = "std", derive(thiserror::Error))] - #[ffi_type] - pub struct BinaryOpIncompatibleNumericValueTypesError { - pub left: NumericValue, - pub right: NumericValue, - } - /// Mintability logic error #[derive( Debug, @@ -1160,9 +1483,10 @@ pub mod error { NameLength, } + /// Repetition of of `{instruction_type}` for id `{id}` #[derive( Debug, - Display, + displaydoc::Display, Clone, PartialEq, Eq, @@ -1174,11 +1498,12 @@ pub mod error { Encode, IntoSchema, )] - #[display(fmt = "Repetition of of `{instruction_type}` for id `{id}`")] #[cfg_attr(feature = "std", derive(thiserror::Error))] #[ffi_type] pub struct RepetitionError { + /// Instruction type pub instruction_type: InstructionType, + /// Id of the object being repeated pub id: IdBox, } } @@ -1188,11 +1513,6 @@ pub mod error { Self::Evaluate(InstructionEvaluationError::Type(err)) } } - impl From for InstructionExecutionError { - fn from(err: evaluate::EvaluationError) -> Self { - Self::Evaluate(InstructionEvaluationError::Expression(err)) - } - } impl From for MathError { fn from(err: FixedPointOperationError) -> Self { match err { @@ -1215,10 +1535,9 @@ pub mod error { /// The prelude re-exports most commonly used traits, structs and macros from this crate. pub mod prelude { pub use super::{ - Burn, BurnExpr, ConditionalExpr, ExecuteTrigger, ExecuteTriggerExpr, Fail, Grant, - GrantExpr, InstructionExpr, Log, LogExpr, Mint, MintExpr, NewParameter, NewParameterExpr, - PairExpr, Register, RegisterExpr, RemoveKeyValue, RemoveKeyValueExpr, Revoke, RevokeExpr, - SequenceExpr, SetKeyValue, SetKeyValueExpr, SetParameter, SetParameterExpr, Transfer, - TransferExpr, Unregister, UnregisterExpr, Upgrade, UpgradeExpr, + AccountMintBox, AssetBurnBox, AssetMintBox, AssetTransferBox, Burn, BurnBox, + ExecuteTrigger, Fail, Grant, GrantBox, InstructionBox, Log, Mint, MintBox, NewParameter, + Register, RegisterBox, RemoveKeyValue, RemoveKeyValueBox, Revoke, RevokeBox, SetKeyValue, + SetKeyValueBox, SetParameter, Transfer, TransferBox, Unregister, UnregisterBox, Upgrade, }; } diff --git a/data_model/src/lib.rs b/data_model/src/lib.rs index ff1920331da..0852f58d05f 100644 --- a/data_model/src/lib.rs +++ b/data_model/src/lib.rs @@ -56,10 +56,8 @@ pub mod account; pub mod asset; pub mod block; pub mod domain; -pub mod evaluate; pub mod events; pub mod executor; -pub mod expression; pub mod ipfs; pub mod isi; pub mod metadata; @@ -77,39 +75,76 @@ pub mod trigger; pub mod visit; mod seal { - use crate::{isi::prelude::*, query::prelude::*}; + use crate::prelude::*; pub trait Sealed {} macro_rules! impl_sealed { - ($($ident:ident),+ $(,)?) => { $( - impl Sealed for $ident {} )+ + ($($ident:ident $(< $($generic:ident $(< $inner_generic:ident >)?),+ >)?),+ $(,)?) => { $( + impl Sealed for $ident $(< $($generic $(< $inner_generic >)?),+ >)? {} )+ }; } impl_sealed! { // Boxed instructions - InstructionExpr, - SetKeyValueExpr, - RemoveKeyValueExpr, - RegisterExpr, - UnregisterExpr, - MintExpr, - BurnExpr, - TransferExpr, - GrantExpr, - RevokeExpr, - SetParameterExpr, - NewParameterExpr, - UpgradeExpr, - ExecuteTriggerExpr, - LogExpr, - - // Composite instructions - SequenceExpr, - ConditionalExpr, - PairExpr, - + InstructionBox, + + SetKeyValue, + SetKeyValue, + SetKeyValue, + SetKeyValue, + + RemoveKeyValue, + RemoveKeyValue, + RemoveKeyValue, + RemoveKeyValue, + + Register, + Register, + Register, + Register, + Register, + Register, + Register >, + + Unregister, + Unregister, + Unregister, + Unregister, + Unregister, + Unregister, + Unregister >, + + Mint, + Mint, + Mint, + Mint, + Mint, + Mint >, + + Burn, + Burn, + Burn, + Burn, + Burn >, + + Transfer, + Transfer, + Transfer, + Transfer, + Transfer, + + Grant, + Grant, + + Revoke, + Revoke, + + SetParameter, + NewParameter, + Upgrade, + ExecuteTrigger, + Log, Fail, // Boxed queries @@ -227,6 +262,7 @@ pub mod parameter { pub use self::model::*; use super::*; + use crate::isi::InstructionBox; /// Set of parameter names currently used by iroha #[allow(missing_docs)] @@ -450,27 +486,21 @@ pub mod parameter { } /// Create sequence isi for setting parameters - pub fn into_set_parameters(self) -> isi::SequenceExpr { - isi::SequenceExpr { - instructions: self - .parameters - .into_iter() - .map(isi::SetParameterExpr::new) - .map(Into::into) - .collect(), - } + pub fn into_set_parameters(self) -> Vec { + self.parameters + .into_iter() + .map(isi::SetParameter::new) + .map(Into::into) + .collect() } /// Create sequence isi for creating parameters - pub fn into_create_parameters(self) -> isi::SequenceExpr { - isi::SequenceExpr { - instructions: self - .parameters - .into_iter() - .map(isi::NewParameterExpr::new) - .map(Into::into) - .collect(), - } + pub fn into_create_parameters(self) -> Vec { + self.parameters + .into_iter() + .map(isi::NewParameter::new) + .map(Into::into) + .collect() } } @@ -1828,12 +1858,12 @@ pub mod prelude { #[cfg(feature = "std")] pub use super::current_time; pub use super::{ - account::prelude::*, asset::prelude::*, domain::prelude::*, evaluate::prelude::*, - events::prelude::*, executor::prelude::*, expression::prelude::*, isi::prelude::*, - metadata::prelude::*, name::prelude::*, parameter::prelude::*, peer::prelude::*, - permission::prelude::*, query::prelude::*, role::prelude::*, transaction::prelude::*, - trigger::prelude::*, EnumTryAsError, HasMetadata, IdBox, Identifiable, IdentifiableBox, - LengthLimits, NumericValue, PredicateTrait, RegistrableBox, ToValue, TryAsMut, TryAsRef, - TryToValue, UpgradableBox, ValidationFail, Value, + account::prelude::*, asset::prelude::*, domain::prelude::*, events::prelude::*, + executor::prelude::*, isi::prelude::*, metadata::prelude::*, name::prelude::*, + parameter::prelude::*, peer::prelude::*, permission::prelude::*, query::prelude::*, + role::prelude::*, transaction::prelude::*, trigger::prelude::*, EnumTryAsError, + HasMetadata, IdBox, Identifiable, IdentifiableBox, LengthLimits, NumericValue, + PredicateTrait, RegistrableBox, ToValue, TryAsMut, TryAsRef, TryToValue, UpgradableBox, + ValidationFail, Value, }; } diff --git a/data_model/src/query/mod.rs b/data_model/src/query/mod.rs index d5df0b85016..6d96d52634f 100644 --- a/data_model/src/query/mod.rs +++ b/data_model/src/query/mod.rs @@ -88,6 +88,7 @@ macro_rules! queries { #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] #[derive(parity_scale_codec::Decode, parity_scale_codec::Encode)] #[derive(serde::Deserialize, serde::Serialize)] + #[derive(derive_more::Constructor)] #[derive(iroha_schema::IntoSchema)] $($meta)* $item )+ @@ -184,7 +185,7 @@ pub mod model { /// The hash of the block to which `tx` belongs to pub block_hash: HashOf, /// Transaction - pub transaction: TransactionValue, + pub transaction: Box, } /// Type returned from [`Metadata`] queries @@ -220,9 +221,13 @@ pub mod model { )] #[getset(get = "pub")] pub struct QueryWithParameters { + /// The actual query. pub query: Q, + /// Sorting of the query results. pub sorting: Sorting, + /// Pagination of the query results. pub pagination: Pagination, + /// Amount of results to fetch. pub fetch_size: FetchSize, } } @@ -323,7 +328,7 @@ pub mod role { #[ffi_type(unsafe {robust})] pub struct FindRoleByRoleId { /// `Id` of the [`Role`] to find - pub id: EvaluatesTo, + pub id: RoleId, } /// [`FindRolesByAccountId`] Iroha Query finds all [`Role`]s for a specified account. @@ -334,7 +339,7 @@ pub mod role { #[ffi_type(unsafe {robust})] pub struct FindRolesByAccountId { /// `Id` of an account to find. - pub id: EvaluatesTo, + pub id: AccountId, } } @@ -354,22 +359,6 @@ pub mod role { type Output = Role; } - impl FindRoleByRoleId { - /// Construct [`FindRoleByRoleId`]. - pub fn new(id: impl Into>) -> Self { - Self { id: id.into() } - } - } - - impl FindRolesByAccountId { - /// Construct [`FindRolesByAccountId`]. - pub fn new(account_id: impl Into>) -> Self { - Self { - id: account_id.into(), - } - } - } - /// The prelude re-exports most commonly used traits, structs and macros from this module. pub mod prelude { pub use super::{FindAllRoleIds, FindAllRoles, FindRoleByRoleId, FindRolesByAccountId}; @@ -405,7 +394,7 @@ pub mod permission { #[ffi_type(unsafe {robust})] pub struct FindPermissionTokensByAccountId { /// `Id` of an account to find. - pub id: EvaluatesTo, + pub id: AccountId, } } @@ -417,15 +406,6 @@ pub mod permission { type Output = Vec; } - impl FindPermissionTokensByAccountId { - /// Construct [`FindPermissionTokensByAccountId`]. - pub fn new(account_id: impl Into>) -> Self { - Self { - id: account_id.into(), - } - } - } - /// The prelude re-exports most commonly used traits, structs and macros from this module. pub mod prelude { pub use super::{FindPermissionTokenSchema, FindPermissionTokensByAccountId}; @@ -459,7 +439,7 @@ pub mod account { #[ffi_type(unsafe {robust})] pub struct FindAccountById { /// `Id` of an account to find. - pub id: EvaluatesTo, + pub id: AccountId, } /// [`FindAccountKeyValueByIdAndKey`] Iroha Query finds a [`Value`] @@ -469,9 +449,9 @@ pub mod account { #[ffi_type] pub struct FindAccountKeyValueByIdAndKey { /// `Id` of an account to find. - pub id: EvaluatesTo, + pub id: AccountId, /// Key of the specific key-value in the Account's metadata. - pub key: EvaluatesTo, + pub key: Name, } /// [`FindAccountsByName`] Iroha Query gets [`Account`]s name as input and @@ -483,7 +463,7 @@ pub mod account { #[ffi_type(unsafe {robust})] pub struct FindAccountsByName { /// `name` of accounts to find. - pub name: EvaluatesTo, + pub name: Name, } @@ -496,7 +476,7 @@ pub mod account { #[ffi_type(unsafe {robust})] pub struct FindAccountsByDomainId { /// `Id` of the domain under which accounts should be found. - pub domain_id: EvaluatesTo, + pub domain_id: DomainId, } /// [`FindAccountsWithAsset`] Iroha Query gets [`AssetDefinition`]s id as input and @@ -508,7 +488,7 @@ pub mod account { #[ffi_type(unsafe {robust})] pub struct FindAccountsWithAsset { /// `Id` of the definition of the asset which should be stored in founded accounts. - pub asset_definition_id: EvaluatesTo, + pub asset_definition_id: AssetDefinitionId, } } @@ -536,51 +516,6 @@ pub mod account { type Output = Vec; } - impl FindAccountById { - /// Construct [`FindAccountById`]. - pub fn new(id: impl Into>) -> Self { - Self { id: id.into() } - } - } - - impl FindAccountKeyValueByIdAndKey { - /// Construct [`FindAccountById`]. - pub fn new( - id: impl Into>, - key: impl Into>, - ) -> Self { - Self { - id: id.into(), - key: key.into(), - } - } - } - - impl FindAccountsByName { - /// Construct [`FindAccountsByName`]. - pub fn new(name: impl Into>) -> Self { - Self { name: name.into() } - } - } - - impl FindAccountsByDomainId { - /// Construct [`FindAccountsByDomainId`]. - pub fn new(domain_id: impl Into>) -> Self { - Self { - domain_id: domain_id.into(), - } - } - } - - impl FindAccountsWithAsset { - /// Construct [`FindAccountsWithAsset`]. - pub fn new(asset_definition_id: impl Into>) -> Self { - Self { - asset_definition_id: asset_definition_id.into(), - } - } - } - /// The prelude re-exports most commonly used traits, structs and macros from this crate. pub mod prelude { pub use super::{ @@ -627,7 +562,7 @@ pub mod asset { #[ffi_type(unsafe {robust})] pub struct FindAssetById { /// `Id` of an [`Asset`] to find. - pub id: EvaluatesTo, + pub id: AssetId, } /// [`FindAssetDefinitionById`] Iroha Query finds an [`AssetDefinition`] by it's identification in Iroha [`Peer`]. @@ -638,7 +573,7 @@ pub mod asset { #[ffi_type(unsafe {robust})] pub struct FindAssetDefinitionById { /// `Id` of an [`AssetDefinition`] to find. - pub id: EvaluatesTo, + pub id: AssetDefinitionId, } /// [`FindAssetsByName`] Iroha Query gets [`Asset`]s name as input and @@ -650,7 +585,7 @@ pub mod asset { #[ffi_type(unsafe {robust})] pub struct FindAssetsByName { /// [`Name`] of [`Asset`]s to find. - pub name: EvaluatesTo, + pub name: Name, } /// [`FindAssetsByAccountId`] Iroha Query gets [`AccountId`] as input and find all [`Asset`]s @@ -662,7 +597,7 @@ pub mod asset { #[ffi_type(unsafe {robust})] pub struct FindAssetsByAccountId { /// [`AccountId`] under which assets should be found. - pub account_id: EvaluatesTo, + pub account_id: AccountId, } /// [`FindAssetsByAssetDefinitionId`] Iroha Query gets [`AssetDefinitionId`] as input and @@ -674,7 +609,7 @@ pub mod asset { #[ffi_type(unsafe {robust})] pub struct FindAssetsByAssetDefinitionId { /// [`AssetDefinitionId`] with type of [`Asset`]s should be found. - pub asset_definition_id: EvaluatesTo, + pub asset_definition_id: AssetDefinitionId, } /// [`FindAssetsByDomainId`] Iroha Query gets [`Domain`]s id as input and @@ -686,7 +621,7 @@ pub mod asset { #[ffi_type(unsafe {robust})] pub struct FindAssetsByDomainId { /// `Id` of the domain under which assets should be found. - pub domain_id: EvaluatesTo, + pub domain_id: DomainId, } /// [`FindAssetsByDomainIdAndAssetDefinitionId`] Iroha Query gets [`DomainId`] and @@ -697,9 +632,9 @@ pub mod asset { #[ffi_type] pub struct FindAssetsByDomainIdAndAssetDefinitionId { /// `Id` of the domain under which assets should be found. - pub domain_id: EvaluatesTo, + pub domain_id: DomainId, /// [`AssetDefinitionId`] assets of which type should be found. - pub asset_definition_id: EvaluatesTo, + pub asset_definition_id: AssetDefinitionId, } /// [`FindAssetQuantityById`] Iroha Query gets [`AssetId`] as input and finds [`Asset::quantity`] @@ -711,7 +646,7 @@ pub mod asset { #[ffi_type(unsafe {robust})] pub struct FindAssetQuantityById { /// `Id` of an [`Asset`] to find quantity of. - pub id: EvaluatesTo, + pub id: AssetId, } /// [`FindTotalAssetQuantityByAssetDefinitionId`] Iroha Query gets [`AssetDefinitionId`] as input and finds total [`Asset::quantity`] @@ -724,7 +659,7 @@ pub mod asset { #[ffi_type(unsafe {robust})] pub struct FindTotalAssetQuantityByAssetDefinitionId { /// `Id` of an [`Asset`] to find quantity of. - pub id: EvaluatesTo, + pub id: AssetDefinitionId, } /// [`FindAssetKeyValueByIdAndKey`] Iroha Query gets [`AssetId`] and key as input and finds [`Value`] @@ -734,9 +669,9 @@ pub mod asset { #[ffi_type] pub struct FindAssetKeyValueByIdAndKey { /// `Id` of an [`Asset`] acting as [`Store`](crate::asset::AssetValue::Store). - pub id: EvaluatesTo, + pub id: AssetId, /// The key of the key-value pair stored in the asset. - pub key: EvaluatesTo, + pub key: Name, } /// [`FindAssetDefinitionKeyValueByIdAndKey`] Iroha Query gets [`AssetDefinitionId`] and key as input and finds [`Value`] @@ -746,9 +681,9 @@ pub mod asset { #[ffi_type] pub struct FindAssetDefinitionKeyValueByIdAndKey { /// `Id` of an [`Asset`] acting as [`Store`](crate::asset::AssetValue::Store).. - pub id: EvaluatesTo, + pub id: AssetDefinitionId, /// The key of the key-value pair stored in the asset. - pub key: EvaluatesTo, + pub key: Name, } } @@ -804,104 +739,6 @@ pub mod asset { type Output = MetadataValue; } - impl FindAssetById { - /// Construct [`FindAssetById`]. - pub fn new(id: impl Into>) -> Self { - Self { id: id.into() } - } - } - - impl FindAssetDefinitionById { - /// Construct [`FindAssetDefinitionById`]. - pub fn new(id: impl Into>) -> Self { - Self { id: id.into() } - } - } - - impl FindAssetsByName { - /// Construct [`FindAssetsByName`]. - pub fn new(name: impl Into>) -> Self { - Self { name: name.into() } - } - } - - impl FindAssetsByAccountId { - /// Construct [`FindAssetsByAccountId`]. - pub fn new(account_id: impl Into>) -> Self { - Self { - account_id: account_id.into(), - } - } - } - - impl FindAssetsByAssetDefinitionId { - /// Construct [`FindAssetsByAssetDefinitionId`]. - pub fn new(asset_definition_id: impl Into>) -> Self { - Self { - asset_definition_id: asset_definition_id.into(), - } - } - } - - impl FindAssetsByDomainId { - /// Construct [`FindAssetsByDomainId`]. - pub fn new(domain_id: impl Into>) -> Self { - Self { - domain_id: domain_id.into(), - } - } - } - - impl FindAssetsByDomainIdAndAssetDefinitionId { - /// Construct [`FindAssetsByDomainIdAndAssetDefinitionId`]. - pub fn new( - domain_id: impl Into>, - asset_definition_id: impl Into>, - ) -> Self { - Self { - domain_id: domain_id.into(), - asset_definition_id: asset_definition_id.into(), - } - } - } - - impl FindAssetQuantityById { - /// Construct [`FindAssetQuantityById`]. - pub fn new(id: impl Into>) -> Self { - Self { id: id.into() } - } - } - - impl FindTotalAssetQuantityByAssetDefinitionId { - /// Construct [`FindTotalAssetQuantityByAssetDefinitionId`] - pub fn new(id: impl Into>) -> Self { - Self { id: id.into() } - } - } - - impl FindAssetKeyValueByIdAndKey { - /// Construct [`FindAssetKeyValueByIdAndKey`]. - pub fn new(id: impl Into>, key: impl Into>) -> Self { - Self { - id: id.into(), - key: key.into(), - } - } - } - - impl FindAssetDefinitionKeyValueByIdAndKey { - /// Construct [`FindAssetDefinitionKeyValueByIdAndKey`]. - pub fn new( - id: impl Into>, - key: impl Into>, - ) -> Self { - Self { - id: id.into(), - key: key.into(), - } - } - } - /// The prelude re-exports most commonly used traits, structs and macros from this crate. pub mod prelude { pub use super::{ @@ -942,10 +779,9 @@ pub mod domain { #[ffi_type(unsafe {robust})] pub struct FindDomainById { /// `Id` of the domain to find. - pub id: EvaluatesTo, + pub id: DomainId, } - /// [`FindDomainKeyValueByIdAndKey`] Iroha Query finds a [`Value`] of the key-value metadata pair /// in the specified domain. #[derive(Display)] @@ -953,9 +789,9 @@ pub mod domain { #[ffi_type] pub struct FindDomainKeyValueByIdAndKey { /// `Id` of an domain to find. - pub id: EvaluatesTo, + pub id: DomainId, /// Key of the specific key-value in the domain's metadata. - pub key: EvaluatesTo, + pub key: Name, } } @@ -971,26 +807,6 @@ pub mod domain { type Output = MetadataValue; } - impl FindDomainById { - /// Construct [`FindDomainById`]. - pub fn new(id: impl Into>) -> Self { - Self { id: id.into() } - } - } - - impl FindDomainKeyValueByIdAndKey { - /// Construct [`FindDomainKeyValueByIdAndKey`]. - pub fn new( - id: impl Into>, - key: impl Into>, - ) -> Self { - Self { - id: id.into(), - key: key.into(), - } - } - } - /// The prelude re-exports most commonly used traits, structs and macros from this crate. pub mod prelude { pub use super::{FindAllDomains, FindDomainById, FindDomainKeyValueByIdAndKey}; @@ -1015,7 +831,6 @@ pub mod peer { #[ffi_type] pub struct FindAllPeers; - /// [`FindAllParameters`] Iroha Query finds all [`Peer`]s parameters. #[derive(Copy, Display)] #[display(fmt = "Find all peers parameters")] @@ -1049,8 +864,7 @@ pub mod trigger { use crate::{ domain::prelude::*, events::TriggeringFilterBox, - expression::EvaluatesTo, - prelude::InstructionExpr, + prelude::InstructionBox, trigger::{Trigger, TriggerId}, Executable, Identifiable, Name, Value, }; @@ -1063,7 +877,6 @@ pub mod trigger { #[ffi_type] pub struct FindAllActiveTriggerIds; - /// Find Trigger given its ID. #[derive(Display)] #[display(fmt = "Find `{id}` trigger")] @@ -1072,7 +885,7 @@ pub mod trigger { #[ffi_type(unsafe {robust})] pub struct FindTriggerById { /// The Identification of the trigger to be found. - pub id: EvaluatesTo, + pub id: TriggerId, } @@ -1082,9 +895,9 @@ pub mod trigger { #[ffi_type] pub struct FindTriggerKeyValueByIdAndKey { /// The Identification of the trigger to be found. - pub id: EvaluatesTo, + pub id: TriggerId, /// The key inside the metadata dictionary to be returned. - pub key: EvaluatesTo, + pub key: Name, } @@ -1096,7 +909,7 @@ pub mod trigger { #[ffi_type(unsafe {robust})] pub struct FindTriggersByDomainId { /// [`DomainId`] specifies the domain in which to search for triggers. - pub domain_id: EvaluatesTo, + pub domain_id: DomainId, } } @@ -1116,35 +929,6 @@ pub mod trigger { type Output = Vec>; } - impl FindTriggerById { - /// Construct [`FindTriggerById`]. - pub fn new(id: impl Into>) -> Self { - Self { id: id.into() } - } - } - - impl FindTriggerKeyValueByIdAndKey { - /// Construct [`FindTriggerKeyValueByIdAndKey`]. - pub fn new( - id: impl Into>, - key: impl Into>, - ) -> Self { - Self { - id: id.into(), - key: key.into(), - } - } - } - - impl FindTriggersByDomainId { - /// Construct [`FindTriggersByDomainId`]. - pub fn new(domain_id: impl Into>) -> Self { - Self { - domain_id: domain_id.into(), - } - } - } - pub mod prelude { //! Prelude Re-exports most commonly used traits, structs and macros from this crate. pub use super::{ @@ -1166,10 +950,7 @@ pub mod transaction { use iroha_crypto::HashOf; use super::{Query, TransactionQueryOutput}; - use crate::{ - account::AccountId, expression::EvaluatesTo, prelude::Account, - transaction::SignedTransaction, - }; + use crate::{account::AccountId, prelude::Account, transaction::SignedTransaction}; queries! { /// [`FindAllTransactions`] Iroha Query lists all transactions included in a blockchain @@ -1187,19 +968,19 @@ pub mod transaction { #[ffi_type(unsafe {robust})] pub struct FindTransactionsByAccountId { /// Signer's [`AccountId`] under which transactions should be found. - pub account_id: EvaluatesTo, + pub account_id: AccountId, } /// [`FindTransactionByHash`] Iroha Query finds a transaction (if any) /// with corresponding hash value - #[derive(Display)] + #[derive(Copy, Display)] #[display(fmt = "Find transaction with `{hash}` hash")] #[repr(transparent)] // SAFETY: `FindTransactionByHash` has no trap representation in `EvaluatesTo>` #[ffi_type(unsafe {robust})] pub struct FindTransactionByHash { /// Transaction hash. - pub hash: EvaluatesTo>, + pub hash: HashOf, } } @@ -1215,22 +996,6 @@ pub mod transaction { type Output = TransactionQueryOutput; } - impl FindTransactionsByAccountId { - /// Construct [`FindTransactionsByAccountId`]. - pub fn new(account_id: impl Into>) -> Self { - Self { - account_id: account_id.into(), - } - } - } - - impl FindTransactionByHash { - /// Construct [`FindTransactionByHash`]. - pub fn new(hash: impl Into>>) -> Self { - Self { hash: hash.into() } - } - } - /// The prelude re-exports most commonly used traits, structs and macros from this crate. pub mod prelude { pub use super::{FindAllTransactions, FindTransactionByHash, FindTransactionsByAccountId}; @@ -1249,10 +1014,7 @@ pub mod block { use iroha_crypto::HashOf; use super::Query; - use crate::{ - block::{BlockHeader, SignedBlock}, - prelude::EvaluatesTo, - }; + use crate::block::{BlockHeader, SignedBlock}; queries! { /// [`FindAllBlocks`] Iroha Query lists all blocks sorted by @@ -1270,14 +1032,14 @@ pub mod block { pub struct FindAllBlockHeaders; /// [`FindBlockHeaderByHash`] Iroha Query finds block header by block hash - #[derive(Display)] + #[derive(Copy, Display)] #[display(fmt = "Find block header with `{hash}` hash")] #[repr(transparent)] // SAFETY: `FindBlockHeaderByHash` has no trap representation in `EvaluatesTo>` #[ffi_type(unsafe {robust})] pub struct FindBlockHeaderByHash { /// Block hash. - pub hash: EvaluatesTo>, + pub hash: HashOf, } } @@ -1293,13 +1055,6 @@ pub mod block { type Output = BlockHeader; } - impl FindBlockHeaderByHash { - /// Construct [`FindBlockHeaderByHash`]. - pub fn new(hash: impl Into>>) -> Self { - Self { hash: hash.into() } - } - } - /// The prelude re-exports most commonly used traits, structs and macros from this crate. pub mod prelude { pub use super::{FindAllBlockHeaders, FindAllBlocks, FindBlockHeaderByHash}; @@ -1538,12 +1293,6 @@ pub mod error { #[skip_try_from] String, ), - /// Query has a malformed expression: {0} - Evaluate( - #[skip_from] - #[skip_try_from] - String, - ), /// {0} #[cfg_attr(feature = "std", error(transparent))] Find(FindError), diff --git a/data_model/src/transaction.rs b/data_model/src/transaction.rs index a7742873c7a..9d288239636 100644 --- a/data_model/src/transaction.rs +++ b/data_model/src/transaction.rs @@ -22,7 +22,7 @@ use serde::{Deserialize, Serialize}; pub use self::model::*; use crate::{ account::AccountId, - isi::{Instruction, InstructionExpr}, + isi::{Instruction, InstructionBox}, metadata::UnlimitedMetadata, name::Name, Value, @@ -51,7 +51,7 @@ pub mod model { pub enum Executable { /// Ordered set of instructions. #[debug(fmt = "{_0:?}")] - Instructions(Vec), + Instructions(Vec), /// WebAssembly smartcontract Wasm(WasmSmartContract), } @@ -533,7 +533,7 @@ pub mod error { pub struct InstructionExecutionFail { /// Instruction for which execution failed #[getset(get = "pub")] - pub instruction: InstructionExpr, + pub instruction: InstructionBox, /// Error which happened during execution pub reason: String, } @@ -611,15 +611,12 @@ pub mod error { impl Display for InstructionExecutionFail { fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { - use InstructionExpr::*; + use InstructionBox::*; let kind = match self.instruction { Burn(_) => "burn", Fail(_) => "fail", - If(_) => "if", Mint(_) => "mint", - Pair(_) => "pair", Register(_) => "register", - Sequence(_) => "sequence", Transfer(_) => "transfer", Unregister(_) => "un-register", SetKeyValue(_) => "set key-value pair", @@ -691,7 +688,7 @@ mod http { creation_time_ms, nonce: None, time_to_live_ms: None, - instructions: Vec::::new().into(), + instructions: Vec::::new().into(), metadata: UnlimitedMetadata::new(), }, } @@ -707,7 +704,7 @@ mod http { self.payload.instructions = instructions .into_iter() .map(Into::into) - .collect::>() + .collect::>() .into(); self } diff --git a/data_model/src/trigger.rs b/data_model/src/trigger.rs index 0127ce3bf77..4270d2d34ac 100644 --- a/data_model/src/trigger.rs +++ b/data_model/src/trigger.rs @@ -203,7 +203,7 @@ pub mod action { /// Enumeration of possible repetitions schemes. #[derive( - Debug, Clone, PartialEq, Eq, Decode, Encode, Deserialize, Serialize, IntoSchema, + Debug, Copy, Clone, PartialEq, Eq, Decode, Encode, Deserialize, Serialize, IntoSchema, )] #[ffi_type] pub enum Repeats { diff --git a/data_model/src/visit.rs b/data_model/src/visit.rs index 62a33f328c8..9f82b19baae 100644 --- a/data_model/src/visit.rs +++ b/data_model/src/visit.rs @@ -6,7 +6,7 @@ use alloc::format; use iroha_crypto::PublicKey; -use crate::{evaluate::ExpressionEvaluator, isi::Log, prelude::*, NumericValue}; +use crate::{isi::Log, prelude::*}; macro_rules! delegate { ( $($visitor:ident $(<$param:ident $(: $bound:path)?>)?($operation:ty)),+ $(,)? ) => { $( @@ -16,54 +16,36 @@ macro_rules! delegate { }; } -macro_rules! evaluate_expr { - ($visitor:ident, $authority:ident, <$isi:ident as $isi_type:ty>::$field:ident()) => {{ - $visitor.visit_expression($authority, $isi.$field()); - - $visitor.evaluate($isi.$field()).expect(&format!( - "Failed to evaluate field '{}::{}'", - stringify!($isi_type), - stringify!($field), - )) - }}; -} - /// Trait to validate Iroha entities. /// Default implementation of non-leaf visitors runs `visit_` functions for leafs. /// Default implementation for leaf visitors is blank. /// /// This trait is based on the visitor pattern. -pub trait Visit: ExpressionEvaluator { +pub trait Visit { delegate! { - visit_unsupported(T), - // Visit SignedTransaction visit_transaction(&SignedTransaction), - visit_instruction(&InstructionExpr), - visit_expression(&EvaluatesTo), + visit_instruction(&InstructionBox), visit_wasm(&WasmSmartContract), visit_query(&QueryBox), - // Visit InstructionExpr - visit_burn(&BurnExpr), + // Visit InstructionBox + visit_burn(&BurnBox), visit_fail(&Fail), - visit_grant(&GrantExpr), - visit_if(&ConditionalExpr), - visit_mint(&MintExpr), - visit_pair(&PairExpr), - visit_register(&RegisterExpr), - visit_remove_key_value(&RemoveKeyValueExpr), - visit_revoke(&RevokeExpr), - visit_sequence(&SequenceExpr), - visit_set_key_value(&SetKeyValueExpr), - visit_transfer(&TransferExpr), - visit_unregister(&UnregisterExpr), - visit_upgrade(&UpgradeExpr), - - visit_execute_trigger(ExecuteTrigger), - visit_new_parameter(NewParameter), - visit_set_parameter(SetParameter), - visit_log(Log), + visit_grant(&GrantBox), + visit_mint(&MintBox), + visit_register(&RegisterBox), + visit_remove_key_value(&RemoveKeyValueBox), + visit_revoke(&RevokeBox), + visit_set_key_value(&SetKeyValueBox), + visit_transfer(&TransferBox), + visit_unregister(&UnregisterBox), + visit_upgrade(&Upgrade), + + visit_execute_trigger(&ExecuteTrigger), + visit_new_parameter(&NewParameter), + visit_set_parameter(&SetParameter), + visit_log(&Log), // Visit QueryBox visit_find_account_by_id(&FindAccountById), @@ -78,7 +60,7 @@ pub trait Visit: ExpressionEvaluator { visit_find_all_block_headers(&FindAllBlockHeaders), visit_find_all_blocks(&FindAllBlocks), visit_find_all_domains(&FindAllDomains), - visit_find_all_parammeters(&FindAllParameters), + visit_find_all_parameters(&FindAllParameters), visit_find_all_peers(&FindAllPeers), visit_find_permission_token_schema(&FindPermissionTokenSchema), visit_find_all_role_ids(&FindAllRoleIds), @@ -107,74 +89,69 @@ pub trait Visit: ExpressionEvaluator { visit_find_trigger_key_value_by_id_and_key(&FindTriggerKeyValueByIdAndKey), visit_find_triggers_by_domain_id(&FindTriggersByDomainId), - // Visit RegisterExpr - visit_register_peer(Register), - visit_register_domain(Register), - visit_register_account(Register), - visit_register_asset_definition(Register), - visit_register_asset(Register), - visit_register_role(Register), - visit_register_trigger(Register>), - - // Visit UnregisterExpr - visit_unregister_peer(Unregister), - visit_unregister_domain(Unregister), - visit_unregister_account(Unregister), - visit_unregister_asset_definition(Unregister), - visit_unregister_asset(Unregister), + // Visit RegisterBox + visit_register_peer(&Register), + visit_register_domain(&Register), + visit_register_account(&Register), + visit_register_asset_definition(&Register), + visit_register_asset(&Register), + visit_register_role(&Register), + visit_register_trigger(&Register>), + + // Visit UnregisterBox + visit_unregister_peer(&Unregister), + visit_unregister_domain(&Unregister), + visit_unregister_account(&Unregister), + visit_unregister_asset_definition(&Unregister), + visit_unregister_asset(&Unregister), // TODO: Need to allow role creator to unregister it somehow - visit_unregister_role(Unregister), - visit_unregister_trigger(Unregister>), - - // Visit MintExpr - visit_mint_asset(Mint), - visit_mint_account_public_key(Mint), - visit_mint_account_signature_check_condition(Mint), - visit_mint_trigger_repetitions(Mint>), - - // Visit BurnExpr - visit_burn_account_public_key(Burn), - visit_burn_asset(Burn), - visit_burn_trigger_repetitions(Burn>), - - // Visit TransferExpr - visit_transfer_asset_definition(Transfer), - visit_transfer_asset(Transfer), - visit_transfer_domain(Transfer), - - // Visit SetKeyValueExpr - visit_set_domain_key_value(SetKeyValue), - visit_set_account_key_value(SetKeyValue), - visit_set_asset_definition_key_value(SetKeyValue), - visit_set_asset_key_value(SetKeyValue), - - // Visit RemoveKeyValueExpr - visit_remove_domain_key_value(RemoveKeyValue), - visit_remove_account_key_value(RemoveKeyValue), - visit_remove_asset_definition_key_value(RemoveKeyValue), - visit_remove_asset_key_value(RemoveKeyValue), - - // Visit GrantExpr - visit_grant_account_permission(Grant), - visit_grant_account_role(Grant), - - // Visit RevokeExpr - visit_revoke_account_permission(Revoke), - visit_revoke_account_role(Revoke), - - // Visit UpgradeExpr - visit_upgrade_executor(Upgrade), + visit_unregister_role(&Unregister), + visit_unregister_trigger(&Unregister>), + + // Visit MintBox + visit_mint_asset_quantity(&Mint), + visit_mint_asset_big_quantity(&Mint), + visit_mint_asset_fixed(&Mint), + visit_mint_account_public_key(&Mint), + visit_mint_account_signature_check_condition(&Mint), + visit_mint_trigger_repetitions(&Mint>), + + // Visit BurnBox + visit_burn_account_public_key(&Burn), + visit_burn_asset_quantity(&Burn), + visit_burn_asset_big_quantity(&Burn), + visit_burn_asset_fixed(&Burn), + visit_burn_trigger_repetitions(&Burn>), + + // Visit TransferBox + visit_transfer_asset_definition(&Transfer), + visit_transfer_asset_quantity(&Transfer), + visit_transfer_asset_big_quantity(&Transfer), + visit_transfer_asset_fixed(&Transfer), + visit_transfer_domain(&Transfer), + + // Visit SetKeyValueBox + visit_set_domain_key_value(&SetKeyValue), + visit_set_account_key_value(&SetKeyValue), + visit_set_asset_definition_key_value(&SetKeyValue), + visit_set_asset_key_value(&SetKeyValue), + + // Visit RemoveKeyValueBox + visit_remove_domain_key_value(&RemoveKeyValue), + visit_remove_account_key_value(&RemoveKeyValue), + visit_remove_asset_definition_key_value(&RemoveKeyValue), + visit_remove_asset_key_value(&RemoveKeyValue), + + // Visit GrantBox + visit_grant_account_permission(&Grant), + visit_grant_account_role(&Grant), + + // Visit RevokeBox + visit_revoke_account_permission(&Revoke), + visit_revoke_account_role(&Revoke), } } -/// Called when visiting any unsupported syntax tree node -fn visit_unsupported( - _visitor: &mut V, - _authority: &AccountId, - _isi: T, -) { -} - pub fn visit_transaction( visitor: &mut V, authority: &AccountId, @@ -213,7 +190,7 @@ pub fn visit_query(visitor: &mut V, authority: &AccountId, qu visit_find_all_block_headers(FindAllBlockHeaders), visit_find_all_blocks(FindAllBlocks), visit_find_all_domains(FindAllDomains), - visit_find_all_parammeters(FindAllParameters), + visit_find_all_parameters(FindAllParameters), visit_find_all_peers(FindAllPeers), visit_find_permission_token_schema(FindPermissionTokenSchema), visit_find_all_role_ids(FindAllRoleIds), @@ -251,7 +228,7 @@ pub fn visit_wasm( ) { } -/// Default validation for [`InstructionExpr`]. +/// Default validation for [`InstructionBox`]. /// /// # Warning /// @@ -259,430 +236,167 @@ pub fn visit_wasm( pub fn visit_instruction( visitor: &mut V, authority: &AccountId, - isi: &InstructionExpr, -) { - macro_rules! isi_visitors { - ( $($visitor:ident($isi:ident)),+ $(,)? ) => { - match isi { - InstructionExpr::NewParameter(isi) => { - let parameter = evaluate_expr!(visitor, authority, ::parameter()); - visitor.visit_new_parameter(authority, NewParameter{parameter}); - } - InstructionExpr::SetParameter(isi) => { - let parameter = evaluate_expr!(visitor, authority, ::parameter()); - visitor.visit_set_parameter(authority, SetParameter{parameter}); - } - InstructionExpr::ExecuteTrigger(isi) => { - let trigger_id = evaluate_expr!(visitor, authority, ::trigger_id()); - visitor.visit_execute_trigger(authority, ExecuteTrigger{trigger_id}); - } - InstructionExpr::Log(isi) => { - let msg = evaluate_expr!(visitor, authority, ::msg()); - let level = evaluate_expr!(visitor, authority, ::level()); - visitor.visit_log(authority, Log { msg, level }); - } $( - InstructionExpr::$isi(isi) => { - visitor.$visitor(authority, isi); - } )+ - } - }; - } - - isi_visitors! { - visit_burn(Burn), - visit_fail(Fail), - visit_grant(Grant), - visit_mint(Mint), - visit_register(Register), - visit_remove_key_value(RemoveKeyValue), - visit_revoke(Revoke), - visit_set_key_value(SetKeyValue), - visit_transfer(Transfer), - visit_unregister(Unregister), - visit_upgrade(Upgrade), - visit_sequence(Sequence), - visit_pair(Pair), - visit_if(If), - } -} - -pub fn visit_expression( - visitor: &mut V, - authority: &AccountId, - expression: &EvaluatesTo, + isi: &InstructionBox, ) { - macro_rules! visit_binary_math_expression { - ($e:ident) => {{ - visitor.visit_expression(authority, $e.left()); - visitor.visit_expression(authority, $e.right()) - }}; - } - - macro_rules! visit_binary_bool_expression { - ($e:ident) => {{ - visitor.visit_expression(authority, $e.left()); - visitor.visit_expression(authority, $e.right()) - }}; - } - - match expression.expression() { - Expression::Add(expr) => visit_binary_math_expression!(expr), - Expression::Subtract(expr) => visit_binary_math_expression!(expr), - Expression::Multiply(expr) => visit_binary_math_expression!(expr), - Expression::Divide(expr) => visit_binary_math_expression!(expr), - Expression::Mod(expr) => visit_binary_math_expression!(expr), - Expression::RaiseTo(expr) => visit_binary_math_expression!(expr), - Expression::Greater(expr) => visit_binary_math_expression!(expr), - Expression::Less(expr) => visit_binary_math_expression!(expr), - Expression::Equal(expr) => visit_binary_bool_expression!(expr), - Expression::Not(expr) => visitor.visit_expression(authority, expr.expression()), - Expression::And(expr) => visit_binary_bool_expression!(expr), - Expression::Or(expr) => visit_binary_bool_expression!(expr), - Expression::If(expr) => { - visitor.visit_expression(authority, expr.condition()); - visitor.visit_expression(authority, expr.then()); - visitor.visit_expression(authority, expr.otherwise()); + match isi { + InstructionBox::NewParameter(variant_value) => { + visitor.visit_new_parameter(authority, variant_value) } - Expression::Contains(expr) => { - visitor.visit_expression(authority, expr.collection()); - visitor.visit_expression(authority, expr.element()); + InstructionBox::SetParameter(variant_value) => { + visitor.visit_set_parameter(authority, variant_value) } - Expression::ContainsAll(expr) => { - visitor.visit_expression(authority, expr.collection()); - visitor.visit_expression(authority, expr.elements()); + InstructionBox::ExecuteTrigger(variant_value) => { + visitor.visit_execute_trigger(authority, variant_value) } - Expression::ContainsAny(expr) => { - visitor.visit_expression(authority, expr.collection()); - visitor.visit_expression(authority, expr.elements()); + InstructionBox::Log(variant_value) => visitor.visit_log(authority, variant_value), + InstructionBox::Burn(variant_value) => visitor.visit_burn(authority, variant_value), + InstructionBox::Fail(variant_value) => visitor.visit_fail(authority, variant_value), + InstructionBox::Grant(variant_value) => visitor.visit_grant(authority, variant_value), + InstructionBox::Mint(variant_value) => visitor.visit_mint(authority, variant_value), + InstructionBox::Register(variant_value) => visitor.visit_register(authority, variant_value), + InstructionBox::RemoveKeyValue(variant_value) => { + visitor.visit_remove_key_value(authority, variant_value) } - Expression::Where(expr) => visitor.visit_expression(authority, expr.expression()), - Expression::Query(query) => visitor.visit_query(authority, query), - Expression::ContextValue(_) | Expression::Raw(_) => {} + InstructionBox::Revoke(variant_value) => visitor.visit_revoke(authority, variant_value), + InstructionBox::SetKeyValue(variant_value) => { + visitor.visit_set_key_value(authority, variant_value) + } + InstructionBox::Transfer(variant_value) => visitor.visit_transfer(authority, variant_value), + InstructionBox::Unregister(variant_value) => { + visitor.visit_unregister(authority, variant_value) + } + InstructionBox::Upgrade(variant_value) => visitor.visit_upgrade(authority, variant_value), } } pub fn visit_register( visitor: &mut V, authority: &AccountId, - isi: &RegisterExpr, + isi: &RegisterBox, ) { - macro_rules! match_all { - ( $( $visitor:ident($object:ident) ),+ $(,)? ) => { - let object = evaluate_expr!(visitor, authority, ::object()); - - match object { $( - RegistrableBox::$object(object) => visitor.$visitor(authority, Register{object}), )+ - } - }; - } - - match_all! { - visit_register_peer(Peer), - visit_register_domain(Domain), - visit_register_account(Account), - visit_register_asset_definition(AssetDefinition), - visit_register_asset(Asset), - visit_register_role(Role), - visit_register_trigger(Trigger), + match isi { + RegisterBox::Peer(obj) => visitor.visit_register_peer(authority, obj), + RegisterBox::Domain(obj) => visitor.visit_register_domain(authority, obj), + RegisterBox::Account(obj) => visitor.visit_register_account(authority, obj), + RegisterBox::AssetDefinition(obj) => { + visitor.visit_register_asset_definition(authority, obj) + } + RegisterBox::Asset(obj) => visitor.visit_register_asset(authority, obj), + RegisterBox::Role(obj) => visitor.visit_register_role(authority, obj), + RegisterBox::Trigger(obj) => visitor.visit_register_trigger(authority, obj), } } pub fn visit_unregister( visitor: &mut V, authority: &AccountId, - isi: &UnregisterExpr, + isi: &UnregisterBox, ) { - macro_rules! match_all { - ( $( $visitor:ident($id:ident) ),+ $(,)? ) => { - let object_id = evaluate_expr!(visitor, authority, ::object_id()); - match object_id { $( - IdBox::$id(object_id) => visitor.$visitor(authority, Unregister{object_id}), )+ - _ => visitor.visit_unsupported(authority, isi), - } - }; - } - - match_all! { - visit_unregister_peer(PeerId), - visit_unregister_domain(DomainId), - visit_unregister_account(AccountId), - visit_unregister_asset_definition(AssetDefinitionId), - visit_unregister_asset(AssetId), - visit_unregister_role(RoleId), - visit_unregister_trigger(TriggerId), + match isi { + UnregisterBox::Peer(obj) => visitor.visit_unregister_peer(authority, obj), + UnregisterBox::Domain(obj) => visitor.visit_unregister_domain(authority, obj), + UnregisterBox::Account(obj) => visitor.visit_unregister_account(authority, obj), + UnregisterBox::AssetDefinition(obj) => { + visitor.visit_unregister_asset_definition(authority, obj) + } + UnregisterBox::Asset(obj) => visitor.visit_unregister_asset(authority, obj), + UnregisterBox::Role(obj) => visitor.visit_unregister_role(authority, obj), + UnregisterBox::Trigger(obj) => visitor.visit_unregister_trigger(authority, obj), } } -pub fn visit_mint(visitor: &mut V, authority: &AccountId, isi: &MintExpr) { - let destination_id = evaluate_expr!(visitor, authority, ::destination_id()); - let object = evaluate_expr!(visitor, authority, ::object()); - - match (destination_id, object) { - (IdBox::AssetId(destination_id), Value::Numeric(object)) => visitor.visit_mint_asset( - authority, - Mint { - object, - destination_id, - }, - ), - (IdBox::AccountId(destination_id), Value::PublicKey(object)) => visitor - .visit_mint_account_public_key( - authority, - Mint { - object, - destination_id, - }, - ), - (IdBox::AccountId(destination_id), Value::SignatureCheckCondition(object)) => visitor - .visit_mint_account_signature_check_condition( - authority, - Mint { - object, - destination_id, - }, - ), - (IdBox::TriggerId(destination_id), Value::Numeric(NumericValue::U32(object))) => visitor - .visit_mint_trigger_repetitions( - authority, - Mint { - object, - destination_id, - }, - ), - _ => visitor.visit_unsupported(authority, isi), +pub fn visit_mint(visitor: &mut V, authority: &AccountId, isi: &MintBox) { + match isi { + MintBox::Account(mint_account) => match mint_account { + AccountMintBox::PublicKey(obj) => visitor.visit_mint_account_public_key(authority, obj), + AccountMintBox::SignatureCheckCondition(obj) => { + visitor.visit_mint_account_signature_check_condition(authority, obj) + } + }, + MintBox::Asset(mint_asset) => match mint_asset { + AssetMintBox::Quantity(obj) => visitor.visit_mint_asset_quantity(authority, obj), + AssetMintBox::BigQuantity(obj) => visitor.visit_mint_asset_big_quantity(authority, obj), + AssetMintBox::Fixed(obj) => visitor.visit_mint_asset_fixed(authority, obj), + }, + MintBox::TriggerRepetitions(obj) => visitor.visit_mint_trigger_repetitions(authority, obj), } } -pub fn visit_burn(visitor: &mut V, authority: &AccountId, isi: &BurnExpr) { - let destination_id = evaluate_expr!(visitor, authority, ::destination_id()); - let object = evaluate_expr!(visitor, authority, ::object()); - - match (destination_id, object) { - (IdBox::AssetId(destination_id), Value::Numeric(object)) => visitor.visit_burn_asset( - authority, - Burn { - object, - destination_id, - }, - ), - (IdBox::AccountId(destination_id), Value::PublicKey(object)) => visitor - .visit_burn_account_public_key( - authority, - Burn { - object, - destination_id, - }, - ), - (IdBox::TriggerId(destination_id), Value::Numeric(NumericValue::U32(object))) => visitor - .visit_burn_trigger_repetitions( - authority, - Burn { - object, - destination_id, - }, - ), - _ => visitor.visit_unsupported(authority, isi), +pub fn visit_burn(visitor: &mut V, authority: &AccountId, isi: &BurnBox) { + match isi { + BurnBox::AccountPublicKey(obj) => visitor.visit_burn_account_public_key(authority, obj), + BurnBox::Asset(burn_asset) => match burn_asset { + AssetBurnBox::Quantity(obj) => visitor.visit_burn_asset_quantity(authority, obj), + AssetBurnBox::BigQuantity(obj) => visitor.visit_burn_asset_big_quantity(authority, obj), + AssetBurnBox::Fixed(obj) => visitor.visit_burn_asset_fixed(authority, obj), + }, + BurnBox::TriggerRepetitions(obj) => visitor.visit_burn_trigger_repetitions(authority, obj), } } pub fn visit_transfer( visitor: &mut V, authority: &AccountId, - isi: &TransferExpr, + isi: &TransferBox, ) { - let object = evaluate_expr!(visitor, authority, ::object()); - let source_id = evaluate_expr!(visitor, authority, ::source_id()); - let destination_id = evaluate_expr!(visitor, authority, ::destination_id()); - - match (source_id, object, destination_id) { - (IdBox::AssetId(source_id), Value::Numeric(object), IdBox::AccountId(destination_id)) => { - visitor.visit_transfer_asset( - authority, - Transfer { - source_id, - object, - destination_id, - }, - ); + match isi { + TransferBox::Domain(obj) => visitor.visit_transfer_domain(authority, obj), + TransferBox::AssetDefinition(obj) => { + visitor.visit_transfer_asset_definition(authority, obj) } - ( - IdBox::AccountId(source_id), - Value::Id(IdBox::AssetDefinitionId(object)), - IdBox::AccountId(destination_id), - ) => visitor.visit_transfer_asset_definition( - authority, - Transfer { - source_id, - object, - destination_id, - }, - ), - ( - IdBox::AccountId(source_id), - Value::Id(IdBox::DomainId(object)), - IdBox::AccountId(destination_id), - ) => visitor.visit_transfer_domain( - authority, - Transfer { - source_id, - object, - destination_id, - }, - ), - _ => visitor.visit_unsupported(authority, isi), + TransferBox::Asset(transfer_asset) => match transfer_asset { + AssetTransferBox::Quantity(obj) => { + visitor.visit_transfer_asset_quantity(authority, obj) + } + AssetTransferBox::BigQuantity(obj) => { + visitor.visit_transfer_asset_big_quantity(authority, obj) + } + AssetTransferBox::Fixed(obj) => visitor.visit_transfer_asset_fixed(authority, obj), + }, } } pub fn visit_set_key_value( visitor: &mut V, authority: &AccountId, - isi: &SetKeyValueExpr, + isi: &SetKeyValueBox, ) { - let object_id = evaluate_expr!(visitor, authority, ::object_id()); - let key = evaluate_expr!(visitor, authority, ::key()); - let value = evaluate_expr!(visitor, authority, ::value()); - - match object_id { - IdBox::AssetId(object_id) => visitor.visit_set_asset_key_value( - authority, - SetKeyValue { - object_id, - key, - value, - }, - ), - IdBox::AssetDefinitionId(object_id) => visitor.visit_set_asset_definition_key_value( - authority, - SetKeyValue { - object_id, - key, - value, - }, - ), - IdBox::AccountId(object_id) => visitor.visit_set_account_key_value( - authority, - SetKeyValue { - object_id, - key, - value, - }, - ), - IdBox::DomainId(object_id) => visitor.visit_set_domain_key_value( - authority, - SetKeyValue { - object_id, - key, - value, - }, - ), - _ => visitor.visit_unsupported(authority, isi), + match isi { + SetKeyValueBox::Domain(obj) => visitor.visit_set_domain_key_value(authority, obj), + SetKeyValueBox::Account(obj) => visitor.visit_set_account_key_value(authority, obj), + SetKeyValueBox::AssetDefinition(obj) => { + visitor.visit_set_asset_definition_key_value(authority, obj) + } + SetKeyValueBox::Asset(obj) => visitor.visit_set_asset_key_value(authority, obj), } } pub fn visit_remove_key_value( visitor: &mut V, authority: &AccountId, - isi: &RemoveKeyValueExpr, + isi: &RemoveKeyValueBox, ) { - let object_id = evaluate_expr!(visitor, authority, ::object_id()); - let key = evaluate_expr!(visitor, authority, ::key()); - - match object_id { - IdBox::AssetId(object_id) => { - visitor.visit_remove_asset_key_value(authority, RemoveKeyValue { object_id, key }); - } - IdBox::AssetDefinitionId(object_id) => visitor - .visit_remove_asset_definition_key_value(authority, RemoveKeyValue { object_id, key }), - IdBox::AccountId(object_id) => { - visitor.visit_remove_account_key_value(authority, RemoveKeyValue { object_id, key }); - } - IdBox::DomainId(object_id) => { - visitor.visit_remove_domain_key_value(authority, RemoveKeyValue { object_id, key }); - } - _ => visitor.visit_unsupported(authority, isi), - } -} - -pub fn visit_grant(visitor: &mut V, authority: &AccountId, isi: &GrantExpr) { - let destination_id = evaluate_expr!(visitor, authority, ::destination_id()); - let object = evaluate_expr!(visitor, authority, ::object()); - - match object { - Value::PermissionToken(object) => visitor.visit_grant_account_permission( - authority, - Grant { - object, - destination_id, - }, - ), - Value::Id(IdBox::RoleId(object)) => visitor.visit_grant_account_role( - authority, - Grant { - object, - destination_id, - }, - ), - _ => visitor.visit_unsupported(authority, isi), - } -} - -pub fn visit_revoke(visitor: &mut V, authority: &AccountId, isi: &RevokeExpr) { - let destination_id = evaluate_expr!(visitor, authority, ::destination_id()); - let object = evaluate_expr!(visitor, authority, ::object()); - - match object { - Value::PermissionToken(object) => visitor.visit_revoke_account_permission( - authority, - Revoke { - object, - destination_id, - }, - ), - Value::Id(IdBox::RoleId(object)) => visitor.visit_revoke_account_role( - authority, - Revoke { - object, - destination_id, - }, - ), - _ => visitor.visit_unsupported(authority, isi), - } -} - -pub fn visit_upgrade(visitor: &mut V, authority: &AccountId, isi: &UpgradeExpr) { - let object = evaluate_expr!(visitor, authority, ::object()); - - match object { - UpgradableBox::Executor(object) => { - visitor.visit_upgrade_executor(authority, Upgrade { object }); + match isi { + RemoveKeyValueBox::Domain(obj) => visitor.visit_remove_domain_key_value(authority, obj), + RemoveKeyValueBox::Account(obj) => visitor.visit_remove_account_key_value(authority, obj), + RemoveKeyValueBox::AssetDefinition(obj) => { + visitor.visit_remove_asset_definition_key_value(authority, obj) } + RemoveKeyValueBox::Asset(obj) => visitor.visit_remove_asset_key_value(authority, obj), } } -pub fn visit_if(visitor: &mut V, authority: &AccountId, isi: &ConditionalExpr) { - let condition = evaluate_expr!(visitor, authority, ::condition()); - - // TODO: Should visit both by default or not? It will affect Executor behavior - // because only one branch needs to be executed. IMO both should be validated - if condition { - visitor.visit_instruction(authority, isi.then()); - } else if let Some(otherwise) = isi.otherwise() { - visitor.visit_instruction(authority, otherwise); +pub fn visit_grant(visitor: &mut V, authority: &AccountId, isi: &GrantBox) { + match isi { + GrantBox::PermissionToken(obj) => visitor.visit_grant_account_permission(authority, obj), + GrantBox::Role(obj) => visitor.visit_grant_account_role(authority, obj), } } -pub fn visit_pair(visitor: &mut V, authority: &AccountId, isi: &PairExpr) { - visitor.visit_instruction(authority, isi.left_instruction()); - visitor.visit_instruction(authority, isi.right_instruction()); -} - -pub fn visit_sequence( - visitor: &mut V, - authority: &AccountId, - isi: &SequenceExpr, -) { - for instruction in isi.instructions() { - visitor.visit_instruction(authority, instruction); +pub fn visit_revoke(visitor: &mut V, authority: &AccountId, isi: &RevokeBox) { + match isi { + RevokeBox::PermissionToken(obj) => visitor.visit_revoke_account_permission(authority, obj), + RevokeBox::Role(obj) => visitor.visit_revoke_account_role(authority, obj), } } @@ -696,48 +410,54 @@ macro_rules! leaf_visitors { leaf_visitors! { // Instruction visitors - visit_register_account(Register), - visit_unregister_account(Unregister), - visit_mint_account_public_key(Mint), - visit_burn_account_public_key(Burn), - visit_mint_account_signature_check_condition(Mint), - visit_set_account_key_value(SetKeyValue), - visit_remove_account_key_value(RemoveKeyValue), - visit_register_asset(Register), - visit_unregister_asset(Unregister), - visit_mint_asset(Mint), - visit_burn_asset(Burn), - visit_transfer_asset(Transfer), - visit_set_asset_key_value(SetKeyValue), - visit_remove_asset_key_value(RemoveKeyValue), - visit_register_asset_definition(Register), - visit_unregister_asset_definition(Unregister), - visit_transfer_asset_definition(Transfer), - visit_set_asset_definition_key_value(SetKeyValue), - visit_remove_asset_definition_key_value(RemoveKeyValue), - visit_register_domain(Register), - visit_unregister_domain(Unregister), - visit_transfer_domain(Transfer), - visit_set_domain_key_value(SetKeyValue), - visit_remove_domain_key_value(RemoveKeyValue), - visit_register_peer(Register), - visit_unregister_peer(Unregister), - visit_grant_account_permission(Grant), - visit_revoke_account_permission(Revoke), - visit_register_role(Register), - visit_unregister_role(Unregister), - visit_grant_account_role(Grant), - visit_revoke_account_role(Revoke), - visit_register_trigger(Register>), - visit_unregister_trigger(Unregister>), - visit_mint_trigger_repetitions(Mint>), - visit_burn_trigger_repetitions(Burn>), - visit_upgrade_executor(Upgrade), - visit_new_parameter(NewParameter), - visit_set_parameter(SetParameter), - visit_execute_trigger(ExecuteTrigger), + visit_register_account(&Register), + visit_unregister_account(&Unregister), + visit_mint_account_public_key(&Mint), + visit_burn_account_public_key(&Burn), + visit_mint_account_signature_check_condition(&Mint), + visit_set_account_key_value(&SetKeyValue), + visit_remove_account_key_value(&RemoveKeyValue), + visit_register_asset(&Register), + visit_unregister_asset(&Unregister), + visit_mint_asset_quantity(&Mint), + visit_burn_asset_quantity(&Burn), + visit_mint_asset_big_quantity(&Mint), + visit_burn_asset_big_quantity(&Burn), + visit_mint_asset_fixed(&Mint), + visit_burn_asset_fixed(&Burn), + visit_transfer_asset_quantity(&Transfer), + visit_transfer_asset_big_quantity(&Transfer), + visit_transfer_asset_fixed(&Transfer), + visit_set_asset_key_value(&SetKeyValue), + visit_remove_asset_key_value(&RemoveKeyValue), + visit_register_asset_definition(&Register), + visit_unregister_asset_definition(&Unregister), + visit_transfer_asset_definition(&Transfer), + visit_set_asset_definition_key_value(&SetKeyValue), + visit_remove_asset_definition_key_value(&RemoveKeyValue), + visit_register_domain(&Register), + visit_unregister_domain(&Unregister), + visit_transfer_domain(&Transfer), + visit_set_domain_key_value(&SetKeyValue), + visit_remove_domain_key_value(&RemoveKeyValue), + visit_register_peer(&Register), + visit_unregister_peer(&Unregister), + visit_grant_account_permission(&Grant), + visit_revoke_account_permission(&Revoke), + visit_register_role(&Register), + visit_unregister_role(&Unregister), + visit_grant_account_role(&Grant), + visit_revoke_account_role(&Revoke), + visit_register_trigger(&Register>), + visit_unregister_trigger(&Unregister>), + visit_mint_trigger_repetitions(&Mint>), + visit_burn_trigger_repetitions(&Burn>), + visit_upgrade(&Upgrade), + visit_new_parameter(&NewParameter), + visit_set_parameter(&SetParameter), + visit_execute_trigger(&ExecuteTrigger), visit_fail(&Fail), - visit_log(Log), + visit_log(&Log), // Query visitors visit_find_account_by_id(&FindAccountById), @@ -752,7 +472,7 @@ leaf_visitors! { visit_find_all_block_headers(&FindAllBlockHeaders), visit_find_all_blocks(&FindAllBlocks), visit_find_all_domains(&FindAllDomains), - visit_find_all_parammeters(&FindAllParameters), + visit_find_all_parameters(&FindAllParameters), visit_find_all_peers(&FindAllPeers), visit_find_permission_token_schema(&FindPermissionTokenSchema), visit_find_all_role_ids(&FindAllRoleIds), diff --git a/data_model/tests/data_model.rs b/data_model/tests/data_model.rs index 09cf1d602ff..c795f7590a4 100644 --- a/data_model/tests/data_model.rs +++ b/data_model/tests/data_model.rs @@ -1,65 +1,14 @@ -use std::str::FromStr as _; - use iroha_data_model::{prelude::*, ParseError}; #[test] fn transfer_isi_should_be_valid() { - let _instruction = TransferExpr::new( - IdBox::AssetId("btc##seller@crypto".parse().expect("Valid")), + let _instruction = Transfer::asset_quantity( + "btc##seller@crypto".parse().expect("Valid"), 12_u32, - IdBox::AccountId("buyer@crypto".parse().expect("Valid")), + "buyer@crypto".parse().expect("Valid"), ); } -#[test] -fn find_quantity_and_check_it_greater_than_value_isi_should_be_valid() { - let asset_id: AssetId = "rose##alice@wonderland".parse().expect("Valid"); - let find_asset = QueryBox::from(FindAssetQuantityById::new(asset_id)); - - let _instruction = ConditionalExpr::new( - Not::new(Greater::new(EvaluatesTo::new_unchecked(find_asset), 10_u32)), - Fail::new("rate is less or equal to value"), - ); -} - -struct FindRateAndCheckItGreaterThanValue { - from_currency: String, - to_currency: String, - value: u32, -} - -impl FindRateAndCheckItGreaterThanValue { - pub fn new(from_currency: &str, to_currency: &str, value: u32) -> Self { - Self { - from_currency: from_currency.to_string(), - to_currency: to_currency.to_string(), - value, - } - } - - pub fn into_isi(self) -> ConditionalExpr { - ConditionalExpr::new( - Not::new(Greater::new( - EvaluatesTo::new_unchecked(QueryBox::from(FindAssetQuantityById::new( - AssetId::new( - format!("{}2{}_rate#exchange", self.from_currency, self.to_currency) - .parse() - .expect("Valid"), - AccountId::from_str("dex@exchange").expect("Valid"), - ), - ))), - self.value, - )), - Fail::new("rate is less or equal to value"), - ) - } -} - -#[test] -fn find_rate_and_check_it_greater_than_value_predefined_isi_should_be_valid() { - let _instruction = FindRateAndCheckItGreaterThanValue::new("btc", "eth", 10).into_isi(); -} - #[test] fn account_id_parsing() -> Result<(), ParseError> { // `AccountId` should have format `name@domain_name` diff --git a/data_model/tests/ui.rs b/data_model/tests/ui.rs deleted file mode 100644 index 48e1f1bbea3..00000000000 --- a/data_model/tests/ui.rs +++ /dev/null @@ -1,7 +0,0 @@ -#![cfg(not(coverage))] -use trybuild::TestCases; - -#[test] -fn ui() { - TestCases::new().compile_fail("tests/ui_fail/*.rs"); -} diff --git a/data_model/tests/ui_fail/evaluates_to.rs b/data_model/tests/ui_fail/evaluates_to.rs deleted file mode 100644 index 6694b013653..00000000000 --- a/data_model/tests/ui_fail/evaluates_to.rs +++ /dev/null @@ -1,12 +0,0 @@ -//! This test ensures that [`EvaluatesTo`] provides compile-time strong typing - -use iroha_data_model::prelude::*; - -fn get_assets_by_account_id(_account_id: impl Into>) -> Vec { - Vec::new() -} - -fn main() { - let asset_definition_id: AssetDefinitionId = "rose#wonderland".parse().unwrap(); - get_assets_by_account_id(asset_definition_id); -} diff --git a/data_model/tests/ui_fail/evaluates_to.stderr b/data_model/tests/ui_fail/evaluates_to.stderr deleted file mode 100644 index ff7da36be16..00000000000 --- a/data_model/tests/ui_fail/evaluates_to.stderr +++ /dev/null @@ -1,17 +0,0 @@ -error[E0277]: the trait bound `iroha_data_model::account::AccountId: From` is not satisfied - --> tests/ui_fail/evaluates_to.rs:11:30 - | -11 | get_assets_by_account_id(asset_definition_id); - | ------------------------ ^^^^^^^^^^^^^^^^^^^ the trait `From` is not implemented for `iroha_data_model::account::AccountId` - | | - | required by a bound introduced by this call - | - = note: required for `iroha_data_model::asset::AssetDefinitionId` to implement `Into` - = note: required for `iroha_data_model::expression::EvaluatesTo` to implement `From` - = note: 1 redundant requirement hidden - = note: required for `iroha_data_model::asset::AssetDefinitionId` to implement `Into>` -note: required by a bound in `get_assets_by_account_id` - --> tests/ui_fail/evaluates_to.rs:5:47 - | -5 | fn get_assets_by_account_id(_account_id: impl Into>) -> Vec { - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `get_assets_by_account_id` diff --git a/default_executor/src/lib.rs b/default_executor/src/lib.rs index 4834bcc9228..6214fdaf03b 100644 --- a/default_executor/src/lib.rs +++ b/default_executor/src/lib.rs @@ -8,7 +8,7 @@ extern crate panic_halt; use alloc::borrow::ToOwned as _; -use iroha_executor::{default::default_permission_token_schema, prelude::*, smart_contract}; +use iroha_executor::{default::default_permission_token_schema, prelude::*}; use lol_alloc::{FreeListAllocator, LockedAllocator}; #[global_allocator] @@ -19,11 +19,10 @@ static ALLOC: LockedAllocator = LockedAllocator::new(FreeList /// # Warning /// /// The defaults are not guaranteed to be stable. -#[derive(Clone, Constructor, Debug, ValidateEntrypoints, ExpressionEvaluator, Validate, Visit)] +#[derive(Debug, Clone, Constructor, Visit, Validate, ValidateEntrypoints)] pub struct Executor { verdict: Result, block_height: u64, - host: smart_contract::Host, } impl Executor { diff --git a/docs/source/references/schema.json b/docs/source/references/schema.json index 756d22082be..a4640d1122e 100644 --- a/docs/source/references/schema.json +++ b/docs/source/references/schema.json @@ -155,6 +155,20 @@ } ] }, + "AccountMintBox": { + "Enum": [ + { + "tag": "PublicKey", + "discriminant": 0, + "type": "Mint" + }, + { + "tag": "SignatureCheckCondition", + "discriminant": 1, + "type": "Mint" + } + ] + }, "AccountPermissionChanged": { "Struct": [ { @@ -203,18 +217,6 @@ } ] }, - "Add": { - "Struct": [ - { - "name": "left", - "type": "EvaluatesTo" - }, - { - "name": "right", - "type": "EvaluatesTo" - } - ] - }, "Algorithm": { "Enum": [ { @@ -235,18 +237,6 @@ } ] }, - "And": { - "Struct": [ - { - "name": "left", - "type": "EvaluatesTo" - }, - { - "name": "right", - "type": "EvaluatesTo" - } - ] - }, "Array, 8>": { "Array": { "type": "Interval", @@ -289,6 +279,25 @@ } ] }, + "AssetBurnBox": { + "Enum": [ + { + "tag": "Quantity", + "discriminant": 0, + "type": "Burn" + }, + { + "tag": "BigQuantity", + "discriminant": 1, + "type": "Burn" + }, + { + "tag": "Fixed", + "discriminant": 2, + "type": "Burn" + } + ] + }, "AssetChanged": { "Struct": [ { @@ -534,6 +543,44 @@ } ] }, + "AssetMintBox": { + "Enum": [ + { + "tag": "Quantity", + "discriminant": 0, + "type": "Mint" + }, + { + "tag": "BigQuantity", + "discriminant": 1, + "type": "Mint" + }, + { + "tag": "Fixed", + "discriminant": 2, + "type": "Mint" + } + ] + }, + "AssetTransferBox": { + "Enum": [ + { + "tag": "Quantity", + "discriminant": 0, + "type": "Transfer" + }, + { + "tag": "BigQuantity", + "discriminant": 1, + "type": "Transfer" + }, + { + "tag": "Fixed", + "discriminant": 2, + "type": "Transfer" + } + ] + }, "AssetValue": { "Enum": [ { @@ -632,18 +679,6 @@ } ] }, - "BinaryOpIncompatibleNumericValueTypesError": { - "Struct": [ - { - "name": "left", - "type": "NumericValue" - }, - { - "name": "right", - "type": "NumericValue" - } - ] - }, "BlockHeader": { "Struct": [ { @@ -702,31 +737,82 @@ ] }, "BlockSubscriptionRequest": "NonZero", - "BurnExpr": { + "Burn": { + "Struct": [ + { + "name": "object", + "type": "Fixed" + }, + { + "name": "destination_id", + "type": "AssetId" + } + ] + }, + "Burn": { + "Struct": [ + { + "name": "object", + "type": "PublicKey" + }, + { + "name": "destination_id", + "type": "AccountId" + } + ] + }, + "Burn": { + "Struct": [ + { + "name": "object", + "type": "u128" + }, + { + "name": "destination_id", + "type": "AssetId" + } + ] + }, + "Burn": { "Struct": [ { "name": "object", - "type": "EvaluatesTo" + "type": "u32" }, { "name": "destination_id", - "type": "EvaluatesTo" + "type": "AssetId" } ] }, - "ConditionalExpr": { + "Burn>": { "Struct": [ { - "name": "condition", - "type": "EvaluatesTo" + "name": "object", + "type": "u32" + }, + { + "name": "destination_id", + "type": "TriggerId" + } + ] + }, + "BurnBox": { + "Enum": [ + { + "tag": "AccountPublicKey", + "discriminant": 0, + "type": "Burn" }, { - "name": "then", - "type": "InstructionExpr" + "tag": "Asset", + "discriminant": 1, + "type": "AssetBurnBox" }, { - "name": "otherwise", - "type": "Option" + "tag": "TriggerRepetitions", + "discriminant": 2, + "type": "Burn>" } ] }, @@ -778,50 +864,6 @@ } ] }, - "Contains": { - "Struct": [ - { - "name": "collection", - "type": "EvaluatesTo>" - }, - { - "name": "element", - "type": "EvaluatesTo" - } - ] - }, - "ContainsAll": { - "Struct": [ - { - "name": "collection", - "type": "EvaluatesTo>" - }, - { - "name": "elements", - "type": "EvaluatesTo>" - } - ] - }, - "ContainsAny": { - "Struct": [ - { - "name": "collection", - "type": "EvaluatesTo>" - }, - { - "name": "elements", - "type": "EvaluatesTo>" - } - ] - }, - "ContextValue": { - "Struct": [ - { - "name": "value_name", - "type": "Name" - } - ] - }, "DataEntityFilter": { "Enum": [ { @@ -915,18 +957,6 @@ } ] }, - "Divide": { - "Struct": [ - { - "name": "left", - "type": "EvaluatesTo" - }, - { - "name": "right", - "type": "EvaluatesTo" - } - ] - }, "Domain": { "Struct": [ { @@ -1070,423 +1100,131 @@ "u32" ] }, - "Equal": { - "Struct": [ + "Event": { + "Enum": [ { - "name": "left", - "type": "EvaluatesTo" + "tag": "Pipeline", + "discriminant": 0, + "type": "PipelineEvent" }, { - "name": "right", - "type": "EvaluatesTo" - } - ] - }, - "EvaluatesTo": { - "Struct": [ + "tag": "Data", + "discriminant": 1, + "type": "DataEvent" + }, { - "name": "expression", - "type": "Expression" - } - ] - }, - "EvaluatesTo": { - "Struct": [ + "tag": "Time", + "discriminant": 2, + "type": "TimeEvent" + }, { - "name": "expression", - "type": "Expression" - } - ] - }, - "EvaluatesTo": { - "Struct": [ + "tag": "ExecuteTrigger", + "discriminant": 3, + "type": "ExecuteTriggerEvent" + }, { - "name": "expression", - "type": "Expression" + "tag": "Notification", + "discriminant": 4, + "type": "NotificationEvent" } ] }, - "EvaluatesTo": { - "Struct": [ + "EventMessage": "Event", + "EventSubscriptionRequest": "FilterBox", + "Executable": { + "Enum": [ + { + "tag": "Instructions", + "discriminant": 0, + "type": "Vec" + }, { - "name": "expression", - "type": "Expression" + "tag": "Wasm", + "discriminant": 1, + "type": "WasmSmartContract" } ] }, - "EvaluatesTo>": { + "ExecuteTrigger": { "Struct": [ { - "name": "expression", - "type": "Expression" + "name": "trigger_id", + "type": "TriggerId" } ] }, - "EvaluatesTo>": { + "ExecuteTriggerEvent": { "Struct": [ { - "name": "expression", - "type": "Expression" + "name": "trigger_id", + "type": "TriggerId" + }, + { + "name": "authority", + "type": "AccountId" } ] }, - "EvaluatesTo": { + "ExecuteTriggerEventFilter": { "Struct": [ { - "name": "expression", - "type": "Expression" + "name": "trigger_id", + "type": "TriggerId" + }, + { + "name": "authority", + "type": "AccountId" } ] }, - "EvaluatesTo": { - "Struct": [ + "ExecutionTime": { + "Enum": [ + { + "tag": "PreCommit", + "discriminant": 0 + }, { - "name": "expression", - "type": "Expression" + "tag": "Schedule", + "discriminant": 1, + "type": "Schedule" } ] }, - "EvaluatesTo": { + "Executor": { "Struct": [ { - "name": "expression", - "type": "Expression" + "name": "wasm", + "type": "WasmSmartContract" } ] }, - "EvaluatesTo": { - "Struct": [ + "ExecutorEvent": { + "Enum": [ { - "name": "expression", - "type": "Expression" + "tag": "Upgraded", + "discriminant": 0 } ] }, - "EvaluatesTo": { - "Struct": [ + "ExecutorMode": { + "Enum": [ + { + "tag": "Path", + "discriminant": 0, + "type": "String" + }, { - "name": "expression", - "type": "Expression" + "tag": "Inline", + "discriminant": 1, + "type": "Executor" } ] }, - "EvaluatesTo": { + "Fail": { "Struct": [ { - "name": "expression", - "type": "Expression" - } - ] - }, - "EvaluatesTo": { - "Struct": [ - { - "name": "expression", - "type": "Expression" - } - ] - }, - "EvaluatesTo": { - "Struct": [ - { - "name": "expression", - "type": "Expression" - } - ] - }, - "EvaluatesTo": { - "Struct": [ - { - "name": "expression", - "type": "Expression" - } - ] - }, - "EvaluatesTo": { - "Struct": [ - { - "name": "expression", - "type": "Expression" - } - ] - }, - "EvaluatesTo": { - "Struct": [ - { - "name": "expression", - "type": "Expression" - } - ] - }, - "EvaluatesTo>": { - "Struct": [ - { - "name": "expression", - "type": "Expression" - } - ] - }, - "EvaluatesTo": { - "Struct": [ - { - "name": "expression", - "type": "Expression" - } - ] - }, - "EvaluationError": { - "Enum": [ - { - "tag": "Math", - "discriminant": 0, - "type": "MathError" - }, - { - "tag": "Validation", - "discriminant": 1, - "type": "ValidationFail" - }, - { - "tag": "Find", - "discriminant": 2, - "type": "String" - }, - { - "tag": "Conversion", - "discriminant": 3, - "type": "String" - } - ] - }, - "Event": { - "Enum": [ - { - "tag": "Pipeline", - "discriminant": 0, - "type": "PipelineEvent" - }, - { - "tag": "Data", - "discriminant": 1, - "type": "DataEvent" - }, - { - "tag": "Time", - "discriminant": 2, - "type": "TimeEvent" - }, - { - "tag": "ExecuteTrigger", - "discriminant": 3, - "type": "ExecuteTriggerEvent" - }, - { - "tag": "Notification", - "discriminant": 4, - "type": "NotificationEvent" - } - ] - }, - "EventMessage": "Event", - "EventSubscriptionRequest": "FilterBox", - "Executable": { - "Enum": [ - { - "tag": "Instructions", - "discriminant": 0, - "type": "Vec" - }, - { - "tag": "Wasm", - "discriminant": 1, - "type": "WasmSmartContract" - } - ] - }, - "ExecuteTriggerEvent": { - "Struct": [ - { - "name": "trigger_id", - "type": "TriggerId" - }, - { - "name": "authority", - "type": "AccountId" - } - ] - }, - "ExecuteTriggerEventFilter": { - "Struct": [ - { - "name": "trigger_id", - "type": "TriggerId" - }, - { - "name": "authority", - "type": "AccountId" - } - ] - }, - "ExecuteTriggerExpr": { - "Struct": [ - { - "name": "trigger_id", - "type": "EvaluatesTo" - } - ] - }, - "ExecutionTime": { - "Enum": [ - { - "tag": "PreCommit", - "discriminant": 0 - }, - { - "tag": "Schedule", - "discriminant": 1, - "type": "Schedule" - } - ] - }, - "Executor": { - "Struct": [ - { - "name": "wasm", - "type": "WasmSmartContract" - } - ] - }, - "ExecutorEvent": { - "Enum": [ - { - "tag": "Upgraded", - "discriminant": 0 - } - ] - }, - "ExecutorMode": { - "Enum": [ - { - "tag": "Path", - "discriminant": 0, - "type": "String" - }, - { - "tag": "Inline", - "discriminant": 1, - "type": "Executor" - } - ] - }, - "Expression": { - "Enum": [ - { - "tag": "Add", - "discriminant": 0, - "type": "Add" - }, - { - "tag": "Subtract", - "discriminant": 1, - "type": "Subtract" - }, - { - "tag": "Multiply", - "discriminant": 2, - "type": "Multiply" - }, - { - "tag": "Divide", - "discriminant": 3, - "type": "Divide" - }, - { - "tag": "Mod", - "discriminant": 4, - "type": "Mod" - }, - { - "tag": "RaiseTo", - "discriminant": 5, - "type": "RaiseTo" - }, - { - "tag": "Greater", - "discriminant": 6, - "type": "Greater" - }, - { - "tag": "Less", - "discriminant": 7, - "type": "Less" - }, - { - "tag": "Equal", - "discriminant": 8, - "type": "Equal" - }, - { - "tag": "Not", - "discriminant": 9, - "type": "Not" - }, - { - "tag": "And", - "discriminant": 10, - "type": "And" - }, - { - "tag": "Or", - "discriminant": 11, - "type": "Or" - }, - { - "tag": "If", - "discriminant": 12, - "type": "If" - }, - { - "tag": "Raw", - "discriminant": 13, - "type": "Value" - }, - { - "tag": "Query", - "discriminant": 14, - "type": "QueryBox" - }, - { - "tag": "Contains", - "discriminant": 15, - "type": "Contains" - }, - { - "tag": "ContainsAll", - "discriminant": 16, - "type": "ContainsAll" - }, - { - "tag": "ContainsAny", - "discriminant": 17, - "type": "ContainsAny" - }, - { - "tag": "Where", - "discriminant": 18, - "type": "Where" - }, - { - "tag": "ContextValue", - "discriminant": 19, - "type": "ContextValue" - } - ] - }, - "Fail": { - "Struct": [ - { - "name": "message", - "type": "String" + "name": "message", + "type": "String" } ] }, @@ -1809,7 +1547,7 @@ "Struct": [ { "name": "id", - "type": "EvaluatesTo" + "type": "AccountId" } ] }, @@ -1817,11 +1555,11 @@ "Struct": [ { "name": "id", - "type": "EvaluatesTo" + "type": "AccountId" }, { "name": "key", - "type": "EvaluatesTo" + "type": "Name" } ] }, @@ -1829,7 +1567,7 @@ "Struct": [ { "name": "domain_id", - "type": "EvaluatesTo" + "type": "DomainId" } ] }, @@ -1837,7 +1575,7 @@ "Struct": [ { "name": "name", - "type": "EvaluatesTo" + "type": "Name" } ] }, @@ -1845,7 +1583,7 @@ "Struct": [ { "name": "asset_definition_id", - "type": "EvaluatesTo" + "type": "AssetDefinitionId" } ] }, @@ -1865,7 +1603,7 @@ "Struct": [ { "name": "id", - "type": "EvaluatesTo" + "type": "AssetId" } ] }, @@ -1873,7 +1611,7 @@ "Struct": [ { "name": "id", - "type": "EvaluatesTo" + "type": "AssetDefinitionId" } ] }, @@ -1881,11 +1619,11 @@ "Struct": [ { "name": "id", - "type": "EvaluatesTo" + "type": "AssetDefinitionId" }, { "name": "key", - "type": "EvaluatesTo" + "type": "Name" } ] }, @@ -1893,11 +1631,11 @@ "Struct": [ { "name": "id", - "type": "EvaluatesTo" + "type": "AssetId" }, { "name": "key", - "type": "EvaluatesTo" + "type": "Name" } ] }, @@ -1905,7 +1643,7 @@ "Struct": [ { "name": "id", - "type": "EvaluatesTo" + "type": "AssetId" } ] }, @@ -1913,7 +1651,7 @@ "Struct": [ { "name": "account_id", - "type": "EvaluatesTo" + "type": "AccountId" } ] }, @@ -1921,7 +1659,7 @@ "Struct": [ { "name": "asset_definition_id", - "type": "EvaluatesTo" + "type": "AssetDefinitionId" } ] }, @@ -1929,7 +1667,7 @@ "Struct": [ { "name": "domain_id", - "type": "EvaluatesTo" + "type": "DomainId" } ] }, @@ -1937,11 +1675,11 @@ "Struct": [ { "name": "domain_id", - "type": "EvaluatesTo" + "type": "DomainId" }, { "name": "asset_definition_id", - "type": "EvaluatesTo" + "type": "AssetDefinitionId" } ] }, @@ -1949,7 +1687,7 @@ "Struct": [ { "name": "name", - "type": "EvaluatesTo" + "type": "Name" } ] }, @@ -1957,7 +1695,7 @@ "Struct": [ { "name": "hash", - "type": "EvaluatesTo>" + "type": "HashOf" } ] }, @@ -1965,7 +1703,7 @@ "Struct": [ { "name": "id", - "type": "EvaluatesTo" + "type": "DomainId" } ] }, @@ -1973,11 +1711,11 @@ "Struct": [ { "name": "id", - "type": "EvaluatesTo" + "type": "DomainId" }, { "name": "key", - "type": "EvaluatesTo" + "type": "Name" } ] }, @@ -2055,7 +1793,7 @@ "Struct": [ { "name": "id", - "type": "EvaluatesTo" + "type": "AccountId" } ] }, @@ -2063,7 +1801,7 @@ "Struct": [ { "name": "id", - "type": "EvaluatesTo" + "type": "RoleId" } ] }, @@ -2071,7 +1809,7 @@ "Struct": [ { "name": "id", - "type": "EvaluatesTo" + "type": "AccountId" } ] }, @@ -2079,7 +1817,7 @@ "Struct": [ { "name": "id", - "type": "EvaluatesTo" + "type": "AssetDefinitionId" } ] }, @@ -2087,7 +1825,7 @@ "Struct": [ { "name": "hash", - "type": "EvaluatesTo>" + "type": "HashOf" } ] }, @@ -2095,7 +1833,7 @@ "Struct": [ { "name": "account_id", - "type": "EvaluatesTo" + "type": "AccountId" } ] }, @@ -2103,7 +1841,7 @@ "Struct": [ { "name": "id", - "type": "EvaluatesTo" + "type": "TriggerId" } ] }, @@ -2111,11 +1849,11 @@ "Struct": [ { "name": "id", - "type": "EvaluatesTo" + "type": "TriggerId" }, { "name": "key", - "type": "EvaluatesTo" + "type": "Name" } ] }, @@ -2123,7 +1861,7 @@ "Struct": [ { "name": "domain_id", - "type": "EvaluatesTo" + "type": "DomainId" } ] }, @@ -2170,27 +1908,41 @@ } ] }, - "GrantExpr": { + "Grant": { "Struct": [ { "name": "object", - "type": "EvaluatesTo" + "type": "PermissionToken" }, { "name": "destination_id", - "type": "EvaluatesTo" + "type": "AccountId" } ] }, - "Greater": { + "Grant": { "Struct": [ { - "name": "left", - "type": "EvaluatesTo" + "name": "object", + "type": "RoleId" }, { - "name": "right", - "type": "EvaluatesTo" + "name": "destination_id", + "type": "AccountId" + } + ] + }, + "GrantBox": { + "Enum": [ + { + "tag": "PermissionToken", + "discriminant": 0, + "type": "Grant" + }, + { + "tag": "Role", + "discriminant": 1, + "type": "Grant" } ] }, @@ -2291,76 +2043,134 @@ { "tag": "Domain", "discriminant": 5, - "type": "Domain" + "type": "Domain" + }, + { + "tag": "Account", + "discriminant": 6, + "type": "Account" + }, + { + "tag": "AssetDefinition", + "discriminant": 7, + "type": "AssetDefinition" + }, + { + "tag": "Asset", + "discriminant": 8, + "type": "Asset" + }, + { + "tag": "Trigger", + "discriminant": 9, + "type": "Trigger" + }, + { + "tag": "Role", + "discriminant": 10, + "type": "Role" + }, + { + "tag": "Parameter", + "discriminant": 11, + "type": "Parameter" + } + ] + }, + "InstructionBox": { + "Enum": [ + { + "tag": "Register", + "discriminant": 0, + "type": "RegisterBox" + }, + { + "tag": "Unregister", + "discriminant": 1, + "type": "UnregisterBox" + }, + { + "tag": "Mint", + "discriminant": 2, + "type": "MintBox" + }, + { + "tag": "Burn", + "discriminant": 3, + "type": "BurnBox" + }, + { + "tag": "Transfer", + "discriminant": 4, + "type": "TransferBox" + }, + { + "tag": "SetKeyValue", + "discriminant": 5, + "type": "SetKeyValueBox" }, { - "tag": "Account", + "tag": "RemoveKeyValue", "discriminant": 6, - "type": "Account" + "type": "RemoveKeyValueBox" }, { - "tag": "AssetDefinition", + "tag": "Grant", "discriminant": 7, - "type": "AssetDefinition" + "type": "GrantBox" }, { - "tag": "Asset", + "tag": "Revoke", "discriminant": 8, - "type": "Asset" + "type": "RevokeBox" }, { - "tag": "Trigger", + "tag": "ExecuteTrigger", "discriminant": 9, - "type": "Trigger" + "type": "ExecuteTrigger" }, { - "tag": "Role", + "tag": "SetParameter", "discriminant": 10, - "type": "Role" + "type": "SetParameter" }, { - "tag": "Parameter", + "tag": "NewParameter", "discriminant": 11, - "type": "Parameter" - } - ] - }, - "If": { - "Struct": [ + "type": "NewParameter" + }, { - "name": "condition", - "type": "EvaluatesTo" + "tag": "Upgrade", + "discriminant": 12, + "type": "Upgrade" }, { - "name": "then", - "type": "EvaluatesTo" + "tag": "Log", + "discriminant": 13, + "type": "Log" }, { - "name": "otherwise", - "type": "EvaluatesTo" + "tag": "Fail", + "discriminant": 14, + "type": "Fail" } ] }, "InstructionEvaluationError": { "Enum": [ - { - "tag": "Expression", - "discriminant": 0, - "type": "EvaluationError" - }, { "tag": "Unsupported", - "discriminant": 1, + "discriminant": 0, "type": "InstructionType" }, { "tag": "PermissionParameter", - "discriminant": 2, + "discriminant": 1, "type": "String" }, { "tag": "Type", - "discriminant": 3, + "discriminant": 2, "type": "TypeError" } ] @@ -2428,7 +2238,7 @@ "Struct": [ { "name": "instruction", - "type": "InstructionExpr" + "type": "InstructionBox" }, { "name": "reason", @@ -2436,100 +2246,6 @@ } ] }, - "InstructionExpr": { - "Enum": [ - { - "tag": "Register", - "discriminant": 0, - "type": "RegisterExpr" - }, - { - "tag": "Unregister", - "discriminant": 1, - "type": "UnregisterExpr" - }, - { - "tag": "Mint", - "discriminant": 2, - "type": "MintExpr" - }, - { - "tag": "Burn", - "discriminant": 3, - "type": "BurnExpr" - }, - { - "tag": "Transfer", - "discriminant": 4, - "type": "TransferExpr" - }, - { - "tag": "If", - "discriminant": 5, - "type": "ConditionalExpr" - }, - { - "tag": "Pair", - "discriminant": 6, - "type": "PairExpr" - }, - { - "tag": "Sequence", - "discriminant": 7, - "type": "SequenceExpr" - }, - { - "tag": "SetKeyValue", - "discriminant": 8, - "type": "SetKeyValueExpr" - }, - { - "tag": "RemoveKeyValue", - "discriminant": 9, - "type": "RemoveKeyValueExpr" - }, - { - "tag": "Grant", - "discriminant": 10, - "type": "GrantExpr" - }, - { - "tag": "Revoke", - "discriminant": 11, - "type": "RevokeExpr" - }, - { - "tag": "ExecuteTrigger", - "discriminant": 12, - "type": "ExecuteTriggerExpr" - }, - { - "tag": "SetParameter", - "discriminant": 13, - "type": "SetParameterExpr" - }, - { - "tag": "NewParameter", - "discriminant": 14, - "type": "NewParameterExpr" - }, - { - "tag": "Upgrade", - "discriminant": 15, - "type": "UpgradeExpr" - }, - { - "tag": "Log", - "discriminant": 16, - "type": "LogExpr" - }, - { - "tag": "Fail", - "discriminant": 17, - "type": "Fail" - } - ] - }, "InstructionType": { "Enum": [ { @@ -2552,57 +2268,45 @@ "tag": "Transfer", "discriminant": 4 }, - { - "tag": "If", - "discriminant": 5 - }, - { - "tag": "Pair", - "discriminant": 6 - }, - { - "tag": "Sequence", - "discriminant": 7 - }, { "tag": "SetKeyValue", - "discriminant": 8 + "discriminant": 5 }, { "tag": "RemoveKeyValue", - "discriminant": 9 + "discriminant": 6 }, { "tag": "Grant", - "discriminant": 10 + "discriminant": 7 }, { "tag": "Revoke", - "discriminant": 11 + "discriminant": 8 }, { "tag": "ExecuteTrigger", - "discriminant": 12 + "discriminant": 9 }, { "tag": "SetParameter", - "discriminant": 13 + "discriminant": 10 }, { "tag": "NewParameter", - "discriminant": 14 + "discriminant": 11 }, { "tag": "Upgrade", - "discriminant": 15 + "discriminant": 12 }, { "tag": "Log", - "discriminant": 16 + "discriminant": 13 }, { "tag": "Fail", - "discriminant": 17 + "discriminant": 14 } ] }, @@ -2660,18 +2364,6 @@ } ] }, - "Less": { - "Struct": [ - { - "name": "left", - "type": "EvaluatesTo" - }, - { - "name": "right", - "type": "EvaluatesTo" - } - ] - }, "Level": { "Enum": [ { @@ -2708,15 +2400,15 @@ } ] }, - "LogExpr": { + "Log": { "Struct": [ { "name": "level", - "type": "EvaluatesTo" + "type": "Level" }, { "name": "msg", - "type": "EvaluatesTo" + "type": "String" } ] }, @@ -2746,14 +2438,9 @@ "tag": "Unknown", "discriminant": 5 }, - { - "tag": "BinaryOpIncompatibleNumericValueTypes", - "discriminant": 6, - "type": "BinaryOpIncompatibleNumericValueTypesError" - }, { "tag": "FixedPointConversion", - "discriminant": 7, + "discriminant": 6, "type": "String" } ] @@ -2801,75 +2488,154 @@ } ] }, - "MetadataChanged": { + "MetadataChanged": { + "Struct": [ + { + "name": "target_id", + "type": "AssetId" + }, + { + "name": "key", + "type": "Name" + }, + { + "name": "value", + "type": "Value" + } + ] + }, + "MetadataChanged": { + "Struct": [ + { + "name": "target_id", + "type": "DomainId" + }, + { + "name": "key", + "type": "Name" + }, + { + "name": "value", + "type": "Value" + } + ] + }, + "MetadataError": { + "Enum": [ + { + "tag": "EntryTooBig", + "discriminant": 0, + "type": "SizeError" + }, + { + "tag": "OverallSize", + "discriminant": 1, + "type": "SizeError" + }, + { + "tag": "EmptyPath", + "discriminant": 2 + }, + { + "tag": "MissingSegment", + "discriminant": 3, + "type": "Name" + }, + { + "tag": "InvalidSegment", + "discriminant": 4, + "type": "Name" + } + ] + }, + "Mint": { + "Struct": [ + { + "name": "object", + "type": "Fixed" + }, + { + "name": "destination_id", + "type": "AssetId" + } + ] + }, + "Mint": { + "Struct": [ + { + "name": "object", + "type": "PublicKey" + }, + { + "name": "destination_id", + "type": "AccountId" + } + ] + }, + "Mint": { + "Struct": [ + { + "name": "object", + "type": "SignatureCheckCondition" + }, + { + "name": "destination_id", + "type": "AccountId" + } + ] + }, + "Mint": { + "Struct": [ + { + "name": "object", + "type": "u128" + }, + { + "name": "destination_id", + "type": "AssetId" + } + ] + }, + "Mint": { "Struct": [ { - "name": "target_id", - "type": "AssetId" - }, - { - "name": "key", - "type": "Name" + "name": "object", + "type": "u32" }, { - "name": "value", - "type": "Value" + "name": "destination_id", + "type": "AssetId" } ] }, - "MetadataChanged": { + "Mint>": { "Struct": [ { - "name": "target_id", - "type": "DomainId" - }, - { - "name": "key", - "type": "Name" + "name": "object", + "type": "u32" }, { - "name": "value", - "type": "Value" + "name": "destination_id", + "type": "TriggerId" } ] }, - "MetadataError": { + "MintBox": { "Enum": [ { - "tag": "EntryTooBig", + "tag": "Account", "discriminant": 0, - "type": "SizeError" + "type": "AccountMintBox" }, { - "tag": "OverallSize", + "tag": "Asset", "discriminant": 1, - "type": "SizeError" - }, - { - "tag": "EmptyPath", - "discriminant": 2 - }, - { - "tag": "MissingSegment", - "discriminant": 3, - "type": "Name" - }, - { - "tag": "InvalidSegment", - "discriminant": 4, - "type": "Name" - } - ] - }, - "MintExpr": { - "Struct": [ - { - "name": "object", - "type": "EvaluatesTo" + "type": "AssetMintBox" }, { - "name": "destination_id", - "type": "EvaluatesTo" + "tag": "TriggerRepetitions", + "discriminant": 2, + "type": "Mint>" } ] }, @@ -2937,30 +2703,6 @@ } ] }, - "Mod": { - "Struct": [ - { - "name": "left", - "type": "EvaluatesTo" - }, - { - "name": "right", - "type": "EvaluatesTo" - } - ] - }, - "Multiply": { - "Struct": [ - { - "name": "left", - "type": "EvaluatesTo" - }, - { - "name": "right", - "type": "EvaluatesTo" - } - ] - }, "Name": "String", "NewAccount": { "Struct": [ @@ -3018,11 +2760,11 @@ } ] }, - "NewParameterExpr": { + "NewParameter": { "Struct": [ { "name": "parameter", - "type": "EvaluatesTo" + "type": "Parameter" } ] }, @@ -3037,14 +2779,6 @@ "NonTrivial>": "Vec>", "NonZero": "u32", "NonZero": "u64", - "Not": { - "Struct": [ - { - "name": "expression", - "type": "EvaluatesTo" - } - ] - }, "NotificationEvent": { "Enum": [ { @@ -3106,9 +2840,6 @@ "Option>": { "Option": "HashOf" }, - "Option": { - "Option": "InstructionExpr" - }, "Option": { "Option": "IpfsPath" }, @@ -3139,18 +2870,6 @@ "Option": { "Option": "TriggerId" }, - "Or": { - "Struct": [ - { - "name": "left", - "type": "EvaluatesTo" - }, - { - "name": "right", - "type": "EvaluatesTo" - } - ] - }, "OriginFilter": "AccountId", "OriginFilter": "AssetDefinitionId", "OriginFilter": "AssetId", @@ -3158,18 +2877,6 @@ "OriginFilter": "PeerId", "OriginFilter": "RoleId", "OriginFilter": "TriggerId", - "PairExpr": { - "Struct": [ - { - "name": "left_instruction", - "type": "InstructionExpr" - }, - { - "name": "right_instruction", - "type": "InstructionExpr" - } - ] - }, "Parameter": { "Struct": [ { @@ -3610,28 +3317,23 @@ "discriminant": 0, "type": "String" }, - { - "tag": "Evaluate", - "discriminant": 1, - "type": "String" - }, { "tag": "Find", - "discriminant": 2, + "discriminant": 1, "type": "FindError" }, { "tag": "Conversion", - "discriminant": 3, + "discriminant": 2, "type": "String" }, { "tag": "UnknownCursor", - "discriminant": 4 + "discriminant": 3 }, { "tag": "FetchSizeTooBig", - "discriminant": 5 + "discriminant": 4 } ] }, @@ -3651,35 +3353,110 @@ } ] }, - "RaiseTo": { + "RawGenesisBlock": { "Struct": [ { - "name": "left", - "type": "EvaluatesTo" + "name": "transactions", + "type": "Vec>" }, { - "name": "right", - "type": "EvaluatesTo" + "name": "executor", + "type": "ExecutorMode" } ] }, - "RawGenesisBlock": { + "Register": { "Struct": [ { - "name": "transactions", - "type": "Vec>" - }, + "name": "object", + "type": "NewAccount" + } + ] + }, + "Register": { + "Struct": [ { - "name": "executor", - "type": "ExecutorMode" + "name": "object", + "type": "Asset" + } + ] + }, + "Register": { + "Struct": [ + { + "name": "object", + "type": "NewAssetDefinition" } ] }, - "RegisterExpr": { + "Register": { "Struct": [ { "name": "object", - "type": "EvaluatesTo" + "type": "NewDomain" + } + ] + }, + "Register": { + "Struct": [ + { + "name": "object", + "type": "Peer" + } + ] + }, + "Register": { + "Struct": [ + { + "name": "object", + "type": "NewRole" + } + ] + }, + "Register>": { + "Struct": [ + { + "name": "object", + "type": "Trigger" + } + ] + }, + "RegisterBox": { + "Enum": [ + { + "tag": "Peer", + "discriminant": 0, + "type": "Register" + }, + { + "tag": "Domain", + "discriminant": 1, + "type": "Register" + }, + { + "tag": "Account", + "discriminant": 2, + "type": "Register" + }, + { + "tag": "AssetDefinition", + "discriminant": 3, + "type": "Register" + }, + { + "tag": "Asset", + "discriminant": 4, + "type": "Register" + }, + { + "tag": "Role", + "discriminant": 5, + "type": "Register" + }, + { + "tag": "Trigger", + "discriminant": 6, + "type": "Register>" } ] }, @@ -3722,15 +3499,75 @@ } ] }, - "RemoveKeyValueExpr": { + "RemoveKeyValue": { + "Struct": [ + { + "name": "object_id", + "type": "AccountId" + }, + { + "name": "key", + "type": "Name" + } + ] + }, + "RemoveKeyValue": { "Struct": [ { "name": "object_id", - "type": "EvaluatesTo" + "type": "AssetId" + }, + { + "name": "key", + "type": "Name" + } + ] + }, + "RemoveKeyValue": { + "Struct": [ + { + "name": "object_id", + "type": "AssetDefinitionId" + }, + { + "name": "key", + "type": "Name" + } + ] + }, + "RemoveKeyValue": { + "Struct": [ + { + "name": "object_id", + "type": "DomainId" }, { "name": "key", - "type": "EvaluatesTo" + "type": "Name" + } + ] + }, + "RemoveKeyValueBox": { + "Enum": [ + { + "tag": "Domain", + "discriminant": 0, + "type": "RemoveKeyValue" + }, + { + "tag": "Account", + "discriminant": 1, + "type": "RemoveKeyValue" + }, + { + "tag": "AssetDefinition", + "discriminant": 2, + "type": "RemoveKeyValue" + }, + { + "tag": "Asset", + "discriminant": 3, + "type": "RemoveKeyValue" } ] }, @@ -3747,27 +3584,53 @@ } ] }, - "RepetitionError": { + "RepetitionError": { + "Struct": [ + { + "name": "instruction_type", + "type": "InstructionType" + }, + { + "name": "id", + "type": "IdBox" + } + ] + }, + "Revoke": { + "Struct": [ + { + "name": "object", + "type": "PermissionToken" + }, + { + "name": "destination_id", + "type": "AccountId" + } + ] + }, + "Revoke": { "Struct": [ { - "name": "instruction_type", - "type": "InstructionType" + "name": "object", + "type": "RoleId" }, { - "name": "id", - "type": "IdBox" + "name": "destination_id", + "type": "AccountId" } ] }, - "RevokeExpr": { - "Struct": [ + "RevokeBox": { + "Enum": [ { - "name": "object", - "type": "EvaluatesTo" + "tag": "PermissionToken", + "discriminant": 0, + "type": "Revoke" }, { - "name": "destination_id", - "type": "EvaluatesTo" + "tag": "Role", + "discriminant": 1, + "type": "Revoke" } ] }, @@ -3905,35 +3768,99 @@ } ] }, - "SequenceExpr": { + "SetKeyValue": { "Struct": [ { - "name": "instructions", - "type": "Vec" + "name": "object_id", + "type": "AccountId" + }, + { + "name": "key", + "type": "Name" + }, + { + "name": "value", + "type": "Value" + } + ] + }, + "SetKeyValue": { + "Struct": [ + { + "name": "object_id", + "type": "AssetId" + }, + { + "name": "key", + "type": "Name" + }, + { + "name": "value", + "type": "Value" + } + ] + }, + "SetKeyValue": { + "Struct": [ + { + "name": "object_id", + "type": "AssetDefinitionId" + }, + { + "name": "key", + "type": "Name" + }, + { + "name": "value", + "type": "Value" } ] }, - "SetKeyValueExpr": { + "SetKeyValue": { "Struct": [ { "name": "object_id", - "type": "EvaluatesTo" + "type": "DomainId" }, { "name": "key", - "type": "EvaluatesTo" + "type": "Name" }, { "name": "value", - "type": "EvaluatesTo" + "type": "Value" + } + ] + }, + "SetKeyValueBox": { + "Enum": [ + { + "tag": "Domain", + "discriminant": 0, + "type": "SetKeyValue" + }, + { + "tag": "Account", + "discriminant": 1, + "type": "SetKeyValue" + }, + { + "tag": "AssetDefinition", + "discriminant": 2, + "type": "SetKeyValue" + }, + { + "tag": "Asset", + "discriminant": 3, + "type": "SetKeyValue" } ] }, - "SetParameterExpr": { + "SetParameter": { "Struct": [ { "name": "parameter", - "type": "EvaluatesTo" + "type": "Parameter" } ] }, @@ -4136,12 +4063,6 @@ "value": "Asset" } }, - "SortedMap>": { - "Map": { - "key": "Name", - "value": "EvaluatesTo" - } - }, "SortedMap": { "Map": { "key": "Name", @@ -4186,18 +4107,6 @@ ] }, "StringWithJson": "String", - "Subtract": { - "Struct": [ - { - "name": "left", - "type": "EvaluatesTo" - }, - { - "name": "right", - "type": "EvaluatesTo" - } - ] - }, "TimeEvent": { "Struct": [ { @@ -4328,19 +4237,102 @@ } ] }, - "TransferExpr": { + "Transfer": { + "Struct": [ + { + "name": "source_id", + "type": "AccountId" + }, + { + "name": "object", + "type": "AssetDefinitionId" + }, + { + "name": "destination_id", + "type": "AccountId" + } + ] + }, + "Transfer": { + "Struct": [ + { + "name": "source_id", + "type": "AccountId" + }, + { + "name": "object", + "type": "DomainId" + }, + { + "name": "destination_id", + "type": "AccountId" + } + ] + }, + "Transfer": { + "Struct": [ + { + "name": "source_id", + "type": "AssetId" + }, + { + "name": "object", + "type": "Fixed" + }, + { + "name": "destination_id", + "type": "AccountId" + } + ] + }, + "Transfer": { + "Struct": [ + { + "name": "source_id", + "type": "AssetId" + }, + { + "name": "object", + "type": "u128" + }, + { + "name": "destination_id", + "type": "AccountId" + } + ] + }, + "Transfer": { "Struct": [ { "name": "source_id", - "type": "EvaluatesTo" + "type": "AssetId" }, { "name": "object", - "type": "EvaluatesTo" + "type": "u32" }, { "name": "destination_id", - "type": "EvaluatesTo" + "type": "AccountId" + } + ] + }, + "TransferBox": { + "Enum": [ + { + "tag": "Domain", + "discriminant": 0, + "type": "Transfer" + }, + { + "tag": "AssetDefinition", + "discriminant": 1, + "type": "Transfer" + }, + { + "tag": "Asset", + "discriminant": 2, + "type": "AssetTransferBox" } ] }, @@ -4529,11 +4521,98 @@ ] }, "UniqueVec": "Vec", - "UnregisterExpr": { + "Unregister": { + "Struct": [ + { + "name": "object_id", + "type": "AccountId" + } + ] + }, + "Unregister": { + "Struct": [ + { + "name": "object_id", + "type": "AssetId" + } + ] + }, + "Unregister": { + "Struct": [ + { + "name": "object_id", + "type": "AssetDefinitionId" + } + ] + }, + "Unregister": { + "Struct": [ + { + "name": "object_id", + "type": "DomainId" + } + ] + }, + "Unregister": { + "Struct": [ + { + "name": "object_id", + "type": "PeerId" + } + ] + }, + "Unregister": { + "Struct": [ + { + "name": "object_id", + "type": "RoleId" + } + ] + }, + "Unregister>": { "Struct": [ { "name": "object_id", - "type": "EvaluatesTo" + "type": "TriggerId" + } + ] + }, + "UnregisterBox": { + "Enum": [ + { + "tag": "Peer", + "discriminant": 0, + "type": "Unregister" + }, + { + "tag": "Domain", + "discriminant": 1, + "type": "Unregister" + }, + { + "tag": "Account", + "discriminant": 2, + "type": "Unregister" + }, + { + "tag": "AssetDefinition", + "discriminant": 3, + "type": "Unregister" + }, + { + "tag": "Asset", + "discriminant": 4, + "type": "Unregister" + }, + { + "tag": "Role", + "discriminant": 5, + "type": "Unregister" + }, + { + "tag": "Trigger", + "discriminant": 6, + "type": "Unregister>" } ] }, @@ -4546,11 +4625,11 @@ } ] }, - "UpgradeExpr": { + "Upgrade": { "Struct": [ { - "name": "object", - "type": "EvaluatesTo" + "name": "executor", + "type": "Executor" } ] }, @@ -4761,8 +4840,8 @@ "Vec>": { "Vec": "GenericPredicateBox" }, - "Vec": { - "Vec": "InstructionExpr" + "Vec": { + "Vec": "InstructionBox" }, "Vec": { "Vec": "Name" @@ -4782,8 +4861,8 @@ "Vec": { "Vec": "Value" }, - "Vec>": { - "Vec": "Vec" + "Vec>": { + "Vec": "Vec" }, "Vec": { "Vec": "u8" @@ -4797,18 +4876,6 @@ ] }, "WasmSmartContract": "Vec", - "Where": { - "Struct": [ - { - "name": "expression", - "type": "EvaluatesTo" - }, - { - "name": "values", - "type": "SortedMap>" - } - ] - }, "bool": "bool", "i64": { "Int": "FixedWidth" diff --git a/dsl/Cargo.toml b/dsl/Cargo.toml deleted file mode 100755 index 7cdbd40ee01..00000000000 --- a/dsl/Cargo.toml +++ /dev/null @@ -1,28 +0,0 @@ -[package] -name = "iroha_dsl" - -edition.workspace = true -version.workspace = true -authors.workspace = true - -license.workspace = true - -[lints] -workspace = true - -[lib] -proc-macro = true - -[dependencies] -quote = "1.0" -proc-macro2 = "1.0" -litrs = "0.4.0" - -[dev-dependencies] -iroha_data_model = { workspace = true } -iroha_crypto = { workspace = true } -iroha_config = { workspace = true } -iroha_client = { workspace = true } -serde_json = "1.0" - - diff --git a/dsl/src/lib.rs b/dsl/src/lib.rs deleted file mode 100755 index c1378c24b1d..00000000000 --- a/dsl/src/lib.rs +++ /dev/null @@ -1,208 +0,0 @@ -// TODO: add docs -#![allow(missing_docs)] - -use std::{convert::TryFrom, iter::Peekable, str::FromStr}; - -use litrs::Literal; -use proc_macro2::{ - token_stream::IntoIter, - TokenStream, TokenTree, - TokenTree::{Ident, Punct}, -}; -use quote::quote; - -#[derive(PartialEq)] -enum ExprType { - Nil, - Int, - Bool, -} - -// within the compiler, these generate tokens for -// Value -struct ParsedExpr { - t: ExprType, - tokens: TokenStream, -} - -struct Operator(i32, ExprType, &'static str); - -fn parse_literal(it: &mut Peekable) -> ParsedExpr { - let token = it - .next() - .expect("failed to parse literal, hit end of string"); - - match token { - Punct(ref x) => { - if x.as_char() == '(' { - let v = parse_expr(it); - - it.next() - .expect("expected closing paren, hit end of string"); - return v; - } - } - - // boolean literals - Ident(ref x) => match x.to_string().as_str() { - "true" => { - return ParsedExpr { - t: ExprType::Bool, - tokens: quote! { true }, - } - } - "false" => { - return ParsedExpr { - t: ExprType::Bool, - tokens: quote! { false }, - } - } - - // unary not - "not" => { - let v = parse_literal(it).tokens; - return ParsedExpr { - t: ExprType::Bool, - tokens: quote! { Not::new(#v) }, - }; - } - _ => panic!("error: unknown identifier"), - }, - - // kinda ugly but we fallthrough basically - _ => {} - } - - // integer literals - if let Ok(Literal::Integer(i)) = Literal::try_from(token) { - let v = i.value::().expect("i don't think this integer fits?"); - return ParsedExpr { - t: ExprType::Int, - tokens: quote! { #v }, - }; - } - - ParsedExpr { - t: ExprType::Nil, - tokens: TokenStream::new(), - } -} - -fn precedence(it: &mut Peekable) -> Option { - match it.peek() { - Some(Punct(x)) => match x.as_char() { - // arithmatic - '+' => Some(Operator(4, ExprType::Int, "Add")), - '-' => Some(Operator(4, ExprType::Int, "Subtract")), - '*' => Some(Operator(3, ExprType::Int, "Multiply")), - - // compares - '=' => Some(Operator(2, ExprType::Int, "Equal")), - '>' => Some(Operator(2, ExprType::Int, "Greater")), - '<' => Some(Operator(2, ExprType::Int, "Less")), - _ => None, - }, - Some(Ident(ref x)) => match x.to_string().as_str() { - "and" => Some(Operator(1, ExprType::Bool, "And")), - "or" => Some(Operator(1, ExprType::Bool, "Or")), - _ => None, - }, - _ => None, - } -} - -/// precedence walking -fn parse_binop(it: &mut Peekable, min_prec: i32) -> ParsedExpr { - let mut lhs = parse_literal(it); - while let Some(prec) = precedence(it) { - it.next(); - if prec.0 < min_prec { - break; - } - - let op = proc_macro2::TokenStream::from_str(prec.2).unwrap(); - let rhs = parse_literal(it); - - assert!( - lhs.t == prec.1 && rhs.t == prec.1, - "cannot perform binary operator on these!" - ); - - let lhs_tokens = lhs.tokens; - let rhs_tokens = rhs.tokens; - lhs = ParsedExpr { - t: prec.1, - tokens: quote! { #op::new(#lhs_tokens, #rhs_tokens) }, - }; - } - - lhs -} - -fn is_ident(a: &TokenTree, b: &'static str) -> bool { - if let Ident(ref x) = a { - return x.to_string().as_str() == b; - } - - false -} - -fn parse_expr(it: &mut Peekable) -> ParsedExpr { - if is_ident(it.peek().expect("hit end of string"), "if") { - it.next(); - - let cond_tokens = parse_binop(it, 0).tokens; - assert!( - is_ident(&it.next().expect("hit end of string"), "then"), - "expected 'then'" - ); - - let true_case = parse_binop(it, 0); - assert!( - is_ident(&it.next().expect("hit end of string"), "else"), - "expected 'else'" - ); - - let false_case = parse_binop(it, 0); - assert!( - true_case.t == false_case.t, - "both types in a conditional must match" - ); - - let true_tokens = true_case.tokens; - let false_tokens = false_case.tokens; - return ParsedExpr { - t: ExprType::Int, - tokens: quote! { If::new(#cond_tokens, #true_tokens, #false_tokens) }, - }; - } - - // normal expression - parse_binop(it, 0) -} - -/// Convert arithmetic expression into bare expression in the iroha data model. -/// -/// Basic arithmetic and boolean expressions are supported, namely: `> < = + - * and or if not` -/// -/// # Examples -/// -/// ``` -/// extern crate iroha_dsl; -/// extern crate iroha_data_model; -/// use iroha_dsl::expr; -/// use iroha_data_model::{prelude::*, ParseError}; -/// -/// fn main() { -/// assert_eq!(expr!(54654*5 + 1), Add::new(Multiply::new(54654_u64, 5_u64), 1_u64)); -/// println!("{}", expr!(not true and false)); -/// println!("{}", expr!(if 4 = 4 then 64 else 32)); -/// } -/// ``` -#[proc_macro] -pub fn expr(input: proc_macro::TokenStream) -> proc_macro::TokenStream { - let input = proc_macro2::TokenStream::from(input); - let mut it = input.into_iter().peekable(); - - proc_macro::TokenStream::from(parse_expr(&mut it).tokens) -} diff --git a/ffi/src/lib.rs b/ffi/src/lib.rs index dcdd52f0d9a..4590f55b1c7 100644 --- a/ffi/src/lib.rs +++ b/ffi/src/lib.rs @@ -415,6 +415,7 @@ pub struct Extern { /// ``` pub trait WrapperTypeOf { /// Correct return type of `T` in a function generated via [`ffi_import`] + // TODO: Is associated type necessary if we already have a generic? type Type; } diff --git a/genesis/src/lib.rs b/genesis/src/lib.rs index 0428a2240ac..84cd9964e49 100644 --- a/genesis/src/lib.rs +++ b/genesis/src/lib.rs @@ -61,7 +61,7 @@ impl GenesisNetwork { // First instruction should be Executor upgrade. // This makes possible to grant permissions to users in genesis. let transactions_iter = std::iter::once(GenesisTransactionBuilder { - isi: vec![UpgradeExpr::new(Executor::try_from(raw_block.executor)?).into()], + isi: vec![Upgrade::new(Executor::try_from(raw_block.executor)?).into()], }) .chain(raw_block.transactions); @@ -190,7 +190,7 @@ impl ExecutorPath { #[repr(transparent)] pub struct GenesisTransactionBuilder { /// Instructions - isi: Vec, + isi: Vec, } impl GenesisTransactionBuilder { @@ -208,7 +208,7 @@ impl GenesisTransactionBuilder { } /// Add new instruction to the transaction. - pub fn append_instruction(&mut self, instruction: InstructionExpr) { + pub fn append_instruction(&mut self, instruction: InstructionBox) { self.isi.push(instruction); } } @@ -287,7 +287,7 @@ impl RawGenesisBlockBuilder { let new_domain = Domain::new(domain_id.clone()).with_metadata(metadata); self.transaction .isi - .push(RegisterExpr::new(new_domain).into()); + .push(Register::domain(new_domain).into()); RawGenesisDomainBuilder { transaction: self.transaction, domain_id, @@ -322,7 +322,7 @@ impl RawGenesisDomainBuilder { let account_id = AccountId::new(account_name, self.domain_id.clone()); self.transaction .isi - .push(RegisterExpr::new(Account::new(account_id, [])).into()); + .push(Register::account(Account::new(account_id, [])).into()); self } @@ -340,7 +340,7 @@ impl RawGenesisDomainBuilder { ) -> Self { let account_id = AccountId::new(account_name, self.domain_id.clone()); let register = - RegisterExpr::new(Account::new(account_id, [public_key]).with_metadata(metadata)); + Register::account(Account::new(account_id, [public_key]).with_metadata(metadata)); self.transaction.isi.push(register.into()); self } @@ -356,7 +356,7 @@ impl RawGenesisDomainBuilder { }; self.transaction .isi - .push(RegisterExpr::new(asset_definition).into()); + .push(Register::asset_definition(asset_definition).into()); self } } @@ -418,11 +418,11 @@ mod tests { let domain_id: DomainId = "wonderland".parse().unwrap(); assert_eq!( finished_genesis_block.transactions[0].isi[0], - RegisterExpr::new(Domain::new(domain_id.clone())).into() + Register::domain(Domain::new(domain_id.clone())).into() ); assert_eq!( finished_genesis_block.transactions[0].isi[1], - RegisterExpr::new(Account::new( + Register::account(Account::new( AccountId::new("alice".parse().unwrap(), domain_id.clone()), [] )) @@ -430,7 +430,7 @@ mod tests { ); assert_eq!( finished_genesis_block.transactions[0].isi[2], - RegisterExpr::new(Account::new( + Register::account(Account::new( AccountId::new("bob".parse().unwrap(), domain_id), [] )) @@ -441,11 +441,11 @@ mod tests { let domain_id: DomainId = "tulgey_wood".parse().unwrap(); assert_eq!( finished_genesis_block.transactions[0].isi[3], - RegisterExpr::new(Domain::new(domain_id.clone())).into() + Register::domain(Domain::new(domain_id.clone())).into() ); assert_eq!( finished_genesis_block.transactions[0].isi[4], - RegisterExpr::new(Account::new( + Register::account(Account::new( AccountId::new("Cheshire_Cat".parse().unwrap(), domain_id), [] )) @@ -456,11 +456,11 @@ mod tests { let domain_id: DomainId = "meadow".parse().unwrap(); assert_eq!( finished_genesis_block.transactions[0].isi[5], - RegisterExpr::new(Domain::new(domain_id.clone())).into() + Register::domain(Domain::new(domain_id.clone())).into() ); assert_eq!( finished_genesis_block.transactions[0].isi[6], - RegisterExpr::new(Account::new( + Register::account(Account::new( AccountId::new("Mad_Hatter".parse().unwrap(), domain_id), [public_key.parse().unwrap()], )) @@ -468,7 +468,7 @@ mod tests { ); assert_eq!( finished_genesis_block.transactions[0].isi[7], - RegisterExpr::new(AssetDefinition::big_quantity( + Register::asset_definition(AssetDefinition::big_quantity( "hats#meadow".parse().unwrap() )) .into() diff --git a/p2p/src/peer.rs b/p2p/src/peer.rs index e08a93f6aca..182b72e9e7d 100644 --- a/p2p/src/peer.rs +++ b/p2p/src/peer.rs @@ -409,6 +409,7 @@ mod state { } impl ConnectedTo { + #[allow(clippy::similar_names)] pub(super) async fn send_client_hello( Self { peer_addr, @@ -452,6 +453,7 @@ mod state { } impl ConnectedFrom { + #[allow(clippy::similar_names)] pub(super) async fn read_client_hello( Self { peer_addr, diff --git a/schema/derive/src/lib.rs b/schema/derive/src/lib.rs index 233a901e5cb..3f801b67459 100644 --- a/schema/derive/src/lib.rs +++ b/schema/derive/src/lib.rs @@ -65,6 +65,7 @@ impl FromMeta for Transparent { #[darling(attributes(schema))] struct SchemaAttributes { transparent: Transparent, + bounds: Option, } // NOTE: this will fail on unknown attributes.. This is not ideal @@ -184,12 +185,16 @@ pub fn schema_derive(input: TokenStream) -> Result { let impl_type_id = impl_type_id(&mut syn2::parse2(original_input).unwrap()); let impl_schema = match &input.schema_attrs.transparent { - Transparent::NotTransparent => impl_into_schema(&input), + Transparent::NotTransparent => impl_into_schema(&input, input.schema_attrs.bounds.as_ref()), Transparent::Transparent(transparent_type) => { let transparent_type = transparent_type .clone() .unwrap_or_else(|| infer_transparent_type(&input.data, &mut emitter)); - Ok(impl_transparent_into_schema(&input, &transparent_type)) + impl_transparent_into_schema( + &input, + &transparent_type, + input.schema_attrs.bounds.as_ref(), + ) } }; let impl_schema = match impl_schema { @@ -211,11 +216,16 @@ pub fn schema_derive(input: TokenStream) -> Result { fn impl_transparent_into_schema( input: &IntoSchemaInput, transparent_type: &syn2::Type, -) -> TokenStream { + bounds: Option<&String>, +) -> Result { let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); let name = &input.ident; + let where_clause: Option = match bounds { + Some(bounds) => Some(syn2::parse_str(&format!("where {bounds}"))?), + None => where_clause.cloned(), + }; - quote! { + Ok(quote! { impl #impl_generics iroha_schema::IntoSchema for #name #ty_generics #where_clause { fn update_schema_map(map: &mut iroha_schema::MetaMap) { if !map.contains_key::() { @@ -233,14 +243,18 @@ fn impl_transparent_into_schema( <#transparent_type as iroha_schema::IntoSchema>::type_name() } } - } + }) } -fn impl_into_schema(input: &IntoSchemaInput) -> Result { +fn impl_into_schema(input: &IntoSchemaInput, bounds: Option<&String>) -> Result { let name = &input.ident; let type_name_body = trait_body(name, &input.generics, false); let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); let metadata = metadata(&input.data)?; + let where_clause: Option = match bounds { + Some(bounds) => Some(syn2::parse_str(&format!("where {bounds}"))?), + None => where_clause.cloned(), + }; Ok(quote! { impl #impl_generics iroha_schema::IntoSchema for #name #ty_generics #where_clause { diff --git a/schema/derive/tests/ui_pass/derive_into_schema.rs b/schema/derive/tests/ui_pass/derive_into_schema.rs index 576fb77f5ce..f75295a3f82 100644 --- a/schema/derive/tests/ui_pass/derive_into_schema.rs +++ b/schema/derive/tests/ui_pass/derive_into_schema.rs @@ -42,4 +42,14 @@ pub enum Enum { FortyTwo, } +pub trait Trait { + type Assoc; +} + +#[derive(IntoSchema)] +#[schema(bounds = "T: Trait, T::Assoc: IntoSchema")] +pub struct WithComplexGeneric { + _value: T::Assoc, +} + pub fn main() {} diff --git a/schema/gen/src/lib.rs b/schema/gen/src/lib.rs index ddac0fad272..ef3c1cab1f8 100644 --- a/schema/gen/src/lib.rs +++ b/schema/gen/src/lib.rs @@ -66,13 +66,13 @@ types!( AccountEventFilter, AccountFilter, AccountId, + AccountMintBox, AccountPermissionChanged, AccountRoleChanged, Action, - Add, Algorithm, - And, Asset, + AssetBurnBox, AssetChanged, AssetDefinition, AssetDefinitionEvent, @@ -85,6 +85,8 @@ types!( AssetEventFilter, AssetFilter, AssetId, + AssetMintBox, + AssetTransferBox, AssetValue, AssetValueType, AtIndex, @@ -92,7 +94,6 @@ types!( BTreeMap, BTreeMap, BTreeMap, - BTreeMap>, BTreeMap, BTreeSet, BTreeSet, @@ -106,53 +107,31 @@ types!( BlockSubscriptionRequest, Box, Box>, - Box, Box, Box, - BurnExpr, - ConditionalExpr, + BurnBox, ConfigurationEvent, ConstString, Container, - Contains, - ContainsAll, - ContainsAny, - ContextValue, DataEntityFilter, DataEvent, DataEventFilter, - Divide, Domain, DomainEvent, DomainEventFilter, DomainFilter, DomainId, Duration, - Equal, - EvaluatesTo, - EvaluatesTo, - EvaluatesTo, - EvaluatesTo, - EvaluatesTo, - EvaluatesTo, - EvaluatesTo, - EvaluatesTo, - EvaluatesTo, - EvaluatesTo, - EvaluatesTo, - EvaluatesTo, - EvaluatesTo, - EvaluatesTo>, - EvaluatesTo, Event, EventMessage, EventSubscriptionRequest, Executable, - ExecuteTriggerExpr, + ExecuteTrigger, ExecuteTriggerEvent, ExecuteTriggerEventFilter, ExecutionTime, - Expression, + Executor, + ExecutorEvent, Fail, FilterBox, FilterOpt, @@ -220,16 +199,14 @@ types!( FixNum, Fixed, ForwardCursor, - GrantExpr, - Greater, + GrantBox, Hash, HashOf>, HashOf, HashOf, IdBox, IdentifiableBox, - If, - InstructionExpr, + InstructionBox, InstructionExecutionFail, Interval, Interval, @@ -239,7 +216,6 @@ types!( Ipv6Addr, Ipv6Predicate, LengthLimits, - Less, MerkleTree, Metadata, MetadataChanged, @@ -247,19 +223,16 @@ types!( MetadataChanged, MetadataChanged, MetadataLimits, - MintExpr, + MintBox, Mintable, - Mod, - Multiply, Name, NewAccount, NewAssetDefinition, NewDomain, - NewParameterExpr, + NewParameter, NewRole, NonTrivial, NonZeroU64, - Not, NotificationEventFilter, NumericValue, Option, @@ -267,7 +240,6 @@ types!( Option, Option>>, Option>, - Option, Option, Option, Option, @@ -275,7 +247,6 @@ types!( Option, Option, Option, - Or, OriginFilter, OriginFilter, OriginFilter, @@ -283,7 +254,6 @@ types!( OriginFilter, OriginFilter, OriginFilter, - PairExpr, Parameter, ParameterId, Peer, @@ -306,12 +276,11 @@ types!( QueryBox, QueryExecutionFail, QueryPayload, - RaiseTo, - RegisterExpr, + RegisterBox, RegistrableBox, - RemoveKeyValueExpr, + RemoveKeyValueBox, Repeats, - RevokeExpr, + RevokeBox, Role, RoleEvent, RoleEventFilter, @@ -321,9 +290,8 @@ types!( SemiInterval, SemiInterval, SemiRange, - SequenceExpr, - SetKeyValueExpr, - SetParameterExpr, + SetKeyValueBox, + SetParameter, Signature, SignatureCheckCondition, SignatureOf, @@ -339,7 +307,6 @@ types!( SignedTransactionV1, String, StringPredicate, - Subtract, TimeEvent, TimeEventFilter, TimeInterval, @@ -350,7 +317,7 @@ types!( TransactionQueryOutput, TransactionRejectionReason, TransactionValue, - TransferExpr, + TransferBox, Trigger, TriggerCompletedEventFilter, TriggerCompletedOutcomeType, @@ -360,16 +327,14 @@ types!( TriggerId, TriggerNumberOfExecutionsChanged, TriggeringFilterBox, - UnregisterExpr, + UnregisterBox, UpgradableBox, ValidationFail, - Executor, - ExecutorEvent, Value, ValueOfKey, ValuePredicate, Vec, - Vec, + Vec, Vec, Vec, Vec, @@ -377,7 +342,6 @@ types!( Vec, WasmExecutionFail, WasmSmartContract, - Where, [Interval; 8], [Interval; 4], [u16; 8], @@ -465,40 +429,18 @@ mod tests { let mut missing_schemas = HashMap::<&str, _>::new(); for type_name in type_names { - if let (Some(mut start), Some(end)) = (type_name.find('<'), type_name.rfind('>')) { - start += 1; - - let mut angle_bracket_diff = 0_u8; - for (i, c) in type_name[start..end].chars().enumerate() { - if c == '<' { - angle_bracket_diff += 1_u8; - } - if c == '>' { - angle_bracket_diff -= 1_u8; - } - - if c == ',' && angle_bracket_diff == 0_u8 { - let generic = type_name[start..(start + i)].trim(); + let (Some(start), Some(end)) = (type_name.find('<'), type_name.rfind('>')) else { + continue; + }; - start += i + 1; - if !is_const_generic(generic) { - continue; - } + assert!(start < end, "Invalid type name: {type_name}"); - if !type_names.contains(generic) { - missing_schemas - .entry(type_name) - .or_insert_with(Vec::new) - .push(generic); - } - } + for generic in type_name.split(", ") { + if !is_const_generic(generic) { + continue; } - let generic = type_name[start..end].trim(); - if !generic.is_empty() - && !is_const_generic(generic) - && !type_names.contains(generic) - { + if !type_names.contains(generic) { missing_schemas .entry(type_name) .or_insert_with(Vec::new) diff --git a/smart_contract/executor/derive/src/default.rs b/smart_contract/executor/derive/src/default.rs index 6045bd40b82..fa12601e750 100644 --- a/smart_contract/executor/derive/src/default.rs +++ b/smart_contract/executor/derive/src/default.rs @@ -54,7 +54,7 @@ pub fn impl_derive_entrypoints(emitter: &mut Emitter, input: &syn2::DeriveInput) #[::iroha_executor::prelude::entrypoint] pub fn validate_instruction( authority: ::iroha_executor::prelude::AccountId, - instruction: ::iroha_executor::prelude::InstructionExpr, + instruction: ::iroha_executor::prelude::InstructionBox, block_height: u64, #(#custom_args),* ) -> ::iroha_executor::prelude::Result { @@ -107,54 +107,63 @@ pub fn impl_derive_entrypoints(emitter: &mut Emitter, input: &syn2::DeriveInput) } } +#[allow(clippy::too_many_lines)] pub fn impl_derive_visit(emitter: &mut Emitter, input: &syn2::DeriveInput) -> TokenStream2 { let Some(input) = emitter.handle(ExecutorDeriveInput::from_derive_input(input)) else { return quote!(); }; let ExecutorDeriveInput { ident, custom, .. } = &input; let default_visit_sigs: Vec = [ - "fn visit_unsupported(operation: T)", "fn visit_transaction(operation: &SignedTransaction)", - "fn visit_instruction(operation: &InstructionExpr)", - "fn visit_expression(operation: &EvaluatesTo)", - "fn visit_sequence(operation: &SequenceExpr)", - "fn visit_if(operation: &ConditionalExpr)", - "fn visit_pair(operation: &PairExpr)", - "fn visit_unregister_peer(operation: Unregister)", - "fn visit_unregister_domain(operation: Unregister)", - "fn visit_transfer_domain(operation: Transfer)", - "fn visit_set_domain_key_value(operation: SetKeyValue)", - "fn visit_remove_domain_key_value(operation: RemoveKeyValue)", - "fn visit_unregister_account(operation: Unregister)", - "fn visit_mint_account_public_key(operation: Mint)", - "fn visit_burn_account_public_key(operation: Burn)", - "fn visit_mint_account_signature_check_condition(operation: Mint)", - "fn visit_set_account_key_value(operation: SetKeyValue)", - "fn visit_remove_account_key_value(operation: RemoveKeyValue)", - "fn visit_register_asset(operation: Register)", - "fn visit_unregister_asset(operation: Unregister)", - "fn visit_mint_asset(operation: Mint)", - "fn visit_burn_asset(operation: Burn)", - "fn visit_transfer_asset(operation: Transfer)", - "fn visit_set_asset_key_value(operation: SetKeyValue)", - "fn visit_remove_asset_key_value(operation: RemoveKeyValue)", - "fn visit_unregister_asset_definition(operation: Unregister)", - "fn visit_transfer_asset_definition(operation: Transfer)", - "fn visit_set_asset_definition_key_value(operation: SetKeyValue)", - "fn visit_remove_asset_definition_key_value(operation: RemoveKeyValue)", - "fn visit_grant_account_permission(operation: Grant)", - "fn visit_revoke_account_permission(operation: Revoke)", - "fn visit_register_role(operation: Register)", - "fn visit_unregister_role(operation: Unregister)", - "fn visit_grant_account_role(operation: Grant)", - "fn visit_revoke_account_role(operation: Revoke)", - "fn visit_unregister_trigger(operation: Unregister>)", - "fn visit_mint_trigger_repetitions(operation: Mint>)", - "fn visit_burn_trigger_repetitions(operation: Burn>)", - "fn visit_execute_trigger(operation: ExecuteTrigger)", - "fn visit_set_parameter(operation: SetParameter)", - "fn visit_new_parameter(operation: NewParameter)", - "fn visit_upgrade_executor(operation: Upgrade)", + "fn visit_instruction(operation: &InstructionBox)", + "fn visit_register_peer(operation: &Register)", + "fn visit_unregister_peer(operation: &Unregister)", + "fn visit_register_domain(operation: &Register)", + "fn visit_unregister_domain(operation: &Unregister)", + "fn visit_transfer_domain(operation: &Transfer)", + "fn visit_set_domain_key_value(operation: &SetKeyValue)", + "fn visit_remove_domain_key_value(operation: &RemoveKeyValue)", + "fn visit_register_account(operation: &Register)", + "fn visit_unregister_account(operation: &Unregister)", + "fn visit_mint_account_public_key(operation: &Mint)", + "fn visit_burn_account_public_key(operation: &Burn)", + "fn visit_mint_account_signature_check_condition(operation: &Mint)", + "fn visit_set_account_key_value(operation: &SetKeyValue)", + "fn visit_remove_account_key_value(operation: &RemoveKeyValue)", + "fn visit_register_asset(operation: &Register)", + "fn visit_unregister_asset(operation: &Unregister)", + "fn visit_mint_asset_quantity(operation: &Mint)", + "fn visit_burn_asset_quantity(operation: &Burn)", + "fn visit_mint_asset_big_quantity(operation: &Mint)", + "fn visit_burn_asset_big_quantity(operation: &Burn)", + "fn visit_mint_asset_fixed(operation: &Mint)", + "fn visit_burn_asset_fixed(operation: &Burn)", + "fn visit_transfer_asset_quantity(operation: &Transfer)", + "fn visit_transfer_asset_big_quantity(operation: &Transfer)", + "fn visit_transfer_asset_fixed(operation: &Transfer)", + "fn visit_set_asset_key_value(operation: &SetKeyValue)", + "fn visit_remove_asset_key_value(operation: &RemoveKeyValue)", + "fn visit_register_asset_definition(operation: &Register)", + "fn visit_unregister_asset_definition(operation: &Unregister)", + "fn visit_transfer_asset_definition(operation: &Transfer)", + "fn visit_set_asset_definition_key_value(operation: &SetKeyValue)", + "fn visit_remove_asset_definition_key_value(operation: &RemoveKeyValue)", + "fn visit_grant_account_permission(operation: &Grant)", + "fn visit_revoke_account_permission(operation: &Revoke)", + "fn visit_register_role(operation: &Register)", + "fn visit_unregister_role(operation: &Unregister)", + "fn visit_grant_account_role(operation: &Grant)", + "fn visit_revoke_account_role(operation: &Revoke)", + "fn visit_register_trigger(operation: &Register>)", + "fn visit_unregister_trigger(operation: &Unregister>)", + "fn visit_mint_trigger_repetitions(operation: &Mint>)", + "fn visit_burn_trigger_repetitions(operation: &Burn>)", + "fn visit_execute_trigger(operation: &ExecuteTrigger)", + "fn visit_set_parameter(operation: &SetParameter)", + "fn visit_new_parameter(operation: &NewParameter)", + "fn visit_upgrade(operation: &Upgrade)", + "fn visit_log(operation: &Log)", + "fn visit_fail(operation: &Fail)", ] .into_iter() .map(|item| { @@ -228,29 +237,6 @@ pub fn impl_derive_validate(emitter: &mut Emitter, input: &syn2::DeriveInput) -> } } -pub fn impl_derive_expression_evaluator( - emitter: &mut Emitter, - input: &syn2::DeriveInput, -) -> TokenStream2 { - let Some(input) = emitter.handle(ExecutorDeriveInput::from_derive_input(input)) else { - return quote!(); - }; - let ExecutorDeriveInput { ident, data, .. } = &input; - check_required_fields(data, emitter); - quote! { - impl ::iroha_executor::data_model::evaluate::ExpressionEvaluator for #ident { - fn evaluate( - &self, - expression: &E, - ) -> ::core::result::Result - { - self.host.evaluate(expression) - } - } - - } -} - pub fn impl_derive_constructor(emitter: &mut Emitter, input: &syn2::DeriveInput) -> TokenStream2 { let Some(input) = emitter.handle(ExecutorDeriveInput::from_derive_input(input)) else { return quote!(); @@ -268,7 +254,6 @@ pub fn impl_derive_constructor(emitter: &mut Emitter, input: &syn2::DeriveInput) Self { verdict: Ok(()), block_height, - host: ::iroha_executor::smart_contract::Host, #(#custom_idents),* } } @@ -278,7 +263,8 @@ pub fn impl_derive_constructor(emitter: &mut Emitter, input: &syn2::DeriveInput) } fn check_required_fields(ast: &ExecutorData, emitter: &mut Emitter) { - let required_fields: syn2::FieldsNamed = parse_quote!({ verdict: ::iroha_executor::prelude::Result, block_height: u64, host: ::iroha_executor::smart_contract::Host }); + let required_fields: syn2::FieldsNamed = + parse_quote!({ verdict: ::iroha_executor::prelude::Result, block_height: u64 }); let struct_fields = ast .as_ref() .take_struct() @@ -328,7 +314,7 @@ fn check_type_equivalence(full_ty: &syn2::Type, given_ty: &syn2::Type) -> bool { /// Processes an `Executor` by draining it of default fields and returning the idents of the /// custom fields and the corresponding function arguments for use in the constructor fn custom_field_idents_and_fn_args(ast: &ExecutorData) -> (Vec<&Ident>, Vec) { - let required_idents: Vec = ["verdict", "block_height", "host"] + let required_idents: Vec = ["verdict", "block_height"] .iter() .map(|s| Ident::new(s, Span::call_site())) .collect(); diff --git a/smart_contract/executor/derive/src/lib.rs b/smart_contract/executor/derive/src/lib.rs index 228357ca7ef..71d682c974c 100644 --- a/smart_contract/executor/derive/src/lib.rs +++ b/smart_contract/executor/derive/src/lib.rs @@ -233,9 +233,7 @@ pub fn derive_ref_into_domain_owner(input: TokenStream) -> Result { } /// Implements the `iroha_executor::Validate` trait for the given `Executor` struct. As -/// this trait has a `iroha_executor::prelude::Visit`, and the latter has an -/// `iroha_executor::data_model::evaluate::ExpressionEvaluator` -/// bound, at least these two should be implemented as well. +/// this trait has a `iroha_executor::prelude::Visit` at least this one should be implemented as well. /// /// Emits a compile error if the struct didn't have all the expected fields with corresponding /// types, i.e. `verdict`: `iroha_executor::prelude::Result`, `block_height`: `u64` and @@ -271,7 +269,7 @@ pub fn derive_validate(input: TokenStream) -> TokenStream { /// ```ignore /// use iroha_executor::{smart_contract, prelude::*}; /// -/// #[derive(Constructor, Entrypoints, ExpressionEvaluator, Validate, Visit)] +/// #[derive(Constructor, Entrypoints, Validate, Visit)] /// #[visit(custom(visit_query)] /// pub struct Executor { /// verdict: Result, @@ -318,7 +316,7 @@ pub fn derive_visit(input: TokenStream) -> TokenStream { /// ```ignore /// use iroha_executor::{smart_contract, prelude::*}; /// -/// #[derive(Constructor, Entrypoints, ExpressionEvaluator, Validate, Visit)] +/// #[derive(Constructor, Entrypoints, Validate, Visit)] /// #[entrypoints(custom(validate_query))] /// pub struct Executor { /// verdict: Result, @@ -341,27 +339,6 @@ pub fn derive_entrypoints(input: TokenStream) -> TokenStream { emitter.finish_token_stream_with(result) } -/// Implements `iroha_executor::data_model::evaluate::ExpressionEvaluator` trait -/// for the given `Executor` struct. -/// -/// Emits a compile error if the struct didn't have all the expected fields with corresponding -/// types, i.e. `verdict`: `iroha_executor::prelude::Result`, `block_height`: `u64` and -/// `host`: `iroha_executor::smart_contract::Host`, though technically only `host` is needed. -/// The types can be unqualified, but not aliased. -#[manyhow] -#[proc_macro_derive(ExpressionEvaluator)] -pub fn derive_expression_evaluator(input: TokenStream) -> TokenStream { - let mut emitter = Emitter::new(); - - let Some(input) = emitter.handle(syn2::parse2(input)) else { - return emitter.finish_token_stream(); - }; - - let result = default::impl_derive_expression_evaluator(&mut emitter, &input); - - emitter.finish_token_stream_with(result) -} - /// Implements a constructor for the given `Executor` struct. If the `Executor` has any custom fields /// (i.e. different from the expected fields listed below), they will be included into the constructor /// automatically and will need to be passed into `new()` function explicitly. In the default case, diff --git a/smart_contract/executor/src/default.rs b/smart_contract/executor/src/default.rs index 771439e9742..5b70d96a884 100644 --- a/smart_contract/executor/src/default.rs +++ b/smart_contract/executor/src/default.rs @@ -7,48 +7,42 @@ use alloc::format; pub use account::{ visit_burn_account_public_key, visit_mint_account_public_key, - visit_mint_account_signature_check_condition, visit_remove_account_key_value, - visit_set_account_key_value, visit_unregister_account, + visit_mint_account_signature_check_condition, visit_register_account, + visit_remove_account_key_value, visit_set_account_key_value, visit_unregister_account, }; pub use asset::{ - visit_burn_asset, visit_mint_asset, visit_register_asset, visit_remove_asset_key_value, - visit_set_asset_key_value, visit_transfer_asset, visit_unregister_asset, + visit_burn_asset_big_quantity, visit_burn_asset_fixed, visit_burn_asset_quantity, + visit_mint_asset_big_quantity, visit_mint_asset_fixed, visit_mint_asset_quantity, + visit_register_asset, visit_remove_asset_key_value, visit_set_asset_key_value, + visit_transfer_asset_big_quantity, visit_transfer_asset_fixed, visit_transfer_asset_quantity, + visit_unregister_asset, }; pub use asset_definition::{ - visit_remove_asset_definition_key_value, visit_set_asset_definition_key_value, - visit_transfer_asset_definition, visit_unregister_asset_definition, + visit_register_asset_definition, visit_remove_asset_definition_key_value, + visit_set_asset_definition_key_value, visit_transfer_asset_definition, + visit_unregister_asset_definition, }; pub use domain::{ - visit_remove_domain_key_value, visit_set_domain_key_value, visit_transfer_domain, - visit_unregister_domain, + visit_register_domain, visit_remove_domain_key_value, visit_set_domain_key_value, + visit_transfer_domain, visit_unregister_domain, }; -pub use executor::visit_upgrade_executor; -use iroha_smart_contract::debug::DebugExpectExt as _; +pub use executor::visit_upgrade; +pub use fail::visit_fail; +use iroha_smart_contract::data_model::isi::InstructionBox; +pub use log::visit_log; pub use parameter::{visit_new_parameter, visit_set_parameter}; -pub use peer::visit_unregister_peer; +pub use peer::{visit_register_peer, visit_unregister_peer}; pub use permission_token::{visit_grant_account_permission, visit_revoke_account_permission}; pub use role::{ visit_grant_account_role, visit_register_role, visit_revoke_account_role, visit_unregister_role, }; pub use trigger::{ visit_burn_trigger_repetitions, visit_execute_trigger, visit_mint_trigger_repetitions, - visit_unregister_trigger, + visit_register_trigger, visit_unregister_trigger, }; use crate::{permission, permission::Token as _, prelude::*}; -macro_rules! evaluate_expr { - ($visitor:ident, $authority:ident, <$isi:ident as $isi_type:ty>::$field:ident()) => {{ - $visitor.visit_expression($authority, $isi.$field()); - - $visitor.evaluate($isi.$field()).dbg_expect(&alloc::format!( - "Failed to evaluate field '{}::{}'", - stringify!($isi_type), - stringify!($field), - )) - }}; -} - pub fn default_permission_token_schema() -> PermissionTokenSchema { let mut schema = iroha_executor::PermissionTokenSchema::default(); @@ -86,7 +80,7 @@ pub fn visit_transaction( } } -/// Default validation for [`InstructionExpr`]. +/// Default validation for [`InstructionBox`]. /// /// # Warning /// @@ -94,198 +88,53 @@ pub fn visit_transaction( pub fn visit_instruction( executor: &mut V, authority: &AccountId, - isi: &InstructionExpr, + isi: &InstructionBox, ) { - macro_rules! isi_executors { - ( - single {$( - $executor:ident($isi:ident) - ),+ $(,)?} - composite {$( - $composite_executor:ident($composite_isi:ident) - ),+ $(,)?} - ) => { - match isi { - InstructionExpr::NewParameter(isi) => { - let parameter = evaluate_expr!(executor, authority, ::parameter()); - executor.visit_new_parameter(authority, NewParameter{parameter}); - - if executor.verdict().is_ok() { - isi_executors!(@execute isi); - } - } - InstructionExpr::SetParameter(isi) => { - let parameter = evaluate_expr!(executor, authority, ::parameter()); - executor.visit_set_parameter(authority, SetParameter{parameter}); - - if executor.verdict().is_ok() { - isi_executors!(@execute isi); - } - } - InstructionExpr::ExecuteTrigger(isi) => { - let trigger_id = evaluate_expr!(executor, authority, ::trigger_id()); - executor.visit_execute_trigger(authority, ExecuteTrigger{trigger_id}); - - if executor.verdict().is_ok() { - isi_executors!(@execute isi); - } - } - InstructionExpr::Log(isi) => { - let msg = evaluate_expr!(executor, authority, ::msg()); - let level = evaluate_expr!(executor, authority, ::level()); - executor.visit_log(authority, Log{level, msg}); - - if executor.verdict().is_ok() { - isi_executors!(@execute isi); - } - } $( - InstructionExpr::$isi(isi) => { - executor.$executor(authority, isi); - - if executor.verdict().is_ok() { - isi_executors!(@execute isi); - } - } )+ $( - // NOTE: `visit_and_execute_instructions` is reentrant, so don't execute composite instructions - InstructionExpr::$composite_isi(isi) => executor.$composite_executor(authority, isi), )+ - } - }; - (@execute $isi:ident) => { - // TODO: Execution should be infallible after successful validation - if let Err(err) = isi.execute() { - executor.deny(err); - } + match isi { + InstructionBox::NewParameter(isi) => { + executor.visit_new_parameter(authority, isi); } - } - - isi_executors! { - single { - visit_burn(Burn), - visit_fail(Fail), - visit_grant(Grant), - visit_mint(Mint), - visit_register(Register), - visit_remove_key_value(RemoveKeyValue), - visit_revoke(Revoke), - visit_set_key_value(SetKeyValue), - visit_transfer(Transfer), - visit_unregister(Unregister), - visit_upgrade(Upgrade), - } - - composite { - visit_sequence(Sequence), - visit_pair(Pair), - visit_if(If), + InstructionBox::SetParameter(isi) => { + executor.visit_set_parameter(authority, isi); } - } -} - -pub fn visit_unsupported( - executor: &mut V, - _authority: &AccountId, - isi: T, -) { - deny!(executor, "{isi:?}: Unsupported operation"); -} - -pub fn visit_expression( - executor: &mut V, - authority: &AccountId, - expression: &EvaluatesTo, -) { - macro_rules! visit_binary_expression { - ($e:ident) => {{ - executor.visit_expression(authority, $e.left()); - - if executor.verdict().is_ok() { - executor.visit_expression(authority, $e.right()); - } - }}; - } - - match expression.expression() { - Expression::Add(expr) => visit_binary_expression!(expr), - Expression::Subtract(expr) => visit_binary_expression!(expr), - Expression::Multiply(expr) => visit_binary_expression!(expr), - Expression::Divide(expr) => visit_binary_expression!(expr), - Expression::Mod(expr) => visit_binary_expression!(expr), - Expression::RaiseTo(expr) => visit_binary_expression!(expr), - Expression::Greater(expr) => visit_binary_expression!(expr), - Expression::Less(expr) => visit_binary_expression!(expr), - Expression::Equal(expr) => visit_binary_expression!(expr), - Expression::Not(expr) => executor.visit_expression(authority, expr.expression()), - Expression::And(expr) => visit_binary_expression!(expr), - Expression::Or(expr) => visit_binary_expression!(expr), - Expression::If(expr) => { - executor.visit_expression(authority, expr.condition()); - - if executor.verdict().is_ok() { - executor.visit_expression(authority, expr.then()); - } - - if executor.verdict().is_ok() { - executor.visit_expression(authority, expr.otherwise()); - } + InstructionBox::Log(isi) => { + executor.visit_log(authority, isi); } - Expression::Contains(expr) => { - executor.visit_expression(authority, expr.collection()); - - if executor.verdict().is_ok() { - executor.visit_expression(authority, expr.element()); - } + InstructionBox::ExecuteTrigger(isi) => { + executor.visit_execute_trigger(authority, isi); } - Expression::ContainsAll(expr) => { - executor.visit_expression(authority, expr.collection()); - - if executor.verdict().is_ok() { - executor.visit_expression(authority, expr.elements()); - } + InstructionBox::Burn(isi) => { + executor.visit_burn(authority, isi); } - Expression::ContainsAny(expr) => { - executor.visit_expression(authority, expr.collection()); - - if executor.verdict().is_ok() { - executor.visit_expression(authority, expr.elements()); - } + InstructionBox::Fail(isi) => { + executor.visit_fail(authority, isi); } - Expression::Where(expr) => executor.visit_expression(authority, expr.expression()), - Expression::Query(query) => executor.visit_query(authority, query), - Expression::ContextValue(_) | Expression::Raw(_) => (), - } -} - -pub fn visit_if( - executor: &mut V, - authority: &AccountId, - isi: &ConditionalExpr, -) { - let condition = evaluate_expr!(executor, authority, ::condition()); - - // TODO: Do we have to make sure both branches are syntactically valid? - if condition { - executor.visit_instruction(authority, isi.then()); - } else if let Some(otherwise) = isi.otherwise() { - executor.visit_instruction(authority, otherwise); - } -} - -pub fn visit_pair(executor: &mut V, authority: &AccountId, isi: &PairExpr) { - executor.visit_instruction(authority, isi.left_instruction()); - - if executor.verdict().is_ok() { - executor.visit_instruction(authority, isi.right_instruction()) - } -} - -pub fn visit_sequence( - executor: &mut V, - authority: &AccountId, - sequence: &SequenceExpr, -) { - for isi in sequence.instructions() { - if executor.verdict().is_ok() { - executor.visit_instruction(authority, isi); + InstructionBox::Grant(isi) => { + executor.visit_grant(authority, isi); + } + InstructionBox::Mint(isi) => { + executor.visit_mint(authority, isi); + } + InstructionBox::Register(isi) => { + executor.visit_register(authority, isi); + } + InstructionBox::RemoveKeyValue(isi) => { + executor.visit_remove_key_value(authority, isi); + } + InstructionBox::Revoke(isi) => { + executor.visit_revoke(authority, isi); + } + InstructionBox::SetKeyValue(isi) => { + executor.visit_set_key_value(authority, isi); + } + InstructionBox::Transfer(isi) => { + executor.visit_transfer(authority, isi); + } + InstructionBox::Unregister(isi) => { + executor.visit_unregister(authority, isi); + } + InstructionBox::Upgrade(isi) => { + executor.visit_upgrade(authority, isi); } } } @@ -293,17 +142,25 @@ pub fn visit_sequence( pub mod peer { use super::*; + pub fn visit_register_peer( + executor: &mut V, + _authority: &AccountId, + isi: &Register, + ) { + execute!(executor, isi) + } + #[allow(clippy::needless_pass_by_value)] pub fn visit_unregister_peer( executor: &mut V, authority: &AccountId, - _isi: Unregister, + isi: &Unregister, ) { if is_genesis(executor) { - pass!(executor); + execute!(executor, isi); } if tokens::peer::CanUnregisterAnyPeer.is_owned_by(authority) { - pass!(executor); + execute!(executor, isi); } deny!(executor, "Can't unregister peer"); @@ -315,24 +172,34 @@ pub mod domain { use super::*; + pub fn visit_register_domain( + executor: &mut V, + _authority: &AccountId, + isi: &Register, + ) { + execute!(executor, isi) + } + pub fn visit_unregister_domain( executor: &mut V, authority: &AccountId, - isi: Unregister, + isi: &Unregister, ) { - let domain_id = isi.object_id; + let domain_id = isi.object_id(); if is_genesis(executor) { - pass!(executor); + execute!(executor, isi); } - match is_domain_owner(&domain_id, authority) { + match is_domain_owner(domain_id, authority) { Err(err) => deny!(executor, err), - Ok(true) => pass!(executor), + Ok(true) => execute!(executor, isi), Ok(false) => {} } - let can_unregister_domain_token = tokens::domain::CanUnregisterDomain { domain_id }; + let can_unregister_domain_token = tokens::domain::CanUnregisterDomain { + domain_id: domain_id.clone(), + }; if can_unregister_domain_token.is_owned_by(authority) { - pass!(executor); + execute!(executor, isi); } deny!(executor, "Can't unregister domain"); @@ -341,16 +208,16 @@ pub mod domain { pub fn visit_transfer_domain( executor: &mut V, authority: &AccountId, - isi: Transfer, + isi: &Transfer, ) { - let destination_id = isi.object; + let destination_id = isi.object(); if is_genesis(executor) { - pass!(executor); + execute!(executor, isi); } - match is_domain_owner(&destination_id, authority) { + match is_domain_owner(destination_id, authority) { Err(err) => deny!(executor, err), - Ok(true) => pass!(executor), + Ok(true) => execute!(executor, isi), Ok(false) => {} } @@ -360,22 +227,23 @@ pub mod domain { pub fn visit_set_domain_key_value( executor: &mut V, authority: &AccountId, - isi: SetKeyValue, + isi: &SetKeyValue, ) { - let domain_id = isi.object_id; + let domain_id = isi.object_id(); if is_genesis(executor) { - pass!(executor); + execute!(executor, isi); } - match is_domain_owner(&domain_id, authority) { + match is_domain_owner(domain_id, authority) { Err(err) => deny!(executor, err), - Ok(true) => pass!(executor), + Ok(true) => execute!(executor, isi), Ok(false) => {} } - let can_set_key_value_in_domain_token = - tokens::domain::CanSetKeyValueInDomain { domain_id }; + let can_set_key_value_in_domain_token = tokens::domain::CanSetKeyValueInDomain { + domain_id: domain_id.clone(), + }; if can_set_key_value_in_domain_token.is_owned_by(authority) { - pass!(executor); + execute!(executor, isi); } deny!(executor, "Can't set key value in domain metadata"); @@ -384,22 +252,23 @@ pub mod domain { pub fn visit_remove_domain_key_value( executor: &mut V, authority: &AccountId, - isi: RemoveKeyValue, + isi: &RemoveKeyValue, ) { - let domain_id = isi.object_id; + let domain_id = isi.object_id(); if is_genesis(executor) { - pass!(executor); + execute!(executor, isi); } - match is_domain_owner(&domain_id, authority) { + match is_domain_owner(domain_id, authority) { Err(err) => deny!(executor, err), - Ok(true) => pass!(executor), + Ok(true) => execute!(executor, isi), Ok(false) => {} } - let can_remove_key_value_in_domain_token = - tokens::domain::CanRemoveKeyValueInDomain { domain_id }; + let can_remove_key_value_in_domain_token = tokens::domain::CanRemoveKeyValueInDomain { + domain_id: domain_id.clone(), + }; if can_remove_key_value_in_domain_token.is_owned_by(authority) { - pass!(executor); + execute!(executor, isi); } deny!(executor, "Can't remove key value in domain metadata"); @@ -411,24 +280,34 @@ pub mod account { use super::*; + pub fn visit_register_account( + executor: &mut V, + _authority: &AccountId, + isi: &Register, + ) { + execute!(executor, isi) + } + pub fn visit_unregister_account( executor: &mut V, authority: &AccountId, - isi: Unregister, + isi: &Unregister, ) { - let account_id = isi.object_id; + let account_id = isi.object_id(); if is_genesis(executor) { - pass!(executor); + execute!(executor, isi); } - match is_account_owner(&account_id, authority) { + match is_account_owner(account_id, authority) { Err(err) => deny!(executor, err), - Ok(true) => pass!(executor), + Ok(true) => execute!(executor, isi), Ok(false) => {} } - let can_unregister_user_account = tokens::account::CanUnregisterAccount { account_id }; + let can_unregister_user_account = tokens::account::CanUnregisterAccount { + account_id: account_id.clone(), + }; if can_unregister_user_account.is_owned_by(authority) { - pass!(executor); + execute!(executor, isi); } deny!(executor, "Can't unregister another account"); @@ -437,21 +316,23 @@ pub mod account { pub fn visit_mint_account_public_key( executor: &mut V, authority: &AccountId, - isi: Mint, + isi: &Mint, ) { - let account_id = isi.destination_id; + let account_id = isi.destination_id(); if is_genesis(executor) { - pass!(executor); + execute!(executor, isi); } - match is_account_owner(&account_id, authority) { + match is_account_owner(account_id, authority) { Err(err) => deny!(executor, err), - Ok(true) => pass!(executor), + Ok(true) => execute!(executor, isi), Ok(false) => {} } - let can_mint_user_public_keys = tokens::account::CanMintUserPublicKeys { account_id }; + let can_mint_user_public_keys = tokens::account::CanMintUserPublicKeys { + account_id: account_id.clone(), + }; if can_mint_user_public_keys.is_owned_by(authority) { - pass!(executor); + execute!(executor, isi); } deny!(executor, "Can't mint public keys of another account"); @@ -460,21 +341,23 @@ pub mod account { pub fn visit_burn_account_public_key( executor: &mut V, authority: &AccountId, - isi: Burn, + isi: &Burn, ) { - let account_id = isi.destination_id; + let account_id = isi.destination_id(); if is_genesis(executor) { - pass!(executor); + execute!(executor, isi); } - match is_account_owner(&account_id, authority) { + match is_account_owner(account_id, authority) { Err(err) => deny!(executor, err), - Ok(true) => pass!(executor), + Ok(true) => execute!(executor, isi), Ok(false) => {} } - let can_burn_user_public_keys = tokens::account::CanBurnUserPublicKeys { account_id }; + let can_burn_user_public_keys = tokens::account::CanBurnUserPublicKeys { + account_id: account_id.clone(), + }; if can_burn_user_public_keys.is_owned_by(authority) { - pass!(executor); + execute!(executor, isi); } deny!(executor, "Can't burn public keys of another account"); @@ -483,22 +366,24 @@ pub mod account { pub fn visit_mint_account_signature_check_condition( executor: &mut V, authority: &AccountId, - isi: Mint, + isi: &Mint, ) { - let account_id = isi.destination_id; + let account_id = isi.destination_id(); if is_genesis(executor) { - pass!(executor); + execute!(executor, isi); } - match is_account_owner(&account_id, authority) { + match is_account_owner(account_id, authority) { Err(err) => deny!(executor, err), - Ok(true) => pass!(executor), + Ok(true) => execute!(executor, isi), Ok(false) => {} } let can_mint_user_signature_check_conditions_token = - tokens::account::CanMintUserSignatureCheckConditions { account_id }; + tokens::account::CanMintUserSignatureCheckConditions { + account_id: account_id.clone(), + }; if can_mint_user_signature_check_conditions_token.is_owned_by(authority) { - pass!(executor); + execute!(executor, isi); } deny!( @@ -510,22 +395,24 @@ pub mod account { pub fn visit_set_account_key_value( executor: &mut V, authority: &AccountId, - isi: SetKeyValue, + isi: &SetKeyValue, ) { - let account_id = isi.object_id; + let account_id = isi.object_id(); if is_genesis(executor) { - pass!(executor); + execute!(executor, isi); } - match is_account_owner(&account_id, authority) { + match is_account_owner(account_id, authority) { Err(err) => deny!(executor, err), - Ok(true) => pass!(executor), + Ok(true) => execute!(executor, isi), Ok(false) => {} } let can_set_key_value_in_user_account_token = - tokens::account::CanSetKeyValueInUserAccount { account_id }; + tokens::account::CanSetKeyValueInUserAccount { + account_id: account_id.clone(), + }; if can_set_key_value_in_user_account_token.is_owned_by(authority) { - pass!(executor); + execute!(executor, isi); } deny!( @@ -537,22 +424,24 @@ pub mod account { pub fn visit_remove_account_key_value( executor: &mut V, authority: &AccountId, - isi: RemoveKeyValue, + isi: &RemoveKeyValue, ) { - let account_id = isi.object_id; + let account_id = isi.object_id(); if is_genesis(executor) { - pass!(executor); + execute!(executor, isi); } - match is_account_owner(&account_id, authority) { + match is_account_owner(account_id, authority) { Err(err) => deny!(executor, err), - Ok(true) => pass!(executor), + Ok(true) => execute!(executor, isi), Ok(false) => {} } let can_remove_key_value_in_user_account_token = - tokens::account::CanRemoveKeyValueInUserAccount { account_id }; + tokens::account::CanRemoveKeyValueInUserAccount { + account_id: account_id.clone(), + }; if can_remove_key_value_in_user_account_token.is_owned_by(authority) { - pass!(executor); + execute!(executor, isi); } deny!( @@ -567,27 +456,35 @@ pub mod asset_definition { use super::*; + pub fn visit_register_asset_definition( + executor: &mut V, + _authority: &AccountId, + isi: &Register, + ) { + execute!(executor, isi); + } + pub fn visit_unregister_asset_definition( executor: &mut V, authority: &AccountId, - isi: Unregister, + isi: &Unregister, ) { - let asset_definition_id = isi.object_id; + let asset_definition_id = isi.object_id(); if is_genesis(executor) { - pass!(executor); + execute!(executor, isi); } - match is_asset_definition_owner(&asset_definition_id, authority) { + match is_asset_definition_owner(asset_definition_id, authority) { Err(err) => deny!(executor, err), - Ok(true) => pass!(executor), + Ok(true) => execute!(executor, isi), Ok(false) => {} } let can_unregister_asset_definition_token = tokens::asset_definition::CanUnregisterAssetDefinition { - asset_definition_id, + asset_definition_id: asset_definition_id.clone(), }; if can_unregister_asset_definition_token.is_owned_by(authority) { - pass!(executor); + execute!(executor, isi); } deny!( @@ -599,22 +496,22 @@ pub mod asset_definition { pub fn visit_transfer_asset_definition( executor: &mut V, authority: &AccountId, - isi: Transfer, + isi: &Transfer, ) { - let source_id = isi.source_id; - let destination_id = isi.object; + let source_id = isi.source_id(); + let destination_id = isi.object(); if is_genesis(executor) { - pass!(executor); + execute!(executor, isi); } - match is_account_owner(&source_id, authority) { + match is_account_owner(source_id, authority) { Err(err) => deny!(executor, err), - Ok(true) => pass!(executor), + Ok(true) => execute!(executor, isi), Ok(false) => {} } - match is_asset_definition_owner(&destination_id, authority) { + match is_asset_definition_owner(destination_id, authority) { Err(err) => deny!(executor, err), - Ok(true) => pass!(executor), + Ok(true) => execute!(executor, isi), Ok(false) => {} } @@ -627,24 +524,24 @@ pub mod asset_definition { pub fn visit_set_asset_definition_key_value( executor: &mut V, authority: &AccountId, - isi: SetKeyValue, + isi: &SetKeyValue, ) { - let asset_definition_id = isi.object_id; + let asset_definition_id = isi.object_id(); if is_genesis(executor) { - pass!(executor); + execute!(executor, isi); } - match is_asset_definition_owner(&asset_definition_id, authority) { + match is_asset_definition_owner(asset_definition_id, authority) { Err(err) => deny!(executor, err), - Ok(true) => pass!(executor), + Ok(true) => execute!(executor, isi), Ok(false) => {} } let can_set_key_value_in_asset_definition_token = tokens::asset_definition::CanSetKeyValueInAssetDefinition { - asset_definition_id, + asset_definition_id: asset_definition_id.clone(), }; if can_set_key_value_in_asset_definition_token.is_owned_by(authority) { - pass!(executor); + execute!(executor, isi); } deny!( @@ -656,24 +553,24 @@ pub mod asset_definition { pub fn visit_remove_asset_definition_key_value( executor: &mut V, authority: &AccountId, - isi: RemoveKeyValue, + isi: &RemoveKeyValue, ) { - let asset_definition_id = isi.object_id; + let asset_definition_id = isi.object_id(); if is_genesis(executor) { - pass!(executor); + execute!(executor, isi); } - match is_asset_definition_owner(&asset_definition_id, authority) { + match is_asset_definition_owner(asset_definition_id, authority) { Err(err) => deny!(executor, err), - Ok(true) => pass!(executor), + Ok(true) => execute!(executor, isi), Ok(false) => {} } let can_remove_key_value_in_asset_definition_token = tokens::asset_definition::CanRemoveKeyValueInAssetDefinition { - asset_definition_id, + asset_definition_id: asset_definition_id.clone(), }; if can_remove_key_value_in_asset_definition_token.is_owned_by(authority) { - pass!(executor); + execute!(executor, isi); } deny!( @@ -684,6 +581,8 @@ pub mod asset_definition { } pub mod asset { + use iroha_smart_contract::data_model::isi::Instruction; + use iroha_smart_contract_utils::Encode; use permission::{asset::is_asset_owner, asset_definition::is_asset_definition_owner}; use super::*; @@ -691,16 +590,16 @@ pub mod asset { pub fn visit_register_asset( executor: &mut V, authority: &AccountId, - isi: Register, + isi: &Register, ) { - let asset = isi.object; + let asset = isi.object(); if is_genesis(executor) { - pass!(executor); + execute!(executor, isi); } match is_asset_definition_owner(asset.id().definition_id(), authority) { Err(err) => deny!(executor, err), - Ok(true) => pass!(executor), + Ok(true) => execute!(executor, isi), Ok(false) => {} } let can_register_assets_with_definition_token = @@ -708,7 +607,7 @@ pub mod asset { asset_definition_id: asset.id().definition_id().clone(), }; if can_register_assets_with_definition_token.is_owned_by(authority) { - pass!(executor); + execute!(executor, isi); } deny!( @@ -720,21 +619,21 @@ pub mod asset { pub fn visit_unregister_asset( executor: &mut V, authority: &AccountId, - isi: Unregister, + isi: &Unregister, ) { - let asset_id = isi.object_id; + let asset_id = isi.object_id(); if is_genesis(executor) { - pass!(executor); + execute!(executor, isi); } - match is_asset_owner(&asset_id, authority) { + match is_asset_owner(asset_id, authority) { Err(err) => deny!(executor, err), - Ok(true) => pass!(executor), + Ok(true) => execute!(executor, isi), Ok(false) => {} } match is_asset_definition_owner(asset_id.definition_id(), authority) { Err(err) => deny!(executor, err), - Ok(true) => pass!(executor), + Ok(true) => execute!(executor, isi), Ok(false) => {} } let can_unregister_assets_with_definition_token = @@ -742,36 +641,38 @@ pub mod asset { asset_definition_id: asset_id.definition_id().clone(), }; if can_unregister_assets_with_definition_token.is_owned_by(authority) { - pass!(executor); + execute!(executor, isi); } - let can_unregister_user_asset_token = tokens::asset::CanUnregisterUserAsset { asset_id }; + let can_unregister_user_asset_token = tokens::asset::CanUnregisterUserAsset { + asset_id: asset_id.clone(), + }; if can_unregister_user_asset_token.is_owned_by(authority) { - pass!(executor); + execute!(executor, isi); } deny!(executor, "Can't unregister asset from another account"); } - pub fn visit_mint_asset( - executor: &mut V, - authority: &AccountId, - isi: Mint, - ) { - let asset_id = isi.destination_id; - + fn validate_mint_asset(executor: &mut V, authority: &AccountId, isi: &Mint) + where + V: Validate + ?Sized, + Q: Into, + Mint: Instruction + Encode + Clone, + { + let asset_id = isi.destination_id(); if is_genesis(executor) { - pass!(executor); + execute!(executor, isi); } match is_asset_definition_owner(asset_id.definition_id(), authority) { Err(err) => deny!(executor, err), - Ok(true) => pass!(executor), + Ok(true) => execute!(executor, isi), Ok(false) => {} } let can_mint_assets_with_definition_token = tokens::asset::CanMintAssetsWithDefinition { asset_definition_id: asset_id.definition_id().clone(), }; if can_mint_assets_with_definition_token.is_owned_by(authority) { - pass!(executor); + execute!(executor, isi); } deny!( @@ -780,58 +681,111 @@ pub mod asset { ); } - pub fn visit_burn_asset( + pub fn visit_mint_asset_quantity( executor: &mut V, authority: &AccountId, - isi: Burn, + isi: &Mint, ) { - let asset_id = isi.destination_id; + validate_mint_asset(executor, authority, isi); + } + pub fn visit_mint_asset_big_quantity( + executor: &mut V, + authority: &AccountId, + isi: &Mint, + ) { + validate_mint_asset(executor, authority, isi); + } + + pub fn visit_mint_asset_fixed( + executor: &mut V, + authority: &AccountId, + isi: &Mint, + ) { + validate_mint_asset(executor, authority, isi); + } + + fn validate_burn_asset(executor: &mut V, authority: &AccountId, isi: &Burn) + where + V: Validate + ?Sized, + Q: Into, + Burn: Instruction + Encode + Clone, + { + let asset_id = isi.destination_id(); if is_genesis(executor) { - pass!(executor); + execute!(executor, isi); } - match is_asset_owner(&asset_id, authority) { + match is_asset_owner(asset_id, authority) { Err(err) => deny!(executor, err), - Ok(true) => pass!(executor), + Ok(true) => execute!(executor, isi), Ok(false) => {} } match is_asset_definition_owner(asset_id.definition_id(), authority) { Err(err) => deny!(executor, err), - Ok(true) => pass!(executor), + Ok(true) => execute!(executor, isi), Ok(false) => {} } let can_burn_assets_with_definition_token = tokens::asset::CanBurnAssetsWithDefinition { asset_definition_id: asset_id.definition_id().clone(), }; if can_burn_assets_with_definition_token.is_owned_by(authority) { - pass!(executor); + execute!(executor, isi); } - let can_burn_user_asset_token = tokens::asset::CanBurnUserAsset { asset_id }; + let can_burn_user_asset_token = tokens::asset::CanBurnUserAsset { + asset_id: asset_id.clone(), + }; if can_burn_user_asset_token.is_owned_by(authority) { - pass!(executor); + execute!(executor, isi); } deny!(executor, "Can't burn assets from another account"); } - pub fn visit_transfer_asset( + pub fn visit_burn_asset_quantity( + executor: &mut V, + authority: &AccountId, + isi: &Burn, + ) { + validate_burn_asset(executor, authority, isi); + } + + pub fn visit_burn_asset_big_quantity( + executor: &mut V, + authority: &AccountId, + isi: &Burn, + ) { + validate_burn_asset(executor, authority, isi); + } + + pub fn visit_burn_asset_fixed( executor: &mut V, authority: &AccountId, - isi: Transfer, + isi: &Burn, ) { - let asset_id = isi.source_id; + validate_burn_asset(executor, authority, isi); + } + fn validate_transfer_asset( + executor: &mut V, + authority: &AccountId, + isi: &Transfer, + ) where + V: Validate + ?Sized, + Q: Into, + Transfer: Instruction + Encode + Clone, + { + let asset_id = isi.source_id(); if is_genesis(executor) { - pass!(executor); + execute!(executor, isi); } - match is_asset_owner(&asset_id, authority) { + match is_asset_owner(asset_id, authority) { Err(err) => deny!(executor, err), - Ok(true) => pass!(executor), + Ok(true) => execute!(executor, isi), Ok(false) => {} } match is_asset_definition_owner(asset_id.definition_id(), authority) { Err(err) => deny!(executor, err), - Ok(true) => pass!(executor), + Ok(true) => execute!(executor, isi), Ok(false) => {} } let can_transfer_assets_with_definition_token = @@ -839,36 +793,63 @@ pub mod asset { asset_definition_id: asset_id.definition_id().clone(), }; if can_transfer_assets_with_definition_token.is_owned_by(authority) { - pass!(executor); + execute!(executor, isi); } - let can_transfer_user_asset_token = tokens::asset::CanTransferUserAsset { asset_id }; + let can_transfer_user_asset_token = tokens::asset::CanTransferUserAsset { + asset_id: asset_id.clone(), + }; if can_transfer_user_asset_token.is_owned_by(authority) { - pass!(executor); + execute!(executor, isi); } deny!(executor, "Can't transfer assets of another account"); } + pub fn visit_transfer_asset_quantity( + executor: &mut V, + authority: &AccountId, + isi: &Transfer, + ) { + validate_transfer_asset(executor, authority, isi); + } + + pub fn visit_transfer_asset_big_quantity( + executor: &mut V, + authority: &AccountId, + isi: &Transfer, + ) { + validate_transfer_asset(executor, authority, isi); + } + + pub fn visit_transfer_asset_fixed( + executor: &mut V, + authority: &AccountId, + isi: &Transfer, + ) { + validate_transfer_asset(executor, authority, isi); + } + pub fn visit_set_asset_key_value( executor: &mut V, authority: &AccountId, - isi: SetKeyValue, + isi: &SetKeyValue, ) { - let asset_id = isi.object_id; + let asset_id = isi.object_id(); if is_genesis(executor) { - pass!(executor); + execute!(executor, isi); } - match is_asset_owner(&asset_id, authority) { + match is_asset_owner(asset_id, authority) { Err(err) => deny!(executor, err), - Ok(true) => pass!(executor), + Ok(true) => execute!(executor, isi), Ok(false) => {} } - let can_set_key_value_in_user_asset_token = - tokens::asset::CanSetKeyValueInUserAsset { asset_id }; + let can_set_key_value_in_user_asset_token = tokens::asset::CanSetKeyValueInUserAsset { + asset_id: asset_id.clone(), + }; if can_set_key_value_in_user_asset_token.is_owned_by(authority) { - pass!(executor); + execute!(executor, isi); } deny!( @@ -880,22 +861,24 @@ pub mod asset { pub fn visit_remove_asset_key_value( executor: &mut V, authority: &AccountId, - isi: RemoveKeyValue, + isi: &RemoveKeyValue, ) { - let asset_id = isi.object_id; + let asset_id = isi.object_id(); if is_genesis(executor) { - pass!(executor); + execute!(executor, isi); } - match is_asset_owner(&asset_id, authority) { + match is_asset_owner(asset_id, authority) { Err(err) => deny!(executor, err), - Ok(true) => pass!(executor), + Ok(true) => execute!(executor, isi), Ok(false) => {} } let can_remove_key_value_in_user_asset_token = - tokens::asset::CanRemoveKeyValueInUserAsset { asset_id }; + tokens::asset::CanRemoveKeyValueInUserAsset { + asset_id: asset_id.clone(), + }; if can_remove_key_value_in_user_asset_token.is_owned_by(authority) { - pass!(executor); + execute!(executor, isi); } deny!( @@ -912,13 +895,13 @@ pub mod parameter { pub fn visit_new_parameter( executor: &mut V, authority: &AccountId, - _isi: NewParameter, + isi: &NewParameter, ) { if is_genesis(executor) { - pass!(executor); + execute!(executor, isi); } if tokens::parameter::CanCreateParameters.is_owned_by(authority) { - pass!(executor); + execute!(executor, isi); } deny!( @@ -931,13 +914,13 @@ pub mod parameter { pub fn visit_set_parameter( executor: &mut V, authority: &AccountId, - _isi: SetParameter, + isi: &SetParameter, ) { if is_genesis(executor) { - pass!(executor); + execute!(executor, isi); } if tokens::parameter::CanSetParameters.is_owned_by(authority) { - pass!(executor); + execute!(executor, isi); } deny!( @@ -952,9 +935,9 @@ pub mod role { macro_rules! impl_validate { ($executor:ident, $isi:ident, $authority:ident, $method:ident) => { - let role_id = $isi.object; + let role_id = $isi.object(); - let find_role_query_res = match FindRoleByRoleId::new(role_id).execute() { + let find_role_query_res = match FindRoleByRoleId::new(role_id.clone()).execute() { Ok(res) => res.into_raw_parts().0, Err(error) => { deny!($executor, error); @@ -986,6 +969,7 @@ pub mod role { } assert!(unknown_tokens.is_empty(), "Role contains unknown permission tokens: {unknown_tokens:?}"); + execute!($executor, $isi) }; } @@ -993,9 +977,9 @@ pub mod role { pub fn visit_register_role( executor: &mut V, _authority: &AccountId, - isi: Register, + isi: &Register, ) { - let role = isi.object.inner(); + let role = isi.object().inner(); let mut unknown_tokens = Vec::new(); for token in role.permissions() { @@ -1021,20 +1005,20 @@ pub mod role { ); } - pass!(executor); + execute!(executor, isi); } #[allow(clippy::needless_pass_by_value)] pub fn visit_unregister_role( executor: &mut V, authority: &AccountId, - _isi: Unregister, + isi: &Unregister, ) { if is_genesis(executor) { - pass!(executor); + execute!(executor, isi); } if tokens::role::CanUnregisterAnyRole.is_owned_by(authority) { - pass!(executor); + execute!(executor, isi); } deny!(executor, "Can't unregister role"); @@ -1043,7 +1027,7 @@ pub mod role { pub fn visit_grant_account_role( executor: &mut V, authority: &AccountId, - isi: Grant, + isi: &Grant, ) { impl_validate!(executor, isi, authority, validate_grant); } @@ -1051,7 +1035,7 @@ pub mod role { pub fn visit_revoke_account_role( executor: &mut V, authority: &AccountId, - isi: Revoke, + isi: &Revoke, ) { impl_validate!(executor, isi, authority, validate_revoke); } @@ -1062,25 +1046,34 @@ pub mod trigger { use super::*; + pub fn visit_register_trigger( + executor: &mut V, + _authority: &AccountId, + isi: &Register>, + ) { + execute!(executor, isi) + } + pub fn visit_unregister_trigger( executor: &mut V, authority: &AccountId, - isi: Unregister>, + isi: &Unregister>, ) { - let trigger_id = isi.object_id; + let trigger_id = isi.object_id(); if is_genesis(executor) { - pass!(executor); + execute!(executor, isi); } - match is_trigger_owner(&trigger_id, authority) { + match is_trigger_owner(trigger_id, authority) { Err(err) => deny!(executor, err), - Ok(true) => pass!(executor), + Ok(true) => execute!(executor, isi), Ok(false) => {} } - let can_unregister_user_trigger_token = - tokens::trigger::CanUnregisterUserTrigger { trigger_id }; + let can_unregister_user_trigger_token = tokens::trigger::CanUnregisterUserTrigger { + trigger_id: trigger_id.clone(), + }; if can_unregister_user_trigger_token.is_owned_by(authority) { - pass!(executor); + execute!(executor, isi); } deny!( @@ -1092,21 +1085,23 @@ pub mod trigger { pub fn visit_mint_trigger_repetitions( executor: &mut V, authority: &AccountId, - isi: Mint>, + isi: &Mint>, ) { - let trigger_id = isi.destination_id; + let trigger_id = isi.destination_id(); if is_genesis(executor) { - pass!(executor); + execute!(executor, isi); } - match is_trigger_owner(&trigger_id, authority) { + match is_trigger_owner(trigger_id, authority) { Err(err) => deny!(executor, err), - Ok(true) => pass!(executor), + Ok(true) => execute!(executor, isi), Ok(false) => {} } - let can_mint_user_trigger_token = tokens::trigger::CanMintUserTrigger { trigger_id }; + let can_mint_user_trigger_token = tokens::trigger::CanMintUserTrigger { + trigger_id: trigger_id.clone(), + }; if can_mint_user_trigger_token.is_owned_by(authority) { - pass!(executor); + execute!(executor, isi); } deny!( @@ -1118,21 +1113,23 @@ pub mod trigger { pub fn visit_burn_trigger_repetitions( executor: &mut V, authority: &AccountId, - isi: Burn>, + isi: &Burn>, ) { - let trigger_id = isi.destination_id; + let trigger_id = isi.destination_id(); if is_genesis(executor) { - pass!(executor); + execute!(executor, isi); } - match is_trigger_owner(&trigger_id, authority) { + match is_trigger_owner(trigger_id, authority) { Err(err) => deny!(executor, err), - Ok(true) => pass!(executor), + Ok(true) => execute!(executor, isi), Ok(false) => {} } - let can_mint_user_trigger_token = tokens::trigger::CanBurnUserTrigger { trigger_id }; + let can_mint_user_trigger_token = tokens::trigger::CanBurnUserTrigger { + trigger_id: trigger_id.clone(), + }; if can_mint_user_trigger_token.is_owned_by(authority) { - pass!(executor); + execute!(executor, isi); } deny!( @@ -1144,21 +1141,23 @@ pub mod trigger { pub fn visit_execute_trigger( executor: &mut V, authority: &AccountId, - isi: ExecuteTrigger, + isi: &ExecuteTrigger, ) { - let trigger_id = isi.trigger_id; + let trigger_id = isi.trigger_id(); if is_genesis(executor) { - pass!(executor); + execute!(executor, isi); } - match is_trigger_owner(&trigger_id, authority) { + match is_trigger_owner(trigger_id, authority) { Err(err) => deny!(executor, err), - Ok(true) => pass!(executor), + Ok(true) => execute!(executor, isi), Ok(false) => {} } - let can_execute_trigger_token = tokens::trigger::CanExecuteUserTrigger { trigger_id }; + let can_execute_trigger_token = tokens::trigger::CanExecuteUserTrigger { + trigger_id: trigger_id.clone(), + }; if can_execute_trigger_token.is_owned_by(authority) { - pass!(executor); + execute!(executor, isi); } deny!(executor, "Can't execute trigger owned by another account"); @@ -1169,13 +1168,14 @@ pub mod permission_token { use super::*; macro_rules! impl_validate { - ($executor:ident, $authority:ident, $self:ident, $method:ident) => { - let token = $self.object; + ($executor:ident, $authority:ident, $isi:ident, $method:ident) => { + // TODO: https://github.com/hyperledger/iroha/issues/4082 + let token = $isi.object().clone(); macro_rules! visit_internal { ($token:ident) => { if is_genesis($executor) { - pass!($executor); + execute!($executor, $isi); } if let Err(error) = permission::ValidateGrantRevoke::$method( &$token, @@ -1185,7 +1185,7 @@ pub mod permission_token { deny!($executor, error); } - pass!($executor); + execute!($executor, $isi); }; } @@ -1201,7 +1201,7 @@ pub mod permission_token { pub fn visit_grant_account_permission( executor: &mut V, authority: &AccountId, - isi: Grant, + isi: &Grant, ) { impl_validate!(executor, authority, isi, validate_grant); } @@ -1209,7 +1209,7 @@ pub mod permission_token { pub fn visit_revoke_account_permission( executor: &mut V, authority: &AccountId, - isi: Revoke, + isi: &Revoke, ) { impl_validate!(executor, authority, isi, validate_revoke); } @@ -1219,22 +1219,38 @@ pub mod executor { use super::*; #[allow(clippy::needless_pass_by_value)] - pub fn visit_upgrade_executor( + pub fn visit_upgrade( executor: &mut V, authority: &AccountId, - _isi: Upgrade, + isi: &Upgrade, ) { if is_genesis(executor) { - pass!(executor); + execute!(executor, isi); } if tokens::executor::CanUpgradeExecutor.is_owned_by(authority) { - pass!(executor); + execute!(executor, isi); } deny!(executor, "Can't upgrade executor"); } } +pub mod log { + use super::*; + + pub fn visit_log(executor: &mut V, _authority: &AccountId, isi: &Log) { + execute!(executor, isi) + } +} + +pub mod fail { + use super::*; + + pub fn visit_fail(executor: &mut V, _authority: &AccountId, isi: &Fail) { + execute!(executor, isi) + } +} + fn is_genesis(executor: &V) -> bool { executor.block_height() == 0 } diff --git a/smart_contract/executor/src/lib.rs b/smart_contract/executor/src/lib.rs index 8b9a7403a52..ef953f78f14 100644 --- a/smart_contract/executor/src/lib.rs +++ b/smart_contract/executor/src/lib.rs @@ -49,7 +49,7 @@ pub fn get_validate_transaction_payload() -> payloads::Validate payloads::Validate { +pub fn get_validate_instruction_payload() -> payloads::Validate { // Safety: ownership of the returned result is transferred into `_decode_from_raw` unsafe { decode_with_length_prefix_from_raw(host::get_validate_instruction_payload()) } } @@ -131,20 +131,23 @@ mod host { } } -/// Shortcut for `return Ok(())`. +/// Execute instruction if verdict is [`Ok`], deny if execution failed and return. +/// +/// Convention is that you have no checks left if you decided to execute instruction. #[macro_export] -macro_rules! pass { - ($executor:ident) => {{ - #[cfg(debug_assertions)] - if let Err(_error) = $executor.verdict() { - unreachable!("Executor already denied"); +macro_rules! execute { + ($executor:ident, $isi:ident) => {{ + if $executor.verdict().is_ok() { + if let Err(err) = $isi.execute() { + $executor.deny(err); + } } return; }}; } -/// Shortcut for `return Err(ValidationFail)`. +/// Shortcut for setting verdict to [`Err`] and return. /// /// Supports [`format!`](alloc::fmt::format) syntax as well as any expression returning [`String`](alloc::string::String). #[macro_export] @@ -219,10 +222,9 @@ pub mod prelude { pub use alloc::vec::Vec; pub use iroha_executor_derive::{ - entrypoint, Constructor, ExpressionEvaluator, Token, Validate, ValidateEntrypoints, - ValidateGrantRevoke, Visit, + entrypoint, Constructor, Token, Validate, ValidateEntrypoints, ValidateGrantRevoke, Visit, }; - pub use iroha_smart_contract::{prelude::*, Context}; + pub use iroha_smart_contract::prelude::*; pub use super::{ data_model::{ @@ -230,6 +232,6 @@ pub mod prelude { visit::Visit, ValidationFail, }, - deny, pass, PermissionTokenSchema, Validate, + deny, execute, PermissionTokenSchema, Validate, }; } diff --git a/smart_contract/src/lib.rs b/smart_contract/src/lib.rs index 45232f9cdfb..2bdd93706db 100644 --- a/smart_contract/src/lib.rs +++ b/smart_contract/src/lib.rs @@ -4,14 +4,14 @@ extern crate alloc; -use alloc::{boxed::Box, collections::BTreeMap, vec::Vec}; +use alloc::{boxed::Box, vec::Vec}; #[cfg(not(test))] use data_model::smart_contract::payloads; use data_model::{ isi::Instruction, prelude::*, - query::{cursor::ForwardCursor, sorting::Sorting, Pagination, Query, QueryBox}, + query::{cursor::ForwardCursor, sorting::Sorting, Pagination, Query}, smart_contract::SmartContractQueryRequest, BatchedResponse, }; @@ -88,7 +88,7 @@ impl ExecuteOnHost for I { use tests::_iroha_smart_contract_execute_instruction_mock as host_execute_instruction; // TODO: Redundant conversion into `InstructionExpr` - let isi_box: InstructionExpr = self.clone().into(); + let isi_box: InstructionBox = self.clone().into(); // Safety: `host_execute_instruction` doesn't take ownership of it's pointer parameter unsafe { decode_with_length_prefix_from_raw(encode_and_execute( @@ -374,52 +374,6 @@ pub enum QueryOutputCursorError { #[derive(Debug, Clone, Copy)] pub struct Host; -impl iroha_data_model::evaluate::ExpressionEvaluator for Host { - fn evaluate( - &self, - expression: &E, - ) -> Result { - expression.evaluate(&Context::new()) - } -} - -/// Context of expression evaluation -#[derive(Clone, Default)] -#[repr(transparent)] -pub struct Context { - values: BTreeMap, -} - -impl Context { - /// Create new [`Self`] - pub fn new() -> Self { - Self { - values: BTreeMap::new(), - } - } -} - -impl iroha_data_model::evaluate::Context for Context { - fn query(&self, query: &QueryBox) -> Result { - let value_cursor = query.clone().execute()?; - match value_cursor.collect() { - Ok(value) => Ok(value), - Err(QueryOutputCursorError::Validation(err)) => Err(err), - Err(QueryOutputCursorError::Conversion(err)) => { - panic!("Conversion error during collecting query result: {err:?}") - } - } - } - - fn get(&self, name: &Name) -> Option<&Value> { - self.values.get(name) - } - - fn update(&mut self, other: impl IntoIterator) { - self.values.extend(other) - } -} - /// Get payload for smart contract `main()` entrypoint. #[cfg(not(test))] pub fn get_smart_contract_payload() -> payloads::SmartContract { @@ -481,11 +435,10 @@ mod tests { cursor: ForwardCursor::new(None, None), }); const ISI_RESULT: Result<(), ValidationFail> = Ok(()); - const EXPRESSION_RESULT: NumericValue = NumericValue::U32(5_u32); - fn get_test_instruction() -> InstructionExpr { + fn get_test_instruction() -> InstructionBox { let new_account_id = "mad_hatter@wonderland".parse().expect("Valid"); - let register_isi = RegisterExpr::new(Account::new(new_account_id, [])); + let register_isi = Register::account(Account::new(new_account_id, [])); register_isi.into() } @@ -495,17 +448,13 @@ mod tests { FindAssetQuantityById::new(asset_id).into() } - fn get_test_expression() -> EvaluatesTo { - Add::new(2_u32, 3_u32).into() - } - #[no_mangle] pub unsafe extern "C" fn _iroha_smart_contract_execute_instruction_mock( ptr: *const u8, len: usize, ) -> *const u8 { let bytes = slice::from_raw_parts(ptr, len); - let instruction = InstructionExpr::decode_all(&mut &*bytes); + let instruction = InstructionBox::decode_all(&mut &*bytes); assert_eq!(get_test_instruction(), instruction.unwrap()); ManuallyDrop::new(encode_with_length_prefix(&ISI_RESULT)).as_ptr() @@ -538,12 +487,4 @@ mod tests { fn execute_query() { assert_eq!(get_test_query().execute(), QUERY_RESULT); } - - #[webassembly_test] - fn evaluate_expression() { - assert_eq!( - get_test_expression().evaluate(&Context::new()), - Ok(EXPRESSION_RESULT) - ); - } } diff --git a/smart_contract/utils/src/lib.rs b/smart_contract/utils/src/lib.rs index ec9f70a242e..5e0919b095a 100644 --- a/smart_contract/utils/src/lib.rs +++ b/smart_contract/utils/src/lib.rs @@ -7,7 +7,7 @@ extern crate alloc; use alloc::{boxed::Box, format, vec::Vec}; use core::ops::RangeFrom; -use parity_scale_codec::{DecodeAll, Encode}; +pub use parity_scale_codec::{DecodeAll, Encode}; pub mod debug; pub mod log; diff --git a/tools/kagami/src/genesis.rs b/tools/kagami/src/genesis.rs index 48caeb74337..6036e4723ab 100644 --- a/tools/kagami/src/genesis.rs +++ b/tools/kagami/src/genesis.rs @@ -4,11 +4,9 @@ use clap::{ArgGroup, Parser, Subcommand}; use iroha_config::{sumeragi::default::*, wasm::default::*, wsv::default::*}; use iroha_data_model::{ asset::AssetValueType, - isi::{MintExpr, RegisterExpr}, metadata::Limits, parameter::{default::*, ParametersBuilder}, prelude::AssetId, - IdBox, }; use iroha_genesis::{ExecutorMode, ExecutorPath, RawGenesisBlock, RawGenesisBlockBuilder}; use serde_json::json; @@ -146,22 +144,19 @@ pub fn generate_default(executor: ExecutorMode) -> color_eyre::Result color_eyre::Result::new( - vec![MintExpr::new(1_u32, rose_id)], + vec![Mint::asset_quantity(1_u32, rose_id)], Repeats::Indefinitely, account_id, FilterBox::Data(DataEventFilter::BySome(DataEntityFilter::ByAccount( From 7523a69475a42ac4b2c0b1893b8bbd01c85331f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marin=20Ver=C5=A1i=C4=87?= Date: Thu, 14 Dec 2023 13:24:33 +0300 Subject: [PATCH 08/21] [refactor]: Export iroha_crypto through iroha_client (#4149) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Marin Veršić --- Cargo.lock | 1 - client/benches/torii.rs | 3 +-- client/benches/tps/utils.rs | 3 ++- client/examples/tutorial.rs | 2 +- client/src/client.rs | 18 +++++++++--------- client/src/lib.rs | 4 +++- client/tests/integration/asset.rs | 3 +-- client/tests/integration/asset_propagation.rs | 3 +-- client/tests/integration/burn_public_keys.rs | 2 +- client/tests/integration/domain_owner.rs | 6 ++++-- client/tests/integration/events/pipeline.rs | 12 +++++++----- .../integration/multiple_blocks_created.rs | 3 +-- .../integration/multisignature_account.rs | 2 +- .../integration/multisignature_transaction.rs | 2 +- client/tests/integration/permissions.rs | 9 +++++---- client/tests/integration/queries/asset.rs | 4 ++-- client/tests/integration/roles.rs | 3 ++- client/tests/integration/transfer_asset.rs | 5 ++--- client/tests/integration/unregister_peer.rs | 2 +- client/tests/integration/upgrade.rs | 2 +- client_cli/Cargo.toml | 1 - smart_contract/executor/derive/src/validate.rs | 6 +++++- 22 files changed, 51 insertions(+), 45 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ff9e1f49ce9..66eeddd28c3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2720,7 +2720,6 @@ dependencies = [ "erased-serde", "iroha_client", "iroha_config", - "iroha_crypto", "iroha_primitives", "json5", "once_cell", diff --git a/client/benches/torii.rs b/client/benches/torii.rs index 95b6be46304..4ca452b930f 100644 --- a/client/benches/torii.rs +++ b/client/benches/torii.rs @@ -6,10 +6,9 @@ use criterion::{criterion_group, criterion_main, Criterion, Throughput}; use iroha::samples::{construct_executor, get_config}; use iroha_client::{ client::{asset, Client}, + crypto::KeyPair, data_model::prelude::*, }; -use iroha_crypto::KeyPair; -use iroha_data_model::isi::InstructionBox; use iroha_genesis::{GenesisNetwork, RawGenesisBlockBuilder}; use iroha_primitives::unique_vec; use iroha_version::Encode; diff --git a/client/benches/tps/utils.rs b/client/benches/tps/utils.rs index 5c37499ce51..045a95527fe 100644 --- a/client/benches/tps/utils.rs +++ b/client/benches/tps/utils.rs @@ -163,7 +163,8 @@ impl MeasurerUnit { /// Submit initial transactions for measurement fn ready(self) -> Result { - let keypair = iroha_crypto::KeyPair::generate().expect("Failed to generate KeyPair."); + let keypair = + iroha_client::crypto::KeyPair::generate().expect("Failed to generate KeyPair."); let account_id = account_id(self.name); let asset_id = asset_id(self.name); diff --git a/client/examples/tutorial.rs b/client/examples/tutorial.rs index 5cc86cd8495..b22b0d1be52 100644 --- a/client/examples/tutorial.rs +++ b/client/examples/tutorial.rs @@ -114,12 +114,12 @@ fn account_registration_test(config: &Configuration) -> Result<(), Error> { // #region register_account_crates use iroha_client::{ client::Client, + crypto::KeyPair, data_model::{ metadata::UnlimitedMetadata, prelude::{Account, AccountId, InstructionBox, Register}, }, }; - use iroha_crypto::KeyPair; // #endregion register_account_crates // Create an Iroha client diff --git a/client/src/client.rs b/client/src/client.rs index e057a8c7c58..a33b9f6a8d4 100644 --- a/client/src/client.rs +++ b/client/src/client.rs @@ -14,7 +14,6 @@ use eyre::{eyre, Result, WrapErr}; use futures_util::StreamExt; use http_default::{AsyncWebSocketStream, WebSocketStream}; use iroha_config::{client::Configuration, client_api::ConfigurationDTO, torii::uri}; -use iroha_crypto::{HashOf, KeyPair}; use iroha_logger::prelude::*; use iroha_telemetry::metrics::Status; use iroha_version::prelude::*; @@ -24,6 +23,7 @@ use url::Url; use self::{blocks_api::AsyncBlockStream, events_api::AsyncEventStream}; use crate::{ + crypto::{HashOf, KeyPair}, data_model::{ block::SignedBlock, isi::Instruction, @@ -69,15 +69,15 @@ pub trait Sign { /// Fails if signature creation fails fn sign( self, - key_pair: iroha_crypto::KeyPair, - ) -> Result; + key_pair: crate::crypto::KeyPair, + ) -> Result; } impl Sign for TransactionBuilder { fn sign( self, - key_pair: iroha_crypto::KeyPair, - ) -> Result { + key_pair: crate::crypto::KeyPair, + ) -> Result { self.sign(key_pair) } } @@ -85,8 +85,8 @@ impl Sign for TransactionBuilder { impl Sign for SignedTransaction { fn sign( self, - key_pair: iroha_crypto::KeyPair, - ) -> Result { + key_pair: crate::crypto::KeyPair, + ) -> Result { self.sign(key_pair) } } @@ -1699,8 +1699,8 @@ mod tests { .parse() .expect("Public key not in mulithash format"), ), - private_key: Some(iroha_crypto::PrivateKey::from_hex( - iroha_crypto::Algorithm::Ed25519, + private_key: Some(crate::crypto::PrivateKey::from_hex( + crate::crypto::Algorithm::Ed25519, "9AC47ABF59B356E0BD7DCBBBB4DEC080E302156A48CA907E47CB6AEA1D32719E7233BFC89DCBD68C19FDE6CE6158225298EC1131B6A130D1AEB454C1AB5183C0" ).expect("Private key not hex encoded")), account_id: Some( diff --git a/client/src/lib.rs b/client/src/lib.rs index 3d12fef5365..c33fd84f409 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -13,7 +13,8 @@ pub mod samples { client::{Configuration, ConfigurationProxy}, torii::uri::DEFAULT_API_ADDR, }; - use iroha_crypto::KeyPair; + + use crate::crypto::KeyPair; /// Get sample client configuration. pub fn get_client_config(key_pair: &KeyPair) -> Configuration { @@ -38,4 +39,5 @@ pub mod samples { } } +pub use iroha_crypto as crypto; pub use iroha_data_model as data_model; diff --git a/client/tests/integration/asset.rs b/client/tests/integration/asset.rs index 2cd3cab768f..5744827d134 100644 --- a/client/tests/integration/asset.rs +++ b/client/tests/integration/asset.rs @@ -3,10 +3,9 @@ use std::{str::FromStr as _, thread}; use eyre::Result; use iroha_client::{ client::{self, QueryResult}, + crypto::{KeyPair, PublicKey}, data_model::prelude::*, }; -use iroha_crypto::{KeyPair, PublicKey}; -use iroha_data_model::isi::InstructionBox; use iroha_primitives::fixed::Fixed; use serde_json::json; use test_network::*; diff --git a/client/tests/integration/asset_propagation.rs b/client/tests/integration/asset_propagation.rs index de7a5238418..4e35ef266a1 100644 --- a/client/tests/integration/asset_propagation.rs +++ b/client/tests/integration/asset_propagation.rs @@ -3,13 +3,12 @@ use std::{str::FromStr as _, thread}; use eyre::Result; use iroha_client::{ client::{self, QueryResult}, + crypto::KeyPair, data_model::{ parameter::{default::MAX_TRANSACTIONS_IN_BLOCK, ParametersBuilder}, prelude::*, }, }; -use iroha_crypto::KeyPair; -use iroha_data_model::isi::InstructionBox; use test_network::*; use super::Configuration; diff --git a/client/tests/integration/burn_public_keys.rs b/client/tests/integration/burn_public_keys.rs index fb4c54b2251..f207894995d 100644 --- a/client/tests/integration/burn_public_keys.rs +++ b/client/tests/integration/burn_public_keys.rs @@ -1,8 +1,8 @@ use iroha_client::{ client::{account, transaction, Client}, + crypto::{HashOf, KeyPair, PublicKey}, data_model::{isi::Instruction, prelude::*, transaction::TransactionPayload}, }; -use iroha_crypto::{HashOf, KeyPair, PublicKey}; use test_network::*; fn submit( diff --git a/client/tests/integration/domain_owner.rs b/client/tests/integration/domain_owner.rs index b420d5e81c2..eeeb881b324 100644 --- a/client/tests/integration/domain_owner.rs +++ b/client/tests/integration/domain_owner.rs @@ -1,6 +1,8 @@ use eyre::Result; -use iroha_client::data_model::{account::SignatureCheckCondition, prelude::*}; -use iroha_crypto::KeyPair; +use iroha_client::{ + crypto::KeyPair, + data_model::{account::SignatureCheckCondition, prelude::*}, +}; use serde_json::json; use test_network::*; diff --git a/client/tests/integration/events/pipeline.rs b/client/tests/integration/events/pipeline.rs index 966a922f6e0..35b8e16629a 100644 --- a/client/tests/integration/events/pipeline.rs +++ b/client/tests/integration/events/pipeline.rs @@ -1,11 +1,13 @@ use std::thread::{self, JoinHandle}; use eyre::Result; -use iroha_client::data_model::{ - parameter::{default::MAX_TRANSACTIONS_IN_BLOCK, ParametersBuilder}, - prelude::*, +use iroha_client::{ + crypto::HashOf, + data_model::{ + parameter::{default::MAX_TRANSACTIONS_IN_BLOCK, ParametersBuilder}, + prelude::*, + }, }; -use iroha_crypto::HashOf; use test_network::*; use super::Configuration; @@ -75,7 +77,7 @@ fn test_with_instruction_and_status_and_port( #[derive(Clone)] struct Checker { listener: iroha_client::client::Client, - hash: iroha_crypto::HashOf, + hash: HashOf, } impl Checker { diff --git a/client/tests/integration/multiple_blocks_created.rs b/client/tests/integration/multiple_blocks_created.rs index aa3f3a551ba..f0d28bb4a26 100644 --- a/client/tests/integration/multiple_blocks_created.rs +++ b/client/tests/integration/multiple_blocks_created.rs @@ -3,13 +3,12 @@ use std::thread; use eyre::Result; use iroha_client::{ client::{self, Client, QueryResult}, + crypto::KeyPair, data_model::{ parameter::{default::MAX_TRANSACTIONS_IN_BLOCK, ParametersBuilder}, prelude::*, }, }; -use iroha_crypto::KeyPair; -use iroha_data_model::isi::InstructionBox; use test_network::*; use super::Configuration; diff --git a/client/tests/integration/multisignature_account.rs b/client/tests/integration/multisignature_account.rs index 9f35fb9bbfa..515920dad3c 100644 --- a/client/tests/integration/multisignature_account.rs +++ b/client/tests/integration/multisignature_account.rs @@ -3,9 +3,9 @@ use std::thread; use eyre::Result; use iroha_client::{ client::{self, Client, QueryResult}, + crypto::KeyPair, data_model::prelude::*, }; -use iroha_crypto::KeyPair; use test_network::*; use super::Configuration; diff --git a/client/tests/integration/multisignature_transaction.rs b/client/tests/integration/multisignature_transaction.rs index 280a07751f3..c3bbaf7a1be 100644 --- a/client/tests/integration/multisignature_transaction.rs +++ b/client/tests/integration/multisignature_transaction.rs @@ -3,13 +3,13 @@ use std::{str::FromStr as _, thread, time::Duration}; use eyre::Result; use iroha_client::{ client::{self, Client, QueryResult}, + crypto::KeyPair, data_model::{ parameter::{default::MAX_TRANSACTIONS_IN_BLOCK, ParametersBuilder}, prelude::*, }, }; use iroha_config::client::Configuration as ClientConfiguration; -use iroha_crypto::KeyPair; use test_network::*; use super::Configuration; diff --git a/client/tests/integration/permissions.rs b/client/tests/integration/permissions.rs index fb829ed2ce3..da443789009 100644 --- a/client/tests/integration/permissions.rs +++ b/client/tests/integration/permissions.rs @@ -3,6 +3,7 @@ use std::{str::FromStr as _, thread, time::Duration}; use eyre::Result; use iroha_client::{ client::{self, Client, QueryResult}, + crypto::KeyPair, data_model::prelude::*, }; use iroha_genesis::GenesisNetwork; @@ -80,7 +81,7 @@ fn permissions_disallow_asset_transfer() { let asset_definition_id: AssetDefinitionId = "xor#wonderland".parse().expect("Valid"); let create_asset = Register::asset_definition(AssetDefinition::quantity(asset_definition_id.clone())); - let mouse_keypair = iroha_crypto::KeyPair::generate().expect("Failed to generate KeyPair."); + let mouse_keypair = KeyPair::generate().expect("Failed to generate KeyPair."); let alice_start_assets = get_assets(&iroha_client, &alice_id); iroha_client @@ -134,7 +135,7 @@ fn permissions_disallow_asset_burn() { let asset_definition_id = AssetDefinitionId::from_str("xor#wonderland").expect("Valid"); let create_asset = Register::asset_definition(AssetDefinition::quantity(asset_definition_id.clone())); - let mouse_keypair = iroha_crypto::KeyPair::generate().expect("Failed to generate KeyPair."); + let mouse_keypair = KeyPair::generate().expect("Failed to generate KeyPair."); let alice_start_assets = get_assets(&iroha_client, &alice_id); @@ -204,7 +205,7 @@ fn permissions_differ_not_only_by_names() { let alice_id: AccountId = "alice@wonderland".parse().expect("Valid"); let mouse_id: AccountId = "mouse@wonderland".parse().expect("Valid"); - let mouse_keypair = iroha_crypto::KeyPair::generate().expect("Failed to generate KeyPair."); + let mouse_keypair = KeyPair::generate().expect("Failed to generate KeyPair."); // Registering `Store` asset definitions let hat_definition_id: AssetDefinitionId = "hat#wonderland".parse().expect("Valid"); @@ -300,7 +301,7 @@ fn stored_vs_granted_token_payload() -> Result<()> { let create_asset = Register::asset_definition(AssetDefinition::store(asset_definition_id.clone())); let mouse_id: AccountId = "mouse@wonderland".parse().expect("Valid"); - let mouse_keypair = iroha_crypto::KeyPair::generate().expect("Failed to generate KeyPair."); + let mouse_keypair = KeyPair::generate().expect("Failed to generate KeyPair."); let new_mouse_account = Account::new(mouse_id.clone(), [mouse_keypair.public_key().clone()]); let instructions: [InstructionBox; 2] = [ Register::account(new_mouse_account).into(), diff --git a/client/tests/integration/queries/asset.rs b/client/tests/integration/queries/asset.rs index bb34d302158..6046c8cbf88 100644 --- a/client/tests/integration/queries/asset.rs +++ b/client/tests/integration/queries/asset.rs @@ -1,14 +1,14 @@ use eyre::Result; use iroha_client::{ client::{Client, ClientQueryError}, + crypto::KeyPair, data_model::{ asset::AssetValue, + isi::Instruction, prelude::*, query::{asset::FindTotalAssetQuantityByAssetDefinitionId, error::QueryExecutionFail}, }, }; -use iroha_crypto::KeyPair; -use iroha_data_model::isi::Instruction; use iroha_primitives::fixed::Fixed; use test_network::*; diff --git a/client/tests/integration/roles.rs b/client/tests/integration/roles.rs index 8523cfb39b6..ef1884806a3 100644 --- a/client/tests/integration/roles.rs +++ b/client/tests/integration/roles.rs @@ -3,6 +3,7 @@ use std::str::FromStr as _; use eyre::Result; use iroha_client::{ client::{self, QueryResult}, + crypto::KeyPair, data_model::prelude::*, }; use serde_json::json; @@ -52,7 +53,7 @@ fn register_and_grant_role_for_metadata_access() -> Result<()> { let mouse_id = AccountId::from_str("mouse@wonderland")?; // Registering Mouse - let mouse_key_pair = iroha_crypto::KeyPair::generate()?; + let mouse_key_pair = KeyPair::generate()?; let register_mouse = Register::account(Account::new( mouse_id.clone(), [mouse_key_pair.public_key().clone()], diff --git a/client/tests/integration/transfer_asset.rs b/client/tests/integration/transfer_asset.rs index cbc23b190fa..be37310c5cd 100644 --- a/client/tests/integration/transfer_asset.rs +++ b/client/tests/integration/transfer_asset.rs @@ -1,9 +1,8 @@ use iroha_client::{ client::{self, QueryResult}, - data_model::{prelude::*, Registered}, + crypto::KeyPair, + data_model::{isi::Instruction, prelude::*, Registered}, }; -use iroha_crypto::KeyPair; -use iroha_data_model::isi::Instruction; use iroha_primitives::fixed::Fixed; use test_network::*; diff --git a/client/tests/integration/unregister_peer.rs b/client/tests/integration/unregister_peer.rs index 84a2e4fa5b3..e8246a38514 100644 --- a/client/tests/integration/unregister_peer.rs +++ b/client/tests/integration/unregister_peer.rs @@ -3,12 +3,12 @@ use std::thread; use eyre::Result; use iroha_client::{ client::{self, QueryResult}, + crypto::KeyPair, data_model::{ parameter::{default::MAX_TRANSACTIONS_IN_BLOCK, ParametersBuilder}, prelude::*, }, }; -use iroha_crypto::KeyPair; use test_network::*; use super::Configuration; diff --git a/client/tests/integration/upgrade.rs b/client/tests/integration/upgrade.rs index cfa822046c2..3ec49a84600 100644 --- a/client/tests/integration/upgrade.rs +++ b/client/tests/integration/upgrade.rs @@ -3,9 +3,9 @@ use std::{path::Path, str::FromStr as _}; use eyre::Result; use iroha_client::{ client::{self, Client, QueryResult}, + crypto::KeyPair, data_model::prelude::*, }; -use iroha_crypto::KeyPair; use iroha_logger::info; use serde_json::json; use test_network::*; diff --git a/client_cli/Cargo.toml b/client_cli/Cargo.toml index 12d6ffa40c5..c5725467446 100644 --- a/client_cli/Cargo.toml +++ b/client_cli/Cargo.toml @@ -25,7 +25,6 @@ maintenance = { status = "actively-developed" } [dependencies] iroha_client = { workspace = true } iroha_primitives = { workspace = true } -iroha_crypto = { workspace = true } iroha_config = { workspace = true } color-eyre = { workspace = true } diff --git a/smart_contract/executor/derive/src/validate.rs b/smart_contract/executor/derive/src/validate.rs index 8a6a890b695..e552005dbf1 100644 --- a/smart_contract/executor/derive/src/validate.rs +++ b/smart_contract/executor/derive/src/validate.rs @@ -58,7 +58,11 @@ impl FromAttributes for ValidateAttribute { continue; } - let Some(list) = accumulator.handle(attr.meta.require_list().map_err(darling::Error::from)) else { continue; }; + let Some(list) = + accumulator.handle(attr.meta.require_list().map_err(darling::Error::from)) + else { + continue; + }; let tokens = &list.tokens; if path.is_ident("validate") { From def9d1964d7d40daa20ace8d77bd0f5b47240c3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marin=20Ver=C5=A1i=C4=87?= Date: Thu, 14 Dec 2023 13:57:37 +0300 Subject: [PATCH 09/21] [refactor]: Export iroha_config through iroha_client (#4147) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Marin Veršić --- Cargo.lock | 1234 +++++++++-------- client/examples/tutorial.rs | 2 +- client/src/client.rs | 42 +- client/src/lib.rs | 14 +- client/tests/integration/add_account.rs | 3 +- client/tests/integration/add_domain.rs | 3 +- client/tests/integration/asset.rs | 3 +- client/tests/integration/asset_propagation.rs | 3 +- client/tests/integration/connected_peers.rs | 3 +- client/tests/integration/events/mod.rs | 1 - client/tests/integration/events/pipeline.rs | 3 +- client/tests/integration/mod.rs | 5 - .../integration/multiple_blocks_created.rs | 3 +- .../integration/multisignature_account.rs | 3 +- .../integration/multisignature_transaction.rs | 5 +- client/tests/integration/restart_peer.rs | 3 +- client/tests/integration/tx_history.rs | 3 +- client/tests/integration/unregister_peer.rs | 3 +- client/tests/integration/unstable_network.rs | 3 +- client_cli/Cargo.toml | 1 - client_cli/src/main.rs | 2 +- 21 files changed, 716 insertions(+), 626 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 66eeddd28c3..fb0b976dace 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -35,13 +35,14 @@ dependencies = [ [[package]] name = "ahash" -version = "0.8.3" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" +checksum = "91429305e9f0a25f6205c5b8e0d2db09e0708a7a6df0f42212bb56c32c8ac97a" dependencies = [ "cfg-if", "once_cell", "version_check", + "zerocopy", ] [[package]] @@ -101,9 +102,9 @@ checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" [[package]] name = "anstream" -version = "0.6.4" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab91ebe16eb252986481c5b62f6098f3b698a45e34b5b98200cf20dd2484a44" +checksum = "d664a92ecae85fd0a7392615844904654d1d5f5514837f471ddef4a057aba1b6" dependencies = [ "anstyle", "anstyle-parse", @@ -121,30 +122,30 @@ checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" [[package]] name = "anstyle-parse" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "317b9a89c1868f5ea6ff1d9539a69f45dffc21ce321ac1fd1160dfa48c8e2140" +checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.0.0" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.1" +version = "3.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628" +checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" dependencies = [ "anstyle", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -196,18 +197,18 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.41", ] [[package]] name = "async-trait" -version = "0.1.73" +version = "0.1.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0" +checksum = "a66537f1bb974b254c98ed142ff995236e81b9d0fe4db0575f46612cb15eb0f9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.41", ] [[package]] @@ -222,7 +223,7 @@ dependencies = [ "rustls", "rustls-native-certs", "url", - "webpki-roots 0.25.2", + "webpki-roots 0.25.3", ] [[package]] @@ -310,15 +311,9 @@ checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" [[package]] name = "base64" -version = "0.13.1" +version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" - -[[package]] -name = "base64" -version = "0.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ba43ea6f343b788c8764558649e08df62f86c6ef251fdaeb1ffd010a9ae50a2" +checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" [[package]] name = "base64ct" @@ -328,9 +323,9 @@ checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" [[package]] name = "basic-toml" -version = "0.1.4" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bfc506e7a2370ec239e1d072507b2a80c833083699d3c6fa176fbb4de8448c6" +checksum = "2f2139706359229bfa8f19142ac1155b4b80beafb7a60471ac5dd109d4a19778" dependencies = [ "serde", ] @@ -367,9 +362,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.4.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" +checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" [[package]] name = "bitvec" @@ -424,12 +419,12 @@ dependencies = [ [[package]] name = "bstr" -version = "1.7.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c79ad7fb2dd38f3dabd76b09c6a5a20c038fc0213ef1e9afd30eb777f120f019" +checksum = "542f33a8835a0884b006a0c3df3dadd99c0c3f296ed26c2fdc8028e01ad6230c" dependencies = [ "memchr", - "regex-automata 0.4.1", + "regex-automata 0.4.3", "serde", ] @@ -482,15 +477,6 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" -[[package]] -name = "c2-chacha" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "217192c943108d8b13bac38a1d51df9ce8a407a3f5a71ab633980665e68fbd9a" -dependencies = [ - "ppv-lite86", -] - [[package]] name = "cast" version = "0.3.0" @@ -608,23 +594,23 @@ dependencies = [ [[package]] name = "clap" -version = "4.4.6" +version = "4.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d04704f56c2cde07f43e8e2c154b43f216dc5c92fc98ada720177362f953b956" +checksum = "bfaff671f6b22ca62406885ece523383b9b64022e341e53e009a62ebc47a45f2" dependencies = [ "clap_builder", - "clap_derive 4.4.2", + "clap_derive 4.4.7", ] [[package]] name = "clap_builder" -version = "4.4.6" +version = "4.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e231faeaca65ebd1ea3c737966bf858971cd38c3849107aa3ea7de90a804e45" +checksum = "a216b506622bb1d316cd51328dce24e07bdff4a6128a47c7e7fad11878d5adbb" dependencies = [ "anstream", "anstyle", - "clap_lex 0.5.1", + "clap_lex 0.6.0", "strsim", ] @@ -643,14 +629,14 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.4.2" +version = "4.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0862016ff20d69b84ef8247369fabf5c008a7417002411897d40ee1f4532b873" +checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442" dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.41", ] [[package]] @@ -664,9 +650,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.5.1" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd7cc57abe963c6d3b9d8be5b06ba7c8957a930305ca90304f24ef040aa6f961" +checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" [[package]] name = "clru" @@ -701,9 +687,9 @@ dependencies = [ [[package]] name = "color-spantrace" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ba75b3d9449ecdccb27ecbc479fdc0b87fa2dd43d2f8298f9bf0e59aacc8dce" +checksum = "cd6be1b2a7e382e2b98b43b2adcca6bb0e465af0bdd38123873ae61eb17a72c2" dependencies = [ "once_cell", "owo-colors", @@ -719,11 +705,10 @@ checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" [[package]] name = "colored" -version = "2.0.4" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2674ec482fbc38012cf31e6c42ba0177b431a0cb6f15fe40efa5aab1bda516f6" +checksum = "cbf2150cce219b664a8a70df7a1f933836724b503f8a413af9365b4dcc4d90b8" dependencies = [ - "is-terminal", "lazy_static", "windows-sys 0.48.0", ] @@ -786,9 +771,9 @@ checksum = "28c122c3980598d243d63d9a704629a2d748d101f278052ff068be5a4423ab6f" [[package]] name = "core-foundation" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" dependencies = [ "core-foundation-sys", "libc", @@ -796,9 +781,9 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" [[package]] name = "core2" @@ -820,27 +805,27 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.2.9" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1" +checksum = "ce420fe07aecd3e67c5f910618fe65e94158f6dcc0adf44e00d69ce2bdfe0fd0" dependencies = [ "libc", ] [[package]] name = "cranelift-bforest" -version = "0.102.0" +version = "0.102.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76eb38f2af690b5a4411d9a8782b6d77dabff3ca939e0518453ab9f9a4392d41" +checksum = "8e7e56668d2263f92b691cb9e4a2fcb186ca0384941fe420484322fa559c3329" dependencies = [ "cranelift-entity", ] [[package]] name = "cranelift-codegen" -version = "0.102.0" +version = "0.102.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39526c036b92912417e8931f52c1e235796688068d3efdbbd8b164f299d19156" +checksum = "2a9ff61938bf11615f55b80361288c68865318025632ea73c65c0b44fa16283c" dependencies = [ "bumpalo", "cranelift-bforest", @@ -850,7 +835,7 @@ dependencies = [ "cranelift-entity", "cranelift-isle", "gimli", - "hashbrown 0.14.1", + "hashbrown 0.14.3", "log", "regalloc2", "smallvec", @@ -859,33 +844,33 @@ dependencies = [ [[package]] name = "cranelift-codegen-meta" -version = "0.102.0" +version = "0.102.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdb0deedc9fccf2db53a5a3c9c9d0163e44143b0d004dca9bf6ab6a0024cd79a" +checksum = "50656bf19e3d4a153b404ff835b8b59e924cfa3682ebe0d3df408994f37983f6" dependencies = [ "cranelift-codegen-shared", ] [[package]] name = "cranelift-codegen-shared" -version = "0.102.0" +version = "0.102.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cea2d1b274e45aa8e61e9103efa1ba82d4b5a19d12bd1fd10744c3b7380ba3ff" +checksum = "388041deeb26109f1ea73c1812ea26bfd406c94cbce0bb5230aa44277e43b209" [[package]] name = "cranelift-control" -version = "0.102.0" +version = "0.102.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea5977559a71e63db79a263f0e81a89b996e8a38212c4281e37dd1dbaa8b65c" +checksum = "b39b7c512ffac527e5b5df9beae3d67ab85d07dca6d88942c16195439fedd1d3" dependencies = [ "arbitrary", ] [[package]] name = "cranelift-entity" -version = "0.102.0" +version = "0.102.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f871ada808b58158d84dfc43a6a2e2d2756baaf4ed1c51fd969ca8330e6ca5c" +checksum = "fdb25f573701284fe2bcf88209d405342125df00764b396c923e11eafc94d892" dependencies = [ "serde", "serde_derive", @@ -893,9 +878,9 @@ dependencies = [ [[package]] name = "cranelift-frontend" -version = "0.102.0" +version = "0.102.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8e6890f587ef59824b3debe577e68fdf9b307b3808c54b8d93a18fd0b70941b" +checksum = "e57374fd11d72cf9ffb85ff64506ed831440818318f58d09f45b4185e5e9c376" dependencies = [ "cranelift-codegen", "log", @@ -905,15 +890,15 @@ dependencies = [ [[package]] name = "cranelift-isle" -version = "0.102.0" +version = "0.102.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8d5fc6d5d3b52d1917002b17a8ecce448c2621b5bf394bb4e77e2f676893537" +checksum = "ae769b235f6ea2f86623a3ff157cc04a4ff131dc9fe782c2ebd35f272043581e" [[package]] name = "cranelift-native" -version = "0.102.0" +version = "0.102.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e10c2e7faa65d4ae7de9a83b44f2c31aca7dc638e17d0a79572fdf8103d720b" +checksum = "3dc7bfb8f13a0526fe20db338711d9354729b861c336978380bb10f7f17dd207" dependencies = [ "cranelift-codegen", "libc", @@ -922,9 +907,9 @@ dependencies = [ [[package]] name = "cranelift-wasm" -version = "0.102.0" +version = "0.102.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2755807efc7ec80d1cc0b6815e70f10cedf968889f0469091dbff9c5c0741c48" +checksum = "2c5f41a4af931b756be05af0dd374ce200aae2d52cea16b0beb07e8b52732c35" dependencies = [ "cranelift-codegen", "cranelift-entity", @@ -954,7 +939,7 @@ dependencies = [ "anes", "cast", "ciborium", - "clap 4.4.6", + "clap 4.4.11", "criterion-plot", "is-terminal", "itertools 0.10.5", @@ -997,9 +982,9 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.8" +version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" +checksum = "14c3242926edf34aec4ac3a77108ad4854bffaa2e4ddc1824124ce59231302d5" dependencies = [ "cfg-if", "crossbeam-utils", @@ -1007,9 +992,9 @@ dependencies = [ [[package]] name = "crossbeam-deque" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" +checksum = "fca89a0e215bab21874660c67903c5f143333cab1da83d041c7ded6053774751" dependencies = [ "cfg-if", "crossbeam-epoch", @@ -1018,22 +1003,21 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.15" +version = "0.9.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7" +checksum = "2d2fe95351b870527a5d09bf563ed3c97c0cffb87cf1c78a591bf48bb218d9aa" dependencies = [ "autocfg", "cfg-if", "crossbeam-utils", "memoffset", - "scopeguard", ] [[package]] name = "crossbeam-queue" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add" +checksum = "b9bcf5bdbfdd6030fb4a1c497b5d5fc5921aa2f60d359a17e249c0e6df3de153" dependencies = [ "cfg-if", "crossbeam-utils", @@ -1041,9 +1025,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.16" +version = "0.8.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" +checksum = "c06d96137f14f244c37f989d9fff8f95e6c18b918e71f36638f8c49112e4c78f" dependencies = [ "cfg-if", ] @@ -1075,9 +1059,9 @@ dependencies = [ [[package]] name = "crypto-bigint" -version = "0.5.3" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "740fe28e594155f10cfc383984cbefd529d7396050557148f79cb0f621204124" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" dependencies = [ "generic-array 0.14.7", "rand_core 0.6.4", @@ -1121,14 +1105,14 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.41", ] [[package]] name = "cxx" -version = "1.0.108" +version = "1.0.110" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "292b4841d939b20ba44fff686a35808b0ab31a3256e3629917d9aedd43eb7b3a" +checksum = "7129e341034ecb940c9072817cd9007974ea696844fc4dd582dc1653a7fbe2e8" dependencies = [ "cc", "cxxbridge-flags", @@ -1138,9 +1122,9 @@ dependencies = [ [[package]] name = "cxx-build" -version = "1.0.108" +version = "1.0.110" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e7e35cf85fd4e90dcaba251f3ee95e08fb6f9d66e5c0588816f16a6ab939b40" +checksum = "a2a24f3f5f8eed71936f21e570436f024f5c2e25628f7496aa7ccd03b90109d5" dependencies = [ "cc", "codespan-reporting", @@ -1148,24 +1132,24 @@ dependencies = [ "proc-macro2", "quote", "scratch", - "syn 2.0.38", + "syn 2.0.41", ] [[package]] name = "cxxbridge-flags" -version = "1.0.108" +version = "1.0.110" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7030aff1908ba2b7eb639466df50792b2a3fdf02bea9557c4ee1a531975554b" +checksum = "06fdd177fc61050d63f67f5bd6351fac6ab5526694ea8e359cd9cd3b75857f44" [[package]] name = "cxxbridge-macro" -version = "1.0.108" +version = "1.0.110" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79418ecb0c2322a7926a5fa5a9660535432b5b3588b947e1eb484cc509edbe3c" +checksum = "587663dd5fb3d10932c8aecfe7c844db1bcf0aee93eeab08fac13dc1212c2e7f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.41", ] [[package]] @@ -1189,7 +1173,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.38", + "syn 2.0.41", ] [[package]] @@ -1200,7 +1184,7 @@ checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5" dependencies = [ "darling_core", "quote", - "syn 2.0.38", + "syn 2.0.41", ] [[package]] @@ -1216,7 +1200,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" dependencies = [ "cfg-if", - "hashbrown 0.14.1", + "hashbrown 0.14.3", "lock_api", "once_cell", "parking_lot_core", @@ -1224,9 +1208,9 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" +checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" [[package]] name = "debugid" @@ -1249,9 +1233,12 @@ dependencies = [ [[package]] name = "deranged" -version = "0.3.8" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2696e8a945f658fd14dc3b87242e6b80cd0f36ff04ea560fa39082368847946" +checksum = "8eb30d70a07a3b04884d2677f06bec33509dc67ca60d92949e5535352d3191dc" +dependencies = [ + "powerfmt", +] [[package]] name = "derive_more" @@ -1325,7 +1312,7 @@ checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.41", ] [[package]] @@ -1348,15 +1335,15 @@ checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b" [[package]] name = "dyn-clone" -version = "1.0.14" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23d2f3407d9a573d666de4b5bdf10569d73ca9478087346697dcbae6244bfbcd" +checksum = "545b22097d44f8a9581187cdf93de7a71e4722bf51200cfaba810865b49a495d" [[package]] name = "ecdsa" -version = "0.16.8" +version = "0.16.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4b1e0c257a9e9f25f90ff76d7a68360ed497ee519c8e428d1825ef0000799d4" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" dependencies = [ "der", "digest 0.10.7", @@ -1378,15 +1365,16 @@ dependencies = [ [[package]] name = "ed25519-dalek" -version = "2.0.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7277392b266383ef8396db7fdeb1e77b6c52fed775f5df15bb24f35b72156980" +checksum = "1f628eaec48bfd21b865dc2950cfa014450c01d2fa2b69a86c2fd5844ec523c0" dependencies = [ "curve25519-dalek", "ed25519", "rand_core 0.6.4", "serde", "sha2", + "subtle", "zeroize", ] @@ -1398,9 +1386,9 @@ checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" [[package]] name = "elliptic-curve" -version = "0.13.6" +version = "0.13.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d97ca172ae9dc9f9b779a6e3a65d308f2af74e5b8c921299075bdb4a0370e914" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" dependencies = [ "base16ct", "crypto-bigint", @@ -1447,12 +1435,12 @@ dependencies = [ [[package]] name = "errno" -version = "0.3.5" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3e13f66a2f95e32a39eaa81f6b95d42878ca0e1db0c7543723dfe12557e860" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" dependencies = [ "libc", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -1467,9 +1455,9 @@ dependencies = [ [[package]] name = "eyre" -version = "0.6.8" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c2b6b5a29c02cdc822728b7d7b8ae1bab3e3b05d44522770ddd49722eeac7eb" +checksum = "b6267a1fa6f59179ea4afc8e50fd8612a3cc60bc858f786ff877a4a8cb042799" dependencies = [ "indenter", "once_cell", @@ -1483,9 +1471,9 @@ checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" [[package]] name = "faster-hex" -version = "0.8.1" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "239f7bfb930f820ab16a9cd95afc26f88264cf6905c960b340a615384aa3338a" +checksum = "a2a2b11eda1d40935b26cf18f6833c526845ae8c41e58d09af6adeb6f0269183" dependencies = [ "serde", ] @@ -1508,20 +1496,20 @@ dependencies = [ [[package]] name = "fiat-crypto" -version = "0.2.2" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a481586acf778f1b1455424c343f71124b048ffa5f4fc3f8f6ae9dc432dcb3c7" +checksum = "27573eac26f4dd11e2b1916c3fe1baa56407c83c71a773a8ba17ec0bca03b6b7" [[package]] name = "filetime" -version = "0.2.22" +version = "0.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4029edd3e734da6fe05b6cd7bd2960760a616bd2ddd0d59a0124746d6272af0" +checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.3.5", - "windows-sys 0.48.0", + "redox_syscall", + "windows-sys 0.52.0", ] [[package]] @@ -1546,9 +1534,9 @@ dependencies = [ [[package]] name = "flate2" -version = "1.0.27" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6c98ee8095e9d1dcbf2fcc6d95acccb90d1c81db1e44725c6a984b1dbdfb010" +checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" dependencies = [ "crc32fast", "miniz_oxide", @@ -1577,9 +1565,9 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" dependencies = [ "percent-encoding", ] @@ -1592,9 +1580,9 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" [[package]] name = "futures" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" +checksum = "da0290714b38af9b4a7b094b8a37086d1b4e61f2df9122c3cad2577669145335" dependencies = [ "futures-channel", "futures-core", @@ -1607,9 +1595,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" +checksum = "ff4dd66668b557604244583e3e1e1eada8c5c2e96a6d0d6653ede395b78bbacb" dependencies = [ "futures-core", "futures-sink", @@ -1617,15 +1605,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" +checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c" [[package]] name = "futures-executor" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" +checksum = "0f4fb8693db0cf099eadcca0efe2a5a22e4550f98ed16aba6c48700da29597bc" dependencies = [ "futures-core", "futures-task", @@ -1634,38 +1622,38 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" +checksum = "8bf34a163b5c4c52d0478a4d757da8fb65cabef42ba90515efee0f6f9fa45aaa" [[package]] name = "futures-macro" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" +checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.41", ] [[package]] name = "futures-sink" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" +checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817" [[package]] name = "futures-task" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" +checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2" [[package]] name = "futures-util" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" +checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104" dependencies = [ "futures-channel", "futures-core", @@ -1694,7 +1682,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27d12c0aed7f1e24276a241aadc4cb8ea9f83000f34bc062b7cc2d51e3b0fabd" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.4.1", "debugid", "fxhash", "serde", @@ -1734,9 +1722,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.10" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" dependencies = [ "cfg-if", "libc", @@ -1757,9 +1745,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.28.0" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" dependencies = [ "fallible-iterator", "indexmap 2.1.0", @@ -1768,9 +1756,9 @@ dependencies = [ [[package]] name = "gix" -version = "0.53.1" +version = "0.55.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06a8c9f9452078f474fecd2880de84819b8c77224ab62273275b646bf785f906" +checksum = "002667cd1ebb789313d0d0afe3d23b2821cf3b0e91605095f0e6d8751f0ceeea" dependencies = [ "gix-actor", "gix-commitgraph", @@ -1811,9 +1799,9 @@ dependencies = [ [[package]] name = "gix-actor" -version = "0.26.0" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e8c6778cc03bca978b2575a03e04e5ba6f430a9dd9b0f1259f0a8a9a5e5cc66" +checksum = "2eadca029ef716b4378f7afb19f7ee101fde9e58ba1f1445971315ac866db417" dependencies = [ "bstr", "btoi", @@ -1825,41 +1813,41 @@ dependencies = [ [[package]] name = "gix-bitmap" -version = "0.2.7" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ccab4bc576844ddb51b78d81b4a42d73e6229660fa614dfc3d3999c874d1959" +checksum = "d49e1a13a30d3f88be4bceae184dd13a2d3fb9ffa7515f7ed7ae771b857f4916" dependencies = [ "thiserror", ] [[package]] name = "gix-chunk" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b42ea64420f7994000130328f3c7a2038f639120518870436d31b8bde704493" +checksum = "d411ecd9b558b0c20b3252b7e409eec48eabc41d18324954fe526bac6e2db55f" dependencies = [ "thiserror", ] [[package]] name = "gix-commitgraph" -version = "0.20.0" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4676ede3a7d37e7028e2889830349a6aca22efc1d2f2dd9fa3351c1a8ddb0c6a" +checksum = "85a7007ba021f059803afaf6f8a48872422abc20550ac12ede6ddea2936cec36" dependencies = [ "bstr", "gix-chunk", "gix-features", "gix-hash", - "memmap2", + "memmap2 0.9.0", "thiserror", ] [[package]] name = "gix-config" -version = "0.29.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1108c4ac88248dd25cc8ab0d0dae796e619fb72d92f88e30e00b29d61bb93cc4" +checksum = "5cae98c6b4c66c09379bc35274b172587d6b0ac369a416c39128ad8c6454f9bb" dependencies = [ "bstr", "gix-config-value", @@ -1878,11 +1866,11 @@ dependencies = [ [[package]] name = "gix-config-value" -version = "0.14.0" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea7505b97f4d8e7933e29735a568ba2f86d8de466669d9f0e8321384f9972f47" +checksum = "6419db582ea84dfb58c7e7b0af7fd62c808aa14954af2936a33f89b0f4ed018e" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.4.1", "bstr", "gix-path", "libc", @@ -1891,9 +1879,9 @@ dependencies = [ [[package]] name = "gix-date" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7df669639582dc7c02737642f76890b03b5544e141caba68a7d6b4eb551e0d" +checksum = "468dfbe411f335f01525a1352271727f8e7772075a93fa747260f502086b30be" dependencies = [ "bstr", "itoa", @@ -1903,9 +1891,9 @@ dependencies = [ [[package]] name = "gix-diff" -version = "0.35.0" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b45e342d148373bd9070d557e6fb1280aeae29a3e05e32506682d027278501eb" +checksum = "931394f69fb8c9ed6afc0aae3487bd869e936339bcc13ed8884472af072e0554" dependencies = [ "gix-hash", "gix-object", @@ -1914,9 +1902,9 @@ dependencies = [ [[package]] name = "gix-discover" -version = "0.24.0" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da4cacda5ee9dd1b38b0e2506834e40e66c08cf050ef55c344334c76745f277b" +checksum = "a45d5cf0321178883e38705ab2b098f625d609a7d4c391b33ac952eff2c490f2" dependencies = [ "bstr", "dunce", @@ -1929,9 +1917,9 @@ dependencies = [ [[package]] name = "gix-features" -version = "0.34.0" +version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f414c99e1a7abc69b21f3225a6539d203b0513f1d1d448607c4ea81cdcf9ee59" +checksum = "4d46a4a5c6bb5bebec9c0d18b65ada20e6517dbd7cf855b87dd4bbdce3a771b2" dependencies = [ "crc32fast", "flate2", @@ -1947,20 +1935,20 @@ dependencies = [ [[package]] name = "gix-fs" -version = "0.6.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "404795da3d4c660c9ab6c3b2ad76d459636d1e1e4b37b0c7ff68eee898c298d4" +checksum = "20e86eb040f5776a5ade092282e51cdcad398adb77d948b88d17583c2ae4e107" dependencies = [ "gix-features", ] [[package]] name = "gix-glob" -version = "0.12.0" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ac79c444193b0660fe0c0925d338bd338bd643e32138784dccfb12c628b892" +checksum = "5db19298c5eeea2961e5b3bf190767a2d1f09b8802aeb5f258e42276350aff19" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.4.1", "bstr", "gix-features", "gix-path", @@ -1968,9 +1956,9 @@ dependencies = [ [[package]] name = "gix-hash" -version = "0.13.0" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ccf425543779cddaa4a7c62aba3fa9d90ea135b160be0a72dd93c063121ad4a" +checksum = "1f8cf8c2266f63e582b7eb206799b63aa5fa68ee510ad349f637dfe2d0653de0" dependencies = [ "faster-hex", "thiserror", @@ -1978,22 +1966,22 @@ dependencies = [ [[package]] name = "gix-hashtable" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "409268480841ad008e81c17ca5a293393fbf9f2b6c2f85b8ab9de1f0c5176a16" +checksum = "feb61880816d7ec4f0b20606b498147d480860ddd9133ba542628df2f548d3ca" dependencies = [ "gix-hash", - "hashbrown 0.14.1", + "hashbrown 0.14.3", "parking_lot", ] [[package]] name = "gix-index" -version = "0.24.0" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e9599fc30b3d6aad231687a403f85dfa36ae37ccf1b68ee1f621ad5b7fc7a0d" +checksum = "c83a4fcc121b2f2e109088f677f89f85e7a8ebf39e8e6659c0ae54d4283b1650" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.4.1", "bstr", "btoi", "filetime", @@ -2005,16 +1993,16 @@ dependencies = [ "gix-object", "gix-traverse", "itoa", - "memmap2", + "memmap2 0.7.1", "smallvec", "thiserror", ] [[package]] name = "gix-lock" -version = "9.0.0" +version = "11.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1568c3d90594c60d52670f325f5db88c2d572e85c8dd45fabc23d91cadb0fd52" +checksum = "7e5c65e6a29830a435664891ced3f3c1af010f14900226019590ee0971a22f37" dependencies = [ "gix-tempfile", "gix-utils", @@ -2023,20 +2011,20 @@ dependencies = [ [[package]] name = "gix-macros" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d8acb5ee668d55f0f2d19a320a3f9ef67a6999ad483e11135abcc2464ed18b6" +checksum = "02a5bcaf6704d9354a3071cede7e77d366a5980c7352e102e2c2f9b645b1d3ae" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.41", ] [[package]] name = "gix-object" -version = "0.36.0" +version = "0.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e5528d5b2c984044d547e696e44a8c45fa122e83cd8c2ac1da69bd474336be8" +checksum = "740f2a44267f58770a1cb3a3d01d14e67b089c7136c48d4bddbb3cfd2bf86a51" dependencies = [ "bstr", "btoi", @@ -2053,9 +2041,9 @@ dependencies = [ [[package]] name = "gix-odb" -version = "0.52.0" +version = "0.54.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0446eca295459deb3d6dd6ed7d44a631479f1b7381d8087166605c7a9f717c6" +checksum = "8630b56cb80d8fa684d383dad006a66401ee8314e12fbf0e566ddad8c115143b" dependencies = [ "arc-swap", "gix-date", @@ -2072,9 +2060,9 @@ dependencies = [ [[package]] name = "gix-pack" -version = "0.42.0" +version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be19ee650300d7cbac5829b637685ec44a8d921a7c2eaff8a245d8f2f008870c" +checksum = "1431ba2e30deff1405920693d54ab231c88d7c240dd6ccc936ee223d8f8697c3" dependencies = [ "clru", "gix-chunk", @@ -2084,7 +2072,7 @@ dependencies = [ "gix-object", "gix-path", "gix-tempfile", - "memmap2", + "memmap2 0.7.1", "parking_lot", "smallvec", "thiserror", @@ -2092,9 +2080,9 @@ dependencies = [ [[package]] name = "gix-path" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a1d370115171e3ae03c5c6d4f7d096f2981a40ddccb98dfd704c773530ba73b" +checksum = "d86d6fac2fabe07b67b7835f46d07571f68b11aa1aaecae94fe722ea4ef305e1" dependencies = [ "bstr", "gix-trace", @@ -2105,9 +2093,9 @@ dependencies = [ [[package]] name = "gix-quote" -version = "0.4.7" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "475c86a97dd0127ba4465fbb239abac9ea10e68301470c9791a6dd5351cdc905" +checksum = "4f84845efa535468bc79c5a87b9d29219f1da0313c8ecf0365a5daa7e72786f2" dependencies = [ "bstr", "btoi", @@ -2116,9 +2104,9 @@ dependencies = [ [[package]] name = "gix-ref" -version = "0.36.0" +version = "0.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cccbfa8d5cd9b86465f27a521e0c017de54b92d9fd37c143e49c658a2f04f3a" +checksum = "0ec2f6d07ac88d2fb8007ee3fa3e801856fb9d82e7366ec0ca332eb2c9d74a52" dependencies = [ "gix-actor", "gix-date", @@ -2130,16 +2118,16 @@ dependencies = [ "gix-path", "gix-tempfile", "gix-validate", - "memmap2", + "memmap2 0.7.1", "thiserror", "winnow", ] [[package]] name = "gix-refspec" -version = "0.17.0" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "678ba30d95baa5462df9875628ed40655d5f5b8aba7028de86ed57f36e762c6c" +checksum = "ccb0974cc41dbdb43a180c7f67aa481e1c1e160fcfa8f4a55291fd1126c1a6e7" dependencies = [ "bstr", "gix-hash", @@ -2151,9 +2139,9 @@ dependencies = [ [[package]] name = "gix-revision" -version = "0.21.0" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3e80a5992ae446fe1745dd26523b86084e3f1b6b3e35377fe09b4f35ac8f151" +checksum = "2ca97ac73459a7f3766aa4a5638a6e37d56d4c7962bc1986fbaf4883d0772588" dependencies = [ "bstr", "gix-date", @@ -2167,9 +2155,9 @@ dependencies = [ [[package]] name = "gix-revwalk" -version = "0.7.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b806349bc1f668e09035800e07ac8045da4e39a8925a245d93142c4802224ec1" +checksum = "a16d8c892e4cd676d86f0265bf9d40cefd73d8d94f86b213b8b77d50e77efae0" dependencies = [ "gix-commitgraph", "gix-date", @@ -2182,11 +2170,11 @@ dependencies = [ [[package]] name = "gix-sec" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92b9542ac025a8c02ed5d17b3fc031a111a384e859d0be3532ec4d58c40a0f28" +checksum = "a36ea2c5907d64a9b4b5d3cc9f430e6c30f0509646b5e38eb275ca57c5bf29e2" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.4.1", "gix-path", "libc", "windows", @@ -2194,9 +2182,9 @@ dependencies = [ [[package]] name = "gix-tempfile" -version = "9.0.0" +version = "11.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2762b91ff95e27ff3ea95758c0d4efacd7435a1be3629622928b8276de0f72a8" +checksum = "388dd29114a86ec69b28d1e26d6d63a662300ecf61ab3f4cc578f7d7dc9e7e23" dependencies = [ "gix-fs", "libc", @@ -2209,15 +2197,15 @@ dependencies = [ [[package]] name = "gix-trace" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96b6d623a1152c3facb79067d6e2ecdae48130030cf27d6eb21109f13bd7b836" +checksum = "b686a35799b53a9825575ca3f06481d0a053a409c4d97ffcf5ddd67a8760b497" [[package]] name = "gix-traverse" -version = "0.32.0" +version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ec6358f8373fb018af8fc96c9d2ec6a5b66999e2377dc40b7801351fec409ed" +checksum = "14d050ec7d4e1bb76abf0636cf4104fb915b70e54e3ced9a4427c999100ff38a" dependencies = [ "gix-commitgraph", "gix-date", @@ -2231,9 +2219,9 @@ dependencies = [ [[package]] name = "gix-url" -version = "0.23.0" +version = "0.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c79d595b99a6c7ab274f3c991735a0c0f5a816a3da460f513c48edf1c7bf2cc" +checksum = "0c427a1a11ccfa53a4a2da47d9442c2241deee63a154bc15cc14b8312fbc4005" dependencies = [ "bstr", "gix-features", @@ -2245,18 +2233,18 @@ dependencies = [ [[package]] name = "gix-utils" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b85d89dc728613e26e0ed952a19583744e7f5240fcd4aa30d6c824ffd8b52f0f" +checksum = "9f82c41937f00e15a1f6cb0b55307f0ca1f77f4407ff2bf440be35aa688c6a3e" dependencies = [ "fastrand", ] [[package]] name = "gix-validate" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e05cab2b03a45b866156e052aa38619f4ece4adcb2f79978bfc249bc3b21b8c5" +checksum = "75b7d8e4274be69f284bbc7e6bb2ccf7065dbcdeba22d8c549f2451ae426883f" dependencies = [ "bstr", "thiserror", @@ -2281,9 +2269,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.21" +version = "0.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91fc23aa11be92976ef4729127f1a74adf36d8436f7816b185d18df956790833" +checksum = "4d6250322ef6e60f93f9a2162799302cd6f68f79f6e5d85c8c16f14d1d958178" dependencies = [ "bytes", "fnv", @@ -2291,7 +2279,7 @@ dependencies = [ "futures-sink", "futures-util", "http", - "indexmap 1.9.3", + "indexmap 2.1.0", "slab", "tokio", "tokio-util", @@ -2321,20 +2309,20 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.14.1" +version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dfda62a12f55daeae5015f81b0baea145391cb4520f86c248fc615d72640d12" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" dependencies = [ "ahash", ] [[package]] name = "hdrhistogram" -version = "7.5.2" +version = "7.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f19b9f54f7c7f55e31401bb647626ce0cf0f67b0004982ce815b3ee72a02aa8" +checksum = "765c9198f173dd59ce26ff9f95ef0aafd0a0fe01fb9d72841bc5066a4c06511d" dependencies = [ - "base64 0.13.1", + "base64", "byteorder", "flate2", "nom", @@ -2347,7 +2335,7 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06683b93020a07e3dbcf5f8c0f6d40080d725bea7936fc01ad345c01b97dc270" dependencies = [ - "base64 0.21.4", + "base64", "bytes", "headers-core", "http", @@ -2403,9 +2391,9 @@ checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" [[package]] name = "hkdf" -version = "0.12.3" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "791a029f6b9fc27657f6f188ec6e5e43f6911f6f878e0dc5501396e09809d437" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" dependencies = [ "hmac", ] @@ -2430,9 +2418,9 @@ dependencies = [ [[package]] name = "http" -version = "0.2.9" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" +checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb" dependencies = [ "bytes", "fnv", @@ -2441,9 +2429,9 @@ dependencies = [ [[package]] name = "http-body" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" dependencies = [ "bytes", "http", @@ -2485,7 +2473,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "socket2 0.4.9", + "socket2 0.4.10", "tokio", "tower-service", "tracing", @@ -2506,16 +2494,16 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.57" +version = "0.1.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613" +checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", "wasm-bindgen", - "windows", + "windows-core", ] [[package]] @@ -2541,9 +2529,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" dependencies = [ "unicode-bidi", "unicode-normalization", @@ -2589,7 +2577,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" dependencies = [ "equivalent", - "hashbrown 0.14.1", + "hashbrown 0.14.3", "serde", ] @@ -2675,7 +2663,7 @@ name = "iroha_client" version = "2.0.0-pre-rc.20" dependencies = [ "attohttpc", - "base64 0.21.4", + "base64", "color-eyre", "criterion", "derive_more", @@ -2719,7 +2707,6 @@ dependencies = [ "dialoguer", "erased-serde", "iroha_client", - "iroha_config", "iroha_primitives", "json5", "once_cell", @@ -2877,7 +2864,7 @@ dependencies = [ name = "iroha_data_model" version = "2.0.0-pre-rc.20" dependencies = [ - "base64 0.21.4", + "base64", "criterion", "derive_more", "displaydoc", @@ -2915,7 +2902,7 @@ dependencies = [ "quote", "serde", "serde_json", - "syn 2.0.38", + "syn 2.0.41", "trybuild", ] @@ -2929,7 +2916,7 @@ dependencies = [ "manyhow", "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.41", "trybuild", ] @@ -2954,7 +2941,7 @@ dependencies = [ "manyhow", "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.41", ] [[package]] @@ -2980,7 +2967,7 @@ dependencies = [ "proc-macro2", "quote", "rustc-hash", - "syn 2.0.38", + "syn 2.0.41", "trybuild", ] @@ -3006,7 +2993,7 @@ dependencies = [ "manyhow", "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.41", ] [[package]] @@ -3064,7 +3051,7 @@ dependencies = [ "proc-macro2", "quote", "syn 1.0.109", - "syn 2.0.38", + "syn 2.0.41", ] [[package]] @@ -3118,7 +3105,7 @@ dependencies = [ "manyhow", "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.41", ] [[package]] @@ -3142,7 +3129,7 @@ dependencies = [ "manyhow", "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.41", "trybuild", ] @@ -3178,7 +3165,7 @@ dependencies = [ "manyhow", "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.41", ] [[package]] @@ -3198,7 +3185,7 @@ version = "2.0.0-pre-rc.20" name = "iroha_swarm" version = "2.0.0-pre-rc.20" dependencies = [ - "clap 4.4.6", + "clap 4.4.11", "color-eyre", "derive_more", "expect-test", @@ -3271,7 +3258,7 @@ dependencies = [ "manyhow", "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.41", ] [[package]] @@ -3302,7 +3289,7 @@ dependencies = [ "quote", "serde", "serde_json", - "syn 2.0.38", + "syn 2.0.41", "trybuild", ] @@ -3321,7 +3308,7 @@ dependencies = [ name = "iroha_wasm_builder_cli" version = "2.0.0-pre-rc.20" dependencies = [ - "clap 4.4.6", + "clap 4.4.11", "color-eyre", "iroha_wasm_builder", "owo-colors", @@ -3383,9 +3370,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.9" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" +checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" [[package]] name = "ittapi" @@ -3409,18 +3396,18 @@ dependencies = [ [[package]] name = "jobserver" -version = "0.1.26" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2" +checksum = "8c37f63953c4c63420ed5fd3d6d398c719489b9f872b9fa683262f8edd363c7d" dependencies = [ "libc", ] [[package]] name = "js-sys" -version = "0.3.64" +version = "0.3.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" +checksum = "cee9c64da59eae3b50095c18d3e74f8b73c0b86d2792824ff01bbce68ba229ca" dependencies = [ "wasm-bindgen", ] @@ -3438,9 +3425,9 @@ dependencies = [ [[package]] name = "k256" -version = "0.13.1" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cadb76004ed8e97623117f3df85b17aaa6626ab0b0831e6573f104df16cd1bcc" +checksum = "3f01b677d82ef7a676aa37e099defd83a28e15687112cafdd112d60236b6115b" dependencies = [ "cfg-if", "ecdsa", @@ -3454,7 +3441,7 @@ dependencies = [ name = "kagami" version = "2.0.0-pre-rc.20" dependencies = [ - "clap 4.4.6", + "clap 4.4.11", "color-eyre", "derive_more", "iroha_config", @@ -3481,7 +3468,7 @@ dependencies = [ name = "kura_inspector" version = "2.0.0-pre-rc.20" dependencies = [ - "clap 4.4.6", + "clap 4.4.11", "iroha_core", "iroha_data_model", "iroha_version", @@ -3501,9 +3488,9 @@ checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" [[package]] name = "libc" -version = "0.2.150" +version = "0.2.151" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" +checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4" [[package]] name = "libflate" @@ -3535,11 +3522,22 @@ version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" +[[package]] +name = "libredox" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8" +dependencies = [ + "bitflags 2.4.1", + "libc", + "redox_syscall", +] + [[package]] name = "libsodium-sys-stable" -version = "1.20.3" +version = "1.20.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfc31f983531631496f4e621110cd81468ab78b65dee0046cfddea83caa2c327" +checksum = "d1d164bc6f9139c5f95efb4f0be931b2bd5a9edf7e4e3c945d26b95ab8fa669b" dependencies = [ "cc", "libc", @@ -3563,15 +3561,15 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "969488b55f8ac402214f3f5fd243ebb7206cf82de60d3172994707a4bcc2b829" +checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456" [[package]] name = "lock_api" -version = "0.4.10" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" +checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" dependencies = [ "autocfg", "scopeguard", @@ -3603,7 +3601,7 @@ dependencies = [ "proc-macro2", "quote", "syn 1.0.109", - "syn 2.0.38", + "syn 2.0.41", ] [[package]] @@ -3656,6 +3654,15 @@ dependencies = [ "libc", ] +[[package]] +name = "memmap2" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "deaba38d7abf1d4cca21cc89e932e542ba2b9258664d2a9ef0e61512039c9375" +dependencies = [ + "libc", +] + [[package]] name = "memoffset" version = "0.9.0" @@ -3704,9 +3711,9 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.8" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" +checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" dependencies = [ "libc", "log", @@ -3734,7 +3741,7 @@ dependencies = [ "log", "memchr", "mime", - "spin 0.9.8", + "spin", "version_check", ] @@ -3821,16 +3828,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" dependencies = [ "crc32fast", - "hashbrown 0.14.1", + "hashbrown 0.14.3", "indexmap 2.1.0", "memchr", ] [[package]] name = "once_cell" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "oorandom" @@ -3852,11 +3859,11 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] name = "openssl" -version = "0.10.59" +version = "0.10.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a257ad03cd8fb16ad4172fedf8094451e1af1c4b70097636ef2eac9a5f0cc33" +checksum = "6b8419dc8cc6d866deb801274bba2e6f8f6108c1bb7fcc10ee5ab864931dbb45" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.4.1", "cfg-if", "foreign-types", "libc", @@ -3873,7 +3880,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.41", ] [[package]] @@ -3884,18 +3891,18 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-src" -version = "300.1.5+3.1.3" +version = "300.2.1+3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "559068e4c12950d7dcaa1857a61725c0d38d4fc03ff8e070ab31a75d6e316491" +checksum = "3fe476c29791a5ca0d1273c697e96085bbabbbea2ef7afd5617e78a4b40332d3" dependencies = [ "cc", ] [[package]] name = "openssl-sys" -version = "0.9.95" +version = "0.9.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40a4130519a360279579c2053038317e40eff64d13fd3f004f9e1b72b8a6aaf9" +checksum = "c3eaad34cdd97d81de97964fc7f29e2d104f483840d906ef56daa1912338460b" dependencies = [ "cc", "libc", @@ -3906,9 +3913,9 @@ dependencies = [ [[package]] name = "os_str_bytes" -version = "6.5.1" +version = "6.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d5d9eb14b174ee9aa2ef96dc2b94637a2d4b6e7cb873c7e171f0c20c6cf3eac" +checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1" [[package]] name = "overload" @@ -3927,9 +3934,9 @@ dependencies = [ [[package]] name = "parity-scale-codec" -version = "3.6.5" +version = "3.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dec8a8073036902368c2cdc0387e85ff9a37054d7e7c98e592145e0c92cd4fb" +checksum = "881331e34fa842a2fb61cc2db9643a8fedc615e47cfcc52597d1af0db9a7e8fe" dependencies = [ "arrayvec", "bitvec", @@ -3941,9 +3948,9 @@ dependencies = [ [[package]] name = "parity-scale-codec-derive" -version = "3.6.5" +version = "3.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "312270ee71e1cd70289dacf597cab7b207aa107d2f28191c2ae45b2ece18a260" +checksum = "be30eaf4b0a9fba5336683b38de57bb86d179a35862ba6bfcf57625d006bde5b" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -3955,7 +3962,7 @@ dependencies = [ name = "parity_scale_decoder" version = "2.0.0-pre-rc.20" dependencies = [ - "clap 4.4.6", + "clap 4.4.11", "colored", "eyre", "iroha_crypto", @@ -3982,15 +3989,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.8" +version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" +checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" dependencies = [ "backtrace", "cfg-if", "libc", "petgraph", - "redox_syscall 0.3.5", + "redox_syscall", "smallvec", "thread-id", "windows-targets 0.48.5", @@ -4019,7 +4026,7 @@ dependencies = [ "regex", "regex-syntax 0.7.5", "structmeta", - "syn 2.0.38", + "syn 2.0.41", ] [[package]] @@ -4054,15 +4061,15 @@ checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" [[package]] name = "percent-encoding" -version = "2.3.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pest" -version = "2.7.4" +version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c022f1e7b65d6a24c0dbbd5fb344c66881bc01f3e5ae74a1c8100f2f985d98a4" +checksum = "ae9cee2a55a544be8b89dc6848072af97a20f2422603c10865be2a42b580fff5" dependencies = [ "memchr", "thiserror", @@ -4071,9 +4078,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.7.4" +version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35513f630d46400a977c4cb58f78e1bfbe01434316e60c37d27b9ad6139c66d8" +checksum = "81d78524685f5ef2a3b3bd1cafbc9fcabb036253d9b1463e726a91cd16e2dfc2" dependencies = [ "pest", "pest_generator", @@ -4081,22 +4088,22 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.7.4" +version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc9fc1b9e7057baba189b5c626e2d6f40681ae5b6eb064dc7c7834101ec8123a" +checksum = "68bd1206e71118b5356dae5ddc61c8b11e28b09ef6a31acbd15ea48a28e0c227" dependencies = [ "pest", "pest_meta", "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.41", ] [[package]] name = "pest_meta" -version = "2.7.4" +version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1df74e9e7ec4053ceb980e7c0c8bd3594e977fde1af91daba9c928e8e8c6708d" +checksum = "7c747191d4ad9e4a4ab9c8798f1e82a39affe7ef9648390b7e5548d18e099de6" dependencies = [ "once_cell", "pest", @@ -4130,7 +4137,7 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.41", ] [[package]] @@ -4163,9 +4170,9 @@ checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" [[package]] name = "platforms" -version = "3.1.2" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4503fa043bf02cee09a9582e9554b4c6403b2ef55e4612e96561d294419429f8" +checksum = "14e6ab3f592e6fb464fc9712d8d6e6912de6473954635fd76a589d832cffcbb0" [[package]] name = "plotters" @@ -4206,6 +4213,12 @@ dependencies = [ "universal-hash", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -4214,11 +4227,11 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro-crate" -version = "1.3.1" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +checksum = "97dc5fea232fc28d2f597b37c4876b348a40e33f3b02cc975c8d006d78d94b1a" dependencies = [ - "once_cell", + "toml_datetime", "toml_edit", ] @@ -4259,9 +4272,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.69" +version = "1.0.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" +checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b" dependencies = [ "unicode-ident", ] @@ -4288,19 +4301,19 @@ dependencies = [ [[package]] name = "proptest" -version = "1.3.1" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c003ac8c77cb07bb74f5f198bce836a689bcd5a42574612bf14d17bfd08c20e" +checksum = "31b476131c3c86cb68032fdc5cb6d5a1045e3e42d96b69fa599fd77701e1f5bf" dependencies = [ "bit-set", "bit-vec", - "bitflags 2.4.0", + "bitflags 2.4.1", "lazy_static", "num-traits", "rand 0.8.5", "rand_chacha 0.3.1", "rand_xorshift", - "regex-syntax 0.7.5", + "regex-syntax 0.8.2", "rusty-fork", "tempfile", "unarray", @@ -4308,9 +4321,9 @@ dependencies = [ [[package]] name = "prost" -version = "0.12.1" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4fdd22f3b9c31b53c060df4a0613a1c7f062d4115a2b984dd15b1858f7e340d" +checksum = "146c289cda302b98a28d40c8b3b90498d6e526dd24ac2ecea73e4e491685b94a" dependencies = [ "bytes", "prost-derive", @@ -4318,22 +4331,22 @@ dependencies = [ [[package]] name = "prost-derive" -version = "0.12.1" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "265baba7fabd416cf5078179f7d2cbeca4ce7a9041111900675ea7c4cb8a4c32" +checksum = "efb6c9a1dd1def8e2124d17e83a20af56f1570d6c2d2bd9e266ccb768df3840e" dependencies = [ "anyhow", "itertools 0.11.0", "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.41", ] [[package]] name = "prost-types" -version = "0.12.1" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e081b29f63d83a4bc75cfc9f3fe424f9156cf92d8a4f0c9407cce9a1b67327cf" +checksum = "193898f59edcf43c26227dcd4c8427f00d99d61e95dcde58dabd49fa291d470e" dependencies = [ "prost", ] @@ -4376,7 +4389,7 @@ checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" dependencies = [ "getrandom 0.1.16", "libc", - "rand_chacha 0.2.1", + "rand_chacha 0.2.2", "rand_core 0.5.1", "rand_hc", ] @@ -4394,11 +4407,11 @@ dependencies = [ [[package]] name = "rand_chacha" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03a2a90da8c7523f554344f921aa97283eadf6ac484a6d2a7d0212fa7f8d6853" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" dependencies = [ - "c2-chacha", + "ppv-lite86", "rand_core 0.5.1", ] @@ -4427,7 +4440,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.10", + "getrandom 0.2.11", ] [[package]] @@ -4470,30 +4483,21 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" -dependencies = [ - "bitflags 1.3.2", -] - -[[package]] -name = "redox_syscall" -version = "0.3.5" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" dependencies = [ "bitflags 1.3.2", ] [[package]] name = "redox_users" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" +checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4" dependencies = [ - "getrandom 0.2.10", - "redox_syscall 0.2.16", + "getrandom 0.2.11", + "libredox", "thiserror", ] @@ -4512,14 +4516,14 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.0" +version = "1.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d119d7c7ca818f8a53c300863d4f87566aac09943aef5b355bb83969dae75d87" +checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.1", - "regex-syntax 0.8.0", + "regex-automata 0.4.3", + "regex-syntax 0.8.2", ] [[package]] @@ -4533,13 +4537,13 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.1" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "465c6fc0621e4abc4187a2bda0937bfd4f722c2730b29562e19689ea796c9a4b" +checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.0", + "regex-syntax 0.8.2", ] [[package]] @@ -4556,9 +4560,9 @@ checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" [[package]] name = "regex-syntax" -version = "0.8.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3cbb081b9784b07cceb8824c8583f86db4814d172ab043f3c23f7dc600bf83d" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" [[package]] name = "rfc6979" @@ -4572,17 +4576,16 @@ dependencies = [ [[package]] name = "ring" -version = "0.16.20" +version = "0.17.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +checksum = "688c63d65483050968b2a8937f7995f443e27041a0f7700aa59b0822aedebb74" dependencies = [ "cc", + "getrandom 0.2.11", "libc", - "once_cell", - "spin 0.5.2", + "spin", "untrusted", - "web-sys", - "winapi", + "windows-sys 0.48.0", ] [[package]] @@ -4614,22 +4617,22 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.25" +version = "0.38.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc99bc2d4f1fed22595588a013687477aedf3cdcfb26558c559edb67b4d9b22e" +checksum = "72e572a5e8ca657d7366229cdde4bd14c4eb5499a9573d4d366fe1b599daa316" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.4.1", "errno", "libc", "linux-raw-sys", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] name = "rustls" -version = "0.21.7" +version = "0.21.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd8d6c9f025a446bc4d18ad9632e69aec8f287aa84499ee335599fabd20c3fd8" +checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba" dependencies = [ "log", "ring", @@ -4651,18 +4654,18 @@ dependencies = [ [[package]] name = "rustls-pemfile" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" dependencies = [ - "base64 0.21.4", + "base64", ] [[package]] name = "rustls-webpki" -version = "0.101.6" +version = "0.101.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c7d5dece342910d9ba34d259310cae3e0154b873b35408b787b59bce53d34fe" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" dependencies = [ "ring", "untrusted", @@ -4688,9 +4691,9 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.15" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" +checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" [[package]] name = "same-file" @@ -4730,9 +4733,9 @@ checksum = "a3cf7c11c38cb994f3d40e8a8cde3bbd1f72a435e4c49e85d6553d8312306152" [[package]] name = "sct" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" dependencies = [ "ring", "untrusted", @@ -4765,9 +4768,9 @@ dependencies = [ [[package]] name = "secp256k1-sys" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09e67c467c38fd24bd5499dc9a18183b31575c12ee549197e3e20d57aa4fe3b7" +checksum = "4dd97a086ec737e30053fd5c46f097465d25bb81dd3608825f65298c4c98be83" dependencies = [ "cc", ] @@ -4803,9 +4806,9 @@ checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" [[package]] name = "serde" -version = "1.0.188" +version = "1.0.193" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" +checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" dependencies = [ "serde_derive", ] @@ -4821,20 +4824,20 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.188" +version = "1.0.193" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" +checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.41", ] [[package]] name = "serde_json" -version = "1.0.107" +version = "1.0.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65" +checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" dependencies = [ "itoa", "ryu", @@ -4855,9 +4858,9 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.3.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ca3b16a3d82c4088f343b7480a93550b3eabe1a358569c2dfe38bbcead07237" +checksum = "64cd236ccc1b7a29e7e2739f27c0b2dd199804abc4290e32f59f3b68d6405c23" dependencies = [ "serde", "serde_with_macros", @@ -4865,21 +4868,21 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.3.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e6be15c453eb305019bfa438b1593c731f36a289a7853f7707ee29e870b3b3c" +checksum = "93634eb5f75a2323b16de4748022ac4297f9e76b6dced2be287a099f41b5e788" dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.41", ] [[package]] name = "serde_yaml" -version = "0.9.25" +version = "0.9.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a49e178e4452f45cb61d0cd8cebc1b0fafd3e41929e996cef79aa3aca91f574" +checksum = "3cc7a1570e38322cfe4154732e5110f887ea57e22b76f4bfd32b5bdd3368666c" dependencies = [ "indexmap 2.1.0", "itoa", @@ -4910,7 +4913,7 @@ checksum = "91d129178576168c589c9ec973feedf7d3126c01ac2bf08795109aa35b69fb8f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.41", ] [[package]] @@ -5014,9 +5017,9 @@ dependencies = [ [[package]] name = "signature" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e1788eed21689f9cf370582dfc467ef36ed9c707f073528ddafa8d83e3b8500" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ "digest 0.10.7", "rand_core 0.6.4", @@ -5049,18 +5052,18 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.11.1" +version = "1.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" +checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" dependencies = [ "serde", ] [[package]] name = "socket2" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" +checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" dependencies = [ "libc", "winapi", @@ -5068,20 +5071,14 @@ dependencies = [ [[package]] name = "socket2" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4031e820eb552adee9295814c0ced9e5cf38ddf1e8b7d566d6de8e2538ea989e" +checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" dependencies = [ "libc", "windows-sys 0.48.0", ] -[[package]] -name = "spin" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" - [[package]] name = "spin" version = "0.9.8" @@ -5101,9 +5098,9 @@ dependencies = [ [[package]] name = "spki" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d1e996ef02c474957d681f1b05213dfb0abab947b446a62d37770b23500184a" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" dependencies = [ "base64ct", "der", @@ -5164,7 +5161,7 @@ dependencies = [ "proc-macro2", "quote", "structmeta-derive", - "syn 2.0.38", + "syn 2.0.41", ] [[package]] @@ -5175,7 +5172,7 @@ checksum = "a60bcaff7397072dca0017d1db428e30d5002e00b6847703e2e42005c95fbe00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.41", ] [[package]] @@ -5190,7 +5187,7 @@ version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" dependencies = [ - "strum_macros 0.25.2", + "strum_macros 0.25.3", ] [[package]] @@ -5208,15 +5205,15 @@ dependencies = [ [[package]] name = "strum_macros" -version = "0.25.2" +version = "0.25.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad8d03b598d3d0fff69bf533ee3ef19b8eeb342729596df84bcc7e1f96ec4059" +checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0" dependencies = [ "heck", "proc-macro2", "quote", "rustversion", - "syn 2.0.38", + "syn 2.0.41", ] [[package]] @@ -5267,9 +5264,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.38" +version = "2.0.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b" +checksum = "44c8b28c477cc3bf0e7966561e3460130e1255f7a1cf71931075f1c5e7a7e269" dependencies = [ "proc-macro2", "quote", @@ -5307,22 +5304,22 @@ checksum = "14c39fd04924ca3a864207c66fc2cd7d22d7c016007f9ce846cbb9326331930a" [[package]] name = "tempfile" -version = "3.8.0" +version = "3.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef" +checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5" dependencies = [ "cfg-if", "fastrand", - "redox_syscall 0.3.5", + "redox_syscall", "rustix", "windows-sys 0.48.0", ] [[package]] name = "termcolor" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6093bad37da69aab9d123a8091e4be0aa4a03e4d601ec641c327398315f62b64" +checksum = "ff1bc3d3f05aff0403e8ac0d92ced918ec05b666a43f83297ccef5bea8a3d449" dependencies = [ "winapi-util", ] @@ -5373,17 +5370,16 @@ checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.41", ] [[package]] name = "thread-id" -version = "4.2.0" +version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79474f573561cdc4871a0de34a51c92f7f5a56039113fbb5b9c9f96bdb756669" +checksum = "f0ec81c46e9eb50deaa257be2f148adf052d1fb7701cfd55ccfab2525280b70b" dependencies = [ "libc", - "redox_syscall 0.2.16", "winapi", ] @@ -5405,14 +5401,15 @@ dependencies = [ [[package]] name = "time" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "426f806f4089c493dcac0d24c29c01e2c38baf8e30f1b716ee37e83d200b18fe" +checksum = "c4a34ab300f2dee6e562c10a046fc05e358b29f9bf92277f30c3c8d82275f6f5" dependencies = [ "deranged", "itoa", "libc", "num_threads", + "powerfmt", "serde", "time-core", "time-macros", @@ -5460,9 +5457,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.33.0" +version = "1.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f38200e3ef7995e5ef13baec2f432a6da0aa9ac495b2c0e8f3b7eec2c92d653" +checksum = "841d45b238a16291a4e1584e61820b8ae57d696cc5015c459c229ccc6990cc1c" dependencies = [ "backtrace", "bytes", @@ -5471,7 +5468,7 @@ dependencies = [ "num_cpus", "pin-project-lite", "signal-hook-registry", - "socket2 0.5.4", + "socket2 0.5.5", "tokio-macros", "tracing", "windows-sys 0.48.0", @@ -5489,13 +5486,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" +checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.41", ] [[package]] @@ -5545,14 +5542,14 @@ dependencies = [ "tokio-native-tls", "tokio-rustls", "tungstenite", - "webpki-roots 0.25.2", + "webpki-roots 0.25.3", ] [[package]] name = "tokio-util" -version = "0.7.9" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d68074620f57a0b21594d9735eb2e98ab38b17f80d3fcb189fca266771ca60d" +checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" dependencies = [ "bytes", "futures-core", @@ -5579,9 +5576,9 @@ checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" [[package]] name = "toml_edit" -version = "0.19.15" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" dependencies = [ "indexmap 2.1.0", "toml_datetime", @@ -5597,7 +5594,7 @@ dependencies = [ "async-stream", "async-trait", "axum", - "base64 0.21.4", + "base64", "bytes", "h2", "http", @@ -5649,11 +5646,10 @@ checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" [[package]] name = "tracing" -version = "0.1.37" +version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ - "cfg-if", "log", "pin-project-lite", "tracing-attributes", @@ -5662,20 +5658,20 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.26" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.41", ] [[package]] name = "tracing-core" -version = "0.1.31" +version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ "once_cell", "valuable", @@ -5724,9 +5720,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.17" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" dependencies = [ "matchers", "nu-ansi-term", @@ -5744,9 +5740,9 @@ dependencies = [ [[package]] name = "try-lock" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "trybuild" @@ -5815,15 +5811,15 @@ dependencies = [ [[package]] name = "unicode-bidi" -version = "0.3.13" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" +checksum = "6f2528f27a9eb2b21e69c95319b30bd0efd85d09c379741b0f78ea1d86be2416" [[package]] name = "unicode-bom" -version = "2.0.2" +version = "2.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98e90c70c9f0d4d1ee6d0a7d04aa06cb9bbd53d8cfbdd62a0269a7c2eb640552" +checksum = "7eec5d1121208364f6793f7d2e222bf75a915c19557537745b195b253dd64217" [[package]] name = "unicode-ident" @@ -5885,30 +5881,30 @@ checksum = "f28467d3e1d3c6586d8f25fa243f544f5800fec42d97032474e17222c2b75cfa" [[package]] name = "untrusted" -version = "0.7.1" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "ureq" -version = "2.8.0" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5ccd538d4a604753ebc2f17cd9946e89b77bf87f6a8e2309667c6f2e87855e3" +checksum = "f8cdd25c339e200129fe4de81451814e5228c9b771d57378817d6117cc2b3f97" dependencies = [ - "base64 0.21.4", + "base64", "log", "once_cell", "rustls", "rustls-webpki", "url", - "webpki-roots 0.25.2", + "webpki-roots 0.25.3", ] [[package]] name = "url" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5" +checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" dependencies = [ "form_urlencoded", "idna", @@ -5924,9 +5920,9 @@ checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" [[package]] name = "utf8-width" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5190c9442dcdaf0ddd50f37420417d219ae5261bbf5db120d0f9bab996c9cba1" +checksum = "86bd8d4e895da8537e5315b8254664e6b769c4ff3db18321b297a1e7004392e3" [[package]] name = "utf8parse" @@ -5936,11 +5932,11 @@ checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" [[package]] name = "uuid" -version = "1.4.1" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d" +checksum = "5e395fcf16a7a3d8127ec99782007af141946b4795001f876d54fb0d55978560" dependencies = [ - "getrandom 0.2.10", + "getrandom 0.2.11", ] [[package]] @@ -5957,9 +5953,9 @@ checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "vergen" -version = "8.2.5" +version = "8.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85e7dc29b3c54a2ea67ef4f953d5ec0c4085035c0ae2d325be1c0d2144bd9f16" +checksum = "1290fd64cc4e7d3c9b07d7f333ce0ce0007253e32870e632624835cc80b83939" dependencies = [ "anyhow", "gix", @@ -6046,9 +6042,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.87" +version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" +checksum = "0ed0d4f68a3015cc185aff4db9506a015f4b96f95303897bfa23f846db54064e" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -6056,24 +6052,24 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.87" +version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" +checksum = "1b56f625e64f3a1084ded111c4d5f477df9f8c92df113852fa5a374dbda78826" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.41", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.87" +version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" +checksum = "0162dbf37223cd2afce98f3d0785506dcb8d266223983e4b5b525859e6e182b2" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -6081,22 +6077,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.87" +version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" +checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.41", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.87" +version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" +checksum = "7ab9b36309365056cd639da3134bf87fa8f3d86008abf99e612384a6eecd459f" [[package]] name = "wasm-encoder" @@ -6109,9 +6105,9 @@ dependencies = [ [[package]] name = "wasm-encoder" -version = "0.38.0" +version = "0.38.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b09bc5df933a3dabbdb72ae4b6b71be8ae07f58774d5aa41bd20adcd41a235a" +checksum = "0ad2b51884de9c7f4fe2fd1043fccb8dcad4b1e29558146ee57a144d15779f3f" dependencies = [ "leb128", ] @@ -6168,9 +6164,9 @@ dependencies = [ [[package]] name = "wasmtime" -version = "15.0.0" +version = "15.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae4b1702ef55144d6f594085f4989dc71fb71a791be1c8354ecc8e489b81199b" +checksum = "642e12d108e800215263e3b95972977f473957923103029d7d617db701d67ba4" dependencies = [ "anyhow", "async-trait", @@ -6205,21 +6201,21 @@ dependencies = [ [[package]] name = "wasmtime-asm-macros" -version = "15.0.0" +version = "15.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c981d0e87bb3e98e08e76644e7ae5dfdef7f1d4105145853f3d677bb4535d65f" +checksum = "beada8bb15df52503de0a4c58de4357bfd2f96d9a44a6e547bad11efdd988b47" dependencies = [ "cfg-if", ] [[package]] name = "wasmtime-cache" -version = "15.0.0" +version = "15.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d7ba8adaa84fdb9dd659275edcf7fc5282c44b9c9f829986c71d44fd52ea80a" +checksum = "aba5bf44d044d25892c03fb3534373936ee204141ff92bac8297787ac7f22318" dependencies = [ "anyhow", - "base64 0.21.4", + "base64", "bincode", "directories-next", "log", @@ -6234,14 +6230,14 @@ dependencies = [ [[package]] name = "wasmtime-component-macro" -version = "15.0.0" +version = "15.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c91dcbbd0e1f094351d1ae0e53463c63ba53ec8f8e0e21d17567c1979a8c3758" +checksum = "56ccba556991465cca68d5a54769684bcf489fb532059da55105f851642d52c1" dependencies = [ "anyhow", "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.41", "wasmtime-component-util", "wasmtime-wit-bindgen", "wit-parser", @@ -6249,15 +6245,15 @@ dependencies = [ [[package]] name = "wasmtime-component-util" -version = "15.0.0" +version = "15.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e85f1319a7ed36aa59446ab7e967d0c2fb0cd179bf56913633190b44572023e" +checksum = "05492a177a6006cb73f034d6e9a6fad6da55b23c4398835cb0012b5fa51ecf67" [[package]] name = "wasmtime-cranelift" -version = "15.0.0" +version = "15.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1453665878e16245b9a25405e550c4a36c6731c6e34ea804edc002a38c3e6741" +checksum = "fe2e7532f1d6adbcc57e69bb6a7c503f0859076d07a9b4b6aabe8021ff8a05fd" dependencies = [ "anyhow", "cfg-if", @@ -6280,9 +6276,9 @@ dependencies = [ [[package]] name = "wasmtime-cranelift-shared" -version = "15.0.0" +version = "15.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3dface3d9b72b4670781ff72675eabb291e2836b5dded6bb312b577d2bb561f" +checksum = "8c98d5378a856cbf058d36278627dfabf0ed68a888142958c7ae8e6af507dafa" dependencies = [ "anyhow", "cranelift-codegen", @@ -6296,9 +6292,9 @@ dependencies = [ [[package]] name = "wasmtime-environ" -version = "15.0.0" +version = "15.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0116108e7d231cce15fe7dd642c66c3abb14dbcf169b0130e11f223ce8d1ad7" +checksum = "a6d33a9f421da810a070cd56add9bc51f852bd66afbb8b920489d6242f15b70e" dependencies = [ "anyhow", "cranelift-entity", @@ -6316,9 +6312,9 @@ dependencies = [ [[package]] name = "wasmtime-fiber" -version = "15.0.0" +version = "15.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8a5896355c37bf0f9feb4f1299142ef4bed8c92576aa3a41d150fed0cafa056" +checksum = "404741f4c6d7f4e043be2e8b466406a2aee289ccdba22bf9eba6399921121b97" dependencies = [ "anyhow", "cc", @@ -6331,9 +6327,9 @@ dependencies = [ [[package]] name = "wasmtime-jit" -version = "15.0.0" +version = "15.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e32b210767452f6b20157bb7c7d98295b92cc47aaad2a8aa31652f4469813a5d" +checksum = "8d0994a86d6dca5f7d9740d7f2bd0568be06d2014a550361dc1c397d289d81ef" dependencies = [ "addr2line", "anyhow", @@ -6358,9 +6354,9 @@ dependencies = [ [[package]] name = "wasmtime-jit-debug" -version = "15.0.0" +version = "15.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bffd2785a16c55ac77565613ebda625f5850d4014af0499df750e8de97c04547" +checksum = "4e0c4b74e606d1462d648631d5bc328e3d5b14e7f9d3ff93bc6db062fb8c5cd8" dependencies = [ "object", "once_cell", @@ -6370,9 +6366,9 @@ dependencies = [ [[package]] name = "wasmtime-jit-icache-coherence" -version = "15.0.0" +version = "15.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b73ad1395eda136baec5ece7e079e0536a82ef73488e345456cc9b89858ad0ec" +checksum = "3090a69ba1476979e090aa7ed4bc759178bafdb65b22f98b9ba24fc6e7e578d5" dependencies = [ "cfg-if", "libc", @@ -6381,9 +6377,9 @@ dependencies = [ [[package]] name = "wasmtime-runtime" -version = "15.0.0" +version = "15.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77b50f7f3c1a8dabb2607f32a81242917bd77cee75f3dec66e04b02ccbb8ba07" +checksum = "b993ac8380385ed67bf71b51b9553edcf1ab0801b78a805a067de581b9a3e88a" dependencies = [ "anyhow", "cc", @@ -6410,9 +6406,9 @@ dependencies = [ [[package]] name = "wasmtime-types" -version = "15.0.0" +version = "15.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "447973db3dc5c24db14130ab0922795c58790aec296d198ad9d253b82ec67471" +checksum = "8b5778112fcab2dc3d4371f4203ab8facf0c453dd94312b0a88dd662955e64e0" dependencies = [ "cranelift-entity", "serde", @@ -6423,20 +6419,20 @@ dependencies = [ [[package]] name = "wasmtime-versioned-export-macros" -version = "15.0.0" +version = "15.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a347bb8ecf12275fb180afb1b1c85c9e186553c43109737bffed4f54c2aa365" +checksum = "f50f51f8d79bfd2aa8e9d9a0ae7c2d02b45fe412e62ff1b87c0c81b07c738231" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.41", ] [[package]] name = "wasmtime-wit-bindgen" -version = "15.0.0" +version = "15.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41786c7bbbf250c0e685b291323b50c6bb65f0505a2c0b4f0b598c740f13f185" +checksum = "4b804dfd3d0c0d6d37aa21026fe7772ba1a769c89ee4f5c4f13b82d91d75216f" dependencies = [ "anyhow", "heck", @@ -6446,36 +6442,36 @@ dependencies = [ [[package]] name = "wasmtime-wmemcheck" -version = "15.0.0" +version = "15.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47907bdd67500c66fa308acbce7387c7bfb63b5505ef81be7fc897709afcca60" +checksum = "9b6060bc082cc32d9a45587c7640e29e3c7b89ada82677ac25d87850aaccb368" [[package]] name = "wast" -version = "69.0.0" +version = "69.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efa51b5ad1391943d1bfad537e50f28fe938199ee76b115be6bae83802cd5185" +checksum = "c1ee37317321afde358e4d7593745942c48d6d17e0e6e943704de9bbee121e7a" dependencies = [ "leb128", "memchr", "unicode-width", - "wasm-encoder 0.38.0", + "wasm-encoder 0.38.1", ] [[package]] name = "wat" -version = "1.0.81" +version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74a4c2488d058326466e086a43f5d4ea448241a8d0975e3eb0642c0828be1eb3" +checksum = "aeb338ee8dee4d4cd05e6426683f21c5087dc7cfc8903e839ccf48d43332da3c" dependencies = [ "wast", ] [[package]] name = "web-sys" -version = "0.3.64" +version = "0.3.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" +checksum = "50c24a44ec86bb68fbecd1b3efed7e85ea5621b39b35ef2766b66cd984f8010f" dependencies = [ "js-sys", "wasm-bindgen", @@ -6502,9 +6498,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.25.2" +version = "0.25.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14247bb57be4f377dfb94c72830b8ce8fc6beac03cf4bf7b9732eadd414123fc" +checksum = "1778a42e8b3b90bff8d0f5032bf22250792889a5cdc752aa0020c84abe3aaf10" [[package]] name = "winapi" @@ -6546,6 +6542,15 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "windows-core" +version = "0.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows-sys" version = "0.45.0" @@ -6564,6 +6569,15 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.0", +] + [[package]] name = "windows-targets" version = "0.42.2" @@ -6594,6 +6608,21 @@ dependencies = [ "windows_x86_64_msvc 0.48.5", ] +[[package]] +name = "windows-targets" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +dependencies = [ + "windows_aarch64_gnullvm 0.52.0", + "windows_aarch64_msvc 0.52.0", + "windows_i686_gnu 0.52.0", + "windows_i686_msvc 0.52.0", + "windows_x86_64_gnu 0.52.0", + "windows_x86_64_gnullvm 0.52.0", + "windows_x86_64_msvc 0.52.0", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" @@ -6606,6 +6635,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" + [[package]] name = "windows_aarch64_msvc" version = "0.42.2" @@ -6618,6 +6653,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" + [[package]] name = "windows_i686_gnu" version = "0.42.2" @@ -6630,6 +6671,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" +[[package]] +name = "windows_i686_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" + [[package]] name = "windows_i686_msvc" version = "0.42.2" @@ -6642,6 +6689,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" +[[package]] +name = "windows_i686_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" + [[package]] name = "windows_x86_64_gnu" version = "0.42.2" @@ -6654,6 +6707,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" + [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" @@ -6666,6 +6725,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" + [[package]] name = "windows_x86_64_msvc" version = "0.42.2" @@ -6678,11 +6743,17 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" + [[package]] name = "winnow" -version = "0.5.16" +version = "0.5.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711d82167854aff2018dfd193aa0fef5370f456732f0d5a0c59b0f1b4b907" +checksum = "6c830786f7720c2fd27a1a0e27a709dbd3c4d009b56d098fc742d4f4eab91fe2" dependencies = [ "memchr", ] @@ -6727,18 +6798,40 @@ dependencies = [ [[package]] name = "xattr" -version = "1.0.1" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4686009f71ff3e5c4dbcf1a282d0a44db3f021ba69350cd42086b3e5f1c6985" +checksum = "a7dae5072fe1f8db8f8d29059189ac175196e410e40ba42d5d4684ae2f750995" dependencies = [ "libc", + "linux-raw-sys", + "rustix", +] + +[[package]] +name = "zerocopy" +version = "0.7.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c4061bedbb353041c12f413700357bec76df2c7e2ca8e4df8bac24c6bf68e3d" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3c129550b3e6de3fd0ba67ba5c81818f9805e58b8d7fee80a3a59d2c9fc601a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.41", ] [[package]] name = "zeroize" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" +checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" dependencies = [ "zeroize_derive", ] @@ -6751,7 +6844,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.41", ] [[package]] @@ -6787,11 +6880,10 @@ dependencies = [ [[package]] name = "zstd-sys" -version = "2.0.8+zstd.1.5.5" +version = "2.0.9+zstd.1.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5556e6ee25d32df2586c098bbfa278803692a20d0ab9565e049480d52707ec8c" +checksum = "9e16efa8a874a0481a574084d34cc26fdb3b99627480f785888deb6386506656" dependencies = [ "cc", - "libc", "pkg-config", ] diff --git a/client/examples/tutorial.rs b/client/examples/tutorial.rs index b22b0d1be52..cead2516b4a 100644 --- a/client/examples/tutorial.rs +++ b/client/examples/tutorial.rs @@ -3,8 +3,8 @@ use std::fs::File; use eyre::{Error, WrapErr}; +use iroha_client::config::Configuration; // #region rust_config_crates -use iroha_config::client::Configuration; // #endregion rust_config_crates fn main() { diff --git a/client/src/client.rs b/client/src/client.rs index a33b9f6a8d4..3a4c7615397 100644 --- a/client/src/client.rs +++ b/client/src/client.rs @@ -13,7 +13,6 @@ use derive_more::{DebugCustom, Display}; use eyre::{eyre, Result, WrapErr}; use futures_util::StreamExt; use http_default::{AsyncWebSocketStream, WebSocketStream}; -use iroha_config::{client::Configuration, client_api::ConfigurationDTO, torii::uri}; use iroha_logger::prelude::*; use iroha_telemetry::metrics::Status; use iroha_version::prelude::*; @@ -23,6 +22,7 @@ use url::Url; use self::{blocks_api::AsyncBlockStream, events_api::AsyncEventStream}; use crate::{ + config::{api::ConfigurationDTO, Configuration}, crypto::{HashOf, KeyPair}, data_model::{ block::SignedBlock, @@ -372,7 +372,7 @@ pub struct QueryRequest { impl QueryRequest { #[cfg(test)] fn dummy() -> Self { - let torii_url = iroha_config::torii::uri::DEFAULT_API_ADDR; + let torii_url = crate::config::torii::DEFAULT_API_ADDR; Self { torii_url: format!("http://{torii_url}").parse().unwrap(), @@ -391,7 +391,9 @@ impl QueryRequest { fn assemble(self) -> DefaultRequestBuilder { let builder = DefaultRequestBuilder::new( HttpMethod::POST, - self.torii_url.join(uri::QUERY).expect("Valid URI"), + self.torii_url + .join(crate::config::torii::QUERY) + .expect("Valid URI"), ) .headers(self.headers); @@ -682,7 +684,9 @@ impl Client { ( B::new( HttpMethod::POST, - self.torii_url.join(uri::TRANSACTION).expect("Valid URI"), + self.torii_url + .join(crate::config::torii::TRANSACTION) + .expect("Valid URI"), ) .headers(self.headers.clone()) .body(transaction_bytes), @@ -752,7 +756,7 @@ impl Client { /// /// ```ignore /// use eyre::Result; - /// use iroha_client::{ + /// use crate::{ /// data_model::{predicate::PredicateBox, prelude::{Account, FindAllAccounts, Pagination}}, /// client::Client, /// http::{RequestBuilder, Response, Method}, @@ -951,7 +955,9 @@ impl Client { events_api::flow::Init::new( event_filter, self.headers.clone(), - self.torii_url.join(uri::SUBSCRIPTION).expect("Valid URI"), + self.torii_url + .join(crate::config::torii::SUBSCRIPTION) + .expect("Valid URI"), ) } @@ -985,7 +991,9 @@ impl Client { blocks_api::flow::Init::new( height, self.headers.clone(), - self.torii_url.join(uri::BLOCKS_STREAM).expect("Valid URI"), + self.torii_url + .join(crate::config::torii::BLOCKS_STREAM) + .expect("Valid URI"), ) } @@ -1018,7 +1026,7 @@ impl Client { let response = DefaultRequestBuilder::new( HttpMethod::GET, self.torii_url - .join(uri::PENDING_TRANSACTIONS) + .join(crate::config::torii::PENDING_TRANSACTIONS) .expect("Valid URI"), ) .params(pagination.clone()) @@ -1079,7 +1087,9 @@ impl Client { pub fn get_config(&self) -> Result { let resp = DefaultRequestBuilder::new( HttpMethod::GET, - self.torii_url.join(uri::CONFIGURATION).expect("Valid URI"), + self.torii_url + .join(crate::config::torii::CONFIGURATION) + .expect("Valid URI"), ) .header(http::header::CONTENT_TYPE, APPLICATION_JSON) .build()? @@ -1101,7 +1111,10 @@ impl Client { /// If sending request or decoding fails pub fn set_config(&self, dto: ConfigurationDTO) -> Result<()> { let body = serde_json::to_vec(&dto).wrap_err(format!("Failed to serialize {dto:?}"))?; - let url = self.torii_url.join(uri::CONFIGURATION).expect("Valid URI"); + let url = self + .torii_url + .join(crate::config::torii::CONFIGURATION) + .expect("Valid URI"); let resp = DefaultRequestBuilder::new(HttpMethod::POST, url) .header(http::header::CONTENT_TYPE, APPLICATION_JSON) .body(body) @@ -1138,7 +1151,9 @@ impl Client { pub fn prepare_status_request(&self) -> B { B::new( HttpMethod::GET, - self.torii_url.join(uri::STATUS).expect("Valid URI"), + self.torii_url + .join(crate::config::torii::STATUS) + .expect("Valid URI"), ) .headers(self.headers.clone()) } @@ -1634,13 +1649,10 @@ pub mod parameter { mod tests { use std::str::FromStr; - use iroha_config::{ - client::{BasicAuth, ConfigurationProxy, WebLogin}, - torii::uri::DEFAULT_API_ADDR, - }; use iroha_primitives::small::SmallStr; use super::*; + use crate::config::{torii::DEFAULT_API_ADDR, BasicAuth, ConfigurationProxy, WebLogin}; const LOGIN: &str = "mad_hatter"; const PASSWORD: &str = "ilovetea"; diff --git a/client/src/lib.rs b/client/src/lib.rs index c33fd84f409..78a3cbeac13 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -9,13 +9,11 @@ mod query_builder; /// Module containing sample configurations for tests and benchmarks. pub mod samples { - use iroha_config::{ - client::{Configuration, ConfigurationProxy}, - torii::uri::DEFAULT_API_ADDR, + use crate::{ + config::{torii::DEFAULT_API_ADDR, Configuration, ConfigurationProxy}, + crypto::KeyPair, }; - use crate::crypto::KeyPair; - /// Get sample client configuration. pub fn get_client_config(key_pair: &KeyPair) -> Configuration { let (public_key, private_key) = key_pair.clone().into(); @@ -39,5 +37,11 @@ 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_crypto as crypto; pub use iroha_data_model as data_model; diff --git a/client/tests/integration/add_account.rs b/client/tests/integration/add_account.rs index bdec9441783..d46b3bb65af 100644 --- a/client/tests/integration/add_account.rs +++ b/client/tests/integration/add_account.rs @@ -2,6 +2,7 @@ use std::thread; use eyre::Result; use iroha_client::{client, data_model::prelude::*}; +use iroha_config::iroha::Configuration; use test_network::*; #[test] @@ -10,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 = super::Configuration::pipeline_time(); + let pipeline_time = Configuration::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 09bf95bb90d..bb889c25c15 100644 --- a/client/tests/integration/add_domain.rs +++ b/client/tests/integration/add_domain.rs @@ -2,10 +2,9 @@ use std::thread; use eyre::Result; use iroha_client::{client, data_model::prelude::*}; +use iroha_config::iroha::Configuration; use test_network::*; -use super::Configuration; - #[test] fn client_add_domain_with_name_length_more_than_limit_should_not_commit_transaction() -> Result<()> { diff --git a/client/tests/integration/asset.rs b/client/tests/integration/asset.rs index 5744827d134..3b151b99ec8 100644 --- a/client/tests/integration/asset.rs +++ b/client/tests/integration/asset.rs @@ -6,12 +6,11 @@ use iroha_client::{ crypto::{KeyPair, PublicKey}, data_model::prelude::*, }; +use iroha_config::iroha::Configuration; use iroha_primitives::fixed::Fixed; use serde_json::json; use test_network::*; -use super::Configuration; - #[test] fn client_register_asset_should_add_asset_once_but_not_twice() -> Result<()> { let (_rt, _peer, test_client) = ::new().with_port(10_620).start_with_runtime(); diff --git a/client/tests/integration/asset_propagation.rs b/client/tests/integration/asset_propagation.rs index 4e35ef266a1..cb88cbef3e6 100644 --- a/client/tests/integration/asset_propagation.rs +++ b/client/tests/integration/asset_propagation.rs @@ -9,10 +9,9 @@ use iroha_client::{ prelude::*, }, }; +use iroha_config::iroha::Configuration; use test_network::*; -use super::Configuration; - #[test] fn client_add_asset_quantity_to_existing_asset_should_increase_asset_amount_on_another_peer( ) -> Result<()> { diff --git a/client/tests/integration/connected_peers.rs b/client/tests/integration/connected_peers.rs index 91745b15ff8..6d80fe530cf 100644 --- a/client/tests/integration/connected_peers.rs +++ b/client/tests/integration/connected_peers.rs @@ -9,10 +9,9 @@ use iroha_client::{ prelude::*, }, }; +use iroha_config::iroha::Configuration; use test_network::*; -use super::Configuration; - #[ignore = "ignore, more in #2851"] #[test] fn connected_peers_with_f_2_1_2() -> Result<()> { diff --git a/client/tests/integration/events/mod.rs b/client/tests/integration/events/mod.rs index cf35d126a5e..2f478366933 100644 --- a/client/tests/integration/events/mod.rs +++ b/client/tests/integration/events/mod.rs @@ -1,4 +1,3 @@ -pub use super::Configuration; mod data; mod notification; mod pipeline; diff --git a/client/tests/integration/events/pipeline.rs b/client/tests/integration/events/pipeline.rs index 35b8e16629a..ec4579bcbe4 100644 --- a/client/tests/integration/events/pipeline.rs +++ b/client/tests/integration/events/pipeline.rs @@ -8,10 +8,9 @@ use iroha_client::{ prelude::*, }, }; +use iroha_config::iroha::Configuration; use test_network::*; -use super::Configuration; - // Needed to re-enable ignored tests. #[allow(dead_code)] const PEER_COUNT: usize = 7; diff --git a/client/tests/integration/mod.rs b/client/tests/integration/mod.rs index 5bb44460bb0..8c8008ce649 100644 --- a/client/tests/integration/mod.rs +++ b/client/tests/integration/mod.rs @@ -1,8 +1,3 @@ -pub use iroha_config::{ - base::proxy::Builder, - iroha::{Configuration, ConfigurationProxy}, -}; - mod add_account; mod add_domain; mod asset; diff --git a/client/tests/integration/multiple_blocks_created.rs b/client/tests/integration/multiple_blocks_created.rs index f0d28bb4a26..a674bb357da 100644 --- a/client/tests/integration/multiple_blocks_created.rs +++ b/client/tests/integration/multiple_blocks_created.rs @@ -9,10 +9,9 @@ use iroha_client::{ prelude::*, }, }; +use iroha_config::iroha::Configuration; use test_network::*; -use super::Configuration; - const N_BLOCKS: usize = 510; #[ignore = "Takes a lot of time."] diff --git a/client/tests/integration/multisignature_account.rs b/client/tests/integration/multisignature_account.rs index 515920dad3c..6a7650a7a34 100644 --- a/client/tests/integration/multisignature_account.rs +++ b/client/tests/integration/multisignature_account.rs @@ -6,10 +6,9 @@ use iroha_client::{ crypto::KeyPair, data_model::prelude::*, }; +use iroha_config::iroha::Configuration; use test_network::*; -use super::Configuration; - #[test] fn transaction_signed_by_new_signatory_of_account_should_pass() -> Result<()> { let (_rt, peer, client) = ::new().with_port(10_605).start_with_runtime(); diff --git a/client/tests/integration/multisignature_transaction.rs b/client/tests/integration/multisignature_transaction.rs index c3bbaf7a1be..ef88d276d9b 100644 --- a/client/tests/integration/multisignature_transaction.rs +++ b/client/tests/integration/multisignature_transaction.rs @@ -3,17 +3,16 @@ use std::{str::FromStr as _, thread, time::Duration}; use eyre::Result; use iroha_client::{ client::{self, Client, QueryResult}, + config::Configuration as ClientConfiguration, crypto::KeyPair, data_model::{ parameter::{default::MAX_TRANSACTIONS_IN_BLOCK, ParametersBuilder}, prelude::*, }, }; -use iroha_config::client::Configuration as ClientConfiguration; +use iroha_config::iroha::Configuration; use test_network::*; -use super::Configuration; - #[allow(clippy::too_many_lines)] #[test] fn multisignature_transactions_should_wait_for_all_signatures() -> Result<()> { diff --git a/client/tests/integration/restart_peer.rs b/client/tests/integration/restart_peer.rs index 1699ae9763e..2faa56e8815 100644 --- a/client/tests/integration/restart_peer.rs +++ b/client/tests/integration/restart_peer.rs @@ -5,13 +5,12 @@ use iroha_client::{ client::{self, QueryResult}, data_model::prelude::*, }; +use iroha_config::iroha::Configuration; use iroha_primitives::unique_vec; use tempfile::TempDir; use test_network::*; use tokio::runtime::Runtime; -use super::Configuration; - #[test] fn restarted_peer_should_have_the_same_asset_amount() -> Result<()> { let temp_dir = Arc::new(TempDir::new()?); diff --git a/client/tests/integration/tx_history.rs b/client/tests/integration/tx_history.rs index 8cbf9a3f5cc..4d26d32fe19 100644 --- a/client/tests/integration/tx_history.rs +++ b/client/tests/integration/tx_history.rs @@ -9,10 +9,9 @@ use iroha_client::{ client::{transaction, QueryResult}, data_model::{prelude::*, query::Pagination}, }; +use iroha_config::iroha::Configuration; use test_network::*; -use super::Configuration; - #[ignore = "ignore, more in #2851"] #[test] fn client_has_rejected_and_acepted_txs_should_return_tx_history() -> Result<()> { diff --git a/client/tests/integration/unregister_peer.rs b/client/tests/integration/unregister_peer.rs index e8246a38514..78ed01325ac 100644 --- a/client/tests/integration/unregister_peer.rs +++ b/client/tests/integration/unregister_peer.rs @@ -9,10 +9,9 @@ use iroha_client::{ prelude::*, }, }; +use iroha_config::iroha::Configuration; use test_network::*; -use super::Configuration; - // Note the test is marked as `unstable`, not the network. #[ignore = "ignore, more in #2851"] #[test] diff --git a/client/tests/integration/unstable_network.rs b/client/tests/integration/unstable_network.rs index d0cd9ce186b..cf711cfd9d4 100644 --- a/client/tests/integration/unstable_network.rs +++ b/client/tests/integration/unstable_network.rs @@ -5,12 +5,11 @@ use iroha_client::{ client::{self, Client, QueryResult}, data_model::{prelude::*, Level}, }; +use iroha_config::iroha::Configuration; use rand::seq::SliceRandom; use test_network::*; use tokio::runtime::Runtime; -use super::Configuration; - const MAX_TRANSACTIONS_IN_BLOCK: u32 = 5; #[test] diff --git a/client_cli/Cargo.toml b/client_cli/Cargo.toml index c5725467446..c05c17decb2 100644 --- a/client_cli/Cargo.toml +++ b/client_cli/Cargo.toml @@ -25,7 +25,6 @@ maintenance = { status = "actively-developed" } [dependencies] iroha_client = { workspace = true } iroha_primitives = { workspace = true } -iroha_config = { workspace = true } color-eyre = { workspace = true } # TODO: migrate to clap v4 (and use the workspace dependency) diff --git a/client_cli/src/main.rs b/client_cli/src/main.rs index 8aca7cef98c..83f380d63a8 100644 --- a/client_cli/src/main.rs +++ b/client_cli/src/main.rs @@ -16,9 +16,9 @@ use dialoguer::Confirm; use erased_serde::Serialize; use iroha_client::{ client::{Client, QueryResult}, + config::{path::Path as ConfigPath, Configuration as ClientConfiguration}, data_model::prelude::*, }; -use iroha_config::{client::Configuration as ClientConfiguration, path::Path as ConfigPath}; use iroha_primitives::addr::SocketAddr; /// Metadata wrapper, which can be captured from cli arguments (from user supplied file). From 4765612e7382f71c1207e0fc1809468eb36c6f88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marin=20Ver=C5=A1i=C4=87?= Date: Thu, 14 Dec 2023 14:58:24 +0300 Subject: [PATCH 10/21] [fix] #4140: Fix registration of new peer (#4142) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Marin Veršić --- client/benches/tps/utils.rs | 2 +- client/tests/integration/asset_propagation.rs | 2 +- client/tests/integration/config.rs | 2 +- client/tests/integration/connected_peers.rs | 123 +++++++++++++----- client/tests/integration/events/pipeline.rs | 2 +- .../integration/multiple_blocks_created.rs | 2 +- .../integration/multisignature_transaction.rs | 2 +- client/tests/integration/offline_peers.rs | 52 ++++++-- client/tests/integration/restart_peer.rs | 74 ++++++----- .../integration/triggers/trigger_rollback.rs | 6 +- client/tests/integration/unregister_peer.rs | 2 +- client/tests/integration/unstable_network.rs | 2 +- config/src/iroha.rs | 7 +- config/src/sumeragi.rs | 13 +- core/src/block.rs | 37 +++--- core/src/gossiper.rs | 1 - core/src/kiso.rs | 4 +- core/src/sumeragi/main_loop.rs | 23 ++-- core/src/sumeragi/mod.rs | 53 ++++---- core/src/wsv.rs | 6 - core/test_network/src/lib.rs | 29 +++-- docker-compose.dev.local.yml | 8 +- docker-compose.dev.single.yml | 1 - docker-compose.dev.yml | 8 +- docker-compose.single.yml | 1 + docker-compose.stable.single.yml | 3 +- docker-compose.stable.yml | 16 +-- docker-compose.yml | 4 + p2p/src/network.rs | 2 +- .../executor/derive/src/validate.rs | 2 +- smart_contract/src/lib.rs | 2 +- telemetry/src/metrics.rs | 8 +- tools/swarm/src/compose.rs | 33 +++-- 33 files changed, 326 insertions(+), 206 deletions(-) diff --git a/client/benches/tps/utils.rs b/client/benches/tps/utils.rs index 045a95527fe..c1a3494260f 100644 --- a/client/benches/tps/utils.rs +++ b/client/benches/tps/utils.rs @@ -56,7 +56,7 @@ impl Config { pub fn measure(self) -> Result { // READY - let (_rt, network, client) = ::start_test_with_runtime(self.peers, None); + let (_rt, network, client) = Network::start_test_with_runtime(self.peers, None); let clients = network.clients(); wait_for_genesis_committed(&clients, 0); diff --git a/client/tests/integration/asset_propagation.rs b/client/tests/integration/asset_propagation.rs index cb88cbef3e6..d248d160f86 100644 --- a/client/tests/integration/asset_propagation.rs +++ b/client/tests/integration/asset_propagation.rs @@ -16,7 +16,7 @@ use test_network::*; fn client_add_asset_quantity_to_existing_asset_should_increase_asset_amount_on_another_peer( ) -> Result<()> { // Given - let (_rt, network, client) = ::start_test_with_runtime(4, Some(10_450)); + 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(); diff --git a/client/tests/integration/config.rs b/client/tests/integration/config.rs index 54c7f4f596b..1c71aba683d 100644 --- a/client/tests/integration/config.rs +++ b/client/tests/integration/config.rs @@ -1,4 +1,4 @@ -use iroha_data_model::Level; +use iroha_client::data_model::Level; use test_network::*; #[test] diff --git a/client/tests/integration/connected_peers.rs b/client/tests/integration/connected_peers.rs index 6d80fe530cf..9d9634316d1 100644 --- a/client/tests/integration/connected_peers.rs +++ b/client/tests/integration/connected_peers.rs @@ -4,13 +4,15 @@ use eyre::{Context, Result}; use iroha_client::{ client::Client, data_model::{ - parameter::{default::MAX_TRANSACTIONS_IN_BLOCK, ParametersBuilder}, + isi::{Register, Unregister}, peer::Peer as DataModelPeer, - prelude::*, }, }; use iroha_config::iroha::Configuration; +use iroha_primitives::unique_vec; +use rand::{seq::SliceRandom, thread_rng, Rng}; use test_network::*; +use tokio::runtime::Runtime; #[ignore = "ignore, more in #2851"] #[test] @@ -23,11 +25,50 @@ fn connected_peers_with_f_1_0_1() -> Result<()> { connected_peers_with_f(1, Some(11_000)) } +#[test] +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 mut peer_clients: Vec<_> = Network::peers(&network) + .zip(Network::clients(&network)) + .collect(); + + check_status(&peer_clients, 1); + + // Start new peer + let mut configuration = Configuration::test(); + configuration.sumeragi.trusted_peers.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_into_genesis(WithGenesis::None) + .with_port(11_200) + .start(), + ); + + let register_peer = Register::peer(DataModelPeer::new(new_peer.id.clone())); + peer_clients + .choose(&mut thread_rng()) + .unwrap() + .1 + .submit_blocking(register_peer)?; + peer_clients.push((&new_peer, Client::test(&new_peer.api_address))); + thread::sleep(pipeline_time * 2); // Wait for some time to allow peers to connect + + check_status(&peer_clients, 2); + + Ok(()) +} + /// Test the number of connected peers, changing the number of faults tolerated down and up fn connected_peers_with_f(faults: u64, start_port: Option) -> Result<()> { let n_peers = 3 * faults + 1; - let (_rt, network, client) = ::start_test_with_runtime( + let (_rt, network, _) = Network::start_test_with_runtime( (n_peers) .try_into() .wrap_err("`faults` argument `u64` value too high, cannot convert to `u32`")?, @@ -36,40 +77,52 @@ fn connected_peers_with_f(faults: u64, start_port: Option) -> Result<()> { wait_for_genesis_committed(&network.clients(), 0); let pipeline_time = Configuration::pipeline_time(); - client.submit_all_blocking( - ParametersBuilder::new() - .add_parameter(MAX_TRANSACTIONS_IN_BLOCK, 1u32)? - .into_set_parameters(), - )?; - - // Confirm all peers connected - let mut status = client.get_status()?; - assert_eq!(status.peers, n_peers - 1); - assert_eq!(status.blocks, 2); - - // Unregister a peer: committed with f = `faults` - // then `status.peers` decrements - let peer = network.peers.values().last().unwrap(); - let peer_client = Client::test(&peer.api_address); - let unregister_peer = Unregister::peer(peer.id.clone()); - client.submit_blocking(unregister_peer)?; + let mut peer_clients: Vec<_> = Network::peers(&network) + .zip(Network::clients(&network)) + .collect(); + + check_status(&peer_clients, 1); + + // Unregister a peer: committed with f = `faults` then `status.peers` decrements + let removed_peer_idx = rand::thread_rng().gen_range(0..peer_clients.len()); + let (removed_peer, _) = &peer_clients[removed_peer_idx]; + let unregister_peer = Unregister::peer(removed_peer.id.clone()); + peer_clients + .choose(&mut thread_rng()) + .unwrap() + .1 + .submit_blocking(unregister_peer)?; thread::sleep(pipeline_time * 2); // Wait for some time to allow peers to connect - status = client.get_status()?; - assert_eq!(status.peers, n_peers - 2); - assert_eq!(status.blocks, 3); - status = peer_client.get_status()?; + let (removed_peer, removed_peer_client) = peer_clients.remove(removed_peer_idx); + + check_status(&peer_clients, 2); + let status = removed_peer_client.get_status()?; + // Peer might have been disconnected before getting the block + assert!(status.blocks == 1 || status.blocks == 2); assert_eq!(status.peers, 0); - // Re-register the peer: committed with f = `faults` - 1 then - // `status.peers` increments - let register_peer = Register::peer(DataModelPeer::new(peer.id.clone())); - client.submit_blocking(register_peer)?; - thread::sleep(pipeline_time * 4); // Wait for some time to allow peers to connect - status = client.get_status()?; - assert_eq!(status.peers, n_peers - 1); - assert_eq!(status.blocks, 4); - status = peer_client.get_status()?; - assert_eq!(status.peers, n_peers - 1); - assert_eq!(status.blocks, 4); + // Re-register the peer: committed with f = `faults` - 1 then `status.peers` increments + let register_peer = Register::peer(DataModelPeer::new(removed_peer.id.clone())); + peer_clients + .choose(&mut thread_rng()) + .unwrap() + .1 + .submit_blocking(register_peer)?; + peer_clients.insert(removed_peer_idx, (removed_peer, removed_peer_client)); + thread::sleep(pipeline_time * 2); // Wait for some time to allow peers to connect + + check_status(&peer_clients, 3); + Ok(()) } + +fn check_status(peer_clients: &[(&Peer, Client)], expected_blocks: u64) { + let n_peers = peer_clients.len() as u64; + + for (_, peer_client) in peer_clients { + let status = peer_client.get_status().unwrap(); + + assert_eq!(status.peers, n_peers - 1); + assert_eq!(status.blocks, expected_blocks); + } +} diff --git a/client/tests/integration/events/pipeline.rs b/client/tests/integration/events/pipeline.rs index ec4579bcbe4..77d99dd1b57 100644 --- a/client/tests/integration/events/pipeline.rs +++ b/client/tests/integration/events/pipeline.rs @@ -40,7 +40,7 @@ fn test_with_instruction_and_status_and_port( port: u16, ) -> Result<()> { let (_rt, network, client) = - ::start_test_with_runtime(PEER_COUNT.try_into().unwrap(), Some(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(); diff --git a/client/tests/integration/multiple_blocks_created.rs b/client/tests/integration/multiple_blocks_created.rs index a674bb357da..bac51f52cd7 100644 --- a/client/tests/integration/multiple_blocks_created.rs +++ b/client/tests/integration/multiple_blocks_created.rs @@ -18,7 +18,7 @@ const N_BLOCKS: usize = 510; #[test] fn long_multiple_blocks_created() -> Result<()> { // Given - let (_rt, network, client) = ::start_test_with_runtime(4, Some(10_965)); + 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(); diff --git a/client/tests/integration/multisignature_transaction.rs b/client/tests/integration/multisignature_transaction.rs index ef88d276d9b..4cf5739788b 100644 --- a/client/tests/integration/multisignature_transaction.rs +++ b/client/tests/integration/multisignature_transaction.rs @@ -16,7 +16,7 @@ use test_network::*; #[allow(clippy::too_many_lines)] #[test] fn multisignature_transactions_should_wait_for_all_signatures() -> Result<()> { - let (_rt, network, client) = ::start_test_with_runtime(4, Some(10_945)); + 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(); diff --git a/client/tests/integration/offline_peers.rs b/client/tests/integration/offline_peers.rs index 193ff34afaf..fc14502caa3 100644 --- a/client/tests/integration/offline_peers.rs +++ b/client/tests/integration/offline_peers.rs @@ -1,11 +1,13 @@ use eyre::Result; use iroha_client::{ - client::{self, QueryResult}, + client::{self, Client, QueryResult}, data_model::{ - parameter::{default::MAX_TRANSACTIONS_IN_BLOCK, ParametersBuilder}, + peer::{Peer as DataModelPeer, PeerId}, prelude::*, }, }; +use iroha_config::iroha::Configuration; +use iroha_crypto::KeyPair; use test_network::*; use tokio::runtime::Runtime; @@ -14,19 +16,13 @@ fn genesis_block_is_committed_with_some_offline_peers() -> Result<()> { // Given let rt = Runtime::test(); - let (network, client) = rt.block_on(::start_test_with_offline_and_set_n_shifts( + let (network, client) = rt.block_on(Network::start_test_with_offline_and_set_n_shifts( 4, 1, Some(10_560), )); wait_for_genesis_committed(&network.clients(), 1); - client.submit_all_blocking( - ParametersBuilder::new() - .add_parameter(MAX_TRANSACTIONS_IN_BLOCK, 1u32)? - .into_set_parameters(), - )?; - //When let alice_id: AccountId = "alice@wonderland".parse()?; let roses = "rose#wonderland".parse()?; @@ -43,3 +39,41 @@ fn genesis_block_is_committed_with_some_offline_peers() -> Result<()> { assert_eq!(AssetValue::Quantity(alice_has_roses), *asset.value()); Ok(()) } + +#[test] +fn register_offline_peer() -> Result<()> { + let n_peers = 4; + + 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 peer_clients = Network::clients(&network); + + check_status(&peer_clients, 1); + + let address = "128.0.0.2:8085".parse()?; + let key_pair = KeyPair::generate().unwrap(); + let public_key = key_pair.public_key().clone(); + let peer_id = PeerId::new(&address, &public_key); + let register_peer = Register::peer(DataModelPeer::new(peer_id)); + + // Wait for some time to allow peers to connect + client.submit_blocking(register_peer)?; + std::thread::sleep(pipeline_time * 2); + + // Make sure status hasn't change + check_status(&peer_clients, 2); + + Ok(()) +} + +fn check_status(peer_clients: &[Client], expected_blocks: u64) { + let n_peers = peer_clients.len() as u64; + + for peer_client in peer_clients { + let status = peer_client.get_status().unwrap(); + + assert_eq!(status.peers, n_peers - 1); + assert_eq!(status.blocks, expected_blocks); + } +} diff --git a/client/tests/integration/restart_peer.rs b/client/tests/integration/restart_peer.rs index 2faa56e8815..8249edee078 100644 --- a/client/tests/integration/restart_peer.rs +++ b/client/tests/integration/restart_peer.rs @@ -1,50 +1,51 @@ -use std::{str::FromStr, sync::Arc}; +use std::{str::FromStr, thread}; use eyre::Result; use iroha_client::{ - client::{self, QueryResult}, + client::{self, Client, QueryResult}, data_model::prelude::*, }; use iroha_config::iroha::Configuration; -use iroha_primitives::unique_vec; -use tempfile::TempDir; +use rand::{seq::SliceRandom, thread_rng, Rng}; use test_network::*; use tokio::runtime::Runtime; #[test] fn restarted_peer_should_have_the_same_asset_amount() -> Result<()> { - let temp_dir = Arc::new(TempDir::new()?); - - let mut configuration = Configuration::test(); - let mut peer = ::new().with_port(10_000).build()?; - configuration.sumeragi.trusted_peers.peers = unique_vec![peer.id.clone()]; - let account_id = AccountId::from_str("alice@wonderland").unwrap(); let asset_definition_id = AssetDefinitionId::from_str("xor#wonderland").unwrap(); - let create_asset = - Register::asset_definition(AssetDefinition::quantity(asset_definition_id.clone())); let quantity: u32 = 200; - let iroha_client = client::Client::test(&peer.api_address); + let mut removed_peer = { + let n_peers = 4; - { - let rt = Runtime::test(); - rt.block_on( - PeerBuilder::new() - .with_configuration(configuration.clone()) - .with_dir(temp_dir.clone()) - .start_with_peer(&mut peer), - ); - wait_for_genesis_committed(&vec![iroha_client.clone()], 0); + let (_rt, network, _) = Network::start_test_with_runtime(n_peers, Some(11_200)); + wait_for_genesis_committed(&network.clients(), 0); + let pipeline_time = Configuration::pipeline_time(); + let peer_clients = Network::clients(&network); + + let create_asset = + Register::asset_definition(AssetDefinition::quantity(asset_definition_id.clone())); + peer_clients + .choose(&mut thread_rng()) + .unwrap() + .submit_blocking(create_asset)?; - iroha_client.submit_blocking(create_asset)?; let mint_asset = Mint::asset_quantity( quantity, AssetId::new(asset_definition_id.clone(), account_id.clone()), ); - iroha_client.submit_blocking(mint_asset)?; + peer_clients + .choose(&mut thread_rng()) + .unwrap() + .submit_blocking(mint_asset)?; + + // Wait for observing peer to get the block + thread::sleep(pipeline_time); - let assets = iroha_client + let assets = peer_clients + .choose(&mut thread_rng()) + .unwrap() .request(client::asset::by_account_id(account_id.clone()))? .collect::>>()?; let asset = assets @@ -52,20 +53,29 @@ fn restarted_peer_should_have_the_same_asset_amount() -> Result<()> { .find(|asset| asset.id().definition_id == asset_definition_id) .expect("Asset not found"); assert_eq!(AssetValue::Quantity(quantity), *asset.value()); - peer.stop(); - } + let mut all_peers: Vec<_> = core::iter::once(network.genesis) + .chain(network.peers.into_values()) + .collect(); + let removed_peer_idx = rand::thread_rng().gen_range(0..all_peers.len()); + let mut removed_peer = all_peers.swap_remove(removed_peer_idx); + removed_peer.stop(); + removed_peer + }; + // All peers have been stopped here + + // Restart just one peer and check if it updates itself from the blockstore { let rt = Runtime::test(); rt.block_on( PeerBuilder::new() - .with_configuration(configuration) - .with_dir(temp_dir) - .start_with_peer(&mut peer), + .with_dir(removed_peer.temp_dir.as_ref().unwrap().clone()) + .start_with_peer(&mut removed_peer), ); - wait_for_genesis_committed(&vec![iroha_client.clone()], 0); + let removed_peer_client = Client::test(&removed_peer.api_address); + wait_for_genesis_committed(&vec![removed_peer_client.clone()], 0); - iroha_client.poll_request(client::asset::by_account_id(account_id), |result| { + removed_peer_client.poll_request(client::asset::by_account_id(account_id), |result| { let assets = result.collect::>>().expect("Valid"); iroha_logger::error!(?assets); diff --git a/client/tests/integration/triggers/trigger_rollback.rs b/client/tests/integration/triggers/trigger_rollback.rs index 182045a2c7e..3565301e26f 100644 --- a/client/tests/integration/triggers/trigger_rollback.rs +++ b/client/tests/integration/triggers/trigger_rollback.rs @@ -1,8 +1,10 @@ use std::str::FromStr as _; use eyre::Result; -use iroha_client::client::QueryResult; -use iroha_data_model::{prelude::*, query::asset::FindAllAssetsDefinitions, trigger::TriggerId}; +use iroha_client::{ + client::QueryResult, + data_model::{prelude::*, query::asset::FindAllAssetsDefinitions, trigger::TriggerId}, +}; use test_network::*; #[test] diff --git a/client/tests/integration/unregister_peer.rs b/client/tests/integration/unregister_peer.rs index 78ed01325ac..3121bf1de76 100644 --- a/client/tests/integration/unregister_peer.rs +++ b/client/tests/integration/unregister_peer.rs @@ -99,7 +99,7 @@ fn init() -> Result<( AccountId, AssetDefinitionId, )> { - let (rt, network, client) = ::start_test_with_runtime(4, Some(10_925)); + let (rt, network, client) = Network::start_test_with_runtime(4, Some(10_925)); let pipeline_time = Configuration::pipeline_time(); iroha_logger::info!("Started"); let parameters = ParametersBuilder::new() diff --git a/client/tests/integration/unstable_network.rs b/client/tests/integration/unstable_network.rs index cf711cfd9d4..84d1b2d9762 100644 --- a/client/tests/integration/unstable_network.rs +++ b/client/tests/integration/unstable_network.rs @@ -59,7 +59,7 @@ fn unstable_network( { configuration.sumeragi.debug_force_soft_fork = force_soft_fork; } - let network = ::new_with_offline_peers( + let network = Network::new_with_offline_peers( Some(configuration), n_peers + n_offline_peers, 0, diff --git a/config/src/iroha.rs b/config/src/iroha.rs index 6fc69f5069b..ffa28eddc2e 100644 --- a/config/src/iroha.rs +++ b/config/src/iroha.rs @@ -144,11 +144,8 @@ impl ConfigurationProxy { message: "Torii config should have at least `p2p_addr` provided for sumeragi finalisation", }); } - // Finally, if trusted peers were not supplied, we can fall back to inserting itself as - // the only trusted one - if sumeragi_proxy.trusted_peers.is_none() { - sumeragi_proxy.insert_self_as_trusted_peers() - } + + sumeragi_proxy.insert_self_as_trusted_peers() } Ok(()) diff --git a/config/src/sumeragi.rs b/config/src/sumeragi.rs index 18698d634c6..a4eb7760069 100644 --- a/config/src/sumeragi.rs +++ b/config/src/sumeragi.rs @@ -94,11 +94,16 @@ impl ConfigurationProxy { pub fn insert_self_as_trusted_peers(&mut self) { let peer_id = self .peer_id - .clone() + .as_ref() .expect("Insertion of `self` as `trusted_peers` implies that `peer_id` field should be initialized"); - self.trusted_peers = Some(TrustedPeers { - peers: unique_vec![peer_id], - }); + 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()], + }) + }; } } diff --git a/core/src/block.rs b/core/src/block.rs index 21a7405ef42..164dc6b5456 100644 --- a/core/src/block.rs +++ b/core/src/block.rs @@ -267,27 +267,28 @@ mod valid { topology: &Topology, wsv: &mut WorldStateView, ) -> Result { - let actual_commit_topology = &block.payload().commit_topology; - let expected_commit_topology = &topology.ordered_peers; - - if actual_commit_topology != expected_commit_topology { - let actual_commit_topology = actual_commit_topology.clone(); - - return Err(( - block, - BlockValidationError::TopologyMismatch { - expected: expected_commit_topology.clone(), - actual: actual_commit_topology, - }, - )); - } + if !block.payload().header.is_genesis() { + let actual_commit_topology = &block.payload().commit_topology; + let expected_commit_topology = &topology.ordered_peers; + + if actual_commit_topology != expected_commit_topology { + let actual_commit_topology = actual_commit_topology.clone(); + + return Err(( + block, + BlockValidationError::TopologyMismatch { + expected: expected_commit_topology.clone(), + actual: actual_commit_topology, + }, + )); + } - if !block.payload().header.is_genesis() - && topology + if topology .filter_signatures_by_roles(&[Role::Leader], block.signatures()) .is_empty() - { - return Err((block, SignatureVerificationError::LeaderMissing.into())); + { + return Err((block, SignatureVerificationError::LeaderMissing.into())); + } } let expected_block_height = wsv.height() + 1; diff --git a/core/src/gossiper.rs b/core/src/gossiper.rs index 5856dfd4a1b..365ebb7ac7a 100644 --- a/core/src/gossiper.rs +++ b/core/src/gossiper.rs @@ -100,7 +100,6 @@ impl TransactionGossiper { .n_random_transactions(self.gossip_batch_size, &self.wsv); if txs.is_empty() { - iroha_logger::debug!("Nothing to gossip"); return; } diff --git a/core/src/kiso.rs b/core/src/kiso.rs index 464b01acb0e..cb6d98bf05b 100644 --- a/core/src/kiso.rs +++ b/core/src/kiso.rs @@ -117,11 +117,11 @@ struct Actor { impl Actor { async fn run(&mut self) { while let Some(msg) = self.handle.recv().await { - self.handle_message(msg).await + self.handle_message(msg) } } - async fn handle_message(&mut self, msg: Message) { + fn handle_message(&mut self, msg: Message) { match msg { Message::GetDTO { respond_to } => { let dto = ConfigurationDTO::from(&self.state); diff --git a/core/src/sumeragi/main_loop.rs b/core/src/sumeragi/main_loop.rs index b24b263188a..33e5e41515f 100644 --- a/core/src/sumeragi/main_loop.rs +++ b/core/src/sumeragi/main_loop.rs @@ -186,7 +186,7 @@ impl Sumeragi { &mut self, shutdown_receiver: &mut tokio::sync::oneshot::Receiver<()>, ) -> Result<(), EarlyReturn> { - trace!("Listen for genesis"); + info!(addr = %self.peer_id.address, "Listen for genesis"); loop { std::thread::sleep(Duration::from_millis(50)); @@ -223,6 +223,8 @@ impl Sumeragi { } }; + new_wsv.world_mut().trusted_peers_ids = + block.payload().commit_topology.clone(); self.commit_block(block, new_wsv); return Err(EarlyReturn::GenesisBlockReceivedAndCommitted); } @@ -295,7 +297,7 @@ impl Sumeragi { info!( addr=%self.peer_id.address, role=%self.current_topology.role(&self.peer_id), - block_height=%self.wsv.height(), + block_height=%block.payload().header.height, block_hash=%block.hash(), "{}", Strategy::LOG_MESSAGE, ); @@ -313,11 +315,8 @@ impl Sumeragi { // Parameters are updated before updating public copy of sumeragi self.update_params(); - let new_topology = Topology::recreate_topology( - block.as_ref(), - 0, - self.wsv.peers_ids().iter().cloned().collect(), - ); + let new_topology = + Topology::recreate_topology(block.as_ref(), 0, self.wsv.peers().cloned().collect()); let events = block.produce_events(); // https://github.com/hyperledger/iroha/issues/3396 @@ -801,10 +800,10 @@ pub(crate) fn run( }; span.exit(); - trace!( - me=%sumeragi.peer_id.public_key, + info!( + addr=%sumeragi.peer_id.address, role_in_next_round=%sumeragi.current_topology.role(&sumeragi.peer_id), - "Finished sumeragi init.", + "Sumeragi initialized", ); let mut voting_block = None; @@ -1125,7 +1124,7 @@ fn handle_block_sync( let last_committed_block = new_wsv .latest_block_ref() .expect("Not in genesis round so must have at least genesis block"); - let new_peers = new_wsv.peers_ids().clone(); + let new_peers = new_wsv.peers().cloned().collect(); let view_change_index = block.payload().header().view_change_index; Topology::recreate_topology(&last_committed_block, view_change_index, new_peers) }; @@ -1145,7 +1144,7 @@ fn handle_block_sync( let last_committed_block = new_wsv .latest_block_ref() .expect("Not in genesis round so must have at least genesis block"); - let new_peers = new_wsv.peers_ids().clone(); + let new_peers = new_wsv.peers().cloned().collect(); let view_change_index = block.payload().header().view_change_index; Topology::recreate_topology(&last_committed_block, view_change_index, new_peers) }; diff --git a/core/src/sumeragi/mod.rs b/core/src/sumeragi/mod.rs index 7066d5e9e05..8c82663ee6c 100644 --- a/core/src/sumeragi/mod.rs +++ b/core/src/sumeragi/mod.rs @@ -10,7 +10,7 @@ use std::{ use eyre::{Result, WrapErr as _}; use iroha_config::sumeragi::Configuration; use iroha_crypto::{KeyPair, SignatureOf}; -use iroha_data_model::prelude::*; +use iroha_data_model::{block::SignedBlock, prelude::*}; use iroha_genesis::GenesisNetwork; use iroha_logger::prelude::*; use iroha_telemetry::metrics::Metrics; @@ -226,6 +226,28 @@ impl SumeragiHandle { } } + fn replay_block( + block: &SignedBlock, + wsv: &mut WorldStateView, + current_topology: &Topology, + ) -> Topology { + let block = ValidBlock::validate(block.clone(), current_topology, wsv) + .expect("Kura blocks should be valid") + .commit(current_topology) + .expect("Kura blocks should be valid"); + + if block.payload().header.is_genesis() { + wsv.world_mut().trusted_peers_ids = block.payload().commit_topology.clone(); + } + + wsv.apply_without_execution(&block).expect( + "Block application in init should not fail. \ + Blocks loaded from kura assumed to be valid", + ); + + Topology::recreate_topology(block.as_ref(), 0, wsv.peers().cloned().collect()) + } + /// Start [`Sumeragi`] actor and return handle to it. /// /// # Panics @@ -254,7 +276,7 @@ impl SumeragiHandle { ) }); - let current_topology = match wsv.height() { + let mut current_topology = match wsv.height() { 0 => { assert!(!configuration.trusted_peers.peers.is_empty()); Topology::new(configuration.trusted_peers.peers.clone()) @@ -264,40 +286,21 @@ impl SumeragiHandle { "Sumeragi could not load block that was reported as present. \ Please check that the block storage was not disconnected.", ); - Topology::recreate_topology( - &block_ref, - 0, - wsv.peers_ids().iter().cloned().collect(), - ) + Topology::recreate_topology(&block_ref, 0, wsv.peers().cloned().collect()) } }; let block_iter_except_last = (&mut blocks_iter).take(block_count.saturating_sub(skip_block_count + 1)); for block in block_iter_except_last { - let block = ValidBlock::validate(Clone::clone(&block), ¤t_topology, &mut wsv) - .expect("Kura blocks should be valid") - .commit(¤t_topology) - .expect("Kura blocks should be valid"); - wsv.apply_without_execution(&block).expect( - "Block application in init should not fail. \ - Blocks loaded from kura assumed to be valid", - ); + current_topology = Self::replay_block(&block, &mut wsv, ¤t_topology); } // finalized_wsv is one block behind let finalized_wsv = wsv.clone(); - if let Some(latest_block) = blocks_iter.next() { - let latest_block = - ValidBlock::validate(Clone::clone(&latest_block), ¤t_topology, &mut wsv) - .expect("Kura blocks should be valid") - .commit(¤t_topology) - .expect("Kura blocks should be valid"); - wsv.apply_without_execution(&latest_block).expect( - "Block application in init should not fail. \ - Blocks loaded from kura assumed to be valid", - ); + if let Some(block) = blocks_iter.next() { + current_topology = Self::replay_block(&block, &mut wsv, ¤t_topology); } info!("Sumeragi has finished loading blocks and setting up the WSV"); diff --git a/core/src/wsv.rs b/core/src/wsv.rs index 01c4e8e8d30..0e653e6c488 100644 --- a/core/src/wsv.rs +++ b/core/src/wsv.rs @@ -855,12 +855,6 @@ impl WorldStateView { &mut self.world } - /// Returns reference for trusted peer ids - #[inline] - pub fn peers_ids(&self) -> &PeersIds { - &self.world.trusted_peers_ids - } - /// Return an iterator over blockchain block hashes starting with the block of the given `height` pub fn block_hashes_from_height(&self, height: usize) -> Vec> { self.block_hashes diff --git a/core/test_network/src/lib.rs b/core/test_network/src/lib.rs index b51e1726e47..bf61826e05c 100644 --- a/core/test_network/src/lib.rs +++ b/core/test_network/src/lib.rs @@ -26,7 +26,7 @@ use iroha_primitives::{ unique_vec, unique_vec::UniqueVec, }; -use rand::seq::IteratorRandom; +use rand::{seq::IteratorRandom, thread_rng}; use serde_json::json; use tempfile::TempDir; use tokio::{ @@ -179,7 +179,12 @@ impl Network { ) .await .expect("Failed to init peers"); - let client = Client::test(&network.genesis.api_address); + let client = Client::test( + &Network::peers(&network) + .choose(&mut thread_rng()) + .unwrap() + .api_address, + ); (network, client) } @@ -197,7 +202,12 @@ impl Network { /// Adds peer to network and waits for it to start block /// synchronization. pub async fn add_peer(&self) -> (Peer, Client) { - let genesis_client = Client::test(&self.genesis.api_address); + let client = Client::test( + &Network::peers(self) + .choose(&mut thread_rng()) + .unwrap() + .api_address, + ); let mut config = Configuration::test(); config.sumeragi.trusted_peers.peers = @@ -212,13 +222,10 @@ impl Network { time::sleep(Configuration::pipeline_time() + Configuration::block_sync_gossip_time()).await; let add_peer = Register::peer(DataModelPeer::new(peer.id.clone())); - genesis_client - .submit(add_peer) - .expect("Failed to add new peer."); - - let client = Client::test(&peer.api_address); + client.submit(add_peer).expect("Failed to add new peer."); - (peer, client) + let peer_client = Client::test(&peer.api_address); + (peer, peer_client) } /// Creates new network with some offline peers @@ -359,7 +366,7 @@ pub struct Peer { pub iroha: Option, /// Temporary directory // Note: last field to be dropped after Iroha (struct fields drops in FIFO RFC 1857) - temp_dir: Option>, + pub temp_dir: Option>, } impl From for Box { @@ -556,7 +563,7 @@ impl PeerBuilder { /// Set Iroha configuration #[must_use] pub fn with_configuration(mut self, configuration: Configuration) -> Self { - self.configuration.replace(configuration); + self.configuration = Some(configuration); self } diff --git a/docker-compose.dev.local.yml b/docker-compose.dev.local.yml index 162572d7dd7..a1d0855dc04 100644 --- a/docker-compose.dev.local.yml +++ b/docker-compose.dev.local.yml @@ -13,7 +13,7 @@ services: TORII_API_URL: iroha0:8080 IROHA_GENESIS_ACCOUNT_PUBLIC_KEY: ed01204164BF554923ECE1FD412D241036D863A6AE430476C898248B8237D77534CFC4 IROHA_GENESIS_ACCOUNT_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"82b3bde54aebeca4146257da0de8d59d8e46d5fe34887dcd8072866792fcb3ad4164bf554923ece1fd412d241036d863a6ae430476c898248b8237d77534cfc4"}' - SUMERAGI_TRUSTED_PEERS: '[{"address":"iroha1:1338","public_key":"ed0120815BBDC9775D28C3633269B25F22D048E2AA2E36017CBE5AD85F15220BEB6F6F"},{"address":"iroha0:1337","public_key":"ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB"},{"address":"iroha3:1340","public_key":"ed0120A66522370D60B9C09E79ADE2E9BB1EF2E78733A944B999B3A6AEE687CE476D61"},{"address":"iroha2:1339","public_key":"ed0120F417E0371E6ADB32FD66749477402B1AB67F84A8E9B082E997980CC91F327736"}]' + 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 @@ -30,7 +30,7 @@ services: TORII_P2P_ADDR: iroha1:1338 TORII_API_URL: iroha1:8081 IROHA_GENESIS_ACCOUNT_PUBLIC_KEY: ed01204164BF554923ECE1FD412D241036D863A6AE430476C898248B8237D77534CFC4 - SUMERAGI_TRUSTED_PEERS: '[{"address":"iroha1:1338","public_key":"ed0120815BBDC9775D28C3633269B25F22D048E2AA2E36017CBE5AD85F15220BEB6F6F"},{"address":"iroha0:1337","public_key":"ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB"},{"address":"iroha3:1340","public_key":"ed0120A66522370D60B9C09E79ADE2E9BB1EF2E78733A944B999B3A6AEE687CE476D61"},{"address":"iroha2:1339","public_key":"ed0120F417E0371E6ADB32FD66749477402B1AB67F84A8E9B082E997980CC91F327736"}]' + 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 @@ -46,7 +46,7 @@ services: TORII_P2P_ADDR: iroha2:1339 TORII_API_URL: iroha2:8082 IROHA_GENESIS_ACCOUNT_PUBLIC_KEY: ed01204164BF554923ECE1FD412D241036D863A6AE430476C898248B8237D77534CFC4 - SUMERAGI_TRUSTED_PEERS: '[{"address":"iroha1:1338","public_key":"ed0120815BBDC9775D28C3633269B25F22D048E2AA2E36017CBE5AD85F15220BEB6F6F"},{"address":"iroha0:1337","public_key":"ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB"},{"address":"iroha3:1340","public_key":"ed0120A66522370D60B9C09E79ADE2E9BB1EF2E78733A944B999B3A6AEE687CE476D61"},{"address":"iroha2:1339","public_key":"ed0120F417E0371E6ADB32FD66749477402B1AB67F84A8E9B082E997980CC91F327736"}]' + 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 @@ -62,7 +62,7 @@ services: TORII_P2P_ADDR: iroha3:1340 TORII_API_URL: iroha3:8083 IROHA_GENESIS_ACCOUNT_PUBLIC_KEY: ed01204164BF554923ECE1FD412D241036D863A6AE430476C898248B8237D77534CFC4 - SUMERAGI_TRUSTED_PEERS: '[{"address":"iroha1:1338","public_key":"ed0120815BBDC9775D28C3633269B25F22D048E2AA2E36017CBE5AD85F15220BEB6F6F"},{"address":"iroha0:1337","public_key":"ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB"},{"address":"iroha3:1340","public_key":"ed0120A66522370D60B9C09E79ADE2E9BB1EF2E78733A944B999B3A6AEE687CE476D61"},{"address":"iroha2:1339","public_key":"ed0120F417E0371E6ADB32FD66749477402B1AB67F84A8E9B082E997980CC91F327736"}]' + 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 diff --git a/docker-compose.dev.single.yml b/docker-compose.dev.single.yml index 9a4891ff227..b817eece0c8 100644 --- a/docker-compose.dev.single.yml +++ b/docker-compose.dev.single.yml @@ -13,7 +13,6 @@ services: TORII_API_URL: iroha0:8080 IROHA_GENESIS_ACCOUNT_PUBLIC_KEY: ed01204164BF554923ECE1FD412D241036D863A6AE430476C898248B8237D77534CFC4 IROHA_GENESIS_ACCOUNT_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"82b3bde54aebeca4146257da0de8d59d8e46d5fe34887dcd8072866792fcb3ad4164bf554923ece1fd412d241036d863a6ae430476c898248b8237d77534cfc4"}' - SUMERAGI_TRUSTED_PEERS: '[{"address":"iroha0:1337","public_key":"ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB"}]' ports: - 1337:1337 - 8080:8080 diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 9c45d746a06..54404425d99 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -13,7 +13,7 @@ services: TORII_API_URL: iroha0:8080 IROHA_GENESIS_ACCOUNT_PUBLIC_KEY: ed01204164BF554923ECE1FD412D241036D863A6AE430476C898248B8237D77534CFC4 IROHA_GENESIS_ACCOUNT_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"82b3bde54aebeca4146257da0de8d59d8e46d5fe34887dcd8072866792fcb3ad4164bf554923ece1fd412d241036d863a6ae430476c898248b8237d77534cfc4"}' - SUMERAGI_TRUSTED_PEERS: '[{"address":"iroha1:1338","public_key":"ed0120815BBDC9775D28C3633269B25F22D048E2AA2E36017CBE5AD85F15220BEB6F6F"},{"address":"iroha0:1337","public_key":"ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB"},{"address":"iroha3:1340","public_key":"ed0120A66522370D60B9C09E79ADE2E9BB1EF2E78733A944B999B3A6AEE687CE476D61"},{"address":"iroha2:1339","public_key":"ed0120F417E0371E6ADB32FD66749477402B1AB67F84A8E9B082E997980CC91F327736"}]' + 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 @@ -30,7 +30,7 @@ services: TORII_P2P_ADDR: iroha1:1338 TORII_API_URL: iroha1:8081 IROHA_GENESIS_ACCOUNT_PUBLIC_KEY: ed01204164BF554923ECE1FD412D241036D863A6AE430476C898248B8237D77534CFC4 - SUMERAGI_TRUSTED_PEERS: '[{"address":"iroha1:1338","public_key":"ed0120815BBDC9775D28C3633269B25F22D048E2AA2E36017CBE5AD85F15220BEB6F6F"},{"address":"iroha0:1337","public_key":"ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB"},{"address":"iroha3:1340","public_key":"ed0120A66522370D60B9C09E79ADE2E9BB1EF2E78733A944B999B3A6AEE687CE476D61"},{"address":"iroha2:1339","public_key":"ed0120F417E0371E6ADB32FD66749477402B1AB67F84A8E9B082E997980CC91F327736"}]' + 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 @@ -46,7 +46,7 @@ services: TORII_P2P_ADDR: iroha2:1339 TORII_API_URL: iroha2:8082 IROHA_GENESIS_ACCOUNT_PUBLIC_KEY: ed01204164BF554923ECE1FD412D241036D863A6AE430476C898248B8237D77534CFC4 - SUMERAGI_TRUSTED_PEERS: '[{"address":"iroha1:1338","public_key":"ed0120815BBDC9775D28C3633269B25F22D048E2AA2E36017CBE5AD85F15220BEB6F6F"},{"address":"iroha0:1337","public_key":"ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB"},{"address":"iroha3:1340","public_key":"ed0120A66522370D60B9C09E79ADE2E9BB1EF2E78733A944B999B3A6AEE687CE476D61"},{"address":"iroha2:1339","public_key":"ed0120F417E0371E6ADB32FD66749477402B1AB67F84A8E9B082E997980CC91F327736"}]' + 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 @@ -62,7 +62,7 @@ services: TORII_P2P_ADDR: iroha3:1340 TORII_API_URL: iroha3:8083 IROHA_GENESIS_ACCOUNT_PUBLIC_KEY: ed01204164BF554923ECE1FD412D241036D863A6AE430476C898248B8237D77534CFC4 - SUMERAGI_TRUSTED_PEERS: '[{"address":"iroha1:1338","public_key":"ed0120815BBDC9775D28C3633269B25F22D048E2AA2E36017CBE5AD85F15220BEB6F6F"},{"address":"iroha0:1337","public_key":"ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB"},{"address":"iroha3:1340","public_key":"ed0120A66522370D60B9C09E79ADE2E9BB1EF2E78733A944B999B3A6AEE687CE476D61"},{"address":"iroha2:1339","public_key":"ed0120F417E0371E6ADB32FD66749477402B1AB67F84A8E9B082E997980CC91F327736"}]' + 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 diff --git a/docker-compose.single.yml b/docker-compose.single.yml index d46667110c2..454f92ff312 100644 --- a/docker-compose.single.yml +++ b/docker-compose.single.yml @@ -3,6 +3,7 @@ services: iroha0: build: . image: iroha2:lts + platform: linux/amd64 environment: TORII_P2P_ADDR: iroha0:1337 TORII_API_URL: iroha0:8080 diff --git a/docker-compose.stable.single.yml b/docker-compose.stable.single.yml index e2c07b8e2fc..dfb250f6d9f 100644 --- a/docker-compose.stable.single.yml +++ b/docker-compose.stable.single.yml @@ -3,13 +3,13 @@ services: iroha0: build: . image: iroha2:stable + platform: linux/amd64 environment: TORII_P2P_ADDR: iroha0:1337 TORII_API_URL: iroha0:8080 TORII_TELEMETRY_URL: iroha0:8180 IROHA_PUBLIC_KEY: "ed01201C61FAF8FE94E253B93114240394F79A607B7FA55F9E5A41EBEC74B88055768B" IROHA_PRIVATE_KEY: '{"digest_function": "ed25519", "payload": "282ED9F3CF92811C3818DBC4AE594ED59DC1A2F78E4241E31924E101D6B1FB831C61FAF8FE94E253B93114240394F79A607B7FA55F9E5A41EBEC74B88055768B"}' - SUMERAGI_TRUSTED_PEERS: '[{"address":"iroha0:1337", "public_key": "ed01201C61FAF8FE94E253B93114240394F79A607B7FA55F9E5A41EBEC74B88055768B"}]' IROHA_GENESIS_ACCOUNT_PUBLIC_KEY: 'ed01203F4E3E98571B55514EDC5CCF7E53CA7509D89B2868E62921180A6F57C2F4E255' IROHA_GENESIS_ACCOUNT_PRIVATE_KEY: '{"digest_function": "ed25519", "payload": "038AE16B219DA35AA036335ED0A43C28A2CC737150112C78A7B8034B9D99C9023F4E3E98571B55514EDC5CCF7E53CA7509D89B2868E62921180A6F57C2F4E255"}' IROHA_GENESIS_WAIT_FOR_PEERS_RETRY_COUNT_LIMIT: 100 @@ -18,7 +18,6 @@ services: ports: - "1337:1337" - "8080:8080" - - "8180:8180" init: true command: iroha --submit-genesis volumes: diff --git a/docker-compose.stable.yml b/docker-compose.stable.yml index e57463504fc..95cdf9e04d8 100644 --- a/docker-compose.stable.yml +++ b/docker-compose.stable.yml @@ -2,13 +2,14 @@ version: "3.8" services: iroha0: image: hyperledger/iroha2:stable + platform: linux/amd64 environment: TORII_P2P_ADDR: iroha0:1337 TORII_API_URL: iroha0:8080 TORII_TELEMETRY_URL: iroha0:8180 IROHA_PUBLIC_KEY: "ed01201C61FAF8FE94E253B93114240394F79A607B7FA55F9E5A41EBEC74B88055768B" IROHA_PRIVATE_KEY: '{"digest_function": "ed25519", "payload": "282ED9F3CF92811C3818DBC4AE594ED59DC1A2F78E4241E31924E101D6B1FB831C61FAF8FE94E253B93114240394F79A607B7FA55F9E5A41EBEC74B88055768B"}' - SUMERAGI_TRUSTED_PEERS: '[{"address":"iroha0:1337", "public_key": "ed01201C61FAF8FE94E253B93114240394F79A607B7FA55F9E5A41EBEC74B88055768B"}, {"address":"iroha1:1338", "public_key": "ed0120CC25624D62896D3A0BFD8940F928DC2ABF27CC57CEFEB442AA96D9081AAE58A1"}, {"address": "iroha2:1339", "public_key": "ed0120FACA9E8AA83225CB4D16D67F27DD4F93FC30FFA11ADC1F5C88FD5495ECC91020"}, {"address": "iroha3:1340", "public_key": "ed01208E351A70B6A603ED285D666B8D689B680865913BA03CE29FB7D13A166C4E7F1F"}]' + SUMERAGI_TRUSTED_PEERS: '[{"address":"iroha1:1338", "public_key": "ed0120CC25624D62896D3A0BFD8940F928DC2ABF27CC57CEFEB442AA96D9081AAE58A1"}, {"address": "iroha2:1339", "public_key": "ed0120FACA9E8AA83225CB4D16D67F27DD4F93FC30FFA11ADC1F5C88FD5495ECC91020"}, {"address": "iroha3:1340", "public_key": "ed01208E351A70B6A603ED285D666B8D689B680865913BA03CE29FB7D13A166C4E7F1F"}]' IROHA_GENESIS_ACCOUNT_PUBLIC_KEY: 'ed01203f4e3e98571b55514edc5ccf7e53ca7509d89b2868e62921180a6f57c2f4e255' IROHA_GENESIS_ACCOUNT_PRIVATE_KEY: '{ "digest_function": "ed25519", "payload": "038AE16B219DA35AA036335ED0A43C28A2CC737150112C78A7B8034B9D99C9023F4E3E98571B55514EDC5CCF7E53CA7509D89B2868E62921180A6F57C2F4E255" }' IROHA_GENESIS_WAIT_FOR_PEERS_RETRY_COUNT_LIMIT: 100 @@ -17,7 +18,6 @@ services: ports: - "1337:1337" - "8080:8080" - - "8180:8180" volumes: - './configs/peer/stable:/config' init: true @@ -25,13 +25,14 @@ services: iroha1: image: hyperledger/iroha2:stable + platform: linux/amd64 environment: TORII_P2P_ADDR: iroha1:1338 TORII_API_URL: iroha1:8081 TORII_TELEMETRY_URL: iroha1:8181 IROHA_PUBLIC_KEY: "ed0120CC25624D62896D3A0BFD8940F928DC2ABF27CC57CEFEB442AA96D9081AAE58A1" IROHA_PRIVATE_KEY: '{"digest_function": "ed25519", "payload": "3BAC34CDA9E3763FA069C1198312D1EC73B53023B8180C822AC355435EDC4A24CC25624D62896D3A0BFD8940F928DC2ABF27CC57CEFEB442AA96D9081AAE58A1"}' - SUMERAGI_TRUSTED_PEERS: '[{"address":"iroha0:1337", "public_key": "ed01201C61FAF8FE94E253B93114240394F79A607B7FA55F9E5A41EBEC74B88055768B"}, {"address":"iroha1:1338", "public_key": "ed0120CC25624D62896D3A0BFD8940F928DC2ABF27CC57CEFEB442AA96D9081AAE58A1"}, {"address": "iroha2:1339", "public_key": "ed0120FACA9E8AA83225CB4D16D67F27DD4F93FC30FFA11ADC1F5C88FD5495ECC91020"}, {"address": "iroha3:1340", "public_key": "ed01208E351A70B6A603ED285D666B8D689B680865913BA03CE29FB7D13A166C4E7F1F"}]' + SUMERAGI_TRUSTED_PEERS: '[{"address":"iroha0:1337", "public_key": "ed01201C61FAF8FE94E253B93114240394F79A607B7FA55F9E5A41EBEC74B88055768B"}, {"address": "iroha2:1339", "public_key": "ed0120FACA9E8AA83225CB4D16D67F27DD4F93FC30FFA11ADC1F5C88FD5495ECC91020"}, {"address": "iroha3:1340", "public_key": "ed01208E351A70B6A603ED285D666B8D689B680865913BA03CE29FB7D13A166C4E7F1F"}]' IROHA_GENESIS_ACCOUNT_PUBLIC_KEY: 'ed01203F4E3E98571B55514EDC5CCF7E53CA7509D89B2868E62921180A6F57C2F4E255' IROHA_GENESIS_WAIT_FOR_PEERS_RETRY_COUNT_LIMIT: 100 IROHA_GENESIS_WAIT_FOR_PEERS_RETRY_PERIOD_MS: 500 @@ -39,20 +40,20 @@ services: ports: - "1338:1338" - "8081:8081" - - "8181:8181" volumes: - './configs/peer/stable:/config' init: true iroha2: image: hyperledger/iroha2:stable + platform: linux/amd64 environment: TORII_P2P_ADDR: iroha2:1339 TORII_API_URL: iroha2:8082 TORII_TELEMETRY_URL: iroha2:8182 IROHA_PUBLIC_KEY: "ed0120FACA9E8AA83225CB4D16D67F27DD4F93FC30FFA11ADC1F5C88FD5495ECC91020" IROHA_PRIVATE_KEY: '{"digest_function": "ed25519", "payload": "1261A436D36779223D7D6CF20E8B644510E488E6A50BAFD77A7485264D27197DFACA9E8AA83225CB4D16D67F27DD4F93FC30FFA11ADC1F5C88FD5495ECC91020"}' - SUMERAGI_TRUSTED_PEERS: '[{"address":"iroha0:1337", "public_key": "ed01201C61FAF8FE94E253B93114240394F79A607B7FA55F9E5A41EBEC74B88055768B"}, {"address":"iroha1:1338", "public_key": "ed0120CC25624D62896D3A0BFD8940F928DC2ABF27CC57CEFEB442AA96D9081AAE58A1"}, {"address": "iroha2:1339", "public_key": "ed0120FACA9E8AA83225CB4D16D67F27DD4F93FC30FFA11ADC1F5C88FD5495ECC91020"}, {"address": "iroha3:1340", "public_key": "ed01208E351A70B6A603ED285D666B8D689B680865913BA03CE29FB7D13A166C4E7F1F"}]' + SUMERAGI_TRUSTED_PEERS: '[{"address":"iroha0:1337", "public_key": "ed01201C61FAF8FE94E253B93114240394F79A607B7FA55F9E5A41EBEC74B88055768B"}, {"address":"iroha1:1338", "public_key": "ed0120CC25624D62896D3A0BFD8940F928DC2ABF27CC57CEFEB442AA96D9081AAE58A1"}, {"address": "iroha3:1340", "public_key": "ed01208E351A70B6A603ED285D666B8D689B680865913BA03CE29FB7D13A166C4E7F1F"}]' IROHA_GENESIS_ACCOUNT_PUBLIC_KEY: 'ed01203F4E3E98571B55514EDC5CCF7E53CA7509D89B2868E62921180A6F57C2F4E255' IROHA_GENESIS_WAIT_FOR_PEERS_RETRY_COUNT_LIMIT: 100 IROHA_GENESIS_WAIT_FOR_PEERS_RETRY_PERIOD_MS: 500 @@ -60,20 +61,20 @@ services: ports: - "1339:1339" - "8082:8082" - - "8182:8182" volumes: - './configs/peer/stable:/config' init: true iroha3: image: hyperledger/iroha2:stable + platform: linux/amd64 environment: TORII_P2P_ADDR: iroha3:1340 TORII_API_URL: iroha3:8083 TORII_TELEMETRY_URL: iroha3:8183 IROHA_PUBLIC_KEY: "ed01208E351A70B6A603ED285D666B8D689B680865913BA03CE29FB7D13A166C4E7F1F" IROHA_PRIVATE_KEY: '{"digest_function": "ed25519", "payload": "A70DAB95C7482EB9F159111B65947E482108CFE67DF877BD8D3B9441A781C7C98E351A70B6A603ED285D666B8D689B680865913BA03CE29FB7D13A166C4E7F1F"}' - SUMERAGI_TRUSTED_PEERS: '[{"address":"iroha0:1337", "public_key": "ed01201C61FAF8FE94E253B93114240394F79A607B7FA55F9E5A41EBEC74B88055768B"}, {"address":"iroha1:1338", "public_key": "ed0120CC25624D62896D3A0BFD8940F928DC2ABF27CC57CEFEB442AA96D9081AAE58A1"}, {"address": "iroha2:1339", "public_key": "ed0120FACA9E8AA83225CB4D16D67F27DD4F93FC30FFA11ADC1F5C88FD5495ECC91020"}, {"address": "iroha3:1340", "public_key": "ed01208E351A70B6A603ED285D666B8D689B680865913BA03CE29FB7D13A166C4E7F1F"}]' + SUMERAGI_TRUSTED_PEERS: '[{"address":"iroha0:1337", "public_key": "ed01201C61FAF8FE94E253B93114240394F79A607B7FA55F9E5A41EBEC74B88055768B"}, {"address":"iroha1:1338", "public_key": "ed0120CC25624D62896D3A0BFD8940F928DC2ABF27CC57CEFEB442AA96D9081AAE58A1"}, {"address": "iroha2:1339", "public_key": "ed0120FACA9E8AA83225CB4D16D67F27DD4F93FC30FFA11ADC1F5C88FD5495ECC91020"}]' IROHA_GENESIS_ACCOUNT_PUBLIC_KEY: 'ed01203F4E3E98571B55514EDC5CCF7E53CA7509D89B2868E62921180A6F57C2F4E255' IROHA_GENESIS_WAIT_FOR_PEERS_RETRY_COUNT_LIMIT: 100 IROHA_GENESIS_WAIT_FOR_PEERS_RETRY_PERIOD_MS: 500 @@ -81,7 +82,6 @@ services: ports: - "1340:1340" - "8083:8083" - - "8183:8183" volumes: - './configs/peer/stable:/config' init: true diff --git a/docker-compose.yml b/docker-compose.yml index e781607ee9a..d24025c2fb3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,6 +2,7 @@ version: "3.8" services: iroha0: image: hyperledger/iroha2:lts + platform: linux/amd64 environment: TORII_P2P_ADDR: iroha0:1337 TORII_API_URL: iroha0:8080 @@ -25,6 +26,7 @@ services: iroha1: image: hyperledger/iroha2:lts + platform: linux/amd64 environment: TORII_P2P_ADDR: iroha1:1338 TORII_API_URL: iroha1:8081 @@ -46,6 +48,7 @@ services: iroha2: image: hyperledger/iroha2:lts + platform: linux/amd64 environment: TORII_P2P_ADDR: iroha2:1339 TORII_API_URL: iroha2:8082 @@ -67,6 +70,7 @@ services: iroha3: image: hyperledger/iroha2:lts + platform: linux/amd64 environment: TORII_P2P_ADDR: iroha3:1340 TORII_API_URL: iroha3:8083 diff --git a/p2p/src/network.rs b/p2p/src/network.rs index 751eb779d3d..51b97d661e2 100644 --- a/p2p/src/network.rs +++ b/p2p/src/network.rs @@ -366,7 +366,7 @@ impl NetworkBase { }: Connected, ) { if !self.current_topology.contains_key(&peer_id) { - iroha_logger::warn!(topology=?self.current_topology, "Peer not present in topology is trying to connect"); + iroha_logger::warn!(%peer_id, topology=?self.current_topology, "Peer not present in topology is trying to connect"); return; } diff --git a/smart_contract/executor/derive/src/validate.rs b/smart_contract/executor/derive/src/validate.rs index e552005dbf1..3b7df471f0f 100644 --- a/smart_contract/executor/derive/src/validate.rs +++ b/smart_contract/executor/derive/src/validate.rs @@ -154,7 +154,7 @@ impl FromAttributes for ValidateAttribute { let res = accumulator.handle(result); - accumulator.finish().map(|_| res.unwrap()) + accumulator.finish().map(|()| res.unwrap()) } } diff --git a/smart_contract/src/lib.rs b/smart_contract/src/lib.rs index 2bdd93706db..db86863330d 100644 --- a/smart_contract/src/lib.rs +++ b/smart_contract/src/lib.rs @@ -351,7 +351,7 @@ impl> Iterator for QueryOutputCursorIterator { let mut next_iter = match self.next_batch() { Ok(next_iter) => next_iter, Err(QueryOutputCursorError::Validation(ValidationFail::QueryFailed( - iroha_data_model::query::error::QueryExecutionFail::UnknownCursor, + data_model::query::error::QueryExecutionFail::UnknownCursor, ))) => return None, Err(err) => return Some(Err(err)), }; diff --git a/telemetry/src/metrics.rs b/telemetry/src/metrics.rs index 1043cbb9954..ad4f7744750 100644 --- a/telemetry/src/metrics.rs +++ b/telemetry/src/metrics.rs @@ -35,10 +35,10 @@ impl Encode for Uptime { /// Response body for GET status request #[derive(Clone, Copy, Debug, Default, Deserialize, Serialize, Encode)] pub struct Status { - /// Number of connected peers, except for the reporting peer itself + /// Number of currently connected peers excluding the reporting peer #[codec(compact)] pub peers: u64, - /// Number of committed blocks + /// Number of committed blocks (blockchain height) #[codec(compact)] pub blocks: u64, /// Number of accepted transactions @@ -77,9 +77,9 @@ impl> From<&T> for Status { pub struct Metrics { /// Total number of transactions pub txs: IntCounterVec, - /// Current block height + /// Number of committed blocks (blockchain height) pub block_height: IntCounter, - /// Total number of currently connected peers + /// Number of currently connected peers excluding the reporting peer pub connected_peers: GenericGauge, /// Uptime of the network, starting from commit of the genesis block pub uptime_since_genesis_ms: GenericGauge, diff --git a/tools/swarm/src/compose.rs b/tools/swarm/src/compose.rs index e08ab109e91..36c43317750 100644 --- a/tools/swarm/src/compose.rs +++ b/tools/swarm/src/compose.rs @@ -215,7 +215,8 @@ struct FullPeerEnv { iroha_genesis_account_public_key: Option, #[serde(skip_serializing_if = "Option::is_none")] iroha_genesis_account_private_key: Option>, - sumeragi_trusted_peers: SerializeAsJsonStr>, + #[serde(skip_serializing_if = "Option::is_none")] + sumeragi_trusted_peers: Option>>, } struct CompactPeerEnv { @@ -237,7 +238,11 @@ impl From for FullPeerEnv { iroha_genesis_account_private_key: value.genesis_private_key.map(SerializeAsJsonStr), torii_p2p_addr: value.p2p_addr, torii_api_url: value.api_addr, - sumeragi_trusted_peers: SerializeAsJsonStr(value.trusted_peers), + sumeragi_trusted_peers: if value.trusted_peers.is_empty() { + None + } else { + Some(SerializeAsJsonStr(value.trusted_peers)) + }, } } } @@ -311,7 +316,11 @@ impl DockerComposeBuilder<'_> { peer, service_source.clone(), volumes.clone(), - trusted_peers.clone(), + trusted_peers + .iter() + .filter(|trusted_peer| trusted_peer.public_key() != peer.key_pair.public_key()) + .cloned() + .collect(), Some(genesis_key_pair.public_key().clone()), Some(genesis_key_pair.private_key().clone()), ); @@ -325,7 +334,13 @@ impl DockerComposeBuilder<'_> { peer, service_source.clone(), volumes.clone(), - trusted_peers.clone(), + trusted_peers + .iter() + .filter(|trusted_peer| { + trusted_peer.public_key() != peer.key_pair.public_key() + }) + .cloned() + .collect(), Some(genesis_key_pair.public_key().clone()), None, ); @@ -615,7 +630,6 @@ mod tests { TORII_API_URL: iroha1:1338 IROHA_GENESIS_ACCOUNT_PUBLIC_KEY: ed012039E5BF092186FACC358770792A493CA98A83740643A3D41389483CF334F748C8 IROHA_GENESIS_ACCOUNT_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"db9d90d20f969177bd5882f9fe211d14d1399d5440d04e3468783d169bbc4a8e39e5bf092186facc358770792a493ca98a83740643a3d41389483cf334f748c8"}' - SUMERAGI_TRUSTED_PEERS: '[]' ports: - 1337:1337 - 8080:8080 @@ -651,7 +665,6 @@ mod tests { TORII_P2P_ADDR: iroha0:1337 TORII_API_URL: iroha0:1337 IROHA_GENESIS_ACCOUNT_PUBLIC_KEY: ed0120415388A90FA238196737746A70565D041CFB32EAA0C89FF8CB244C7F832A6EBD - SUMERAGI_TRUSTED_PEERS: '[]' "#]]; expected.assert_eq(&actual); } @@ -691,7 +704,7 @@ mod tests { TORII_API_URL: iroha0:8080 IROHA_GENESIS_ACCOUNT_PUBLIC_KEY: ed01203420F48A9EEB12513B8EB7DAF71979CE80A1013F5F341C10DCDA4F6AA19F97A9 IROHA_GENESIS_ACCOUNT_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"5a6d5f06a90d29ad906e2f6ea8b41b4ef187849d0d397081a4a15ffcbe71e7c73420f48a9eeb12513b8eb7daf71979ce80a1013f5f341c10dcda4f6aa19f97a9"}' - SUMERAGI_TRUSTED_PEERS: '[{"address":"iroha2:1339","public_key":"ed0120312C1B7B5DE23D366ADCF23CD6DB92CE18B2AA283C7D9F5033B969C2DC2B92F4"},{"address":"iroha3:1340","public_key":"ed0120854457B2E3D6082181DA73DC01C1E6F93A72D0C45268DC8845755287E98A5DEE"},{"address":"iroha1:1338","public_key":"ed0120A88554AA5C86D28D0EEBEC497235664433E807881CD31E12A1AF6C4D8B0F026C"},{"address":"iroha0:1337","public_key":"ed0120F0321EB4139163C35F88BF78520FF7071499D7F4E79854550028A196C7B49E13"}]' + SUMERAGI_TRUSTED_PEERS: '[{"address":"iroha2:1339","public_key":"ed0120312C1B7B5DE23D366ADCF23CD6DB92CE18B2AA283C7D9F5033B969C2DC2B92F4"},{"address":"iroha3:1340","public_key":"ed0120854457B2E3D6082181DA73DC01C1E6F93A72D0C45268DC8845755287E98A5DEE"},{"address":"iroha1:1338","public_key":"ed0120A88554AA5C86D28D0EEBEC497235664433E807881CD31E12A1AF6C4D8B0F026C"}]' ports: - 1337:1337 - 8080:8080 @@ -708,7 +721,7 @@ mod tests { TORII_P2P_ADDR: iroha1:1338 TORII_API_URL: iroha1:8081 IROHA_GENESIS_ACCOUNT_PUBLIC_KEY: ed01203420F48A9EEB12513B8EB7DAF71979CE80A1013F5F341C10DCDA4F6AA19F97A9 - SUMERAGI_TRUSTED_PEERS: '[{"address":"iroha2:1339","public_key":"ed0120312C1B7B5DE23D366ADCF23CD6DB92CE18B2AA283C7D9F5033B969C2DC2B92F4"},{"address":"iroha3:1340","public_key":"ed0120854457B2E3D6082181DA73DC01C1E6F93A72D0C45268DC8845755287E98A5DEE"},{"address":"iroha1:1338","public_key":"ed0120A88554AA5C86D28D0EEBEC497235664433E807881CD31E12A1AF6C4D8B0F026C"},{"address":"iroha0:1337","public_key":"ed0120F0321EB4139163C35F88BF78520FF7071499D7F4E79854550028A196C7B49E13"}]' + SUMERAGI_TRUSTED_PEERS: '[{"address":"iroha2:1339","public_key":"ed0120312C1B7B5DE23D366ADCF23CD6DB92CE18B2AA283C7D9F5033B969C2DC2B92F4"},{"address":"iroha3:1340","public_key":"ed0120854457B2E3D6082181DA73DC01C1E6F93A72D0C45268DC8845755287E98A5DEE"},{"address":"iroha0:1337","public_key":"ed0120F0321EB4139163C35F88BF78520FF7071499D7F4E79854550028A196C7B49E13"}]' ports: - 1338:1338 - 8081:8081 @@ -724,7 +737,7 @@ mod tests { TORII_P2P_ADDR: iroha2:1339 TORII_API_URL: iroha2:8082 IROHA_GENESIS_ACCOUNT_PUBLIC_KEY: ed01203420F48A9EEB12513B8EB7DAF71979CE80A1013F5F341C10DCDA4F6AA19F97A9 - SUMERAGI_TRUSTED_PEERS: '[{"address":"iroha2:1339","public_key":"ed0120312C1B7B5DE23D366ADCF23CD6DB92CE18B2AA283C7D9F5033B969C2DC2B92F4"},{"address":"iroha3:1340","public_key":"ed0120854457B2E3D6082181DA73DC01C1E6F93A72D0C45268DC8845755287E98A5DEE"},{"address":"iroha1:1338","public_key":"ed0120A88554AA5C86D28D0EEBEC497235664433E807881CD31E12A1AF6C4D8B0F026C"},{"address":"iroha0:1337","public_key":"ed0120F0321EB4139163C35F88BF78520FF7071499D7F4E79854550028A196C7B49E13"}]' + SUMERAGI_TRUSTED_PEERS: '[{"address":"iroha3:1340","public_key":"ed0120854457B2E3D6082181DA73DC01C1E6F93A72D0C45268DC8845755287E98A5DEE"},{"address":"iroha1:1338","public_key":"ed0120A88554AA5C86D28D0EEBEC497235664433E807881CD31E12A1AF6C4D8B0F026C"},{"address":"iroha0:1337","public_key":"ed0120F0321EB4139163C35F88BF78520FF7071499D7F4E79854550028A196C7B49E13"}]' ports: - 1339:1339 - 8082:8082 @@ -740,7 +753,7 @@ mod tests { TORII_P2P_ADDR: iroha3:1340 TORII_API_URL: iroha3:8083 IROHA_GENESIS_ACCOUNT_PUBLIC_KEY: ed01203420F48A9EEB12513B8EB7DAF71979CE80A1013F5F341C10DCDA4F6AA19F97A9 - SUMERAGI_TRUSTED_PEERS: '[{"address":"iroha2:1339","public_key":"ed0120312C1B7B5DE23D366ADCF23CD6DB92CE18B2AA283C7D9F5033B969C2DC2B92F4"},{"address":"iroha3:1340","public_key":"ed0120854457B2E3D6082181DA73DC01C1E6F93A72D0C45268DC8845755287E98A5DEE"},{"address":"iroha1:1338","public_key":"ed0120A88554AA5C86D28D0EEBEC497235664433E807881CD31E12A1AF6C4D8B0F026C"},{"address":"iroha0:1337","public_key":"ed0120F0321EB4139163C35F88BF78520FF7071499D7F4E79854550028A196C7B49E13"}]' + SUMERAGI_TRUSTED_PEERS: '[{"address":"iroha2:1339","public_key":"ed0120312C1B7B5DE23D366ADCF23CD6DB92CE18B2AA283C7D9F5033B969C2DC2B92F4"},{"address":"iroha1:1338","public_key":"ed0120A88554AA5C86D28D0EEBEC497235664433E807881CD31E12A1AF6C4D8B0F026C"},{"address":"iroha0:1337","public_key":"ed0120F0321EB4139163C35F88BF78520FF7071499D7F4E79854550028A196C7B49E13"}]' ports: - 1340:1340 - 8083:8083 From e33208ddf89aba047412ec21730c954fbae5e399 Mon Sep 17 00:00:00 2001 From: 0x009922 <43530070+0x009922@users.noreply.github.com> Date: Fri, 15 Dec 2023 08:09:44 +0000 Subject: [PATCH 11/21] [refactor]: split `iroha_torii` from `iroha` (#4139) * [refactor]: split `iroha_torii` from `iroha` Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [test]: fix doctest Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [refactor]: rename `iroha_torii_` to `*_` Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --------- Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- Cargo.lock | 63 +++-- Cargo.toml | 6 +- cli/Cargo.toml | 33 +-- cli/src/lib.rs | 5 +- cli/src/torii/mod.rs | 145 ---------- torii/Cargo.toml | 48 ++++ {cli/derive => torii/macro}/Cargo.toml | 2 +- {cli/derive => torii/macro}/src/lib.rs | 3 +- {cli => torii}/src/event.rs | 0 torii/src/lib.rs | 359 ++++++++++++++++++++++++ {cli/src/torii => torii/src}/routing.rs | 238 ++-------------- {cli => torii}/src/stream.rs | 0 {cli/src/torii => torii/src}/utils.rs | 2 +- 13 files changed, 480 insertions(+), 424 deletions(-) delete mode 100644 cli/src/torii/mod.rs create mode 100644 torii/Cargo.toml rename {cli/derive => torii/macro}/Cargo.toml (93%) rename {cli/derive => torii/macro}/src/lib.rs (98%) rename {cli => torii}/src/event.rs (100%) create mode 100644 torii/src/lib.rs rename {cli/src/torii => torii/src}/routing.rs (57%) rename {cli => torii}/src/stream.rs (100%) rename {cli/src/torii => torii/src}/utils.rs (97%) diff --git a/Cargo.lock b/Cargo.lock index fb0b976dace..1ff201d4026 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2610,14 +2610,8 @@ dependencies = [ name = "iroha" version = "2.0.0-pre-rc.20" dependencies = [ - "async-trait", "color-eyre", - "dashmap", - "derive_more", - "displaydoc", "eyre", - "futures", - "iroha_cli_derive", "iroha_config", "iroha_core", "iroha_crypto", @@ -2625,37 +2619,19 @@ dependencies = [ "iroha_futures", "iroha_genesis", "iroha_logger", - "iroha_macro", "iroha_p2p", "iroha_primitives", - "iroha_schema_gen", "iroha_telemetry", - "iroha_version", + "iroha_torii", "iroha_wasm_builder", "once_cell", "owo-colors", - "parity-scale-codec", - "serde", - "serde_json", "serial_test", "supports-color 2.1.0", - "tempfile", - "thiserror", "thread-local-panic-hook", "tokio", "tracing", "vergen", - "warp", -] - -[[package]] -name = "iroha_cli_derive" -version = "2.0.0-pre-rc.20" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", - "warp", ] [[package]] @@ -3240,6 +3216,43 @@ dependencies = [ "trybuild", ] +[[package]] +name = "iroha_torii" +version = "2.0.0-pre-rc.20" +dependencies = [ + "async-trait", + "displaydoc", + "eyre", + "futures", + "iroha_config", + "iroha_core", + "iroha_data_model", + "iroha_futures", + "iroha_logger", + "iroha_macro", + "iroha_primitives", + "iroha_schema_gen", + "iroha_telemetry", + "iroha_torii_macro", + "iroha_version", + "parity-scale-codec", + "serde", + "serde_json", + "thiserror", + "tokio", + "warp", +] + +[[package]] +name = "iroha_torii_macro" +version = "2.0.0-pre-rc.20" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", + "warp", +] + [[package]] name = "iroha_trigger" version = "2.0.0-pre-rc.20" diff --git a/Cargo.toml b/Cargo.toml index 3da58f35d73..66d4334ade9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,8 @@ categories = ["cryptography::cryptocurrencies"] [workspace.dependencies] iroha = { path = "cli" } iroha_dsl = { version = "=2.0.0-pre-rc.20", path = "dsl" } -iroha_cli_derive = { version = "=2.0.0-pre-rc.20", path = "cli/derive" } +iroha_torii = { version = "=2.0.0-pre-rc.20", path = "torii" } +iroha_torii_macro = { version = "=2.0.0-pre-rc.20", path = "torii/macro" } 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" } @@ -205,7 +206,6 @@ clippy.wildcard_dependencies = "deny" resolver = "2" members = [ "cli", - "cli/derive", "client", "client_cli", "config", @@ -245,6 +245,8 @@ members = [ "tools/swarm", "tools/wasm_builder_cli", "tools/wasm_test_runner", + "torii", + "torii/macro", "version", "version/derive", "wasm_codec", diff --git a/cli/Cargo.toml b/cli/Cargo.toml index e0abd7c480b..0ca76872233 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -25,13 +25,13 @@ bridge = ["iroha_core/bridge"] # Support Decentralised Exchange, including functionality for atomic exchange instruction dex = ["iroha_core/dex"] # Support lightweight telemetry, including diagnostics -telemetry = ["iroha_telemetry", "iroha_core/telemetry"] +telemetry = ["iroha_telemetry", "iroha_core/telemetry", "iroha_torii/telemetry"] # Support developer-specific telemetry. # Should not be enabled on production builds. dev-telemetry = ["iroha_core/dev-telemetry", "iroha_telemetry"] # Support schema generation from the `schema` endpoint in the local binary. # Useful for debugging issues with decoding in SDKs. -schema-endpoint = ["iroha_schema_gen"] +schema-endpoint = ["iroha_torii/schema"] # Support internal testing infrastructure for integration tests. # Disable in production. test-network = ["thread-local-panic-hook"] @@ -43,40 +43,25 @@ maintenance = { status = "actively-developed" } [dependencies] iroha_core = { workspace = true } -iroha_macro = { workspace = true } iroha_logger = { workspace = true } iroha_futures = { workspace = true } iroha_data_model = { workspace = true, features = ["http"] } iroha_primitives = { workspace = true } iroha_telemetry = { workspace = true, optional = true } -iroha_version = { workspace = true, features = ["http"] } iroha_config = { workspace = true } iroha_crypto = { workspace = true } iroha_p2p = { workspace = true } -iroha_schema_gen = { workspace = true, optional = true } -iroha_cli_derive = { workspace = true } +iroha_torii = { workspace = true } iroha_genesis = { workspace = true } iroha_wasm_builder = { workspace = true } - -derive_more = { workspace = true } -async-trait = { workspace = true } color-eyre = { workspace = true } eyre = { workspace = true } tracing = { workspace = true } -futures = { workspace = true, features = ["std", "async-await"] } -parity-scale-codec = { workspace = true, features = ["derive"] } -serde = { workspace = true, features = ["derive"] } -serde_json = { workspace = true } -thiserror = { workspace = true } -displaydoc = { workspace = true } -tokio = { workspace = true, features = ["sync", "time", "rt", "io-util", "rt-multi-thread", "macros", "fs", "signal"] } -warp = { workspace = true, features = ["multipart", "websocket"] } +tokio = { workspace = true, features = ["macros", "signal"] } once_cell = { workspace = true } owo-colors = { workspace = true, features = ["supports-colors"] } supports-color = { workspace = true } -tempfile = { workspace = true } -dashmap = { workspace = true } thread-local-panic-hook = { version = "0.1.0", optional = true } @@ -91,10 +76,10 @@ vergen = { workspace = true, features = ["cargo"] } [package.metadata.cargo-all-features] denylist = [ -"bridge", -"dex", -"schema-endpoint", -"telemetry", -"test-network" + "bridge", + "dex", + "schema-endpoint", + "telemetry", + "test-network", ] # TODO: remove `dex` and `bridge` once there's code for them. skip_optional_dependencies = true diff --git a/cli/src/lib.rs b/cli/src/lib.rs index 2d8a9a9c078..53ee9fe3306 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -33,18 +33,15 @@ use iroha_core::{ use iroha_data_model::prelude::*; use iroha_genesis::GenesisNetwork; use iroha_logger::actor::LoggerHandle; +use iroha_torii::Torii; use tokio::{ signal, sync::{broadcast, mpsc, Notify}, task, }; -use torii::Torii; -mod event; pub mod samples; -mod stream; pub mod style; -pub mod torii; /// Arguments for Iroha2. Configuration for arguments is parsed from /// environment variables and then the appropriate object is diff --git a/cli/src/torii/mod.rs b/cli/src/torii/mod.rs deleted file mode 100644 index 7780d4e5b74..00000000000 --- a/cli/src/torii/mod.rs +++ /dev/null @@ -1,145 +0,0 @@ -//! Translates to gateway. Request handling logic of Iroha. `Torii` -//! is used to receive, accept and route incoming instructions, -//! queries and messages. - -use std::{ - convert::Infallible, - fmt::{Debug, Write as _}, - net::ToSocketAddrs, - sync::Arc, -}; - -use futures::{stream::FuturesUnordered, StreamExt}; -use iroha_config::torii::Configuration as ToriiConfiguration; -use iroha_core::{ - kiso::{Error as KisoError, KisoHandle}, - kura::Kura, - prelude::*, - query::store::LiveQueryStoreHandle, - queue::{self, Queue}, - sumeragi::SumeragiHandle, - EventsSender, -}; -use iroha_primitives::addr::SocketAddr; -use tokio::sync::Notify; -use utils::*; -use warp::{ - http::StatusCode, - reply::{self, Json, Response}, - ws::{WebSocket, Ws}, - Filter as _, Reply, -}; - -#[macro_use] -pub(crate) mod utils; -mod routing; - -/// Main network handler and the only entrypoint of the Iroha. -pub struct Torii { - kiso: KisoHandle, - queue: Arc, - events: EventsSender, - notify_shutdown: Arc, - sumeragi: SumeragiHandle, - query_service: LiveQueryStoreHandle, - kura: Arc, - transaction_max_content_length: u64, - address: SocketAddr, -} - -/// Torii errors. -#[derive(Debug, thiserror::Error, displaydoc::Display)] -pub enum Error { - /// Failed to process query - Query(#[from] iroha_data_model::ValidationFail), - /// Failed to accept transaction - AcceptTransaction(#[from] iroha_core::tx::AcceptTransactionFail), - /// Error while getting or setting configuration - Config(#[source] eyre::Report), - /// Failed to push into queue - PushIntoQueue(#[from] Box), - #[cfg(feature = "telemetry")] - /// Error while getting Prometheus metrics - Prometheus(#[source] eyre::Report), - /// Internal error while getting status - StatusFailure(#[source] eyre::Report), - /// Failure caused by configuration subsystem - ConfigurationFailure(#[from] KisoError), - /// Cannot find status segment by provided path - StatusSegmentNotFound(#[source] eyre::Report), -} - -impl Reply for Error { - fn into_response(self) -> Response { - match self { - Self::Query(err) => { - reply::with_status(utils::Scale(&err), Self::query_status_code(&err)) - .into_response() - } - _ => reply::with_status(Self::to_string(&self), self.status_code()).into_response(), - } - } -} - -impl Error { - fn status_code(&self) -> StatusCode { - use Error::*; - - match self { - Query(e) => Self::query_status_code(e), - AcceptTransaction(_) => StatusCode::BAD_REQUEST, - Config(_) | StatusSegmentNotFound(_) => StatusCode::NOT_FOUND, - PushIntoQueue(err) => match **err { - queue::Error::Full => StatusCode::INTERNAL_SERVER_ERROR, - queue::Error::SignatureCondition { .. } => StatusCode::UNAUTHORIZED, - _ => StatusCode::BAD_REQUEST, - }, - #[cfg(feature = "telemetry")] - Prometheus(_) | StatusFailure(_) | ConfigurationFailure(_) => { - StatusCode::INTERNAL_SERVER_ERROR - } - } - } - - fn query_status_code(validation_error: &iroha_data_model::ValidationFail) -> StatusCode { - use iroha_data_model::{ - isi::error::InstructionExecutionError, query::error::QueryExecutionFail::*, - ValidationFail::*, - }; - - match validation_error { - NotPermitted(_) => StatusCode::FORBIDDEN, - QueryFailed(query_error) - | InstructionFailed(InstructionExecutionError::Query(query_error)) => match query_error - { - Conversion(_) | UnknownCursor | FetchSizeTooBig => StatusCode::BAD_REQUEST, - Signature(_) => StatusCode::UNAUTHORIZED, - Find(_) => StatusCode::NOT_FOUND, - }, - TooComplex => StatusCode::UNPROCESSABLE_ENTITY, - InternalError(_) => StatusCode::INTERNAL_SERVER_ERROR, - InstructionFailed(error) => { - iroha_logger::error!( - ?error, - "Query validation failed with unexpected error. This means a bug inside Runtime Executor", - ); - StatusCode::INTERNAL_SERVER_ERROR - } - } - } - - fn to_string(err: &dyn std::error::Error) -> String { - let mut s = "Error:\n".to_owned(); - let mut idx = 0_i32; - let mut err_opt = Some(err); - while let Some(e) = err_opt { - write!(s, " {idx}: {}", &e.to_string()).expect("Valid"); - idx += 1_i32; - err_opt = e.source() - } - s - } -} - -/// Result type -pub type Result = std::result::Result; diff --git a/torii/Cargo.toml b/torii/Cargo.toml new file mode 100644 index 00000000000..5c6783dc63c --- /dev/null +++ b/torii/Cargo.toml @@ -0,0 +1,48 @@ +[package] +name = "iroha_torii" + +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 + +[features] +# Enables Telemetry (i.e. Status, Metrics, and API Version) endpoints +telemetry = ["iroha_telemetry", "iroha_core/telemetry", "serde_json"] +# Enables Data Model Schema endpoint +schema = ["iroha_schema_gen"] + +[dependencies] +iroha_core = { workspace = true } +iroha_config = { workspace = true } +iroha_primitives = { workspace = true } +iroha_logger = { workspace = true } +iroha_data_model = { workspace = true, features = ["http"] } +iroha_version = { workspace = true, features = ["http"] } +iroha_torii_macro = { workspace = true } +iroha_futures = { workspace = true } +iroha_macro = { workspace = true } +iroha_schema_gen = { workspace = true, optional = true } +iroha_telemetry = { workspace = true, optional = true } + +thiserror = { workspace = true } +displaydoc = { workspace = true } +futures = { workspace = true, features = ["std", "async-await"] } +warp = { workspace = true, features = ["multipart", "websocket"] } +tokio = { workspace = true, features = ["sync", "time", "macros"] } +eyre = { workspace = true } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true, optional = true } +async-trait = { workspace = true } +parity-scale-codec = { workspace = true, features = ["derive"] } diff --git a/cli/derive/Cargo.toml b/torii/macro/Cargo.toml similarity index 93% rename from cli/derive/Cargo.toml rename to torii/macro/Cargo.toml index d258df86e55..3baec913c14 100644 --- a/cli/derive/Cargo.toml +++ b/torii/macro/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "iroha_cli_derive" +name = "iroha_torii_macro" edition.workspace = true version.workspace = true diff --git a/cli/derive/src/lib.rs b/torii/macro/src/lib.rs similarity index 98% rename from cli/derive/src/lib.rs rename to torii/macro/src/lib.rs index 00438a1df47..44bab73e304 100644 --- a/cli/derive/src/lib.rs +++ b/torii/macro/src/lib.rs @@ -39,9 +39,8 @@ use syn::{ /// ```rust /// use warp::{Rejection, Filter}; /// use std::{convert::Infallible, marker::PhantomData}; -/// // use iroha_cli::torii::utils::WarpResult; /// pub struct WarpResult(Result); -/// use iroha_cli_derive::generate_endpoints; +/// use iroha_torii_macro::generate_endpoints; /// /// // An example with arguments of both acceptable kinds. /// // This would generate endpoints accepting functions with diff --git a/cli/src/event.rs b/torii/src/event.rs similarity index 100% rename from cli/src/event.rs rename to torii/src/event.rs diff --git a/torii/src/lib.rs b/torii/src/lib.rs new file mode 100644 index 00000000000..31245d6ffc0 --- /dev/null +++ b/torii/src/lib.rs @@ -0,0 +1,359 @@ +//! The web server of Iroha. `Torii` translates to gateway. +//! +//! Crate provides the following features that are not enabled by default: +//! +//! - `telemetry`: enables Status, Metrics, and API Version endpoints +//! - `schema`: enables Data Model Schema endpoint + +use std::{ + convert::Infallible, + fmt::{Debug, Write as _}, + net::ToSocketAddrs, + sync::Arc, +}; + +use futures::{stream::FuturesUnordered, StreamExt}; +use iroha_config::torii::{uri, Configuration as ToriiConfiguration}; +use iroha_core::{ + kiso::{Error as KisoError, KisoHandle}, + kura::Kura, + prelude::*, + query::store::LiveQueryStoreHandle, + queue::{self, Queue}, + sumeragi::SumeragiHandle, + EventsSender, +}; +use iroha_primitives::addr::SocketAddr; +use tokio::{sync::Notify, task}; +use utils::*; +use warp::{ + http::StatusCode, + reply::{self, Json, Response}, + ws::{WebSocket, Ws}, + Filter as _, Reply, +}; + +#[macro_use] +pub(crate) mod utils; +mod event; +mod routing; +mod stream; + +/// Main network handler and the only entrypoint of the Iroha. +pub struct Torii { + kiso: KisoHandle, + queue: Arc, + events: EventsSender, + notify_shutdown: Arc, + sumeragi: SumeragiHandle, + query_service: LiveQueryStoreHandle, + kura: Arc, + transaction_max_content_length: u64, + address: SocketAddr, +} + +impl Torii { + /// Construct `Torii`. + #[allow(clippy::too_many_arguments)] + pub fn new( + kiso: KisoHandle, + config: &ToriiConfiguration, + queue: Arc, + events: EventsSender, + notify_shutdown: Arc, + sumeragi: SumeragiHandle, + query_service: LiveQueryStoreHandle, + kura: Arc, + ) -> Self { + Self { + kiso, + queue, + events, + notify_shutdown, + sumeragi, + query_service, + kura, + address: config.api_url.clone(), + transaction_max_content_length: config.max_content_len.into(), + } + } + + /// Helper function to create router. This router can be tested without starting up an HTTP server + #[allow(clippy::too_many_lines)] + fn create_api_router(&self) -> impl warp::Filter + Clone + Send { + let health_route = warp::get() + .and(warp::path(uri::HEALTH)) + .and_then(|| async { Ok::<_, Infallible>(routing::handle_health()) }); + + let get_router = warp::get().and( + endpoint3( + routing::handle_pending_transactions, + warp::path(uri::PENDING_TRANSACTIONS) + .and(add_state!(self.queue, self.sumeragi,)) + .and(routing::paginate()), + ) + .or(warp::path(uri::CONFIGURATION) + .and(add_state!(self.kiso)) + .and_then(|kiso| async move { + Ok::<_, Infallible>(WarpResult(routing::handle_get_configuration(kiso).await)) + })), + ); + + #[cfg(feature = "telemetry")] + let get_router = get_router + .or(warp::path(uri::STATUS) + .and(add_state!(self.sumeragi.clone())) + .and(warp::header::optional(warp::http::header::ACCEPT.as_str())) + .and(warp::path::tail()) + .and_then(|sumeragi, accept: Option, tail| async move { + Ok::<_, Infallible>(crate::utils::WarpResult(routing::handle_status( + &sumeragi, + accept.as_ref(), + &tail, + ))) + })) + .or(warp::path(uri::METRICS) + .and(add_state!(self.sumeragi)) + .and_then(|sumeragi| async move { + Ok::<_, Infallible>(crate::utils::WarpResult(routing::handle_metrics( + &sumeragi, + ))) + })) + .or(warp::path(uri::API_VERSION) + .and(add_state!(self.sumeragi.clone())) + .and_then(|sumeragi| async { + Ok::<_, Infallible>(routing::handle_version(sumeragi).await) + })); + + #[cfg(feature = "schema")] + let get_router = get_router.or(warp::path(uri::SCHEMA) + .and_then(|| async { Ok::<_, Infallible>(routing::handle_schema().await) })); + + let post_router = warp::post() + .and( + endpoint3( + routing::handle_transaction, + warp::path(uri::TRANSACTION) + .and(add_state!(self.queue, self.sumeragi)) + .and(warp::body::content_length_limit( + self.transaction_max_content_length, + )) + .and(body::versioned()), + ) + .or(endpoint3( + routing::handle_queries, + warp::path(uri::QUERY) + .and(add_state!(self.query_service, self.sumeragi,)) + .and(routing::client_query_request()), + )) + .or(endpoint2( + routing::handle_post_configuration, + warp::path(uri::CONFIGURATION) + .and(add_state!(self.kiso)) + .and(warp::body::json()), + )), + ) + .recover(|rejection| async move { body::recover_versioned(rejection) }); + + let events_ws_router = warp::path(uri::SUBSCRIPTION) + .and(add_state!(self.events)) + .and(warp::ws()) + .map(|events, ws: Ws| { + ws.on_upgrade(|this_ws| async move { + if let Err(error) = + routing::subscription::handle_subscription(events, this_ws).await + { + iroha_logger::error!(%error, "Failure during subscription"); + } + }) + }); + + // `warp` panics if there is `/` in the string given to the `warp::path` filter + // Path filter has to be boxed to have a single uniform type during iteration + let block_ws_router_path = uri::BLOCKS_STREAM + .split('/') + .skip_while(|p| p.is_empty()) + .fold(warp::any().boxed(), |path_filter, path| { + path_filter.and(warp::path(path)).boxed() + }); + + let blocks_ws_router = block_ws_router_path + .and(add_state!(self.kura)) + .and(warp::ws()) + .map(|sumeragi: Arc<_>, ws: Ws| { + ws.on_upgrade(|this_ws| async move { + if let Err(error) = routing::handle_blocks_stream(sumeragi, this_ws).await { + iroha_logger::error!(%error, "Failed to subscribe to blocks stream"); + } + }) + }); + + let ws_router = events_ws_router.or(blocks_ws_router); + + warp::any() + .and( + // we want to avoid logging for the "health" endpoint. + // we have to place it **first** so that warp's trace will + // not log 404 if it doesn't find "/health" which might be placed + // **after** `.with(trace)` + health_route, + ) + .or(ws_router + .or(get_router) + .or(post_router) + .with(warp::trace::request())) + } + + /// Start main api endpoints. + /// + /// # Errors + /// Can fail due to listening to network or if http server fails + fn start_api(self: Arc) -> eyre::Result>> { + let torii_address = &self.address; + + let mut handles = vec![]; + match torii_address.to_socket_addrs() { + Ok(addrs) => { + for addr in addrs { + let torii = Arc::clone(&self); + + let api_router = torii.create_api_router(); + let signal_fut = async move { torii.notify_shutdown.notified().await }; + let (_, serve_fut) = + warp::serve(api_router).bind_with_graceful_shutdown(addr, signal_fut); + + handles.push(task::spawn(serve_fut)); + } + + Ok(handles) + } + Err(error) => { + iroha_logger::error!(%torii_address, %error, "API address configuration parse error"); + Err(eyre::Error::new(error)) + } + } + } + + /// To handle incoming requests `Torii` should be started first. + /// + /// # Errors + /// Can fail due to listening to network or if http server fails + #[iroha_futures::telemetry_future] + pub async fn start(self) -> eyre::Result<()> { + let torii = Arc::new(self); + let mut handles = vec![]; + + handles.extend(Arc::clone(&torii).start_api()?); + + handles + .into_iter() + .collect::>() + .for_each(|handle| { + if let Err(error) = handle { + iroha_logger::error!(%error, "Join handle error"); + } + + futures::future::ready(()) + }) + .await; + + Ok(()) + } +} + +/// Torii errors. +#[derive(Debug, thiserror::Error, displaydoc::Display)] +pub enum Error { + /// Failed to process query + Query(#[from] iroha_data_model::ValidationFail), + /// Failed to accept transaction + AcceptTransaction(#[from] iroha_core::tx::AcceptTransactionFail), + /// Error while getting or setting configuration + Config(#[source] eyre::Report), + /// Failed to push into queue + PushIntoQueue(#[from] Box), + #[cfg(feature = "telemetry")] + /// Error while getting Prometheus metrics + Prometheus(#[source] eyre::Report), + #[cfg(feature = "telemetry")] + /// Internal error while getting status + StatusFailure(#[source] eyre::Report), + /// Failure caused by configuration subsystem + ConfigurationFailure(#[from] KisoError), + /// Cannot find status segment by provided path + StatusSegmentNotFound(#[source] eyre::Report), +} + +impl Reply for Error { + fn into_response(self) -> Response { + match self { + Self::Query(err) => { + reply::with_status(utils::Scale(&err), Self::query_status_code(&err)) + .into_response() + } + _ => reply::with_status(Self::to_string(&self), self.status_code()).into_response(), + } + } +} + +impl Error { + fn status_code(&self) -> StatusCode { + use Error::*; + + match self { + Query(e) => Self::query_status_code(e), + AcceptTransaction(_) => StatusCode::BAD_REQUEST, + Config(_) | StatusSegmentNotFound(_) => StatusCode::NOT_FOUND, + PushIntoQueue(err) => match **err { + queue::Error::Full => StatusCode::INTERNAL_SERVER_ERROR, + queue::Error::SignatureCondition { .. } => StatusCode::UNAUTHORIZED, + _ => StatusCode::BAD_REQUEST, + }, + #[cfg(feature = "telemetry")] + Prometheus(_) | StatusFailure(_) => StatusCode::INTERNAL_SERVER_ERROR, + ConfigurationFailure(_) => StatusCode::INTERNAL_SERVER_ERROR, + } + } + + fn query_status_code(validation_error: &iroha_data_model::ValidationFail) -> StatusCode { + use iroha_data_model::{ + isi::error::InstructionExecutionError, query::error::QueryExecutionFail::*, + ValidationFail::*, + }; + + match validation_error { + NotPermitted(_) => StatusCode::FORBIDDEN, + QueryFailed(query_error) + | InstructionFailed(InstructionExecutionError::Query(query_error)) => match query_error + { + Conversion(_) | UnknownCursor | FetchSizeTooBig => StatusCode::BAD_REQUEST, + Signature(_) => StatusCode::UNAUTHORIZED, + Find(_) => StatusCode::NOT_FOUND, + }, + TooComplex => StatusCode::UNPROCESSABLE_ENTITY, + InternalError(_) => StatusCode::INTERNAL_SERVER_ERROR, + InstructionFailed(error) => { + iroha_logger::error!( + ?error, + "Query validation failed with unexpected error. This means a bug inside Runtime Executor", + ); + StatusCode::INTERNAL_SERVER_ERROR + } + } + } + + fn to_string(err: &dyn std::error::Error) -> String { + let mut s = "Error:\n".to_owned(); + let mut idx = 0_i32; + let mut err_opt = Some(err); + while let Some(e) = err_opt { + write!(s, " {idx}: {}", &e.to_string()).expect("Valid"); + idx += 1_i32; + err_opt = e.source() + } + s + } +} + +/// Result type +pub type Result = std::result::Result; diff --git a/cli/src/torii/routing.rs b/torii/src/routing.rs similarity index 57% rename from cli/src/torii/routing.rs rename to torii/src/routing.rs index 6a9298974ce..baf083998b6 100644 --- a/cli/src/torii/routing.rs +++ b/torii/src/routing.rs @@ -5,9 +5,10 @@ // FIXME: This can't be fixed, because one trait in `warp` is private. #![allow(opaque_hidden_inferred_bound)] +#[cfg(feature = "telemetry")] use eyre::{eyre, WrapErr}; use futures::TryStreamExt; -use iroha_config::{client_api::ConfigurationDTO, torii::uri}; +use iroha_config::client_api::ConfigurationDTO; use iroha_core::{ query::{pagination::Paginate, store::LiveQueryStoreHandle}, smartcontracts::query::ValidQueryRequest, @@ -33,7 +34,7 @@ use super::*; use crate::stream::{Sink, Stream}; /// Filter for warp which extracts [`http::ClientQueryRequest`] -fn client_query_request( +pub fn client_query_request( ) -> impl warp::Filter + Copy { body::versioned::() .and(sorting()) @@ -64,7 +65,7 @@ fn cursor() -> impl warp::Filter impl warp::Filter + Copy { +pub fn paginate() -> impl warp::Filter + Copy { warp::query() } @@ -74,7 +75,7 @@ fn fetch_size() -> impl warp::Filter, sumeragi: SumeragiHandle, transaction: SignedTransaction, @@ -98,13 +99,13 @@ async fn handle_transaction( } #[iroha_futures::telemetry_future] -async fn handle_queries( +pub async fn handle_queries( live_query_store: LiveQueryStoreHandle, sumeragi: SumeragiHandle, query_request: http::ClientQueryRequest, ) -> Result>> { - let handle = tokio::task::spawn_blocking(move || match query_request.0 { + let handle = task::spawn_blocking(move || match query_request.0 { QueryRequest::Query(QueryWithParameters { query: signed_query, sorting, @@ -134,18 +135,18 @@ enum Health { Healthy, } -fn handle_health() -> Json { +pub fn handle_health() -> Json { reply::json(&Health::Healthy) } #[iroha_futures::telemetry_future] -#[cfg(feature = "schema-endpoint")] -async fn handle_schema() -> Json { +#[cfg(feature = "schema")] +pub async fn handle_schema() -> Json { reply::json(&iroha_schema_gen::build_schemas()) } #[iroha_futures::telemetry_future] -async fn handle_pending_transactions( +pub async fn handle_pending_transactions( queue: Arc, sumeragi: SumeragiHandle, pagination: Pagination, @@ -164,13 +165,13 @@ async fn handle_pending_transactions( } #[iroha_futures::telemetry_future] -async fn handle_get_configuration(kiso: KisoHandle) -> Result { +pub async fn handle_get_configuration(kiso: KisoHandle) -> Result { let dto = kiso.get_dto().await?; Ok(reply::json(&dto)) } #[iroha_futures::telemetry_future] -async fn handle_post_configuration( +pub async fn handle_post_configuration( kiso: KisoHandle, value: ConfigurationDTO, ) -> Result { @@ -179,7 +180,7 @@ async fn handle_post_configuration( } #[iroha_futures::telemetry_future] -async fn handle_blocks_stream(kura: Arc, mut stream: WebSocket) -> eyre::Result<()> { +pub async fn handle_blocks_stream(kura: Arc, mut stream: WebSocket) -> eyre::Result<()> { let BlockSubscriptionRequest(mut from_height) = stream.recv().await?; let mut interval = tokio::time::interval(std::time::Duration::from_millis(10)); @@ -221,7 +222,7 @@ async fn handle_blocks_stream(kura: Arc, mut stream: WebSocket) -> eyre::R } } -mod subscription { +pub mod subscription { //! Contains the `handle_subscription` functions and used for general routing. use super::*; @@ -303,7 +304,7 @@ mod subscription { #[iroha_futures::telemetry_future] #[cfg(feature = "telemetry")] -async fn handle_version(sumeragi: SumeragiHandle) -> Json { +pub async fn handle_version(sumeragi: SumeragiHandle) -> Json { use iroha_version::Version; let string = sumeragi @@ -315,7 +316,7 @@ async fn handle_version(sumeragi: SumeragiHandle) -> Json { } #[cfg(feature = "telemetry")] -fn handle_metrics(sumeragi: &SumeragiHandle) -> Result { +pub fn handle_metrics(sumeragi: &SumeragiHandle) -> Result { if let Err(error) = sumeragi.update_metrics() { iroha_logger::error!(%error, "Error while calling sumeragi::update_metrics."); } @@ -333,7 +334,7 @@ fn update_metrics_gracefully(sumeragi: &SumeragiHandle) { #[cfg(feature = "telemetry")] #[allow(clippy::unnecessary_wraps)] -fn handle_status( +pub fn handle_status( sumeragi: &SumeragiHandle, accept: Option>, tail: &warp::path::Tail, @@ -370,206 +371,3 @@ fn handle_status( Ok(reply) } } - -impl Torii { - /// Construct `Torii`. - #[allow(clippy::too_many_arguments)] - pub fn new( - kiso: KisoHandle, - config: &ToriiConfiguration, - queue: Arc, - events: EventsSender, - notify_shutdown: Arc, - sumeragi: SumeragiHandle, - query_service: LiveQueryStoreHandle, - kura: Arc, - ) -> Self { - Self { - kiso, - queue, - events, - notify_shutdown, - sumeragi, - query_service, - kura, - address: config.api_url.clone(), - transaction_max_content_length: config.max_content_len.into(), - } - } - - /// Helper function to create router. This router can tested without starting up an HTTP server - #[allow(clippy::too_many_lines)] - fn create_api_router(&self) -> impl warp::Filter + Clone + Send { - let health_route = warp::get() - .and(warp::path(uri::HEALTH)) - .and_then(|| async { Ok::<_, Infallible>(handle_health()) }); - - let get_router = warp::get().and( - endpoint3( - handle_pending_transactions, - warp::path(uri::PENDING_TRANSACTIONS) - .and(add_state!(self.queue, self.sumeragi,)) - .and(paginate()), - ) - .or(warp::path(uri::CONFIGURATION) - .and(add_state!(self.kiso)) - .and_then(|kiso| async move { - Ok::<_, Infallible>(WarpResult(handle_get_configuration(kiso).await)) - })), - ); - - let get_router_status = warp::path(uri::STATUS) - .and(add_state!(self.sumeragi.clone())) - .and(warp::header::optional(warp::http::header::ACCEPT.as_str())) - .and(warp::path::tail()) - .and_then(|sumeragi, accept: Option, tail| async move { - Ok::<_, Infallible>(WarpResult(handle_status(&sumeragi, accept.as_ref(), &tail))) - }); - let get_router_metrics = warp::path(uri::METRICS) - .and(add_state!(self.sumeragi)) - .and_then(|sumeragi| async move { - Ok::<_, Infallible>(WarpResult(handle_metrics(&sumeragi))) - }); - let get_api_version = warp::path(uri::API_VERSION) - .and(add_state!(self.sumeragi.clone())) - .and_then(|sumeragi| async { Ok::<_, Infallible>(handle_version(sumeragi).await) }); - - #[cfg(feature = "telemetry")] - let get_router = get_router.or(warp::any() - .and(get_router_status) - .or(get_router_metrics) - .or(get_api_version)); - - #[cfg(feature = "schema-endpoint")] - let get_router = get_router.or(warp::path(uri::SCHEMA) - .and_then(|| async { Ok::<_, Infallible>(handle_schema().await) })); - - let post_router = warp::post() - .and( - endpoint3( - handle_transaction, - warp::path(uri::TRANSACTION) - .and(add_state!(self.queue, self.sumeragi)) - .and(warp::body::content_length_limit( - self.transaction_max_content_length, - )) - .and(body::versioned()), - ) - .or(endpoint3( - handle_queries, - warp::path(uri::QUERY) - .and(add_state!(self.query_service, self.sumeragi,)) - .and(client_query_request()), - )) - .or(endpoint2( - handle_post_configuration, - warp::path(uri::CONFIGURATION) - .and(add_state!(self.kiso)) - .and(warp::body::json()), - )), - ) - .recover(|rejection| async move { body::recover_versioned(rejection) }); - - let events_ws_router = warp::path(uri::SUBSCRIPTION) - .and(add_state!(self.events)) - .and(warp::ws()) - .map(|events, ws: Ws| { - ws.on_upgrade(|this_ws| async move { - if let Err(error) = subscription::handle_subscription(events, this_ws).await { - iroha_logger::error!(%error, "Failure during subscription"); - } - }) - }); - - // `warp` panics if there is `/` in the string given to the `warp::path` filter - // Path filter has to be boxed to have a single uniform type during iteration - let block_ws_router_path = uri::BLOCKS_STREAM - .split('/') - .skip_while(|p| p.is_empty()) - .fold(warp::any().boxed(), |path_filter, path| { - path_filter.and(warp::path(path)).boxed() - }); - - let blocks_ws_router = block_ws_router_path - .and(add_state!(self.kura)) - .and(warp::ws()) - .map(|sumeragi: Arc<_>, ws: Ws| { - ws.on_upgrade(|this_ws| async move { - if let Err(error) = handle_blocks_stream(sumeragi, this_ws).await { - iroha_logger::error!(%error, "Failed to subscribe to blocks stream"); - } - }) - }); - - let ws_router = events_ws_router.or(blocks_ws_router); - - warp::any() - .and( - // we want to avoid logging for the "health" endpoint. - // we have to place it **first** so that warp's trace will - // not log 404 if it doesn't find "/health" which might be placed - // **after** `.with(trace)` - health_route, - ) - .or(ws_router - .or(get_router) - .or(post_router) - .with(warp::trace::request())) - } - - /// Start main api endpoints. - /// - /// # Errors - /// Can fail due to listening to network or if http server fails - fn start_api(self: Arc) -> eyre::Result>> { - let torii_address = &self.address; - - let mut handles = vec![]; - match torii_address.to_socket_addrs() { - Ok(addrs) => { - for addr in addrs { - let torii = Arc::clone(&self); - - let api_router = torii.create_api_router(); - let signal_fut = async move { torii.notify_shutdown.notified().await }; - let (_, serve_fut) = - warp::serve(api_router).bind_with_graceful_shutdown(addr, signal_fut); - - handles.push(task::spawn(serve_fut)); - } - - Ok(handles) - } - Err(error) => { - iroha_logger::error!(%torii_address, %error, "API address configuration parse error"); - Err(eyre::Error::new(error)) - } - } - } - - /// To handle incoming requests `Torii` should be started first. - /// - /// # Errors - /// Can fail due to listening to network or if http server fails - #[iroha_futures::telemetry_future] - pub(crate) async fn start(self) -> eyre::Result<()> { - let torii = Arc::new(self); - let mut handles = vec![]; - - handles.extend(Arc::clone(&torii).start_api()?); - - handles - .into_iter() - .collect::>() - .for_each(|handle| { - if let Err(error) = handle { - iroha_logger::error!(%error, "Join handle error"); - } - - futures::future::ready(()) - }) - .await; - - Ok(()) - } -} diff --git a/cli/src/stream.rs b/torii/src/stream.rs similarity index 100% rename from cli/src/stream.rs rename to torii/src/stream.rs diff --git a/cli/src/torii/utils.rs b/torii/src/utils.rs similarity index 97% rename from cli/src/torii/utils.rs rename to torii/src/utils.rs index 6a5e2432fa6..805917872bb 100644 --- a/cli/src/torii/utils.rs +++ b/torii/src/utils.rs @@ -81,4 +81,4 @@ impl Reply for WarpResult { } } -iroha_cli_derive::generate_endpoints!(2, 3, 4, 5, 6, 7); +iroha_torii_macro::generate_endpoints!(2, 3, 4, 5, 6, 7); From 50ec2bc71821d81d05b2f4a7b9746c84166db025 Mon Sep 17 00:00:00 2001 From: BAStos525 Date: Wed, 13 Dec 2023 15:09:15 +0300 Subject: [PATCH 12/21] [ci]: Use default settings for Coveralls action Signed-off-by: BAStos525 --- .github/workflows/iroha2-dev-pr.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/iroha2-dev-pr.yml b/.github/workflows/iroha2-dev-pr.yml index 9ba479df65d..4ddc04e98e8 100644 --- a/.github/workflows/iroha2-dev-pr.yml +++ b/.github/workflows/iroha2-dev-pr.yml @@ -66,8 +66,7 @@ jobs: uses: coverallsapp/github-action@v2 with: file: lcov.info - compare-ref: ${{ github.base_ref }} - compare-sha: ${{ github.event.pull_request.base.sha}} + git-branch: ${{ github.base_ref }} github-token: ${{ secrets.GITHUB_TOKEN }} allow-empty: true fail_ci_if_error: true From 54e65c554b6c0614659a50082d2d60aee139210e Mon Sep 17 00:00:00 2001 From: 0x009922 <43530070+0x009922@users.noreply.github.com> Date: Fri, 22 Dec 2023 08:33:19 +0000 Subject: [PATCH 13/21] [refactor] #4029, #4105, #4136: refactor CLIs (#4153) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [refactor]: refactor cli(s) - `iroha`: use `clap` `v4` - `iroha`: update CLI (`--config`, `--terminal-colors`) - `iroha`: handle user-provided config file strictly - `iroha`: parse ENV config after file config - `iroha_config`: add `genesis.file`, remove `account_*` prefix - `iroha_config`: move some genesis config validation logic from `iroha` - `iroha_client_cli`: upgrade `clap` to `v4` - `iroha_genesis`: remove side effects (bail, logs) - `iroha_genesis`: clarify validation logic (#3075) - `iroha_genesis`: remove `pub` to guard invariant of `GenesisNetwork` Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [refactor]: chore Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [fix]: fix & test config - `iroha_config`: use `PathBuf` when appropriate - `iroha_config`: fix `Path` behaviour - `iroha_config`: finally, make `disable_panic_terminal_colors` truly optional (#3506) - `iroha`: resolve all relative paths in config file - `iroha`: improve config error messages - `iroha`: add config integration tests Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [test]: test & refactor - `iroha_client_cli`: adopt `--config` from `iroha` - `iroha_client_cli`: refactor `--metadata` arg - `iroha`: extend tests - update test code overall Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [fix]: clippy, tests Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [fix]: use `serde_as_str` for paths Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [chore]: re-generate peer config Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [test]: update `test_env.py` Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [test]: remove deprecated option from test config Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [docs]: update README Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [chore]: remove obsolete comment Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [fix]: update `iroha_swarm` Now it relies on that `genesis.file` is set in the config file Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [feat]: `kagami config peer` + `--genesis-file-in-config` Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [refactor]: chores Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Ekaterina Mekhnetsova Signed-off-by: 0x009922 <43530070+0x009922@users.noreply.github.com> * [feat]: use `PATH` as value name Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [docs]: align `StatusFailure` message Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [docs]: adopt suggestion in `iroha_client_cli` https://github.com/hyperledger/iroha/pull/4153#discussion_r1427824253 Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [build]: exclude `genesis.file` from default config Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [feat]: refine genesis config parse error Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [test]: fix & refactor `test_env.py` Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [test]: fix CANNOT_BE_EMPTY message It is updated with a new version of clap Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [test]: return `disable_panic_terminal_colors: null` to test config It is needed so that only `iroha_test_config.json` without defaults layer is valid (will fix on config refactoring) Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [fix]: handle configuration properly in `iroha_swarm` - set `GENESIS_FILE` in env, since `config.json` doesn't have it - strictly specify `config.json` and `genesis.json` files Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: ⭐️NINIKA⭐️ Signed-off-by: 0x009922 <43530070+0x009922@users.noreply.github.com> * Update client_cli/src/main.rs Co-authored-by: ⭐️NINIKA⭐️ Signed-off-by: 0x009922 <43530070+0x009922@users.noreply.github.com> * [refactor]: eliminate extra `iroha_config` dependency Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [refactor]: remove dead code Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [test]: use `serde_json::json!` Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [refactor]: remove `disable_panic_terminal_colors` parameter Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [refactor]: update genesis & its testing - remove `cfg(test)` behaviour from `GenesisNetwork::new()` - refactor `impl TestGenesis for Genesis` - remove extra `bool` + `Option` invariant - allow creating genesis with additional ISIs - remove `test-utils` feature & `GenesisNetwork::transactions_mut()` - update related tests - update error message on signing failure Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [refactor]: remove extension check from `Path::default()` Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [docs]: update genesis topology messages Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [refactor]: use `Popen`'s `env` instead of global `os.environ` Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [fix]: typo Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [refactor]: use `Partial`/`Full` terminology Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [test]: inherit env from OS Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [docs]: link issue #4161 Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [fix]: format Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [refactor]: apply lints Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [feat]: panic on default path extension Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [test]: update err message Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [docs]: update docs of `read_config` Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --------- Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> Signed-off-by: 0x009922 <43530070+0x009922@users.noreply.github.com> Co-authored-by: Ekaterina Mekhnetsova Co-authored-by: ⭐️NINIKA⭐️ --- Cargo.lock | 95 ++--- README.md | 17 +- cli/Cargo.toml | 6 + cli/src/lib.rs | 364 +++++++++++++----- cli/src/main.rs | 342 +++++++--------- cli/src/samples.rs | 7 +- cli/src/style.rs | 67 ---- client/benches/torii.rs | 19 +- client/examples/million_accounts_genesis.rs | 18 +- client/tests/integration/permissions.rs | 15 +- .../integration/triggers/by_call_trigger.rs | 9 +- client_cli/Cargo.toml | 4 +- client_cli/pytests/common/consts.py | 2 +- client_cli/src/main.rs | 357 +++++++++-------- config/Cargo.toml | 2 +- config/iroha_test_config.json | 8 +- config/src/genesis.rs | 85 +++- config/src/iroha.rs | 10 +- config/src/kura.rs | 5 +- config/src/path.rs | 147 ++++--- config/src/snapshot.rs | 9 +- configs/peer/config.json | 6 +- core/src/snapshot.rs | 2 +- core/src/sumeragi/main_loop.rs | 2 +- core/test_network/src/lib.rs | 39 +- docker-compose.dev.local.yml | 15 +- docker-compose.dev.single.yml | 6 +- docker-compose.dev.yml | 15 +- genesis/Cargo.toml | 3 - genesis/src/lib.rs | 96 ++--- scripts/test_env.py | 73 ++-- tools/kagami/src/config.rs | 24 +- tools/swarm/src/cli.rs | 2 +- tools/swarm/src/compose.rs | 66 +++- torii/src/lib.rs | 10 +- 35 files changed, 1046 insertions(+), 901 deletions(-) delete mode 100644 cli/src/style.rs diff --git a/Cargo.lock b/Cargo.lock index 1ff201d4026..d49f0ccdacd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -178,6 +178,12 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" +[[package]] +name = "assertables" +version = "7.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c24e9d990669fbd16806bff449e4ac644fd9b1fca014760087732fe4102f131" + [[package]] name = "async-stream" version = "0.3.5" @@ -575,23 +581,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "clap" -version = "3.2.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" -dependencies = [ - "atty", - "bitflags 1.3.2", - "clap_derive 3.2.25", - "clap_lex 0.2.4", - "indexmap 1.9.3", - "once_cell", - "strsim", - "termcolor", - "textwrap", -] - [[package]] name = "clap" version = "4.4.11" @@ -599,7 +588,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfaff671f6b22ca62406885ece523383b9b64022e341e53e009a62ebc47a45f2" dependencies = [ "clap_builder", - "clap_derive 4.4.7", + "clap_derive", ] [[package]] @@ -610,23 +599,10 @@ checksum = "a216b506622bb1d316cd51328dce24e07bdff4a6128a47c7e7fad11878d5adbb" dependencies = [ "anstream", "anstyle", - "clap_lex 0.6.0", + "clap_lex", "strsim", ] -[[package]] -name = "clap_derive" -version = "3.2.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae6371b8bdc8b7d3959e9cf7b22d4435ef3e79e138688421ec654acf8c81b008" -dependencies = [ - "heck", - "proc-macro-error", - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "clap_derive" version = "4.4.7" @@ -639,15 +615,6 @@ dependencies = [ "syn 2.0.41", ] -[[package]] -name = "clap_lex" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" -dependencies = [ - "os_str_bytes", -] - [[package]] name = "clap_lex" version = "0.6.0" @@ -765,9 +732,9 @@ dependencies = [ [[package]] name = "const-oid" -version = "0.9.5" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28c122c3980598d243d63d9a704629a2d748d101f278052ff068be5a4423ab6f" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" [[package]] name = "core-foundation" @@ -939,7 +906,7 @@ dependencies = [ "anes", "cast", "ciborium", - "clap 4.4.11", + "clap", "criterion-plot", "is-terminal", "itertools 0.10.5", @@ -2610,8 +2577,11 @@ dependencies = [ name = "iroha" version = "2.0.0-pre-rc.20" dependencies = [ + "assertables", + "clap", "color-eyre", "eyre", + "futures", "iroha_config", "iroha_core", "iroha_crypto", @@ -2626,8 +2596,11 @@ dependencies = [ "iroha_wasm_builder", "once_cell", "owo-colors", + "path-absolutize", + "serde_json", "serial_test", "supports-color 2.1.0", + "tempfile", "thread-local-panic-hook", "tokio", "tracing", @@ -2678,11 +2651,12 @@ dependencies = [ name = "iroha_client_cli" version = "2.0.0-pre-rc.20" dependencies = [ - "clap 3.2.25", + "clap", "color-eyre", "dialoguer", "erased-serde", "iroha_client", + "iroha_config_base", "iroha_primitives", "json5", "once_cell", @@ -2702,10 +2676,10 @@ dependencies = [ "iroha_config_base", "iroha_crypto", "iroha_data_model", + "iroha_genesis", "iroha_primitives", "json5", "once_cell", - "path-absolutize", "proptest", "serde", "serde_json", @@ -2978,11 +2952,8 @@ version = "2.0.0-pre-rc.20" dependencies = [ "derive_more", "eyre", - "iroha_config", "iroha_crypto", "iroha_data_model", - "iroha_logger", - "iroha_primitives", "iroha_schema", "once_cell", "serde", @@ -3161,7 +3132,7 @@ version = "2.0.0-pre-rc.20" name = "iroha_swarm" version = "2.0.0-pre-rc.20" dependencies = [ - "clap 4.4.11", + "clap", "color-eyre", "derive_more", "expect-test", @@ -3321,7 +3292,7 @@ dependencies = [ name = "iroha_wasm_builder_cli" version = "2.0.0-pre-rc.20" dependencies = [ - "clap 4.4.11", + "clap", "color-eyre", "iroha_wasm_builder", "owo-colors", @@ -3454,7 +3425,7 @@ dependencies = [ name = "kagami" version = "2.0.0-pre-rc.20" dependencies = [ - "clap 4.4.11", + "clap", "color-eyre", "derive_more", "iroha_config", @@ -3481,7 +3452,7 @@ dependencies = [ name = "kura_inspector" version = "2.0.0-pre-rc.20" dependencies = [ - "clap 4.4.11", + "clap", "iroha_core", "iroha_data_model", "iroha_version", @@ -3924,12 +3895,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "os_str_bytes" -version = "6.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1" - [[package]] name = "overload" version = "0.1.1" @@ -3975,7 +3940,7 @@ dependencies = [ name = "parity_scale_decoder" version = "2.0.0-pre-rc.20" dependencies = [ - "clap 4.4.11", + "clap", "colored", "eyre", "iroha_crypto", @@ -5360,12 +5325,6 @@ dependencies = [ "unique_port", ] -[[package]] -name = "textwrap" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" - [[package]] name = "thiserror" version = "1.0.50" @@ -5888,9 +5847,9 @@ dependencies = [ [[package]] name = "unsafe-libyaml" -version = "0.2.9" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f28467d3e1d3c6586d8f25fa243f544f5800fec42d97032474e17222c2b75cfa" +checksum = "ab4c90930b95a82d00dc9e9ac071b4991924390d46cbd0dfe566148667605e4b" [[package]] name = "untrusted" diff --git a/README.md b/README.md index d93882849bc..98f41678038 100644 --- a/README.md +++ b/README.md @@ -157,7 +157,18 @@ A brief overview on how to configure and maintain an Iroha instance: ### Configuration -**Note:** this section is under development. You can track it in the [issue](https://github.com/hyperledger/iroha-2-docs/issues/392). +There is a set of configuration parameters that could be passed either through a configuration file or environment variables. + +```shell +# look for `config.json` or `config.json5` (won't fail if files are not found) +iroha + +# Override default config path through CLI or ENV +iroha --config /path/to/config.json +IROHA_CONFIG=/path/to/config.json iroha +``` + +**Note:** detailed configuration reference is [work in progress](https://github.com/hyperledger/iroha-2-docs/issues/392). ### Endpoints @@ -198,9 +209,9 @@ prometheus --config.file=configs/prometheus.yml ### Storage -The blocks are written to the `blocks` sub-folder, which is created automatically by Iroha in the working directory of the peer. Additionally, if specified, the logging file must also be stored in a user-specified directory. +Iroha stores blocks and snapshots in the `storage` directory, which is created automatically by Iroha in the working directory of the peer. If `kura.block_store_path` is specified in the config file, it overrides the default one and is resolved relative to the config file location. -No additional storage is necessary. +**Note:** detailed configuration reference is [work in progress](https://github.com/hyperledger/iroha-2-docs/issues/392). ### Scalability diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 0ca76872233..8507b7d77ef 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -55,6 +55,7 @@ iroha_torii = { workspace = true } iroha_genesis = { workspace = true } iroha_wasm_builder = { workspace = true } +clap = { workspace = true, features = ["derive", "env", "string"] } color-eyre = { workspace = true } eyre = { workspace = true } tracing = { workspace = true } @@ -67,6 +68,11 @@ thread-local-panic-hook = { version = "0.1.0", optional = true } [dev-dependencies] serial_test = "2.0.0" +tempfile = { workspace = true } +serde_json = { workspace = true } +futures = { workspace = true } +path-absolutize = { workspace = true } +assertables = "7" [build-dependencies] iroha_wasm_builder = { workspace = true } diff --git a/cli/src/lib.rs b/cli/src/lib.rs index 53ee9fe3306..b63e0880efd 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -6,13 +6,14 @@ //! should be constructed externally: (see `main.rs`). #[cfg(debug_assertions)] use core::sync::atomic::{AtomicBool, Ordering}; -use std::sync::Arc; +use std::{path::PathBuf, sync::Arc}; use color_eyre::eyre::{eyre, Result, WrapErr}; use iroha_config::{ base::proxy::{LoadFromDisk, LoadFromEnv, Override}, + genesis::ParsedConfiguration as ParsedGenesisConfiguration, iroha::{Configuration, ConfigurationProxy}, - path::Path as ConfigPath, + path::Path, telemetry::Configuration as TelemetryConfiguration, }; use iroha_core::{ @@ -40,58 +41,8 @@ use tokio::{ task, }; +// FIXME: move from CLI pub mod samples; -pub mod style; - -/// Arguments for Iroha2. Configuration for arguments is parsed from -/// environment variables and then the appropriate object is -/// constructed. -#[derive(Debug)] -pub struct Arguments { - /// Set this flag on the peer that should submit genesis on the network initial start. - pub submit_genesis: bool, - /// Set custom genesis file path. `None` if `submit_genesis` set to `false`. - pub genesis_path: Option, - /// Set custom config file path. - pub config_path: ConfigPath, -} - -/// Default configuration path -static CONFIGURATION_PATH: once_cell::sync::Lazy<&'static std::path::Path> = - once_cell::sync::Lazy::new(|| std::path::Path::new("config")); - -/// Default genesis path -static GENESIS_PATH: once_cell::sync::Lazy<&'static std::path::Path> = - once_cell::sync::Lazy::new(|| std::path::Path::new("genesis")); - -impl Default for Arguments { - fn default() -> Self { - Self { - submit_genesis: false, - genesis_path: Some(ConfigPath::default(&GENESIS_PATH)), - config_path: ConfigPath::default(&CONFIGURATION_PATH), - } - } -} - -/// Reflects user decision (or its absence) about ANSI colored output -#[derive(Copy, Clone, Debug)] -pub enum TerminalColorsArg { - /// Coloring should be decided automatically - Default, - /// User explicitly specified the value - UserSet(bool), -} - -impl TerminalColorsArg { - /// Transforms the enumeration into flag - pub fn evaluate(self) -> bool { - match self { - Self::Default => supports_color::on(supports_color::Stream::Stdout).is_some(), - Self::UserSet(x) => x, - } - } -} /// Iroha is an /// [Orchestrator](https://en.wikipedia.org/wiki/Orchestration_%28computing%29) @@ -99,9 +50,8 @@ impl TerminalColorsArg { /// and queries processing, work of consensus and storage. /// /// # Usage -/// Construct and then `start` or `start_as_task`. If you experience -/// an immediate shutdown after constructing Iroha, then you probably -/// forgot this step. +/// Construct and then use [`Iroha::start()`] or [`Iroha::start_as_task()`]. If you experience +/// an immediate shutdown after constructing Iroha, then you probably forgot this step. #[must_use = "run `.start().await?` to not immediately stop Iroha"] pub struct Iroha { /// Actor responsible for the configuration @@ -235,17 +185,20 @@ impl Iroha { })); } - /// Create Iroha with specified broker, config, and genesis. + /// Create new Iroha instance. /// /// # Errors /// - Reading telemetry configs - /// - telemetry setup - /// - Initialization of [`Sumeragi`] + /// - Telemetry setup + /// - Initialization of [`Sumeragi`] and [`Kura`] + /// + /// # Side Effects + /// - Sets global panic hook #[allow(clippy::too_many_lines)] #[iroha_logger::log(name = "init", skip_all)] // This is actually easier to understand as a linear sequence of init statements. - pub async fn with_genesis( - genesis: Option, + pub async fn new( config: Configuration, + genesis: Option, logger: LoggerHandle, ) -> Result { let listen_addr = config.torii.p2p_addr.clone(); @@ -255,7 +208,7 @@ impl Iroha { let (events_sender, _) = broadcast::channel(10000); let world = World::with( - [genesis_domain(config.genesis.account_public_key.clone())], + [genesis_domain(config.genesis.public_key.clone())], config.sumeragi.trusted_peers.peers.clone(), ); @@ -530,56 +483,261 @@ fn genesis_domain(public_key: PublicKey) -> Domain { domain } -/// Combine configuration proxies from several locations, preferring `ENV` vars over config file +macro_rules! mutate_nested_option { + ($obj:expr, self, $func:expr) => { + $obj.as_mut().map($func) + }; + ($obj:expr, $field:ident, $func:expr) => { + $obj.$field.as_mut().map($func) + }; + ($obj:expr, [$field:ident, $($rest:tt)+], $func:expr) => { + $obj.$field.as_mut().map(|x| { + mutate_nested_option!(x, [$($rest)+], $func) + }) + }; + ($obj:tt, [$field:tt], $func:expr) => { + mutate_nested_option!($obj, $field, $func) + }; +} + +/// Read and parse Iroha configuration and genesis block. +/// +/// The pipeline of configuration reading is as follows: +/// +/// 1. Construct a layer with default values +/// 2. If [`Path`] resolves, construct a layer from the file and merge it into the previous one +/// 3. Construct a layer from ENV vars and merge it into the previous one +/// 4. Check whether the final layer contains the complete configuration +/// +/// After reading it, this function ensures validity of genesis configuration and constructs the +/// [`GenesisNetwork`] according to it. /// /// # Errors -/// - if config fails to build -pub fn combine_configs(args: &Arguments) -> color_eyre::eyre::Result { - args.config_path - .first_existing_path() - .map_or_else( - || { - eprintln!("Configuration file not found. Using environment variables as fallback."); - ConfigurationProxy::default() - }, - |path| { - let path_proxy = ConfigurationProxy::from_path(&path.as_path()); - // Override the default to ensure that the variables - // not specified in the config file don't have to be - // explicitly specified in the env. - ConfigurationProxy::default().override_with(path_proxy) - }, - ) - .override_with( - ConfigurationProxy::from_std_env() - .wrap_err("Failed to build configuration from env")?, - ) +/// - If provided user configuration is invalid or incomplete +/// - If genesis config is invalid +pub fn read_config( + path: &Path, + submit_genesis: bool, +) -> Result<(Configuration, Option)> { + let config = ConfigurationProxy::default(); + + let config = if let Some(actual_config_path) = path + .try_resolve() + .wrap_err("Failed to resolve configuration file")? + { + let mut cfg = config.override_with(ConfigurationProxy::from_path(&*actual_config_path)); + let config_dir = actual_config_path + .parent() + .expect("If config file was read, than it should has a parent. It is a bug."); + + // careful here: `genesis.file` might be a path relative to the config file. + // we need to resolve it before proceeding + // TODO: move this logic into `iroha_config` + // https://github.com/hyperledger/iroha/issues/4161 + let join_to_config_dir = |x: &mut PathBuf| { + *x = config_dir.join(&x); + }; + mutate_nested_option!(cfg, [genesis, file, self], join_to_config_dir); + mutate_nested_option!(cfg, [snapshot, dir_path], join_to_config_dir); + mutate_nested_option!(cfg, [kura, block_store_path], join_to_config_dir); + mutate_nested_option!(cfg, [telemetry, file, self], join_to_config_dir); + + cfg + } else { + config + }; + + // it is not chained to the previous expressions so that config proxy from env is evaluated + // after reading a file + let config = config.override_with( + ConfigurationProxy::from_std_env().wrap_err("Failed to build configuration from env")?, + ); + + let config = config .build() - .map_err(Into::into) + .wrap_err("Failed to finalize configuration")?; + + // TODO: move validation logic below to `iroha_config` + + if !submit_genesis && config.sumeragi.trusted_peers.peers.len() < 2 { + return Err(eyre!("\ + The network consists from this one peer only (`sumeragi.trusted_peers` is less than 2). \ + Since `--submit-genesis` is not set, there is no way to receive the genesis block. \ + Either provide the genesis by setting `--submit-genesis` argument, `genesis.private_key`, \ + and `genesis.file` configuration parameters, or increase the number of trusted peers in \ + the network using `sumeragi.trusted_peers` configuration parameter. + ")); + } + + let genesis = if let ParsedGenesisConfiguration::Full { + key_pair, + raw_block, + } = config + .genesis + .clone() + .parse(submit_genesis) + .wrap_err("Invalid genesis configuration")? + { + Some( + GenesisNetwork::new(raw_block, &key_pair) + .wrap_err("Failed to construct the genesis")?, + ) + } else { + None + }; + + Ok((config, genesis)) } -#[cfg(not(feature = "test-network"))] #[cfg(test)] mod tests { - use std::{iter::repeat, panic, thread}; - - use futures::future::join_all; - use serial_test::serial; + use iroha_genesis::RawGenesisBlockBuilder; use super::*; - #[tokio::test] - #[serial] - async fn iroha_should_notify_on_panic() { - let notify = Arc::new(Notify::new()); - let hook = panic::take_hook(); - ::prepare_panic_hook(Arc::clone(¬ify)); - let waiters: Vec<_> = repeat(()).take(10).map(|_| Arc::clone(¬ify)).collect(); - let handles: Vec<_> = waiters.iter().map(|waiter| waiter.notified()).collect(); - thread::spawn(move || { - panic!("Test panic"); - }); - join_all(handles).await; - panic::set_hook(hook); + #[cfg(not(feature = "test-network"))] + mod no_test_network { + use std::{iter::repeat, panic, thread}; + + use futures::future::join_all; + use serial_test::serial; + + use super::*; + + #[tokio::test] + #[serial] + async fn iroha_should_notify_on_panic() { + let notify = Arc::new(Notify::new()); + let hook = panic::take_hook(); + ::prepare_panic_hook(Arc::clone(¬ify)); + let waiters: Vec<_> = repeat(()).take(10).map(|_| Arc::clone(¬ify)).collect(); + let handles: Vec<_> = waiters.iter().map(|waiter| waiter.notified()).collect(); + thread::spawn(move || { + panic!("Test panic"); + }); + join_all(handles).await; + panic::set_hook(hook); + } + } + + mod config_integration { + use assertables::{assert_contains, assert_contains_as_result}; + use iroha_crypto::KeyPair; + use iroha_genesis::{ExecutorMode, ExecutorPath}; + use iroha_primitives::addr::socket_addr; + use path_absolutize::Absolutize as _; + + use super::*; + + fn config_factory() -> Result { + let mut base = ConfigurationProxy::default(); + + let key_pair = KeyPair::generate()?; + + base.public_key = Some(key_pair.public_key().clone()); + base.private_key = Some(key_pair.private_key().clone()); + + let torii = base.torii.as_mut().unwrap(); + torii.p2p_addr = Some(socket_addr!(127.0.0.1:1337)); + torii.api_url = Some(socket_addr!(127.0.0.1:1337)); + + let genesis = base.genesis.as_mut().unwrap(); + genesis.private_key = Some(Some(key_pair.private_key().clone())); + genesis.public_key = Some(key_pair.public_key().clone()); + + Ok(base) + } + + #[test] + fn relative_file_paths_resolution() -> Result<()> { + // Given + + let genesis = RawGenesisBlockBuilder::default() + .executor(ExecutorMode::Path(ExecutorPath("./executor.wasm".into()))) + .build(); + + let config = { + let mut cfg = config_factory()?; + cfg.genesis.as_mut().unwrap().file = Some(Some("./genesis/gen.json".into())); + cfg.kura.as_mut().unwrap().block_store_path = Some("../storage".into()); + cfg.snapshot.as_mut().unwrap().dir_path = Some("../snapshots".into()); + cfg.telemetry.as_mut().unwrap().file = Some(Some("../logs/telemetry".into())); + cfg + }; + + let dir = tempfile::tempdir()?; + let genesis_path = dir.path().join("config/genesis/gen.json"); + let executor_path = dir.path().join("config/genesis/executor.wasm"); + let config_path = dir.path().join("config/config.json5"); + std::fs::create_dir(dir.path().join("config"))?; + std::fs::create_dir(dir.path().join("config/genesis"))?; + std::fs::write(config_path, serde_json::to_string(&config)?)?; + std::fs::write(genesis_path, serde_json::to_string(&genesis)?)?; + std::fs::write(executor_path, "")?; + + let config_path = Path::default(dir.path().join("config/config")); + + // When + + let (config, genesis) = read_config(&config_path, true)?; + + // Then + + // No need to check whether genesis.file is resolved - if not, genesis wouldn't be read + assert!(genesis.is_some()); + + assert_eq!( + config.kura.block_store_path.absolutize()?, + dir.path().join("storage") + ); + assert_eq!( + config.snapshot.dir_path.absolutize()?, + dir.path().join("snapshots") + ); + assert_eq!( + config.telemetry.file.expect("Should be set").absolutize()?, + dir.path().join("logs/telemetry") + ); + + Ok(()) + } + + #[test] + fn fails_with_no_trusted_peers_and_submit_role() -> Result<()> { + // Given + + let genesis = RawGenesisBlockBuilder::default() + .executor(ExecutorMode::Path(ExecutorPath("./executor.wasm".into()))) + .build(); + + let config = { + let mut cfg = config_factory()?; + cfg.genesis.as_mut().unwrap().file = Some(Some("./genesis.json".into())); + cfg + }; + + let dir = tempfile::tempdir()?; + std::fs::write( + dir.path().join("config.json"), + serde_json::to_string(&config)?, + )?; + std::fs::write( + dir.path().join("genesis.json"), + serde_json::to_string(&genesis)?, + )?; + std::fs::write(dir.path().join("executor.wasm"), "")?; + let config_path = Path::user_provided(dir.path().join("config.json"))?; + + // When & Then + + let report = read_config(&config_path, false).unwrap_err(); + + assert_contains!( + format!("{report}"), + "The network consists from this one peer only" + ); + + Ok(()) + } } } diff --git a/cli/src/main.rs b/cli/src/main.rs index 16629ea7ea7..14ef7a587d5 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -1,235 +1,159 @@ //! Iroha peer command-line interface. use std::env; -use color_eyre::eyre::WrapErr as _; -use iroha::{style::Styling, TerminalColorsArg}; -use iroha_config::path::Path as ConfigPath; -use iroha_genesis::{GenesisNetwork, RawGenesisBlock}; -use owo_colors::OwoColorize as _; - -const HELP_ARG: [&str; 2] = ["--help", "-h"]; -const SUBMIT_ARG: [&str; 2] = ["--submit-genesis", "-s"]; -const VERSION_ARG: [&str; 2] = ["--version", "-V"]; -const TERMINAL_COLORS_ARG: &str = "--terminal-colors"; -const NO_TERMINAL_COLORS_ARG: &str = "--no-terminal-colors"; - -const REQUIRED_ENV_VARS: [(&str, &str); 7] = [ - ("IROHA_TORII", "Torii (gateway) endpoint configuration"), - ( - "IROHA_SUMERAGI", - "Sumeragi (emperor) consensus configuration", - ), - ( - "IROHA_KURA", - "Kura (storage). Configuration of block storage ", - ), - ("IROHA_BLOCK_SYNC", "Block synchronisation configuration"), - ("IROHA_PUBLIC_KEY", "Peer public key"), - ("IROHA_PRIVATE_KEY", "Peer private key"), - ("IROHA_GENESIS", "Genesis block configuration"), -]; +use clap::Parser; +use color_eyre::eyre::Result; +use iroha_config::path::Path; -#[tokio::main] -/// To make `Iroha` peer work all actors should be started first. -/// After that moment it you can start it with listening to torii events. -/// -/// # Side effect -/// - Prints welcome message in the log -/// -/// # Errors -/// - Reading genesis from disk -/// - Reading config file -/// - Reading config from `env` -/// - Missing required fields in combined configuration -/// - Telemetry setup -/// - [`Sumeragi`] init -async fn main() -> Result<(), color_eyre::Report> { - let mut args = iroha::Arguments::default(); - - let terminal_colors = env::var("TERMINAL_COLORS") - .ok() - .map(|s| !s.as_str().parse().unwrap_or(true)) - .or_else(|| { - if env::args().any(|a| a == TERMINAL_COLORS_ARG) { - Some(true) - } else if env::args().any(|a| a == NO_TERMINAL_COLORS_ARG) { - Some(false) - } else { - None - } - }) - .map_or(TerminalColorsArg::Default, TerminalColorsArg::UserSet) - .evaluate(); - - if terminal_colors { - color_eyre::install()?; - } +const DEFAULT_CONFIG_PATH: &str = "config"; - let styling = Styling::new(terminal_colors); +fn is_colouring_supported() -> bool { + supports_color::on(supports_color::Stream::Stdout).is_some() +} - if env::args().any(|a| HELP_ARG.contains(&a.as_str())) { - print_help(&styling)?; - return Ok(()); - } +fn default_terminal_colors_str() -> clap::builder::OsStr { + is_colouring_supported().to_string().into() +} - if env::args().any(|a| VERSION_ARG.contains(&a.as_str())) { - print_version(&styling); - return Ok(()); - } +/// Iroha peer Command-Line Interface. +#[derive(Parser, Debug)] +#[command(name = "iroha", version = concat!("version=", env!("CARGO_PKG_VERSION"), " git_commit_sha=", env!("VERGEN_GIT_SHA")), author)] +struct Args { + /// Path to the configuration file, defaults to `config.json`/`config.json5` + /// + /// Supported extensions are `.json` and `.json5`. By default, Iroha looks for a + /// `config` file with one of the supported extensions in the current working directory. + /// If the default config file is not found, Iroha will rely on default values and environment + /// variables. However, if the config path is set explicitly with this argument and the file + /// is not found, Iroha will exit with an error. + #[arg( + long, + short, + env("IROHA_CONFIG"), + value_name("PATH"), + value_parser(Path::user_provided_str), + value_hint(clap::ValueHint::FilePath) + )] + config: Option, + /// Whether to enable ANSI colored output or not + /// + /// By default, Iroha determines whether the terminal supports colors or not. + /// + /// In order to disable this flag explicitly, pass `--terminal-colors=false`. + #[arg( + long, + env, + default_missing_value("true"), + default_value(default_terminal_colors_str()), + action(clap::ArgAction::Set), + require_equals(true), + num_args(0..=1), + )] + terminal_colors: bool, + /// Whether the current peer should submit the genesis block or not + /// + /// Only one peer in the network should submit the genesis block. + /// + /// This argument must be set alongside with `genesis.file` and `genesis.private_key` + /// configuration options. If not, Iroha will exit with an error. + /// + /// In case when the network consists only of this one peer, i.e. the amount of trusted + /// peers in the configuration (`sumeragi.trusted_peers`) is less than 2, this peer must + /// submit the genesis, since there are no other peers who can provide it. In this case, Iroha + /// will exit with an error if `--submit-genesis` is not set. + #[arg(long)] + submit_genesis: bool, +} - if env::args().any(|a| SUBMIT_ARG.contains(&a.as_str())) { - args.submit_genesis = true; - if let Ok(genesis_path) = env::var("IROHA2_GENESIS_PATH") { - args.genesis_path = Some( - ConfigPath::user_provided(&genesis_path) - .wrap_err_with(|| "Required, because `--submit-genesis` was specified.") - .wrap_err_with(|| format!("Could not read `{genesis_path}`"))?, - ); - } - } else { - args.genesis_path = None; - } +#[tokio::main] +async fn main() -> Result<()> { + let args = Args::parse(); - for arg in env::args().skip(1) { - if !arg.is_empty() - && !([HELP_ARG, SUBMIT_ARG] - .iter() - .any(|group| group.contains(&arg.as_str()))) - { - print_help(&styling)?; - eyre::bail!( - "Unrecognised command-line flag `{}`", - arg.style(styling.negative) - ); - } + if args.terminal_colors { + color_eyre::install()?; } - if let Ok(config_path) = env::var("IROHA2_CONFIG_PATH") { - args.config_path = ConfigPath::user_provided(&config_path) - .wrap_err_with(|| format!("Failed to parse `{config_path}` as configuration path"))?; - } - if !args.config_path.exists() { - // Require all the fields defined in default `config.json` - // to be specified as env vars with their respective prefixes - - // TODO: Consider moving these into the - // `iroha::combine_configs` and dependent functions. - for var_name in REQUIRED_ENV_VARS { - // Rather than short circuit and require the person to fix - // the missing env vars one by one, print out the whole - // list of missing environment variables. - let _ = env::var(var_name.0).map_err(|e| { - println!( - "{}: {}", - var_name.0.style(styling.highlight), - e.style(styling.negative) - ); - }); - } - } + let config_path = args + .config + .unwrap_or_else(|| Path::default(DEFAULT_CONFIG_PATH)); + + let (config, genesis) = iroha::read_config(&config_path, args.submit_genesis)?; + let logger = iroha_logger::init_global(&config.logger, args.terminal_colors)?; - let config = iroha::combine_configs(&args)?; - let logger = iroha_logger::init_global(&config.logger, terminal_colors)?; - if !config.disable_panic_terminal_colors { - // FIXME: it shouldn't be logged here; it is a part of configuration domain - // this message can be very simply broken by the changes in the configuration - // https://github.com/hyperledger/iroha/issues/3506 - iroha_logger::warn!("The configuration parameter `DISABLE_PANIC_TERMINAL_COLORS` is deprecated. Set `TERMINAL_COLORS=false` instead. ") - } iroha_logger::info!( - version = %env!("CARGO_PKG_VERSION"), git_commit_sha = env!("VERGEN_GIT_SHA"), + version = env!("CARGO_PKG_VERSION"), + git_commit_sha = env!("VERGEN_GIT_SHA"), "Hyperledgerいろは2にようこそ!(translation) Welcome to Hyperledger Iroha!" ); - assert!(args.submit_genesis || config.sumeragi.trusted_peers.peers.len() > 1, - "Only peer in network, yet required to receive genesis topology. This is a configuration error." - ); + if genesis.is_some() { + iroha_logger::debug!("Submitting genesis."); + } - let genesis = args - .submit_genesis - .then_some(()) - .and(args.genesis_path) - .map(|genesis_path| { - let genesis_path = genesis_path.first_existing_path().ok_or({ - color_eyre::eyre::eyre!("Genesis block file {genesis_path:?} doesn't exist") - })?; - - let genesis_block = RawGenesisBlock::from_path(genesis_path.as_ref())?; - GenesisNetwork::from_configuration(genesis_block, Some(&config.genesis)) - .wrap_err("Failed to initialize genesis.") - }) - .transpose()?; - - iroha::Iroha::with_genesis(genesis, config, logger) + iroha::Iroha::new(config, genesis, logger) .await? .start() .await?; + Ok(()) } -fn print_help(styling: &Styling) -> Result<(), std::io::Error> { - use std::io::Write; - - let stdout = std::io::stdout(); - let lock = stdout.lock(); - let mut buffer = std::io::BufWriter::with_capacity(1024 * REQUIRED_ENV_VARS.len(), lock); - writeln!(buffer, "{}", "Iroha 2".bold().green())?; - writeln!(buffer, "pass {} for this message", styling.or(&HELP_ARG))?; - writeln!( - buffer, - "pass {} to submit genesis from this peer", - styling.or(&SUBMIT_ARG) - )?; - writeln!( - buffer, - "pass {} to print version information", - styling.or(&VERSION_ARG) - )?; - writeln!(buffer)?; - writeln!(buffer, "Iroha 2 is configured via environment variables:")?; - writeln!( - buffer, - " {} is the location of your {}", - "IROHA2_CONFIG_PATH".style(styling.highlight), - styling.with_json_file_ext("config") - )?; - writeln!( - buffer, - " {} is the location of your {}", - "IROHA2_GENESIS_PATH".style(styling.highlight), - styling.with_json_file_ext("genesis") - )?; - writeln!( - buffer, - "If either of these is not provided, Iroha checks the current directory." - )?; - writeln!(buffer)?; - writeln!( - buffer, - "Additionally, in case of absence of both {} and {} -in the current directory, all the variables from {} should be set via the environment -as follows:", - "IROHA2_CONFIG_PATH".style(styling.highlight), - styling.with_json_file_ext("config"), - styling.with_json_file_ext("config") - )?; - for var in REQUIRED_ENV_VARS { - writeln!(buffer, " {}: {}", var.0.style(styling.highlight), var.1)?; +#[cfg(test)] +mod tests { + use assertables::{assert_contains, assert_contains_as_result}; + + use super::*; + + #[test] + #[allow(clippy::bool_assert_comparison)] // for expressiveness + fn default_args() -> Result<()> { + let args = Args::try_parse_from(["test"])?; + + assert_eq!(args.config, None); + assert_eq!(args.terminal_colors, is_colouring_supported()); + assert_eq!(args.submit_genesis, false); + + Ok(()) } - writeln!( - buffer, - "Examples of these variables can be found in the default `configs/peer/config.json`." - )?; - Ok(()) -} -fn print_version(styling: &Styling) { - println!( - "{} {} (git hash {}) \n {}: {}", - "Hyperledger Iroha".style(styling.positive), - env!("CARGO_PKG_VERSION").style(styling.highlight), - env!("VERGEN_GIT_SHA"), - "cargo features".style(styling.highlight), - env!("VERGEN_CARGO_FEATURES") - ); + #[test] + #[allow(clippy::bool_assert_comparison)] // for expressiveness + fn terminal_colors_works_as_expected() -> Result<()> { + fn try_with(arg: &str) -> Result { + Ok(Args::try_parse_from(["test", arg])?.terminal_colors) + } + + assert_eq!( + Args::try_parse_from(["test"])?.terminal_colors, + is_colouring_supported() + ); + assert_eq!(try_with("--terminal-colors")?, true); + assert_eq!(try_with("--terminal-colors=false")?, false); + assert_eq!(try_with("--terminal-colors=true")?, true); + assert!(try_with("--terminal-colors=random").is_err()); + + Ok(()) + } + + #[test] + fn user_provided_config_path_works() -> Result<()> { + let args = Args::try_parse_from(["test", "--config", "/home/custom/file.json"])?; + + assert_eq!( + args.config, + Some(Path::user_provided("/home/custom/file.json").unwrap()) + ); + + Ok(()) + } + + #[test] + fn user_cannot_provide_invalid_extension() { + let err = Args::try_parse_from(["test", "--config", "file.toml"]) + .expect_err("Should not allow TOML"); + + let formatted = format!("{err}"); + assert_contains!(formatted, "invalid value 'file.toml' for '--config"); + assert_contains!(formatted, "unsupported file extension `toml`"); + } } diff --git a/cli/src/samples.rs b/cli/src/samples.rs index a91c6354356..1a59f2b0a25 100644 --- a/cli/src/samples.rs +++ b/cli/src/samples.rs @@ -1,4 +1,4 @@ -//! This module contains the sample configurations used for testing and benchmarking throghout Iroha. +//! This module contains the sample configurations used for testing and benchmarking throughout Iroha. use std::{collections::HashSet, path::Path, str::FromStr}; use iroha_config::{ @@ -79,8 +79,9 @@ pub fn get_config_proxy(peers: UniqueVec, key_pair: Option) -> ..iroha_config::queue::ConfigurationProxy::default() }), genesis: Some(Box::new(iroha_config::genesis::ConfigurationProxy { - account_private_key: Some(Some(private_key)), - account_public_key: Some(public_key), + private_key: Some(Some(private_key)), + public_key: Some(public_key), + file: Some(Some("./genesis.json".into())), })), ..ConfigurationProxy::default() } diff --git a/cli/src/style.rs b/cli/src/style.rs deleted file mode 100644 index 393ae591140..00000000000 --- a/cli/src/style.rs +++ /dev/null @@ -1,67 +0,0 @@ -//! Style and colouration of Iroha CLI outputs. -use owo_colors::{OwoColorize, Style}; - -/// Styling information set at run-time for pretty-printing with colour -#[derive(Clone, Copy, Debug)] -pub struct Styling { - /// Positive highlight - pub positive: Style, - /// Negative highlight. Usually error message. - pub negative: Style, - /// Neutral highlight - pub highlight: Style, - /// Minor message - pub minor: Style, -} - -impl Default for Styling { - fn default() -> Self { - Self { - positive: Style::new().green().bold(), - negative: Style::new().red().bold(), - highlight: Style::new().bold(), - minor: Style::new().green(), - } - } -} - -impl Styling { - #[must_use] - /// Constructor - pub fn new(terminal_colors: bool) -> Self { - if terminal_colors { - Self::default() - } else { - Self::no_color() - } - } - - fn no_color() -> Self { - Self { - positive: Style::new(), - negative: Style::new(), - highlight: Style::new(), - minor: Style::new(), - } - } - - /// Produce documentation for argument group - pub fn or(&self, arg_group: &[&str; 2]) -> String { - format!( - "`{}` (short `{}`)", - arg_group[0].style(self.positive), - arg_group[1].style(self.minor) - ) - } - - /// Convenience method for ".json or .json5" pattern - pub fn with_json_file_ext(&self, name: &str) -> String { - let json = format!("{name}.json"); - let json5 = format!("{name}.json5"); - format!( - "`{}` or `{}`", - json.style(self.highlight), - json5.style(self.highlight) - ) - } -} diff --git a/client/benches/torii.rs b/client/benches/torii.rs index 4ca452b930f..088ec7eb406 100644 --- a/client/benches/torii.rs +++ b/client/benches/torii.rs @@ -17,12 +17,23 @@ use tokio::runtime::Runtime; const MINIMUM_SUCCESS_REQUEST_RATIO: f32 = 0.9; +// assumes that config is having a complete genesis key pair +fn get_genesis_key_pair(config: &iroha_config::iroha::Configuration) -> KeyPair { + if let (public_key, Some(private_key)) = + (&config.genesis.public_key, &config.genesis.private_key) + { + KeyPair::new(public_key.clone(), private_key.clone()).expect("Should be valid") + } else { + panic!("Cannot get genesis key pair from the config. Probably a bug.") + } +} + fn query_requests(criterion: &mut Criterion) { let mut peer = ::new().expect("Failed to create peer"); let configuration = get_config(unique_vec![peer.id.clone()], Some(get_key_pair())); let rt = Runtime::test(); - let genesis = GenesisNetwork::from_configuration( + let genesis = GenesisNetwork::new( RawGenesisBlockBuilder::default() .domain("wonderland".parse().expect("Valid")) .account( @@ -34,7 +45,7 @@ fn query_requests(criterion: &mut Criterion) { construct_executor("../default_executor").expect("Failed to construct executor"), ) .build(), - Some(&configuration.genesis), + &get_genesis_key_pair(&configuration), ) .expect("genesis creation failed"); @@ -120,7 +131,7 @@ fn instruction_submits(criterion: &mut Criterion) { let rt = Runtime::test(); let mut peer = ::new().expect("Failed to create peer"); let configuration = get_config(unique_vec![peer.id.clone()], Some(get_key_pair())); - let genesis = GenesisNetwork::from_configuration( + let genesis = GenesisNetwork::new( RawGenesisBlockBuilder::default() .domain("wonderland".parse().expect("Valid")) .account( @@ -132,7 +143,7 @@ fn instruction_submits(criterion: &mut Criterion) { construct_executor("../default_executor").expect("Failed to construct executor"), ) .build(), - Some(&configuration.genesis), + &get_genesis_key_pair(&configuration), ) .expect("failed to create genesis"); let builder = PeerBuilder::new() diff --git a/client/examples/million_accounts_genesis.rs b/client/examples/million_accounts_genesis.rs index 57993c1a972..a6de431c796 100644 --- a/client/examples/million_accounts_genesis.rs +++ b/client/examples/million_accounts_genesis.rs @@ -2,7 +2,7 @@ use std::{thread, time::Duration}; use iroha::samples::{construct_executor, get_config}; -use iroha_client::data_model::prelude::*; +use iroha_client::{crypto::KeyPair, data_model::prelude::*}; use iroha_data_model::isi::InstructionBox; use iroha_genesis::{GenesisNetwork, RawGenesisBlock, RawGenesisBlockBuilder}; use iroha_primitives::unique_vec; @@ -38,10 +38,18 @@ fn main_genesis() { let mut peer = ::new().expect("Failed to create peer"); let configuration = get_config(unique_vec![peer.id.clone()], Some(get_key_pair())); let rt = Runtime::test(); - let genesis = GenesisNetwork::from_configuration( - generate_genesis(1_000_000_u32), - Some(&configuration.genesis), - ) + let genesis = GenesisNetwork::new(generate_genesis(1_000_000_u32), &{ + let private_key = configuration + .genesis + .private_key + .as_ref() + .expect("Should be from get_config"); + KeyPair::new( + configuration.genesis.public_key.clone(), + private_key.clone(), + ) + .expect("Should be a valid key pair") + }) .expect("genesis creation failed"); let builder = PeerBuilder::new() diff --git a/client/tests/integration/permissions.rs b/client/tests/integration/permissions.rs index da443789009..a6250427fbd 100644 --- a/client/tests/integration/permissions.rs +++ b/client/tests/integration/permissions.rs @@ -17,20 +17,11 @@ fn genesis_transactions_are_validated() { // Setting up genesis - let mut genesis = GenesisNetwork::test(true).expect("Expected genesis"); - - let grant_invalid_token = Grant::permission_token( + let genesis = GenesisNetwork::test_with_instructions([Grant::permission_token( PermissionToken::new("InvalidToken".parse().unwrap(), &json!(null)), AccountId::from_str("alice@wonderland").unwrap(), - ); - - let tx_ref = &mut genesis.transactions.last_mut().unwrap().0; - match &mut tx_ref.payload_mut().instructions { - Executable::Instructions(instructions) => { - instructions.push(grant_invalid_token.into()); - } - Executable::Wasm(_) => panic!("Expected instructions"), - } + ) + .into()]); // Starting peer let (_rt, _peer, test_client) = ::new() diff --git a/client/tests/integration/triggers/by_call_trigger.rs b/client/tests/integration/triggers/by_call_trigger.rs index 41644169a73..7c2cf8aff41 100644 --- a/client/tests/integration/triggers/by_call_trigger.rs +++ b/client/tests/integration/triggers/by_call_trigger.rs @@ -359,14 +359,7 @@ fn trigger_in_genesis_using_base64() -> Result<()> { ); // Registering trigger in genesis - let mut genesis = GenesisNetwork::test(true).expect("Expected genesis"); - let tx_ref = &mut genesis.transactions[0].0; - match &mut tx_ref.payload_mut().instructions { - Executable::Instructions(instructions) => { - instructions.push(Register::trigger(trigger).into()); - } - Executable::Wasm(_) => panic!("Expected instructions"), - } + let genesis = GenesisNetwork::test_with_instructions([Register::trigger(trigger).into()]); let (_rt, _peer, mut test_client) = ::new() .with_genesis(genesis) diff --git a/client_cli/Cargo.toml b/client_cli/Cargo.toml index c05c17decb2..c9ac564c6de 100644 --- a/client_cli/Cargo.toml +++ b/client_cli/Cargo.toml @@ -25,10 +25,10 @@ maintenance = { status = "actively-developed" } [dependencies] iroha_client = { workspace = true } iroha_primitives = { workspace = true } +iroha_config_base = { workspace = true } color-eyre = { workspace = true } -# TODO: migrate to clap v4 (and use the workspace dependency) -clap = { version = "3.2.25", features = ["derive"] } +clap = { workspace = true, features = ["derive"] } dialoguer = { version = "0.11.0", default-features = false } json5 = { workspace = true } once_cell = { workspace = true } diff --git a/client_cli/pytests/common/consts.py b/client_cli/pytests/common/consts.py index 1bc8e500f4b..c7f4201a4c4 100644 --- a/client_cli/pytests/common/consts.py +++ b/client_cli/pytests/common/consts.py @@ -14,7 +14,7 @@ class Stderr(Enum): """ Enum for standard error messages. """ - CANNOT_BE_EMPTY = 'cannot be empty\n\nFor more information try --help\n' + CANNOT_BE_EMPTY = 'cannot be empty\n\nFor more information, try \'--help\'.\n' REPETITION = 'Repetition' TOO_LONG = 'Name length violation' FAILED_TO_FIND_DOMAIN = 'Entity missing' diff --git a/client_cli/src/main.rs b/client_cli/src/main.rs index 83f380d63a8..7f22f2dbb89 100644 --- a/client_cli/src/main.rs +++ b/client_cli/src/main.rs @@ -1,86 +1,94 @@ //! iroha client command line use std::{ - fmt, fs::{self, read as read_file}, io::{stdin, stdout}, + path::PathBuf, str::FromStr, time::Duration, }; -use clap::StructOpt; use color_eyre::{ - eyre::{ContextCompat as _, Error, WrapErr}, + eyre::{eyre, Error, WrapErr}, Result, }; +// FIXME: sync with `kagami` (it uses `inquiry`, migrate both to something single) use dialoguer::Confirm; use erased_serde::Serialize; use iroha_client::{ client::{Client, QueryResult}, - config::{path::Path as ConfigPath, Configuration as ClientConfiguration}, + config::{path::Path, Configuration as ClientConfiguration, ConfigurationProxy}, data_model::prelude::*, }; +use iroha_config_base::proxy::{LoadFromDisk, LoadFromEnv, Override}; use iroha_primitives::addr::SocketAddr; -/// Metadata wrapper, which can be captured from cli arguments (from user supplied file). -#[derive(Debug, Clone)] -pub struct Metadata(pub UnlimitedMetadata); - -impl fmt::Display for Metadata { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{self:?}") - } +/// Re-usable clap `--metadata ` (`-m`) argument. +/// Should be combined with `#[command(flatten)]` attr. +#[derive(clap::Args, Debug, Clone)] +// FIXME: `pub` is needed because Rust complains about "leaking private types" +// when this type is used inside of modules. I don't know how to fix it. +pub struct MetadataArgs { + /// The JSON/JSON5 file with key-value metadata pairs + #[arg(short, long, value_name("PATH"), value_hint(clap::ValueHint::FilePath))] + metadata: Option, } -impl FromStr for Metadata { - type Err = Error; - fn from_str(file: &str) -> Result { - if file.is_empty() { - return Ok(Self(UnlimitedMetadata::default())); - } - let err_msg = format!("Failed to open the metadata file {}.", &file); - let deser_err_msg = format!("Failed to deserialize metadata from file: {}", &file); - let content = fs::read_to_string(file).wrap_err(err_msg)?; - let metadata: UnlimitedMetadata = json5::from_str(&content).wrap_err(deser_err_msg)?; - Ok(Self(metadata)) - } -} +impl MetadataArgs { + fn load(self) -> Result { + let value: Option = self + .metadata + .map(|path| { + let content = fs::read_to_string(&path).wrap_err_with(|| { + eyre!("Failed to read the metadata file `{}`", path.display()) + })?; + let metadata: UnlimitedMetadata = + json5::from_str(&content).wrap_err_with(|| { + eyre!( + "Failed to deserialize metadata from file `{}`", + path.display() + ) + })?; + Ok::<_, color_eyre::Report>(metadata) + }) + .transpose()?; -/// Client configuration wrapper. Allows getting itself from arguments from cli (from user supplied file). -#[derive(Debug, Clone)] -struct Configuration(pub ClientConfiguration); - -impl FromStr for Configuration { - type Err = Error; - fn from_str(file: &str) -> Result { - let deser_err_msg = format!("Failed to decode config file {} ", &file); - let err_msg = format!("Failed to open config file {}", &file); - let content = fs::read_to_string(file).wrap_err(err_msg)?; - let cfg = json5::from_str(&content).wrap_err(deser_err_msg)?; - Ok(Self(cfg)) + Ok(value.unwrap_or_default()) } } /// Iroha CLI Client provides an ability to interact with Iroha Peers Web API without direct network usage. -#[derive(StructOpt, Debug)] -#[structopt(name = "iroha_client_cli", version = concat!("version=", env!("CARGO_PKG_VERSION"), " git_commit_sha=", env!("VERGEN_GIT_SHA")), author)] +#[derive(clap::Parser, Debug)] +#[command(name = "iroha_client_cli", version = concat!("version=", env!("CARGO_PKG_VERSION"), " git_commit_sha=", env!("VERGEN_GIT_SHA")), author)] struct Args { - /// Sets a config file path - #[structopt(short, long)] - config: Option, + /// Path to the configuration file, defaults to `config.json`/`config.json5` + /// + /// Supported extensions are `.json` and `.json5`. By default, Iroha Client looks for a + /// `config` file with one of the supported extensions in the current working directory. + /// If the default config file is not found, Iroha will rely on default values and environment + /// variables. However, if the config path is set explicitly with this argument and the file + /// is not found, Iroha Client will exit with an error. + #[arg( + short, + long, + value_name("PATH"), + value_hint(clap::ValueHint::FilePath), + value_parser(Path::user_provided_str) + )] + config: Option, /// More verbose output - #[structopt(short, long)] + #[arg(short, long)] verbose: bool, /// Skip MST check. By setting this flag searching similar transactions on the server can be omitted. /// Thus if you don't use multisignature transactions you should use this flag as it will increase speed of submitting transactions. /// Also setting this flag could be useful when `iroha_client_cli` is used to submit the same transaction multiple times (like mint for example) in short period of time. - #[structopt(long)] + #[arg(long)] skip_mst_check: bool, /// Subcommands of client cli - #[structopt(subcommand)] + #[command(subcommand)] subcommand: Subcommand, } -#[derive(StructOpt, Debug)] +#[derive(clap::Subcommand, Debug)] enum Subcommand { /// The subcommand related to domains #[clap(subcommand)] @@ -170,32 +178,33 @@ impl RunArgs for Subcommand { const RETRY_COUNT_MST: u32 = 1; const RETRY_IN_MST: Duration = Duration::from_millis(100); -static DEFAULT_CONFIG_PATH: once_cell::sync::Lazy<&'static std::path::Path> = - once_cell::sync::Lazy::new(|| std::path::Path::new("config")); +static DEFAULT_CONFIG_PATH: &str = "config"; fn main() -> Result<()> { color_eyre::install()?; let Args { - config: config_opt, + config: config_path, subcommand, verbose, skip_mst_check, } = clap::Parser::parse(); - let config = if let Some(config) = config_opt { - config + + let config = ConfigurationProxy::default(); + let config = if let Some(path) = config_path + .unwrap_or_else(|| Path::default(DEFAULT_CONFIG_PATH)) + .try_resolve() + .wrap_err("Failed to resolve config file")? + { + config.override_with(ConfigurationProxy::from_path(&*path)) } else { - let config_path = ConfigPath::default(&DEFAULT_CONFIG_PATH); - Configuration::from_str( - config_path - .first_existing_path() - .wrap_err("Configuration file does not exist")? - .as_ref() - .to_string_lossy() - .as_ref(), - )? + config }; - - let Configuration(config) = config; + let config = config.override_with( + ConfigurationProxy::from_std_env().wrap_err("Failed to read config from ENV")?, + ); + let config = config + .build() + .wrap_err("Failed to finalize configuration")?; if verbose { eprintln!( @@ -287,7 +296,7 @@ mod events { use super::*; /// Get event stream from iroha peer - #[derive(StructOpt, Debug, Clone, Copy)] + #[derive(clap::Subcommand, Debug, Clone, Copy)] pub enum Args { /// Gets pipeline events Pipeline, @@ -327,7 +336,7 @@ mod blocks { use super::*; /// Get block stream from iroha peer - #[derive(StructOpt, Debug, Clone, Copy)] + #[derive(clap::Args, Debug, Clone, Copy)] pub struct Args { /// Block height from which to start streaming blocks height: NonZeroU64, @@ -375,29 +384,25 @@ mod domain { } /// Add subcommand for domain - #[derive(Debug, StructOpt)] + #[derive(Debug, clap::Args)] pub struct Register { /// Domain name as double-quoted string - #[structopt(short, long)] + #[arg(short, long)] pub id: DomainId, - /// The JSON/JSON5 file with key-value metadata pairs - #[structopt(short, long, default_value = "")] - pub metadata: super::Metadata, + #[command(flatten)] + pub metadata: MetadataArgs, } impl RunArgs for Register { fn run(self, context: &mut dyn RunContext) -> Result<()> { - let Self { - id, - metadata: Metadata(metadata), - } = self; + let Self { id, metadata } = self; let create_domain = iroha_client::data_model::isi::Register::domain(Domain::new(id)); - submit([create_domain], metadata, context).wrap_err("Failed to create domain") + submit([create_domain], metadata.load()?, context).wrap_err("Failed to create domain") } } /// List domains with this command - #[derive(StructOpt, Debug, Clone)] + #[derive(clap::Subcommand, Debug, Clone)] pub enum List { /// All domains All, @@ -425,20 +430,19 @@ mod domain { } /// Transfer a domain between accounts - #[derive(Debug, StructOpt)] + #[derive(Debug, clap::Args)] pub struct Transfer { /// Domain name as double-quited string - #[structopt(short, long)] + #[arg(short, long)] pub id: DomainId, /// Account from which to transfer (in form `name@domain_name') - #[structopt(short, long)] + #[arg(short, long)] pub from: AccountId, /// Account to which to transfer (in form `name@domain_name') - #[structopt(short, long)] + #[arg(short, long)] pub to: AccountId, - /// The JSON/JSON5 file with key-value metadata pairs - #[structopt(short, long, default_value = "")] - pub metadata: super::Metadata, + #[command(flatten)] + pub metadata: MetadataArgs, } impl RunArgs for Transfer { @@ -447,10 +451,11 @@ mod domain { id, from, to, - metadata: Metadata(metadata), + metadata, } = self; let transfer_domain = iroha_client::data_model::isi::Transfer::domain(from, id, to); - submit([transfer_domain], metadata, context).wrap_err("Failed to transfer domain") + submit([transfer_domain], metadata.load()?, context) + .wrap_err("Failed to transfer domain") } } } @@ -463,15 +468,15 @@ mod account { use super::*; /// subcommands for account subcommand - #[derive(StructOpt, Debug)] + #[derive(clap::Subcommand, Debug)] pub enum Args { /// Register account Register(Register), /// Set something in account - #[clap(subcommand)] + #[command(subcommand)] Set(Set), /// List accounts - #[clap(subcommand)] + #[command(subcommand)] List(List), /// Grant a permission to the account Grant(Grant), @@ -492,34 +497,30 @@ mod account { } /// Register account - #[derive(StructOpt, Debug)] + #[derive(clap::Args, Debug)] pub struct Register { /// Id of account in form `name@domain_name' - #[structopt(short, long)] + #[arg(short, long)] pub id: AccountId, /// Its public key - #[structopt(short, long)] + #[arg(short, long)] pub key: PublicKey, - /// /// The JSON file with key-value metadata pairs - #[structopt(short, long, default_value = "")] - pub metadata: super::Metadata, + #[command(flatten)] + pub metadata: MetadataArgs, } impl RunArgs for Register { fn run(self, context: &mut dyn RunContext) -> Result<()> { - let Self { - id, - key, - metadata: Metadata(metadata), - } = self; + let Self { id, key, metadata } = self; let create_account = iroha_client::data_model::isi::Register::account(Account::new(id, [key])); - submit([create_account], metadata, context).wrap_err("Failed to register account") + submit([create_account], metadata.load()?, context) + .wrap_err("Failed to register account") } } /// Set subcommand of account - #[derive(StructOpt, Debug)] + #[derive(clap::Subcommand, Debug)] pub enum Set { /// Signature condition SignatureCondition(SignatureCondition), @@ -531,7 +532,7 @@ mod account { } } - #[derive(Debug)] + #[derive(Debug, Clone)] pub struct Signature(SignatureCheckCondition); impl FromStr for Signature { @@ -548,13 +549,12 @@ mod account { } /// Set accounts signature condition - #[derive(StructOpt, Debug)] + #[derive(clap::Args, Debug)] pub struct SignatureCondition { /// Signature condition file pub condition: Signature, - /// The JSON/JSON5 file with key-value metadata pairs - #[structopt(short, long, default_value = "")] - pub metadata: super::Metadata, + #[command(flatten)] + pub metadata: MetadataArgs, } impl RunArgs for SignatureCondition { @@ -562,15 +562,16 @@ mod account { let account_id = context.configuration().account_id.clone(); let Self { condition: Signature(condition), - metadata: Metadata(metadata), + metadata, } = self; let mint_box = Mint::account_signature_check_condition(condition, account_id); - submit([mint_box], metadata, context).wrap_err("Failed to set signature condition") + submit([mint_box], metadata.load()?, context) + .wrap_err("Failed to set signature condition") } } /// List accounts with this command - #[derive(StructOpt, Debug, Clone)] + #[derive(clap::Subcommand, Debug, Clone)] pub enum List { /// All accounts All, @@ -597,21 +598,20 @@ mod account { } } - #[derive(StructOpt, Debug)] + #[derive(clap::Args, Debug)] pub struct Grant { /// Account id - #[structopt(short, long)] + #[arg(short, long)] pub id: AccountId, /// The JSON/JSON5 file with a permission token - #[structopt(short, long)] + #[arg(short, long)] pub permission: Permission, - /// The JSON/JSON5 file with key-value metadata pairs - #[structopt(short, long, default_value = "")] - pub metadata: super::Metadata, + #[command(flatten)] + pub metadata: MetadataArgs, } /// [`PermissionToken`] wrapper implementing [`FromStr`] - #[derive(Debug)] + #[derive(Debug, Clone)] pub struct Permission(PermissionToken); impl FromStr for Permission { @@ -633,19 +633,19 @@ mod account { let Self { id, permission, - metadata: Metadata(metadata), + metadata, } = self; let grant = iroha_client::data_model::isi::Grant::permission_token(permission.0, id); - submit([grant], metadata, context) + submit([grant], metadata.load()?, context) .wrap_err("Failed to grant the permission to the account") } } /// List all account permissions - #[derive(StructOpt, Debug)] + #[derive(clap::Args, Debug)] pub struct ListPermissions { /// Account id - #[structopt(short, long)] + #[arg(short, long)] id: AccountId, } @@ -668,7 +668,7 @@ mod asset { use super::*; /// Subcommand for dealing with asset - #[derive(StructOpt, Debug)] + #[derive(clap::Subcommand, Debug)] pub enum Args { /// Register subcommand of asset Register(Register), @@ -695,20 +695,19 @@ mod asset { } /// Register subcommand of asset - #[derive(StructOpt, Debug)] + #[derive(clap::Args, Debug)] pub struct Register { /// Asset id for registering (in form of `name#domain_name') - #[structopt(short, long)] + #[arg(short, long)] pub id: AssetDefinitionId, /// Mintability of asset - #[structopt(short, long)] + #[arg(short, long)] pub unmintable: bool, /// Value type stored in asset - #[structopt(short, long)] + #[arg(short, long)] pub value_type: AssetValueType, - /// The JSON/JSON5 file with key-value metadata pairs - #[structopt(short, long, default_value = "")] - pub metadata: super::Metadata, + #[command(flatten)] + pub metadata: MetadataArgs, } impl RunArgs for Register { @@ -717,7 +716,7 @@ mod asset { id, value_type, unmintable, - metadata: Metadata(metadata), + metadata, } = self; let mut asset_definition = match value_type { AssetValueType::Quantity => AssetDefinition::quantity(id), @@ -730,26 +729,25 @@ mod asset { } let create_asset_definition = iroha_client::data_model::isi::Register::asset_definition(asset_definition); - submit([create_asset_definition], metadata, context) + submit([create_asset_definition], metadata.load()?, context) .wrap_err("Failed to register asset") } } /// Command for minting asset in existing Iroha account - #[derive(StructOpt, Debug)] + #[derive(clap::Args, Debug)] pub struct Mint { /// Account id where asset is stored (in form of `name@domain_name') - #[structopt(long)] + #[arg(long)] pub account: AccountId, /// Asset id from which to mint (in form of `name#domain_name') - #[structopt(long)] + #[arg(long)] pub asset: AssetDefinitionId, /// Quantity to mint - #[structopt(short, long)] + #[arg(short, long)] pub quantity: u32, - /// The JSON/JSON5 file with key-value metadata pairs - #[structopt(short, long, default_value = "")] - pub metadata: super::Metadata, + #[command(flatten)] + pub metadata: MetadataArgs, } impl RunArgs for Mint { @@ -758,32 +756,31 @@ mod asset { account, asset, quantity, - metadata: Metadata(metadata), + metadata, } = self; let mint_asset = iroha_client::data_model::isi::Mint::asset_quantity( quantity, AssetId::new(asset, account), ); - submit([mint_asset], metadata, context) + submit([mint_asset], metadata.load()?, context) .wrap_err("Failed to mint asset of type `NumericValue::U32`") } } /// Command for minting asset in existing Iroha account - #[derive(StructOpt, Debug)] + #[derive(clap::Args, Debug)] pub struct Burn { /// Account id where asset is stored (in form of `name@domain_name') - #[structopt(long)] + #[arg(long)] pub account: AccountId, /// Asset id from which to mint (in form of `name#domain_name') - #[structopt(long)] + #[arg(long)] pub asset: AssetDefinitionId, /// Quantity to mint - #[structopt(short, long)] + #[arg(short, long)] pub quantity: u32, - /// The JSON/JSON5 file with key-value metadata pairs - #[structopt(short, long, default_value = "")] - pub metadata: super::Metadata, + #[command(flatten)] + pub metadata: MetadataArgs, } impl RunArgs for Burn { @@ -792,35 +789,34 @@ mod asset { account, asset, quantity, - metadata: Metadata(metadata), + metadata, } = self; let burn_asset = iroha_client::data_model::isi::Burn::asset_quantity( quantity, AssetId::new(asset, account), ); - submit([burn_asset], metadata, context) + submit([burn_asset], metadata.load()?, context) .wrap_err("Failed to burn asset of type `NumericValue::U32`") } } /// Transfer asset between accounts - #[derive(StructOpt, Debug)] + #[derive(clap::Args, Debug)] pub struct Transfer { /// Account from which to transfer (in form `name@domain_name') - #[structopt(short, long)] + #[arg(short, long)] pub from: AccountId, /// Account to which to transfer (in form `name@domain_name') - #[structopt(short, long)] + #[arg(short, long)] pub to: AccountId, /// Asset id to transfer (in form like `name#domain_name') - #[structopt(short, long)] + #[arg(short, long)] pub asset_id: AssetDefinitionId, /// Quantity of asset as number - #[structopt(short, long)] + #[arg(short, long)] pub quantity: u32, - /// The JSON/JSON5 file with key-value metadata pairs - #[structopt(short, long, default_value = "")] - pub metadata: super::Metadata, + #[command(flatten)] + pub metadata: MetadataArgs, } impl RunArgs for Transfer { @@ -830,25 +826,25 @@ mod asset { to, asset_id, quantity, - metadata: Metadata(metadata), + metadata, } = self; let transfer_asset = iroha_client::data_model::isi::Transfer::asset_quantity( AssetId::new(asset_id, from), quantity, to, ); - submit([transfer_asset], metadata, context).wrap_err("Failed to transfer asset") + submit([transfer_asset], metadata.load()?, context).wrap_err("Failed to transfer asset") } } /// Get info of asset - #[derive(StructOpt, Debug)] + #[derive(clap::Args, Debug)] pub struct Get { /// Account where asset is stored (in form of `name@domain_name') - #[structopt(long)] + #[arg(long)] pub account: AccountId, /// Asset name to lookup (in form of `name#domain_name') - #[structopt(long)] + #[arg(long)] pub asset: AssetDefinitionId, } @@ -866,7 +862,7 @@ mod asset { } /// List assets with this command - #[derive(StructOpt, Debug, Clone)] + #[derive(clap::Subcommand, Debug, Clone)] pub enum List { /// All assets All, @@ -898,7 +894,7 @@ mod peer { use super::*; /// Subcommand for dealing with peer - #[derive(StructOpt, Debug)] + #[derive(clap::Subcommand, Debug)] pub enum Args { /// Register subcommand of peer Register(Register), @@ -916,17 +912,16 @@ mod peer { } /// Register subcommand of peer - #[derive(StructOpt, Debug)] + #[derive(clap::Args, Debug)] pub struct Register { /// P2P address of the peer e.g. `127.0.0.1:1337` - #[structopt(short, long)] + #[arg(short, long)] pub address: SocketAddr, /// Public key of the peer - #[structopt(short, long)] + #[arg(short, long)] pub key: PublicKey, - /// The JSON/JSON5 file with key-value metadata pairs - #[structopt(short, long, default_value = "")] - pub metadata: super::Metadata, + #[command(flatten)] + pub metadata: MetadataArgs, } impl RunArgs for Register { @@ -934,27 +929,26 @@ mod peer { let Self { address, key, - metadata: Metadata(metadata), + metadata, } = self; let register_peer = iroha_client::data_model::isi::Register::peer(Peer::new( PeerId::new(&address, &key), )); - submit([register_peer], metadata, context).wrap_err("Failed to register peer") + submit([register_peer], metadata.load()?, context).wrap_err("Failed to register peer") } } /// Unregister subcommand of peer - #[derive(StructOpt, Debug)] + #[derive(clap::Args, Debug)] pub struct Unregister { /// P2P address of the peer e.g. `127.0.0.1:1337` - #[structopt(short, long)] + #[arg(short, long)] pub address: SocketAddr, /// Public key of the peer - #[structopt(short, long)] + #[arg(short, long)] pub key: PublicKey, - /// The JSON/JSON5 file with key-value metadata pairs - #[structopt(short, long, default_value = "")] - pub metadata: super::Metadata, + #[command(flatten)] + pub metadata: MetadataArgs, } impl RunArgs for Unregister { @@ -962,11 +956,12 @@ mod peer { let Self { address, key, - metadata: Metadata(metadata), + metadata, } = self; let unregister_peer = iroha_client::data_model::isi::Unregister::peer(PeerId::new(&address, &key)); - submit([unregister_peer], metadata, context).wrap_err("Failed to unregister peer") + submit([unregister_peer], metadata.load()?, context) + .wrap_err("Failed to unregister peer") } } } @@ -977,10 +972,10 @@ mod wasm { use super::*; /// Subcommand for dealing with Wasm - #[derive(Debug, StructOpt)] + #[derive(Debug, clap::Args)] pub struct Args { /// Specify a path to the Wasm file or skip this flag to read from stdin - #[structopt(short, long)] + #[arg(short, long)] path: Option, } @@ -1012,7 +1007,7 @@ mod json { use super::*; /// Subcommand for submitting multi-instructions - #[derive(Clone, Copy, Debug, StructOpt)] + #[derive(Clone, Copy, Debug, clap::Args)] pub struct Args; impl RunArgs for Args { diff --git a/config/Cargo.toml b/config/Cargo.toml index 2f548f60946..d6df71128fa 100644 --- a/config/Cargo.toml +++ b/config/Cargo.toml @@ -15,6 +15,7 @@ iroha_config_base = { workspace = true } iroha_data_model = { workspace = true } iroha_primitives = { workspace = true } iroha_crypto = { workspace = true } +iroha_genesis = { workspace = true } eyre = { workspace = true } tracing = { workspace = true } @@ -29,7 +30,6 @@ thiserror = { workspace = true } displaydoc = { workspace = true } derive_more = { workspace = true } cfg-if = { workspace = true } -path-absolutize = { workspace = true } once_cell = { workspace = true } [dev-dependencies] diff --git a/config/iroha_test_config.json b/config/iroha_test_config.json index 6ebbf417a26..7a180598bbb 100644 --- a/config/iroha_test_config.json +++ b/config/iroha_test_config.json @@ -4,7 +4,6 @@ "digest_function": "ed25519", "payload": "282ED9F3CF92811C3818DBC4AE594ED59DC1A2F78E4241E31924E101D6B1FB831C61FAF8FE94E253B93114240394F79A607B7FA55F9E5A41EBEC74B88055768B" }, - "DISABLE_PANIC_TERMINAL_COLORS": false, "KURA": { "INIT_MODE": "strict", "BLOCK_STORE_PATH": "./storage", @@ -62,14 +61,15 @@ "TOKIO_CONSOLE_ADDR": "127.0.0.1:5555" }, "GENESIS": { - "ACCOUNT_PUBLIC_KEY": "ed01204CFFD0EE429B1BDD36B3910EC570852B8BB63F18750341772FB46BC856C5CAAF", - "ACCOUNT_PRIVATE_KEY": { + "PUBLIC_KEY": "ed01204CFFD0EE429B1BDD36B3910EC570852B8BB63F18750341772FB46BC856C5CAAF", + "PRIVATE_KEY": { "digest_function": "ed25519", "payload": "D748E18CE60CB30DEA3E73C9019B7AF45A8D465E3D71BCC9A5EF99A008205E534CFFD0EE429B1BDD36B3910EC570852B8BB63F18750341772FB46BC856C5CAAF" }, "WAIT_FOR_PEERS_RETRY_COUNT_LIMIT": 100, "WAIT_FOR_PEERS_RETRY_PERIOD_MS": 500, - "GENESIS_SUBMISSION_DELAY_MS": 1000 + "GENESIS_SUBMISSION_DELAY_MS": 1000, + "FILE": "./genesis.json" }, "WSV": { "ASSET_METADATA_LIMITS": { diff --git a/config/src/genesis.rs b/config/src/genesis.rs index 2bb9e8d892b..b6881ac4d65 100644 --- a/config/src/genesis.rs +++ b/config/src/genesis.rs @@ -1,6 +1,10 @@ //! Module with genesis configuration logic. +use std::path::PathBuf; + +use eyre::Report; use iroha_config_base::derive::{view, Proxy}; -use iroha_crypto::{PrivateKey, PublicKey}; +use iroha_crypto::{KeyPair, PrivateKey, PublicKey}; +use iroha_genesis::RawGenesisBlock; use serde::{Deserialize, Serialize}; // Generate `ConfigurationView` without the private key @@ -12,22 +16,88 @@ view! { pub struct Configuration { /// The public key of the genesis account, should be supplied to all peers. #[config(serde_as_str)] - pub account_public_key: PublicKey, + pub public_key: PublicKey, /// The private key of the genesis account, only needed for the peer that submits the genesis block. #[view(ignore)] - pub account_private_key: Option, + pub private_key: Option, + /// Path to the genesis file + #[config(serde_as_str)] + pub file: Option } } impl Default for ConfigurationProxy { fn default() -> Self { Self { - account_public_key: None, - account_private_key: Some(None), + public_key: None, + private_key: Some(None), + file: None, } } } +/// Parsed variant of the user-provided [`Configuration`] +// TODO: incorporate this struct into the final, parsed configuration +// https://github.com/hyperledger/iroha/issues/3500 +pub enum ParsedConfiguration { + /// The peer can only observe the genesis block + Partial { + /// Genesis account public key + public_key: PublicKey, + }, + /// The peer is responsible for submitting the genesis block + Full { + /// Genesis account key pair + key_pair: KeyPair, + /// Raw genesis block + raw_block: RawGenesisBlock, + }, +} + +impl Configuration { + /// Parses user configuration into a stronger-typed structure [`ParsedConfiguration`] + /// + /// # Errors + /// See [`ParseError`] + pub fn parse(self, submit: bool) -> Result { + match (self.private_key, self.file, submit) { + (None, None, false) => Ok(ParsedConfiguration::Partial { + public_key: self.public_key, + }), + (Some(private_key), Some(path), true) => { + let raw_block = RawGenesisBlock::from_path(&path) + .map_err(|report| ParseError::File { path, report })?; + + Ok(ParsedConfiguration::Full { + key_pair: KeyPair::new(self.public_key, private_key)?, + raw_block, + }) + } + (_, _, true) => Err(ParseError::SubmitIsSetButRestAreNot), + (_, _, false) => Err(ParseError::SubmitIsNotSetButRestAre), + } + } +} + +/// Error which might occur during [`Configuration::parse()`] +#[derive(Debug, displaydoc::Display, thiserror::Error)] +pub enum ParseError { + /// `--submit-genesis` was provided, but `genesis.private_key` and/or `genesis.file` are missing + SubmitIsSetButRestAreNot, + /// `--submit-genesis` was not provided, but `genesis.private_key` and/or `genesis.file` are set + SubmitIsNotSetButRestAre, + /// Genesis key pair is invalid + InvalidKeyPair(#[from] iroha_crypto::error::Error), + /// Cannot read the genesis block from file `{path}` + File { + /// Original error report + #[source] + report: Report, + /// Path to the file + path: PathBuf, + }, +} + #[cfg(test)] pub mod tests { use iroha_crypto::KeyPair; @@ -61,10 +131,11 @@ pub mod tests { prop_compose! { pub fn arb_proxy() ( - (account_public_key, account_private_key) in arb_keys(), + (public_key, private_key) in arb_keys(), + file in prop::option::of(Just(None)) ) -> ConfigurationProxy { - ConfigurationProxy { account_public_key, account_private_key } + ConfigurationProxy { public_key, private_key, file } } } } diff --git a/config/src/iroha.rs b/config/src/iroha.rs index ffa28eddc2e..1946b2571b1 100644 --- a/config/src/iroha.rs +++ b/config/src/iroha.rs @@ -20,8 +20,6 @@ view! { /// Private key of this peer #[view(ignore)] pub private_key: PrivateKey, - /// Disable coloring of the backtrace and error report on panic - pub disable_panic_terminal_colors: bool, /// `Kura` configuration #[config(inner)] pub kura: Box, @@ -68,7 +66,6 @@ impl Default for ConfigurationProxy { Self { public_key: None, private_key: None, - disable_panic_terminal_colors: Some(bool::default()), kura: Some(Box::default()), sumeragi: Some(Box::default()), torii: Some(Box::default()), @@ -169,6 +166,8 @@ impl ConfigurationProxy { #[cfg(test)] mod tests { + use std::path::PathBuf; + use proptest::prelude::*; use super::*; @@ -204,7 +203,6 @@ mod tests { prop_compose! { fn arb_proxy()( (public_key, private_key) in arb_keys(), - disable_panic_terminal_colors in prop::option::of(Just(true)), kura in prop::option::of(kura::tests::arb_proxy().prop_map(Box::new)), sumeragi in (prop::option::of(sumeragi::tests::arb_proxy().prop_map(Box::new))), torii in (prop::option::of(torii::tests::arb_proxy().prop_map(Box::new))), @@ -218,7 +216,7 @@ mod tests { snapshot in prop::option::of(snapshot::tests::arb_proxy().prop_map(Box::new)), live_query_store in prop::option::of(live_query_store::tests::arb_proxy()), ) -> ConfigurationProxy { - ConfigurationProxy { public_key, private_key, disable_panic_terminal_colors, kura, sumeragi, torii, block_sync, queue, + ConfigurationProxy { public_key, private_key, kura, sumeragi, torii, block_sync, queue, logger, genesis, wsv, network, telemetry, snapshot, live_query_store } } } @@ -244,7 +242,7 @@ mod tests { fn parse_example_json() { let cfg_proxy = ConfigurationProxy::from_path(CONFIGURATION_PATH); assert_eq!( - "./storage", + PathBuf::from("./storage"), cfg_proxy.kura.unwrap().block_store_path.unwrap() ); assert_eq!( diff --git a/config/src/kura.rs b/config/src/kura.rs index 5ce29c4ce95..8f97dbbf94b 100644 --- a/config/src/kura.rs +++ b/config/src/kura.rs @@ -1,5 +1,7 @@ //! Module for kura-related configuration and structs +use std::path::PathBuf; + use eyre::Result; use iroha_config_base::derive::Proxy; use serde::{Deserialize, Serialize}; @@ -14,7 +16,8 @@ pub struct Configuration { /// Initialization mode: `strict` or `fast`. pub init_mode: Mode, /// Path to the existing block store folder or path to create new folder. - pub block_store_path: String, + #[config(serde_as_str)] + pub block_store_path: PathBuf, /// Whether or not new blocks be outputted to a file called blocks.json. pub debug_output_new_blocks: bool, } diff --git a/config/src/path.rs b/config/src/path.rs index f6f14887c4e..488af5ded46 100644 --- a/config/src/path.rs +++ b/config/src/path.rs @@ -5,8 +5,6 @@ extern crate alloc; use alloc::borrow::Cow; use std::path::PathBuf; -// TODO: replace with `std::fs::absolute` when it's stable. -use path_absolutize::Absolutize as _; use InnerPath::*; /// Allowed configuration file extension that user can provide. @@ -14,105 +12,140 @@ pub const ALLOWED_CONFIG_EXTENSIONS: [&str; 2] = ["json", "json5"]; /// Error type for [`Path`]. #[derive(Debug, Clone, thiserror::Error, displaydoc::Display)] -pub enum ExtensionError { - /// No valid file extension found. Allowed file extensions are: {ALLOWED_CONFIG_EXTENSIONS:?} - Missing, - /// Provided config file has an unsupported file extension `{0}`, allowed extensions are: {ALLOWED_CONFIG_EXTENSIONS:?}. - Invalid(String), +pub enum Error { + /// File doesn't have an extension. Allowed file extensions are: {ALLOWED_CONFIG_EXTENSIONS:?} + MissingExtension, + /// Provided config file has an unsupported file extension `{0}`. Allowed extensions are: {ALLOWED_CONFIG_EXTENSIONS:?}. + InvalidExtension(String), + /// User-provided file `{0}` is not found. + FileNotFound(String), } /// Result type for [`Path`] constructors. -pub type Result = std::result::Result; +pub type Result = std::result::Result; /// Inner helper struct. /// /// With this struct, we force to use [`Path`]'s constructors instead of constructing it directly. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] enum InnerPath { + /// Contains path without an extension, so that it will try to resolve + /// using [`ALLOWED_CONFIG_EXTENSIONS`]. [`Path::try_resolve()`] will not fail if file isn't + /// found. Default(PathBuf), + /// Contains full path, with extension. [`Path::try_resolve()`] will fail if not found. UserProvided(PathBuf), } /// Wrapper around path to config file (e.g. `config.json`). /// /// Provides abstraction above user-provided config and default ones. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub struct Path(InnerPath); impl core::fmt::Display for Path { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match &self.0 { - Default(pth) => write!( - f, - "{:?} (default)", - pth.with_extension("json") - .absolutize() - .expect("Malformed default path") - ), - UserProvided(pth) => write!( - f, - "{:?} (user-provided)", - pth.with_extension("json") - .absolutize() - .expect("Malformed user-provided path") - ), + Default(path) => { + write!( + f, + "{}.{{{}}}", + path.display(), + ALLOWED_CONFIG_EXTENSIONS.join(",") + ) + } + UserProvided(path) => write!(f, "{}", path.display()), } } } impl Path { - /// Construct new [`Path`] from the default `path`. + /// Construct new [`Path`] which will try to resolve multiple allowed extensions and will not + /// fail resolution ([`Self::try_resolve()`]) if file is not found. /// - /// # Panics + /// The path should not have an extension. /// - /// Panics if `path` contains an extension. - pub fn default(path: &'static std::path::Path) -> Self { - assert!( - path.extension().is_none(), - "Default configuration path should have no extension" - ); - - Self(Default(path.to_owned())) + /// # Panics + /// If the path has an extension. + pub fn default(path: impl AsRef) -> Self { + let path = path.as_ref().to_path_buf(); + if path.extension().is_some() { + panic!("Default config path is not supposed to have an extension. It is a bug.") + } + Self(Default(path)) } - /// Construct new [`Path`] from user-provided `path`. + /// Construct new [`Path`] from user-provided `path` which will fail to [`Self::try_resolve()`] + /// if file is not found. /// /// # Errors - /// - /// An error will be returned if `path` contains no file extension - /// or contains unsupported one. - pub fn user_provided(path: impl Into) -> Result { - let path = path.into(); + /// If `path`'s extension is absent or unsupported. + pub fn user_provided(path: impl AsRef) -> Result { + let path = path.as_ref(); let extension = path .extension() - .ok_or(ExtensionError::Missing)? + .ok_or(Error::MissingExtension)? .to_string_lossy(); if !ALLOWED_CONFIG_EXTENSIONS.contains(&extension.as_ref()) { - return Err(ExtensionError::Invalid(extension.into_owned())); + return Err(Error::InvalidExtension(extension.into_owned())); } - Ok(Self(UserProvided(path))) + Ok(Self(UserProvided(path.to_path_buf()))) + } + + /// Same as [`Self::user_provided()`], but accepts `&str` (useful for clap) + /// + /// # Errors + /// See [`Self::user_provided()`] + pub fn user_provided_str(raw: &str) -> Result { + Self::user_provided(raw) } /// Try to get first existing path by applying possible extensions if there are any. - pub fn first_existing_path(&self) -> Option> { + /// + /// # Errors + /// If user-provided path is not found + pub fn try_resolve(&self) -> Result>> { match &self.0 { - Default(path) => ALLOWED_CONFIG_EXTENSIONS.iter().find_map(|extension| { - let path_ext = path.with_extension(extension); - path_ext.exists().then_some(Cow::Owned(path_ext)) - }), - UserProvided(path) => path.exists().then_some(Cow::Borrowed(path)), + Default(path) => { + let maybe = ALLOWED_CONFIG_EXTENSIONS.iter().find_map(|extension| { + let path_ext = path.with_extension(extension); + path_ext.exists().then_some(Cow::Owned(path_ext)) + }); + Ok(maybe) + } + UserProvided(path) => { + if path.exists() { + Ok(Some(Cow::Borrowed(path))) + } else { + Err(Error::FileNotFound(path.to_string_lossy().into_owned())) + } + } } } +} - /// Check if config path exists by applying allowed extensions if there are any. - pub fn exists(&self) -> bool { - match &self.0 { - Default(path) => ALLOWED_CONFIG_EXTENSIONS - .iter() - .any(|extension| path.with_extension(extension).exists()), - UserProvided(path) => path.exists(), - } +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn display_multi_extensions() { + let path = Path::default("config"); + + let display = format!("{path}"); + + assert_eq!(display, "config.{json,json5}") + } + + #[test] + fn display_strict_extension() { + let path = + Path::user_provided("config.json").expect("Should be valid since extension is valid"); + + let display = format!("{path}"); + + assert_eq!(display, "config.json") } } diff --git a/config/src/snapshot.rs b/config/src/snapshot.rs index ea949340767..e828e5635d0 100644 --- a/config/src/snapshot.rs +++ b/config/src/snapshot.rs @@ -1,5 +1,7 @@ //! Module for `SnapshotMaker`-related configuration and structs. +use std::path::PathBuf; + use iroha_config_base::derive::Proxy; use serde::{Deserialize, Serialize}; @@ -16,7 +18,8 @@ pub struct Configuration { /// The period of time to wait between attempts to create new snapshot. pub create_every_ms: u64, /// Path to the directory where snapshots should be stored - pub dir_path: String, + #[config(serde_as_str)] + pub dir_path: PathBuf, /// Flag to enable or disable snapshot creation pub creation_enabled: bool, } @@ -25,7 +28,7 @@ impl Default for ConfigurationProxy { fn default() -> Self { Self { create_every_ms: Some(DEFAULT_SNAPSHOT_CREATE_EVERY_MS), - dir_path: Some(DEFAULT_SNAPSHOT_PATH.to_owned()), + dir_path: Some(DEFAULT_SNAPSHOT_PATH.into()), creation_enabled: Some(DEFAULT_ENABLED), } } @@ -41,7 +44,7 @@ pub mod tests { pub fn arb_proxy() ( create_every_ms in prop::option::of(Just(DEFAULT_SNAPSHOT_CREATE_EVERY_MS)), - dir_path in prop::option::of(Just(DEFAULT_SNAPSHOT_PATH.to_owned())), + dir_path in prop::option::of(Just(DEFAULT_SNAPSHOT_PATH.into())), creation_enabled in prop::option::of(Just(DEFAULT_ENABLED)), ) -> ConfigurationProxy { diff --git a/configs/peer/config.json b/configs/peer/config.json index 11d5b354ce8..3f0dc2f87a9 100644 --- a/configs/peer/config.json +++ b/configs/peer/config.json @@ -1,7 +1,6 @@ { "PUBLIC_KEY": null, "PRIVATE_KEY": null, - "DISABLE_PANIC_TERMINAL_COLORS": false, "KURA": { "INIT_MODE": "strict", "BLOCK_STORE_PATH": "./storage", @@ -40,8 +39,9 @@ "FORMAT": "full" }, "GENESIS": { - "ACCOUNT_PUBLIC_KEY": null, - "ACCOUNT_PRIVATE_KEY": null + "PUBLIC_KEY": null, + "PRIVATE_KEY": null, + "FILE": null }, "WSV": { "ASSET_METADATA_LIMITS": { diff --git a/core/src/snapshot.rs b/core/src/snapshot.rs index e642dc01635..52aad1bd6ee 100644 --- a/core/src/snapshot.rs +++ b/core/src/snapshot.rs @@ -41,7 +41,7 @@ pub struct SnapshotMaker { /// Frequency at which snapshot is made snapshot_create_every: Duration, /// Path to the directory where snapshots are stored - snapshot_dir: String, + snapshot_dir: PathBuf, /// Flag to enable/disable snapshot creation snapshot_creation_enabled: bool, /// Flag to signal that new wsv is available for taking snapshot diff --git a/core/src/sumeragi/main_loop.rs b/core/src/sumeragi/main_loop.rs index 33e5e41515f..fcca60b867b 100644 --- a/core/src/sumeragi/main_loop.rs +++ b/core/src/sumeragi/main_loop.rs @@ -242,7 +242,7 @@ impl Sumeragi { assert_eq!(self.wsv.latest_block_hash(), None); let transactions: Vec<_> = genesis_network - .transactions + .into_transactions() .into_iter() .map(AcceptedTransaction::accept_genesis) .collect(); diff --git a/core/test_network/src/lib.rs b/core/test_network/src/lib.rs index bf61826e05c..9b217a69d59 100644 --- a/core/test_network/src/lib.rs +++ b/core/test_network/src/lib.rs @@ -68,13 +68,17 @@ pub fn get_key_pair() -> KeyPair { /// Trait used to differentiate a test instance of `genesis`. pub trait TestGenesis: Sized { - /// Construct Iroha genesis network and optionally submit genesis - /// from the given peer. - fn test(submit_genesis: bool) -> Option; + /// Construct Iroha genesis network + fn test() -> Self { + Self::test_with_instructions([]) + } + + /// Construct genesis network with additional instructions + fn test_with_instructions(extra_isi: impl IntoIterator) -> Self; } impl TestGenesis for GenesisNetwork { - fn test(submit_genesis: bool) -> Option { + fn test_with_instructions(extra_isi: impl IntoIterator) -> Self { let cfg = Configuration::test(); // TODO: Fix this somehow. Probably we need to make `kagami` a library (#3253). @@ -121,14 +125,16 @@ impl TestGenesis for GenesisNetwork { .append_instruction(Grant::permission_token(permission, alice_id.clone()).into()); } - if submit_genesis { - return Some( - GenesisNetwork::from_configuration(genesis, Some(&cfg.genesis)) - .expect("Failed to init genesis"), - ); + for isi in extra_isi.into_iter() { + first_transaction.append_instruction(isi); } - None + let key_pair = KeyPair::new( + cfg.genesis.public_key.clone(), + cfg.genesis.private_key.expect("Should be"), + ) + .expect("Genesis key pair should be valid"); + GenesisNetwork::new(genesis, &key_pair).expect("Failed to init genesis") } } @@ -215,7 +221,7 @@ impl Network { let peer = PeerBuilder::new() .with_configuration(config) - .with_into_genesis(GenesisNetwork::test(false)) + .with_genesis(GenesisNetwork::test()) .start() .await; @@ -256,7 +262,7 @@ impl Network { (n, builder) } }) - .map(|(n, builder)| builder.with_into_genesis(GenesisNetwork::test(n == 0))) + .map(|(n, builder)| builder.with_into_genesis((n == 0).then(GenesisNetwork::test))) .take(n_peers as usize) .collect::>(); let mut peers = builders @@ -408,7 +414,6 @@ impl Peer { }), public_key: self.key_pair.public_key().clone(), private_key: self.key_pair.private_key().clone(), - disable_panic_terminal_colors: true, ..configuration } } @@ -432,7 +437,7 @@ impl Peer { let handle = task::spawn( async move { - let mut iroha = Iroha::with_genesis(genesis, configuration, logger) + let mut iroha = Iroha::new(configuration, genesis, logger) .await .expect("Failed to start iroha"); let job_handle = iroha.start_as_task().unwrap(); @@ -556,8 +561,8 @@ impl PeerBuilder { /// Set the test genesis network. #[must_use] - pub fn with_test_genesis(self, submit_genesis: bool) -> Self { - self.with_into_genesis(GenesisNetwork::test(submit_genesis)) + pub fn with_test_genesis(self) -> Self { + self.with_into_genesis(GenesisNetwork::test()) } /// Set Iroha configuration @@ -603,7 +608,7 @@ impl PeerBuilder { config }); let genesis = match self.genesis { - WithGenesis::Default => GenesisNetwork::test(true), + WithGenesis::Default => Some(GenesisNetwork::test()), WithGenesis::None => None, WithGenesis::Has(genesis) => Some(genesis), }; diff --git a/docker-compose.dev.local.yml b/docker-compose.dev.local.yml index a1d0855dc04..b7e9c831877 100644 --- a/docker-compose.dev.local.yml +++ b/docker-compose.dev.local.yml @@ -7,12 +7,14 @@ services: build: ./ platform: linux/amd64 environment: + IROHA_CONFIG: /config/config.json IROHA_PUBLIC_KEY: ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB IROHA_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb"}' TORII_P2P_ADDR: iroha0:1337 TORII_API_URL: iroha0:8080 - IROHA_GENESIS_ACCOUNT_PUBLIC_KEY: ed01204164BF554923ECE1FD412D241036D863A6AE430476C898248B8237D77534CFC4 - IROHA_GENESIS_ACCOUNT_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"82b3bde54aebeca4146257da0de8d59d8e46d5fe34887dcd8072866792fcb3ad4164bf554923ece1fd412d241036d863a6ae430476c898248b8237d77534cfc4"}' + IROHA_GENESIS_PUBLIC_KEY: ed01204164BF554923ECE1FD412D241036D863A6AE430476C898248B8237D77534CFC4 + IROHA_GENESIS_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"82b3bde54aebeca4146257da0de8d59d8e46d5fe34887dcd8072866792fcb3ad4164bf554923ece1fd412d241036d863a6ae430476c898248b8237d77534cfc4"}' + IROHA_GENESIS_FILE: /config/genesis.json SUMERAGI_TRUSTED_PEERS: '[{"address":"iroha1:1338","public_key":"ed0120815BBDC9775D28C3633269B25F22D048E2AA2E36017CBE5AD85F15220BEB6F6F"},{"address":"iroha3:1340","public_key":"ed0120A66522370D60B9C09E79ADE2E9BB1EF2E78733A944B999B3A6AEE687CE476D61"},{"address":"iroha2:1339","public_key":"ed0120F417E0371E6ADB32FD66749477402B1AB67F84A8E9B082E997980CC91F327736"}]' ports: - 1337:1337 @@ -25,11 +27,12 @@ services: build: ./ platform: linux/amd64 environment: + IROHA_CONFIG: /config/config.json IROHA_PUBLIC_KEY: ed0120815BBDC9775D28C3633269B25F22D048E2AA2E36017CBE5AD85F15220BEB6F6F IROHA_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"c02ffad5e455e7ec620d74de5769681e4d8385906bce5a437eb67452a9efbbc2815bbdc9775d28c3633269b25f22d048e2aa2e36017cbe5ad85f15220beb6f6f"}' TORII_P2P_ADDR: iroha1:1338 TORII_API_URL: iroha1:8081 - IROHA_GENESIS_ACCOUNT_PUBLIC_KEY: ed01204164BF554923ECE1FD412D241036D863A6AE430476C898248B8237D77534CFC4 + IROHA_GENESIS_PUBLIC_KEY: ed01204164BF554923ECE1FD412D241036D863A6AE430476C898248B8237D77534CFC4 SUMERAGI_TRUSTED_PEERS: '[{"address":"iroha0:1337","public_key":"ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB"},{"address":"iroha3:1340","public_key":"ed0120A66522370D60B9C09E79ADE2E9BB1EF2E78733A944B999B3A6AEE687CE476D61"},{"address":"iroha2:1339","public_key":"ed0120F417E0371E6ADB32FD66749477402B1AB67F84A8E9B082E997980CC91F327736"}]' ports: - 1338:1338 @@ -41,11 +44,12 @@ services: build: ./ platform: linux/amd64 environment: + IROHA_CONFIG: /config/config.json IROHA_PUBLIC_KEY: ed0120F417E0371E6ADB32FD66749477402B1AB67F84A8E9B082E997980CC91F327736 IROHA_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"29c5ed1409cb10fd791bc4ff8a6cb5e22a5fae7e36f448ef3ea2988b1319a88bf417e0371e6adb32fd66749477402b1ab67f84a8e9b082e997980cc91f327736"}' TORII_P2P_ADDR: iroha2:1339 TORII_API_URL: iroha2:8082 - IROHA_GENESIS_ACCOUNT_PUBLIC_KEY: ed01204164BF554923ECE1FD412D241036D863A6AE430476C898248B8237D77534CFC4 + IROHA_GENESIS_PUBLIC_KEY: ed01204164BF554923ECE1FD412D241036D863A6AE430476C898248B8237D77534CFC4 SUMERAGI_TRUSTED_PEERS: '[{"address":"iroha1:1338","public_key":"ed0120815BBDC9775D28C3633269B25F22D048E2AA2E36017CBE5AD85F15220BEB6F6F"},{"address":"iroha0:1337","public_key":"ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB"},{"address":"iroha3:1340","public_key":"ed0120A66522370D60B9C09E79ADE2E9BB1EF2E78733A944B999B3A6AEE687CE476D61"}]' ports: - 1339:1339 @@ -57,11 +61,12 @@ services: build: ./ platform: linux/amd64 environment: + IROHA_CONFIG: /config/config.json IROHA_PUBLIC_KEY: ed0120A66522370D60B9C09E79ADE2E9BB1EF2E78733A944B999B3A6AEE687CE476D61 IROHA_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"5eed4855fad183c451aac39dfc50831607e4cf408c98e2b977f3ce4a2df42ce2a66522370d60b9c09e79ade2e9bb1ef2e78733a944b999b3a6aee687ce476d61"}' TORII_P2P_ADDR: iroha3:1340 TORII_API_URL: iroha3:8083 - IROHA_GENESIS_ACCOUNT_PUBLIC_KEY: ed01204164BF554923ECE1FD412D241036D863A6AE430476C898248B8237D77534CFC4 + IROHA_GENESIS_PUBLIC_KEY: ed01204164BF554923ECE1FD412D241036D863A6AE430476C898248B8237D77534CFC4 SUMERAGI_TRUSTED_PEERS: '[{"address":"iroha1:1338","public_key":"ed0120815BBDC9775D28C3633269B25F22D048E2AA2E36017CBE5AD85F15220BEB6F6F"},{"address":"iroha0:1337","public_key":"ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB"},{"address":"iroha2:1339","public_key":"ed0120F417E0371E6ADB32FD66749477402B1AB67F84A8E9B082E997980CC91F327736"}]' ports: - 1340:1340 diff --git a/docker-compose.dev.single.yml b/docker-compose.dev.single.yml index b817eece0c8..a6d0af1cdae 100644 --- a/docker-compose.dev.single.yml +++ b/docker-compose.dev.single.yml @@ -7,12 +7,14 @@ services: build: ./ platform: linux/amd64 environment: + IROHA_CONFIG: /config/config.json IROHA_PUBLIC_KEY: ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB IROHA_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb"}' TORII_P2P_ADDR: iroha0:1337 TORII_API_URL: iroha0:8080 - IROHA_GENESIS_ACCOUNT_PUBLIC_KEY: ed01204164BF554923ECE1FD412D241036D863A6AE430476C898248B8237D77534CFC4 - IROHA_GENESIS_ACCOUNT_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"82b3bde54aebeca4146257da0de8d59d8e46d5fe34887dcd8072866792fcb3ad4164bf554923ece1fd412d241036d863a6ae430476c898248b8237d77534cfc4"}' + IROHA_GENESIS_PUBLIC_KEY: ed01204164BF554923ECE1FD412D241036D863A6AE430476C898248B8237D77534CFC4 + IROHA_GENESIS_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"82b3bde54aebeca4146257da0de8d59d8e46d5fe34887dcd8072866792fcb3ad4164bf554923ece1fd412d241036d863a6ae430476c898248b8237d77534cfc4"}' + IROHA_GENESIS_FILE: /config/genesis.json ports: - 1337:1337 - 8080:8080 diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 54404425d99..a483a1c1b16 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -7,12 +7,14 @@ services: image: hyperledger/iroha2:dev platform: linux/amd64 environment: + IROHA_CONFIG: /config/config.json IROHA_PUBLIC_KEY: ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB IROHA_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb"}' TORII_P2P_ADDR: iroha0:1337 TORII_API_URL: iroha0:8080 - IROHA_GENESIS_ACCOUNT_PUBLIC_KEY: ed01204164BF554923ECE1FD412D241036D863A6AE430476C898248B8237D77534CFC4 - IROHA_GENESIS_ACCOUNT_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"82b3bde54aebeca4146257da0de8d59d8e46d5fe34887dcd8072866792fcb3ad4164bf554923ece1fd412d241036d863a6ae430476c898248b8237d77534cfc4"}' + IROHA_GENESIS_PUBLIC_KEY: ed01204164BF554923ECE1FD412D241036D863A6AE430476C898248B8237D77534CFC4 + IROHA_GENESIS_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"82b3bde54aebeca4146257da0de8d59d8e46d5fe34887dcd8072866792fcb3ad4164bf554923ece1fd412d241036d863a6ae430476c898248b8237d77534cfc4"}' + IROHA_GENESIS_FILE: /config/genesis.json SUMERAGI_TRUSTED_PEERS: '[{"address":"iroha1:1338","public_key":"ed0120815BBDC9775D28C3633269B25F22D048E2AA2E36017CBE5AD85F15220BEB6F6F"},{"address":"iroha3:1340","public_key":"ed0120A66522370D60B9C09E79ADE2E9BB1EF2E78733A944B999B3A6AEE687CE476D61"},{"address":"iroha2:1339","public_key":"ed0120F417E0371E6ADB32FD66749477402B1AB67F84A8E9B082E997980CC91F327736"}]' ports: - 1337:1337 @@ -25,11 +27,12 @@ services: image: hyperledger/iroha2:dev platform: linux/amd64 environment: + IROHA_CONFIG: /config/config.json IROHA_PUBLIC_KEY: ed0120815BBDC9775D28C3633269B25F22D048E2AA2E36017CBE5AD85F15220BEB6F6F IROHA_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"c02ffad5e455e7ec620d74de5769681e4d8385906bce5a437eb67452a9efbbc2815bbdc9775d28c3633269b25f22d048e2aa2e36017cbe5ad85f15220beb6f6f"}' TORII_P2P_ADDR: iroha1:1338 TORII_API_URL: iroha1:8081 - IROHA_GENESIS_ACCOUNT_PUBLIC_KEY: ed01204164BF554923ECE1FD412D241036D863A6AE430476C898248B8237D77534CFC4 + IROHA_GENESIS_PUBLIC_KEY: ed01204164BF554923ECE1FD412D241036D863A6AE430476C898248B8237D77534CFC4 SUMERAGI_TRUSTED_PEERS: '[{"address":"iroha0:1337","public_key":"ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB"},{"address":"iroha3:1340","public_key":"ed0120A66522370D60B9C09E79ADE2E9BB1EF2E78733A944B999B3A6AEE687CE476D61"},{"address":"iroha2:1339","public_key":"ed0120F417E0371E6ADB32FD66749477402B1AB67F84A8E9B082E997980CC91F327736"}]' ports: - 1338:1338 @@ -41,11 +44,12 @@ services: image: hyperledger/iroha2:dev platform: linux/amd64 environment: + IROHA_CONFIG: /config/config.json IROHA_PUBLIC_KEY: ed0120F417E0371E6ADB32FD66749477402B1AB67F84A8E9B082E997980CC91F327736 IROHA_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"29c5ed1409cb10fd791bc4ff8a6cb5e22a5fae7e36f448ef3ea2988b1319a88bf417e0371e6adb32fd66749477402b1ab67f84a8e9b082e997980cc91f327736"}' TORII_P2P_ADDR: iroha2:1339 TORII_API_URL: iroha2:8082 - IROHA_GENESIS_ACCOUNT_PUBLIC_KEY: ed01204164BF554923ECE1FD412D241036D863A6AE430476C898248B8237D77534CFC4 + IROHA_GENESIS_PUBLIC_KEY: ed01204164BF554923ECE1FD412D241036D863A6AE430476C898248B8237D77534CFC4 SUMERAGI_TRUSTED_PEERS: '[{"address":"iroha1:1338","public_key":"ed0120815BBDC9775D28C3633269B25F22D048E2AA2E36017CBE5AD85F15220BEB6F6F"},{"address":"iroha0:1337","public_key":"ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB"},{"address":"iroha3:1340","public_key":"ed0120A66522370D60B9C09E79ADE2E9BB1EF2E78733A944B999B3A6AEE687CE476D61"}]' ports: - 1339:1339 @@ -57,11 +61,12 @@ services: image: hyperledger/iroha2:dev platform: linux/amd64 environment: + IROHA_CONFIG: /config/config.json IROHA_PUBLIC_KEY: ed0120A66522370D60B9C09E79ADE2E9BB1EF2E78733A944B999B3A6AEE687CE476D61 IROHA_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"5eed4855fad183c451aac39dfc50831607e4cf408c98e2b977f3ce4a2df42ce2a66522370d60b9c09e79ade2e9bb1ef2e78733a944b999b3a6aee687ce476d61"}' TORII_P2P_ADDR: iroha3:1340 TORII_API_URL: iroha3:8083 - IROHA_GENESIS_ACCOUNT_PUBLIC_KEY: ed01204164BF554923ECE1FD412D241036D863A6AE430476C898248B8237D77534CFC4 + IROHA_GENESIS_PUBLIC_KEY: ed01204164BF554923ECE1FD412D241036D863A6AE430476C898248B8237D77534CFC4 SUMERAGI_TRUSTED_PEERS: '[{"address":"iroha1:1338","public_key":"ed0120815BBDC9775D28C3633269B25F22D048E2AA2E36017CBE5AD85F15220BEB6F6F"},{"address":"iroha0:1337","public_key":"ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB"},{"address":"iroha2:1339","public_key":"ed0120F417E0371E6ADB32FD66749477402B1AB67F84A8E9B082E997980CC91F327736"}]' ports: - 1340:1340 diff --git a/genesis/Cargo.toml b/genesis/Cargo.toml index 994afeb51a9..7727bc779c0 100644 --- a/genesis/Cargo.toml +++ b/genesis/Cargo.toml @@ -11,11 +11,8 @@ license.workspace = true workspace = true [dependencies] -iroha_config = { workspace = true } iroha_crypto = { workspace = true } iroha_data_model = { workspace = true, features = ["http"] } -iroha_logger = { workspace = true } -iroha_primitives = { workspace = true } iroha_schema = { workspace = true } derive_more = { workspace = true, features = ["deref"] } diff --git a/genesis/src/lib.rs b/genesis/src/lib.rs index 84cd9964e49..d32ebb22405 100644 --- a/genesis/src/lib.rs +++ b/genesis/src/lib.rs @@ -8,8 +8,7 @@ use std::{ }; use derive_more::From; -use eyre::{bail, eyre, ErrReport, Result, WrapErr}; -use iroha_config::genesis::Configuration; +use eyre::{eyre, ErrReport, Result, WrapErr}; use iroha_crypto::{KeyPair, PublicKey}; use iroha_data_model::{ asset::AssetDefinition, @@ -34,58 +33,50 @@ pub struct GenesisTransaction(pub SignedTransaction); /// [`GenesisNetwork`] contains initial transactions and genesis setup related parameters. #[derive(Debug, Clone)] pub struct GenesisNetwork { - /// transactions from `GenesisBlock`, any transacton is accepted - pub transactions: Vec, + /// Transactions from [`RawGenesisBlock`]. This vector is guaranteed to be non-empty, + /// unless [`GenesisNetwork::transactions_mut()`] is used. + transactions: Vec, } impl GenesisNetwork { /// Construct [`GenesisNetwork`] from configuration. /// /// # Errors - /// Fails if genesis block is not found or cannot be deserialized. - pub fn from_configuration( - raw_block: RawGenesisBlock, - genesis_config: Option<&Configuration>, - ) -> Result { - iroha_logger::debug!("Submitting genesis."); - let genesis_config = - genesis_config.expect("Should be `Some` when `submit_genesis` is true"); - let genesis_key_pair = KeyPair::new( - genesis_config.account_public_key.clone(), - genesis_config - .account_private_key - .clone() - .ok_or_else(|| eyre!("Genesis account private key is empty."))?, - )?; - #[cfg(not(test))] + /// - If fails to sign a transaction (which means that the `key_pair` is malformed rather + /// than anything else) + /// - If transactions set is empty + pub fn new(raw_block: RawGenesisBlock, genesis_key_pair: &KeyPair) -> Result { // First instruction should be Executor upgrade. // This makes possible to grant permissions to users in genesis. let transactions_iter = std::iter::once(GenesisTransactionBuilder { - isi: vec![Upgrade::new(Executor::try_from(raw_block.executor)?).into()], + isi: vec![Upgrade::new( + Executor::try_from(raw_block.executor) + .wrap_err("Failed to construct the executor")?, + ) + .into()], }) .chain(raw_block.transactions); - #[cfg(test)] - let transactions_iter = raw_block.transactions.into_iter(); - let transactions = transactions_iter - .map(|raw_transaction| { - raw_transaction.sign(genesis_key_pair.clone()) - }) .enumerate() - .filter_map(|(i, res)| { - res.map_err(|error| { - let error_msg = format!("{error:#}"); - iroha_logger::error!(error = %error_msg, transaction_num=i, "Genesis transaction failed") - }) - .ok() - }).map(GenesisTransaction) - .collect::>(); - if transactions.is_empty() { - bail!("Genesis transaction set contains no valid transactions"); - } + .map(|(i, raw_transaction)| { + raw_transaction + // FIXME: fix underlying chain of `.sign` so that it doesn't + // consume the key pair unnecessarily. It might be costly to clone + // the key pair for a large genesis. + .sign(genesis_key_pair.clone()) + .map(GenesisTransaction) + .wrap_err_with(|| eyre!("Failed to sign transaction at index {i}")) + }) + .collect::>>()?; + Ok(GenesisNetwork { transactions }) } + + /// Consume `self` into genesis transactions + pub fn into_transactions(self) -> Vec { + self.transactions + } } /// [`RawGenesisBlock`] is an initial block of the network @@ -106,19 +97,18 @@ impl RawGenesisBlock { /// # Errors /// If file not found or deserialization from file fails. pub fn from_path + Debug>(path: P) -> Result { - let file = File::open(&path).wrap_err(format!("Failed to open {:?}", &path))?; + let file = File::open(&path) + .wrap_err_with(|| eyre!("Failed to open {}", path.as_ref().display()))?; let size = file .metadata() .wrap_err("Unable to access genesis file metadata")? .len(); if size >= Self::WARN_ON_GENESIS_GTE { - iroha_logger::warn!(%size, threshold = %Self::WARN_ON_GENESIS_GTE, "Genesis is quite large, it will take some time to apply it"); + eprintln!("Genesis is quite large, it will take some time to apply it (size = {}, threshold = {})", size, Self::WARN_ON_GENESIS_GTE); } let reader = BufReader::new(file); - let mut raw_genesis_block: Self = serde_json::from_reader(reader).wrap_err(format!( - "Failed to deserialize raw genesis block from {:?}", - &path - ))?; + let mut raw_genesis_block: Self = serde_json::from_reader(reader) + .wrap_err_with(|| eyre!("Failed to deserialize raw genesis block from {:?}", &path))?; raw_genesis_block.executor.set_genesis_path(path); Ok(raw_genesis_block) } @@ -363,33 +353,27 @@ impl RawGenesisDomainBuilder { #[cfg(test)] mod tests { - use iroha_config::{base::proxy::Builder, genesis::ConfigurationProxy}; use super::*; fn dummy_executor() -> ExecutorMode { - ExecutorMode::Path(ExecutorPath("./executor.wasm".into())) + ExecutorMode::Inline(Executor::new(WasmSmartContract::from_compiled(vec![ + 1, 2, 3, + ]))) } #[test] fn load_new_genesis_block() -> Result<()> { - let (genesis_public_key, genesis_private_key) = KeyPair::generate()?.into(); + let genesis_key_pair = KeyPair::generate()?; let (alice_public_key, _) = KeyPair::generate()?.into(); - let _genesis_block = GenesisNetwork::from_configuration( + let _genesis_block = GenesisNetwork::new( RawGenesisBlockBuilder::default() .domain("wonderland".parse()?) .account("alice".parse()?, alice_public_key) .finish_domain() .executor(dummy_executor()) .build(), - Some( - &ConfigurationProxy { - account_public_key: Some(genesis_public_key), - account_private_key: Some(Some(genesis_private_key)), - } - .build() - .expect("Default genesis config should build when provided the `public key`"), - ), + &genesis_key_pair, )?; Ok(()) } diff --git a/scripts/test_env.py b/scripts/test_env.py index 458fc0ae6c1..cd72d89c5aa 100755 --- a/scripts/test_env.py +++ b/scripts/test_env.py @@ -29,29 +29,32 @@ def __init__(self, args: argparse.Namespace): self.out_dir = args.out_dir peers_dir = args.out_dir.joinpath("peers") os.makedirs(peers_dir, exist_ok=True) + self.shared_env = dict(os.environ) + + self.peers = [_Peer(args, i) for i in range(args.n_peers)] + try: shutil.copy2(f"{args.root_dir}/configs/peer/config.json", peers_dir) - shutil.copy2(f"{args.root_dir}/configs/peer/genesis.json", peers_dir) - shutil.copy2(f"{args.root_dir}/configs/peer/executor.wasm", peers_dir) + # genesis should be supplied only for the first peer + peer_0_dir = self.peers[0].peer_dir + shutil.copy2(f"{args.root_dir}/configs/peer/genesis.json", peer_0_dir) + # assuming that `genesis.json` contains path to the executor as `./executor.wasm` + shutil.copy2(f"{args.root_dir}/configs/peer/executor.wasm", peer_0_dir) except FileNotFoundError: logging.error(f"Some of the config files are missing. \ Please provide them in the `{args.root_dir}/configs/peer` directory") sys.exit(1) copy_or_prompt_build_bin("iroha", args.root_dir, peers_dir) - self.peers = [_Peer(args, i) for i in range(args.n_peers)] - - os.environ["IROHA2_CONFIG_PATH"] = str(peers_dir.joinpath("config.json")) - os.environ["IROHA2_GENESIS_PATH"] = str(peers_dir.joinpath("genesis.json")) - os.environ["IROHA_GENESIS_ACCOUNT_PUBLIC_KEY"] = self.peers[0].public_key - os.environ["IROHA_GENESIS_ACCOUNT_PRIVATE_KEY"] = self.peers[0].private_key + self.shared_env["IROHA_CONFIG"] = str(peers_dir.joinpath("config.json")) + self.shared_env["IROHA_GENESIS_PUBLIC_KEY"] = self.peers[0].public_key logging.info("Generating trusted peers...") self.trusted_peers = [] for peer in self.peers: peer_entry = {"address": f"{peer.host_ip}:{peer.p2p_port}", "public_key": peer.public_key} self.trusted_peers.append(json.dumps(peer_entry)) - os.environ["SUMERAGI_TRUSTED_PEERS"] = f"[{','.join(self.trusted_peers)}]" + self.shared_env["SUMERAGI_TRUSTED_PEERS"] = f"[{','.join(self.trusted_peers)}]" def wait_for_genesis(self, n_tries: int): for i in range(n_tries): @@ -73,9 +76,8 @@ def wait_for_genesis(self, n_tries: int): sys.exit(2) def run(self): - self.peers[0].run(is_genesis=True) - for peer in self.peers[1:]: - peer.run() + for i, peer in enumerate(self.peers): + peer.run(shared_env=self.shared_env, submit_genesis=(i == 0)) self.wait_for_genesis(20) class _Peer: @@ -91,6 +93,7 @@ def __init__(self, args: argparse.Namespace, nth: int): self.tokio_console_port = 5555 + nth self.out_dir = args.out_dir self.root_dir = args.root_dir + self.peer_dir = self.out_dir.joinpath(f"peers/{self.name}") self.host_ip = args.host_ip logging.info(f"Peer {self.name} generating key pair...") @@ -108,31 +111,37 @@ def __init__(self, args: argparse.Namespace, nth: int): self.public_key = json_keypair['public_key'] self.private_key = json.dumps(json_keypair['private_key']) + os.makedirs(self.peer_dir, exist_ok=True) + os.makedirs(self.peer_dir.joinpath("storage"), exist_ok=True) + logging.info(f"Peer {self.name} initialized") - def run(self, is_genesis: bool = False): + def run(self, shared_env: dict(), submit_genesis: bool = False): logging.info(f"Running peer {self.name}...") - peer_dir = self.out_dir.joinpath(f"peers/{self.name}") - os.makedirs(peer_dir, exist_ok=True) - os.makedirs(peer_dir.joinpath("storage"), exist_ok=True) - - os.environ["KURA_BLOCK_STORE_PATH"] = str(peer_dir.joinpath("storage")) - os.environ["SNAPSHOT_DIR_PATH"] = str(peer_dir.joinpath("storage")) - os.environ["LOG_LEVEL"] = "TRACE" - os.environ["LOG_FORMAT"] = "\"pretty\"" - os.environ["LOG_TOKIO_CONSOLE_ADDR"] = f"{self.host_ip}:{self.tokio_console_port}" - os.environ["IROHA_PUBLIC_KEY"] = self.public_key - os.environ["IROHA_PRIVATE_KEY"] = self.private_key - os.environ["SUMERAGI_DEBUG_FORCE_SOFT_FORK"] = "false" - os.environ["TORII_P2P_ADDR"] = f"{self.host_ip}:{self.p2p_port}" - os.environ["TORII_API_URL"] = f"{self.host_ip}:{self.api_port}" - - genesis_arg = "--submit-genesis" if is_genesis else "" + + peer_env = dict(shared_env) + peer_env["KURA_BLOCK_STORE_PATH"] = str(self.peer_dir.joinpath("storage")) + peer_env["SNAPSHOT_DIR_PATH"] = str(self.peer_dir.joinpath("storage")) + peer_env["LOG_LEVEL"] = "INFO" + peer_env["LOG_FORMAT"] = "\"pretty\"" + peer_env["LOG_TOKIO_CONSOLE_ADDR"] = f"{self.host_ip}:{self.tokio_console_port}" + peer_env["IROHA_PUBLIC_KEY"] = self.public_key + peer_env["IROHA_PRIVATE_KEY"] = self.private_key + peer_env["SUMERAGI_DEBUG_FORCE_SOFT_FORK"] = "false" + peer_env["TORII_P2P_ADDR"] = f"{self.host_ip}:{self.p2p_port}" + peer_env["TORII_API_URL"] = f"{self.host_ip}:{self.api_port}" + + if submit_genesis: + peer_env["IROHA_GENESIS_PRIVATE_KEY"] = self.private_key + # Assuming it was copied to the peer's directory + peer_env["IROHA_GENESIS_FILE"] = str(self.peer_dir.joinpath("genesis.json")) + # FD never gets closed - log_file = open(peer_dir.joinpath(".log"), "w") + stdout_file = open(self.peer_dir.joinpath(".stdout"), "w") + stderr_file = open(self.peer_dir.joinpath(".stderr"), "w") # These processes are created detached from the parent process already - subprocess.Popen([self.name, genesis_arg], executable=f"{self.out_dir}/peers/iroha", - stdout=log_file, stderr=subprocess.STDOUT) + subprocess.Popen([self.name] + (["--submit-genesis"] if submit_genesis else []), + executable=f"{self.out_dir}/peers/iroha", env=peer_env, stdout=stdout_file, stderr=stderr_file) def pos_int(arg): if int(arg) > 0: diff --git a/tools/kagami/src/config.rs b/tools/kagami/src/config.rs index 2d33d5b4a4b..e36b53fcd18 100644 --- a/tools/kagami/src/config.rs +++ b/tools/kagami/src/config.rs @@ -6,13 +6,13 @@ use iroha_primitives::small::SmallStr; use super::*; -#[derive(Parser, Debug, Clone, Copy)] +#[derive(Parser, Debug, Clone)] pub struct Args { #[clap(subcommand)] mode: Mode, } -#[derive(Subcommand, Debug, Clone, Copy)] +#[derive(Subcommand, Debug, Clone)] pub enum Mode { Client(client::Args), Peer(peer::Args), @@ -64,16 +64,30 @@ mod client { } mod peer { + use std::path::PathBuf; + use iroha_config::iroha::ConfigurationProxy as IrohaConfigurationProxy; use super::*; - #[derive(ClapArgs, Debug, Clone, Copy)] - pub struct Args; + #[derive(ClapArgs, Debug, Clone)] + pub struct Args { + /// Specifies the value of `genesis.file` configuration parameter. + /// + /// Note: relative paths are not resolved but included as-is. + #[arg(long, value_name = "PATH")] + genesis_file_in_config: Option, + } impl RunArgs for Args { fn run(self, writer: &mut BufWriter) -> Outcome { - let config = IrohaConfigurationProxy::default(); + let mut config = IrohaConfigurationProxy::default(); + + if let Some(path) = self.genesis_file_in_config { + let genesis = config.genesis.as_mut().unwrap(); + genesis.file = Some(Some(path)); + } + writeln!(writer, "{}", serde_json::to_string_pretty(&config)?) .wrap_err("Failed to write serialized peer configuration to the buffer.") } diff --git a/tools/swarm/src/cli.rs b/tools/swarm/src/cli.rs index eb7f45e9371..f7185e9519c 100644 --- a/tools/swarm/src/cli.rs +++ b/tools/swarm/src/cli.rs @@ -27,7 +27,7 @@ pub struct Cli { pub no_banner: bool, /// Path to a directory with Iroha configuration. It will be mapped as volume for containers. /// - /// The directory should contain `config.json` and `genesis.json`. + /// The directory should contain `config.json` and `genesis.json` #[arg(long, short)] pub config_dir: PathBuf, #[command(flatten)] diff --git a/tools/swarm/src/compose.rs b/tools/swarm/src/compose.rs index 36c43317750..80dc51f7ac4 100644 --- a/tools/swarm/src/compose.rs +++ b/tools/swarm/src/compose.rs @@ -20,6 +20,8 @@ use crate::{cli::SourceParsed, util::AbsolutePath}; /// Config directory inside of the docker image const DIR_CONFIG_IN_DOCKER: &str = "/config"; +const PATH_TO_CONFIG: &str = "/config/config.json"; +const PATH_TO_GENESIS: &str = "/config/genesis.json"; const GENESIS_KEYPAIR_SEED: &[u8; 7] = b"genesis"; const COMMAND_SUBMIT_GENESIS: &str = "iroha --submit-genesis"; const DOCKER_COMPOSE_VERSION: &str = "3.8"; @@ -105,7 +107,7 @@ impl DockerComposeService { source: ServiceSource, volumes: Vec<(String, String)>, trusted_peers: BTreeSet, - genesis_public_key: Option, + genesis_public_key: PublicKey, genesis_private_key: Option, ) -> Self { let ports = vec![ @@ -207,21 +209,23 @@ pub enum ServiceSource { #[derive(Serialize, Debug)] #[serde(rename_all = "UPPERCASE")] struct FullPeerEnv { + iroha_config: String, iroha_public_key: PublicKey, iroha_private_key: SerializeAsJsonStr, torii_p2p_addr: SocketAddr, torii_api_url: SocketAddr, + iroha_genesis_public_key: PublicKey, #[serde(skip_serializing_if = "Option::is_none")] - iroha_genesis_account_public_key: Option, + iroha_genesis_private_key: Option>, #[serde(skip_serializing_if = "Option::is_none")] - iroha_genesis_account_private_key: Option>, + iroha_genesis_file: Option, #[serde(skip_serializing_if = "Option::is_none")] sumeragi_trusted_peers: Option>>, } struct CompactPeerEnv { key_pair: KeyPair, - genesis_public_key: Option, + genesis_public_key: PublicKey, /// Genesis private key is only needed for a peer that is submitting the genesis block genesis_private_key: Option, p2p_addr: SocketAddr, @@ -231,11 +235,23 @@ struct CompactPeerEnv { impl From for FullPeerEnv { fn from(value: CompactPeerEnv) -> Self { + let (iroha_genesis_private_key, iroha_genesis_file) = + value + .genesis_private_key + .map_or((None, None), |private_key| { + ( + Some(private_key).map(SerializeAsJsonStr), + Some(PATH_TO_GENESIS.to_string()), + ) + }); + Self { + iroha_config: PATH_TO_CONFIG.to_string(), iroha_public_key: value.key_pair.public_key().clone(), iroha_private_key: SerializeAsJsonStr(value.key_pair.private_key().clone()), - iroha_genesis_account_public_key: value.genesis_public_key, - iroha_genesis_account_private_key: value.genesis_private_key.map(SerializeAsJsonStr), + iroha_genesis_public_key: value.genesis_public_key, + iroha_genesis_private_key, + iroha_genesis_file, torii_p2p_addr: value.p2p_addr, torii_api_url: value.api_addr, sumeragi_trusted_peers: if value.trusted_peers.is_empty() { @@ -321,7 +337,7 @@ impl DockerComposeBuilder<'_> { .filter(|trusted_peer| trusted_peer.public_key() != peer.key_pair.public_key()) .cloned() .collect(), - Some(genesis_key_pair.public_key().clone()), + genesis_key_pair.public_key().clone(), Some(genesis_key_pair.private_key().clone()), ); @@ -341,7 +357,7 @@ impl DockerComposeBuilder<'_> { }) .cloned() .collect(), - Some(genesis_key_pair.public_key().clone()), + genesis_key_pair.public_key().clone(), None, ); @@ -542,11 +558,11 @@ mod tests { } #[test] - fn default_config_with_swarm_env_are_exhaustive() { + fn default_config_with_swarm_env_is_exhaustive() { let keypair = KeyPair::generate().unwrap(); let env: TestEnv = CompactPeerEnv { key_pair: keypair.clone(), - genesis_public_key: Some(keypair.public_key().clone()), + genesis_public_key: keypair.public_key().clone(), genesis_private_key: Some(keypair.private_key().clone()), p2p_addr: SocketAddr::from_str("127.0.0.1:1337").unwrap(), api_addr: SocketAddr::from_str("127.0.0.1:1338").unwrap(), @@ -554,6 +570,8 @@ mod tests { } .into(); + // pretending like we've read `IROHA_CONFIG` env to know the config location + let _ = env.fetch("IROHA_CONFIG").expect("should be presented"); let proxy = ConfigurationProxy::default() .override_with(ConfigurationProxy::from_env(&env).expect("valid env")); @@ -591,7 +609,7 @@ mod tests { source: ServiceSource::Build(PathBuf::from(".")), environment: CompactPeerEnv { key_pair: key_pair.clone(), - genesis_public_key: Some(key_pair.public_key().clone()), + genesis_public_key: key_pair.public_key().clone(), genesis_private_key: Some(key_pair.private_key().clone()), p2p_addr: SocketAddr::from_str("iroha1:1339").unwrap(), api_addr: SocketAddr::from_str("iroha1:1338").unwrap(), @@ -624,12 +642,14 @@ mod tests { build: . platform: linux/amd64 environment: + IROHA_CONFIG: /config/config.json IROHA_PUBLIC_KEY: ed012039E5BF092186FACC358770792A493CA98A83740643A3D41389483CF334F748C8 IROHA_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"db9d90d20f969177bd5882f9fe211d14d1399d5440d04e3468783d169bbc4a8e39e5bf092186facc358770792a493ca98a83740643a3d41389483cf334f748c8"}' TORII_P2P_ADDR: iroha1:1339 TORII_API_URL: iroha1:1338 - IROHA_GENESIS_ACCOUNT_PUBLIC_KEY: ed012039E5BF092186FACC358770792A493CA98A83740643A3D41389483CF334F748C8 - IROHA_GENESIS_ACCOUNT_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"db9d90d20f969177bd5882f9fe211d14d1399d5440d04e3468783d169bbc4a8e39e5bf092186facc358770792a493ca98a83740643a3d41389483cf334f748c8"}' + IROHA_GENESIS_PUBLIC_KEY: ed012039E5BF092186FACC358770792A493CA98A83740643A3D41389483CF334F748C8 + IROHA_GENESIS_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"db9d90d20f969177bd5882f9fe211d14d1399d5440d04e3468783d169bbc4a8e39e5bf092186facc358770792a493ca98a83740643a3d41389483cf334f748c8"}' + IROHA_GENESIS_FILE: /config/genesis.json ports: - 1337:1337 - 8080:8080 @@ -650,7 +670,7 @@ mod tests { .unwrap(); let env: FullPeerEnv = CompactPeerEnv { key_pair: key_pair.clone(), - genesis_public_key: Some(key_pair.public_key().clone()), + genesis_public_key: key_pair.public_key().clone(), genesis_private_key: None, p2p_addr: SocketAddr::from_str("iroha0:1337").unwrap(), api_addr: SocketAddr::from_str("iroha0:1337").unwrap(), @@ -660,11 +680,12 @@ mod tests { let actual = serde_yaml::to_string(&env).unwrap(); let expected = expect_test::expect![[r#" + IROHA_CONFIG: /config/config.json IROHA_PUBLIC_KEY: ed0120415388A90FA238196737746A70565D041CFB32EAA0C89FF8CB244C7F832A6EBD IROHA_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"6bf163fd75192b81a78cb20c5f8cb917f591ac6635f2577e6ca305c27a456a5d415388a90fa238196737746a70565d041cfb32eaa0c89ff8cb244c7f832a6ebd"}' TORII_P2P_ADDR: iroha0:1337 TORII_API_URL: iroha0:1337 - IROHA_GENESIS_ACCOUNT_PUBLIC_KEY: ed0120415388A90FA238196737746A70565D041CFB32EAA0C89FF8CB244C7F832A6EBD + IROHA_GENESIS_PUBLIC_KEY: ed0120415388A90FA238196737746A70565D041CFB32EAA0C89FF8CB244C7F832A6EBD "#]]; expected.assert_eq(&actual); } @@ -698,12 +719,14 @@ mod tests { build: ./iroha-cloned platform: linux/amd64 environment: + IROHA_CONFIG: /config/config.json IROHA_PUBLIC_KEY: ed0120F0321EB4139163C35F88BF78520FF7071499D7F4E79854550028A196C7B49E13 IROHA_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"5f8d1291bf6b762ee748a87182345d135fd167062857aa4f20ba39f25e74c4b0f0321eb4139163c35f88bf78520ff7071499d7f4e79854550028a196c7b49e13"}' TORII_P2P_ADDR: iroha0:1337 TORII_API_URL: iroha0:8080 - IROHA_GENESIS_ACCOUNT_PUBLIC_KEY: ed01203420F48A9EEB12513B8EB7DAF71979CE80A1013F5F341C10DCDA4F6AA19F97A9 - IROHA_GENESIS_ACCOUNT_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"5a6d5f06a90d29ad906e2f6ea8b41b4ef187849d0d397081a4a15ffcbe71e7c73420f48a9eeb12513b8eb7daf71979ce80a1013f5f341c10dcda4f6aa19f97a9"}' + IROHA_GENESIS_PUBLIC_KEY: ed01203420F48A9EEB12513B8EB7DAF71979CE80A1013F5F341C10DCDA4F6AA19F97A9 + IROHA_GENESIS_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"5a6d5f06a90d29ad906e2f6ea8b41b4ef187849d0d397081a4a15ffcbe71e7c73420f48a9eeb12513b8eb7daf71979ce80a1013f5f341c10dcda4f6aa19f97a9"}' + IROHA_GENESIS_FILE: /config/genesis.json SUMERAGI_TRUSTED_PEERS: '[{"address":"iroha2:1339","public_key":"ed0120312C1B7B5DE23D366ADCF23CD6DB92CE18B2AA283C7D9F5033B969C2DC2B92F4"},{"address":"iroha3:1340","public_key":"ed0120854457B2E3D6082181DA73DC01C1E6F93A72D0C45268DC8845755287E98A5DEE"},{"address":"iroha1:1338","public_key":"ed0120A88554AA5C86D28D0EEBEC497235664433E807881CD31E12A1AF6C4D8B0F026C"}]' ports: - 1337:1337 @@ -716,11 +739,12 @@ mod tests { build: ./iroha-cloned platform: linux/amd64 environment: + IROHA_CONFIG: /config/config.json IROHA_PUBLIC_KEY: ed0120A88554AA5C86D28D0EEBEC497235664433E807881CD31E12A1AF6C4D8B0F026C IROHA_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"8d34d2c6a699c61e7a9d5aabbbd07629029dfb4f9a0800d65aa6570113edb465a88554aa5c86d28d0eebec497235664433e807881cd31e12a1af6c4d8b0f026c"}' TORII_P2P_ADDR: iroha1:1338 TORII_API_URL: iroha1:8081 - IROHA_GENESIS_ACCOUNT_PUBLIC_KEY: ed01203420F48A9EEB12513B8EB7DAF71979CE80A1013F5F341C10DCDA4F6AA19F97A9 + IROHA_GENESIS_PUBLIC_KEY: ed01203420F48A9EEB12513B8EB7DAF71979CE80A1013F5F341C10DCDA4F6AA19F97A9 SUMERAGI_TRUSTED_PEERS: '[{"address":"iroha2:1339","public_key":"ed0120312C1B7B5DE23D366ADCF23CD6DB92CE18B2AA283C7D9F5033B969C2DC2B92F4"},{"address":"iroha3:1340","public_key":"ed0120854457B2E3D6082181DA73DC01C1E6F93A72D0C45268DC8845755287E98A5DEE"},{"address":"iroha0:1337","public_key":"ed0120F0321EB4139163C35F88BF78520FF7071499D7F4E79854550028A196C7B49E13"}]' ports: - 1338:1338 @@ -732,11 +756,12 @@ mod tests { build: ./iroha-cloned platform: linux/amd64 environment: + IROHA_CONFIG: /config/config.json IROHA_PUBLIC_KEY: ed0120312C1B7B5DE23D366ADCF23CD6DB92CE18B2AA283C7D9F5033B969C2DC2B92F4 IROHA_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"cf4515a82289f312868027568c0da0ee3f0fde7fef1b69deb47b19fde7cbc169312c1b7b5de23d366adcf23cd6db92ce18b2aa283c7d9f5033b969c2dc2b92f4"}' TORII_P2P_ADDR: iroha2:1339 TORII_API_URL: iroha2:8082 - IROHA_GENESIS_ACCOUNT_PUBLIC_KEY: ed01203420F48A9EEB12513B8EB7DAF71979CE80A1013F5F341C10DCDA4F6AA19F97A9 + IROHA_GENESIS_PUBLIC_KEY: ed01203420F48A9EEB12513B8EB7DAF71979CE80A1013F5F341C10DCDA4F6AA19F97A9 SUMERAGI_TRUSTED_PEERS: '[{"address":"iroha3:1340","public_key":"ed0120854457B2E3D6082181DA73DC01C1E6F93A72D0C45268DC8845755287E98A5DEE"},{"address":"iroha1:1338","public_key":"ed0120A88554AA5C86D28D0EEBEC497235664433E807881CD31E12A1AF6C4D8B0F026C"},{"address":"iroha0:1337","public_key":"ed0120F0321EB4139163C35F88BF78520FF7071499D7F4E79854550028A196C7B49E13"}]' ports: - 1339:1339 @@ -748,11 +773,12 @@ mod tests { build: ./iroha-cloned platform: linux/amd64 environment: + IROHA_CONFIG: /config/config.json IROHA_PUBLIC_KEY: ed0120854457B2E3D6082181DA73DC01C1E6F93A72D0C45268DC8845755287E98A5DEE IROHA_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"ab0e99c2b845b4ac7b3e88d25a860793c7eb600a25c66c75cba0bae91e955aa6854457b2e3d6082181da73dc01c1e6f93a72d0c45268dc8845755287e98a5dee"}' TORII_P2P_ADDR: iroha3:1340 TORII_API_URL: iroha3:8083 - IROHA_GENESIS_ACCOUNT_PUBLIC_KEY: ed01203420F48A9EEB12513B8EB7DAF71979CE80A1013F5F341C10DCDA4F6AA19F97A9 + IROHA_GENESIS_PUBLIC_KEY: ed01203420F48A9EEB12513B8EB7DAF71979CE80A1013F5F341C10DCDA4F6AA19F97A9 SUMERAGI_TRUSTED_PEERS: '[{"address":"iroha2:1339","public_key":"ed0120312C1B7B5DE23D366ADCF23CD6DB92CE18B2AA283C7D9F5033B969C2DC2B92F4"},{"address":"iroha1:1338","public_key":"ed0120A88554AA5C86D28D0EEBEC497235664433E807881CD31E12A1AF6C4D8B0F026C"},{"address":"iroha0:1337","public_key":"ed0120F0321EB4139163C35F88BF78520FF7071499D7F4E79854550028A196C7B49E13"}]' ports: - 1340:1340 diff --git a/torii/src/lib.rs b/torii/src/lib.rs index 31245d6ffc0..83d9ce10a26 100644 --- a/torii/src/lib.rs +++ b/torii/src/lib.rs @@ -204,7 +204,7 @@ impl Torii { .with(warp::trace::request())) } - /// Start main api endpoints. + /// Start main API endpoints. /// /// # Errors /// Can fail due to listening to network or if http server fails @@ -268,19 +268,19 @@ pub enum Error { Query(#[from] iroha_data_model::ValidationFail), /// Failed to accept transaction AcceptTransaction(#[from] iroha_core::tx::AcceptTransactionFail), - /// Error while getting or setting configuration + /// Failed to get or set configuration Config(#[source] eyre::Report), /// Failed to push into queue PushIntoQueue(#[from] Box), #[cfg(feature = "telemetry")] - /// Error while getting Prometheus metrics + /// Failed to get Prometheus metrics Prometheus(#[source] eyre::Report), #[cfg(feature = "telemetry")] - /// Internal error while getting status + /// Failed to get status StatusFailure(#[source] eyre::Report), /// Failure caused by configuration subsystem ConfigurationFailure(#[from] KisoError), - /// Cannot find status segment by provided path + /// Failed to find status segment by provided path StatusSegmentNotFound(#[source] eyre::Report), } From 83de555f97d895805107dec695101ebfee49257f Mon Sep 17 00:00:00 2001 From: Shanin Roman Date: Thu, 21 Dec 2023 17:26:27 +0300 Subject: [PATCH 14/21] [fix] #4164: Fix topology update on restart Signed-off-by: Shanin Roman --- core/src/sumeragi/mod.rs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/core/src/sumeragi/mod.rs b/core/src/sumeragi/mod.rs index 8c82663ee6c..2bafc67e9f2 100644 --- a/core/src/sumeragi/mod.rs +++ b/core/src/sumeragi/mod.rs @@ -229,11 +229,14 @@ impl SumeragiHandle { fn replay_block( block: &SignedBlock, wsv: &mut WorldStateView, - current_topology: &Topology, + mut current_topology: Topology, ) -> Topology { - let block = ValidBlock::validate(block.clone(), current_topology, wsv) + // NOTE: topology need to be updated up to block's view_change_index + current_topology.rotate_all_n(block.payload().header.view_change_index); + + let block = ValidBlock::validate(block.clone(), ¤t_topology, wsv) .expect("Kura blocks should be valid") - .commit(current_topology) + .commit(¤t_topology) .expect("Kura blocks should be valid"); if block.payload().header.is_genesis() { @@ -293,14 +296,14 @@ 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(&block, &mut wsv, ¤t_topology); + current_topology = Self::replay_block(&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(&block, &mut wsv, ¤t_topology); + current_topology = Self::replay_block(&block, &mut wsv, current_topology); } info!("Sumeragi has finished loading blocks and setting up the WSV"); From ba01819a8037ddfd4fc03ccba970b3b8eee0e35e Mon Sep 17 00:00:00 2001 From: Daniil Polyakov Date: Wed, 13 Dec 2023 19:08:50 +0300 Subject: [PATCH 15/21] [fix] #4133: Count identical wasm for triggers Signed-off-by: Daniil Polyakov --- .../integration/triggers/by_call_trigger.rs | 55 +++++- core/src/smartcontracts/isi/triggers/set.rs | 181 ++++++++++++------ 2 files changed, 178 insertions(+), 58 deletions(-) diff --git a/client/tests/integration/triggers/by_call_trigger.rs b/client/tests/integration/triggers/by_call_trigger.rs index 7c2cf8aff41..f40963bc2ce 100644 --- a/client/tests/integration/triggers/by_call_trigger.rs +++ b/client/tests/integration/triggers/by_call_trigger.rs @@ -9,7 +9,7 @@ use iroha_client::{ transaction::Executable, }, }; -use iroha_data_model::events::TriggeringFilterBox; +use iroha_data_model::{events::TriggeringFilterBox, transaction::WasmSmartContract}; use iroha_genesis::GenesisNetwork; use iroha_logger::info; use test_network::*; @@ -480,6 +480,59 @@ fn trigger_burn_repetitions() -> Result<()> { Ok(()) } +#[test] +fn unregistering_one_of_two_triggers_with_identical_wasm_should_not_cause_original_wasm_loss( +) -> Result<()> { + let (_rt, _peer, test_client) = ::new().with_port(11_105).start_with_runtime(); + wait_for_genesis_committed(&vec![test_client.clone()], 0); + + let account_id = AccountId::from_str("alice@wonderland")?; + let first_trigger_id = TriggerId::from_str("mint_rose_1")?; + let second_trigger_id = TriggerId::from_str("mint_rose_2")?; + + let wasm = + iroha_wasm_builder::Builder::new("tests/integration/smartcontracts/mint_rose_trigger") + .show_output() + .build()? + .optimize()? + .into_bytes()?; + let wasm = WasmSmartContract::from_compiled(wasm); + + let build_trigger = |trigger_id: TriggerId| { + Trigger::new( + trigger_id.clone(), + Action::new( + wasm.clone(), + Repeats::Indefinitely, + account_id.clone(), + TriggeringFilterBox::ExecuteTrigger(ExecuteTriggerEventFilter::new( + trigger_id, + account_id.clone(), + )), + ), + ) + }; + + let first_trigger = build_trigger(first_trigger_id.clone()); + let second_trigger = build_trigger(second_trigger_id.clone()); + + test_client.submit_all_blocking([ + Register::trigger(first_trigger), + Register::trigger(second_trigger.clone()), + ])?; + + test_client.submit_blocking(Unregister::trigger(first_trigger_id))?; + let got_second_trigger = test_client + .request(FindTriggerById { + id: second_trigger_id, + }) + .expect("Failed to request second trigger"); + + assert_eq!(got_second_trigger, second_trigger); + + Ok(()) +} + fn get_asset_value(client: &mut Client, asset_id: AssetId) -> Result { let asset = client.request(client::asset::by_id(asset_id))?; Ok(*TryAsRef::::try_as_ref(asset.value())?) diff --git a/core/src/smartcontracts/isi/triggers/set.rs b/core/src/smartcontracts/isi/triggers/set.rs index 57e32ae1406..b8a41ea0b3e 100644 --- a/core/src/smartcontracts/isi/triggers/set.rs +++ b/core/src/smartcontracts/isi/triggers/set.rs @@ -9,9 +9,9 @@ //! trigger hooks. use core::cmp::min; -use std::fmt; +use std::{fmt, num::NonZeroU64}; -use indexmap::IndexMap; +use indexmap::{map::Entry, IndexMap}; use iroha_crypto::HashOf; use iroha_data_model::{ events::Filter as EventFilter, @@ -58,6 +58,15 @@ pub struct LoadedAction { pub metadata: Metadata, } +impl LoadedAction { + fn extract_blob_hash(&self) -> Option> { + match self.executable { + LoadedExecutable::Wasm(LoadedWasm { blob_hash, .. }) => Some(blob_hash), + LoadedExecutable::Instructions(_) => None, + } + } +} + /// Trait common for all `LoadedAction`s pub trait LoadedActionTrait { /// Get action executable @@ -133,6 +142,10 @@ impl + Clone> LoadedActionTrait for Loaded } } +/// [`WasmSmartContract`]s by [`TriggerId`]. +/// Stored together with number to count triggers with identical [`WasmSmartContract`]. +type WasmSmartContractMap = IndexMap, (WasmSmartContract, NonZeroU64)>; + /// Specialized structure that maps event filters to Triggers. // NB: `Set` has custom `Serialize` and `DeserializeSeed` implementations // which need to be manually updated when changing the struct @@ -149,7 +162,7 @@ pub struct Set { /// Trigger ids with type of events they process ids: IndexMap, /// Original [`WasmSmartContract`]s by [`TriggerId`] for querying purposes. - original_contracts: IndexMap, WasmSmartContract>, + original_contracts: WasmSmartContractMap, /// List of actions that should be triggered by events provided by `handle_*` methods. /// Vector is used to save the exact triggers order. matched_ids: Vec<(Event, TriggerId)>, @@ -159,7 +172,7 @@ pub struct Set { struct TriggersWithContext<'s, F> { /// Triggers being serialized triggers: &'s IndexMap>, - /// Containing Set, used for looking up origignal [`WasmSmartContract`]s + /// Containing Set, used for looking up original [`WasmSmartContract`]s /// during serialization. set: &'s Set, } @@ -189,24 +202,33 @@ impl Serialize for Set { where S: serde::Serializer, { + let &Self { + data_triggers, + pipeline_triggers, + time_triggers, + by_call_triggers, + ids, + original_contracts: _original_contracts, + matched_ids: _matched_ids, + } = &self; let mut set = serializer.serialize_struct("Set", 6)?; set.serialize_field( "data_triggers", - &TriggersWithContext::new(&self.data_triggers, self), + &TriggersWithContext::new(data_triggers, self), )?; set.serialize_field( "pipeline_triggers", - &TriggersWithContext::new(&self.pipeline_triggers, self), + &TriggersWithContext::new(pipeline_triggers, self), )?; set.serialize_field( "time_triggers", - &TriggersWithContext::new(&self.time_triggers, self), + &TriggersWithContext::new(time_triggers, self), )?; set.serialize_field( "by_call_triggers", - &TriggersWithContext::new(&self.by_call_triggers, self), + &TriggersWithContext::new(by_call_triggers, self), )?; - set.serialize_field("ids", &self.ids)?; + set.serialize_field("ids", ids)?; set.end() } } @@ -411,7 +433,16 @@ impl Set { blob_hash: hash, }); // Store original executable representation to respond to queries with. - self.original_contracts.insert(hash, bytes); + self.original_contracts + .entry(hash) + .and_modify(|(_, count)| { + // Considering 1 trigger registration takes 1 second, + // it would take 584 942 417 355 years to overflow. + *count = count.checked_add(1).expect( + "There is no way someone could register 2^64 amount of same triggers", + ) + }) + .or_insert((bytes, NonZeroU64::MIN)); loaded } Executable::Instructions(instructions) => LoadedExecutable::Instructions(instructions), @@ -438,7 +469,9 @@ impl Set { &self, hash: &HashOf, ) -> Option<&WasmSmartContract> { - self.original_contracts.get(hash) + self.original_contracts + .get(hash) + .map(|(contract, _)| contract) } /// Convert [`LoadedAction`] to original [`Action`] by retrieving original @@ -633,52 +666,86 @@ impl Set { /// Remove a trigger from the [`Set`]. /// /// Return `false` if [`Set`] doesn't contain the trigger with the given `id`. + /// + /// # Panics + /// + /// Panics on inconsistent state of [`Set`]. This is a bug. pub fn remove(&mut self, id: &TriggerId) -> bool { - // Used in a map that requires this signature - #[allow(clippy::needless_pass_by_value)] - fn extract_blob_hash(action: LoadedAction) -> Option> { - match action.executable { - LoadedExecutable::Wasm(LoadedWasm { blob_hash, .. }) => Some(blob_hash), - LoadedExecutable::Instructions(_) => None, - } - } - let Some(event_type) = self.ids.remove(id) else { return false; }; - let blob_hash = match event_type { - TriggeringEventType::Data => self - .data_triggers - .remove(id) - .map(extract_blob_hash) - .expect("`Set::data_triggers` doesn't contain required id. This is a bug"), - TriggeringEventType::Pipeline => self - .pipeline_triggers - .remove(id) - .map(extract_blob_hash) - .expect("`Set::pipeline_triggers` doesn't contain required id. This is a bug"), - TriggeringEventType::Time => self - .time_triggers - .remove(id) - .map(extract_blob_hash) - .expect("`Set::time_triggers` doesn't contain required id. This is a bug"), - TriggeringEventType::ExecuteTrigger => self - .by_call_triggers - .remove(id) - .map(extract_blob_hash) - .expect("`Set::by_call_triggers` doesn't contain required id. This is a bug"), + let removed = match event_type { + TriggeringEventType::Data => { + Self::remove_from(&mut self.original_contracts, &mut self.data_triggers, id) + } + TriggeringEventType::Pipeline => Self::remove_from( + &mut self.original_contracts, + &mut self.pipeline_triggers, + id, + ), + TriggeringEventType::Time => { + Self::remove_from(&mut self.original_contracts, &mut self.time_triggers, id) + } + TriggeringEventType::ExecuteTrigger => { + Self::remove_from(&mut self.original_contracts, &mut self.by_call_triggers, id) + } }; - if let Some(blob_hash) = blob_hash { - self.original_contracts - .remove(&blob_hash) - .expect("`Set::original_contracts` doesn't contain required hash. This is a bug"); - } + assert!( + removed, + "`Set`'s `ids` and typed trigger collections are inconsistent. This is a bug" + ); true } + /// Remove trigger from `triggers` and decrease the counter of the original [`WasmSmartContract`]. + /// + /// Note that this function doesn't remove the trigger from [`Set::ids`]. + /// + /// Returns `true` if trigger was removed and `false` otherwise. + fn remove_from( + original_contracts: &mut WasmSmartContractMap, + triggers: &mut IndexMap>, + trigger_id: &TriggerId, + ) -> bool { + triggers + .remove(trigger_id) + .map(|loaded_action| { + if let Some(blob_hash) = loaded_action.extract_blob_hash() { + Self::remove_original_trigger(original_contracts, blob_hash); + } + }) + .is_some() + } + + /// Decrease the counter of the original [`WasmSmartContract`] by `blob_hash` + /// or remove it if the counter reaches zero. + /// + /// # Panics + /// + /// Panics if `blob_hash` is not in the [`Set::original_contracts`]. + fn remove_original_trigger( + original_contracts: &mut WasmSmartContractMap, + blob_hash: HashOf, + ) { + #[allow(clippy::option_if_let_else)] // More readable this way + match original_contracts.entry(blob_hash) { + Entry::Occupied(mut entry) => { + let count = &mut entry.get_mut().1; + if let Some(new_count) = NonZeroU64::new(count.get() - 1) { + *count = new_count; + } else { + entry.remove(); + } + } + Entry::Vacant(_) => { + panic!("`Set::original_contracts` doesn't contain required hash. This is a bug") + } + } + } + /// Check if [`Set`] contains `id`. #[inline] pub fn contains(&self, id: &TriggerId) -> bool { @@ -807,35 +874,35 @@ impl Set { time_triggers, by_call_triggers, ids, + original_contracts, .. } = self; - Self::remove_zeros(ids, data_triggers); - Self::remove_zeros(ids, pipeline_triggers); - Self::remove_zeros(ids, time_triggers); - Self::remove_zeros(ids, by_call_triggers); + Self::remove_zeros(ids, original_contracts, data_triggers); + Self::remove_zeros(ids, original_contracts, pipeline_triggers); + Self::remove_zeros(ids, original_contracts, time_triggers); + Self::remove_zeros(ids, original_contracts, by_call_triggers); } /// Remove actions with zero execution count from `triggers` fn remove_zeros( ids: &mut IndexMap, + original_contracts: &mut WasmSmartContractMap, triggers: &mut IndexMap>, ) { let to_remove: Vec = triggers .iter() .filter_map(|(id, action)| { - if let Repeats::Exactly(repeats) = action.repeats { - if repeats == 0 { - return Some(id.clone()); - } + if let Repeats::Exactly(0) = action.repeats { + return Some(id.clone()); } None }) .collect(); for id in to_remove { - triggers.remove(&id).and_then(|_| ids.remove(&id)).expect( - "Removing existing keys from `Set` should be always possible. This is a bug", - ); + ids.remove(&id) + .and_then(|_| Self::remove_from(original_contracts, triggers, &id).then_some(())) + .expect("`Set`'s `ids`, `original_contracts` and typed trigger collections are inconsistent. This is a bug") } } From 40914549e85c521cb9f2e9e3c390bf61e9c19254 Mon Sep 17 00:00:00 2001 From: Nikita Strygin Date: Mon, 25 Dec 2023 14:50:18 +0300 Subject: [PATCH 16/21] [fix] #4155: ensure the secp256k1 signatures coming out of OpenSSL are normalized This is an interoperability problem between OpenSSL and newer cryptographic libraries, see [https://github.com/bitcoin/bips/blob/master/bip-0062.mediawiki#user-content-Low_S_values_in_signatures)[BIP-0062] for details This fixes the flaky secp256k1_sign test Also includes a drive-by cleanup of useless `assert(result.is_ok())` Signed-off-by: Nikita Strygin --- crypto/src/signature/secp256k1.rs | 84 ++++++++++++++++++------------- 1 file changed, 48 insertions(+), 36 deletions(-) diff --git a/crypto/src/signature/secp256k1.rs b/crypto/src/signature/secp256k1.rs index 4d7ec814141..1939213af37 100644 --- a/crypto/src/signature/secp256k1.rs +++ b/crypto/src/signature/secp256k1.rs @@ -148,8 +148,8 @@ mod test { #[test] fn secp256k1_load_keys() { let secret = PrivateKey::from_hex(ALGORITHM, PRIVATE_KEY).unwrap(); - let sres = EcdsaSecp256k1Sha256::keypair(Some(KeyGenOption::FromPrivateKey(secret))); - assert!(sres.is_ok()); + let _sres = + EcdsaSecp256k1Sha256::keypair(Some(KeyGenOption::FromPrivateKey(secret))).unwrap(); } #[test] @@ -158,16 +158,14 @@ mod test { let (p, s) = EcdsaSecp256k1Sha256::keypair(Some(KeyGenOption::FromPrivateKey(secret))).unwrap(); - let sk = secp256k1::SecretKey::from_slice(s.payload()); - assert!(sk.is_ok()); - let pk = secp256k1::PublicKey::from_slice(p.payload()); - assert!(pk.is_ok()); + let _sk = secp256k1::SecretKey::from_slice(s.payload()).unwrap(); + let _pk = secp256k1::PublicKey::from_slice(p.payload()).unwrap(); let openssl_group = EcGroup::from_curve_name(Nid::SECP256K1).unwrap(); let mut ctx = BigNumContext::new().unwrap(); - let openssl_point = - EcPoint::from_bytes(&openssl_group, &public_key_uncompressed(&p)[..], &mut ctx); - assert!(openssl_point.is_ok()); + let _openssl_point = + EcPoint::from_bytes(&openssl_group, &public_key_uncompressed(&p)[..], &mut ctx) + .unwrap(); } #[test] @@ -179,7 +177,8 @@ mod test { hex::decode(SIGNATURE_1).unwrap().as_slice(), &p, ); - assert!(result.is_ok()); + // we are returning a `Result` + // unwrap will catch the `Err(_)`, and assert will catch the `false` assert!(result.unwrap()); let context = secp256k1::Secp256k1::new(); @@ -189,13 +188,11 @@ mod test { let h = sha2::Sha256::digest(MESSAGE_1); let msg = secp256k1::Message::from_digest_slice(h.as_slice()).unwrap(); - //Check if signatures produced here can be verified by secp256k1 - let mut signature = + // Check if signatures produced here can be verified by secp256k1 + let signature = secp256k1::ecdsa::Signature::from_compact(&hex::decode(SIGNATURE_1).unwrap()[..]) .unwrap(); - signature.normalize_s(); - let result = context.verify_ecdsa(&msg, &signature, &pk); - assert!(result.is_ok()); + context.verify_ecdsa(&msg, &signature, &pk).unwrap(); let openssl_group = EcGroup::from_curve_name(Nid::SECP256K1).unwrap(); let mut ctx = BigNumContext::new().unwrap(); @@ -209,19 +206,19 @@ mod test { let openssl_s = BigNum::from_hex_str(s).unwrap(); let openssl_sig = EcdsaSig::from_private_components(openssl_r, openssl_s).unwrap(); let openssl_result = openssl_sig.verify(h.as_slice(), &openssl_pkey); - assert!(openssl_result.is_ok()); assert!(openssl_result.unwrap()); } #[test] fn secp256k1_sign() { let secret = PrivateKey::from_hex(ALGORITHM, PRIVATE_KEY).unwrap(); - let (p, s) = + let (pk, sk) = EcdsaSecp256k1Sha256::keypair(Some(KeyGenOption::FromPrivateKey(secret))).unwrap(); - let sig = EcdsaSecp256k1Sha256::sign(MESSAGE_1, &s).unwrap(); - let result = EcdsaSecp256k1Sha256::verify(MESSAGE_1, &sig, &p); - assert!(result.is_ok()); + let sig = EcdsaSecp256k1Sha256::sign(MESSAGE_1, &sk).unwrap(); + let result = EcdsaSecp256k1Sha256::verify(MESSAGE_1, &sig, &pk); + // we are returning a `Result` + // unwrap will catch the `Err(_)`, and assert will catch the `false` assert!(result.unwrap()); assert_eq!(sig.len(), 64); @@ -237,16 +234,13 @@ mod test { let msg = secp256k1::Message::from_digest_slice(h.as_slice()).unwrap(); let sig_1 = context.sign_ecdsa(&msg, &sk).serialize_compact(); - let result = EcdsaSecp256k1Sha256::verify(MESSAGE_1, &sig_1, &p); - - assert!(result.is_ok()); + let result = EcdsaSecp256k1Sha256::verify(MESSAGE_1, &sig_1, &pk); assert!(result.unwrap()); let openssl_group = EcGroup::from_curve_name(Nid::SECP256K1).unwrap(); let mut ctx = BigNumContext::new().unwrap(); let openssl_point = - EcPoint::from_bytes(&openssl_group, &public_key_uncompressed(&p)[..], &mut ctx) - .unwrap(); + EcPoint::from_bytes(&openssl_group, &public_key_uncompressed(&pk), &mut ctx).unwrap(); let openssl_public_key = EcKey::from_public_key(&openssl_group, &openssl_point).unwrap(); let openssl_secret_key = EcKey::from_private_components( &openssl_group, @@ -257,23 +251,41 @@ mod test { let openssl_sig = EcdsaSig::sign(h.as_slice(), &openssl_secret_key).unwrap(); let openssl_result = openssl_sig.verify(h.as_slice(), &openssl_public_key); - assert!(openssl_result.is_ok()); assert!(openssl_result.unwrap()); - let mut temp_sig = Vec::new(); - temp_sig.extend(openssl_sig.r().to_vec()); - temp_sig.extend(openssl_sig.s().to_vec()); - - // secp256k1 expects normalized "s"'s. - // scheme.normalize_s(temp_sig.as_mut_slice()).unwrap(); - // k256 seems to be normalizing always now - let result = EcdsaSecp256k1Sha256::verify(MESSAGE_1, temp_sig.as_slice(), &p); - assert!(result.is_ok()); + + let openssl_sig = { + use std::ops::{Shr, Sub}; + + // ensure the S value is "low" (see BIP-0062) https://github.com/bitcoin/bips/blob/master/bip-0062.mediawiki#user-content-Low_S_values_in_signatures + // this is required for k256 to successfully verify the signature, as it will fail verification of any signature with a High S value + // Based on https://github.com/bitcoin/bitcoin/blob/v0.9.3/src/key.cpp#L202-L227 + // this is only required for interoperability with OpenSSL + // if we are only using signatures from iroha_crypto, all of this dance is not necessary + let mut s = openssl_sig.s().to_owned().unwrap(); + let mut order = BigNum::new().unwrap(); + openssl_group.order(&mut order, &mut ctx).unwrap(); + let half_order = order.shr(1); + + // if the S is "high" (s > half_order), convert it to "low" form (order - s) + if s.cmp(&half_order) == std::cmp::Ordering::Greater { + s = order.sub(&s); + } + + let r = openssl_sig.r(); + + // serialize the key + let mut res = Vec::new(); + res.extend(r.to_vec()); + res.extend(s.to_vec()); + res + }; + + let result = EcdsaSecp256k1Sha256::verify(MESSAGE_1, openssl_sig.as_slice(), &pk); assert!(result.unwrap()); let (p, s) = EcdsaSecp256k1Sha256::keypair(None).unwrap(); let signed = EcdsaSecp256k1Sha256::sign(MESSAGE_1, &s).unwrap(); let result = EcdsaSecp256k1Sha256::verify(MESSAGE_1, &signed, &p); - assert!(result.is_ok()); assert!(result.unwrap()); } } From 390e955de931306a788428f6ded9dec640af745b Mon Sep 17 00:00:00 2001 From: Nikita Strygin Date: Mon, 25 Dec 2023 11:40:34 +0300 Subject: [PATCH 17/21] [refactor] #3068: remove flattened events Signed-off-by: Nikita Strygin --- client/tests/integration/events/data.rs | 16 ++-- .../integration/triggers/data_trigger.rs | 22 ++++- .../integration/triggers/event_trigger.rs | 12 ++- configs/peer/executor.wasm | Bin 389245 -> 390610 bytes core/src/smartcontracts/isi/domain.rs | 2 +- core/src/wsv.rs | 8 +- data_model/src/events/data/events.rs | 80 +----------------- data_model/src/events/data/filters.rs | 78 +++++++++++------ docs/source/references/schema.json | 44 ++-------- tools/parity_scale_decoder/build.rs | 10 +-- .../parity_scale_decoder/samples/trigger.bin | Bin 76 -> 80 bytes .../parity_scale_decoder/samples/trigger.json | 7 +- tools/parity_scale_decoder/src/main.rs | 7 +- 13 files changed, 118 insertions(+), 168 deletions(-) diff --git a/client/tests/integration/events/data.rs b/client/tests/integration/events/data.rs index d3aa2a96834..9f914bb5bb3 100644 --- a/client/tests/integration/events/data.rs +++ b/client/tests/integration/events/data.rs @@ -181,46 +181,46 @@ fn produce_multiple_events() -> Result<()> { } let expected_domain_events: Vec = [ - WorldEvent::Domain(DomainEvent::Account(AccountEvent::PermissionAdded( + DataEvent::Domain(DomainEvent::Account(AccountEvent::PermissionAdded( AccountPermissionChanged { account_id: bob_id.clone(), permission_id: token_1.definition_id.clone(), }, ))), - WorldEvent::Domain(DomainEvent::Account(AccountEvent::PermissionAdded( + DataEvent::Domain(DomainEvent::Account(AccountEvent::PermissionAdded( AccountPermissionChanged { account_id: bob_id.clone(), permission_id: token_2.definition_id.clone(), }, ))), - WorldEvent::Domain(DomainEvent::Account(AccountEvent::RoleGranted( + DataEvent::Domain(DomainEvent::Account(AccountEvent::RoleGranted( AccountRoleChanged { account_id: bob_id.clone(), role_id: role_id.clone(), }, ))), - WorldEvent::Domain(DomainEvent::Account(AccountEvent::PermissionRemoved( + DataEvent::Domain(DomainEvent::Account(AccountEvent::PermissionRemoved( AccountPermissionChanged { account_id: bob_id.clone(), permission_id: token_1.definition_id, }, ))), - WorldEvent::Domain(DomainEvent::Account(AccountEvent::PermissionRemoved( + DataEvent::Domain(DomainEvent::Account(AccountEvent::PermissionRemoved( AccountPermissionChanged { account_id: bob_id.clone(), permission_id: token_2.definition_id, }, ))), - WorldEvent::Domain(DomainEvent::Account(AccountEvent::RoleRevoked( + DataEvent::Domain(DomainEvent::Account(AccountEvent::RoleRevoked( AccountRoleChanged { account_id: bob_id, role_id: role_id.clone(), }, ))), - WorldEvent::Role(RoleEvent::Deleted(role_id)), + DataEvent::Role(RoleEvent::Deleted(role_id)), ] .into_iter() - .flat_map(WorldEvent::flatten) + .map(Into::into) .collect(); for expected_event in expected_domain_events { diff --git a/client/tests/integration/triggers/data_trigger.rs b/client/tests/integration/triggers/data_trigger.rs index e7096f7b024..2c197c9da18 100644 --- a/client/tests/integration/triggers/data_trigger.rs +++ b/client/tests/integration/triggers/data_trigger.rs @@ -20,8 +20,15 @@ fn must_execute_both_triggers() -> Result<()> { [instruction.clone()], Repeats::Indefinitely, account_id.clone(), - TriggeringFilterBox::Data(BySome(DataEntityFilter::ByAccount(BySome( - AccountFilter::new(AcceptAll, BySome(AccountEventFilter::ByCreated)), + // FIXME: rewrite the filters using the builder DSL https://github.com/hyperledger/iroha/issues/3068 + TriggeringFilterBox::Data(BySome(DataEntityFilter::ByDomain(BySome( + DomainFilter::new( + AcceptAll, + BySome(DomainEventFilter::ByAccount(BySome(AccountFilter::new( + AcceptAll, + BySome(AccountEventFilter::ByCreated), + )))), + ), )))), ), )); @@ -86,8 +93,15 @@ fn domain_scoped_trigger_must_be_executed_only_on_events_in_its_domain() -> Resu [Mint::asset_quantity(1_u32, asset_id.clone())], Repeats::Indefinitely, account_id, - TriggeringFilterBox::Data(BySome(DataEntityFilter::ByAccount(BySome( - AccountFilter::new(AcceptAll, BySome(AccountEventFilter::ByCreated)), + // FIXME: rewrite the filters using the builder DSL https://github.com/hyperledger/iroha/issues/3068 + TriggeringFilterBox::Data(BySome(DataEntityFilter::ByDomain(BySome( + DomainFilter::new( + AcceptAll, + BySome(DomainEventFilter::ByAccount(BySome(AccountFilter::new( + AcceptAll, + BySome(AccountEventFilter::ByCreated), + )))), + ), )))), ), )); diff --git a/client/tests/integration/triggers/event_trigger.rs b/client/tests/integration/triggers/event_trigger.rs index 8269a244ad4..bc340775aff 100644 --- a/client/tests/integration/triggers/event_trigger.rs +++ b/client/tests/integration/triggers/event_trigger.rs @@ -24,10 +24,16 @@ fn test_mint_asset_when_new_asset_definition_created() -> Result<()> { vec![instruction], Repeats::Indefinitely, account_id, - TriggeringFilterBox::Data(BySome(DataEntityFilter::ByAssetDefinition(BySome( - AssetDefinitionFilter::new( + // FIXME: rewrite the filters using the builder DSL https://github.com/hyperledger/iroha/issues/3068 + TriggeringFilterBox::Data(BySome(DataEntityFilter::ByDomain(BySome( + DomainFilter::new( AcceptAll, - BySome(AssetDefinitionEventFilter::ByCreated), + BySome(DomainEventFilter::ByAssetDefinition(BySome( + AssetDefinitionFilter::new( + AcceptAll, + BySome(AssetDefinitionEventFilter::ByCreated), + ), + ))), ), )))), ), diff --git a/configs/peer/executor.wasm b/configs/peer/executor.wasm index 48c0f25b41cc2e2c2b2ef6a26a9d75b19fd5ad4d..7af36698d6cf6663f58b1f0a0e07e4bcce86101e 100644 GIT binary patch delta 135708 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 delta 132396 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 diff --git a/core/src/smartcontracts/isi/domain.rs b/core/src/smartcontracts/isi/domain.rs index 81855321e7c..2912b511b09 100644 --- a/core/src/smartcontracts/isi/domain.rs +++ b/core/src/smartcontracts/isi/domain.rs @@ -179,7 +179,7 @@ pub mod isi { domain.remove_asset_total_quantity(&asset_definition_id); - events.push(WorldEvent::from(DomainEvent::AssetDefinition( + events.push(DataEvent::from(DomainEvent::AssetDefinition( AssetDefinitionEvent::Deleted(asset_definition_id), ))); diff --git a/core/src/wsv.rs b/core/src/wsv.rs index 0e653e6c488..2bbf58ff7ca 100644 --- a/core/src/wsv.rs +++ b/core/src/wsv.rs @@ -1252,7 +1252,7 @@ impl WorldStateView { /// The function puts events produced by iterator into `events_buffer`. /// Events should be produced in the order of expanding scope: from specific to general. /// Example: account events before domain events. - pub fn emit_events, T: Into>(&mut self, world_events: I) { + pub fn emit_events, T: Into>(&mut self, world_events: I) { Self::emit_events_impl( &mut self.world.triggers, &mut self.events_buffer, @@ -1263,7 +1263,7 @@ impl WorldStateView { /// Implementation of [`Self::emit_events()`]. /// /// Usable when you can't call [`Self::emit_events()`] due to mutable reference to self. - fn emit_events_impl, T: Into>( + fn emit_events_impl, T: Into>( triggers: &mut TriggerSet, events_buffer: &mut Vec, world_events: I, @@ -1271,7 +1271,7 @@ impl WorldStateView { let data_events: SmallVec<[DataEvent; 3]> = world_events .into_iter() .map(Into::into) - .flat_map(WorldEvent::flatten) + .map(Into::into) .collect(); for event in data_events.iter() { @@ -1285,7 +1285,7 @@ impl WorldStateView { /// Produces [`PermissionTokenSchemaUpdateEvent`]. pub fn set_permission_token_schema(&mut self, schema: PermissionTokenSchema) { let old_schema = std::mem::replace(&mut self.world.permission_token_schema, schema.clone()); - self.emit_events(std::iter::once(WorldEvent::PermissionTokenSchemaUpdate( + self.emit_events(std::iter::once(DataEvent::PermissionToken( PermissionTokenSchemaUpdateEvent { old_schema, new_schema: schema, diff --git a/data_model/src/events/data/events.rs b/data_model/src/events/data/events.rs index 90f1caf2cf8..c08b9e504c0 100644 --- a/data_model/src/events/data/events.rs +++ b/data_model/src/events/data/events.rs @@ -3,7 +3,6 @@ use getset::Getters; use iroha_data_model_derive::{model, Filter, HasOrigin}; -use iroha_primitives::small::SmallVec; pub use self::model::*; use super::*; @@ -61,22 +60,6 @@ pub mod model { pub value: Box, } - /// World event - /// - /// Does not participate in `Event`, but useful for events warranties when modifying `wsv` - #[derive( - Debug, Clone, PartialEq, Eq, FromVariant, Decode, Encode, Deserialize, Serialize, IntoSchema, - )] - pub enum WorldEvent { - Peer(peer::PeerEvent), - Domain(domain::DomainEvent), - Role(role::RoleEvent), - Trigger(trigger::TriggerEvent), - PermissionTokenSchemaUpdate(permission::PermissionTokenSchemaUpdateEvent), - Configuration(config::ConfigurationEvent), - Executor(executor::ExecutorEvent), - } - /// Event #[derive( Debug, @@ -98,12 +81,6 @@ pub mod model { Peer(peer::PeerEvent), /// Domain event Domain(domain::DomainEvent), - /// Account event - Account(account::AccountEvent), - /// Asset definition event - AssetDefinition(asset::AssetDefinitionEvent), - /// Asset event - Asset(asset::AssetEvent), /// Trigger event Trigger(trigger::TriggerEvent), /// Role event @@ -629,65 +606,19 @@ pub trait HasOrigin { fn origin_id(&self) -> &::Id; } -impl WorldEvent { - /// Unfold [`Self`] and return vector of [`Event`]s in the expanding scope order: from specific to general. - /// E.g [`AssetEvent`] -> [`AccountEvent`] -> [`DomainEvent`] - pub fn flatten(self) -> SmallVec<[DataEvent; 3]> { - let mut events = SmallVec::new(); - - match self { - WorldEvent::Domain(domain_event) => { - match &domain_event { - DomainEvent::Account(account_event) => { - if let AccountEvent::Asset(asset_event) = account_event { - events.push(DataEvent::Asset(asset_event.clone())); - } - events.push(DataEvent::Account(account_event.clone())); - } - DomainEvent::AssetDefinition(asset_definition_event) => { - events.push(DataEvent::AssetDefinition(asset_definition_event.clone())); - } - _ => (), - } - events.push(DataEvent::Domain(domain_event)); - } - WorldEvent::Peer(peer_event) => { - events.push(DataEvent::Peer(peer_event)); - } - WorldEvent::Role(role_event) => { - events.push(DataEvent::Role(role_event)); - } - WorldEvent::Trigger(trigger_event) => { - events.push(DataEvent::Trigger(trigger_event)); - } - WorldEvent::PermissionTokenSchemaUpdate(token_event) => { - events.push(DataEvent::PermissionToken(token_event)); - } - WorldEvent::Configuration(config_event) => { - events.push(DataEvent::Configuration(config_event)); - } - WorldEvent::Executor(executor_event) => { - events.push(DataEvent::Executor(executor_event)); - } - } - - events - } -} - -impl From for WorldEvent { +impl From for DataEvent { fn from(value: AccountEvent) -> Self { DomainEvent::Account(value).into() } } -impl From for WorldEvent { +impl From for DataEvent { fn from(value: AssetDefinitionEvent) -> Self { DomainEvent::AssetDefinition(value).into() } } -impl From for WorldEvent { +impl From for DataEvent { fn from(value: AssetEvent) -> Self { AccountEvent::Asset(value).into() } @@ -698,9 +629,6 @@ impl DataEvent { pub fn domain_id(&self) -> Option<&DomainId> { match self { Self::Domain(event) => Some(event.origin_id()), - Self::Account(event) => Some(&event.origin_id().domain_id), - Self::AssetDefinition(event) => Some(&event.origin_id().domain_id), - Self::Asset(event) => Some(&event.origin_id().definition_id.domain_id), Self::Trigger(event) => event.origin_id().domain_id.as_ref(), Self::Peer(_) | Self::Configuration(_) @@ -731,6 +659,6 @@ pub mod prelude { trigger::{ TriggerEvent, TriggerEventFilter, TriggerFilter, TriggerNumberOfExecutionsChanged, }, - DataEvent, HasOrigin, MetadataChanged, WorldEvent, + DataEvent, HasOrigin, MetadataChanged, }; } diff --git a/data_model/src/events/data/filters.rs b/data_model/src/events/data/filters.rs index 30d4029e018..c03b9b7c6b6 100644 --- a/data_model/src/events/data/filters.rs +++ b/data_model/src/events/data/filters.rs @@ -52,12 +52,6 @@ pub mod model { ByPeer(FilterOpt), /// Filter by Domain entity. `AcceptAll` value will accept all `Domain` events ByDomain(FilterOpt), - /// Filter by Account entity. `AcceptAll` value will accept all `Account` events - ByAccount(FilterOpt), - /// Filter by AssetDefinition entity. `AcceptAll` value will accept all `AssetDefinition` events - ByAssetDefinition(FilterOpt), - /// Filter by Asset entity. `AcceptAll` value will accept all `Asset` events - ByAsset(FilterOpt), /// Filter by Trigger entity. `AcceptAll` value will accept all `Trigger` events ByTrigger(FilterOpt), /// Filter by Role entity. `AcceptAll` value will accept all `Role` events @@ -145,13 +139,9 @@ impl Filter for DataEntityFilter { match (self, event) { (Self::ByPeer(filter_opt), DataEvent::Peer(peer)) => filter_opt.matches(peer), (Self::ByDomain(filter_opt), DataEvent::Domain(domain)) => filter_opt.matches(domain), - (Self::ByAccount(filter_opt), DataEvent::Account(account)) => { - filter_opt.matches(account) + (Self::ByTrigger(filter_opt), DataEvent::Trigger(trigger)) => { + filter_opt.matches(trigger) } - (Self::ByAssetDefinition(filter_opt), DataEvent::AssetDefinition(asset_definition)) => { - filter_opt.matches(asset_definition) - } - (Self::ByAsset(filter_opt), DataEvent::Asset(asset)) => filter_opt.matches(asset), (Self::ByRole(filter_opt), DataEvent::Role(role)) => filter_opt.matches(role), _ => false, } @@ -241,22 +231,60 @@ mod tests { metadata: Metadata::default(), }; let asset_id = AssetId::new( - AssetDefinitionId::new(asset_name, domain_id), + AssetDefinitionId::new(asset_name, domain_id.clone()), account_id.clone(), ); - let asset = Asset::new(asset_id, 0u32); - - let domain_created = DomainEvent::Created(domain); - let account_created = AccountEvent::Created(account); - let asset_created = AssetEvent::Created(asset); - let account_asset_created = AccountEvent::Asset(asset_created.clone()); - let account_filter = BySome(DataEntityFilter::ByAccount(BySome(AccountFilter::new( - BySome(OriginFilter(account_id)), + let asset = Asset::new(asset_id.clone(), 0u32); + + // Create three events with three levels of nesting + // the first one is just a domain event + // the second one is an account event with a domain event inside + // the third one is an asset event with an account event with a domain event inside + let domain_created = DomainEvent::Created(domain).into(); + let account_created = DomainEvent::Account(AccountEvent::Created(account)).into(); + let asset_created = + DomainEvent::Account(AccountEvent::Asset(AssetEvent::Created(asset))).into(); + + // test how the differently nested filters with with the events + // FIXME: rewrite the filters using the builder DSL https://github.com/hyperledger/iroha/issues/3068 + let domain_filter = BySome(DataEntityFilter::ByDomain(BySome(DomainFilter::new( + BySome(OriginFilter(domain_id)), + AcceptAll, + )))); + let account_filter = BySome(DataEntityFilter::ByDomain(BySome(DomainFilter::new( + // kind of unfortunately, we have to specify the domain id filter, + // even though we will filter it with the account id filter (account id contains domain id with and account name) + // FIXME: maybe make this more orthogonal by introducing a partial id (in account event filter only by account name) AcceptAll, + BySome(DomainEventFilter::ByAccount(BySome(AccountFilter::new( + BySome(OriginFilter(account_id)), + AcceptAll, + )))), )))); - assert!(!account_filter.matches(&domain_created.into())); - assert!(!account_filter.matches(&asset_created.into())); - assert!(account_filter.matches(&account_created.into())); - assert!(account_filter.matches(&account_asset_created.into())); + let asset_filter = BySome(DataEntityFilter::ByDomain(BySome(DomainFilter::new( + AcceptAll, + BySome(DomainEventFilter::ByAccount(BySome(AccountFilter::new( + AcceptAll, + BySome(AccountEventFilter::ByAsset(BySome(AssetFilter::new( + BySome(OriginFilter(asset_id)), + AcceptAll, + )))), + )))), + )))); + + // domain filter matches all of those, because all of those events happened in the same domain + assert!(domain_filter.matches(&domain_created)); + assert!(domain_filter.matches(&account_created)); + assert!(domain_filter.matches(&asset_created)); + + // account event does not match the domain created event, as it is not an account event + assert!(!account_filter.matches(&domain_created)); + assert!(account_filter.matches(&account_created)); + assert!(account_filter.matches(&asset_created)); + + // asset event matches only the domain->account->asset event + assert!(!asset_filter.matches(&domain_created)); + assert!(!asset_filter.matches(&account_created)); + assert!(asset_filter.matches(&asset_created)); } } diff --git a/docs/source/references/schema.json b/docs/source/references/schema.json index a4640d1122e..3ac32350d43 100644 --- a/docs/source/references/schema.json +++ b/docs/source/references/schema.json @@ -876,29 +876,14 @@ "discriminant": 1, "type": "FilterOpt" }, - { - "tag": "ByAccount", - "discriminant": 2, - "type": "FilterOpt" - }, - { - "tag": "ByAssetDefinition", - "discriminant": 3, - "type": "FilterOpt" - }, - { - "tag": "ByAsset", - "discriminant": 4, - "type": "FilterOpt" - }, { "tag": "ByTrigger", - "discriminant": 5, + "discriminant": 2, "type": "FilterOpt" }, { "tag": "ByRole", - "discriminant": 6, + "discriminant": 3, "type": "FilterOpt" } ] @@ -915,44 +900,29 @@ "discriminant": 1, "type": "DomainEvent" }, - { - "tag": "Account", - "discriminant": 2, - "type": "AccountEvent" - }, - { - "tag": "AssetDefinition", - "discriminant": 3, - "type": "AssetDefinitionEvent" - }, - { - "tag": "Asset", - "discriminant": 4, - "type": "AssetEvent" - }, { "tag": "Trigger", - "discriminant": 5, + "discriminant": 2, "type": "TriggerEvent" }, { "tag": "Role", - "discriminant": 6, + "discriminant": 3, "type": "RoleEvent" }, { "tag": "PermissionToken", - "discriminant": 7, + "discriminant": 4, "type": "PermissionTokenSchemaUpdateEvent" }, { "tag": "Configuration", - "discriminant": 8, + "discriminant": 5, "type": "ConfigurationEvent" }, { "tag": "Executor", - "discriminant": 9, + "discriminant": 6, "type": "ExecutorEvent" } ] diff --git a/tools/parity_scale_decoder/build.rs b/tools/parity_scale_decoder/build.rs index 58e3db5dbd8..49223203f69 100644 --- a/tools/parity_scale_decoder/build.rs +++ b/tools/parity_scale_decoder/build.rs @@ -23,13 +23,11 @@ where path_to.push("samples/"); path_to.push(filename); - let mut path_to_json_sample = path_to.clone(); - path_to_json_sample.set_extension("json"); + let path_to_json = path_to.with_extension("json"); + let path_to_binary = path_to.with_extension("bin"); - let mut path_to_binary = path_to; - path_to_binary.set_extension("bin"); - - let buf = fs::read_to_string(path_to_json_sample)?; + println!("cargo:rerun-if-changed={}", path_to_json.to_str().unwrap()); + let buf = fs::read_to_string(path_to_json)?; let sample = serde_json::from_str::(buf.as_str())?; diff --git a/tools/parity_scale_decoder/samples/trigger.bin b/tools/parity_scale_decoder/samples/trigger.bin index 9d08440887748d63b32d6564b5608c27bf9b9241..4ad7ccdfd9e315c9e96cef5a1dab82facefc5e8c 100644 GIT binary patch delta 12 TcmeYXnBd9I$jHFR%D?~s50wFh delta 8 PcmWIWnc&IH#J~Um33~y1 diff --git a/tools/parity_scale_decoder/samples/trigger.json b/tools/parity_scale_decoder/samples/trigger.json index b37816c0352..2cf26cf26f3 100644 --- a/tools/parity_scale_decoder/samples/trigger.json +++ b/tools/parity_scale_decoder/samples/trigger.json @@ -19,7 +19,12 @@ "authority": "alice@wonderland", "filter": { "Data": { - "ByAccount": "AcceptAll" + "ByDomain": { + "origin_filter": "AcceptAll", + "event_filter": { + "ByAccount": "AcceptAll" + } + } } }, "metadata": {} diff --git a/tools/parity_scale_decoder/src/main.rs b/tools/parity_scale_decoder/src/main.rs index 47dc7801e7a..c57d0d1a7cb 100644 --- a/tools/parity_scale_decoder/src/main.rs +++ b/tools/parity_scale_decoder/src/main.rs @@ -264,9 +264,10 @@ mod tests { vec![Mint::asset_quantity(1_u32, rose_id)], Repeats::Indefinitely, account_id, - FilterBox::Data(DataEventFilter::BySome(DataEntityFilter::ByAccount( - AcceptAll, - ))), + // FIXME: rewrite the filters using the builder DSL https://github.com/hyperledger/iroha/issues/3068 + FilterBox::Data(BySome(DataEntityFilter::ByDomain(BySome( + DomainFilter::new(AcceptAll, BySome(DomainEventFilter::ByAccount(AcceptAll))), + )))), ); let trigger = Trigger::new(trigger_id, action); From 2ca4b9f007e1428689db8d457df3985840a6c89e Mon Sep 17 00:00:00 2001 From: Shanin Roman Date: Mon, 25 Dec 2023 16:37:37 +0300 Subject: [PATCH 18/21] [fix] #0000: Use free port in test Signed-off-by: Shanin Roman --- client/tests/integration/restart_peer.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/tests/integration/restart_peer.rs b/client/tests/integration/restart_peer.rs index 8249edee078..6c62a7bb393 100644 --- a/client/tests/integration/restart_peer.rs +++ b/client/tests/integration/restart_peer.rs @@ -19,7 +19,7 @@ fn restarted_peer_should_have_the_same_asset_amount() -> Result<()> { let mut removed_peer = { let n_peers = 4; - let (_rt, network, _) = Network::start_test_with_runtime(n_peers, Some(11_200)); + 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 peer_clients = Network::clients(&network); From f1472954c8b573e2ebe18c2ddba6a2d8b94ccdcc Mon Sep 17 00:00:00 2001 From: Daniil Polyakov Date: Thu, 28 Dec 2023 13:02:00 +0300 Subject: [PATCH 19/21] [fix] #0000: Fix smart contract debug print (#4178) [fix]: Fix smart contract debug print Signed-off-by: Daniil Polyakov --- smart_contract/Cargo.toml | 2 +- smart_contract/src/lib.rs | 2 +- smart_contract/utils/Cargo.toml | 3 +++ smart_contract/utils/src/debug.rs | 7 +++++-- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/smart_contract/Cargo.toml b/smart_contract/Cargo.toml index b0d9950600f..bc03b6a98e5 100644 --- a/smart_contract/Cargo.toml +++ b/smart_contract/Cargo.toml @@ -12,7 +12,7 @@ workspace = true [features] # Enables debugging tools such as `dbg()` and `DebugUnwrapExt` -debug = [] +debug = ["iroha_smart_contract_utils/debug"] [dependencies] iroha_data_model.workspace = true diff --git a/smart_contract/src/lib.rs b/smart_contract/src/lib.rs index db86863330d..b338ef1f6dc 100644 --- a/smart_contract/src/lib.rs +++ b/smart_contract/src/lib.rs @@ -47,7 +47,7 @@ unsafe extern "C" fn _iroha_smart_contract_dealloc(offset: *mut u8, len: usize) /// /// # Example /// -/// ``` +/// ```ignore /// use iroha_smart_contract::{prelude::*, parse}; /// /// let account_id = parse!("alice@wonderland" as AccountId); diff --git a/smart_contract/utils/Cargo.toml b/smart_contract/utils/Cargo.toml index 97f8a59d60d..479ef2b4685 100644 --- a/smart_contract/utils/Cargo.toml +++ b/smart_contract/utils/Cargo.toml @@ -10,6 +10,9 @@ license.workspace = true [lints] workspace = true +[features] +debug = [] + [dependencies] iroha_data_model.workspace = true diff --git a/smart_contract/utils/src/debug.rs b/smart_contract/utils/src/debug.rs index be59f7c0a2e..e3de00ffd51 100644 --- a/smart_contract/utils/src/debug.rs +++ b/smart_contract/utils/src/debug.rs @@ -1,5 +1,7 @@ //! WASM debugging utilities +#[cfg(feature = "debug")] +use alloc::format; use core::fmt::Debug; #[cfg(not(test))] @@ -30,9 +32,9 @@ pub fn dbg(_obj: &T) { use tests::_dbg_mock as host_dbg; #[allow(clippy::used_underscore_binding)] - let s = format!("{:?}", _obj); + let s = format!("{_obj:?}"); // Safety: `host_dbg` doesn't take ownership of it's pointer parameter - unsafe { iroha_smart_contract_utils::encode_and_execute(&s, host_dbg) } + unsafe { crate::encode_and_execute(&s, host_dbg) } } } @@ -89,6 +91,7 @@ impl DebugUnwrapExt for Option { return self.unwrap(); #[cfg(feature = "debug")] + #[allow(clippy::single_match_else, clippy::option_if_let_else)] { match self { Some(out) => out, From b14ad7bdeafce6d866310e76a4af00d51e493650 Mon Sep 17 00:00:00 2001 From: Shanin Roman Date: Fri, 15 Dec 2023 13:25:29 +0300 Subject: [PATCH 20/21] [fix] #3857: Add test to check token unification Signed-off-by: Shanin Roman --- client/tests/integration/connected_peers.rs | 2 +- client/tests/integration/permissions.rs | 36 +++++++++++++++++++ client/tests/integration/roles.rs | 39 +++++++++++++++++++++ 3 files changed, 76 insertions(+), 1 deletion(-) diff --git a/client/tests/integration/connected_peers.rs b/client/tests/integration/connected_peers.rs index 9d9634316d1..20221dd0d2a 100644 --- a/client/tests/integration/connected_peers.rs +++ b/client/tests/integration/connected_peers.rs @@ -46,7 +46,7 @@ fn register_new_peer() -> Result<()> { PeerBuilder::new() .with_configuration(configuration) .with_into_genesis(WithGenesis::None) - .with_port(11_200) + .with_port(11_225) .start(), ); diff --git a/client/tests/integration/permissions.rs b/client/tests/integration/permissions.rs index a6250427fbd..f9ff3c05cdf 100644 --- a/client/tests/integration/permissions.rs +++ b/client/tests/integration/permissions.rs @@ -329,3 +329,39 @@ fn stored_vs_granted_token_payload() -> Result<()> { Ok(()) } + +#[test] +#[allow(deprecated)] +fn permission_tokens_are_unified() { + let (_rt, _peer, iroha_client) = ::new().with_port(11_230).start_with_runtime(); + wait_for_genesis_committed(&[iroha_client.clone()], 0); + + // Given + let alice_id = AccountId::from_str("alice@wonderland").expect("Valid"); + + let allow_alice_to_transfer_rose_1 = Grant::permission_token( + PermissionToken::from_str_unchecked( + "CanTransferUserAsset".parse().unwrap(), + // NOTE: Introduced additional whitespaces in the serialized form + "{ \"asset_id\" : \"rose#wonderland#alice@wonderland\" }", + ), + alice_id.clone(), + ); + + let allow_alice_to_transfer_rose_2 = Grant::permission_token( + PermissionToken::from_str_unchecked( + "CanTransferUserAsset".parse().unwrap(), + // NOTE: Introduced additional whitespaces in the serialized form + "{ \"asset_id\" : \"rose##alice@wonderland\" }", + ), + alice_id, + ); + + iroha_client + .submit_blocking(allow_alice_to_transfer_rose_1) + .expect("failed to grant permission token"); + + let _ = iroha_client + .submit_blocking(allow_alice_to_transfer_rose_2) + .expect_err("permission tokens are not unified"); +} diff --git a/client/tests/integration/roles.rs b/client/tests/integration/roles.rs index ef1884806a3..95245852db4 100644 --- a/client/tests/integration/roles.rs +++ b/client/tests/integration/roles.rs @@ -172,3 +172,42 @@ fn role_with_invalid_permissions_is_not_accepted() -> Result<()> { Ok(()) } + +#[test] +#[allow(deprecated)] +fn role_permissions_unified() { + let (_rt, _peer, test_client) = ::new().with_port(11_235).start_with_runtime(); + wait_for_genesis_committed(&vec![test_client.clone()], 0); + + let allow_alice_to_transfer_rose_1 = PermissionToken::from_str_unchecked( + "CanTransferUserAsset".parse().unwrap(), + // NOTE: Introduced additional whitespaces in the serialized form + "{ \"asset_id\" : \"rose#wonderland#alice@wonderland\" }", + ); + + let allow_alice_to_transfer_rose_2 = PermissionToken::from_str_unchecked( + "CanTransferUserAsset".parse().unwrap(), + // NOTE: Introduced additional whitespaces in the serialized form + "{ \"asset_id\" : \"rose##alice@wonderland\" }", + ); + + let role_id: RoleId = "role_id".parse().expect("Valid"); + let role = Role::new(role_id.clone()) + .add_permission(allow_alice_to_transfer_rose_1) + .add_permission(allow_alice_to_transfer_rose_2); + + test_client + .submit_blocking(Register::role(role)) + .expect("failed to register role"); + + let role = test_client + .request(FindRoleByRoleId::new(role_id)) + .expect("failed to find role"); + + // Permission tokens are unified so only one token left + assert_eq!( + role.permissions().len(), + 1, + "permission tokens for role aren't deduplicated" + ); +} From 1b2b2cd0391465d090cc72648db9380e83a472c9 Mon Sep 17 00:00:00 2001 From: Shanin Roman Date: Fri, 15 Dec 2023 13:30:19 +0300 Subject: [PATCH 21/21] [fix] #3857: Unify permission tokens in executor Signed-off-by: Shanin Roman --- configs/peer/executor.wasm | Bin 390610 -> 479380 bytes data_model/src/permission.rs | 7 +++++ smart_contract/executor/derive/src/token.rs | 15 ++++++++- smart_contract/executor/src/default.rs | 33 ++++++++++++++++---- 4 files changed, 48 insertions(+), 7 deletions(-) diff --git a/configs/peer/executor.wasm b/configs/peer/executor.wasm index 7af36698d6cf6663f58b1f0a0e07e4bcce86101e..ad6286a1ccb83d8066600ca9805b7fd5c72445b5 100644 GIT binary patch literal 479380 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 UP0z_s0wxS?4$#hX-PDQ_01&KGWdHyG diff --git a/data_model/src/permission.rs b/data_model/src/permission.rs index ce3fd423e28..fc2dc765858 100644 --- a/data_model/src/permission.rs +++ b/data_model/src/permission.rs @@ -25,6 +25,13 @@ pub mod model { use super::*; /// Stored proof of the account having a permission for a certain action. + /// + /// Since permission token is represented opaque to core + /// either executor or client should make sure that tokens are represented uniformly. + /// + /// So that: + /// - payload A is equal to payload B then token A must be equal to token B + /// - and if payload A is't equal to B then token A mustn't be equal to token B #[derive( Debug, Clone, diff --git a/smart_contract/executor/derive/src/token.rs b/smart_contract/executor/derive/src/token.rs index 6d961c1c3ad..b7c868e4b0a 100644 --- a/smart_contract/executor/derive/src/token.rs +++ b/smart_contract/executor/derive/src/token.rs @@ -47,7 +47,7 @@ fn impl_token(ident: &syn2::Ident, generics: &syn2::Generics) -> proc_macro2::To fn impl_try_from_permission_token(ident: &syn2::Ident, generics: &syn2::Generics) -> TokenStream { let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); - let token_id = quote! { ::name() }; + let token_id = quote! { <#ident #ty_generics as ::iroha_executor::permission::Token>::name() }; quote! { impl #impl_generics ::core::convert::TryFrom<::iroha_executor::data_model::permission::PermissionToken> for #ident #ty_generics #where_clause { @@ -63,5 +63,18 @@ fn impl_try_from_permission_token(ident: &syn2::Ident, generics: &syn2::Generics .map_err(::iroha_executor::permission::PermissionTokenConversionError::Deserialize) } } + + impl #impl_generics ::core::convert::From<#ident #ty_generics> for ::iroha_executor::data_model::permission::PermissionToken #where_clause { + fn from(token: #ident #ty_generics) -> Self { + let definition_id = #token_id; + + let payload = ::iroha_executor::smart_contract::debug::DebugExpectExt::dbg_expect( + ::serde_json::to_value::<#ident #ty_generics>(token), + "failed to serialize concrete permission token type. This is a bug." + ); + + ::iroha_executor::data_model::permission::PermissionToken::new(definition_id, &payload) + } + } } } diff --git a/smart_contract/executor/src/default.rs b/smart_contract/executor/src/default.rs index 5b70d96a884..0c7eafa30f2 100644 --- a/smart_contract/executor/src/default.rs +++ b/smart_contract/executor/src/default.rs @@ -931,6 +931,8 @@ pub mod parameter { } pub mod role { + use iroha_smart_contract::data_model::role::Role; + use super::*; macro_rules! impl_validate { @@ -981,13 +983,16 @@ pub mod role { ) { let role = isi.object().inner(); + // Unify permission tokens inside role and deduplicate them + let mut new_role = Role::new(role.id().clone()); let mut unknown_tokens = Vec::new(); for token in role.permissions() { iroha_smart_contract::debug!(&format!("Checking `{token:?}`")); macro_rules! try_from_token { ($token:ident) => { - let _token = $token; + let token = PermissionToken::from($token); + new_role = new_role.add_permission(token); continue; }; } @@ -1005,6 +1010,7 @@ pub mod role { ); } + let isi = Register::role(new_role); execute!(executor, isi); } @@ -1168,14 +1174,17 @@ pub mod permission_token { use super::*; macro_rules! impl_validate { - ($executor:ident, $authority:ident, $isi:ident, $method:ident) => { + ($executor:ident, $authority:ident, $isi:ident, $method:ident, $isi_type:ty) => { // TODO: https://github.com/hyperledger/iroha/issues/4082 let token = $isi.object().clone(); + let account_id = $isi.destination_id().clone(); macro_rules! visit_internal { ($token:ident) => { + let token = PermissionToken::from($token.clone()); + let isi = <$isi_type>::permission_token(token, account_id); if is_genesis($executor) { - execute!($executor, $isi); + execute!($executor, isi); } if let Err(error) = permission::ValidateGrantRevoke::$method( &$token, @@ -1185,7 +1194,7 @@ pub mod permission_token { deny!($executor, error); } - execute!($executor, $isi); + execute!($executor, isi); }; } @@ -1203,7 +1212,13 @@ pub mod permission_token { authority: &AccountId, isi: &Grant, ) { - impl_validate!(executor, authority, isi, validate_grant); + impl_validate!( + executor, + authority, + isi, + validate_grant, + Grant + ); } pub fn visit_revoke_account_permission( @@ -1211,7 +1226,13 @@ pub mod permission_token { authority: &AccountId, isi: &Revoke, ) { - impl_validate!(executor, authority, isi, validate_revoke); + impl_validate!( + executor, + authority, + isi, + validate_revoke, + Revoke + ); } }

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 literal 390610 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