From 964476722e2a219becaacdb3676ca058ec5748cd Mon Sep 17 00:00:00 2001 From: 0x009922 <43530070+0x009922@users.noreply.github.com> Date: Fri, 16 Feb 2024 19:09:56 +0900 Subject: [PATCH] [refactor] #4161: rewrite config, _minimally_ (#4239) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [refactor]: wip Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [refactor]: update structure - exclude genesis block loading from config - construct `KeyPair` on `iroha` completion - use full field names in `complete()`s Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [refactor]: apply lints Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [feat]: include more ENV vars, refactor Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [feat]: impl merging - `UserField` wrap instead of `Option` - move trusted peers uniqueness check Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [refactor]: update Kagami - Remove `config` subcommand - Update default `genesis` building Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [refactor]: restructure code - move generic tools to `iroha_config_base` - remove `iroha_client_config` crate - define client config in `iroha_client::config` - add client config sample TOML Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [refactor]: update `iroha_logger` Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [refactor]: update `iroha_telemetry` - update usage of `iroha_config` - use `Duration` in `RetryPeriod` Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [fix]: fix util macro Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [refactor]: re-struct config, update `iroha_core` - split "user-layer" and "actual" config modules - update logger, telemetry, and kagami (genesis) Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [refactor]: update more crates - `iroha_client` - `iroha_torii` - move Torii `uri` to `iroha_torii_const` Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [feat]: compile `iroha`! Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [feat]: compile `iroha_client`! Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [feat]: compile `iroha_client_cli`! Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [feat]: compile everything *_* Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [fix]: chores Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [refactor]: pass tests - update signature of `PeerId::new` (avoid extra clone) - fix `UserField::set` - fix deps of `iroha_config_base` - resolve runtime todos in `iroha_config` - update `iroha_swarm` generation output - some other refactoring Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [feat]: implement `extends` Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [chore]: update default snapshot storage Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [fix]: update after rebase Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [refactor]: update just everything - `test_env.py`: use toml configs - remove other channel configs - put example configs into `config_samples` dir - put docker setup into `config_samples/swarm` dir - pytests: split settings for CLI and config paths - make test env runnable Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [refactor]: refactored config boilerplate into dedicated modules Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [refactor]: apply some lints Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [refactor]: update config naming in `[queue]` also run tests with all features enabled Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [feat]: implement `Config::load` shorthand Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [docs]: add comment Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [ci]: fix typo Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [ci]: install `tomli_w` via pacman Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [chore]: fix whitespace Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [revert]: update the snapshot store path Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [refactor]: fix pytests - do not mutate client config from tests, override via env instead - add `TORII_URL` env var to client config Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [refactor]: apply suggestions from code review Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [refactor]: curl up `SumeragiStartArgs` Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [refactor]: refine telemetry - simplify `regular_telemetry` to just `telemetry` - clearer config passing Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [refactor]: use `Infallible` Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [refactor]: simplify client config - `nonce` instead of `add_nonce` - move `[api]` to root Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [refactor]: update `--config` arg - remove default value - remove `IROHA_CONFIG` env Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [docs]: update `read_config_and_genesis` docs Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [refactor]: chore Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [refactor]: just `idle_time`, without `query_` Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [revert]: use `ident_length_limits` in _actual_ config Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [refactor]: use `Config`, not vague `Root` Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [chore]: remove comment Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [refactor]: use `capacity` term in Queue Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [refactor]: `set_creation_time_ms`, use `*` instead of `mul` Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [fix]: remove dead code Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [refactor]: update `--config` args - use `PathBuf` again - remove default value at Client CLI Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [chore]: cleaning Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [refactor]: use `strum` in place of `parse_display` Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [refactor]: no unsafe code any more Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [refactor]: docs & refinements - document code in: - `iroha_config_base` - `iroha_config` - `iroha_client::config` - refactor `iroha_config_base` APIs - move `ExtendsPaths` into `iroha_config_base` - remove `[iroha]` user config section - move `chain_id` and key pair to the root - move `p2p_address` to `network.address` - rename `user_layer` modules to `user` - add `_bytes` suffix for relevant fields, put a TODO to add a newtype for it Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [docs]: fill `peer.example.toml` Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [chore]: re-export `ConfigurationDTO` from client Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [test]: fix them 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> * [test]: fix pytests Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [misc]: re-arrange sample configurations - use `configs` dir again - name example configs as _templates_ Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [docs]: update README Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [refactor]: remove `parse-display` from deps Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [refactor]: remove extra `iroha_config_base` exposure from the client Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [fix]: import `Deserialize` in the macro Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [refactor]: use `PrivateKey::into_raw` instead Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [ci]: use `--break-system-packages` `pip` flag Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [fix]: regenerate swarms Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [ci]: fix pytests workflow Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [fix]: remove `PrivateKey::payload()` access Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [docs]: fix "unable to validate" doc Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [refactor]: accept `Duration` for `set_creation_time` Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [refactor]: `PrivateKey::to_raw` Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [fix]: do not extend trusted peers in config Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [refactor]: hide user view from `iroha_client::config` Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [refactor]: rename `*configuration` to `config` everywhere﹡ ﹡except crypto Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [fix]: update default wasm fuel limit Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [refactor]: change docs and methods of `WebLogin` Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [fix]: trusted peers and config tests Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [refactor]: rename `telemetry.dev.out_file` Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [docs]: update `GenesisNetwork::new` errors Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [refactor]: use `serde_with` Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [misc]: move `nonzero_ext` to workspace level Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [fix]: make `--config` optional It is still possible to set full config via env (e.g. `iroha_swarm` case) Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [chore]: remove "regular" from telemetry re-export Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [chore]: rename imports in `wasm.rs` Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [refactor]: lints Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [refactor]: use `serde_with` in swarm Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [refactor]: rename parameters, cover full config in tests, fix bugs Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [test]: cover absolute paths Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [refactor]: update client configs, cover full in tests Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [refactor]: lints Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [fix]: also rename ENVs Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [build]: add notes to Dockerfile Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [chore]: update style in `iroha_test_config.toml` Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [test]: fix old params in `test_env.py` Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [test]: add a note to `panic_on_invalid_genesis.sh` Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --------- Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --- .github/workflows/iroha2-dev-pr.yml | 15 +- .github/workflows/iroha2-release-pr.yml | 1 + CONTRIBUTING.md | 2 +- Cargo.lock | 148 ++- Cargo.toml | 7 +- Dockerfile | 4 + README.md | 17 +- cli/Cargo.toml | 3 +- cli/README.md | 13 +- cli/src/lib.rs | 260 ++--- cli/src/main.rs | 48 +- cli/src/samples.rs | 103 +- client/Cargo.toml | 4 + client/README.md | 8 +- client/benches/torii.rs | 52 +- client/benches/tps/utils.rs | 2 +- client/examples/million_accounts_genesis.rs | 24 +- client/examples/tutorial.rs | 54 +- client/src/client.rs | 156 ++- client/src/config.rs | 122 ++ client/src/config/user.rs | 195 ++++ client/src/config/user/boilerplate.rs | 147 +++ client/src/lib.rs | 45 +- client/tests/integration/add_account.rs | 4 +- client/tests/integration/add_domain.rs | 4 +- client/tests/integration/asset.rs | 6 +- client/tests/integration/asset_propagation.rs | 4 +- client/tests/integration/burn_public_keys.rs | 2 +- client/tests/integration/connected_peers.rs | 12 +- client/tests/integration/domain_owner.rs | 6 +- client/tests/integration/events/pipeline.rs | 4 +- .../integration/multiple_blocks_created.rs | 4 +- .../integration/multisignature_account.rs | 4 +- .../integration/multisignature_transaction.rs | 29 +- client/tests/integration/offline_peers.rs | 4 +- client/tests/integration/permissions.rs | 8 +- client/tests/integration/restart_peer.rs | 4 +- client/tests/integration/roles.rs | 2 +- .../integration/triggers/time_trigger.rs | 19 +- client/tests/integration/tx_history.rs | 4 +- client/tests/integration/unregister_peer.rs | 6 +- client/tests/integration/unstable_network.rs | 11 +- client/tests/integration/upgrade.rs | 2 +- client_cli/pytests/README.md | 7 +- client_cli/pytests/common/settings.py | 5 +- client_cli/pytests/poetry.lock | 10 +- client_cli/pytests/pyproject.toml | 1 + .../pytests/src/client_cli/client_cli.py | 6 +- .../pytests/src/client_cli/configuration.py | 49 +- client_cli/pytests/src/client_cli/iroha.py | 7 +- client_cli/src/main.rs | 76 +- config/Cargo.toml | 6 + config/base/Cargo.toml | 19 +- config/base/derive/Cargo.toml | 24 - config/base/derive/src/lib.rs | 51 - config/base/derive/src/proxy.rs | 324 ------ config/base/derive/src/utils.rs | 367 ------ config/base/derive/src/view.rs | 183 --- config/base/src/lib.rs | 1013 ++++++++++------- config/iroha_test_config.json | 123 -- config/iroha_test_config.toml | 34 + config/src/block_sync.rs | 50 - config/src/client.rs | 236 ---- config/src/client_api.rs | 16 +- config/src/genesis.rs | 141 --- config/src/iroha.rs | 282 ----- config/src/kura.rs | 73 +- config/src/lib.rs | 18 +- config/src/live_query_store.rs | 44 - config/src/logger.rs | 82 +- config/src/network.rs | 39 - config/src/parameters/actual.rs | 251 ++++ config/src/parameters/defaults.rs | 104 ++ config/src/parameters/mod.rs | 5 + config/src/parameters/user.rs | 704 ++++++++++++ config/src/parameters/user/boilerplate.rs | 766 +++++++++++++ config/src/path.rs | 152 --- config/src/queue.rs | 55 - config/src/snapshot.rs | 54 - config/src/sumeragi.rs | 186 --- config/src/telemetry.rs | 117 -- config/src/torii.rs | 99 -- config/src/wasm.rs | 34 - config/src/wsv.rs | 86 -- config/test/config.toml | 7 + config/tests/fixtures.rs | 519 +++++++++ config/tests/fixtures/absolute_paths.toml | 14 + .../tests/fixtures/bad.extends_nowhere.toml | 1 + config/tests/fixtures/bad.extra_fields.toml | 4 + config/tests/fixtures/bad.missing_fields.toml | 1 + .../tests/fixtures/bad.multiple_bad_envs.env | 6 + .../fixtures/bad.torii_addr_eq_p2p_addr.toml | 7 + config/tests/fixtures/base.toml | 13 + config/tests/fixtures/base_trusted_peers.toml | 3 + config/tests/fixtures/empty_ok_genesis.json | 4 + config/tests/fixtures/full.env | 16 + config/tests/fixtures/full.toml | 74 ++ .../tests/fixtures/inconsistent_genesis.toml | 7 + .../fixtures/minimal_alone_with_genesis.toml | 6 + .../tests/fixtures/minimal_file_and_env.env | 1 + .../tests/fixtures/minimal_file_and_env.toml | 14 + .../fixtures/minimal_with_trusted_peers.toml | 1 + config/tests/fixtures/multiple_extends.1.toml | 2 + config/tests/fixtures/multiple_extends.2.toml | 5 + .../tests/fixtures/multiple_extends.2a.toml | 2 + config/tests/fixtures/multiple_extends.toml | 6 + configs/client.template.toml | 19 + configs/client/config.json | 17 - configs/client/lts/config.json | 95 -- configs/client/stable/config.json | 95 -- configs/peer.template.toml | 67 ++ configs/peer/config.json | 95 -- configs/peer/lts/config.json | 98 -- configs/peer/lts/executor.wasm | Bin 501157 -> 0 bytes configs/peer/lts/genesis.json | 201 ---- configs/peer/stable/config.json | 98 -- configs/peer/stable/executor.wasm | Bin 501157 -> 0 bytes configs/peer/stable/genesis.json | 201 ---- ...prometheus.yml => prometheus.template.yml} | 0 configs/swarm/client.toml | 11 + .../swarm/docker-compose.local.yml | 77 +- configs/swarm/docker-compose.single.yml | 32 + .../swarm/docker-compose.yml | 69 +- configs/{peer => swarm}/executor.wasm | Bin configs/{peer => swarm}/genesis.json | 0 core/benches/blocks/common.rs | 8 +- core/benches/kura.rs | 8 +- core/benches/validation.rs | 10 +- core/src/block.rs | 18 +- core/src/block_sync.rs | 20 +- core/src/executor.rs | 8 +- core/src/gossiper.rs | 22 +- core/src/kiso.rs | 49 +- core/src/kura.rs | 31 +- core/src/query/store.rs | 20 +- core/src/queue.rs | 165 +-- core/src/smartcontracts/isi/query.rs | 4 +- core/src/smartcontracts/wasm.rs | 36 +- core/src/snapshot.rs | 8 +- core/src/sumeragi/main_loop.rs | 14 +- core/src/sumeragi/mod.rs | 36 +- core/src/sumeragi/network_topology.rs | 2 +- core/src/wsv.rs | 43 +- core/test_network/src/lib.rs | 220 ++-- crypto/src/lib.rs | 8 + data_model/src/lib.rs | 31 +- data_model/src/transaction.rs | 5 +- default_executor/README.md | 2 +- docker-compose.single.yml | 31 - genesis/src/lib.rs | 18 +- logger/src/lib.rs | 17 +- logger/tests/setting_logger.rs | 7 +- macro/utils/Cargo.toml | 2 +- p2p/src/network.rs | 8 +- p2p/tests/integration/p2p.rs | 16 +- scripts/requirements.txt | 1 + scripts/test_env.py | 145 ++- scripts/tests/consistency.sh | 28 +- scripts/tests/panic_on_invalid_genesis.sh | 3 +- telemetry/src/dev.rs | 11 +- telemetry/src/lib.rs | 4 +- telemetry/src/retry_period.rs | 34 +- telemetry/src/ws.rs | 16 +- tools/kagami/src/config.rs | 96 -- tools/kagami/src/genesis.rs | 16 +- tools/kagami/src/main.rs | 4 - tools/swarm/Cargo.toml | 1 + tools/swarm/README.md | 6 +- tools/swarm/src/cli.rs | 4 +- tools/swarm/src/compose.rs | 289 ++--- torii/Cargo.toml | 1 + torii/const/Cargo.toml | 21 + torii/const/src/lib.rs | 38 + torii/src/lib.rs | 9 +- torii/src/routing.rs | 24 +- 175 files changed, 5189 insertions(+), 5785 deletions(-) create mode 100644 client/src/config.rs create mode 100644 client/src/config/user.rs create mode 100644 client/src/config/user/boilerplate.rs delete mode 100644 config/base/derive/Cargo.toml delete mode 100644 config/base/derive/src/lib.rs delete mode 100644 config/base/derive/src/proxy.rs delete mode 100644 config/base/derive/src/utils.rs delete mode 100644 config/base/derive/src/view.rs delete mode 100644 config/iroha_test_config.json create mode 100644 config/iroha_test_config.toml delete mode 100644 config/src/block_sync.rs delete mode 100644 config/src/client.rs delete mode 100644 config/src/genesis.rs delete mode 100644 config/src/iroha.rs delete mode 100644 config/src/live_query_store.rs delete mode 100644 config/src/network.rs create mode 100644 config/src/parameters/actual.rs create mode 100644 config/src/parameters/defaults.rs create mode 100644 config/src/parameters/mod.rs create mode 100644 config/src/parameters/user.rs create mode 100644 config/src/parameters/user/boilerplate.rs delete mode 100644 config/src/path.rs delete mode 100644 config/src/queue.rs delete mode 100644 config/src/snapshot.rs delete mode 100644 config/src/sumeragi.rs delete mode 100644 config/src/telemetry.rs delete mode 100644 config/src/torii.rs delete mode 100644 config/src/wsv.rs create mode 100644 config/test/config.toml create mode 100644 config/tests/fixtures.rs create mode 100644 config/tests/fixtures/absolute_paths.toml create mode 100644 config/tests/fixtures/bad.extends_nowhere.toml create mode 100644 config/tests/fixtures/bad.extra_fields.toml create mode 100644 config/tests/fixtures/bad.missing_fields.toml create mode 100644 config/tests/fixtures/bad.multiple_bad_envs.env create mode 100644 config/tests/fixtures/bad.torii_addr_eq_p2p_addr.toml create mode 100644 config/tests/fixtures/base.toml create mode 100644 config/tests/fixtures/base_trusted_peers.toml create mode 100644 config/tests/fixtures/empty_ok_genesis.json create mode 100644 config/tests/fixtures/full.env create mode 100644 config/tests/fixtures/full.toml create mode 100644 config/tests/fixtures/inconsistent_genesis.toml create mode 100644 config/tests/fixtures/minimal_alone_with_genesis.toml create mode 100644 config/tests/fixtures/minimal_file_and_env.env create mode 100644 config/tests/fixtures/minimal_file_and_env.toml create mode 100644 config/tests/fixtures/minimal_with_trusted_peers.toml create mode 100644 config/tests/fixtures/multiple_extends.1.toml create mode 100644 config/tests/fixtures/multiple_extends.2.toml create mode 100644 config/tests/fixtures/multiple_extends.2a.toml create mode 100644 config/tests/fixtures/multiple_extends.toml create mode 100644 configs/client.template.toml delete mode 100644 configs/client/config.json delete mode 100644 configs/client/lts/config.json delete mode 100644 configs/client/stable/config.json create mode 100644 configs/peer.template.toml delete mode 100644 configs/peer/config.json delete mode 100644 configs/peer/lts/config.json delete mode 100644 configs/peer/lts/executor.wasm delete mode 100644 configs/peer/lts/genesis.json delete mode 100644 configs/peer/stable/config.json delete mode 100644 configs/peer/stable/executor.wasm delete mode 100644 configs/peer/stable/genesis.json rename configs/{prometheus.yml => prometheus.template.yml} (100%) create mode 100644 configs/swarm/client.toml rename docker-compose.local.yml => configs/swarm/docker-compose.local.yml (50%) create mode 100644 configs/swarm/docker-compose.single.yml rename docker-compose.yml => configs/swarm/docker-compose.yml (52%) rename configs/{peer => swarm}/executor.wasm (100%) rename configs/{peer => swarm}/genesis.json (100%) delete mode 100644 docker-compose.single.yml create mode 100644 scripts/requirements.txt delete mode 100644 tools/kagami/src/config.rs create mode 100644 torii/const/Cargo.toml create mode 100644 torii/const/src/lib.rs diff --git a/.github/workflows/iroha2-dev-pr.yml b/.github/workflows/iroha2-dev-pr.yml index 1d5526cff59..7846ef54715 100644 --- a/.github/workflows/iroha2-dev-pr.yml +++ b/.github/workflows/iroha2-dev-pr.yml @@ -28,12 +28,6 @@ jobs: - name: Check genesis.json if: always() run: ./scripts/tests/consistency.sh genesis - - name: Check client/config.json - if: always() - run: ./scripts/tests/consistency.sh client - - name: Check peer/config.json - if: always() - run: ./scripts/tests/consistency.sh peer - name: Check schema.json if: always() run: ./scripts/tests/consistency.sh schema @@ -144,11 +138,10 @@ jobs: - uses: Swatinem/rust-cache@v2 - name: Build binaries run: | - cargo build --bin iroha_client_cli - cargo build --bin kagami - cargo build --bin iroha + cargo build -p iroha_client_cli -p kagami -p iroha - name: Setup test Iroha 2 environment on the bare metal run: | + pip3 install -r scripts/requirements.txt --no-input --break-system-packages ./scripts/test_env.py setup - name: Mark binaries as executable run: | @@ -159,6 +152,10 @@ jobs: poetry install - name: Run client cli tests working-directory: client_cli/pytests + env: + # prepared by `test_env.py` + CLIENT_CLI_BINARY: ../../test/iroha_client_cli + CLIENT_CLI_CONFIG: ../../test/client.toml run: | poetry run pytest - name: Cleanup test environment diff --git a/.github/workflows/iroha2-release-pr.yml b/.github/workflows/iroha2-release-pr.yml index cd1a94b8623..99067c687fb 100644 --- a/.github/workflows/iroha2-release-pr.yml +++ b/.github/workflows/iroha2-release-pr.yml @@ -36,6 +36,7 @@ jobs: cargo build --bin iroha - name: Setup test Iroha 2 environment on bare metal run: | + pip3 install -r scripts/requirements.txt --no-input --break-system-packages ./scripts/test_env.py setup - name: Mark binaries as executable run: | diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5ef9c16a5ec..854f2302c4d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -222,7 +222,7 @@ Follow these commit guidelines: - To run the source-code based tests, execute [`cargo test`](https://doc.rust-lang.org/cargo/commands/cargo-test.html) in the Iroha root. Note that this is a long process. - To run benchmarks, execute [`cargo bench`](https://doc.rust-lang.org/cargo/commands/cargo-bench.html) from the Iroha root. To help debug benchmark outputs, set the `debug_assertions` environment variable like so: `RUSTFLAGS="--cfg debug_assertions" cargo bench`. - If you are working on a particular component, be mindful that when you run `cargo test` in a [workspace](https://doc.rust-lang.org/cargo/reference/workspaces.html), it will only run the tests for that workspace, which usually doesn't include any [integration tests](https://www.testingxperts.com/blog/what-is-integration-testing). -- If you want to test your changes on a minimal network, the provided [`docker-compose.yml`](docker-compose.yml) creates a network of 4 Iroha peers in docker containers that can be used to test consensus and asset propagation-related logic. We recommend interacting with that network using either [`iroha-python`](https://github.com/hyperledger/iroha-python), or the included `iroha_client_cli`. +- If you want to test your changes on a minimal network, the provided [`docker-compose.yml`](configs/swarm/docker-compose.yml) creates a network of 4 Iroha peers in docker containers that can be used to test consensus and asset propagation-related logic. We recommend interacting with that network using either [`iroha-python`](https://github.com/hyperledger/iroha-python), or the included `iroha_client_cli`. - Do not remove failing tests. Even tests that are ignored will be run in our pipeline eventually. - If possible, please benchmark your code both before and after making your changes, as a significant performance regression can break existing users' installations. diff --git a/Cargo.lock b/Cargo.lock index 4b76a0de150..d2bad0789f4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -622,6 +622,7 @@ dependencies = [ "iana-time-zone", "js-sys", "num-traits", + "serde", "wasm-bindgen", "windows-targets 0.48.5", ] @@ -1031,20 +1032,6 @@ dependencies = [ "itertools 0.10.5", ] -[[package]] -name = "crossbeam" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2801af0d36612ae591caa9568261fddce32ce6e08a7275ea334a06a4ad021a2c" -dependencies = [ - "cfg-if", - "crossbeam-channel", - "crossbeam-deque", - "crossbeam-epoch", - "crossbeam-queue", - "crossbeam-utils", -] - [[package]] name = "crossbeam-channel" version = "0.5.9" @@ -1302,6 +1289,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eb30d70a07a3b04884d2677f06bec33509dc67ca60d92949e5535352d3191dc" dependencies = [ "powerfmt", + "serde", ] [[package]] @@ -2626,6 +2614,7 @@ checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", "hashbrown 0.12.3", + "serde", ] [[package]] @@ -2685,15 +2674,16 @@ dependencies = [ "iroha_telemetry", "iroha_torii", "iroha_wasm_builder", + "json5", "once_cell", "owo-colors", "path-absolutize", - "serde_json", "serial_test", "supports-color 2.1.0", "tempfile", "thread-local-panic-hook", "tokio", + "toml 0.8.8", "tracing", "vergen", ] @@ -2720,18 +2710,22 @@ dependencies = [ "iroha_logger", "iroha_primitives", "iroha_telemetry", + "iroha_torii_const", "iroha_version", "iroha_wasm_builder", + "merge", "once_cell", "parity-scale-codec", "rand", "serde", "serde_json", + "serde_with", "tempfile", "test_network", "thiserror", "tokio", "tokio-tungstenite", + "toml 0.8.8", "tracing-flame", "tracing-subscriber", "tungstenite", @@ -2764,21 +2758,27 @@ dependencies = [ "displaydoc", "expect-test", "eyre", + "hex", "iroha_config_base", "iroha_crypto", "iroha_data_model", "iroha_genesis", "iroha_primitives", "json5", + "merge", + "nonzero_ext", "once_cell", "proptest", "serde", "serde_json", + "serde_with", "stacker", "strum 0.25.0", "thiserror", + "toml 0.8.8", "tracing", "tracing-subscriber", + "trybuild", "url", ] @@ -2786,27 +2786,15 @@ dependencies = [ name = "iroha_config_base" version = "2.0.0-pre-rc.20" dependencies = [ - "crossbeam", - "displaydoc", + "derive_more", + "drop_bomb", "eyre", - "iroha_config_derive", - "iroha_crypto", - "json5", - "parking_lot", + "merge", + "num-traits", "serde", - "serde_json", + "serde_with", "thiserror", -] - -[[package]] -name = "iroha_config_derive" -version = "2.0.0-pre-rc.20" -dependencies = [ - "iroha_macro_utils", - "proc-macro-error", - "proc-macro2", - "quote", - "syn 1.0.109", + "toml 0.8.8", ] [[package]] @@ -3235,6 +3223,7 @@ dependencies = [ "pathdiff", "serde", "serde_json", + "serde_with", "serde_yaml", ] @@ -3294,6 +3283,7 @@ dependencies = [ "iroha_primitives", "iroha_schema_gen", "iroha_telemetry", + "iroha_torii_const", "iroha_torii_derive", "iroha_version", "parity-scale-codec", @@ -3305,6 +3295,13 @@ dependencies = [ "warp", ] +[[package]] +name = "iroha_torii_const" +version = "2.0.0-pre-rc.20" +dependencies = [ + "iroha_primitives", +] + [[package]] name = "iroha_torii_derive" version = "2.0.0-pre-rc.20" @@ -3747,6 +3744,28 @@ dependencies = [ "autocfg", ] +[[package]] +name = "merge" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10bbef93abb1da61525bbc45eeaff6473a41907d19f8f9aa5168d214e10693e9" +dependencies = [ + "merge_derive", + "num-traits", +] + +[[package]] +name = "merge_derive" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "209d075476da2e63b4b29e72a2ef627b840589588e71400a25e3565c4f849d07" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "mime" version = "0.3.17" @@ -3862,6 +3881,12 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nonzero_ext" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21" + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -4337,12 +4362,11 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro-crate" -version = "2.0.1" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97dc5fea232fc28d2f597b37c4876b348a40e33f3b02cc975c8d006d78d94b1a" +checksum = "7e8366a6159044a37876a2b9817124296703c586a5c92e2c53751fa06d8d43e8" dependencies = [ - "toml_datetime", - "toml_edit", + "toml_edit 0.20.2", ] [[package]] @@ -4929,6 +4953,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -4947,8 +4980,15 @@ version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "64cd236ccc1b7a29e7e2739f27c0b2dd199804abc4290e32f59f3b68d6405c23" dependencies = [ + "base64", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.1.0", "serde", + "serde_json", "serde_with_macros", + "time", ] [[package]] @@ -5658,11 +5698,26 @@ dependencies = [ "serde", ] +[[package]] +name = "toml" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a195ec8c9da26928f773888e0742ca3ca1040c6cd859c919c9f59c1954ab35" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit 0.21.0", +] + [[package]] name = "toml_datetime" -version = "0.6.3" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +dependencies = [ + "serde", +] [[package]] name = "toml_edit" @@ -5675,6 +5730,19 @@ dependencies = [ "winnow", ] +[[package]] +name = "toml_edit" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34d383cd00a163b4a5b85053df514d45bc330f6de7737edfe0a93311d1eaa03" +dependencies = [ + "indexmap 2.1.0", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + [[package]] name = "tonic" version = "0.10.2" @@ -6331,7 +6399,7 @@ dependencies = [ "serde", "serde_derive", "sha2", - "toml", + "toml 0.5.11", "windows-sys 0.48.0", "zstd", ] diff --git a/Cargo.toml b/Cargo.toml index b961c3d7966..7764075fca4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ iroha = { path = "cli" } iroha_dsl = { version = "=2.0.0-pre-rc.20", path = "dsl" } iroha_torii = { version = "=2.0.0-pre-rc.20", path = "torii" } iroha_torii_derive = { version = "=2.0.0-pre-rc.20", path = "torii/derive" } +iroha_torii_const = { version = "=2.0.0-pre-rc.20", path = "torii/const" } iroha_macro_utils = { version = "=2.0.0-pre-rc.20", path = "macro/utils" } iroha_telemetry = { version = "=2.0.0-pre-rc.20", path = "telemetry" } iroha_telemetry_derive = { version = "=2.0.0-pre-rc.20", path = "telemetry/derive" } @@ -30,7 +31,6 @@ iroha_data_model_derive = { version = "=2.0.0-pre-rc.20", path = "data_model/der iroha_client = { version = "=2.0.0-pre-rc.20", path = "client" } iroha_config = { version = "=2.0.0-pre-rc.20", path = "config" } iroha_config_base = { version = "=2.0.0-pre-rc.20", path = "config/base" } -iroha_config_derive = { version = "=2.0.0-pre-rc.20", path = "config/base/derive" } iroha_schema_gen = { version = "=2.0.0-pre-rc.20", path = "schema/gen" } iroha_schema = { version = "=2.0.0-pre-rc.20", path = "schema", default-features = false } iroha_schema_derive = { version = "=2.0.0-pre-rc.20", path = "schema/derive" } @@ -65,6 +65,7 @@ syn2 = { package = "syn", version = "2.0.38", default-features = false } quote = "1.0.33" manyhow = { version = "0.8.1", features = ["darling"] } darling = "0.20.3" +drop_bomb = "0.1.5" futures = { version = "0.3.28", default-features = false } tokio = "1.33.0" @@ -88,6 +89,7 @@ impls = "1.0.3" base64 = { version = "0.21.4", default-features = false } hex = { version = "0.4.3", default-features = false } +nonzero_ext = "0.3.0" fixnum = { version = "0.9.2", default-features = false } url = "2.4.1" @@ -132,6 +134,7 @@ serde_yaml = "0.9.25" serde_with = { version = "3.3.0", default-features = false } parity-scale-codec = { version = "3.6.5", default-features = false } json5 = "0.4.1" +toml = "0.8.8" [workspace.lints] rustdoc.private_doc_tests = "deny" @@ -206,7 +209,6 @@ members = [ "client_cli", "config", "config/base", - "config/base/derive", "core", "core/test_network", "crypto", @@ -242,6 +244,7 @@ members = [ "tools/wasm_test_runner", "torii", "torii/derive", + "torii/const", "version", "version/derive", "wasm_codec", diff --git a/Dockerfile b/Dockerfile index 5b78c51c074..fbc66fc751c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -29,6 +29,7 @@ ENV CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_LINKER=/x86_64-linux-musl-native/bin/ # builder stage WORKDIR /iroha COPY . . +# FIXME: shouldn't it only build `iroha`, `iroha_client_cli`, and `kagami`? RUN cargo build --target x86_64-unknown-linux-musl --profile deploy @@ -39,9 +40,12 @@ ARG STORAGE=/storage ARG TARGET_DIR=/iroha/target/x86_64-unknown-linux-musl/deploy ENV BIN_PATH=/usr/local/bin/ ENV CONFIG_DIR=/config + +# FIXME: these are obsolete ENV IROHA2_CONFIG_PATH=$CONFIG_DIR/config.json ENV IROHA2_GENESIS_PATH=$CONFIG_DIR/genesis.json ENV KURA_BLOCK_STORE_PATH=$STORAGE + ENV WASM_DIRECTORY=/app/.cache/wasmtime ENV USER=iroha ENV UID=1001 diff --git a/README.md b/README.md index d89ee642b3e..c1306066ac8 100644 --- a/README.md +++ b/README.md @@ -124,9 +124,7 @@ docker compose up With the `docker-compose` instance running, use [Iroha Client CLI](./client_cli/README.md): ```bash -cp configs/client/config.json target/debug/config.json -cd target/debug -./iroha_client_cli --help +cargo run --bin iroha_client_cli -- --config ./configs/swarm/client.toml ``` ## Integration @@ -166,12 +164,7 @@ A brief overview on how to configure and maintain an Iroha instance: There is a set of configuration parameters that could be passed either through a configuration file or environment variables. ```shell -# look for `config.json` or `config.json5` (won't fail if files are not found) -iroha - -# Override default config path through CLI or ENV -iroha --config /path/to/config.json -IROHA_CONFIG=/path/to/config.json iroha +iroha --config /path/to/config.toml ``` **Note:** detailed configuration reference is [work in progress](https://github.com/hyperledger/iroha-2-docs/issues/392). @@ -207,11 +200,7 @@ The details of the `Health` endpoint can be found in the [API Reference > Torii Iroha can produce both JSON-formatted as well as `prometheus`-readable metrics at the `status` and `metrics` endpoints respectively. -The [`prometheus`](https://prometheus.io/docs/introduction/overview/) monitoring system is the de-factor standard for monitoring long-running services such as an Iroha peer. In order to get started, [install `prometheus`](https://prometheus.io/docs/introduction/first_steps/) and execute the following in the project root: - -``` -prometheus --config.file=configs/prometheus.yml -``` +The [`prometheus`](https://prometheus.io/docs/introduction/overview/) monitoring system is the de-factor standard for monitoring long-running services such as an Iroha peer. In order to get started, [install `prometheus`](https://prometheus.io/docs/introduction/first_steps/) and use `configs/prometheus.template.yml` for configuration. ### Storage diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 3d744eded61..4ab631e22e2 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -65,7 +65,8 @@ thread-local-panic-hook = { version = "0.1.0", optional = true } [dev-dependencies] serial_test = "2.0.0" tempfile = { workspace = true } -serde_json = { workspace = true } +toml = { workspace = true } +json5 = { workspace = true } futures = { workspace = true } path-absolutize = { workspace = true } assertables = "7" diff --git a/cli/README.md b/cli/README.md index 87f1c1aed06..5ba8d269b39 100644 --- a/cli/README.md +++ b/cli/README.md @@ -82,17 +82,20 @@ You may deploy Iroha as a [native binary](#native-binary) or by using [Docker](# ### Native binary + + 1. Prepare a deployment environment. If you plan on running the `iroha` peer binary from the directory `deploy`, copy `config.json` and `genesis.json`: ```bash - cp ./target/release/iroha - cp ./configs/peer/config.json deploy - cp ./configs/peer/genesis.json deploy + # FIXME + # cp ./target/release/iroha + # cp ./configs/peer/config.json deploy + # cp ./configs/peer/genesis.json deploy ``` -2. Make necessary edits to `config.json` and `genesis.json`, such as: +2. Make the necessary edits to `config.json` and `genesis.json`, such as: - Generate new key pairs and add their values to `genesis.json`) - Adjust the port values for your initial set of trusted peers @@ -111,7 +114,7 @@ You may deploy Iroha as a [native binary](#native-binary) or by using [Docker](# ### Docker -We provide a sample configuration for Docker in [`docker-compose.yml`](../docker-compose.yml). We highly recommend that you adjust the `config.json` to include a set of new key pairs. +We provide a sample configuration for Docker in [`docker-compose.yml`](../configs/swarm/docker-compose.yml). We highly recommend that you adjust the `config.json` to include a set of new key pairs. [Generate the keys](#generating-keys) and put them into `services.*.environment` in `docker-compose.yml`. Don't forget to update the public keys of `TRUSTED_PEERS`. diff --git a/cli/src/lib.rs b/cli/src/lib.rs index afb4f53e538..5dbf5318efd 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -6,16 +6,10 @@ //! should be constructed externally: (see `main.rs`). #[cfg(debug_assertions)] use core::sync::atomic::{AtomicBool, Ordering}; -use std::{path::PathBuf, sync::Arc}; +use std::{path::Path, sync::Arc}; use color_eyre::eyre::{eyre, Result, WrapErr}; -use iroha_config::{ - base::proxy::{LoadFromDisk, LoadFromEnv, Override}, - genesis::ParsedConfiguration as ParsedGenesisConfiguration, - iroha::{Configuration, ConfigurationProxy}, - path::Path, - telemetry::Configuration as TelemetryConfiguration, -}; +use iroha_config::parameters::{actual::Root as Config, user::CliContext}; use iroha_core::{ block_sync::{BlockSynchronizer, BlockSynchronizerHandle}, gossiper::{TransactionGossiper, TransactionGossiperHandle}, @@ -28,11 +22,10 @@ use iroha_core::{ smartcontracts::isi::Registrable as _, snapshot::{try_read_snapshot, SnapshotMaker, SnapshotMakerHandle}, sumeragi::{SumeragiHandle, SumeragiStartArgs}, - tx::PeerId, IrohaNetwork, }; use iroha_data_model::prelude::*; -use iroha_genesis::GenesisNetwork; +use iroha_genesis::{GenesisNetwork, RawGenesisBlock}; use iroha_logger::actor::LoggerHandle; use iroha_torii::Torii; use tokio::{ @@ -201,28 +194,29 @@ impl Iroha { #[allow(clippy::too_many_lines)] #[iroha_logger::log(name = "init", skip_all)] // This is actually easier to understand as a linear sequence of init statements. pub async fn new( - config: Configuration, + config: Config, genesis: Option, logger: LoggerHandle, ) -> Result { - let listen_addr = config.torii.p2p_addr.clone(); - let network = IrohaNetwork::start(listen_addr, config.sumeragi.key_pair.clone()) - .await - .wrap_err("Unable to start P2P-network")?; + let network = IrohaNetwork::start( + config.common.p2p_address.clone(), + config.common.key_pair.clone(), + ) + .await + .wrap_err("Unable to start P2P-network")?; let (events_sender, _) = broadcast::channel(10000); let world = World::with( - [genesis_domain(config.genesis.public_key.clone())], - config.sumeragi.trusted_peers.peers.clone(), + [genesis_domain(config.genesis.public_key().clone())], + config.sumeragi.trusted_peers.clone(), ); let kura = Kura::new(&config.kura)?; - let live_query_store_handle = - LiveQueryStore::from_configuration(config.live_query_store).start(); + let live_query_store_handle = LiveQueryStore::from_config(config.live_query_store).start(); let block_count = kura.init()?; let wsv = try_read_snapshot( - &config.snapshot.dir_path, + &config.snapshot.store_dir, &kura, live_query_store_handle.clone(), block_count, @@ -230,8 +224,8 @@ impl Iroha { .map_or_else( |error| { iroha_logger::warn!(%error, "Failed to load wsv from snapshot, creating empty wsv"); - WorldStateView::from_configuration( - *config.wsv, + WorldStateView::from_config( + config.chain_wide, world, Arc::clone(&kura), live_query_store_handle.clone(), @@ -246,8 +240,8 @@ impl Iroha { }, ); - let queue = Arc::new(Queue::from_configuration(&config.queue)); - match Self::start_telemetry(&logger, &config.telemetry).await? { + let queue = Arc::new(Queue::from_config(config.queue)); + match Self::start_telemetry(&logger, &config).await? { TelemetryStartStatus::Started => iroha_logger::info!("Telemetry started"), TelemetryStartStatus::NotStarted => iroha_logger::warn!("Telemetry not started"), }; @@ -255,8 +249,8 @@ impl Iroha { let kura_thread_handler = Kura::start(Arc::clone(&kura)); let start_args = SumeragiStartArgs { - chain_id: config.chain_id.clone(), - configuration: config.sumeragi.clone(), + sumeragi_config: config.sumeragi.clone(), + common_config: config.common.clone(), events_sender: events_sender.clone(), wsv, queue: Arc::clone(&queue), @@ -270,18 +264,18 @@ impl Iroha { .await .expect("Failed to join task with Sumeragi start"); - let block_sync = BlockSynchronizer::from_configuration( + let block_sync = BlockSynchronizer::from_config( &config.block_sync, sumeragi.clone(), Arc::clone(&kura), - PeerId::new(config.torii.p2p_addr.clone(), config.public_key.clone()), + config.common.peer_id(), network.clone(), ) .start(); - let gossiper = TransactionGossiper::from_configuration( - config.chain_id.clone(), - &config.sumeragi, + let gossiper = TransactionGossiper::from_config( + config.common.chain_id.clone(), + config.transaction_gossiper, network.clone(), Arc::clone(&queue), sumeragi.clone(), @@ -304,15 +298,14 @@ impl Iroha { } .start(); - let snapshot_maker = - SnapshotMaker::from_configuration(&config.snapshot, sumeragi.clone()).start(); + let snapshot_maker = SnapshotMaker::from_config(&config.snapshot, sumeragi.clone()).start(); let kiso = KisoHandle::new(config.clone()); let torii = Torii::new( - config.chain_id, + config.common.chain_id.clone(), kiso.clone(), - &config.torii, + config.torii, Arc::clone(&queue), events_sender, Arc::clone(¬ify_shutdown), @@ -321,7 +314,7 @@ impl Iroha { Arc::clone(&kura), ); - Self::spawn_configuration_updates_broadcasting(kiso.clone(), logger.clone()); + Self::spawn_config_updates_broadcasting(kiso.clone(), logger.clone()); Self::start_listening_signal(Arc::clone(¬ify_shutdown))?; @@ -376,30 +369,27 @@ impl Iroha { #[cfg(feature = "telemetry")] async fn start_telemetry( logger: &LoggerHandle, - config: &TelemetryConfiguration, + config: &Config, ) -> Result { - #[allow(unused)] - let (config_for_regular, config_for_dev) = config.parse(); - #[cfg(feature = "dev-telemetry")] { - if let Some(config) = config_for_dev { + if let Some(config) = &config.dev_telemetry { let receiver = logger .subscribe_on_telemetry(iroha_logger::telemetry::Channel::Future) .await .wrap_err("Failed to subscribe on telemetry")?; - let _handle = iroha_telemetry::dev::start(config, receiver) + let _handle = iroha_telemetry::dev::start(config.clone(), receiver) .await .wrap_err("Failed to setup telemetry for futures")?; } } - if let Some(config) = config_for_regular { + if let Some(config) = &config.telemetry { let receiver = logger .subscribe_on_telemetry(iroha_logger::telemetry::Channel::Regular) .await .wrap_err("Failed to subscribe on telemetry")?; - let _handle = iroha_telemetry::ws::start(config, receiver) + let _handle = iroha_telemetry::ws::start(config.clone(), receiver) .await .wrap_err("Failed to setup telemetry for websocket communication")?; @@ -412,7 +402,7 @@ impl Iroha { #[cfg(not(feature = "telemetry"))] async fn start_telemetry( _logger: &LoggerHandle, - _config: &TelemetryConfiguration, + _config: &Config, ) -> Result { Ok(TelemetryStartStatus::NotStarted) } @@ -448,7 +438,7 @@ impl Iroha { /// Spawns a task which subscribes on updates from configuration actor /// and broadcasts them further to interested actors. This way, neither config actor nor other ones know /// about each other, achieving loose coupling of code and system. - fn spawn_configuration_updates_broadcasting( + fn spawn_config_updates_broadcasting( kiso: KisoHandle, logger: LoggerHandle, ) -> task::JoinHandle<()> { @@ -498,103 +488,26 @@ fn genesis_domain(public_key: PublicKey) -> Domain { domain } -macro_rules! mutate_nested_option { - ($obj:expr, self, $func:expr) => { - $obj.as_mut().map($func) - }; - ($obj:expr, $field:ident, $func:expr) => { - $obj.$field.as_mut().map($func) - }; - ($obj:expr, [$field:ident, $($rest:tt)+], $func:expr) => { - $obj.$field.as_mut().map(|x| { - mutate_nested_option!(x, [$($rest)+], $func) - }) - }; - ($obj:tt, [$field:tt], $func:expr) => { - mutate_nested_option!($obj, $field, $func) - }; -} - -/// Read and parse Iroha configuration and genesis block. -/// -/// The pipeline of configuration reading is as follows: -/// -/// 1. Construct a layer with default values -/// 2. If [`Path`] resolves, construct a layer from the file and merge it into the previous one -/// 3. Construct a layer from ENV vars and merge it into the previous one -/// 4. Check whether the final layer contains the complete configuration -/// -/// After reading it, this function ensures validity of genesis configuration and constructs the -/// [`GenesisNetwork`] according to it. +/// Read configuration and then a genesis block if specified. /// /// # Errors -/// - If provided user configuration is invalid or incomplete -/// - If genesis config is invalid -pub fn read_config( - path: &Path, +/// - If failed to read the config +/// - If failed to load the genesis block +/// - If failed to build a genesis network +pub fn read_config_and_genesis>( + path: Option

, submit_genesis: bool, -) -> Result<(Configuration, Option)> { - let config = ConfigurationProxy::default(); - - let config = if let Some(actual_config_path) = path - .try_resolve() - .wrap_err("Failed to resolve configuration file")? - { - let mut cfg = config.override_with(ConfigurationProxy::from_path(&*actual_config_path)); - let config_dir = actual_config_path - .parent() - .expect("If config file was read, than it should have a parent. It is a bug."); - - // careful here: `genesis.file` might be a path relative to the config file. - // we need to resolve it before proceeding - // TODO: move this logic into `iroha_config` - // https://github.com/hyperledger/iroha/issues/4161 - let join_to_config_dir = |x: &mut PathBuf| { - *x = config_dir.join(&x); - }; - mutate_nested_option!(cfg, [genesis, file, self], join_to_config_dir); - mutate_nested_option!(cfg, [snapshot, dir_path], join_to_config_dir); - mutate_nested_option!(cfg, [kura, block_store_path], join_to_config_dir); - mutate_nested_option!(cfg, [telemetry, file, self], join_to_config_dir); +) -> Result<(Config, Option)> { + use iroha_config::parameters::actual::Genesis; - cfg - } else { - config - }; + let config = Config::load(path, CliContext { submit_genesis }) + .wrap_err("failed to load configuration")?; - // 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 genesis = if let Genesis::Full { key_pair, file } = &config.genesis { + let raw_block = RawGenesisBlock::from_path(file)?; - let config = config - .build() - .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, &config.chain_id, &key_pair) + GenesisNetwork::new(raw_block, &config.common.chain_id, key_pair) .wrap_err("Failed to construct the genesis")?, ) } else { @@ -637,6 +550,7 @@ mod tests { mod config_integration { use assertables::{assert_contains, assert_contains_as_result}; + use iroha_config::parameters::user::RootPartial as PartialUserConfig; use iroha_crypto::KeyPair; use iroha_genesis::{ExecutorMode, ExecutorPath}; use iroha_primitives::addr::socket_addr; @@ -644,24 +558,20 @@ mod tests { use super::*; - fn config_factory() -> ConfigurationProxy { - let key_pair = KeyPair::generate(); + fn config_factory() -> PartialUserConfig { + let (pubkey, privkey) = KeyPair::generate().into(); - let mut base = ConfigurationProxy { - chain_id: Some(ChainId::new("0")), + let mut base = PartialUserConfig::default(); - public_key: Some(key_pair.public_key().clone()), - private_key: Some(key_pair.private_key().clone()), + base.chain_id.set(ChainId::from("0")); + base.public_key.set(pubkey.clone()); + base.private_key.set(privkey.clone()); + base.network.address.set(socket_addr!(127.0.0.1:1337)); - ..ConfigurationProxy::default() - }; - let genesis = base.genesis.as_mut().unwrap(); - genesis.private_key = Some(Some(key_pair.private_key().clone())); - genesis.public_key = Some(key_pair.public_key().clone()); + base.genesis.public_key.set(pubkey); + base.genesis.private_key.set(privkey); - let torii = base.torii.as_mut().unwrap(); - torii.p2p_addr = Some(socket_addr!(127.0.0.1:1337)); - torii.api_url = Some(socket_addr!(127.0.0.1:1337)); + base.torii.address.set(socket_addr!(127.0.0.1:8080)); base } @@ -676,28 +586,28 @@ mod tests { let config = { let mut cfg = config_factory(); - cfg.genesis.as_mut().unwrap().file = Some(Some("./genesis/gen.json".into())); - cfg.kura.as_mut().unwrap().block_store_path = Some("../storage".into()); - cfg.snapshot.as_mut().unwrap().dir_path = Some("../snapshots".into()); - cfg.telemetry.as_mut().unwrap().file = Some(Some("../logs/telemetry".into())); - cfg + cfg.genesis.file.set("./genesis/gen.json".into()); + cfg.kura.store_dir.set("../storage".into()); + cfg.snapshot.store_dir.set("../snapshots".into()); + cfg.telemetry.dev.out_file.set("../logs/telemetry".into()); + toml::Value::try_from(cfg)? }; let dir = tempfile::tempdir()?; let genesis_path = dir.path().join("config/genesis/gen.json"); let executor_path = dir.path().join("config/genesis/executor.wasm"); - let config_path = dir.path().join("config/config.json5"); + let config_path = dir.path().join("config/config.toml"); std::fs::create_dir(dir.path().join("config"))?; std::fs::create_dir(dir.path().join("config/genesis"))?; - std::fs::write(config_path, serde_json::to_string(&config)?)?; - std::fs::write(genesis_path, serde_json::to_string(&genesis)?)?; + std::fs::write(config_path, toml::to_string(&config)?)?; + std::fs::write(genesis_path, json5::to_string(&genesis)?)?; std::fs::write(executor_path, "")?; - let config_path = Path::default(dir.path().join("config/config")); + let config_path = dir.path().join("config/config.toml"); // When - let (config, genesis) = read_config(&config_path, true)?; + let (config, genesis) = read_config_and_genesis(Some(config_path), true)?; // Then @@ -705,15 +615,19 @@ mod tests { assert!(genesis.is_some()); assert_eq!( - config.kura.block_store_path.absolutize()?, + config.kura.store_dir.absolutize()?, dir.path().join("storage") ); assert_eq!( - config.snapshot.dir_path.absolutize()?, + config.snapshot.store_dir.absolutize()?, dir.path().join("snapshots") ); assert_eq!( - config.telemetry.file.expect("Should be set").absolutize()?, + config + .dev_telemetry + .expect("dev telemetry should be set") + .out_file + .absolutize()?, dir.path().join("logs/telemetry") ); @@ -730,28 +644,22 @@ mod tests { let config = { let mut cfg = config_factory(); - cfg.genesis.as_mut().unwrap().file = Some(Some("./genesis.json".into())); - cfg + cfg.genesis.file.set("./genesis.json".into()); + toml::Value::try_from(cfg)? }; let dir = tempfile::tempdir()?; - std::fs::write( - dir.path().join("config.json"), - serde_json::to_string(&config)?, - )?; - std::fs::write( - dir.path().join("genesis.json"), - serde_json::to_string(&genesis)?, - )?; + std::fs::write(dir.path().join("config.toml"), toml::to_string(&config)?)?; + std::fs::write(dir.path().join("genesis.json"), json5::to_string(&genesis)?)?; std::fs::write(dir.path().join("executor.wasm"), "")?; - let config_path = Path::user_provided(dir.path().join("config.json"))?; + let config_path = dir.path().join("config.toml"); // When & Then - let report = read_config(&config_path, false).unwrap_err(); + let report = read_config_and_genesis(Some(config_path), false).unwrap_err(); assert_contains!( - format!("{report}"), + format!("{report:#}"), "The network consists from this one peer only" ); diff --git a/cli/src/main.rs b/cli/src/main.rs index 14ef7a587d5..34c7909ef9d 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -1,11 +1,8 @@ //! Iroha peer command-line interface. -use std::env; +use std::{env, path::PathBuf}; use clap::Parser; use color_eyre::eyre::Result; -use iroha_config::path::Path; - -const DEFAULT_CONFIG_PATH: &str = "config"; fn is_colouring_supported() -> bool { supports_color::on(supports_color::Stream::Stdout).is_some() @@ -19,22 +16,9 @@ fn default_terminal_colors_str() -> clap::builder::OsStr { #[derive(Parser, Debug)] #[command(name = "iroha", version = concat!("version=", env!("CARGO_PKG_VERSION"), " git_commit_sha=", env!("VERGEN_GIT_SHA")), author)] struct Args { - /// Path to the configuration file, defaults to `config.json`/`config.json5` - /// - /// Supported extensions are `.json` and `.json5`. By default, Iroha looks for a - /// `config` file with one of the supported extensions in the current working directory. - /// If the default config file is not found, Iroha will rely on default values and environment - /// variables. However, if the config path is set explicitly with this argument and the file - /// is not found, Iroha will exit with an error. - #[arg( - long, - short, - env("IROHA_CONFIG"), - value_name("PATH"), - value_parser(Path::user_provided_str), - value_hint(clap::ValueHint::FilePath) - )] - config: Option, + /// Path to the configuration file + #[arg(long, short, value_name("PATH"), 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. @@ -73,11 +57,7 @@ async fn main() -> Result<()> { color_eyre::install()?; } - let config_path = args - .config - .unwrap_or_else(|| Path::default(DEFAULT_CONFIG_PATH)); - - let (config, genesis) = iroha::read_config(&config_path, args.submit_genesis)?; + let (config, genesis) = iroha::read_config_and_genesis(args.config, args.submit_genesis)?; let logger = iroha_logger::init_global(&config.logger, args.terminal_colors)?; iroha_logger::info!( @@ -100,8 +80,6 @@ async fn main() -> Result<()> { #[cfg(test)] mod tests { - use assertables::{assert_contains, assert_contains_as_result}; - use super::*; #[test] @@ -109,7 +87,6 @@ mod tests { 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); @@ -139,21 +116,14 @@ mod tests { fn user_provided_config_path_works() -> Result<()> { let args = Args::try_parse_from(["test", "--config", "/home/custom/file.json"])?; - assert_eq!( - args.config, - Some(Path::user_provided("/home/custom/file.json").unwrap()) - ); + assert_eq!(args.config, Some(PathBuf::from("/home/custom/file.json"))); 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`"); + fn user_can_provide_any_extension() { + let _args = Args::try_parse_from(["test", "--config", "file.toml.but.not"]) + .expect("should allow doing this as well"); } } diff --git a/cli/src/samples.rs b/cli/src/samples.rs index 0a4c13870b2..35fd25da53e 100644 --- a/cli/src/samples.rs +++ b/cli/src/samples.rs @@ -1,14 +1,23 @@ //! This module contains the sample configurations used for testing and benchmarking throughout Iroha. -use std::{collections::HashSet, path::Path, str::FromStr}; +use std::{collections::HashSet, path::Path, str::FromStr, time::Duration}; use iroha_config::{ - iroha::{Configuration, ConfigurationProxy}, - sumeragi::TrustedPeers, - torii::{uri::DEFAULT_API_ADDR, DEFAULT_TORII_P2P_ADDR}, + base::{HumanDuration, UnwrapPartial}, + parameters::{ + actual::Root as Config, + user::{CliContext, RootPartial as UserConfig}, + }, }; use iroha_crypto::{KeyPair, PublicKey}; use iroha_data_model::{peer::PeerId, prelude::*, ChainId}; -use iroha_primitives::unique_vec::UniqueVec; +use iroha_primitives::{ + addr::{socket_addr, SocketAddr}, + unique_vec::UniqueVec, +}; + +// FIXME: move to a global test-related place, re-use everywhere else +const DEFAULT_P2P_ADDR: SocketAddr = socket_addr!(127.0.0.1:1337); +const DEFAULT_TORII_ADDR: SocketAddr = socket_addr!(127.0.0.1:8080); /// Get sample trusted peers. The public key must be the same as `configuration.public_key` /// @@ -33,57 +42,57 @@ pub fn get_trusted_peers(public_key: Option<&PublicKey>) -> HashSet { .map(|(a, k)| PeerId::new(a.parse().expect("Valid"), PublicKey::from_str(k).unwrap())) .collect(); if let Some(pubkey) = public_key { - trusted_peers.insert(PeerId::new(DEFAULT_TORII_P2P_ADDR.clone(), pubkey.clone())); + trusted_peers.insert(PeerId { + address: DEFAULT_P2P_ADDR.clone(), + public_key: pubkey.clone(), + }); } trusted_peers } #[allow(clippy::implicit_hasher)] -/// Get a sample Iroha configuration proxy. Trusted peers must be +/// Get a sample Iroha configuration on user-layer level. Trusted peers must be /// specified in this function, including the current peer. Use [`get_trusted_peers`] /// to populate `trusted_peers` if in doubt. Almost equivalent to the [`get_config`] /// function, except the proxy is left unbuilt. /// /// # Panics /// - when [`KeyPair`] generation fails (rare case). -pub fn get_config_proxy( - peers: UniqueVec, +pub fn get_user_config( + peers: &UniqueVec, chain_id: Option, key_pair: Option, -) -> ConfigurationProxy { - let chain_id = chain_id.unwrap_or_else(|| ChainId::new("0")); +) -> UserConfig { + let chain_id = chain_id.unwrap_or_else(|| ChainId::from("0")); let (public_key, private_key) = key_pair.unwrap_or_else(KeyPair::generate).into(); iroha_logger::info!(%public_key); - ConfigurationProxy { - chain_id: Some(chain_id), - public_key: Some(public_key.clone()), - private_key: Some(private_key.clone()), - sumeragi: Some(Box::new(iroha_config::sumeragi::ConfigurationProxy { - max_transactions_in_block: Some(2), - trusted_peers: Some(TrustedPeers { peers }), - ..iroha_config::sumeragi::ConfigurationProxy::default() - })), - torii: Some(Box::new(iroha_config::torii::ConfigurationProxy { - p2p_addr: Some(DEFAULT_TORII_P2P_ADDR.clone()), - api_url: Some(DEFAULT_API_ADDR.clone()), - ..iroha_config::torii::ConfigurationProxy::default() - })), - block_sync: Some(iroha_config::block_sync::ConfigurationProxy { - block_batch_size: Some(1), - gossip_period_ms: Some(500), - ..iroha_config::block_sync::ConfigurationProxy::default() - }), - queue: Some(iroha_config::queue::ConfigurationProxy { - ..iroha_config::queue::ConfigurationProxy::default() - }), - genesis: Some(Box::new(iroha_config::genesis::ConfigurationProxy { - private_key: Some(Some(private_key)), - public_key: Some(public_key), - file: Some(Some("./genesis.json".into())), - })), - ..ConfigurationProxy::default() - } + + let mut config = UserConfig::new(); + + config.chain_id.set(chain_id); + config.public_key.set(public_key.clone()); + config.private_key.set(private_key.clone()); + config.network.address.set(DEFAULT_P2P_ADDR); + config + .chain_wide + .max_transactions_in_block + .set(2.try_into().unwrap()); + config.sumeragi.trusted_peers.set(peers.to_vec()); + config.torii.address.set(DEFAULT_TORII_ADDR); + config + .network + .block_gossip_max_size + .set(1.try_into().unwrap()); + config + .network + .block_gossip_period + .set(HumanDuration(Duration::from_millis(500))); + config.genesis.private_key.set(private_key); + config.genesis.public_key.set(public_key); + config.genesis.file.set("./genesis.json".into()); + + config } #[allow(clippy::implicit_hasher)] @@ -94,13 +103,17 @@ pub fn get_config_proxy( /// # Panics /// - when [`KeyPair`] generation fails (rare case). pub fn get_config( - trusted_peers: UniqueVec, + trusted_peers: &UniqueVec, chain_id: Option, key_pair: Option, -) -> Configuration { - get_config_proxy(trusted_peers, chain_id, key_pair) - .build() - .expect("Iroha config should build as all required fields were provided") +) -> Config { + get_user_config(trusted_peers, chain_id, key_pair) + .unwrap_partial() + .expect("config should build as all required fields were provided") + .parse(CliContext { + submit_genesis: true, + }) + .expect("config should finalize as the input is semantically valid (or there is a bug)") } /// Construct executor from path. diff --git a/client/Cargo.toml b/client/Cargo.toml index 5a3aba4aadb..1d38df505b9 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -54,6 +54,7 @@ iroha_data_model = { workspace = true, features = ["http"] } iroha_primitives = { workspace = true } iroha_logger = { workspace = true } iroha_telemetry = { workspace = true } +iroha_torii_const = { workspace = true } iroha_version = { workspace = true, features = ["http"] } attohttpc = { version = "0.26.1", default-features = false } @@ -62,6 +63,7 @@ http = "0.2.9" url = { workspace = true } rand = { workspace = true } serde = { workspace = true, features = ["derive"] } +serde_with = { workspace = true } serde_json = { workspace = true } base64 = { workspace = true } thiserror = { workspace = true } @@ -72,6 +74,8 @@ tokio = { workspace = true, features = ["rt"] } tokio-tungstenite = { workspace = true } tungstenite = { workspace = true } futures-util = "0.3.28" +merge = "0.1.0" +toml = { workspace = true } [dev-dependencies] iroha_wasm_builder = { workspace = true } diff --git a/client/README.md b/client/README.md index 10073589a11..c15f7c9b6ad 100644 --- a/client/README.md +++ b/client/README.md @@ -16,15 +16,9 @@ Follow the [Iroha 2 tutorial](https://hyperledger.github.io/iroha-2-docs/guide/r Add the following to the manifest file of your Rust project: ```toml -iroha_client = { git = "https://github.com/hyperledger/iroha/", branch="iroha2-dev" } +iroha_client = { git = "https://github.com/hyperledger/iroha", branch = "iroha2-dev" } ``` ## Examples -```rust -let configuration = - &Configuration::from_path("config.json").expect("Failed to load configuration."); -let mut iroha_client = Client::new(configuration); -``` - We highly recommend looking at the sample [`iroha_client_cli`](../client_cli) implementation binary as well as our [tutorial](https://hyperledger.github.io/iroha-2-docs/guide/rust.html) for more examples and explanations. diff --git a/client/benches/torii.rs b/client/benches/torii.rs index dd503e3c396..669fcc0c917 100644 --- a/client/benches/torii.rs +++ b/client/benches/torii.rs @@ -17,23 +17,12 @@ 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 chain_id = get_chain_id(); let configuration = get_config( - unique_vec![peer.id.clone()], + &unique_vec![peer.id.clone()], Some(chain_id.clone()), Some(get_key_pair()), ); @@ -52,12 +41,15 @@ fn query_requests(criterion: &mut Criterion) { ) .build(), &chain_id, - &get_genesis_key_pair(&configuration), + configuration + .genesis + .key_pair() + .expect("genesis config should be full, probably a bug"), ) .expect("genesis creation failed"); let builder = PeerBuilder::new() - .with_configuration(configuration) + .with_config(configuration) .with_into_genesis(genesis); rt.block_on(builder.start_with_peer(&mut peer)); @@ -81,12 +73,13 @@ fn query_requests(criterion: &mut Criterion) { quantity, AssetId::new(asset_definition_id, account_id.clone()), ); - let mut client_config = - iroha_client::samples::get_client_config(get_chain_id(), &get_key_pair()); - - client_config.torii_api_url = format!("http://{}", peer.api_address).parse().unwrap(); + let client_config = iroha_client::samples::get_client_config( + get_chain_id(), + get_key_pair(), + format!("http://{}", peer.api_address).parse().unwrap(), + ); - let iroha_client = Client::new(&client_config).expect("Invalid client configuration"); + let iroha_client = Client::new(client_config); thread::sleep(std::time::Duration::from_millis(5000)); let instructions: [InstructionBox; 4] = [ @@ -139,7 +132,7 @@ fn instruction_submits(criterion: &mut Criterion) { let chain_id = get_chain_id(); let configuration = get_config( - unique_vec![peer.id.clone()], + &unique_vec![peer.id.clone()], Some(chain_id.clone()), Some(get_key_pair()), ); @@ -148,7 +141,7 @@ fn instruction_submits(criterion: &mut Criterion) { .domain("wonderland".parse().expect("Valid")) .account( "alice".parse().expect("Valid"), - configuration.public_key.clone(), + configuration.common.key_pair.public_key().clone(), ) .finish_domain() .executor( @@ -156,11 +149,14 @@ fn instruction_submits(criterion: &mut Criterion) { ) .build(), &chain_id, - &get_genesis_key_pair(&configuration), + configuration + .genesis + .key_pair() + .expect("config should be full; probably a bug"), ) .expect("failed to create genesis"); let builder = PeerBuilder::new() - .with_configuration(configuration) + .with_config(configuration) .with_into_genesis(genesis); rt.block_on(builder.start_with_peer(&mut peer)); let mut group = criterion.benchmark_group("instruction-requests"); @@ -170,10 +166,12 @@ fn instruction_submits(criterion: &mut Criterion) { let (public_key, _) = KeyPair::generate().into(); let create_account = Register::account(Account::new(account_id.clone(), [public_key])).into(); let asset_definition_id = AssetDefinitionId::new(domain_id, "xor".parse().expect("Valid")); - let mut client_config = - iroha_client::samples::get_client_config(get_chain_id(), &get_key_pair()); - client_config.torii_api_url = format!("http://{}", peer.api_address).parse().unwrap(); - let iroha_client = Client::new(&client_config).expect("Invalid client configuration"); + let client_config = iroha_client::samples::get_client_config( + get_chain_id(), + get_key_pair(), + format!("http://{}", peer.api_address).parse().unwrap(), + ); + let iroha_client = Client::new(client_config); thread::sleep(std::time::Duration::from_millis(5000)); let _ = iroha_client .submit_all([create_domain, create_account]) diff --git a/client/benches/tps/utils.rs b/client/benches/tps/utils.rs index 3901d11b266..f078481269b 100644 --- a/client/benches/tps/utils.rs +++ b/client/benches/tps/utils.rs @@ -196,7 +196,7 @@ impl MeasurerUnit { /// Spawn who periodically submits transactions fn spawn_transaction_submitter(&self, shutdown_signal: mpsc::Receiver<()>) -> JoinHandle<()> { - let chain_id = ChainId::new("0"); + let chain_id = ChainId::from("0"); let submitter = self.client.clone(); let interval_us_per_tx = self.config.interval_us_per_tx; diff --git a/client/examples/million_accounts_genesis.rs b/client/examples/million_accounts_genesis.rs index 737e9236246..c618caf700b 100644 --- a/client/examples/million_accounts_genesis.rs +++ b/client/examples/million_accounts_genesis.rs @@ -2,7 +2,7 @@ use std::{thread, time::Duration}; use iroha::samples::{construct_executor, get_config}; -use iroha_client::{crypto::KeyPair, data_model::prelude::*}; +use iroha_client::data_model::prelude::*; use iroha_data_model::isi::InstructionBox; use iroha_genesis::{GenesisNetwork, RawGenesisBlock, RawGenesisBlockBuilder}; use iroha_primitives::unique_vec; @@ -40,28 +40,24 @@ fn main_genesis() { let chain_id = get_chain_id(); let configuration = get_config( - unique_vec![peer.id.clone()], + &unique_vec![peer.id.clone()], Some(chain_id.clone()), Some(get_key_pair()), ); let rt = Runtime::test(); - let genesis = GenesisNetwork::new(generate_genesis(1_000_000_u32), &chain_id, &{ - let private_key = configuration + let genesis = GenesisNetwork::new( + generate_genesis(1_000_000_u32), + &chain_id, + configuration .genesis - .private_key - .as_ref() - .expect("Should be from get_config"); - KeyPair::new( - configuration.genesis.public_key.clone(), - private_key.clone(), - ) - .expect("Should be a valid key pair") - }) + .key_pair() + .expect("should be available in the config; probably a bug"), + ) .expect("genesis creation failed"); let builder = PeerBuilder::new() .with_into_genesis(genesis) - .with_configuration(configuration); + .with_config(configuration); // This only submits the genesis. It doesn't check if the accounts // are created, because that check is 1) not needed for what the diff --git a/client/examples/tutorial.rs b/client/examples/tutorial.rs index b83a2665ae3..bec8227f6a5 100644 --- a/client/examples/tutorial.rs +++ b/client/examples/tutorial.rs @@ -1,50 +1,34 @@ //! This file contains examples from the Rust tutorial. //! -use std::fs::File; use eyre::{Error, WrapErr}; -use iroha_client::config::Configuration; +use iroha_client::config::Config; // #region rust_config_crates // #endregion rust_config_crates fn main() { // #region rust_config_load - let config_loc = "../configs/client/config.json"; - let file = File::open(config_loc) - .wrap_err("Unable to load the configuration file at `.....`") - .expect("Config file is loading normally."); - let config: Configuration = serde_json::from_reader(file) - .wrap_err("Failed to parse `../configs/client/config.json`") - .expect("Verified in tests"); + let config = Config::load("../configs/swarm/client.toml").unwrap(); // #endregion rust_config_load // Your code goes here… - json_config_client_test(&config) - .expect("JSON config client example is expected to work correctly"); - domain_registration_test(&config) + domain_registration_test(config.clone()) .expect("Domain registration example is expected to work correctly"); account_definition_test().expect("Account definition example is expected to work correctly"); - account_registration_test(&config) + account_registration_test(config.clone()) .expect("Account registration example is expected to work correctly"); - asset_registration_test(&config) + asset_registration_test(config.clone()) .expect("Asset registration example is expected to work correctly"); - asset_minting_test(&config).expect("Asset minting example is expected to work correctly"); - asset_burning_test(&config).expect("Asset burning example is expected to work correctly"); + asset_minting_test(config.clone()) + .expect("Asset minting example is expected to work correctly"); + asset_burning_test(config.clone()) + .expect("Asset burning example is expected to work correctly"); // output_visualising_test(&config).expect(msg: "Visualising outputs example is expected to work correctly"); println!("Success!"); } -fn json_config_client_test(config: &Configuration) -> Result<(), Error> { - use iroha_client::client::Client; - - // Initialise a client with a provided config - let _current_client: Client = Client::new(config)?; - - Ok(()) -} - -fn domain_registration_test(config: &Configuration) -> Result<(), Error> { +fn domain_registration_test(config: Config) -> Result<(), Error> { // #region domain_register_example_crates use iroha_client::{ client::Client, @@ -67,7 +51,7 @@ fn domain_registration_test(config: &Configuration) -> Result<(), Error> { // #region rust_client_create // Create an Iroha client - let iroha_client: Client = Client::new(config)?; + let iroha_client = Client::new(config); // #endregion rust_client_create // #region domain_register_example_prepare_tx @@ -108,7 +92,7 @@ fn account_definition_test() -> Result<(), Error> { Ok(()) } -fn account_registration_test(config: &Configuration) -> Result<(), Error> { +fn account_registration_test(config: Config) -> Result<(), Error> { // #region register_account_crates use iroha_client::{ client::Client, @@ -121,7 +105,7 @@ fn account_registration_test(config: &Configuration) -> Result<(), Error> { // #endregion register_account_crates // Create an Iroha client - let iroha_client: Client = Client::new(config)?; + let iroha_client = Client::new(config); // #region register_account_create // Create an AccountId instance by providing the account and domain name @@ -156,7 +140,7 @@ fn account_registration_test(config: &Configuration) -> Result<(), Error> { Ok(()) } -fn asset_registration_test(config: &Configuration) -> Result<(), Error> { +fn asset_registration_test(config: Config) -> Result<(), Error> { // #region register_asset_crates use std::str::FromStr as _; @@ -169,7 +153,7 @@ fn asset_registration_test(config: &Configuration) -> Result<(), Error> { // #endregion register_asset_crates // Create an Iroha client - let iroha_client: Client = Client::new(config)?; + let iroha_client = Client::new(config); // #region register_asset_create_asset // Create an asset @@ -206,7 +190,7 @@ fn asset_registration_test(config: &Configuration) -> Result<(), Error> { Ok(()) } -fn asset_minting_test(config: &Configuration) -> Result<(), Error> { +fn asset_minting_test(config: Config) -> Result<(), Error> { // #region mint_asset_crates use std::str::FromStr; @@ -217,7 +201,7 @@ fn asset_minting_test(config: &Configuration) -> Result<(), Error> { // #endregion mint_asset_crates // Create an Iroha client - let iroha_client: Client = Client::new(config)?; + let iroha_client = Client::new(config); // Define the instances of an Asset and Account // #region mint_asset_define_asset_account @@ -257,7 +241,7 @@ fn asset_minting_test(config: &Configuration) -> Result<(), Error> { Ok(()) } -fn asset_burning_test(config: &Configuration) -> Result<(), Error> { +fn asset_burning_test(config: Config) -> Result<(), Error> { // #region burn_asset_crates use std::str::FromStr; @@ -268,7 +252,7 @@ fn asset_burning_test(config: &Configuration) -> Result<(), Error> { // #endregion burn_asset_crates // Create an Iroha client - let iroha_client: Client = Client::new(config)?; + let iroha_client = Client::new(config); // #region burn_asset_define_asset_account // Define the instances of an Asset and Account diff --git a/client/src/client.rs b/client/src/client.rs index 3dcbbc44635..fadfcafef36 100644 --- a/client/src/client.rs +++ b/client/src/client.rs @@ -13,8 +13,10 @@ use derive_more::{DebugCustom, Display}; use eyre::{eyre, Result, WrapErr}; use futures_util::StreamExt; use http_default::{AsyncWebSocketStream, WebSocketStream}; +pub use iroha_config::client_api::ConfigDTO; use iroha_logger::prelude::*; use iroha_telemetry::metrics::Status; +use iroha_torii_const::uri as torii_uri; use iroha_version::prelude::*; use parity_scale_codec::DecodeAll; use rand::Rng; @@ -22,7 +24,7 @@ use url::Url; use self::{blocks_api::AsyncBlockStream, events_api::AsyncEventStream}; use crate::{ - config::{api::ConfigurationDTO, Configuration}, + config::Config, crypto::{HashOf, KeyPair}, data_model::{ block::SignedBlock, @@ -361,7 +363,7 @@ pub struct QueryRequest { impl QueryRequest { #[cfg(test)] fn dummy() -> Self { - let torii_url = crate::config::torii::DEFAULT_API_ADDR; + let torii_url = torii_uri::DEFAULT_API_ADDR; Self { torii_url: format!("http://{torii_url}").parse().unwrap(), @@ -380,9 +382,7 @@ impl QueryRequest { fn assemble(self) -> DefaultRequestBuilder { let builder = DefaultRequestBuilder::new( HttpMethod::POST, - self.torii_url - .join(crate::config::torii::QUERY) - .expect("Valid URI"), + self.torii_url.join(torii_uri::QUERY).expect("Valid URI"), ) .headers(self.headers); @@ -402,49 +402,45 @@ impl QueryRequest { /// Representation of `Iroha` client. impl Client { /// Constructor for client from configuration - /// - /// # Errors - /// If configuration isn't valid (e.g public/private keys don't match) #[inline] - pub fn new(configuration: &Configuration) -> Result { + pub fn new(configuration: Config) -> Self { Self::with_headers(configuration, HashMap::new()) } /// Constructor for client from configuration and headers /// - /// *Authorization* header will be added, if `login` and `password` fields are presented - /// - /// # Errors - /// If configuration isn't valid (e.g public/private keys don't match) + /// *Authorization* header will be added if `basic_auth` is presented #[inline] pub fn with_headers( - configuration: &Configuration, + Config { + chain_id, + account_id, + torii_api_url, + key_pair, + basic_auth, + transaction_add_nonce, + transaction_ttl, + transaction_status_timeout, + }: Config, mut headers: HashMap, - ) -> Result { - if let Some(basic_auth) = &configuration.basic_auth { + ) -> Self { + if let Some(basic_auth) = basic_auth { let credentials = format!("{}:{}", basic_auth.web_login, basic_auth.password); let engine = base64::engine::general_purpose::STANDARD; let encoded = base64::engine::Engine::encode(&engine, credentials); headers.insert(String::from("Authorization"), format!("Basic {encoded}")); } - Ok(Self { - chain_id: configuration.chain_id.clone(), - torii_url: configuration.torii_api_url.clone(), - key_pair: KeyPair::new( - configuration.public_key.clone(), - configuration.private_key.clone(), - )?, - transaction_ttl: configuration - .transaction_time_to_live_ms - .map(|ttl| Duration::from_millis(ttl.into())), - transaction_status_timeout: Duration::from_millis( - configuration.transaction_status_timeout_ms, - ), - account_id: configuration.account_id.clone(), + Self { + chain_id, + torii_url: torii_api_url, + key_pair, + transaction_ttl: Some(transaction_ttl), + transaction_status_timeout, + account_id, headers, - add_transaction_nonce: configuration.add_transaction_nonce, - }) + add_transaction_nonce: transaction_add_nonce, + } } /// Builds transaction out of supplied instructions or wasm. @@ -668,7 +664,7 @@ impl Client { B::new( HttpMethod::POST, self.torii_url - .join(crate::config::torii::TRANSACTION) + .join(torii_uri::TRANSACTION) .expect("Valid URI"), ) .headers(self.headers.clone()) @@ -936,7 +932,7 @@ impl Client { event_filter, self.headers.clone(), self.torii_url - .join(crate::config::torii::SUBSCRIPTION) + .join(torii_uri::SUBSCRIPTION) .expect("Valid URI"), ) } @@ -972,7 +968,7 @@ impl Client { height, self.headers.clone(), self.torii_url - .join(crate::config::torii::BLOCKS_STREAM) + .join(torii_uri::BLOCKS_STREAM) .expect("Valid URI"), ) } @@ -990,7 +986,7 @@ impl Client { ) -> Result> { let url = self .torii_url - .join(crate::config::torii::MATCHING_PENDING_TRANSACTIONS) + .join(torii_uri::MATCHING_PENDING_TRANSACTIONS) .expect("Valid URI"); let body = transaction.encode(); @@ -1025,11 +1021,11 @@ impl Client { /// /// # Errors /// Fails if sending request or decoding fails - pub fn get_config(&self) -> Result { + pub fn get_config(&self) -> Result { let resp = DefaultRequestBuilder::new( HttpMethod::GET, self.torii_url - .join(crate::config::torii::CONFIGURATION) + .join(torii_uri::CONFIGURATION) .expect("Valid URI"), ) .headers(&self.headers) @@ -1051,11 +1047,11 @@ impl Client { /// /// # Errors /// If sending request or decoding fails - pub fn set_config(&self, dto: ConfigurationDTO) -> Result<()> { + pub fn set_config(&self, dto: ConfigDTO) -> Result<()> { let body = serde_json::to_vec(&dto).wrap_err(format!("Failed to serialize {dto:?}"))?; let url = self .torii_url - .join(crate::config::torii::CONFIGURATION) + .join(torii_uri::CONFIGURATION) .expect("Valid URI"); let resp = DefaultRequestBuilder::new(HttpMethod::POST, url) .headers(&self.headers) @@ -1094,9 +1090,7 @@ impl Client { pub fn prepare_status_request(&self) -> B { B::new( HttpMethod::GET, - self.torii_url - .join(crate::config::torii::STATUS) - .expect("Valid URI"), + self.torii_url.join(torii_uri::STATUS).expect("Valid URI"), ) .headers(self.headers.clone()) } @@ -1595,33 +1589,34 @@ mod tests { use iroha_primitives::small::SmallStr; use super::*; - use crate::config::{torii::DEFAULT_API_ADDR, BasicAuth, ConfigurationProxy, WebLogin}; + use crate::config::{BasicAuth, Config, WebLogin}; const LOGIN: &str = "mad_hatter"; const PASSWORD: &str = "ilovetea"; // `mad_hatter:ilovetea` encoded with base64 const ENCRYPTED_CREDENTIALS: &str = "bWFkX2hhdHRlcjppbG92ZXRlYQ=="; + fn config_factory() -> Config { + Config { + chain_id: ChainId::from("0"), + key_pair: KeyPair::generate(), + account_id: "alice@wonderland" + .parse() + .expect("This account ID should be valid"), + torii_api_url: "http://127.0.0.1:8080".parse().unwrap(), + basic_auth: None, + transaction_add_nonce: false, + transaction_ttl: Duration::from_secs(5), + transaction_status_timeout: Duration::from_secs(10), + } + } + #[test] fn txs_same_except_for_nonce_have_different_hashes() { - let (public_key, private_key) = KeyPair::generate().into(); - - let cfg = ConfigurationProxy { - chain_id: Some(ChainId::new("0")), - public_key: Some(public_key), - private_key: Some(private_key), - account_id: Some( - "alice@wonderland" - .parse() - .expect("This account ID should be valid"), - ), - torii_api_url: Some(format!("http://{DEFAULT_API_ADDR}").parse().unwrap()), - add_transaction_nonce: Some(true), - ..ConfigurationProxy::default() - } - .build() - .expect("Client config should build as all required fields were provided"); - let client = Client::new(&cfg).expect("Invalid client configuration"); + let client = Client::new(Config { + transaction_add_nonce: true, + ..config_factory() + }); let build_transaction = || client.build_transaction(Vec::::new(), UnlimitedMetadata::new()); @@ -1635,7 +1630,7 @@ mod tests { .with_executable(tx1.instructions().clone()) .with_metadata(tx1.metadata().clone()); - tx.set_creation_time(tx1.creation_time().as_millis().try_into().unwrap()); + tx.set_creation_time(tx1.creation_time()); if let Some(nonce) = tx1.nonce() { tx.set_nonce(nonce); } @@ -1650,34 +1645,13 @@ mod tests { #[test] fn authorization_header() { - let basic_auth = BasicAuth { - web_login: WebLogin::from_str(LOGIN).expect("Failed to create valid `WebLogin`"), - password: SmallStr::from_str(PASSWORD), - }; - - let cfg = ConfigurationProxy { - chain_id: Some(ChainId::new("0")), - public_key: Some( - "ed01207233BFC89DCBD68C19FDE6CE6158225298EC1131B6A130D1AEB454C1AB5183C0" - .parse() - .expect("Public key not in mulithash format"), - ), - private_key: Some(crate::crypto::PrivateKey::from_hex( - crate::crypto::Algorithm::Ed25519, - "9AC47ABF59B356E0BD7DCBBBB4DEC080E302156A48CA907E47CB6AEA1D32719E7233BFC89DCBD68C19FDE6CE6158225298EC1131B6A130D1AEB454C1AB5183C0" - ).expect("Private key not hex encoded")), - account_id: Some( - "alice@wonderland" - .parse() - .expect("This account ID should be valid"), - ), - torii_api_url: Some(format!("http://{DEFAULT_API_ADDR}").parse().unwrap()), - basic_auth: Some(Some(basic_auth)), - ..ConfigurationProxy::default() - } - .build() - .expect("Client config should build as all required fields were provided"); - let client = Client::new(&cfg).expect("Invalid client configuration"); + let client = Client::new(Config { + basic_auth: Some(BasicAuth { + web_login: WebLogin::from_str(LOGIN).expect("Failed to create valid `WebLogin`"), + password: SmallStr::from_str(PASSWORD), + }), + ..config_factory() + }); let value = client .headers diff --git a/client/src/config.rs b/client/src/config.rs new file mode 100644 index 00000000000..c6010c834ec --- /dev/null +++ b/client/src/config.rs @@ -0,0 +1,122 @@ +//! Module for client-related configuration and structs + +use core::str::FromStr; +use std::{path::Path, time::Duration}; + +use derive_more::Display; +use eyre::Result; +use iroha_config::{ + base, + base::{FromEnv, StdEnv, UnwrapPartial}, +}; +use iroha_crypto::prelude::*; +use iroha_data_model::{prelude::*, ChainId}; +use iroha_primitives::small::SmallStr; +use serde::{Deserialize, Serialize}; +use serde_with::{DeserializeFromStr, SerializeDisplay}; +use url::Url; + +use crate::config::user::RootPartial; + +mod user; + +#[allow(missing_docs)] +pub const DEFAULT_TRANSACTION_TIME_TO_LIVE: Duration = Duration::from_secs(100); +#[allow(missing_docs)] +pub const DEFAULT_TRANSACTION_STATUS_TIMEOUT: Duration = Duration::from_secs(15); +#[allow(missing_docs)] +pub const DEFAULT_TRANSACTION_NONCE: bool = false; + +/// Valid web auth login string. See [`WebLogin::from_str`] +#[derive(Debug, Display, Clone, PartialEq, Eq, DeserializeFromStr, SerializeDisplay)] +pub struct WebLogin(SmallStr); + +impl FromStr for WebLogin { + type Err = eyre::ErrReport; + + /// Validates that the string is a valid web login + /// + /// # Errors + /// Fails if `login` contains `:` character, which is the binary representation of the '\0'. + fn from_str(login: &str) -> Result { + if login.contains(':') { + eyre::bail!("The `:` character, in `{login}` is not allowed"); + } + + Ok(Self(SmallStr::from_str(login))) + } +} + +/// Basic Authentication credentials +#[derive(Clone, Deserialize, Serialize, Debug, PartialEq, Eq)] +pub struct BasicAuth { + /// Login for Basic Authentication + pub web_login: WebLogin, + /// Password for Basic Authentication + pub password: SmallStr, +} + +/// Complete client configuration +#[derive(Clone, Debug, Serialize)] +#[allow(missing_docs)] +pub struct Config { + pub chain_id: ChainId, + pub account_id: AccountId, + pub key_pair: KeyPair, + pub basic_auth: Option, + // FIXME: or use `OnlyHttpUrl` here? + pub torii_api_url: Url, + pub transaction_ttl: Duration, + pub transaction_status_timeout: Duration, + pub transaction_add_nonce: bool, +} + +impl Config { + /// Loads configuration from a file + /// + /// # Errors + /// - unable to load config from a TOML file + /// - the config is invalid + pub fn load(path: impl AsRef) -> std::result::Result { + let config = RootPartial::from_toml(path)?; + let config = config.merge(RootPartial::from_env(&StdEnv)?); + Ok(config.unwrap_partial()?.parse()?) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn web_login_ok() { + let _ok = WebLogin::from_str("alice").expect("input is valid"); + } + + #[test] + fn web_login_bad() { + let _err = WebLogin::from_str("alice:wonderland").expect_err("input has `:`"); + } + + #[test] + fn parse_full_toml_config() { + let _: RootPartial = toml::toml! { + chain_id = "00000000-0000-0000-0000-000000000000" + torii_url = "http://127.0.0.1:8080/" + + [basic_auth] + web_login = "mad_hatter" + password = "ilovetea" + + [account] + id = "alice@wonderland" + public_key = "ed01207233BFC89DCBD68C19FDE6CE6158225298EC1131B6A130D1AEB454C1AB5183C0" + private_key = { digest_function = "ed25519", payload = "9ac47abf59b356e0bd7dcbbbb4dec080e302156a48ca907e47cb6aea1d32719e7233bfc89dcbd68c19fde6ce6158225298ec1131b6a130d1aeb454c1ab5183c0" } + + [transaction] + time_to_live = 100_000 + status_timeout = 100_000 + nonce = false + }.try_into().unwrap(); + } +} diff --git a/client/src/config/user.rs b/client/src/config/user.rs new file mode 100644 index 00000000000..30a684e5bac --- /dev/null +++ b/client/src/config/user.rs @@ -0,0 +1,195 @@ +//! User configuration view. + +mod boilerplate; + +use std::{fs::File, io::Read, path::Path, str::FromStr, time::Duration}; + +pub use boilerplate::*; +use eyre::{eyre, Context, Report}; +use iroha_config::base::{Emitter, ErrorsCollection}; +use iroha_crypto::{KeyPair, PrivateKey, PublicKey}; +use iroha_data_model::{account::AccountId, ChainId}; +use merge::Merge; +use serde_with::DeserializeFromStr; +use url::Url; + +use crate::config::BasicAuth; + +impl RootPartial { + /// Reads the partial layer from TOML + /// + /// # Errors + /// - File not found + /// - Not valid TOML or content + pub fn from_toml(path: impl AsRef) -> eyre::Result { + let contents = { + let mut contents = String::new(); + File::open(path.as_ref()) + .wrap_err_with(|| { + eyre!("cannot open file at location `{}`", path.as_ref().display()) + })? + .read_to_string(&mut contents)?; + contents + }; + let layer: Self = toml::from_str(&contents).wrap_err("failed to parse toml")?; + Ok(layer) + } + + /// Merge other into self + #[must_use] + pub fn merge(mut self, other: Self) -> Self { + Merge::merge(&mut self, other); + self + } +} + +/// Root of the user configuration +#[derive(Clone, Debug)] +#[allow(missing_docs)] +pub struct Root { + pub chain_id: ChainId, + pub torii_url: OnlyHttpUrl, + pub basic_auth: Option, + pub account: Account, + pub transaction: Transaction, +} + +impl Root { + /// Validates user configuration for semantic errors and constructs a complete + /// [`super::Config`]. + /// + /// # Errors + /// If a set of validity errors occurs. + pub fn parse(self) -> Result> { + let Self { + chain_id, + torii_url, + basic_auth, + account: + Account { + id: account_id, + public_key, + private_key, + }, + transaction: + Transaction { + time_to_live: tx_ttl, + status_timeout: tx_timeout, + nonce: tx_add_nonce, + }, + } = self; + + let mut emitter = Emitter::new(); + + // TODO: validate if TTL is too small? + + if tx_timeout > tx_ttl { + // TODO: + // would be nice to provide a nice report with spans in the input + // pointing out source data in provided config files + // FIXME: explain why it should be smaller + emitter.emit(eyre!( + "transaction status timeout should be smaller than its time-to-live" + )) + } + + let key_pair = KeyPair::new(public_key, private_key) + .wrap_err("failed to construct a key pair") + .map_or_else( + |err| { + emitter.emit(err); + None + }, + Some, + ); + + emitter.finish()?; + + Ok(super::Config { + chain_id, + account_id, + key_pair: key_pair.unwrap(), + torii_api_url: torii_url.0, + basic_auth, + transaction_ttl: tx_ttl, + transaction_status_timeout: tx_timeout, + transaction_add_nonce: tx_add_nonce, + }) + } +} + +#[derive(Debug, Clone)] +#[allow(missing_docs)] +pub struct Account { + pub id: AccountId, + pub public_key: PublicKey, + pub private_key: PrivateKey, +} + +#[derive(Debug, Clone, Copy)] +#[allow(missing_docs)] +pub struct Transaction { + pub time_to_live: Duration, + pub status_timeout: Duration, + pub nonce: bool, +} + +/// A [`Url`] that might only have HTTP scheme inside +#[derive(Debug, Clone, Eq, PartialEq, DeserializeFromStr)] +pub struct OnlyHttpUrl(Url); + +impl FromStr for OnlyHttpUrl { + type Err = ParseHttpUrlError; + + fn from_str(s: &str) -> Result { + let url = Url::from_str(s)?; + if url.scheme() == "http" { + Ok(Self(url)) + } else { + Err(ParseHttpUrlError::NotHttp { + found: url.scheme().to_owned(), + }) + } + } +} + +/// Possible errors that might occur for [`FromStr::from_str`] for [`OnlyHttpUrl`]. +#[derive(Debug, thiserror::Error)] +pub enum ParseHttpUrlError { + /// Unable to parse the url + #[error(transparent)] + Parse(#[from] url::ParseError), + /// Parsed fine, but doesn't contain HTTP + #[error("expected `http` scheme, found: `{found}`")] + NotHttp { + /// What scheme was actually found + found: String, + }, +} + +#[cfg(test)] +mod tests { + use std::collections::HashSet; + + use iroha_config::base::{FromEnv as _, TestEnv}; + + use super::*; + + #[test] + fn parses_all_envs() { + let env = TestEnv::new().set("TORII_URL", "http://localhost:8080"); + + let _layer = RootPartial::from_env(&env).expect("should not fail since env is valid"); + + assert_eq!(env.unvisited(), HashSet::new()) + } + + #[test] + fn non_http_url_error() { + let error = "https://localhost:1123" + .parse::() + .expect_err("should not allow https"); + + assert_eq!(format!("{error}"), "expected `http` scheme, found: `https`"); + } +} diff --git a/client/src/config/user/boilerplate.rs b/client/src/config/user/boilerplate.rs new file mode 100644 index 00000000000..500b13afecb --- /dev/null +++ b/client/src/config/user/boilerplate.rs @@ -0,0 +1,147 @@ +//! Code to be generated by a proc macro in future + +#![allow(missing_docs)] + +use std::error::Error; + +use iroha_config::base::{ + Emitter, FromEnv, HumanDuration, Merge, ParseEnvResult, UnwrapPartial, UnwrapPartialResult, + UserField, +}; +use iroha_crypto::{PrivateKey, PublicKey}; +use iroha_data_model::{account::AccountId, ChainId}; +use serde::Deserialize; + +use crate::config::{ + base::{FromEnvResult, ReadEnv}, + user::{Account, OnlyHttpUrl, Root, Transaction}, + BasicAuth, DEFAULT_TRANSACTION_NONCE, DEFAULT_TRANSACTION_STATUS_TIMEOUT, + DEFAULT_TRANSACTION_TIME_TO_LIVE, +}; + +#[derive(Debug, Clone, Deserialize, Eq, PartialEq, Default, Merge)] +#[serde(deny_unknown_fields, default)] +pub struct RootPartial { + pub chain_id: UserField, + pub torii_url: UserField, + pub basic_auth: UserField, + pub account: AccountPartial, + pub transaction: TransactionPartial, +} + +impl RootPartial { + #[allow(unused)] + pub fn new() -> Self { + // TODO: gen with macro + Self::default() + } +} + +impl FromEnv for RootPartial { + fn from_env>(env: &R) -> FromEnvResult + where + Self: Sized, + { + let mut emitter = Emitter::new(); + + let torii_url = + ParseEnvResult::parse_simple(&mut emitter, env, "TORII_URL", "torii_url").into(); + + emitter.finish()?; + + Ok(Self { + chain_id: None.into(), + torii_url, + basic_auth: None.into(), + account: AccountPartial::default(), + transaction: TransactionPartial::default(), + }) + } +} + +impl UnwrapPartial for RootPartial { + type Output = Root; + + fn unwrap_partial(self) -> UnwrapPartialResult { + let mut emitter = Emitter::new(); + + if self.chain_id.is_none() { + emitter.emit_missing_field("chain_id"); + } + if self.torii_url.is_none() { + emitter.emit_missing_field("torii_url"); + } + let account = emitter.try_unwrap_partial(self.account); + let transaction = emitter.try_unwrap_partial(self.transaction); + + emitter.finish()?; + + Ok(Root { + chain_id: self.chain_id.get().unwrap(), + torii_url: self.torii_url.get().unwrap(), + basic_auth: self.basic_auth.get(), + account: account.unwrap(), + transaction: transaction.unwrap(), + }) + } +} + +#[derive(Debug, Clone, Deserialize, Eq, PartialEq, Default, Merge)] +#[serde(deny_unknown_fields, default)] +pub struct AccountPartial { + pub id: UserField, + pub public_key: UserField, + pub private_key: UserField, +} + +impl UnwrapPartial for AccountPartial { + type Output = Account; + + fn unwrap_partial(self) -> UnwrapPartialResult { + let mut emitter = Emitter::new(); + + if self.id.is_none() { + emitter.emit_missing_field("account.id"); + } + if self.public_key.is_none() { + emitter.emit_missing_field("account.public_key"); + } + if self.private_key.is_none() { + emitter.emit_missing_field("account.private_key"); + } + + emitter.finish()?; + + Ok(Account { + id: self.id.get().unwrap(), + public_key: self.public_key.get().unwrap(), + private_key: self.private_key.get().unwrap(), + }) + } +} + +#[derive(Debug, Clone, Deserialize, Eq, PartialEq, Default, Merge)] +#[serde(deny_unknown_fields, default)] +pub struct TransactionPartial { + pub time_to_live: UserField, + pub status_timeout: UserField, + pub nonce: UserField, +} + +impl UnwrapPartial for TransactionPartial { + type Output = Transaction; + + fn unwrap_partial(self) -> UnwrapPartialResult { + Ok(Transaction { + time_to_live: self + .time_to_live + .get() + .map_or(DEFAULT_TRANSACTION_TIME_TO_LIVE, HumanDuration::get), + status_timeout: self + .status_timeout + .get() + .map_or(DEFAULT_TRANSACTION_STATUS_TIMEOUT, HumanDuration::get), + nonce: self.nonce.get().unwrap_or(DEFAULT_TRANSACTION_NONCE), + }) + } +} diff --git a/client/src/lib.rs b/client/src/lib.rs index 239a6eb5a7f..3ccb8fdb45c 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -2,6 +2,7 @@ /// Module with iroha client itself pub mod client; +pub mod config; /// Module with general communication primitives like an HTTP request builder. pub mod http; mod http_default; @@ -9,41 +10,33 @@ mod query_builder; /// Module containing sample configurations for tests and benchmarks. pub mod samples { + use url::Url; + use crate::{ - config::{torii::DEFAULT_API_ADDR, Configuration, ConfigurationProxy}, + config::{ + Config, DEFAULT_TRANSACTION_NONCE, DEFAULT_TRANSACTION_STATUS_TIMEOUT, + DEFAULT_TRANSACTION_TIME_TO_LIVE, + }, crypto::KeyPair, data_model::ChainId, }; /// Get sample client configuration. - pub fn get_client_config(chain_id: ChainId, key_pair: &KeyPair) -> Configuration { - let (public_key, private_key) = key_pair.clone().into(); - ConfigurationProxy { - chain_id: Some(chain_id), - public_key: Some(public_key), - private_key: Some(private_key), - account_id: Some( - "alice@wonderland" - .parse() - .expect("This account ID should be valid"), - ), - torii_api_url: Some( - format!("http://{DEFAULT_API_ADDR}") - .parse() - .expect("Should be a valid url"), - ), - ..ConfigurationProxy::default() + pub fn get_client_config(chain_id: ChainId, key_pair: KeyPair, torii_api_url: Url) -> Config { + Config { + chain_id, + key_pair, + torii_api_url, + account_id: "alice@wonderland" + .parse() + .expect("This account ID should be valid"), + basic_auth: None, + transaction_ttl: DEFAULT_TRANSACTION_TIME_TO_LIVE, + transaction_status_timeout: DEFAULT_TRANSACTION_STATUS_TIMEOUT, + transaction_add_nonce: DEFAULT_TRANSACTION_NONCE, } - .build() - .expect("Client config should build as all required fields were provided") } } -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 d46b3bb65af..d7de69d5041 100644 --- a/client/tests/integration/add_account.rs +++ b/client/tests/integration/add_account.rs @@ -2,7 +2,7 @@ use std::thread; use eyre::Result; use iroha_client::{client, data_model::prelude::*}; -use iroha_config::iroha::Configuration; +use iroha_config::parameters::actual::Root as Config; use test_network::*; #[test] @@ -11,7 +11,7 @@ fn client_add_account_with_name_length_more_than_limit_should_not_commit_transac let (_rt, _peer, test_client) = ::new().with_port(10_505).start_with_runtime(); wait_for_genesis_committed(&vec![test_client.clone()], 0); - let pipeline_time = Configuration::pipeline_time(); + let pipeline_time = Config::pipeline_time(); let normal_account_id: AccountId = "bob@wonderland".parse().expect("Valid"); let create_account = Register::account(Account::new(normal_account_id.clone(), [])); diff --git a/client/tests/integration/add_domain.rs b/client/tests/integration/add_domain.rs index bb889c25c15..d4cfe89c3b3 100644 --- a/client/tests/integration/add_domain.rs +++ b/client/tests/integration/add_domain.rs @@ -2,7 +2,7 @@ use std::thread; use eyre::Result; use iroha_client::{client, data_model::prelude::*}; -use iroha_config::iroha::Configuration; +use iroha_config::parameters::actual::Root as Config; use test_network::*; #[test] @@ -10,7 +10,7 @@ fn client_add_domain_with_name_length_more_than_limit_should_not_commit_transact { let (_rt, _peer, test_client) = ::new().with_port(10_500).start_with_runtime(); wait_for_genesis_committed(&vec![test_client.clone()], 0); - let pipeline_time = Configuration::pipeline_time(); + let pipeline_time = Config::pipeline_time(); // Given diff --git a/client/tests/integration/asset.rs b/client/tests/integration/asset.rs index 31d77cc26ae..7838742fd71 100644 --- a/client/tests/integration/asset.rs +++ b/client/tests/integration/asset.rs @@ -6,7 +6,7 @@ use iroha_client::{ crypto::{KeyPair, PublicKey}, data_model::prelude::*, }; -use iroha_config::iroha::Configuration; +use iroha_config::parameters::actual::Root as Config; use iroha_primitives::fixed::Fixed; use serde_json::json; use test_network::*; @@ -205,7 +205,7 @@ fn client_add_asset_with_decimal_should_increase_asset_amount() -> Result<()> { fn client_add_asset_with_name_length_more_than_limit_should_not_commit_transaction() -> Result<()> { let (_rt, _peer, test_client) = ::new().with_port(10_520).start_with_runtime(); wait_for_genesis_committed(&[test_client.clone()], 0); - let pipeline_time = Configuration::pipeline_time(); + let pipeline_time = Config::pipeline_time(); // Given let normal_asset_definition_id = AssetDefinitionId::from_str("xor#wonderland").expect("Valid"); @@ -277,7 +277,7 @@ fn find_rate_and_make_exchange_isi_should_succeed() { alice_id.clone(), ); - let chain_id = ChainId::new("0"); + let chain_id = ChainId::from("0"); let grant_asset_transfer_tx = TransactionBuilder::new(chain_id, asset_id.account_id().clone()) .with_instructions([allow_alice_to_transfer_asset]) diff --git a/client/tests/integration/asset_propagation.rs b/client/tests/integration/asset_propagation.rs index 6047dea994e..99a834017db 100644 --- a/client/tests/integration/asset_propagation.rs +++ b/client/tests/integration/asset_propagation.rs @@ -9,7 +9,7 @@ use iroha_client::{ prelude::*, }, }; -use iroha_config::iroha::Configuration; +use iroha_config::parameters::actual::Root as Config; use test_network::*; #[test] @@ -18,7 +18,7 @@ fn client_add_asset_quantity_to_existing_asset_should_increase_asset_amount_on_a // Given let (_rt, network, client) = Network::start_test_with_runtime(4, Some(10_450)); wait_for_genesis_committed(&network.clients(), 0); - let pipeline_time = Configuration::pipeline_time(); + let pipeline_time = Config::pipeline_time(); client.submit_all_blocking( ParametersBuilder::new() diff --git a/client/tests/integration/burn_public_keys.rs b/client/tests/integration/burn_public_keys.rs index 2bd40263a08..a50bb6cec0e 100644 --- a/client/tests/integration/burn_public_keys.rs +++ b/client/tests/integration/burn_public_keys.rs @@ -13,7 +13,7 @@ fn submit( HashOf, eyre::Result>, ) { - let chain_id = ChainId::new("0"); + let chain_id = ChainId::from("0"); let tx = if let Some((account_id, keypair)) = submitter { TransactionBuilder::new(chain_id, account_id) diff --git a/client/tests/integration/connected_peers.rs b/client/tests/integration/connected_peers.rs index 6cc1df7cf26..0bf17809514 100644 --- a/client/tests/integration/connected_peers.rs +++ b/client/tests/integration/connected_peers.rs @@ -8,7 +8,7 @@ use iroha_client::{ peer::Peer as DataModelPeer, }, }; -use iroha_config::iroha::Configuration; +use iroha_config::parameters::actual::Root as Config; use iroha_primitives::unique_vec; use rand::{seq::SliceRandom, thread_rng, Rng}; use test_network::*; @@ -29,7 +29,7 @@ fn connected_peers_with_f_1_0_1() -> Result<()> { fn register_new_peer() -> Result<()> { let (_rt, network, _) = Network::start_test_with_runtime(4, Some(11_180)); wait_for_genesis_committed(&network.clients(), 0); - let pipeline_time = Configuration::pipeline_time(); + let pipeline_time = Config::pipeline_time(); let mut peer_clients: Vec<_> = Network::peers(&network) .zip(Network::clients(&network)) @@ -38,13 +38,13 @@ fn register_new_peer() -> Result<()> { check_status(&peer_clients, 1); // Start new peer - let mut configuration = Configuration::test(); - configuration.sumeragi.trusted_peers.peers = + let mut configuration = Config::test(); + configuration.sumeragi.trusted_peers = unique_vec![peer_clients.choose(&mut thread_rng()).unwrap().0.id.clone()]; let rt = Runtime::test(); let new_peer = rt.block_on( PeerBuilder::new() - .with_configuration(configuration) + .with_config(configuration) .with_into_genesis(WithGenesis::None) .with_port(11_225) .start(), @@ -75,7 +75,7 @@ fn connected_peers_with_f(faults: u64, start_port: Option) -> Result<()> { start_port, ); wait_for_genesis_committed(&network.clients(), 0); - let pipeline_time = Configuration::pipeline_time(); + let pipeline_time = Config::pipeline_time(); let mut peer_clients: Vec<_> = Network::peers(&network) .zip(Network::clients(&network)) diff --git a/client/tests/integration/domain_owner.rs b/client/tests/integration/domain_owner.rs index f36ce73df85..d2901928317 100644 --- a/client/tests/integration/domain_owner.rs +++ b/client/tests/integration/domain_owner.rs @@ -8,7 +8,7 @@ use test_network::*; #[test] fn domain_owner_domain_permissions() -> Result<()> { - let chain_id = ChainId::new("0"); + let chain_id = ChainId::from("0"); let (_rt, _peer, test_client) = ::new().with_port(11_080).start_with_runtime(); wait_for_genesis_committed(&[test_client.clone()], 0); @@ -147,7 +147,7 @@ fn domain_owner_asset_definition_permissions() -> Result<()> { let (_rt, _peer, test_client) = ::new().with_port(11_085).start_with_runtime(); wait_for_genesis_committed(&[test_client.clone()], 0); - let chain_id = ChainId::new("0"); + let chain_id = ChainId::from("0"); let kingdom_id: DomainId = "kingdom".parse()?; let bob_id: AccountId = "bob@kingdom".parse()?; let rabbit_id: AccountId = "rabbit@kingdom".parse()?; @@ -212,7 +212,7 @@ fn domain_owner_asset_definition_permissions() -> Result<()> { #[test] fn domain_owner_asset_permissions() -> Result<()> { - let chain_id = ChainId::new("0"); + let chain_id = ChainId::from("0"); let (_rt, _peer, test_client) = ::new().with_port(11_090).start_with_runtime(); wait_for_genesis_committed(&[test_client.clone()], 0); diff --git a/client/tests/integration/events/pipeline.rs b/client/tests/integration/events/pipeline.rs index 89d6c91bf0b..aa546b63153 100644 --- a/client/tests/integration/events/pipeline.rs +++ b/client/tests/integration/events/pipeline.rs @@ -8,7 +8,7 @@ use iroha_client::{ prelude::*, }, }; -use iroha_config::iroha::Configuration; +use iroha_config::parameters::actual::Root as Config; use test_network::*; // Needed to re-enable ignored tests. @@ -41,7 +41,7 @@ fn test_with_instruction_and_status_and_port( Network::start_test_with_runtime(PEER_COUNT.try_into().unwrap(), Some(port)); let clients = network.clients(); wait_for_genesis_committed(&clients, 0); - let pipeline_time = Configuration::pipeline_time(); + let pipeline_time = Config::pipeline_time(); client.submit_all_blocking( ParametersBuilder::new() diff --git a/client/tests/integration/multiple_blocks_created.rs b/client/tests/integration/multiple_blocks_created.rs index ccc6c3b020e..1b56abe5590 100644 --- a/client/tests/integration/multiple_blocks_created.rs +++ b/client/tests/integration/multiple_blocks_created.rs @@ -9,7 +9,7 @@ use iroha_client::{ prelude::*, }, }; -use iroha_config::iroha::Configuration; +use iroha_config::parameters::actual::Root as Config; use test_network::*; const N_BLOCKS: usize = 510; @@ -20,7 +20,7 @@ fn long_multiple_blocks_created() -> Result<()> { // Given let (_rt, network, client) = Network::start_test_with_runtime(4, Some(10_965)); wait_for_genesis_committed(&network.clients(), 0); - let pipeline_time = Configuration::pipeline_time(); + let pipeline_time = Config::pipeline_time(); client.submit_all_blocking( ParametersBuilder::new() diff --git a/client/tests/integration/multisignature_account.rs b/client/tests/integration/multisignature_account.rs index be7aa0a224f..c057ef04ea1 100644 --- a/client/tests/integration/multisignature_account.rs +++ b/client/tests/integration/multisignature_account.rs @@ -6,14 +6,14 @@ use iroha_client::{ crypto::KeyPair, data_model::prelude::*, }; -use iroha_config::iroha::Configuration; +use iroha_config::parameters::actual::Root as Config; use test_network::*; #[test] fn transaction_signed_by_new_signatory_of_account_should_pass() -> Result<()> { let (_rt, peer, client) = ::new().with_port(10_605).start_with_runtime(); wait_for_genesis_committed(&[client.clone()], 0); - let pipeline_time = Configuration::pipeline_time(); + let pipeline_time = Config::pipeline_time(); // Given let account_id: AccountId = "alice@wonderland".parse().expect("Valid"); diff --git a/client/tests/integration/multisignature_transaction.rs b/client/tests/integration/multisignature_transaction.rs index 176e03061bb..66c218e32ff 100644 --- a/client/tests/integration/multisignature_transaction.rs +++ b/client/tests/integration/multisignature_transaction.rs @@ -3,14 +3,14 @@ use std::{str::FromStr as _, thread, time::Duration}; use eyre::Result; use iroha_client::{ client::{self, Client, QueryResult}, - config::Configuration as ClientConfiguration, + config::Config as ClientConfig, crypto::KeyPair, data_model::{ parameter::{default::MAX_TRANSACTIONS_IN_BLOCK, ParametersBuilder}, prelude::*, }, }; -use iroha_config::iroha::Configuration; +use iroha_config::parameters::actual::Root as Config; use test_network::*; #[allow(clippy::too_many_lines)] @@ -18,7 +18,7 @@ use test_network::*; fn multisignature_transactions_should_wait_for_all_signatures() -> Result<()> { let (_rt, network, client) = Network::start_test_with_runtime(4, Some(10_945)); wait_for_genesis_committed(&network.clients(), 0); - let pipeline_time = Configuration::pipeline_time(); + let pipeline_time = Config::pipeline_time(); client.submit_all_blocking( ParametersBuilder::new() @@ -39,8 +39,8 @@ fn multisignature_transactions_should_wait_for_all_signatures() -> Result<()> { alice_id.clone(), ); - let mut client_configuration = ClientConfiguration::test(&network.genesis.api_address); - let client = Client::new(&client_configuration)?; + let mut client_config = ClientConfig::test(&network.genesis.api_address); + let client = Client::new(client_config.clone()); let instructions: [InstructionBox; 2] = [create_asset.into(), set_signature_condition.into()]; client.submit_all_blocking(instructions)?; @@ -49,24 +49,22 @@ fn multisignature_transactions_should_wait_for_all_signatures() -> Result<()> { let asset_id = AssetId::new(asset_definition_id, alice_id.clone()); let mint_asset = Mint::asset_quantity(quantity, asset_id.clone()); - let (public_key1, private_key1) = alice_key_pair.into(); - client_configuration.account_id = alice_id.clone(); - client_configuration.public_key = public_key1; - client_configuration.private_key = private_key1; - let client = Client::new(&client_configuration)?; + client_config.account_id = alice_id.clone(); + client_config.key_pair = alice_key_pair; + let client = Client::new(client_config.clone()); let instructions = [mint_asset.clone()]; let transaction = client.build_transaction(instructions, UnlimitedMetadata::new()); client.submit_transaction(&client.sign_transaction(transaction))?; thread::sleep(pipeline_time); //Then - client_configuration.torii_api_url = format!( + client_config.torii_api_url = format!( "http://{}", &network.peers.values().last().unwrap().api_address, ) .parse() .unwrap(); - let client_1 = Client::new(&client_configuration).expect("Invalid client configuration"); + let client_1 = Client::new(client_config.clone()); let request = client::asset::by_account_id(alice_id); let assets = client_1 .request(request.clone())? @@ -76,10 +74,9 @@ fn multisignature_transactions_should_wait_for_all_signatures() -> Result<()> { 2, // Alice has roses and cabbage from Genesis, but doesn't yet have camomile "Multisignature transaction was committed before all required signatures were added" ); - let (public_key2, private_key2) = key_pair_2.into(); - client_configuration.public_key = public_key2; - client_configuration.private_key = private_key2; - let client_2 = Client::new(&client_configuration)?; + + client_config.key_pair = key_pair_2; + let client_2 = Client::new(client_config); let instructions = [mint_asset]; let transaction = client_2.build_transaction(instructions, UnlimitedMetadata::new()); let transaction = client_2 diff --git a/client/tests/integration/offline_peers.rs b/client/tests/integration/offline_peers.rs index 7627c6ddfbd..d7ce1b1fc57 100644 --- a/client/tests/integration/offline_peers.rs +++ b/client/tests/integration/offline_peers.rs @@ -6,7 +6,7 @@ use iroha_client::{ prelude::*, }, }; -use iroha_config::iroha::Configuration; +use iroha_config::parameters::actual::Root as Config; use iroha_crypto::KeyPair; use iroha_primitives::addr::socket_addr; use test_network::*; @@ -47,7 +47,7 @@ fn register_offline_peer() -> Result<()> { let (_rt, network, client) = Network::start_test_with_runtime(n_peers, Some(11_160)); wait_for_genesis_committed(&network.clients(), 0); - let pipeline_time = Configuration::pipeline_time(); + let pipeline_time = Config::pipeline_time(); let peer_clients = Network::clients(&network); check_status(&peer_clients, 1); diff --git a/client/tests/integration/permissions.rs b/client/tests/integration/permissions.rs index 5a9bf6881ee..0d95e964396 100644 --- a/client/tests/integration/permissions.rs +++ b/client/tests/integration/permissions.rs @@ -63,7 +63,7 @@ fn get_assets(iroha_client: &Client, id: &AccountId) -> Vec { #[test] #[ignore = "ignore, more in #2851"] fn permissions_disallow_asset_transfer() { - let chain_id = ChainId::new("0"); + let chain_id = ChainId::from("0"); let (_rt, _peer, iroha_client) = ::new().with_port(10_730).start_with_runtime(); wait_for_genesis_committed(&[iroha_client.clone()], 0); @@ -120,7 +120,7 @@ fn permissions_disallow_asset_transfer() { #[test] #[ignore = "ignore, more in #2851"] fn permissions_disallow_asset_burn() { - let chain_id = ChainId::new("0"); + let chain_id = ChainId::from("0"); let (_rt, _peer, iroha_client) = ::new().with_port(10_735).start_with_runtime(); @@ -195,7 +195,7 @@ fn account_can_query_only_its_own_domain() -> Result<()> { #[test] fn permissions_differ_not_only_by_names() { - let chain_id = ChainId::new("0"); + let chain_id = ChainId::from("0"); let (_rt, _not_drop, client) = ::new().with_port(10_745).start_with_runtime(); @@ -292,7 +292,7 @@ fn permissions_differ_not_only_by_names() { #[test] #[allow(deprecated)] fn stored_vs_granted_token_payload() -> Result<()> { - let chain_id = ChainId::new("0"); + let chain_id = ChainId::from("0"); let (_rt, _peer, iroha_client) = ::new().with_port(10_730).start_with_runtime(); wait_for_genesis_committed(&[iroha_client.clone()], 0); diff --git a/client/tests/integration/restart_peer.rs b/client/tests/integration/restart_peer.rs index 6c62a7bb393..00433722636 100644 --- a/client/tests/integration/restart_peer.rs +++ b/client/tests/integration/restart_peer.rs @@ -5,7 +5,7 @@ use iroha_client::{ client::{self, Client, QueryResult}, data_model::prelude::*, }; -use iroha_config::iroha::Configuration; +use iroha_config::parameters::actual::Root as Config; use rand::{seq::SliceRandom, thread_rng, Rng}; use test_network::*; use tokio::runtime::Runtime; @@ -21,7 +21,7 @@ fn restarted_peer_should_have_the_same_asset_amount() -> Result<()> { let (_rt, network, _) = Network::start_test_with_runtime(n_peers, Some(11_205)); wait_for_genesis_committed(&network.clients(), 0); - let pipeline_time = Configuration::pipeline_time(); + let pipeline_time = Config::pipeline_time(); let peer_clients = Network::clients(&network); let create_asset = diff --git a/client/tests/integration/roles.rs b/client/tests/integration/roles.rs index f5a3f266f95..f7cc75fdaa4 100644 --- a/client/tests/integration/roles.rs +++ b/client/tests/integration/roles.rs @@ -46,7 +46,7 @@ fn register_role_with_empty_token_params() -> Result<()> { /// @s8sato added: This test represents #2081 case. #[test] fn register_and_grant_role_for_metadata_access() -> Result<()> { - let chain_id = ChainId::new("0"); + let chain_id = ChainId::from("0"); let (_rt, _peer, test_client) = ::new().with_port(10_700).start_with_runtime(); wait_for_genesis_committed(&vec![test_client.clone()], 0); diff --git a/client/tests/integration/triggers/time_trigger.rs b/client/tests/integration/triggers/time_trigger.rs index 9b9c76d3fe6..319467fc24a 100644 --- a/client/tests/integration/triggers/time_trigger.rs +++ b/client/tests/integration/triggers/time_trigger.rs @@ -5,7 +5,7 @@ use iroha_client::{ client::{self, Client, QueryResult}, data_model::{prelude::*, transaction::WasmSmartContract}, }; -use iroha_config::sumeragi::default::DEFAULT_CONSENSUS_ESTIMATION_MS; +use iroha_config::parameters::defaults::chain_wide::DEFAULT_CONSENSUS_ESTIMATION; use iroha_logger::info; use test_network::*; @@ -24,9 +24,9 @@ macro_rules! const_assert { #[test] #[allow(clippy::cast_precision_loss)] fn time_trigger_execution_count_error_should_be_less_than_15_percent() -> Result<()> { - const PERIOD_MS: u64 = 100; + const PERIOD: Duration = Duration::from_millis(100); const ACCEPTABLE_ERROR_PERCENT: u8 = 15; - const_assert!(PERIOD_MS < DEFAULT_CONSENSUS_ESTIMATION_MS); + const_assert!(PERIOD.as_millis() < DEFAULT_CONSENSUS_ESTIMATION.as_millis()); const_assert!(ACCEPTABLE_ERROR_PERCENT <= 100); let (_rt, _peer, mut test_client) = ::new().with_port(10_775).start_with_runtime(); @@ -42,8 +42,7 @@ fn time_trigger_execution_count_error_should_be_less_than_15_percent() -> Result let prev_value = get_asset_value(&mut test_client, asset_id.clone())?; - let schedule = - TimeSchedule::starting_at(start_time).with_period(Duration::from_millis(PERIOD_MS)); + let schedule = TimeSchedule::starting_at(start_time).with_period(PERIOD); let instruction = Mint::asset_quantity(1_u32, asset_id.clone()); let register_trigger = Register::trigger(Trigger::new( "mint_rose".parse()?, @@ -63,10 +62,10 @@ fn time_trigger_execution_count_error_should_be_less_than_15_percent() -> Result Duration::from_secs(1), 3, )?; - std::thread::sleep(Duration::from_millis(DEFAULT_CONSENSUS_ESTIMATION_MS)); + std::thread::sleep(DEFAULT_CONSENSUS_ESTIMATION); let finish_time = current_time(); - let average_count = finish_time.saturating_sub(start_time).as_millis() / u128::from(PERIOD_MS); + let average_count = finish_time.saturating_sub(start_time).as_millis() / PERIOD.as_millis(); let actual_value = get_asset_value(&mut test_client, asset_id)?; let expected_value = prev_value + u32::try_from(average_count)?; @@ -83,7 +82,7 @@ fn time_trigger_execution_count_error_should_be_less_than_15_percent() -> Result #[test] fn change_asset_metadata_after_1_sec() -> Result<()> { - const PERIOD_MS: u64 = 1000; + const PERIOD: Duration = Duration::from_secs(1); let (_rt, _peer, mut test_client) = ::new().with_port(10_660).start_with_runtime(); wait_for_genesis_committed(&vec![test_client.clone()], 0); @@ -96,7 +95,7 @@ fn change_asset_metadata_after_1_sec() -> Result<()> { let account_id = AccountId::from_str("alice@wonderland").expect("Valid"); let key = Name::from_str("petal")?; - let schedule = TimeSchedule::starting_at(start_time + Duration::from_millis(PERIOD_MS)); + let schedule = TimeSchedule::starting_at(start_time + PERIOD); let instruction = SetKeyValue::asset_definition(asset_definition_id.clone(), key.clone(), 3_u32.to_value()); let register_trigger = Register::trigger(Trigger::new( @@ -114,7 +113,7 @@ fn change_asset_metadata_after_1_sec() -> Result<()> { &mut test_client, &account_id, Duration::from_secs(1), - usize::try_from(PERIOD_MS / DEFAULT_CONSENSUS_ESTIMATION_MS + 1)?, + usize::try_from(PERIOD.as_millis() / DEFAULT_CONSENSUS_ESTIMATION.as_millis() + 1)?, )?; let value = test_client diff --git a/client/tests/integration/tx_history.rs b/client/tests/integration/tx_history.rs index 1a5fad4192d..c148b3410af 100644 --- a/client/tests/integration/tx_history.rs +++ b/client/tests/integration/tx_history.rs @@ -9,7 +9,7 @@ use iroha_client::{ client::{transaction, QueryResult}, data_model::{prelude::*, query::Pagination}, }; -use iroha_config::iroha::Configuration; +use iroha_config::parameters::actual::Root as Config; use test_network::*; #[ignore = "ignore, more in #2851"] @@ -18,7 +18,7 @@ fn client_has_rejected_and_acepted_txs_should_return_tx_history() -> Result<()> let (_rt, _peer, client) = ::new().with_port(10_715).start_with_runtime(); wait_for_genesis_committed(&vec![client.clone()], 0); - let pipeline_time = Configuration::pipeline_time(); + let pipeline_time = Config::pipeline_time(); // Given let account_id = AccountId::from_str("alice@wonderland")?; diff --git a/client/tests/integration/unregister_peer.rs b/client/tests/integration/unregister_peer.rs index fb45c9db8c4..e73112ae920 100644 --- a/client/tests/integration/unregister_peer.rs +++ b/client/tests/integration/unregister_peer.rs @@ -9,7 +9,7 @@ use iroha_client::{ prelude::*, }, }; -use iroha_config::iroha::Configuration; +use iroha_config::parameters::actual::Root as Config; use test_network::*; // Note the test is marked as `unstable`, not the network. @@ -60,7 +60,7 @@ fn check_assets( iroha_client .poll_request_with_period( client::asset::by_account_id(account_id.clone()), - Configuration::block_sync_gossip_time(), + Config::block_sync_gossip_time(), 15, |result| { let assets = result.collect::>>().expect("Valid"); @@ -100,7 +100,7 @@ fn init() -> Result<( AssetDefinitionId, )> { let (rt, network, client) = Network::start_test_with_runtime(4, Some(10_925)); - let pipeline_time = Configuration::pipeline_time(); + let pipeline_time = Config::pipeline_time(); iroha_logger::info!("Started"); let parameters = ParametersBuilder::new() .add_parameter(MAX_TRANSACTIONS_IN_BLOCK, 1u32)? diff --git a/client/tests/integration/unstable_network.rs b/client/tests/integration/unstable_network.rs index 84d1b2d9762..bfccf0bbad7 100644 --- a/client/tests/integration/unstable_network.rs +++ b/client/tests/integration/unstable_network.rs @@ -5,7 +5,7 @@ use iroha_client::{ client::{self, Client, QueryResult}, data_model::{prelude::*, Level}, }; -use iroha_config::iroha::Configuration; +use iroha_config::parameters::actual::Root as Config; use rand::seq::SliceRandom; use test_network::*; use tokio::runtime::Runtime; @@ -52,8 +52,9 @@ fn unstable_network( let rt = Runtime::test(); // Given let (network, iroha_client) = rt.block_on(async { - let mut configuration = Configuration::test(); - configuration.sumeragi.max_transactions_in_block = MAX_TRANSACTIONS_IN_BLOCK; + let mut configuration = Config::test(); + configuration.chain_wide.max_transactions_in_block = + MAX_TRANSACTIONS_IN_BLOCK.try_into().unwrap(); configuration.logger.level = Level::INFO; #[cfg(debug_assertions)] { @@ -72,7 +73,7 @@ fn unstable_network( }); wait_for_genesis_committed(&network.clients(), n_offline_peers); - let pipeline_time = Configuration::pipeline_time(); + let pipeline_time = Config::pipeline_time(); let account_id: AccountId = "alice@wonderland".parse().expect("Valid"); let asset_definition_id: AssetDefinitionId = "camomile#wonderland".parse().expect("Valid"); @@ -112,7 +113,7 @@ fn unstable_network( iroha_client .poll_request_with_period( client::asset::by_account_id(account_id.clone()), - Configuration::pipeline_time(), + Config::pipeline_time(), 4, |result| { let assets = result.collect::>>().expect("Valid"); diff --git a/client/tests/integration/upgrade.rs b/client/tests/integration/upgrade.rs index ec53fdbbe8b..b7d0f9a1c04 100644 --- a/client/tests/integration/upgrade.rs +++ b/client/tests/integration/upgrade.rs @@ -12,7 +12,7 @@ use test_network::*; #[test] fn executor_upgrade_should_work() -> Result<()> { - let chain_id = ChainId::new("0"); + let chain_id = ChainId::from("0"); let (_rt, _peer, client) = ::new().with_port(10_795).start_with_runtime(); wait_for_genesis_committed(&vec![client.clone()], 0); diff --git a/client_cli/pytests/README.md b/client_cli/pytests/README.md index ad585372c95..f565dc71e26 100644 --- a/client_cli/pytests/README.md +++ b/client_cli/pytests/README.md @@ -54,6 +54,7 @@ The test model has the following structure: ```shell # Must be executed from the repo root: ./scripts/test_env.py setup + # Note: make sure you have installed packages from `./scripts/requirements.txt` ``` By default, this builds `iroha`, `iroha_client_cli`, and `kagami` binaries, and runs four peers with their API exposed through the `8080`-`8083` ports.\ @@ -64,7 +65,8 @@ The test model has the following structure: 3. Configure the tests by creating the following `.env` file in _this_ (`/client_cli/pytests/`) directory: ```shell - CLIENT_CLI_DIR=/path/to/iroha_client_cli/with/config.json/dir/ + CLIENT_CLI_BINARY=/path/to/iroha_client_cli + CLIENT_CLI_CONFIG=/path/to/config.toml TORII_API_PORT_MIN=8080 TORII_API_PORT_MAX=8083 ``` @@ -161,7 +163,8 @@ The variables: **Example**: ```shell -CLIENT_CLI_DIR=/path/to/iroha_client_cli/with/config.json/dir/ +CLIENT_CLI_BINARY=/path/to/iroha_client_cli +CLIENT_CLI_CONFIG=/path/to/config.toml TORII_API_PORT_MIN=8080 TORII_API_PORT_MAX=8083 ``` diff --git a/client_cli/pytests/common/settings.py b/client_cli/pytests/common/settings.py index c79aa7765f2..d5de68f753f 100644 --- a/client_cli/pytests/common/settings.py +++ b/client_cli/pytests/common/settings.py @@ -13,10 +13,9 @@ (os.path.dirname (os.path.abspath(__file__)))) -ROOT_DIR = os.environ.get("CLIENT_CLI_DIR", BASE_DIR) -PATH_CONFIG_CLIENT_CLI = os.path.join(ROOT_DIR, "config.json") -CLIENT_CLI_PATH = os.path.join(ROOT_DIR, "iroha_client_cli") +PATH_CONFIG_CLIENT_CLI = os.environ["CLIENT_CLI_CONFIG"] +CLIENT_CLI_PATH = os.environ["CLIENT_CLI_BINARY"] PORT_MIN = int(os.getenv('TORII_API_PORT_MIN', '8080')) PORT_MAX = int(os.getenv('TORII_API_PORT_MAX', '8083')) diff --git a/client_cli/pytests/poetry.lock b/client_cli/pytests/poetry.lock index 0da88839fa3..53341202632 100644 --- a/client_cli/pytests/poetry.lock +++ b/client_cli/pytests/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. [[package]] name = "allure-pytest" @@ -530,13 +530,13 @@ files = [ [[package]] name = "tomlkit" -version = "0.11.8" +version = "0.12.3" description = "Style preserving TOML library" optional = false python-versions = ">=3.7" files = [ - {file = "tomlkit-0.11.8-py3-none-any.whl", hash = "sha256:8c726c4c202bdb148667835f68d68780b9a003a9ec34167b6c673b38eff2a171"}, - {file = "tomlkit-0.11.8.tar.gz", hash = "sha256:9330fc7faa1db67b541b28e62018c17d20be733177d290a13b24c62d1614e0c3"}, + {file = "tomlkit-0.12.3-py3-none-any.whl", hash = "sha256:b0a645a9156dc7cb5d3a1f0d4bab66db287fcb8e0430bdd4664a095ea16414ba"}, + {file = "tomlkit-0.12.3.tar.gz", hash = "sha256:75baf5012d06501f07bee5bf8e801b9f343e7aac5a92581f20f80ce632e6b5a4"}, ] [[package]] @@ -637,4 +637,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "8c3a17410644637cb551ef878cacf0d76e83eded4d32af50e1312f934e24639b" +content-hash = "101321a5a8443974ff254d60b75c4a59c0da6a8b2e2387a8a3f666692a58b834" diff --git a/client_cli/pytests/pyproject.toml b/client_cli/pytests/pyproject.toml index 0fdeaaead5f..707d8df5d4b 100644 --- a/client_cli/pytests/pyproject.toml +++ b/client_cli/pytests/pyproject.toml @@ -11,6 +11,7 @@ faker = "*" allure-python-commons = "*" cryptography = "*" python-dotenv = "*" +tomlkit = "^0.12.3" [tool.poetry.dev-dependencies] pytest = "*" diff --git a/client_cli/pytests/src/client_cli/client_cli.py b/client_cli/pytests/src/client_cli/client_cli.py index cdd33b58c59..dfdc8629ef8 100644 --- a/client_cli/pytests/src/client_cli/client_cli.py +++ b/client_cli/pytests/src/client_cli/client_cli.py @@ -254,19 +254,21 @@ def execute(self, command=None): :return: The current ClientCli object. :rtype: ClientCli """ + self.config.randomise_torii_url() if command is None: command = self.command else: command = [self.BASE_PATH] + self.BASE_FLAGS + command.split() allure_command = ' '.join(map(str, command[3:])) print(allure_command) - with allure.step(f'{allure_command} on the {str(self.config.torii_api_port)} peer'): + with allure.step(f'{allure_command} on the {str(self.config.torii_url)} peer'): try: with subprocess.Popen( command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, - text=True + text=True, + env=self.config.env ) as process: self.stdout, self.stderr = process.communicate() allure.attach( diff --git a/client_cli/pytests/src/client_cli/configuration.py b/client_cli/pytests/src/client_cli/configuration.py index 2f69e4edc57..a80b0e50202 100644 --- a/client_cli/pytests/src/client_cli/configuration.py +++ b/client_cli/pytests/src/client_cli/configuration.py @@ -2,7 +2,7 @@ This module provides a Config class to manage Iroha network configuration. """ -import json +import tomlkit import os import random from urllib.parse import urlparse @@ -11,8 +11,8 @@ class Config: """ Configuration class to handle Iroha network configuration. The class provides methods for loading - the configuration from a file, updating the TORII_API_URL with a random port number from the specified - range, and accessing the configuration values. + the configuration from a file, accessing the configuration values, and randomising Torii URL + to access different peers. :param port_min: The minimum port number for the TORII_API_URL. :type port_min: int @@ -24,6 +24,7 @@ def __init__(self, port_min, port_max): self.file = None self.port_min = port_min self.port_max = port_max + self._envs = dict() def load(self, path_config_client_cli): """ @@ -36,34 +37,40 @@ def load(self, path_config_client_cli): if not os.path.exists(path_config_client_cli): raise IOError(f"No config file found at {path_config_client_cli}") with open(path_config_client_cli, 'r', encoding='utf-8') as config_file: - self._config = json.load(config_file) + self._config = tomlkit.load(config_file) self.file = path_config_client_cli - def update_torii_api_port(self): + def randomise_torii_url(self): """ - Update the TORII_API_URL configuration value - with a random port number from the specified range. + Update Torii URL. + Note that in order for update to take effect, + `self.env` should be used when executing the client cli. :return: None """ - if self._config is None: - raise ValueError("No configuration loaded. Use load_config(path_config_client_cli) to load the configuration.") - parsed_url = urlparse(self._config['TORII_API_URL']) - new_netloc = parsed_url.hostname + ':' + str(random.randint(self.port_min, self.port_max)) - self._config['TORII_API_URL'] = parsed_url._replace(netloc=new_netloc).geturl() - with open(self.file, 'w', encoding='utf-8') as config_file: - json.dump(self._config, config_file) + parsed_url = urlparse(self._config["torii_url"]) + random_port = random.randint(self.port_min, self.port_max) + self._envs["TORII_URL"] = parsed_url._replace(netloc=f"{parsed_url.hostname}:{random_port}").geturl() @property - def torii_api_port(self): + def torii_url(self): """ - Get the TORII_API_URL configuration value after updating the port number. + Get the Torii URL set in ENV vars. - :return: The updated TORII_API_URL. + :return: Torii URL :rtype: str """ - self.update_torii_api_port() - return self._config['TORII_API_URL'] + return self._envs["TORII_URL"] + + @property + def env(self): + """ + Get the environment variables set to execute the client cli with. + + :return: Dictionary with env vars (mixed with existing OS vars) + :rtype: dict + """ + return {**os.environ, **self._envs} @property def account_id(self): @@ -73,7 +80,7 @@ def account_id(self): :return: The ACCOUNT_ID. :rtype: str """ - return self._config['ACCOUNT_ID'] + return self._config['account']["id"] @property def account_name(self): @@ -103,4 +110,4 @@ def public_key(self): :return: The public key. :rtype: str """ - return self._config['PUBLIC_KEY'].split('ed0120')[1] + return self._config["account"]['public_key'].split('ed0120')[1] diff --git a/client_cli/pytests/src/client_cli/iroha.py b/client_cli/pytests/src/client_cli/iroha.py index 38174fefeac..e62e694dc6f 100644 --- a/client_cli/pytests/src/client_cli/iroha.py +++ b/client_cli/pytests/src/client_cli/iroha.py @@ -51,7 +51,12 @@ def domains(self) -> Dict[str, Dict]: :rtype: List[str] """ self._execute_command('domain') - domains = json.loads(self.stdout) + try: + domains = json.loads(self.stdout) + except json.decoder.JSONDecodeError as e: + print(f"JSON decode error occurred with this input:", self.stdout) + print(f"STDERR:", self.stderr) + raise domains_dict = { domain["id"]: domain for domain in domains } return domains_dict diff --git a/client_cli/src/main.rs b/client_cli/src/main.rs index 80a45b35066..25e8eaf93c0 100644 --- a/client_cli/src/main.rs +++ b/client_cli/src/main.rs @@ -16,10 +16,9 @@ use dialoguer::Confirm; use erased_serde::Serialize; use iroha_client::{ client::{Client, QueryResult}, - config::{path::Path, Configuration as ClientConfiguration, ConfigurationProxy}, + config::Config, data_model::prelude::*, }; -use iroha_config_base::proxy::{LoadFromDisk, LoadFromEnv, Override}; use iroha_primitives::addr::{Ipv4Addr, Ipv6Addr, SocketAddr}; /// Re-usable clap `--metadata ` (`-m`) argument. @@ -90,21 +89,9 @@ impl FromStr for ValueArg { #[derive(clap::Parser, Debug)] #[command(name = "iroha_client_cli", version = concat!("version=", env!("CARGO_PKG_VERSION"), " git_commit_sha=", env!("VERGEN_GIT_SHA")), author)] struct Args { - /// Path to the configuration file, defaults to `config.json`/`config.json5` - /// - /// Supported extensions are `.json` and `.json5`. By default, Iroha Client looks for a - /// `config` file with one of the supported extensions in the current working directory. - /// If the default config file is not found, Iroha will rely on default values and environment - /// variables. However, if the config path is set explicitly with this argument and the file - /// is not found, Iroha Client will exit with an error. - #[arg( - short, - long, - value_name("PATH"), - value_hint(clap::ValueHint::FilePath), - value_parser(Path::user_provided_str) - )] - config: Option, + /// Path to the configuration file + #[arg(short, long, value_name("PATH"), value_hint(clap::ValueHint::FilePath))] + config: PathBuf, /// More verbose output #[arg(short, long)] verbose: bool, @@ -146,7 +133,11 @@ enum Subcommand { /// Context inside which command is executed trait RunContext { /// Get access to configuration - fn configuration(&self) -> &ClientConfiguration; + fn configuration(&self) -> &Config; + + fn client_from_config(&self) -> Client { + Client::new(self.configuration().clone()) + } /// Skip check for MST fn skip_mst_check(&self) -> bool; @@ -161,12 +152,12 @@ trait RunContext { struct PrintJsonContext { write: W, - config: ClientConfiguration, + config: Config, skip_mst_check: bool, } impl RunContext for PrintJsonContext { - fn configuration(&self) -> &ClientConfiguration { + fn configuration(&self) -> &Config { &self.config } @@ -204,14 +195,13 @@ impl RunArgs for Subcommand { } } -// TODO: move into config. +// TODO: move into config? const RETRY_COUNT_MST: u32 = 1; const RETRY_IN_MST: Duration = Duration::from_millis(100); -static DEFAULT_CONFIG_PATH: &str = "config"; - fn main() -> Result<()> { color_eyre::install()?; + let Args { config: config_path, subcommand, @@ -219,22 +209,7 @@ fn main() -> Result<()> { skip_mst_check, } = clap::Parser::parse(); - let config = ConfigurationProxy::default(); - let config = if let Some(path) = config_path - .unwrap_or_else(|| Path::default(DEFAULT_CONFIG_PATH)) - .try_resolve() - .wrap_err("Failed to resolve config file")? - { - config.override_with(ConfigurationProxy::from_path(&*path)) - } else { - config - }; - let config = config.override_with( - ConfigurationProxy::from_std_env().wrap_err("Failed to read config from ENV")?, - ); - let config = config - .build() - .wrap_err("Failed to finalize configuration")?; + let config = Config::load(config_path)?; if verbose { eprintln!( @@ -263,7 +238,7 @@ fn submit( metadata: UnlimitedMetadata, context: &mut dyn RunContext, ) -> Result<()> { - let iroha_client = Client::new(context.configuration())?; + let iroha_client = context.client_from_config(); let instructions = instructions.into(); let tx = iroha_client.build_transaction(instructions, metadata); let transactions = if context.skip_mst_check() { @@ -322,7 +297,6 @@ mod filter { } mod events { - use iroha_client::client::Client; use super::*; @@ -349,7 +323,7 @@ mod events { } fn listen(filter: FilterBox, context: &mut dyn RunContext) -> Result<()> { - let iroha_client = Client::new(context.configuration())?; + let iroha_client = context.client_from_config(); eprintln!("Listening to events with filter: {filter:?}"); iroha_client .listen_for_events(filter) @@ -362,8 +336,6 @@ mod events { mod blocks { use std::num::NonZeroU64; - use iroha_client::client::Client; - use super::*; /// Get block stream from iroha peer @@ -381,7 +353,7 @@ mod blocks { } fn listen(height: NonZeroU64, context: &mut dyn RunContext) -> Result<()> { - let iroha_client = Client::new(context.configuration())?; + let iroha_client = context.client_from_config(); eprintln!("Listening to blocks from height: {height}"); iroha_client .listen_for_blocks(height) @@ -446,7 +418,7 @@ mod domain { impl RunArgs for List { fn run(self, context: &mut dyn RunContext) -> Result<()> { - let client = Client::new(context.configuration())?; + let client = context.client_from_config(); let vec = match self { Self::All => client @@ -682,7 +654,7 @@ mod account { impl RunArgs for List { fn run(self, context: &mut dyn RunContext) -> Result<()> { - let client = Client::new(context.configuration())?; + let client = context.client_from_config(); let vec = match self { Self::All => client @@ -752,7 +724,7 @@ mod account { impl RunArgs for ListPermissions { fn run(self, context: &mut dyn RunContext) -> Result<()> { - let client = Client::new(context.configuration())?; + let client = context.client_from_config(); let find_all_permissions = FindPermissionTokensByAccountId::new(self.id); let permissions = client .request(find_all_permissions) @@ -765,7 +737,7 @@ mod account { mod asset { use iroha_client::{ - client::{self, asset, Client}, + client::{self, asset}, data_model::{asset::AssetDefinition, name::Name}, }; @@ -939,7 +911,7 @@ mod asset { impl RunArgs for Get { fn run(self, context: &mut dyn RunContext) -> Result<()> { let Self { asset_id } = self; - let iroha_client = Client::new(context.configuration())?; + let iroha_client = context.client_from_config(); let asset = iroha_client .request(asset::by_id(asset_id)) .wrap_err("Failed to get asset.")?; @@ -959,7 +931,7 @@ mod asset { impl RunArgs for List { fn run(self, context: &mut dyn RunContext) -> Result<()> { - let client = Client::new(context.configuration())?; + let client = context.client_from_config(); let vec = match self { Self::All => client @@ -1033,7 +1005,7 @@ mod asset { impl RunArgs for GetKeyValue { fn run(self, context: &mut dyn RunContext) -> Result<()> { let Self { asset_id, key } = self; - let client = Client::new(context.configuration())?; + let client = context.client_from_config(); let find_key_value = FindAssetKeyValueByIdAndKey::new(asset_id, key); let asset = client .request(find_key_value) diff --git a/config/Cargo.toml b/config/Cargo.toml index d6df71128fa..a976320e776 100644 --- a/config/Cargo.toml +++ b/config/Cargo.toml @@ -23,6 +23,7 @@ tracing-subscriber = { workspace = true, features = ["fmt", "ansi"] } url = { workspace = true, features = ["serde"] } serde = { workspace = true, features = ["derive"] } +serde_with = { workspace = true } strum = { workspace = true, features = ["derive"] } serde_json = { workspace = true } json5 = { workspace = true } @@ -31,11 +32,16 @@ displaydoc = { workspace = true } derive_more = { workspace = true } cfg-if = { workspace = true } once_cell = { workspace = true } +nonzero_ext = { workspace = true } +toml = { workspace = true } +merge = "0.1.0" [dev-dependencies] proptest = "1.3.1" stacker = "0.1.15" expect-test = { workspace = true } +trybuild = { workspace = true } +hex = { workspace = true } [features] tokio-console = [] diff --git a/config/base/Cargo.toml b/config/base/Cargo.toml index b11b28ea577..b5b469bc524 100644 --- a/config/base/Cargo.toml +++ b/config/base/Cargo.toml @@ -11,15 +11,14 @@ license.workspace = true workspace = true [dependencies] -iroha_config_derive = { workspace = true } -iroha_crypto = { workspace = true, features = ["std"] } - -serde = { workspace = true, default-features = false, features = ["derive"] } -serde_json = { workspace = true, features = ["alloc"] } -parking_lot = { workspace = true } -json5 = { workspace = true } -thiserror = { workspace = true } -displaydoc = { workspace = true } -crossbeam = { workspace = true } +merge = "0.1.0" +drop_bomb = { workspace = true } +derive_more = { workspace = true, features = ["from", "deref", "deref_mut"] } eyre = { workspace = true } +serde = { workspace = true, features = ["derive"] } +serde_with = { workspace = true, features = ["macros", "std"] } +thiserror = { workspace = true } +num-traits = "0.2.17" +[dev-dependencies] +toml = { workspace = true } diff --git a/config/base/derive/Cargo.toml b/config/base/derive/Cargo.toml deleted file mode 100644 index 8aa95845755..00000000000 --- a/config/base/derive/Cargo.toml +++ /dev/null @@ -1,24 +0,0 @@ -[package] -name = "iroha_config_derive" - -edition.workspace = true -version.workspace = true -authors.workspace = true - -license.workspace = true - -[lints] -workspace = true - -[lib] -proc-macro = true - -[dependencies] -iroha_macro_utils = { workspace = true } - -syn = { workspace = true, features = ["derive", "parsing", "proc-macro", "clone-impls", "printing"] } -# This is the maximally compressed set of features. Yes we also need "printing". -quote = { workspace = true } -proc-macro2 = { workspace = true } -proc-macro-error = { workspace = true } - diff --git a/config/base/derive/src/lib.rs b/config/base/derive/src/lib.rs deleted file mode 100644 index 0cd24e4e345..00000000000 --- a/config/base/derive/src/lib.rs +++ /dev/null @@ -1,51 +0,0 @@ -//! Contains various configuration related macro definitions. - -use proc_macro::TokenStream; - -pub(crate) mod proxy; -pub(crate) mod utils; -pub(crate) mod view; - -/// Derive for config loading. More details in `iroha_config_base` reexport -#[proc_macro_derive(Override, attributes(config))] -pub fn override_derive(input: TokenStream) -> TokenStream { - let ast = syn::parse_macro_input!(input as utils::StructWithFields); - proxy::impl_override(&ast) -} - -/// Derive for config querying and setting. More details in `iroha_config_base` reexport -#[proc_macro_derive(Builder, attributes(builder))] -pub fn builder_derive(input: TokenStream) -> TokenStream { - let ast = syn::parse_macro_input!(input as utils::StructWithFields); - proxy::impl_build(&ast) -} - -/// Derive for config querying and setting. More details in `iroha_config_base` reexport -#[proc_macro_error::proc_macro_error] -#[proc_macro_derive(LoadFromEnv, attributes(config))] -pub fn load_from_env_derive(input: TokenStream) -> TokenStream { - let ast = syn::parse_macro_input!(input as utils::StructWithFields); - proxy::impl_load_from_env(&ast) -} - -/// Derive for config querying and setting. More details in `iroha_config_base` reexport -#[proc_macro_derive(LoadFromDisk)] -pub fn load_from_disk_derive(input: TokenStream) -> TokenStream { - let ast = syn::parse_macro_input!(input as utils::StructWithFields); - proxy::impl_load_from_disk(&ast) -} - -/// Derive for config querying and setting. More details in `iroha_config_base` reexport -#[proc_macro_derive(Proxy, attributes(config))] -pub fn proxy_derive(input: TokenStream) -> TokenStream { - let ast = syn::parse_macro_input!(input as utils::StructWithFields); - proxy::impl_proxy(ast) -} - -/// Generate view for given struct and convert from type to its view. -/// More details in `iroha_config_base` reexport. -#[proc_macro] -pub fn view(input: TokenStream) -> TokenStream { - let ast = syn::parse_macro_input!(input as utils::StructWithFields); - view::impl_view(ast) -} diff --git a/config/base/derive/src/proxy.rs b/config/base/derive/src/proxy.rs deleted file mode 100644 index dafef4c6145..00000000000 --- a/config/base/derive/src/proxy.rs +++ /dev/null @@ -1,324 +0,0 @@ -use proc_macro::TokenStream; -use proc_macro_error::abort; -use quote::{format_ident, quote}; -use syn::{parse_quote, Type, TypePath}; - -use super::utils::{get_inner_type, StructWithFields}; -use crate::utils; - -pub fn impl_proxy(ast: StructWithFields) -> TokenStream { - let parent_name = &ast.ident; - let parent_ty: Type = parse_quote! { #parent_name }; - let proxy_struct = gen_proxy_struct(ast); - let loadenv_derive = quote! { ::iroha_config_base::derive::LoadFromEnv }; - let disk_derive = quote! { ::iroha_config_base::derive::LoadFromDisk }; - let builder_derive = quote! { ::iroha_config_base::derive::Builder }; - let override_derive = quote! { ::iroha_config_base::derive::Override }; - quote! { - /// Proxy configuration structure to be used as an intermediate - /// for configuration loading. Both loading from disk and - /// from env should only be done via this struct, which then - /// builds into its parent [`struct@Configuration`]. - #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, - #builder_derive, - #loadenv_derive, - #disk_derive, - #override_derive - )] - #[builder(parent = #parent_ty)] - #proxy_struct - - } - .into() -} - -pub fn impl_override(ast: &StructWithFields) -> TokenStream { - let override_trait = quote! { ::iroha_config_base::proxy::Override }; - let name = &ast.ident; - let clauses = ast.fields.iter().map(|field| { - let field_name = &field.ident; - if field.has_inner { - let inner_ty = get_inner_type("Option", &field.ty); - quote! { - self.#field_name = match (self.#field_name, other.#field_name) { - (Some(this_field), Some(other_field)) => Some(<#inner_ty as #override_trait>::override_with(this_field, other_field)), - (this_field, None) => this_field, - (None, other_field) => other_field, - }; - } - } else { - quote! { - if let Some(other_field) = other.#field_name { - self.#field_name = Some(other_field) - } - } - } - }); - - quote! { - impl #override_trait for #name { - fn override_with(mut self, other: Self) -> Self { - #(#clauses)* - self - } - } - } - .into() -} - -pub fn impl_load_from_env(ast: &StructWithFields) -> TokenStream { - let env_fetcher_ident = quote! { env_fetcher }; - let fetch_env_trait = quote! { ::iroha_config_base::proxy::FetchEnv }; - let env_trait = quote! { ::iroha_config_base::proxy::LoadFromEnv }; - - let set_field = ast.fields - .iter() - .map(|field| { - let ty = &field.ty; - let as_str_attr = field.has_as_str; - let ident = &field.ident; - let field_env = &field.env_str; - - let inner_ty = if field.has_option { - get_inner_type("Option", ty) - } else { - abort!(ast, "This macro should only be used on `ConfigurationProxy` types, \ - i.e. the types which represent a partially finalised configuration \ - (with some required fields omitted and to be read from other sources). \ - These types' fields have the `Option` type wrapped around each of them.") - }; - let is_string = if let Type::Path(TypePath { path, .. }) = inner_ty { - path.is_ident("String") - } else { - false - }; - let inner = if is_string { - quote! { Ok(var) } - } else if as_str_attr { - quote! {{ - let value: ::serde_json::Value = var.into(); - ::json5::from_str(&value.to_string()) - }} - } else { - quote! { ::json5::from_str(&var) } - }; - let mut set_field = quote! { - let #ident = #env_fetcher_ident.fetch(#field_env) - // treating unicode errors the same as variable absence - .ok() - .map(|var| { - #inner.map_err(|err| { - ::iroha_config_base::derive::Error::field_deserialization_from_json5( - // FIXME: specify location precisely - // https://github.com/hyperledger/iroha/issues/3470 - #field_env, - &err - ) - }) - }) - .transpose()?; - }; - if field.has_inner { - let maybe_map_box = gen_maybe_map_box(inner_ty); - set_field.extend(quote! { - let inner_proxy = <#inner_ty as #env_trait>::from_env(#env_fetcher_ident) - #maybe_map_box - ?; - let #ident = if let Some(old_inner) = #ident { - Some(<#inner_ty as ::iroha_config_base::proxy::Override>::override_with(old_inner, inner_proxy)) - } else { - Some(inner_proxy) - }; - }); - } - set_field - }); - - let name = &ast.ident; - let fields = ast - .fields - .iter() - .map(|field| { - let ident = &field.ident; - quote! { #ident } - }) - .collect::>(); - quote! { - impl #env_trait for #name { - type ReturnValue = Result; - fn from_env(#env_fetcher_ident: &F) -> Self::ReturnValue { - #(#set_field)* - let proxy = #name { - #(#fields),* - }; - Ok(proxy) - } - } - } - .into() -} - -pub fn impl_load_from_disk(ast: &StructWithFields) -> TokenStream { - let proxy_name = &ast.ident; - let disk_trait = quote! { ::iroha_config_base::proxy::LoadFromDisk }; - let error_ty = quote! { ::iroha_config_base::derive::Error }; - let disk_err_variant = quote! { ::iroha_config_base::derive::Error::Disk }; - let serde_err_variant = quote! { ::iroha_config_base::derive::Error::Json5 }; - let none_proxy = gen_none_fields_proxy(ast); - quote! { - impl #disk_trait for #proxy_name { - type ReturnValue = Self; - fn from_path + ::std::fmt::Debug + Clone>(path: P) -> Self::ReturnValue { - let mut file = ::std::fs::File::open(path).map_err(#disk_err_variant); - // String has better parsing speed, see [issue](https://github.com/serde-rs/json/issues/160#issuecomment-253446892) - let mut s = String::new(); - let res = file - .and_then(|mut f| { - ::std::io::Read::read_to_string(&mut f, &mut s).map(move |_| s).map_err(#disk_err_variant) - }) - .and_then( - |s| -> ::core::result::Result { - json5::from_str(&s).map_err(#serde_err_variant) - }, - ) - .map_or(#none_proxy, ::std::convert::identity); - res - } - } - }.into() -} - -fn gen_proxy_struct(mut ast: StructWithFields) -> StructWithFields { - // As this changes the field types of the AST, `lvalue_read` - // and `lvalue_write` of its `StructField`s may get desynchronized - ast.fields.iter_mut().for_each(|field| { - // For fields of `Configuration` that have an inner config, the corresponding - // proxy field should have a `..Proxy` type there as well - if field.has_inner { - proxify_field_type(&mut field.ty); - } - let ty = &field.ty; - field.ty = parse_quote! { - Option<#ty> - }; - // - field - .attrs - .retain(|attr| attr.path.is_ident("doc") || attr.path.is_ident("config")); - // Fields that already wrap an option should have a - // custom deserializer so that json `null` becomes - // `Some(None)` and not just `None` - if field.has_option { - let de_helper = stringify! { ::iroha_config_base::proxy::some_option }; - let serde_attr: syn::Attribute = - parse_quote! { #[serde(default, deserialize_with = #de_helper)] }; - field.attrs.push(serde_attr); - } - field.has_option = true; - }); - ast.ident = format_ident!("{}Proxy", ast.ident); - // The only needed struct-level attributes are these - ast.attrs.retain(|attr| { - attr.path.is_ident("config") || attr.path.is_ident("serde") || attr.path.is_ident("cfg") - }); - ast -} - -#[allow(clippy::expect_used)] -pub fn proxify_field_type(field_ty: &mut syn::Type) { - if let Type::Path(path) = field_ty { - let last_segment = path.path.segments.last_mut().expect("Can't be empty"); - if last_segment.ident == "Box" { - let box_generic = utils::extract_box_generic(last_segment); - // Recursion - proxify_field_type(box_generic) - } else { - // TODO: Wouldn't it be better to get it as an associated type? - let new_ident = format_ident!("{}Proxy", last_segment.ident); - last_segment.ident = new_ident; - } - } -} - -pub fn impl_build(ast: &StructWithFields) -> TokenStream { - let checked_fields = gen_none_fields_check(ast); - let proxy_name = &ast.ident; - let parent_ty = utils::get_parent_ty(ast); - let builder_trait = quote! { ::iroha_config_base::proxy::Builder }; - let error_ty = quote! { ::iroha_config_base::derive::Error }; - - quote! { - impl #builder_trait for #proxy_name { - type ReturnValue = Result<#parent_ty, #error_ty>; - fn build(self) -> Self::ReturnValue { - Ok(#parent_ty { - #checked_fields - }) - } - } - } - .into() -} - -/// Helper function to be used in [`impl Builder`]. Verifies that all fields have -/// been initialized. -fn gen_none_fields_check(ast: &StructWithFields) -> proc_macro2::TokenStream { - let checked_fields = ast.fields.iter().map(|field| { - let ident = &field.ident; - let missing_field = quote! { ::iroha_config_base::derive::Error::MissingField }; - if field.has_inner { - let inner_ty = get_inner_type("Option", &field.ty); - let builder_trait = quote! { ::iroha_config_base::proxy::Builder }; - - let maybe_map_box = gen_maybe_map_box(inner_ty); - - quote! { - #ident: <#inner_ty as #builder_trait>::build( - self.#ident.ok_or( - #missing_field{field: stringify!(#ident), message: ""} - )? - ) - #maybe_map_box - ? - } - } else { - quote! { - #ident: self.#ident.ok_or( - #missing_field{field: stringify!(#ident), message: ""} - )? - } - } - }); - quote! { - #(#checked_fields),* - } -} - -fn gen_maybe_map_box(inner_ty: &syn::Type) -> proc_macro2::TokenStream { - if let Type::Path(path) = &inner_ty { - let last_segment = path.path.segments.last().expect("Can't be empty"); - if last_segment.ident == "Box" { - return quote! { - .map(Box::new) - }; - } - } - quote! {} -} - -/// Helper function to be used as an empty fallback for [`impl LoadFromEnv`] or [`impl LoadFromDisk`]. -/// Only meant for proxy types usage. -fn gen_none_fields_proxy(ast: &StructWithFields) -> proc_macro2::TokenStream { - let proxy_name = &ast.ident; - let none_fields = ast.fields.iter().map(|field| { - let ident = &field.ident; - quote! { - #ident: None - } - }); - quote! { - #proxy_name { - #(#none_fields),* - } - } -} diff --git a/config/base/derive/src/utils.rs b/config/base/derive/src/utils.rs deleted file mode 100644 index 36f79a76384..00000000000 --- a/config/base/derive/src/utils.rs +++ /dev/null @@ -1,367 +0,0 @@ -pub use iroha_macro_utils::{attr_struct, AttrParser}; -use proc_macro2::TokenStream; -use quote::{quote, ToTokens}; -use syn::{ - parse::{Parse, ParseStream}, - Attribute, GenericArgument, Ident, LitStr, Meta, NestedMeta, PathArguments, Token, Type, -}; - -/// Keywords used inside `#[view(...)]` and `#[config(...)]` -mod kw { - // config keywords - syn::custom_keyword!(serde_as_str); - syn::custom_keyword!(inner); - syn::custom_keyword!(env_prefix); - // view keywords - syn::custom_keyword!(ignore); - syn::custom_keyword!(into); - // builder keywords - syn::custom_keyword!(parent); -} - -/// Structure to parse `#[view(...)]` attributes. -/// [`Inner`] is responsible for parsing attribute arguments. -pub struct View(std::marker::PhantomData); - -/// Structure to parse `#[config(...)]` attributes. -/// [`Inner`] is responsible for parsing attribute arguments. -struct Config(std::marker::PhantomData); - -/// Structure to parse `#[builder(...)]` attributes. -/// [`Inner`] is responsible for parsing attribute arguments. -struct Builder(std::marker::PhantomData); - -impl AttrParser for View { - const IDENT: &'static str = "view"; -} - -impl AttrParser for Config { - const IDENT: &'static str = "config"; -} - -impl AttrParser for Builder { - const IDENT: &'static str = "builder"; -} - -attr_struct! { - pub struct ViewIgnore { - _kw: kw::ignore, - } -} - -attr_struct! { - pub struct ViewFieldType { - _kw: kw::into, - _eq: Token![=], - ty: Type, - } -} - -attr_struct! { - pub struct ConfigInner { - _kw: kw::inner, - } -} - -attr_struct! { - pub struct ConfigAsStr { - _kw: kw::serde_as_str, - } -} - -attr_struct! { - pub struct ConfigEnvPrefix { - _kw: kw::env_prefix, - _eq: Token![=], - pub prefix: LitStr, - } -} - -attr_struct! { - pub struct BuilderParent { - _kw: kw::parent, - _eq: Token![=], - pub parent: Type, - } -} - -impl From for Type { - fn from(value: ViewFieldType) -> Self { - value.ty - } -} - -#[derive(Clone)] -pub struct StructField { - pub ident: Ident, - pub ty: Type, - pub vis: syn::Visibility, - pub attrs: Vec, - pub env_str: String, - pub has_inner: bool, - pub has_option: bool, - pub has_as_str: bool, - pub lvalue_read: TokenStream, - pub lvalue_write: TokenStream, -} - -impl StructField { - fn from_ast(field: syn::Field, env_prefix: &str) -> Self { - let field_ident = field - .ident - .expect("Already checked for named fields at parsing"); - let (lvalue_read, lvalue_write) = gen_lvalue(&field.ty, &field_ident); - StructField { - has_inner: field - .attrs - .iter() - .any(|attr| Config::::parse(attr).is_ok()), - has_as_str: field - .attrs - .iter() - .any(|attr| Config::::parse(attr).is_ok()), - has_option: is_option_type(&field.ty), - env_str: env_prefix.to_owned() + &field_ident.to_string().to_uppercase(), - attrs: field.attrs, - ident: field_ident, - ty: field.ty, - vis: field.vis, - lvalue_read, - lvalue_write, - } - } -} - -impl ToTokens for StructField { - fn to_tokens(&self, tokens: &mut TokenStream) { - let StructField { - attrs, - ty, - ident, - vis, - .. - } = self; - let stream = quote! { - #(#attrs)* - #vis #ident: #ty - }; - tokens.extend(stream); - } -} - -/// Parsed struct with named fields used in proc macros of this crate -#[derive(Clone)] -pub struct StructWithFields { - pub attrs: Vec, - pub env_prefix: String, - pub vis: syn::Visibility, - _struct_token: Token![struct], - pub ident: Ident, - pub generics: syn::Generics, - pub fields: Vec, - _semi_token: Option, -} - -impl Parse for StructWithFields { - fn parse(input: ParseStream) -> syn::Result { - let attrs = input.call(Attribute::parse_outer)?; - let env_prefix = attrs - .iter() - .map(Config::::parse) - .find_map(Result::ok) - .map(|pref| pref.prefix.value()) - .unwrap_or_default(); - Ok(Self { - attrs, - vis: input.parse()?, - _struct_token: input.parse()?, - ident: input.parse()?, - generics: input.parse()?, - fields: input - .parse::()? - .named - .into_iter() - .map(|field| StructField::from_ast(field, &env_prefix)) - .collect(), - env_prefix, - _semi_token: input.parse()?, - }) - } -} - -impl ToTokens for StructWithFields { - fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { - let StructWithFields { - attrs, - vis, - ident, - generics, - fields, - .. - } = self; - let stream = quote! { - #(#attrs)* - #vis struct #ident #generics { - #(#fields),* - } - }; - tokens.extend(stream); - } -} - -/// Remove attributes with ident [`attr_ident`] from attributes -pub fn remove_attr(attrs: &mut Vec, attr_ident: &str) { - attrs.retain(|attr| !attr.path.is_ident(attr_ident)); -} - -pub fn extract_field_idents(fields: &[StructField]) -> Vec<&Ident> { - fields.iter().map(|field| &field.ident).collect::>() -} - -pub fn extract_field_types(fields: &[StructField]) -> Vec { - fields - .iter() - .map(|field| field.ty.clone()) - .collect::>() -} - -pub fn get_type_argument<'tl>(s: &str, ty: &'tl Type) -> Option<&'tl GenericArgument> { - let Type::Path(path) = ty else { - return None; - }; - let segments = &path.path.segments; - if segments.len() != 1 || segments[0].ident != s { - return None; - } - - if let PathArguments::AngleBracketed(bracketed_arguments) = &segments[0].arguments { - if bracketed_arguments.args.len() == 1 { - return Some(&bracketed_arguments.args[0]); - } - } - None -} - -pub fn get_inner_type<'tl>(outer_ty_ident: &str, ty: &'tl Type) -> &'tl Type { - #[allow(clippy::shadow_unrelated)] - get_type_argument(outer_ty_ident, ty) - .and_then(|ty| { - if let GenericArgument::Type(r#type) = ty { - Some(r#type) - } else { - None - } - }) - .unwrap_or(ty) -} - -pub fn is_arc_rwlock(ty: &Type) -> bool { - let dearced_ty = get_inner_type("Arc", ty); - get_type_argument("RwLock", dearced_ty).is_some() -} - -/// Check if the provided type is of the form [`Option<..>`] -pub fn is_option_type(ty: &Type) -> bool { - get_type_argument("Option", ty).is_some() -} - -/// Remove attributes with ident [`attr_ident`] from struct attributes and field attributes -pub fn remove_attr_from_struct(ast: &mut StructWithFields, attr_ident: &str) { - let StructWithFields { attrs, fields, .. } = ast; - for field in fields { - remove_attr(&mut field.attrs, attr_ident); - } - remove_attr(attrs, attr_ident); -} - -/// Keep only derive attributes passed as a second argument in struct attributes and field attributes -pub fn keep_derive_attr(ast: &mut StructWithFields, kept_attrs: &[&str]) { - ast.attrs - .iter_mut() - .filter(|attr| attr.path.is_ident("derive")) - .for_each(|attr| { - let meta = attr - .parse_meta() - .expect("derive macro must be in one of the meta forms"); - if let Meta::List(list) = meta { - let items: Vec = list - .nested - .into_iter() - .filter(|nested| { - if let NestedMeta::Meta(Meta::Path(path)) = nested { - return kept_attrs.iter().any(|kept_attr| path.is_ident(kept_attr)); - } - // Non-nested all kept by default - true - }) - .collect(); - *attr = syn::parse_quote!( - #[derive(#(#items),*)] - ); - } - }); -} - -/// Keep only attributes passed as a second argument in struct attributes and field attributes -pub fn keep_attrs_in_struct(ast: &mut StructWithFields, kept_attrs: &[&str]) { - let StructWithFields { attrs, fields, .. } = ast; - for field in fields { - field.attrs.retain(|attr| { - kept_attrs - .iter() - .any(|kept_attr| attr.path.is_ident(kept_attr)) - }); - } - attrs.retain(|attr| { - kept_attrs - .iter() - .any(|kept_attr| attr.path.is_ident(kept_attr)) - }); -} - -/// Generate lvalue forms for a struct field, taking [`Arc>`] types -/// into account as well. Returns a 2-tuple of read and write forms. -pub fn gen_lvalue(field_ty: &Type, field_ident: &Ident) -> (TokenStream, TokenStream) { - let is_lvalue = is_arc_rwlock(field_ty); - - let lvalue_read = if is_lvalue { - quote! { self.#field_ident.read().await } - } else { - quote! { self.#field_ident } - }; - - let lvalue_write = if is_lvalue { - quote! { self.#field_ident.write().await } - } else { - quote! { self.#field_ident } - }; - - (lvalue_read, lvalue_write) -} - -/// Check if [`StructWithFields`] has `#[builder(parent = ..)]` -pub fn get_parent_ty(ast: &StructWithFields) -> Type { - ast.attrs - .iter() - .find_map(|attr| Builder::::parse(attr).ok()) - .map(|builder| builder.parent) - .expect("Should not be called on structs with no `#[builder(..)]` attribute") -} - -pub fn extract_box_generic(box_seg: &mut syn::PathSegment) -> &mut syn::Type { - let syn::PathArguments::AngleBracketed(generics) = &mut box_seg.arguments else { - panic!("`Box` should have explicit generic"); - }; - - assert!( - generics.args.len() == 1, - "`Box` should have exactly one generic argument" - ); - let syn::GenericArgument::Type(generic_type) = - generics.args.first_mut().expect("Can't be empty") - else { - panic!("`Box` should have type as a generic argument") - }; - - generic_type -} diff --git a/config/base/derive/src/view.rs b/config/base/derive/src/view.rs deleted file mode 100644 index a020c7edc13..00000000000 --- a/config/base/derive/src/view.rs +++ /dev/null @@ -1,183 +0,0 @@ -use gen::*; -use proc_macro::TokenStream; -use quote::{format_ident, quote}; - -use super::utils::{ - extract_field_idents, extract_field_types, remove_attr, remove_attr_from_struct, AttrParser, - StructField, StructWithFields, View, ViewFieldType, ViewIgnore, -}; - -pub fn impl_view(ast: StructWithFields) -> TokenStream { - let original = original_struct(ast.clone()); - let view = view_struct(ast); - let impl_from = impl_from(&original, &view); - let impl_has_view = impl_has_view(&original); - let assertions = assertions(&view); - let out = quote! { - #original - #impl_has_view - #view - #impl_from - #assertions - }; - out.into() -} - -mod gen { - use super::*; - use crate::utils::{keep_attrs_in_struct, keep_derive_attr}; - - pub fn original_struct(mut ast: StructWithFields) -> StructWithFields { - remove_attr_from_struct(&mut ast, "view"); - ast - } - - pub fn view_struct(mut ast: StructWithFields) -> StructWithFields { - // Remove fields with #[view(ignore)] - ast.fields.retain(is_view_field_ignored); - // Change field type to `Type` if it has attribute #[view(into = Type)] - ast.fields.iter_mut().for_each(view_field_change_type); - // Replace doc-string for view - remove_attr(&mut ast.attrs, "doc"); - let view_doc = format!("View for {}", ast.ident); - ast.attrs.push(syn::parse_quote!( - #[doc = #view_doc] - )); - keep_derive_attr( - &mut ast, - &[ - "Clone", - "Debug", - "Deserialize", - "Serialize", - "PartialEq", - "Eq", - ], - ); - keep_attrs_in_struct(&mut ast, &["serde", "doc", "derive", "cfg"]); - ast.ident = format_ident!("{}View", ast.ident); - ast - } - - pub fn impl_from( - original: &StructWithFields, - view: &StructWithFields, - ) -> proc_macro2::TokenStream { - let StructWithFields { - ident: original_ident, - .. - } = original; - let StructWithFields { - generics, - ident: view_ident, - fields, - .. - } = view; - let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); - let field_idents = extract_field_idents(fields); - let field_cfg_attrs = fields - .iter() - .map(|field| { - field - .attrs - .iter() - .filter(|attr| attr.path.is_ident("cfg")) - .collect::>() - }) - .collect::>(); - - let field_froms: Vec<_> = fields - .iter() - .map(|field| { - let field_ident = &field.ident; - if let syn::Type::Path(syn::TypePath { path, .. }) = &field.ty { - let last_segment = path.segments.last().expect("Not empty"); - if last_segment.ident == "Box" { - return quote! { - #field_ident: Box::new(core::convert::From::<_>::from(*#field_ident)), - }; - } - } - quote! { - #field_ident: core::convert::From::<_>::from(#field_ident), - } - }) - .collect(); - - quote! { - impl #impl_generics core::convert::From<#original_ident> for #view_ident #ty_generics #where_clause { - fn from(config: #original_ident) -> Self { - let #original_ident { - #( - #(#field_cfg_attrs)* - #field_idents, - )* - .. - } = config; - Self { - #( - #(#field_cfg_attrs)* - #field_froms - )* - } - } - } - } - } - - pub fn impl_has_view(original: &StructWithFields) -> proc_macro2::TokenStream { - let StructWithFields { - generics, - ident: view_ident, - .. - } = original; - let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); - - quote! { - impl #impl_generics iroha_config_base::view::HasView for #view_ident #ty_generics #where_clause {} - } - } - - pub fn assertions(view: &StructWithFields) -> proc_macro2::TokenStream { - let StructWithFields { fields, .. } = view; - let field_types = extract_field_types(fields); - let messages: Vec = extract_field_idents(fields) - .iter() - .map(|ident| { - format!("Field `{ident}` has it's own view, consider adding attribute #[view(into = ViewType)]") - }) - .collect(); - quote! { - /// Assert that every field of 'View' doesn't implement `HasView` trait - const _: () = { - use iroha_config_base::view::NoView; - #( - const _: () = assert!(!iroha_config_base::view::IsInstanceHasView::<#field_types>::IS_HAS_VIEW, #messages); - )* - }; - } - } -} - -/// Check if [`Field`] has `#[view(ignore)]` -fn is_view_field_ignored(field: &StructField) -> bool { - field - .attrs - .iter() - .map(View::::parse) - .find_map(Result::ok) - .is_none() -} - -/// Change [`Field`] type to `Type` if `#[view(type = Type)]` is present -fn view_field_change_type(field: &mut StructField) { - if let Some(ty) = field - .attrs - .iter() - .map(View::::parse) - .find_map(Result::ok) - .map(ViewFieldType::into) - { - field.ty = ty; - } -} diff --git a/config/base/src/lib.rs b/config/base/src/lib.rs index 7ea61d35ddb..cf898f6ab57 100644 --- a/config/base/src/lib.rs +++ b/config/base/src/lib.rs @@ -1,493 +1,632 @@ -//! Package for managing iroha configuration -use std::{fmt::Debug, path::Path}; +//! Utilities behind Iroha configurations + +use std::{ + borrow::Cow, + cell::RefCell, + collections::{HashMap, HashSet}, + convert::Infallible, + env::VarError, + error::Error, + ffi::OsString, + fmt::{Debug, Display, Formatter}, + ops::Sub, + path::PathBuf, + str::FromStr, + time::Duration, +}; + +use eyre::{eyre, Report, WrapErr}; +pub use merge::Merge; +pub use serde; +use serde::{Deserialize, Serialize}; + +/// [`Duration`], but can parse a human-readable string. +/// TODO: currently deserializes just as [`Duration`] +#[serde_with::serde_as] +#[derive(Debug, Copy, Clone, Deserialize, Serialize, Ord, PartialOrd, Eq, PartialEq)] +pub struct HumanDuration(#[serde_as(as = "serde_with::DurationMilliSeconds")] pub Duration); + +impl HumanDuration { + /// Get the [`Duration`] + pub fn get(self) -> Duration { + self.0 + } +} -use serde::{de::DeserializeOwned, Deserialize, Deserializer, Serialize}; +/// Representation of amount of bytes, parseable from a human-readable string. +#[derive(Debug, Copy, Clone, Deserialize, Serialize)] +pub struct HumanBytes(pub T); -pub mod derive { - //! Derives for configuration entities - /// Generate view for the type and implement conversion `Type -> View`. - /// View contains a subset of the fields that the type has. - /// - /// Works only with structs. - /// - /// ## Container attributes - /// - /// ## Field attributes - /// ### `#[view(ignore)]` - /// Marks fields to ignore when converting to view type. - /// - /// ### `#[view(into = Ty)]` - /// Sets view's field type to Ty. - /// - /// ## Examples - /// - /// ```rust - /// use iroha_config_base::derive::view; - /// - /// view! { - /// #[derive(Default)] - /// struct Structure { - /// #[view(into = u64)] - /// a: u32, - /// // `View` shouldn't have field `b` so we must exclude it. - /// #[view(ignore)] - /// b: u32, - /// } - /// } - /// - /// // Will generate something like - /// // --//-- original struct - /// // struct StructureView { - /// // a: u64, - /// // } - /// // - /// // impl From for StructureView { - /// // fn from(value: Structure) -> Self { - /// // let Structure { - /// // a, - /// // .. - /// // } = value; - /// // Self { - /// // a: From::<_>::from(a), - /// // } - /// // } - /// // } - /// - /// - /// let structure = Structure { a: 13, b: 37 }; - /// let view: StructureView = structure.into(); - /// assert_eq!(view.a, 13); - /// ``` - pub use iroha_config_derive::view; - /// Derive macro for implementing the trait - /// [`iroha_config::base::proxy::Builder`](`crate::proxy::Builder`) - /// for config structures. Meant to be used on proxy types only, for - /// details see [`iroha_config::base::derive::Proxy`](`crate::derive::Proxy`). - /// - /// # Container attributes - /// - /// ## `#[builder(parent = ..)]` - /// Takes a target type to build into, e.g. for a `ConfigurationProxy` - /// it would be `Configuration`. - /// - /// # Examples - /// - /// ```rust - /// use iroha_config_base::derive::{Builder, Override, LoadFromEnv}; - /// use iroha_config_base::proxy::Builder as _; - /// - /// // Also need `LoadFromEnv` as it owns the `#[config]` attribute - /// #[derive(Debug, PartialEq, serde::Deserialize, serde::Serialize, LoadFromEnv, Builder)] - /// #[builder(parent = Outer)] - /// struct OuterProxy { #[config(inner)] inner: Option } - /// - /// #[derive(Debug, PartialEq, serde::Deserialize, serde::Serialize, LoadFromEnv, Builder, Override)] - /// #[builder(parent = Inner)] - /// struct InnerProxy { b: Option } - /// - /// #[derive(Debug, PartialEq)] - /// struct Outer { inner: Inner } - /// - /// #[derive(Debug, PartialEq)] - /// struct Inner { b: String } - /// - /// let outer_proxy = OuterProxy { inner: Some(InnerProxy { b: Some("a".to_owned()) })}; - /// - /// let outer = Outer { inner: Inner { b: "a".to_owned() } }; - /// - /// assert_eq!(outer, outer_proxy.build().unwrap()); - /// ``` - pub use iroha_config_derive::Builder; - /// Derive macro for implementing the trait - /// [`iroha_config::base::proxy::LoadFromDisk`](`crate::proxy::LoadFromDisk`) - /// trait for config structures. - /// - /// Meant to be used on proxy types only, for - /// details see [`iroha_config::base::derive::Proxy`](`crate::derive::Proxy`). - /// - /// The trait's only method, `from_path`, - /// deserializes a JSON config at the provided path into the parent proxy structure, - /// leaving it empty in case of any error. - /// - /// The `ReturnValue` associated type can be - /// swapped for anything suitable. Currently, the proxy structure is returned - /// by default. - pub use iroha_config_derive::LoadFromDisk; - /// Derive macro for implementing the - /// [`iroha_config::base::proxy::LoadFromDisk`](`crate::proxy::LoadFromDisk`) - /// trait for config structures. - /// - /// Meant to be used on proxy types only, for - /// details see [`iroha_config::base::derive::Proxy`](`crate::derive::Proxy`). - /// - /// The `ReturnValue` associated type can be - /// swapped for anything suitable. Currently, the proxy structure is returned - /// by default. - /// - /// # Container attributes - /// ## `[config(env_prefix)]` - /// Sets prefix for all the env variables derived from fields in the - /// corresponding structure. - /// - /// ### Example - /// - /// ``` rust - /// use iroha_config_base::derive::LoadFromEnv; - /// use iroha_config_base::proxy::LoadFromEnv as _; - /// - /// #[derive(serde::Deserialize, serde::Serialize, LoadFromEnv)] - /// #[config(env_prefix = "PREFIXED_")] - /// struct PrefixedProxy { a: Option } - /// - /// std::env::set_var("PREFIXED_A", "B"); - /// let prefixed = PrefixedProxy::from_std_env().unwrap(); - /// assert_eq!(prefixed.a.unwrap(), "B"); - /// ``` - /// - /// # Field attributes - /// ## `#[config(inner)]` - /// Tells macro that the structure stores another config inside, - /// allowing to load it recursively. Moreover, the types that - /// have this attributes on them should also implement or - /// derive the [`iroha_config::base::proxy::Override`](`crate::proxy::Override`) - /// trait. - /// - /// ### Example - /// - /// ```rust - /// use iroha_config_base::derive::{Override, LoadFromEnv}; - /// use iroha_config_base::proxy::LoadFromEnv as _; - /// - /// #[derive(Debug, PartialEq, serde::Deserialize, serde::Serialize, LoadFromEnv)] - /// struct OuterProxy { #[config(inner)] inner: Option } - /// - /// #[derive(Debug, PartialEq, serde::Deserialize, serde::Serialize, Override, LoadFromEnv)] - /// struct InnerProxy { b: Option } - /// - /// let mut outer = OuterProxy { inner: Some(InnerProxy { b: Some("a".to_owned()) })}; - /// - /// std::env::set_var("B", "a"); - /// let env_outer = OuterProxy::from_std_env().unwrap(); - /// - /// assert_eq!(env_outer, outer); - /// ``` - /// - /// ## `#[config(serde_as_str)]` - /// Tells macro to deserialize from env variable as a bare string. - /// - /// ### Example - /// - /// ``` - /// use iroha_config_base::derive::LoadFromEnv; - /// use iroha_config_base::proxy::LoadFromEnv; - /// use std::net::Ipv4Addr; - /// - /// #[derive(serde::Deserialize, serde::Serialize, LoadFromEnv)] - /// struct IpAddrProxy { #[config(serde_as_str)] ip: Option } - /// - /// std::env::set_var("IP", "127.0.0.1"); - /// let ip = IpAddrProxy::from_std_env().unwrap(); - /// assert_eq!(ip.ip.unwrap(), Ipv4Addr::new(127, 0, 0, 1)); - /// ``` - pub use iroha_config_derive::LoadFromEnv; - /// Derive macro for implementing the trait - /// [`iroha_config::base::proxy::Override`](`crate::proxy::Override`) - /// for config structures. Given two proxies, consumes them by recursively overloading - /// fields of [`self`] with fields of [`other`]. Order matters here, - /// i.e. `self.combine(other)` could yield different results than `other.combine(self)`. - /// - /// Meant to be used on proxy types only, for - /// details see [`iroha_config::base::derive::Proxy`](`crate::derive::Proxy`). - /// - /// # Examples - /// - /// ```rust - /// use iroha_config_base::derive::{Override, LoadFromEnv}; - /// use iroha_config_base::proxy::Override as _; - /// - /// #[derive(Debug, PartialEq, serde::Deserialize, serde::Serialize, Override, LoadFromEnv)] - /// struct OuterProxy { - /// #[config(inner)] - /// inner: Option, - /// a: Option - /// } +impl HumanBytes { + /// Get the number of bytes + pub fn get(self) -> T { + self.0 + } +} + +/// Error representing a missing field in the configuration +#[derive(thiserror::Error, Debug)] +#[error("missing field: `{path}`")] +pub struct MissingFieldError { + path: String, +} + +impl MissingFieldError { + /// Create an instance + pub fn new(s: &str) -> Self { + Self { path: s.to_owned() } + } +} + +/// Provides environment variables +pub trait ReadEnv { + /// Read a value of an environment variable. /// - /// #[derive(Debug, PartialEq, serde::Deserialize, serde::Serialize, Override, LoadFromEnv)] - /// struct InnerProxy { b: Option } + /// This is a fallible operation, which might return an empty value if the given key is not + /// present. /// - /// let left_outer = OuterProxy { - /// inner: Some(InnerProxy { b: Some("a".to_owned()) }), - /// a: None - /// }; + /// [`Cow`] is used for flexibility. The read value might be given both as an owned and as a + /// borrowed string depending on the structure that implements [`ReadEnv`]. On the receiving + /// part, it might be convenient to parse the string while just borrowing it + /// (e.g. with [`FromStr`]), but might be also convenient to own the value. [`Cow`] covers all + /// of this. /// - /// let right_outer = OuterProxy { - /// inner: None, - /// a: Some("b".to_owned()) - /// }; + /// # Errors + /// For any reason an implementor might have. + fn read_env(&self, key: impl AsRef) -> Result>, E>; +} + +/// Constructs from environment variables +pub trait FromEnv { + /// Constructs from environment variables using [`ReadEnv`] /// - /// let res_outer = OuterProxy { - /// inner: Some(InnerProxy { b: Some("a".to_owned()) }), - /// a: Some("b".to_owned()) - /// }; + /// # Errors + /// For any reason an implementor might have. + // `E: Error` so that it could be wrapped into a Report + fn from_env>(env: &R) -> FromEnvResult + where + Self: Sized; +} + +/// Result of [`FromEnv::from_env`]. Intended to contain multiple possible errors at once. +pub type FromEnvResult = eyre::Result>; + +/// Marker trait to implement [`FromEnv`] if a type implements [`Default`] +pub trait FromEnvDefaultFallback {} + +impl FromEnv for T +where + T: FromEnvDefaultFallback + Default, +{ + fn from_env>(_env: &R) -> FromEnvResult + where + Self: Sized, + { + Ok(Self::default()) + } +} + +/// Simple collector of errors. +/// +/// Will panic on [`Drop`] if contains errors that are not handled with [`Emitter::finish`]. +pub struct Emitter { + errors: Vec, + bomb: drop_bomb::DropBomb, +} + +impl Emitter { + /// Create a new empty emitter + pub fn new() -> Self { + Self { + errors: Vec::new(), + bomb: drop_bomb::DropBomb::new( + "Errors emitter is dropped without consuming collected errors", + ), + } + } + + /// Emit a single error + pub fn emit(&mut self, error: T) { + self.errors.push(error); + } + + /// Emit a collection of errors + pub fn emit_collection(&mut self, mut errors: ErrorsCollection) { + self.errors.append(&mut errors.0); + } + + /// Transform the emitter into a [`Result`], containing an [`ErrorCollection`] if + /// any errors were emitted. /// - /// assert_eq!(left_outer.override_with(right_outer), res_outer); - /// ``` - pub use iroha_config_derive::Override; - /// Derive macro for implementing the corresponding proxy type - /// for config structures. Most of the other traits in the - /// [`iroha_config_base::proxy`](`crate::proxy`) module are - /// best derived indirectly via this macro. Proxy types serve - /// as a stand-in for flexible configuration loading either - /// from environment variables or configuration files. Proxy types also - /// provide methods to build the initial parent type from them - /// (via [`iroha_config_base::proxy::Builder`](`crate::proxy::Builder`) - /// trait) and ways to combine two proxies together (via - /// [`iroha_config_base::proxy::Override`](`crate::proxy::Override`)). - pub use iroha_config_derive::Proxy; - use serde::Deserialize; - use thiserror::Error; - - /// Represents a path to a nested field in a config structure - #[derive(Debug, Deserialize)] - #[serde(transparent)] - pub struct Field(pub Vec); - - impl std::fmt::Display for Field { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - // separate fields with dots - std::fmt::Display::fmt(&self.0.join("."), f) + /// # Errors + /// If any errors were emitted. + pub fn finish(mut self) -> Result<(), ErrorsCollection> { + self.bomb.defuse(); + + if self.errors.is_empty() { + Ok(()) + } else { + Err(ErrorsCollection(self.errors)) } } +} - // TODO: deal with `#[serde(skip)]` - /// Derive `Configurable` and `Proxy` error - #[derive(Debug, Error, Deserialize, displaydoc::Display)] - #[ignore_extra_doc_attributes] - #[allow(clippy::enum_variant_names)] - pub enum Error { - /// Failed to deserialize the field `{field}` - /// - /// Used in [`super::proxy::LoadFromEnv`] trait for deserialization - /// errors - #[serde(skip)] - FieldDeserialization { - /// Field name (known at compile time) - field: &'static str, - /// Unified error - #[source] - error: eyre::Report, - }, - - /// Please add `{field}` to the configuration - #[serde(skip)] - MissingField { - /// Field name - field: &'static str, - /// Additional message to be added as `color_eyre::suggestion` - message: &'static str, - }, - - /// Key pair creation failed, most likely because the keys don't form a pair - Crypto(#[from] iroha_crypto::error::Error), - - // IMO this variant should not exist. If the value is inferred, we should only warn people if the inferred value is different from the provided one. - /// You should remove the field `{field}` as its value is determined by other configuration parameters - #[serde(skip)] - ProvidedInferredField { - /// Field name - field: &'static str, - /// Additional message to be added as `color_eyre::suggestion` - message: &'static str, - }, - - /// The value {value} of `{field}` is wrong. Please change the value - #[serde(skip)] - InsaneValue { - /// The value of the field that's incorrect - value: String, - /// Field name that contains invalid value - field: &'static str, - /// Additional message to be added as `color_eyre::suggestion` - message: String, - // docstring: &'static str, // TODO: Inline the docstring for easy access - }, - - /// Reading file from disk failed - /// - /// Used in the [`LoadFromDisk`](`crate::proxy::LoadFromDisk`) trait for file read errors - #[serde(skip)] - Disk(#[from] std::io::Error), - - /// Deserializing JSON failed - /// - /// Used in [`LoadFromDisk`](`crate::proxy::LoadFromDisk`) trait for deserialization errors - #[serde(skip)] - Json5(#[from] json5::Error), - } - - impl Error { - /// This method is needed because a call of [`eyre::eyre!`] cannot be compiled when - /// generated in a proc macro. So, this shorthand is needed for proc macros. - pub fn field_deserialization_from_json( - field: &'static str, - error: &serde_json::Error, - ) -> Self { - Self::FieldDeserialization { - field, - error: eyre::eyre!("JSON: {}", error), +impl Default for Emitter { + fn default() -> Self { + Self::new() + } +} + +impl Emitter { + /// Shorthand to emit a [`MissingFieldError`]. + pub fn emit_missing_field(&mut self, field_name: impl AsRef) { + self.emit(MissingFieldError::new(field_name.as_ref())) + } + + /// Tries to [`UnwrapPartial`], collecting errors on failure. + /// + /// This method is relevant for [`Emitter`], because [`UnwrapPartial`] + /// returns a collection of [`MissingFieldError`]s. + pub fn try_unwrap_partial(&mut self, partial: P) -> Option { + partial.unwrap_partial().map_or_else( + |err| { + self.emit_collection(err); + None + }, + Some, + ) + } +} + +/// An [`Error`] containing multiple errors inside +pub struct ErrorsCollection(Vec); + +impl Error for ErrorsCollection {} + +/// Displays each error on a new line +impl Display for ErrorsCollection +where + T: Display, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + for (i, item) in self.0.iter().enumerate() { + if i > 0 { + writeln!(f)?; } + write!(f, "{item}")?; } + Ok(()) + } +} - /// See [`Self::field_deserialization_from_json`] - pub fn field_deserialization_from_json5(field: &'static str, error: &json5::Error) -> Self { - Self::FieldDeserialization { - field, - error: eyre::eyre!("JSON5: {}", error), +impl Debug for ErrorsCollection +where + T: Debug, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + for (i, item) in self.0.iter().enumerate() { + if i > 0 { + writeln!(f)?; } + write!(f, "{item:?}")?; } + Ok(()) + } +} + +impl From for ErrorsCollection { + fn from(value: T) -> Self { + Self(vec![value]) } } -pub mod view { - //! Module for view related traits and structs +impl IntoIterator for ErrorsCollection { + type Item = T; + type IntoIter = std::vec::IntoIter; - /// Marker trait to set default value [`IsInstanceHasView::IS_INSTANCE_HAS_VIEW`] to `false` - pub trait NoView { - /// [`Self`] doesn't implement [`HasView`] - const IS_HAS_VIEW: bool = false; + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() } +} - impl NoView for T {} +/// An implementation of [`ReadEnv`] for testing convenience. +#[derive(Default)] +pub struct TestEnv { + map: HashMap, + visited: RefCell>, +} - /// Marker traits for types for which views are implemented - pub trait HasView {} +impl TestEnv { + /// Create new empty environment + pub fn new() -> Self { + Self::default() + } - /// Wrapper structure used to check if type implements `[HasView]` - /// If `T` doesn't implement [`HasView`] then - /// [`NoView::IS_INSTANCE_HAS_VIEW`] (`false`) will be used. - /// Otherwise [`IsInstanceHasView::IS_INSTANCE_HAS_VIEW`] (`true`) - /// from `impl` block will shadow `NoView::IS_INSTANCE_HAS_VIEW` - pub struct IsInstanceHasView(core::marker::PhantomData); + /// Create an environment with a given map + pub fn with_map(map: HashMap) -> Self { + Self { map, ..Self::new() } + } - impl IsInstanceHasView { - /// `T` implements trait [`HasView`] - pub const IS_INSTANCE_HAS_VIEW: bool = true; + /// Set a key-value pair + #[must_use] + pub fn set(mut self, key: impl AsRef, value: impl AsRef) -> Self { + self.map + .insert(key.as_ref().to_string(), value.as_ref().to_string()); + self + } + + /// Get a set of keys not visited yet by [`ReadEnv::read_env`] + pub fn unvisited(&self) -> HashSet { + let all_keys: HashSet<_> = self.map.keys().map(ToOwned::to_owned).collect(); + let visited: HashSet<_> = self.visited.borrow().clone(); + all_keys.sub(&visited) } } -pub mod proxy { - //! Module with traits for configuration proxies +impl ReadEnv for TestEnv { + fn read_env(&self, key: impl AsRef) -> Result>, Infallible> { + self.visited.borrow_mut().insert(key.as_ref().to_string()); + Ok(self + .map + .get(key.as_ref()) + .map(String::as_str) + .map(Cow::from)) + } +} - use super::*; +/// Implemented of [`ReadEnv`] on top of [`std::env::var`]. +#[derive(Debug, Copy, Clone)] +pub struct StdEnv; - /// Trait for combining two configuration instances - pub trait Override: Serialize + DeserializeOwned + Sized { - /// If any of the fields in `other` are filled, they - /// override the values of the fields in [`self`]. - #[must_use] - fn override_with(self, other: Self) -> Self; +impl ReadEnv for StdEnv { + fn read_env(&self, key: impl AsRef) -> Result>, StdEnvError> { + match std::env::var(key.as_ref()) { + Ok(value) => Ok(Some(value.into())), + Err(VarError::NotPresent) => Ok(None), + Err(VarError::NotUnicode(input)) => Err(StdEnvError::NotUnicode(input)), + } } +} + +/// An error that might occur while reading from std env. +/// +/// - **Q: Why just [`VarError`] is not used?** +/// - A: Because [`VarError::NotPresent`] is `Ok(None)` in terms of [`ReadEnv`] +#[derive(Debug, thiserror::Error)] +pub enum StdEnvError { + /// Reflects [`VarError::NotUnicode`] + #[error("the specified environment variable was found, but it did not contain valid unicode data: {0:?}")] + NotUnicode(OsString), +} + +/// A tool that simplifies work with graceful parsing of multiple values in combination +/// with [`Emitter`] +pub enum ParseEnvResult { + /// Value was found and parsed + Value(T), + /// An error occurred while reading or parsing the environment + Error, + /// Value was not found, no error occurred + None, +} - impl Override for Box { - fn override_with(self, other: Self) -> Self { - Box::new(T::override_with(*self, *other)) +impl ParseEnvResult +where + T: FromStr, + ::Err: Error + Send + Sync + 'static, +{ + /// _Simple_ parsing using [`FromStr`] + pub fn parse_simple( + emitter: &mut Emitter, + env: &impl ReadEnv, + env_key: impl AsRef, + field_name: impl AsRef, + ) -> Self { + // FIXME: errors handling is such a mess now + let read = match env + .read_env(env_key.as_ref()) + .map_err(|err| eyre!("{err}")) + .wrap_err_with(|| eyre!("ooops")) + { + Ok(Some(value)) => value, + Ok(None) => return Self::None, + Err(report) => { + emitter.emit(report); + return Self::Error; + } + }; + + match FromStr::from_str(read.as_ref()).wrap_err_with(|| { + eyre!( + "failed to parse `{}` field from `{}` env variable", + field_name.as_ref(), + env_key.as_ref() + ) + }) { + Ok(value) => Self::Value(value), + Err(report) => { + emitter.emit(report); + Self::Error + } } } +} - /// Trait for configuration loading and deserialization from - /// the environment - pub trait LoadFromEnv: Sized { - /// The return type. Could be target `Configuration`, - /// some `Result`, `Option`, or any other type that - /// wraps a `..Proxy` or `Configuration` type. - type ReturnValue; - - /// Load configuration from the environment - /// - /// # Errors - /// - Fails if the deserialization of any field fails. - fn from_env(fetcher: &F) -> Self::ReturnValue; - - /// Implementation of [`Self::from_env`] using [`std::env::var`]. - fn from_std_env() -> Self::ReturnValue { - struct FetchStdEnv; - - impl FetchEnv for FetchStdEnv { - fn fetch>( - &self, - key: K, - ) -> Result { - std::env::var(key) - } - } +/// During this conversion, [`ParseEnvResult::Error`] is interpreted as [`None`]. +impl From> for Option { + fn from(value: ParseEnvResult) -> Self { + match value { + ParseEnvResult::None | ParseEnvResult::Error => None, + ParseEnvResult::Value(x) => Some(x), + } + } +} + +/// Value container to be used in the partial layers. +/// +/// In partial layers, values might be present or not. +/// Partial layers consisting from [`UserField`] might be _incomplete_, +/// merged into each other (with [`merge::Merge`]), +/// and finally unwrapped (with [`UnwrapPartial`]) into a _complete_ layer of data. +/// +/// Partial layers might consist of fields other than [`UserField`], but their types should follow +/// the same conventions. This might be used e.g. to implement custom merge strategy. +#[derive( + Serialize, + Deserialize, + Ord, + PartialOrd, + Eq, + PartialEq, + derive_more::From, + Clone, + derive_more::Deref, + derive_more::DerefMut, +)] +pub struct UserField(Option); + +/// Delegating debug repr to [`Option`] +impl Debug for UserField { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +/// Empty user field +impl Default for UserField { + fn default() -> Self { + Self(None) + } +} - Self::from_env(&FetchStdEnv) +/// The other's value takes precedence over the self's +impl Merge for UserField { + fn merge(&mut self, other: Self) { + if let Some(value) = other.0 { + self.0 = Some(value) } } +} + +impl UserField { + /// Get the field value + pub fn get(self) -> Option { + self.0 + } + + /// Set the field value + pub fn set(&mut self, value: T) { + self.0 = Some(value); + } +} + +impl From> for UserField { + fn from(value: ParseEnvResult) -> Self { + let option: Option = value.into(); + option.into() + } +} - impl LoadFromEnv for Box { - type ReturnValue = T::ReturnValue; +/// Conversion from a layer's partial state into its full state, with all required +/// fields presented. +pub trait UnwrapPartial { + /// The output of unwrapping, i.e. the full layer + type Output; - fn from_env(fetcher: &F) -> Self::ReturnValue { - T::from_env(fetcher) + /// Unwraps the partial into a structure with all required fields present. + /// + /// # Errors + /// If there are absent fields, returns a bulk of [`MissingFieldError`]s. + fn unwrap_partial(self) -> UnwrapPartialResult; +} + +/// Used for [`UnwrapPartial::unwrap_partial`] +pub type UnwrapPartialResult = Result>; + +/// A tool to implement "extends" mechanism, i.e. mixins. +/// +/// It allows users to provide a path of other files that should be used as +/// a _base_ layer. +/// +/// ```toml +/// # contents of this file will be merged into the contents of `base.toml` +/// extends = "./base.toml" +/// ``` +/// +/// It is possible to specify multiple extensions at once: +/// +/// ```toml +/// # read `foo`, then merge `bar`, then merge `baz`, then merge this file's contents +/// extends = ["foo", "bar", "baz"] +/// ``` +/// +/// From the developer side, it should be used as a field on a partial layer: +/// +/// ``` +/// use iroha_config_base::ExtendsPaths; +/// +/// struct SomePartial { +/// extends: Option, +/// // ..other fields +/// } +/// ``` +/// +/// When this layer is constructed from a file, `ExtendsPaths` should be handled e.g. +/// with [`ExtendsPaths::iter`]. +#[derive(Debug, Serialize, Deserialize, Eq, PartialEq)] +#[serde(untagged)] +pub enum ExtendsPaths { + /// A single path to extend from + Single(PathBuf), + /// A chain of paths to extend from + Chain(Vec), +} + +/// Iterator over [`ExtendsPaths`] for convenience +pub enum ExtendsPathsIter<'a> { + #[allow(missing_docs)] + Single(Option<&'a PathBuf>), + #[allow(missing_docs)] + Multiple(std::slice::Iter<'a, PathBuf>), +} + +impl ExtendsPaths { + /// Normalise into an iterator over chain of paths to extend from + #[allow(clippy::iter_without_into_iter)] // extra for this case + pub fn iter(&self) -> ExtendsPathsIter<'_> { + match &self { + Self::Single(x) => ExtendsPathsIter::Single(Some(x)), + Self::Chain(vec) => ExtendsPathsIter::Multiple(vec.iter()), + } + } +} + +impl<'a> Iterator for ExtendsPathsIter<'a> { + type Item = &'a PathBuf; + + fn next(&mut self) -> Option { + match self { + Self::Single(x) => x.take(), + Self::Multiple(iter) => iter.next(), } } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn single_missing_field() { + let mut emitter: Emitter = Emitter::new(); + + emitter.emit_missing_field("foo"); + + let err = emitter.finish().unwrap_err(); + + assert_eq!(format!("{err}"), "missing field: `foo`") + } + + #[test] + fn multiple_missing_fields() { + let mut emitter: Emitter = Emitter::new(); + + emitter.emit_missing_field("foo"); + emitter.emit_missing_field("bar"); + + let err = emitter.finish().unwrap_err(); + + assert_eq!( + format!("{err}"), + "missing field: `foo`\nmissing field: `bar`" + ) + } + + #[test] + fn merging_user_fields_overrides_old_value() { + let mut field = UserField(None); + field.merge(UserField(Some(4))); + assert_eq!(field, UserField(Some(4))); + + let mut field = UserField(Some(4)); + field.merge(UserField(Some(5))); + assert_eq!(field, UserField(Some(5))); - /// Abstraction over the actual implementation of how env variables are gotten - /// from the environment. Necessary for mocking in tests. - pub trait FetchEnv { - /// The signature of [`std::env::var`]. - /// - /// # Errors - /// - /// See errors of [`std::env::var`]. - fn fetch>(&self, key: K) -> Result; + let mut field = UserField(Some(4)); + field.merge(UserField(None)); + assert_eq!(field, UserField(Some(4))); } - /// Trait for configuration loading and deserialization from disk - pub trait LoadFromDisk: Sized { - /// The return type. Could be target `Configuration`, - /// some `Result`, `Option`, or any other type that - /// wraps a `..Proxy` or `Configuration` type. - type ReturnValue; + #[derive(Deserialize, Default)] + #[serde(default)] + struct TestExtends { + extends: Option, + } + + #[test] + fn parse_empty_extends() { + let value: TestExtends = toml::from_str("").expect("should be fine with empty input"); - /// Construct [`Self`] from a path-like object. - /// - /// # Errors - /// - File not found. - /// - File found, but peer configuration parsing failed. - fn from_path + Debug + Clone>(path: P) -> Self::ReturnValue; + assert_eq!(value.extends, None); } - /// Trait for building the final config from a proxy one - pub trait Builder { - /// The return type. Could be target `Configuration`, - /// some `Result`, `Option` as users see fit. - type ReturnValue; + #[test] + fn parse_single_extends_path() { + let value: TestExtends = toml::toml! { + extends = "./path" + } + .try_into() + .unwrap(); - /// Construct [`Self::ReturnValue`] from a proxy object. - fn build(self) -> Self::ReturnValue; + assert_eq!(value.extends, Some(ExtendsPaths::Single("./path".into()))); } - impl Builder for Box { - type ReturnValue = T::ReturnValue; + #[test] + fn parse_multiple_extends_paths() { + let value: TestExtends = toml::toml! { + extends = ["foo", "bar", "baz"] + } + .try_into() + .unwrap(); + + assert_eq!( + value.extends, + Some(ExtendsPaths::Chain(vec![ + "foo".into(), + "bar".into(), + "baz".into() + ])) + ); + } - fn build(self) -> Self::ReturnValue { - T::build(*self) + #[test] + fn iterating_over_extends() { + impl ExtendsPaths { + fn as_str_vec(&self) -> Vec<&str> { + self.iter().map(|p| p.to_str().unwrap()).collect() + } } + + let single = ExtendsPaths::Single("single".into()); + assert_eq!(single.as_str_vec(), vec!["single"]); + + let multi = ExtendsPaths::Chain(vec!["foo".into(), "bar".into(), "baz".into()]); + assert_eq!(multi.as_str_vec(), vec!["foo", "bar", "baz"]); } - /// Deserialization helper for proxy fields that wrap an `Option` - /// - /// # Errors - /// When deserialization of the field fails, e.g. it doesn't have - /// the `Option>` - #[allow(clippy::option_option)] - pub fn some_option<'de, T, D>(deserializer: D) -> Result>, D::Error> - where - T: Deserialize<'de>, - D: Deserializer<'de>, - { - Option::::deserialize(deserializer).map(Some) + #[test] + fn deserialize_human_duration() { + #[derive(Deserialize)] + struct Test { + value: HumanDuration, + } + + let Test { value } = toml::toml! { + value = 10_500 + } + .try_into() + .expect("input is fine, should parse"); + + assert_eq!(value.get(), Duration::from_millis(10_500)); } } diff --git a/config/iroha_test_config.json b/config/iroha_test_config.json deleted file mode 100644 index 53339579831..00000000000 --- a/config/iroha_test_config.json +++ /dev/null @@ -1,123 +0,0 @@ -{ - "CHAIN_ID": "00000000-0000-0000-0000-000000000000", - "PUBLIC_KEY": "ed01201C61FAF8FE94E253B93114240394F79A607B7FA55F9E5A41EBEC74B88055768B", - "PRIVATE_KEY": { - "digest_function": "ed25519", - "payload": "282ED9F3CF92811C3818DBC4AE594ED59DC1A2F78E4241E31924E101D6B1FB831C61FAF8FE94E253B93114240394F79A607B7FA55F9E5A41EBEC74B88055768B" - }, - "KURA": { - "INIT_MODE": "strict", - "BLOCK_STORE_PATH": "./storage", - "BLOCKS_PER_STORAGE_FILE": 1000, - "ACTOR_CHANNEL_CAPACITY": 100, - "DEBUG_OUTPUT_NEW_BLOCKS": false - }, - "SUMERAGI": { - "BLOCK_TIME_MS": 1000, - "TRUSTED_PEERS": [ - { - "address": "127.0.0.1:1337", - "public_key": "ed01201C61FAF8FE94E253B93114240394F79A607B7FA55F9E5A41EBEC74B88055768B" - }, - { - "address": "127.0.0.1:1338", - "public_key": "ed0120CC25624D62896D3A0BFD8940F928DC2ABF27CC57CEFEB442AA96D9081AAE58A1" - }, - { - "address": "127.0.0.1:1339", - "public_key": "ed0120FACA9E8AA83225CB4D16D67F27DD4F93FC30FFA11ADC1F5C88FD5495ECC91020" - }, - { - "address": "127.0.0.1:1340", - "public_key": "ed01208E351A70B6A603ED285D666B8D689B680865913BA03CE29FB7D13A166C4E7F1F" - } - ], - "COMMIT_TIME_LIMIT_MS": 2000, - "MAX_TRANSACTIONS_IN_BLOCK": 8192, - "ACTOR_CHANNEL_CAPACITY": 100, - "GOSSIP_BATCH_SIZE": 500, - "GOSSIP_PERIOD_MS": 1000, - "DEBUG_FORCE_SOFT_FORK": false - }, - "TORII": { - "P2P_ADDR": "127.0.0.1:1337", - "API_URL": "127.0.0.1:8080", - "MAX_TRANSACTION_SIZE": 32768, - "MAX_CONTENT_LEN": 16384000 - }, - "BLOCK_SYNC": { - "GOSSIP_PERIOD_MS": 10000, - "BLOCK_BATCH_SIZE": 4, - "ACTOR_CHANNEL_CAPACITY": 100 - }, - "QUEUE": { - "MAX_TRANSACTIONS_IN_QUEUE": 65536, - "MAX_TRANSACTIONS_IN_QUEUE_PER_USER": 65536, - "TRANSACTION_TIME_TO_LIVE_MS": 86400000, - "FUTURE_THRESHOLD_MS": 1000 - }, - "LOGGER": { - "LEVEL": "INFO", - "FORMAT": "full", - "TOKIO_CONSOLE_ADDR": "127.0.0.1:5555" - }, - "GENESIS": { - "PUBLIC_KEY": "ed01204CFFD0EE429B1BDD36B3910EC570852B8BB63F18750341772FB46BC856C5CAAF", - "PRIVATE_KEY": { - "digest_function": "ed25519", - "payload": "D748E18CE60CB30DEA3E73C9019B7AF45A8D465E3D71BCC9A5EF99A008205E534CFFD0EE429B1BDD36B3910EC570852B8BB63F18750341772FB46BC856C5CAAF" - }, - "WAIT_FOR_PEERS_RETRY_COUNT_LIMIT": 100, - "WAIT_FOR_PEERS_RETRY_PERIOD_MS": 500, - "GENESIS_SUBMISSION_DELAY_MS": 1000, - "FILE": "./genesis.json" - }, - "WSV": { - "ASSET_METADATA_LIMITS": { - "max_len": 1048576, - "max_entry_byte_size": 4096 - }, - "ASSET_DEFINITION_METADATA_LIMITS": { - "max_len": 1048576, - "max_entry_byte_size": 4096 - }, - "ACCOUNT_METADATA_LIMITS": { - "max_len": 1048576, - "max_entry_byte_size": 4096 - }, - "DOMAIN_METADATA_LIMITS": { - "max_len": 1048576, - "max_entry_byte_size": 4096 - }, - "IDENT_LENGTH_LIMITS": { - "min": 1, - "max": 128 - }, - "TRANSACTION_LIMITS": { - "max_instruction_number": 4096, - "max_wasm_size_bytes": 4194304 - }, - "WASM_RUNTIME_CONFIG": { - "FUEL_LIMIT": 1000000, - "MAX_MEMORY": 524288000 - } - }, - "NETWORK": { - "ACTOR_CHANNEL_CAPACITY": 100 - }, - "TELEMETRY": { - "NAME": null, - "URL": null, - "MIN_RETRY_PERIOD": 1, - "MAX_RETRY_DELAY_EXPONENT": 4, - "FILE": null - }, - "SNAPSHOT": { - "CREATE_EVERY_MS": 60000, - "DIR_PATH": "./storage", - "CREATION_ENABLED": true - }, - "LIVE_QUERY_STORE": { - "QUERY_IDLE_TIME_MS": 30000 - } -} diff --git a/config/iroha_test_config.toml b/config/iroha_test_config.toml new file mode 100644 index 00000000000..eaade1dbfe4 --- /dev/null +++ b/config/iroha_test_config.toml @@ -0,0 +1,34 @@ +chain_id = "00000000-0000-0000-0000-000000000000" +public_key = "ed01201C61FAF8FE94E253B93114240394F79A607B7FA55F9E5A41EBEC74B88055768B" +private_key = { digest_function = "ed25519", payload = "282ED9F3CF92811C3818DBC4AE594ED59DC1A2F78E4241E31924E101D6B1FB831C61FAF8FE94E253B93114240394F79A607B7FA55F9E5A41EBEC74B88055768B" } + +[network] +address = "127.0.0.1:1337" + +[genesis] +public_key = "ed01204CFFD0EE429B1BDD36B3910EC570852B8BB63F18750341772FB46BC856C5CAAF" +file = "./genesis.json" +private_key = { digest_function = "ed25519", payload = "D748E18CE60CB30DEA3E73C9019B7AF45A8D465E3D71BCC9A5EF99A008205E534CFFD0EE429B1BDD36B3910EC570852B8BB63F18750341772FB46BC856C5CAAF" } + +[torii] +address = "127.0.0.1:8080" + +[[sumeragi.trusted_peers]] +address = "127.0.0.1:1337" +public_key = "ed01201C61FAF8FE94E253B93114240394F79A607B7FA55F9E5A41EBEC74B88055768B" + +[[sumeragi.trusted_peers]] +address = "127.0.0.1:1338" +public_key = "ed0120CC25624D62896D3A0BFD8940F928DC2ABF27CC57CEFEB442AA96D9081AAE58A1" + +[[sumeragi.trusted_peers]] +address = "127.0.0.1:1339" +public_key = "ed0120FACA9E8AA83225CB4D16D67F27DD4F93FC30FFA11ADC1F5C88FD5495ECC91020" + +[[sumeragi.trusted_peers]] +address = "127.0.0.1:1340" +public_key = "ed01208E351A70B6A603ED285D666B8D689B680865913BA03CE29FB7D13A166C4E7F1F" + +[logger] +format = "pretty" + diff --git a/config/src/block_sync.rs b/config/src/block_sync.rs deleted file mode 100644 index dd927df3ece..00000000000 --- a/config/src/block_sync.rs +++ /dev/null @@ -1,50 +0,0 @@ -//! Module for `BlockSynchronizer`-related configuration and structs. -use iroha_config_base::derive::Proxy; -use serde::{Deserialize, Serialize}; - -const DEFAULT_BLOCK_BATCH_SIZE: u32 = 4; -const DEFAULT_GOSSIP_PERIOD_MS: u64 = 10000; -const DEFAULT_ACTOR_CHANNEL_CAPACITY: u32 = 100; - -/// Configuration for `BlockSynchronizer`. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, Proxy)] -#[serde(rename_all = "UPPERCASE")] -#[config(env_prefix = "BLOCK_SYNC_")] -pub struct Configuration { - /// The period of time to wait between sending requests for the latest block. - pub gossip_period_ms: u64, - /// The number of blocks that can be sent in one message. - /// Underlying network (`iroha_network`) should support transferring messages this large. - pub block_batch_size: u32, - /// Buffer capacity of actor's MPSC channel - pub actor_channel_capacity: u32, -} - -impl Default for ConfigurationProxy { - fn default() -> Self { - Self { - gossip_period_ms: Some(DEFAULT_GOSSIP_PERIOD_MS), - block_batch_size: Some(DEFAULT_BLOCK_BATCH_SIZE), - actor_channel_capacity: Some(DEFAULT_ACTOR_CHANNEL_CAPACITY), - } - } -} - -#[cfg(test)] -pub mod tests { - use proptest::prelude::*; - - use super::*; - - prop_compose! { - pub fn arb_proxy() - ( - gossip_period_ms in prop::option::of(Just(DEFAULT_GOSSIP_PERIOD_MS)), - block_batch_size in prop::option::of(Just(DEFAULT_BLOCK_BATCH_SIZE)), - actor_channel_capacity in prop::option::of(Just(DEFAULT_ACTOR_CHANNEL_CAPACITY)), - ) - -> ConfigurationProxy { - ConfigurationProxy { gossip_period_ms, block_batch_size, actor_channel_capacity } - } - } -} diff --git a/config/src/client.rs b/config/src/client.rs deleted file mode 100644 index bdf559ddcda..00000000000 --- a/config/src/client.rs +++ /dev/null @@ -1,236 +0,0 @@ -//! Module for client-related configuration and structs -use core::str::FromStr; -use std::num::NonZeroU64; - -use derive_more::Display; -use eyre::{Result, WrapErr}; -use iroha_config_base::derive::{Error as ConfigError, Proxy}; -use iroha_crypto::prelude::*; -use iroha_data_model::{prelude::*, ChainId}; -use iroha_primitives::small::SmallStr; -use serde::{Deserialize, Serialize}; -use url::Url; - -#[allow(unsafe_code)] -const DEFAULT_TRANSACTION_TIME_TO_LIVE_MS: NonZeroU64 = - unsafe { NonZeroU64::new_unchecked(100_000) }; -const DEFAULT_TRANSACTION_STATUS_TIMEOUT_MS: u64 = 15_000; -const DEFAULT_ADD_TRANSACTION_NONCE: bool = false; - -/// Wrapper over `SmallStr` to provide basic auth login checking -#[derive(Debug, Display, Clone, Serialize, PartialEq, Eq)] -pub struct WebLogin(SmallStr); - -impl WebLogin { - /// Construct new [`Self`] - /// - /// # Errors - /// Fails if `login` contains `:` character, which is the binary representation of the '\0'. - pub fn new(login: &str) -> Result { - Self::from_str(login) - } -} - -impl FromStr for WebLogin { - type Err = eyre::ErrReport; - fn from_str(login: &str) -> Result { - if login.contains(':') { - eyre::bail!("The `:` character, in `{login}` is not allowed"); - } - - Ok(Self(SmallStr::from_str(login))) - } -} - -/// Deserializing `WebLogin` with `FromStr` implementation -impl<'de> Deserialize<'de> for WebLogin { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - let s = String::deserialize(deserializer)?; - FromStr::from_str(&s).map_err(serde::de::Error::custom) - } -} - -/// Basic Authentication credentials -#[derive(Clone, Deserialize, Serialize, Debug, PartialEq, Eq)] -pub struct BasicAuth { - /// Login for Basic Authentication - pub web_login: WebLogin, - /// Password for Basic Authentication - pub password: SmallStr, -} - -/// `Configuration` provides an ability to define client parameters such as `TORII_URL`. -#[derive(Debug, Clone, Deserialize, Serialize, Proxy, PartialEq, Eq)] -#[serde(rename_all = "UPPERCASE")] -#[config(env_prefix = "IROHA_")] -pub struct Configuration { - /// Unique id of the blockchain. Used for simple replay attack protection. - pub chain_id: ChainId, - /// Public key of the user account. - #[config(serde_as_str)] - pub public_key: PublicKey, - /// Private key of the user account. - pub private_key: PrivateKey, - /// User account id. - pub account_id: AccountId, - /// Basic Authentication credentials - pub basic_auth: Option, - /// Torii URL. - pub torii_api_url: Url, - /// Proposed transaction TTL in milliseconds. - pub transaction_time_to_live_ms: Option, - /// Transaction status wait timeout in milliseconds. - pub transaction_status_timeout_ms: u64, - /// If `true` add nonce, which make different hashes for transactions which occur repeatedly and simultaneously - pub add_transaction_nonce: bool, -} - -impl Default for ConfigurationProxy { - fn default() -> Self { - Self { - chain_id: None, - public_key: None, - private_key: None, - account_id: None, - basic_auth: Some(None), - torii_api_url: None, - transaction_time_to_live_ms: Some(Some(DEFAULT_TRANSACTION_TIME_TO_LIVE_MS)), - transaction_status_timeout_ms: Some(DEFAULT_TRANSACTION_STATUS_TIMEOUT_MS), - add_transaction_nonce: Some(DEFAULT_ADD_TRANSACTION_NONCE), - } - } -} - -// TODO: explain why these values were chosen. -const TTL_TOO_SMALL_THRESHOLD: u64 = 500; - -impl ConfigurationProxy { - /// Finalise Iroha client config proxy by checking that certain fields identify reasonable limits or - /// are well formatted. - /// - /// # Errors - /// - If the [`self.transaction_time_to_live_ms`] field is too small - /// - If the [`self.transaction_status_timeout_ms`] field is smaller than [`self.transaction_time_to_live_ms`] - /// - If the [`self.torii_api_url`] is malformed or had the wrong protocol - pub fn finish(&mut self) -> Result<()> { - if let Some(Some(tx_ttl)) = self.transaction_time_to_live_ms { - // Really small TTL would be detrimental to performance - if u64::from(tx_ttl) < TTL_TOO_SMALL_THRESHOLD { - eyre::bail!(ConfigError::InsaneValue { - field: "TRANSACTION_TIME_TO_LIVE_MS", - value: tx_ttl.to_string(), - message: format!(", because if it's smaller than {TTL_TOO_SMALL_THRESHOLD}, Iroha wouldn't be able to produce blocks on time.") - }); - } - // Timeouts bigger than transaction TTL don't make sense as then transaction would be discarded before this timeout - if let Some(timeout) = self.transaction_status_timeout_ms { - if timeout > tx_ttl.into() { - eyre::bail!(ConfigError::InsaneValue { - field: "TRANSACTION_STATUS_TIMEOUT_MS", - value: timeout.to_string(), - message: format!(", because it should be smaller than `TRANSACTION_TIME_TO_LIVE_MS`, which is {tx_ttl}") - }) - } - } - } - if let Some(api_url) = &self.torii_api_url { - if api_url.scheme() != "http" { - eyre::bail!(ConfigError::InsaneValue { - field: "TORII_API_URL", - value: api_url.to_string(), - message: ", because we only support HTTP".to_owned(), - }); - } - } - Ok(()) - } - - /// The wrapper around the client `ConfigurationProxy` that performs - /// finalisation prior to building `Configuration`. Just like - /// Iroha peer config, its `::build()` - /// method should never be used directly, as only this wrapper ensures final - /// coherence and fails if there are any issues. - /// - /// # Errors - /// - Finalisation fails - /// - Building fails, e.g. any of the inner fields had a `None` value when that - /// is not allowed by the defaults. - pub fn build(mut self) -> Result { - self.finish()?; - ::build(self) - .wrap_err("Failed to build `Configuration` from `ConfigurationProxy`") - } -} - -#[cfg(test)] -mod tests { - use iroha_config_base::proxy::LoadFromDisk; - use iroha_crypto::KeyGenConfiguration; - use proptest::prelude::*; - - use super::*; - use crate::torii::uri::DEFAULT_API_ADDR; - - const CONFIGURATION_PATH: &str = "../configs/client/config.json"; - - prop_compose! { - // TODO: make tests to check generated key validity - fn arb_keys_from_seed() - (seed in prop::collection::vec(any::(), 33..64)) -> (PublicKey, PrivateKey) { - let (public_key, private_key) = KeyPair::generate_with_configuration(KeyGenConfiguration::from_seed(seed)).expect("Seed was invalid").into(); - (public_key, private_key) - } - } - - prop_compose! { - fn arb_keys_with_option() - (keys in arb_keys_from_seed()) - ((a, b) in (prop::option::of(Just(keys.0)), prop::option::of(Just(keys.1)))) - -> (Option, Option) { - (a, b) - } - } - - fn placeholder_account() -> AccountId { - AccountId::from_str("alice@wonderland").expect("Invalid account Id ") - } - - prop_compose! { - fn arb_proxy() - ( - chain_id in prop::option::of(Just(crate::iroha::tests::placeholder_chain_id())), - (public_key, private_key) in arb_keys_with_option(), - account_id in prop::option::of(Just(placeholder_account())), - basic_auth in prop::option::of(Just(None)), - torii_api_url in prop::option::of(Just(format!("http://{DEFAULT_API_ADDR}").parse().unwrap())), - transaction_time_to_live_ms in prop::option::of(Just(Some(DEFAULT_TRANSACTION_TIME_TO_LIVE_MS))), - transaction_status_timeout_ms in prop::option::of(Just(DEFAULT_TRANSACTION_STATUS_TIMEOUT_MS)), - add_transaction_nonce in prop::option::of(Just(DEFAULT_ADD_TRANSACTION_NONCE)), - ) - -> ConfigurationProxy { - ConfigurationProxy { chain_id, public_key, private_key, account_id, basic_auth, torii_api_url, transaction_time_to_live_ms, transaction_status_timeout_ms, add_transaction_nonce } - } - } - - proptest! { - #[test] - fn client_proxy_build_fails_on_none(proxy in arb_proxy()) { - let cfg = proxy.build(); - if cfg.is_ok() { - let example_cfg = ConfigurationProxy::from_path(CONFIGURATION_PATH).build().expect("Failed to build example Iroha config. \ - This probably means that some of the fields of the `CONFIGURATION PATH` \ - JSON were not updated properly with new changes."); - let arb_cfg = cfg.expect("Config generated by proptest was checked to be ok by the surrounding if clause"); - // Skipping keys and `basic_auth` check as they're different from the file - assert_eq!(arb_cfg.torii_api_url, example_cfg.torii_api_url); - assert_eq!(arb_cfg.account_id, example_cfg.account_id); - assert_eq!(arb_cfg.transaction_time_to_live_ms, example_cfg.transaction_time_to_live_ms); - assert_eq!(arb_cfg.transaction_status_timeout_ms, example_cfg.transaction_status_timeout_ms); - assert_eq!(arb_cfg.add_transaction_nonce, example_cfg.add_transaction_nonce); - } - } - } -} diff --git a/config/src/client_api.rs b/config/src/client_api.rs index 030edb8523a..f87bc5b7a41 100644 --- a/config/src/client_api.rs +++ b/config/src/client_api.rs @@ -2,8 +2,8 @@ //! //! Intended usage: //! -//! - Create [`ConfigurationDTO`] from [`crate::iroha::Configuration`] and serialize it for the client -//! - Deserialize [`ConfigurationDTO`] from the client and use [`ConfigurationDTO::apply_update()`] to update the configuration +//! - Create [`ConfigDTO`] from [`crate::iroha::Configuration`] and serialize it for the client +//! - Deserialize [`ConfigDTO`] from the client and use [`ConfigDTO::apply_update()`] to update the configuration // TODO: Currently logic here is not generalised and handles only `logger.level` parameter. In future, when // other parts of configuration are refactored and there is a solid foundation e.g. as a general // configuration-related crate, this part should be re-written in a clean way. @@ -12,19 +12,19 @@ use iroha_data_model::Level; use serde::{Deserialize, Serialize}; -use super::{iroha::Configuration as BaseConfiguration, logger::Configuration as BaseLogger}; +use crate::parameters::actual::{Logger as BaseLogger, Root as BaseConfig}; /// Subset of [`super::iroha`] configuration. #[derive(Debug, Serialize, Deserialize, Clone, Copy)] -pub struct ConfigurationDTO { +pub struct ConfigDTO { #[allow(missing_docs)] pub logger: Logger, } -impl From<&'_ BaseConfiguration> for ConfigurationDTO { - fn from(value: &'_ BaseConfiguration) -> Self { +impl From<&'_ BaseConfig> for ConfigDTO { + fn from(value: &'_ BaseConfig) -> Self { Self { - logger: value.logger.as_ref().into(), + logger: (&value.logger).into(), } } } @@ -48,7 +48,7 @@ mod test { #[test] fn snapshot_serialized_form() { - let value = ConfigurationDTO { + let value = ConfigDTO { logger: Logger { level: Level::TRACE, }, diff --git a/config/src/genesis.rs b/config/src/genesis.rs deleted file mode 100644 index b6881ac4d65..00000000000 --- a/config/src/genesis.rs +++ /dev/null @@ -1,141 +0,0 @@ -//! Module with genesis configuration logic. -use std::path::PathBuf; - -use eyre::Report; -use iroha_config_base::derive::{view, Proxy}; -use iroha_crypto::{KeyPair, PrivateKey, PublicKey}; -use iroha_genesis::RawGenesisBlock; -use serde::{Deserialize, Serialize}; - -// Generate `ConfigurationView` without the private key -view! { - /// Configuration of the genesis block and the process of its submission. - #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Proxy)] - #[serde(rename_all = "UPPERCASE")] - #[config(env_prefix = "IROHA_GENESIS_")] - pub struct Configuration { - /// The public key of the genesis account, should be supplied to all peers. - #[config(serde_as_str)] - pub public_key: PublicKey, - /// The private key of the genesis account, only needed for the peer that submits the genesis block. - #[view(ignore)] - pub private_key: Option, - /// Path to the genesis file - #[config(serde_as_str)] - pub file: Option - } -} - -impl Default for ConfigurationProxy { - fn default() -> Self { - Self { - public_key: None, - private_key: Some(None), - file: None, - } - } -} - -/// Parsed variant of the user-provided [`Configuration`] -// TODO: incorporate this struct into the final, parsed configuration -// https://github.com/hyperledger/iroha/issues/3500 -pub enum ParsedConfiguration { - /// The peer can only observe the genesis block - Partial { - /// Genesis account public key - public_key: PublicKey, - }, - /// The peer is responsible for submitting the genesis block - Full { - /// Genesis account key pair - key_pair: KeyPair, - /// Raw genesis block - raw_block: RawGenesisBlock, - }, -} - -impl Configuration { - /// Parses user configuration into a stronger-typed structure [`ParsedConfiguration`] - /// - /// # Errors - /// See [`ParseError`] - pub fn parse(self, submit: bool) -> Result { - match (self.private_key, self.file, submit) { - (None, None, false) => Ok(ParsedConfiguration::Partial { - public_key: self.public_key, - }), - (Some(private_key), Some(path), true) => { - let raw_block = RawGenesisBlock::from_path(&path) - .map_err(|report| ParseError::File { path, report })?; - - Ok(ParsedConfiguration::Full { - key_pair: KeyPair::new(self.public_key, private_key)?, - raw_block, - }) - } - (_, _, true) => Err(ParseError::SubmitIsSetButRestAreNot), - (_, _, false) => Err(ParseError::SubmitIsNotSetButRestAre), - } - } -} - -/// Error which might occur during [`Configuration::parse()`] -#[derive(Debug, displaydoc::Display, thiserror::Error)] -pub enum ParseError { - /// `--submit-genesis` was provided, but `genesis.private_key` and/or `genesis.file` are missing - SubmitIsSetButRestAreNot, - /// `--submit-genesis` was not provided, but `genesis.private_key` and/or `genesis.file` are set - SubmitIsNotSetButRestAre, - /// Genesis key pair is invalid - InvalidKeyPair(#[from] iroha_crypto::error::Error), - /// Cannot read the genesis block from file `{path}` - File { - /// Original error report - #[source] - report: Report, - /// Path to the file - path: PathBuf, - }, -} - -#[cfg(test)] -pub mod tests { - use iroha_crypto::KeyPair; - use proptest::prelude::*; - - use super::*; - - /// Key-pair used by default for test purposes - fn placeholder_keypair() -> KeyPair { - let public_key = "ed01204CFFD0EE429B1BDD36B3910EC570852B8BB63F18750341772FB46BC856C5CAAF" - .parse() - .expect("Public key not in multihash format"); - let private_key = PrivateKey::from_hex( - iroha_crypto::Algorithm::Ed25519, - "D748E18CE60CB30DEA3E73C9019B7AF45A8D465E3D71BCC9A5EF99A008205E534CFFD0EE429B1BDD36B3910EC570852B8BB63F18750341772FB46BC856C5CAAF" - ).expect("Private key not hex encoded"); - - KeyPair::new(public_key, private_key).expect("Key pair mismatch") - } - - #[allow(clippy::option_option)] - fn arb_keys() -> BoxedStrategy<(Option, Option>)> { - let (pub_key, _) = placeholder_keypair().into(); - ( - prop::option::of(Just(pub_key)), - prop::option::of(Just(None)), - ) - .boxed() - } - - prop_compose! { - pub fn arb_proxy() - ( - (public_key, private_key) in arb_keys(), - file in prop::option::of(Just(None)) - ) - -> ConfigurationProxy { - ConfigurationProxy { public_key, private_key, file } - } - } -} diff --git a/config/src/iroha.rs b/config/src/iroha.rs deleted file mode 100644 index ac33c9a2f32..00000000000 --- a/config/src/iroha.rs +++ /dev/null @@ -1,282 +0,0 @@ -//! This module contains [`struct@Configuration`] structure and related implementation. -use std::fmt::Debug; - -use iroha_config_base::derive::{view, Error as ConfigError, Proxy}; -use iroha_crypto::prelude::*; -use iroha_data_model::ChainId; -use serde::{Deserialize, Serialize}; - -use super::*; - -// Generate `ConfigurationView` without the private key -view! { - /// Configuration parameters for a peer - #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Proxy)] - #[serde(rename_all = "UPPERCASE")] - #[config(env_prefix = "IROHA_")] - pub struct Configuration { - /// Unique id of the blockchain. Used for simple replay attack protection. - #[config(serde_as_str)] - pub chain_id: ChainId, - /// Public key of this peer - #[config(serde_as_str)] - pub public_key: PublicKey, - /// Private key of this peer - #[view(ignore)] - pub private_key: PrivateKey, - /// `Kura` configuration - #[config(inner)] - pub kura: Box, - /// `Sumeragi` configuration - #[config(inner)] - #[view(into = Box)] - pub sumeragi: Box, - /// `Torii` configuration - #[config(inner)] - pub torii: Box, - /// `BlockSynchronizer` configuration - #[config(inner)] - pub block_sync: block_sync::Configuration, - /// `Queue` configuration - #[config(inner)] - pub queue: queue::Configuration, - /// `Logger` configuration - #[config(inner)] - pub logger: Box, - /// `GenesisBlock` configuration - #[config(inner)] - #[view(into = Box)] - pub genesis: Box, - /// `WorldStateView` configuration - #[config(inner)] - pub wsv: Box, - /// Network configuration - #[config(inner)] - pub network: network::Configuration, - /// Telemetry configuration - #[config(inner)] - pub telemetry: Box, - /// SnapshotMaker configuration - #[config(inner)] - pub snapshot: Box, - /// LiveQueryStore configuration - #[config(inner)] - pub live_query_store: live_query_store::Configuration, - } -} - -impl Default for ConfigurationProxy { - fn default() -> Self { - Self { - chain_id: None, - public_key: None, - private_key: None, - kura: Some(Box::default()), - sumeragi: Some(Box::default()), - torii: Some(Box::default()), - block_sync: Some(block_sync::ConfigurationProxy::default()), - queue: Some(queue::ConfigurationProxy::default()), - logger: Some(Box::default()), - genesis: Some(Box::default()), - wsv: Some(Box::default()), - network: Some(network::ConfigurationProxy::default()), - telemetry: Some(Box::default()), - snapshot: Some(Box::default()), - live_query_store: Some(live_query_store::ConfigurationProxy::default()), - } - } -} - -impl ConfigurationProxy { - /// Finalise Iroha config proxy by instantiating mutually equivalent fields - /// via the uppermost Iroha config fields. Configuration fields provided in the - /// Iroha config always overwrite those in sumeragi even in case of discrepancy, - /// so proper care is advised. - /// - /// # Errors - /// - If the relevant uppermost Iroha config fields were not provided. - pub fn finish(&mut self) -> Result<(), ConfigError> { - if let Some(sumeragi_proxy) = &mut self.sumeragi { - // First, iroha public/private key and sumeragi keypair are interchangeable, but - // the user is allowed to provide only the former, and keypair is generated automatically, - // bailing out if key_pair provided in sumeragi no matter its value - if sumeragi_proxy.key_pair.is_some() { - return Err(ConfigError::ProvidedInferredField { - field: "key_pair", - message: "Sumeragi should not be provided with `KEY_PAIR` directly. That value is computed from the other config parameters. Please set the `KEY_PAIR` to `null` or omit entirely." - }); - } - if let (Some(public_key), Some(private_key)) = (&self.public_key, &self.private_key) { - sumeragi_proxy.key_pair = - Some(KeyPair::new(public_key.clone(), private_key.clone())?); - } else { - return Err(ConfigError::MissingField { - field: "PUBLIC_KEY and PRIVATE_KEY", - message: "The sumeragi keypair is not provided in the example configuration. It's done this way to ensure you don't re-use the example keys in production, and know how to generate new keys. Please have a look at \n\nhttps://hyperledger.github.io/iroha-2-docs/guide/configure/keys.html\n\nto learn more.\n\n-----", - }); - } - // Second, torii gateway and sumeragi peer id are interchangeable too; the latter is derived from the - // former and overwritten silently in case of difference - if let Some(torii_proxy) = &mut self.torii { - if sumeragi_proxy.peer_id.is_none() { - sumeragi_proxy.peer_id = Some(iroha_data_model::prelude::PeerId::new( - torii_proxy - .p2p_addr - .clone() - .ok_or(ConfigError::MissingField { - field: "p2p_addr", - message: - "`p2p_addr` should not be set to `null` or `None` explicitly.", - })?, - self.public_key.clone().expect( - "Iroha `public_key` should have been initialized above at the latest", - ), - )); - } else { - // TODO: should we just warn the user that this value will be ignored? - // TODO: Consider eliminating this value from the public API. - return Err(ConfigError::ProvidedInferredField { - field: "PEER_ID", - message: "The `peer_id` is computed from the key and address. You should remove it from the config.", - }); - } - } else { - return Err(ConfigError::MissingField{ - field: "p2p_addr", - message: "Torii config should have at least `p2p_addr` provided for sumeragi finalisation", - }); - } - - sumeragi_proxy.insert_self_as_trusted_peers() - } - - Ok(()) - } - - /// The wrapper around the topmost Iroha `ConfigurationProxy` - /// that performs finalisation prior to building. For the uppermost - /// Iroha config, its `::build()` - /// method should never be used directly, as only this wrapper ensures final - /// coherence. - /// - /// # Errors - /// - Finalisation fails - /// - Building fails, e.g. any of the inner fields had a `None` value when that - /// is not allowed by the defaults. - pub fn build(mut self) -> Result { - self.finish()?; - ::build(self) - } -} - -#[cfg(test)] -pub mod tests { - use std::path::PathBuf; - - use proptest::prelude::*; - - use super::*; - use crate::{base::proxy::LoadFromDisk, sumeragi::TrustedPeers}; - - const CONFIGURATION_PATH: &str = "./iroha_test_config.json"; - - /// Key-pair used for proptests generation - pub fn placeholder_keypair() -> KeyPair { - let private_key = PrivateKey::from_hex( - Algorithm::Ed25519, - "282ED9F3CF92811C3818DBC4AE594ED59DC1A2F78E4241E31924E101D6B1FB831C61FAF8FE94E253B93114240394F79A607B7FA55F9E5A41EBEC74B88055768B" - ).expect("Private key not hex encoded"); - - KeyPair::new( - "ed01201C61FAF8FE94E253B93114240394F79A607B7FA55F9E5A41EBEC74B88055768B" - .parse() - .expect("Public key not in mulithash format"), - private_key, - ) - .expect("Key pair mismatch") - } - - fn arb_keys() -> BoxedStrategy<(Option, Option)> { - let (pub_key, priv_key) = placeholder_keypair().into(); - ( - prop::option::of(Just(pub_key)), - prop::option::of(Just(priv_key)), - ) - .boxed() - } - - pub fn placeholder_chain_id() -> ChainId { - ChainId::new("0") - } - - prop_compose! { - fn arb_proxy()( - chain_id in prop::option::of(Just(placeholder_chain_id())), - (public_key, private_key) in arb_keys(), - kura in prop::option::of(kura::tests::arb_proxy().prop_map(Box::new)), - sumeragi in (prop::option::of(sumeragi::tests::arb_proxy().prop_map(Box::new))), - torii in (prop::option::of(torii::tests::arb_proxy().prop_map(Box::new))), - block_sync in prop::option::of(block_sync::tests::arb_proxy()), - queue in prop::option::of(queue::tests::arb_proxy()), - logger in prop::option::of(logger::tests::arb_proxy().prop_map(Box::new)), - genesis in prop::option::of(genesis::tests::arb_proxy().prop_map(Box::new)), - wsv in prop::option::of(wsv::tests::arb_proxy().prop_map(Box::new)), - network in prop::option::of(network::tests::arb_proxy()), - telemetry in prop::option::of(telemetry::tests::arb_proxy().prop_map(Box::new)), - snapshot in prop::option::of(snapshot::tests::arb_proxy().prop_map(Box::new)), - live_query_store in prop::option::of(live_query_store::tests::arb_proxy()), - ) -> ConfigurationProxy { - ConfigurationProxy { chain_id, public_key, private_key, kura, sumeragi, torii, block_sync, queue, - logger, genesis, wsv, network, telemetry, snapshot, live_query_store } - } - } - - proptest! { - fn __iroha_proxy_build_fails_on_none(proxy in arb_proxy()) { - let cfg = proxy.build(); - let example_cfg = ConfigurationProxy::from_path(CONFIGURATION_PATH).build().expect("Failed to build example Iroha config"); - if cfg.is_ok() { - assert_eq!(cfg.unwrap(), example_cfg) - } - } - } - - #[test] - fn iroha_proxy_build_fails_on_none() { - // Using `stacker` because test generated by `proptest!` takes too much stack space. - // Allocating 3MB. - stacker::grow(3 * 1024 * 1024, __iroha_proxy_build_fails_on_none) - } - - #[test] - fn parse_example_json() { - let cfg_proxy = ConfigurationProxy::from_path(CONFIGURATION_PATH); - assert_eq!( - PathBuf::from("./storage"), - cfg_proxy.kura.unwrap().block_store_path.unwrap() - ); - assert_eq!( - 10000, - cfg_proxy - .block_sync - .expect("Block sync configuration was None") - .gossip_period_ms - .expect("Gossip period was None") - ); - } - - #[test] - fn example_json_proxy_builds() { - ConfigurationProxy::from_path(CONFIGURATION_PATH).build().unwrap_or_else(|err| panic!("`ConfigurationProxy` specified in {CONFIGURATION_PATH} \ - failed to build. This probably means that some of the fields there were not updated \ - properly with new changes. Error: {err}")); - } - - #[test] - #[should_panic(expected = "Failed to parse Trusted Peers")] - fn parse_trusted_peers_fail_duplicate_peer_id() { - let trusted_peers_string = r#"[{"address":"127.0.0.1:1337", "public_key": "ed0120954C83A4220FAFFB2C1D23FC5225B3E7952D53ACBB2A065FF30C631E5E1D6B10"}, {"address":"127.0.0.1:1337", "public_key": "ed0120954C83A4220FAFFB2C1D23FC5225B3E7952D53ACBB2A065FF30C631E5E1D6B10"}, {"address":"localhost:1338", "public_key": "ed0120954C83A4220FAFFB2C1D23FC5225B3E7952D53ACBB2A065FF30C631E5E1D6B10"}, {"address": "195.162.0.1:23", "public_key": "ed0120954C83A4220FAFFB2C1D23FC5225B3E7952D53ACBB2A065FF30C631E5E1D6B10"}]"#; - let _result: TrustedPeers = - serde_json::from_str(trusted_peers_string).expect("Failed to parse Trusted Peers"); - } -} diff --git a/config/src/kura.rs b/config/src/kura.rs index 8f97dbbf94b..507e44db3da 100644 --- a/config/src/kura.rs +++ b/config/src/kura.rs @@ -1,40 +1,23 @@ -//! Module for kura-related configuration and structs +//! Configuration tools related to Kura specifically. -use std::path::PathBuf; +// use iroha_config_base::{impl_deserialize_from_str, impl_serialize_display}; -use eyre::Result; -use iroha_config_base::derive::Proxy; -use serde::{Deserialize, Serialize}; - -const DEFAULT_BLOCK_STORE_PATH: &str = "./storage"; - -/// `Kura` configuration. -#[derive(Clone, Deserialize, Serialize, Debug, Proxy, PartialEq, Eq)] -#[serde(rename_all = "UPPERCASE")] -#[config(env_prefix = "KURA_")] -pub struct Configuration { - /// Initialization mode: `strict` or `fast`. - pub init_mode: Mode, - /// Path to the existing block store folder or path to create new folder. - #[config(serde_as_str)] - pub block_store_path: PathBuf, - /// Whether or not new blocks be outputted to a file called blocks.json. - pub debug_output_new_blocks: bool, -} - -impl Default for ConfigurationProxy { - fn default() -> Self { - Self { - init_mode: Some(Mode::default()), - block_store_path: Some(DEFAULT_BLOCK_STORE_PATH.into()), - debug_output_new_blocks: Some(false), - } - } -} +use serde_with::{DeserializeFromStr, SerializeDisplay}; /// Kura initialization mode. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)] -#[serde(rename_all = "snake_case")] +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Eq, + Default, + strum::EnumString, + strum::Display, + DeserializeFromStr, + SerializeDisplay, +)] +#[strum(serialize_all = "snake_case")] pub enum Mode { /// Strict validation of all blocks. #[default] @@ -44,20 +27,14 @@ pub enum Mode { } #[cfg(test)] -pub mod tests { - use proptest::prelude::*; - - use super::*; - - prop_compose! { - pub fn arb_proxy() - ( - init_mode in prop::option::of(Just(Mode::default())), - block_store_path in prop::option::of(Just(DEFAULT_BLOCK_STORE_PATH.into())), - debug_output_new_blocks in prop::option::of(Just(false)) - ) - -> ConfigurationProxy { - ConfigurationProxy { init_mode, block_store_path, debug_output_new_blocks } - } +mod tests { + use crate::kura::Mode; + + #[test] + fn init_mode_display_reprs() { + assert_eq!(format!("{}", Mode::Strict), "strict"); + assert_eq!(format!("{}", Mode::Fast), "fast"); + assert_eq!("strict".parse::().unwrap(), Mode::Strict); + assert_eq!("fast".parse::().unwrap(), Mode::Fast); } } diff --git a/config/src/lib.rs b/config/src/lib.rs index 423e5a8dd19..1697443be46 100644 --- a/config/src/lib.rs +++ b/config/src/lib.rs @@ -1,20 +1,8 @@ -//! Aggregate configuration for different Iroha modules. +//! Iroha configuration and related utilities. + pub use iroha_config_base as base; -pub mod block_sync; -pub mod client; pub mod client_api; -pub mod genesis; -pub mod iroha; pub mod kura; -pub mod live_query_store; pub mod logger; -pub mod network; -pub mod path; -pub mod queue; -pub mod snapshot; -pub mod sumeragi; -pub mod telemetry; -pub mod torii; -pub mod wasm; -pub mod wsv; +pub mod parameters; diff --git a/config/src/live_query_store.rs b/config/src/live_query_store.rs deleted file mode 100644 index de8b2a31ec2..00000000000 --- a/config/src/live_query_store.rs +++ /dev/null @@ -1,44 +0,0 @@ -//! Module for `LiveQueryStore`-related configuration and structs. - -use std::num::NonZeroU64; - -use iroha_config_base::derive::Proxy; -use serde::{Deserialize, Serialize}; - -/// Default max time a query can remain in the store unaccessed -pub static DEFAULT_QUERY_IDLE_TIME_MS: once_cell::sync::Lazy = - once_cell::sync::Lazy::new(|| NonZeroU64::new(30_000).unwrap()); - -/// Configuration for `QueryService`. -#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize, Serialize, Proxy)] -#[serde(rename_all = "UPPERCASE")] -#[config(env_prefix = "LIVE_QUERY_STORE_")] -pub struct Configuration { - /// Time query can remain in the store if unaccessed - pub query_idle_time_ms: NonZeroU64, -} - -impl Default for ConfigurationProxy { - fn default() -> Self { - Self { - query_idle_time_ms: Some(*DEFAULT_QUERY_IDLE_TIME_MS), - } - } -} - -#[cfg(test)] -pub mod tests { - use proptest::prelude::*; - - use super::*; - - prop_compose! { - pub fn arb_proxy() - ( - query_idle_time_ms in prop::option::of(Just(*DEFAULT_QUERY_IDLE_TIME_MS)), - ) - -> ConfigurationProxy { - ConfigurationProxy { query_idle_time_ms } - } - } -} diff --git a/config/src/logger.rs b/config/src/logger.rs index 6d5e4e9d5e6..e5038337396 100644 --- a/config/src/logger.rs +++ b/config/src/logger.rs @@ -1,15 +1,7 @@ -//! Module containing logic related to spawning a logger from the -//! configuration, as well as run-time reloading of the log-level. -use core::fmt::Debug; +//! Configuration utils related to Logger specifically. -use iroha_config_base::derive::Proxy; pub use iroha_data_model::Level; -#[cfg(feature = "tokio-console")] -use iroha_primitives::addr::{socket_addr, SocketAddr}; -use serde::{Deserialize, Serialize}; - -#[cfg(feature = "tokio-console")] -const DEFAULT_TOKIO_CONSOLE_ADDR: SocketAddr = socket_addr!(127.0.0.1:5555); +use serde_with::{DeserializeFromStr, SerializeDisplay}; /// Convert [`Level`] into [`tracing::Level`] pub fn into_tracing_level(level: Level) -> tracing::Level { @@ -22,28 +14,23 @@ pub fn into_tracing_level(level: Level) -> tracing::Level { } } -/// 'Logger' configuration. -#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Proxy)] -#[serde(rename_all = "UPPERCASE")] -#[config(env_prefix = "LOG_")] -// `tokio_console_addr` is not `Copy`, but warning appears without `tokio-console` feature -#[allow(missing_copy_implementations)] -pub struct Configuration { - /// Level of logging verbosity - #[config(serde_as_str)] - pub level: Level, - /// Output format - pub format: Format, - #[cfg(feature = "tokio-console")] - /// Address of tokio console (only available under "tokio-console" feature) - pub tokio_console_addr: SocketAddr, -} - /// Reflects formatters in [`tracing_subscriber::fmt::format`] -#[derive(Debug, Copy, Clone, Eq, PartialEq, Deserialize, Serialize)] -#[serde(rename_all = "lowercase")] +#[derive( + Debug, + Copy, + Clone, + Eq, + PartialEq, + strum::Display, + strum::EnumString, + Default, + SerializeDisplay, + DeserializeFromStr, +)] +#[strum(serialize_all = "snake_case")] pub enum Format { /// See [`tracing_subscriber::fmt::format::Full`] + #[default] Full, /// See [`tracing_subscriber::fmt::format::Compact`] Compact, @@ -53,44 +40,9 @@ pub enum Format { Json, } -impl Default for Format { - fn default() -> Self { - Self::Full - } -} - -impl Default for ConfigurationProxy { - fn default() -> Self { - Self { - level: Some(Level::default()), - format: Some(Format::default()), - #[cfg(feature = "tokio-console")] - tokio_console_addr: Some(DEFAULT_TOKIO_CONSOLE_ADDR), - } - } -} - #[cfg(test)] pub mod tests { - use proptest::prelude::*; - - use super::*; - - #[must_use = "strategies do nothing unless used"] - pub fn arb_proxy() -> impl proptest::strategy::Strategy { - let strat = ( - (prop::option::of(Just(Level::default()))), - (prop::option::of(Just(Format::default()))), - #[cfg(feature = "tokio-console")] - (prop::option::of(Just(DEFAULT_TOKIO_CONSOLE_ADDR))), - ); - proptest::strategy::Strategy::prop_map(strat, move |strat| ConfigurationProxy { - level: strat.0, - format: strat.1, - #[cfg(feature = "tokio-console")] - tokio_console_addr: strat.2, - }) - } + use crate::logger::Format; #[test] fn serialize_pretty_format_in_lowercase() { diff --git a/config/src/network.rs b/config/src/network.rs deleted file mode 100644 index 845743fac42..00000000000 --- a/config/src/network.rs +++ /dev/null @@ -1,39 +0,0 @@ -//! Module for network-related configuration and structs -use iroha_config_base::derive::Proxy; -use serde::{Deserialize, Serialize}; - -const DEFAULT_ACTOR_CHANNEL_CAPACITY: u32 = 100; - -/// Network Configuration parameters -#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, Proxy)] -#[serde(rename_all = "UPPERCASE")] -#[config(env_prefix = "IROHA_NETWORK_")] -pub struct Configuration { - /// Buffer capacity of actor's MPSC channel - pub actor_channel_capacity: u32, -} - -impl Default for ConfigurationProxy { - fn default() -> Self { - Self { - actor_channel_capacity: Some(DEFAULT_ACTOR_CHANNEL_CAPACITY), - } - } -} - -#[cfg(test)] -pub mod tests { - use proptest::prelude::*; - - use super::*; - - prop_compose! { - pub fn arb_proxy() - ( - actor_channel_capacity in prop::option::of(Just(DEFAULT_ACTOR_CHANNEL_CAPACITY)), - ) - -> ConfigurationProxy { - ConfigurationProxy { actor_channel_capacity } - } - } -} diff --git a/config/src/parameters/actual.rs b/config/src/parameters/actual.rs new file mode 100644 index 00000000000..9a54da8e990 --- /dev/null +++ b/config/src/parameters/actual.rs @@ -0,0 +1,251 @@ +//! "Actual" layer of Iroha configuration parameters. It contains strongly-typed validated +//! structures in a way that is efficient for Iroha internally. + +use std::{ + num::NonZeroU32, + path::{Path, PathBuf}, + time::Duration, +}; + +use iroha_config_base::{FromEnv, StdEnv, UnwrapPartial}; +use iroha_crypto::{KeyPair, PublicKey}; +use iroha_data_model::{ + metadata::Limits as MetadataLimits, peer::PeerId, transaction::TransactionLimits, ChainId, + LengthLimits, +}; +use iroha_primitives::{addr::SocketAddr, unique_vec::UniqueVec}; +use serde::{Deserialize, Serialize}; +use url::Url; +pub use user::{Logger, Queue, Snapshot}; + +use crate::{ + kura::Mode, + parameters::{ + defaults, user, + user::{CliContext, RootPartial}, + }, +}; + +/// Parsed configuration root +#[derive(Debug, Clone)] +#[allow(missing_docs)] +pub struct Root { + pub common: Common, + pub genesis: Genesis, + pub torii: Torii, + pub kura: Kura, + pub sumeragi: Sumeragi, + pub block_sync: BlockSync, + pub transaction_gossiper: TransactionGossiper, + pub live_query_store: LiveQueryStore, + pub logger: Logger, + pub queue: Queue, + pub snapshot: Snapshot, + pub telemetry: Option, + pub dev_telemetry: Option, + pub chain_wide: ChainWide, +} + +impl Root { + /// Loads configuration from a file and environment variables + /// + /// # Errors + /// - unable to load config from a TOML file + /// - unable to parse config from envs + /// - the config is invalid + pub fn load>(path: Option

, cli: CliContext) -> Result { + let from_file = path.map(RootPartial::from_toml).transpose()?; + let from_env = RootPartial::from_env(&StdEnv)?; + let merged = match from_file { + Some(x) => x.merge(from_env), + None => from_env, + }; + let config = merged.unwrap_partial()?.parse(cli)?; + Ok(config) + } +} + +/// Common options shared between multiple places +#[allow(missing_docs)] +#[derive(Debug, Clone)] +pub struct Common { + pub chain_id: ChainId, + pub key_pair: KeyPair, + pub p2p_address: SocketAddr, +} + +impl Common { + /// Construct an id of this peer + pub fn peer_id(&self) -> PeerId { + PeerId::new(self.p2p_address.clone(), self.key_pair.public_key().clone()) + } +} + +/// Parsed genesis configuration +#[derive(Debug, Clone)] +pub enum Genesis { + /// The peer can only observe the genesis block + Partial { + /// Genesis account public key + public_key: PublicKey, + }, + /// The peer is responsible for submitting the genesis block + Full { + /// Genesis account key pair + key_pair: KeyPair, + /// Path to the [`RawGenesisBlock`] + file: PathBuf, + }, +} + +impl Genesis { + /// Access the public key, which is always present in the genesis config + pub fn public_key(&self) -> &PublicKey { + match self { + Self::Partial { public_key } => public_key, + Self::Full { key_pair, .. } => key_pair.public_key(), + } + } + + /// Access the key pair, if present + pub fn key_pair(&self) -> Option<&KeyPair> { + match self { + Self::Partial { .. } => None, + Self::Full { key_pair, .. } => Some(key_pair), + } + } +} + +#[allow(missing_docs)] +#[derive(Debug, Clone)] +pub struct Kura { + pub init_mode: Mode, + pub store_dir: PathBuf, + pub debug_output_new_blocks: bool, +} + +impl Default for Queue { + fn default() -> Self { + Self { + transaction_time_to_live: defaults::queue::DEFAULT_TRANSACTION_TIME_TO_LIVE, + future_threshold: defaults::queue::DEFAULT_FUTURE_THRESHOLD, + capacity: defaults::queue::DEFAULT_MAX_TRANSACTIONS_IN_QUEUE, + capacity_per_user: defaults::queue::DEFAULT_MAX_TRANSACTIONS_IN_QUEUE_PER_USER, + } + } +} + +#[derive(Debug, Clone)] +#[allow(missing_docs)] +pub struct Sumeragi { + pub trusted_peers: UniqueVec, + pub debug_force_soft_fork: bool, +} + +#[derive(Debug, Clone, Copy)] +#[allow(missing_docs)] +pub struct LiveQueryStore { + pub idle_time: Duration, +} + +impl Default for LiveQueryStore { + fn default() -> Self { + Self { + idle_time: defaults::torii::DEFAULT_QUERY_IDLE_TIME, + } + } +} + +#[allow(missing_docs)] +#[derive(Debug, Clone, Copy)] +pub struct BlockSync { + pub gossip_period: Duration, + pub gossip_max_size: NonZeroU32, +} + +#[derive(Debug, Clone, Copy)] +#[allow(missing_docs)] +pub struct TransactionGossiper { + pub gossip_period: Duration, + pub gossip_max_size: NonZeroU32, +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +#[allow(missing_docs)] +pub struct ChainWide { + pub max_transactions_in_block: NonZeroU32, + pub block_time: Duration, + pub commit_time: Duration, + pub transaction_limits: TransactionLimits, + pub asset_metadata_limits: MetadataLimits, + pub asset_definition_metadata_limits: MetadataLimits, + pub account_metadata_limits: MetadataLimits, + pub domain_metadata_limits: MetadataLimits, + pub ident_length_limits: LengthLimits, + pub wasm_runtime: WasmRuntime, +} + +impl ChainWide { + /// Calculate pipeline time based on the block time and commit time + pub fn pipeline_time(&self) -> Duration { + self.block_time + self.commit_time + } +} + +impl Default for ChainWide { + fn default() -> Self { + Self { + max_transactions_in_block: defaults::chain_wide::DEFAULT_MAX_TXS, + block_time: defaults::chain_wide::DEFAULT_BLOCK_TIME, + commit_time: defaults::chain_wide::DEFAULT_COMMIT_TIME, + transaction_limits: defaults::chain_wide::DEFAULT_TRANSACTION_LIMITS, + domain_metadata_limits: defaults::chain_wide::DEFAULT_METADATA_LIMITS, + account_metadata_limits: defaults::chain_wide::DEFAULT_METADATA_LIMITS, + asset_definition_metadata_limits: defaults::chain_wide::DEFAULT_METADATA_LIMITS, + asset_metadata_limits: defaults::chain_wide::DEFAULT_METADATA_LIMITS, + ident_length_limits: defaults::chain_wide::DEFAULT_IDENT_LENGTH_LIMITS, + wasm_runtime: WasmRuntime::default(), + } + } +} + +#[allow(missing_docs)] +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +pub struct WasmRuntime { + pub fuel_limit: u64, + // TODO: wrap into a `Bytes` newtype + pub max_memory_bytes: u32, +} + +impl Default for WasmRuntime { + fn default() -> Self { + Self { + fuel_limit: defaults::chain_wide::DEFAULT_WASM_FUEL_LIMIT, + max_memory_bytes: defaults::chain_wide::DEFAULT_WASM_MAX_MEMORY_BYTES, + } + } +} + +#[derive(Debug, Clone)] +#[allow(missing_docs)] +pub struct Torii { + pub address: SocketAddr, + pub max_content_len_bytes: u64, +} + +/// Complete configuration needed to start regular telemetry. +#[derive(Debug, Clone)] +#[allow(missing_docs)] +pub struct Telemetry { + pub name: String, + pub url: Url, + pub min_retry_period: Duration, + pub max_retry_delay_exponent: u8, +} + +/// Complete configuration needed to start dev telemetry. +#[derive(Debug, Clone)] +#[allow(missing_docs)] +pub struct DevTelemetry { + pub out_file: PathBuf, +} diff --git a/config/src/parameters/defaults.rs b/config/src/parameters/defaults.rs new file mode 100644 index 00000000000..a6f779d087b --- /dev/null +++ b/config/src/parameters/defaults.rs @@ -0,0 +1,104 @@ +//! Parameters default values + +// TODO: document if needed +#![allow(missing_docs)] + +use std::{ + num::{NonZeroU32, NonZeroUsize}, + time::Duration, +}; + +use iroha_data_model::{prelude::MetadataLimits, transaction::TransactionLimits, LengthLimits}; +use nonzero_ext::nonzero; + +pub mod queue { + use super::*; + + pub const DEFAULT_MAX_TRANSACTIONS_IN_QUEUE: NonZeroUsize = nonzero!(2_usize.pow(16)); + pub const DEFAULT_MAX_TRANSACTIONS_IN_QUEUE_PER_USER: NonZeroUsize = nonzero!(2_usize.pow(16)); + // 24 hours + pub const DEFAULT_TRANSACTION_TIME_TO_LIVE: Duration = Duration::from_secs(24 * 60 * 60); + pub const DEFAULT_FUTURE_THRESHOLD: Duration = Duration::from_secs(1); +} +pub mod kura { + pub const DEFAULT_STORE_DIR: &str = "./storage"; +} + +#[cfg(feature = "tokio-console")] +pub mod logger { + use iroha_primitives::addr::{socket_addr, SocketAddr}; + + pub const DEFAULT_TOKIO_CONSOLE_ADDR: SocketAddr = socket_addr!(127.0.0.1:5555); +} + +pub mod network { + use super::*; + + pub const DEFAULT_TRANSACTION_GOSSIP_PERIOD: Duration = Duration::from_secs(1); + + pub const DEFAULT_BLOCK_GOSSIP_PERIOD: Duration = Duration::from_secs(10); + + pub const DEFAULT_MAX_TRANSACTIONS_PER_GOSSIP: NonZeroU32 = nonzero!(500u32); + pub const DEFAULT_MAX_BLOCKS_PER_GOSSIP: NonZeroU32 = nonzero!(4u32); +} + +pub mod snapshot { + use super::*; + + pub const DEFAULT_STORE_DIR: &str = "./storage/snapshot"; + // Default frequency of making snapshots is 1 minute, need to be adjusted for larger world state view size + pub const DEFAULT_CREATE_EVERY: Duration = Duration::from_secs(60); + pub const DEFAULT_ENABLED: bool = true; +} + +pub mod chain_wide { + + use super::*; + + pub const DEFAULT_MAX_TXS: NonZeroU32 = nonzero!(2_u32.pow(9)); + pub const DEFAULT_BLOCK_TIME: Duration = Duration::from_secs(2); + pub const DEFAULT_COMMIT_TIME: Duration = Duration::from_secs(4); + pub const DEFAULT_WASM_FUEL_LIMIT: u64 = 55_000_000; + // TODO: wrap into a `Bytes` newtype + pub const DEFAULT_WASM_MAX_MEMORY_BYTES: u32 = 500 * 2_u32.pow(20); + + /// Default estimation of consensus duration. + pub const DEFAULT_CONSENSUS_ESTIMATION: Duration = + match DEFAULT_BLOCK_TIME.checked_add(match DEFAULT_COMMIT_TIME.checked_div(2) { + Some(x) => x, + None => unreachable!(), + }) { + Some(x) => x, + None => unreachable!(), + }; + + /// Default limits for metadata + pub const DEFAULT_METADATA_LIMITS: MetadataLimits = + MetadataLimits::new(2_u32.pow(20), 2_u32.pow(12)); + /// Default limits for ident length + pub const DEFAULT_IDENT_LENGTH_LIMITS: LengthLimits = LengthLimits::new(1, 2_u32.pow(7)); + /// Default maximum number of instructions and expressions per transaction + pub const DEFAULT_MAX_INSTRUCTION_NUMBER: u64 = 2_u64.pow(12); + /// Default maximum number of instructions and expressions per transaction + pub const DEFAULT_MAX_WASM_SIZE_BYTES: u64 = 4 * 2_u64.pow(20); + + /// Default transaction limits + pub const DEFAULT_TRANSACTION_LIMITS: TransactionLimits = + TransactionLimits::new(DEFAULT_MAX_INSTRUCTION_NUMBER, DEFAULT_MAX_WASM_SIZE_BYTES); +} + +pub mod torii { + use std::time::Duration; + + pub const DEFAULT_MAX_CONTENT_LENGTH: u64 = 2_u64.pow(20) * 16; + pub const DEFAULT_QUERY_IDLE_TIME: Duration = Duration::from_secs(30); +} + +pub mod telemetry { + use std::time::Duration; + + /// Default minimal retry period + pub const DEFAULT_MIN_RETRY_PERIOD: Duration = Duration::from_secs(1); + /// Default maximum exponent for the retry delay + pub const DEFAULT_MAX_RETRY_DELAY_EXPONENT: u8 = 4; +} diff --git a/config/src/parameters/mod.rs b/config/src/parameters/mod.rs new file mode 100644 index 00000000000..7a4e330ccc6 --- /dev/null +++ b/config/src/parameters/mod.rs @@ -0,0 +1,5 @@ +//! Iroha configuration parameters on different layers and their default values. + +pub mod actual; +pub mod defaults; +pub mod user; diff --git a/config/src/parameters/user.rs b/config/src/parameters/user.rs new file mode 100644 index 00000000000..57238af0aaf --- /dev/null +++ b/config/src/parameters/user.rs @@ -0,0 +1,704 @@ +//! User configuration view. Contains structures in a format that is +//! convenient from the user perspective. It is less strict and not necessarily valid upon +//! successful parsing of the user-provided content. +//! +//! It begins with [`Root`], containing sub-modules. Every structure has its `-Partial` +//! representation (e.g. [`RootPartial`]). + +// This module's usage is documented in high detail in the Configuration Reference +// (TODO link to docs) +#![allow(missing_docs)] + +use std::{ + error::Error, + fmt::Debug, + fs::File, + io::Read, + num::{NonZeroU32, NonZeroUsize}, + path::{Path, PathBuf}, + time::Duration, +}; + +pub use boilerplate::*; +use eyre::{eyre, Report, WrapErr}; +use iroha_config_base::{Emitter, ErrorsCollection, HumanBytes, Merge, ParseEnvResult, ReadEnv}; +use iroha_crypto::{KeyPair, PrivateKey, PublicKey}; +use iroha_data_model::{ + metadata::Limits as MetadataLimits, peer::PeerId, transaction::TransactionLimits, ChainId, + LengthLimits, Level, +}; +use iroha_primitives::{addr::SocketAddr, unique_vec::UniqueVec}; +use url::Url; + +use crate::{ + kura::Mode, + logger::Format, + parameters::{actual, defaults::telemetry::*}, +}; + +mod boilerplate; + +#[derive(Debug)] +pub struct Root { + chain_id: ChainId, + public_key: PublicKey, + private_key: PrivateKey, + genesis: Genesis, + kura: Kura, + sumeragi: Sumeragi, + network: Network, + logger: Logger, + queue: Queue, + snapshot: Snapshot, + telemetry: Telemetry, + torii: Torii, + chain_wide: ChainWide, +} + +impl RootPartial { + /// Read the partial from TOML file + /// + /// # Errors + /// - If file is not found, or not a valid TOML + /// - If failed to parse data into a layer + /// - If failed to read other configurations specified in `extends` + pub fn from_toml(path: impl AsRef) -> eyre::Result { + let contents = { + let mut file = File::open(path.as_ref()).wrap_err_with(|| { + eyre!("cannot open file at location `{}`", path.as_ref().display()) + })?; + let mut contents = String::new(); + file.read_to_string(&mut contents)?; + contents + }; + let mut layer: Self = toml::from_str(&contents).wrap_err("failed to parse toml")?; + + let base_path = path + .as_ref() + .parent() + .expect("the config file path could not be empty or root"); + + layer.normalise_paths(base_path); + + if let Some(paths) = layer.extends.take() { + let base = paths + .iter() + .try_fold(None, |acc: Option, extends_path| { + // extends path is not normalised relative to the config file yet + let full_path = base_path.join(extends_path); + + let base = Self::from_toml(&full_path) + .wrap_err_with(|| eyre!("cannot extend from `{}`", full_path.display()))?; + + match acc { + None => Ok::, Report>(Some(base)), + Some(other_base) => Ok(Some(other_base.merge(base))), + } + })?; + if let Some(base) = base { + layer = base.merge(layer) + }; + } + + Ok(layer) + } + + /// **Note:** this function doesn't affect `extends` + fn normalise_paths(&mut self, relative_to: impl AsRef) { + let path = relative_to.as_ref(); + + macro_rules! patch { + ($value:expr) => { + $value.as_mut().map(|x| { + *x = path.join(&x); + }) + }; + } + + patch!(self.genesis.file); + patch!(self.snapshot.store_dir); + patch!(self.kura.store_dir); + patch!(self.telemetry.dev.out_file); + } + + // FIXME workaround the inconvenient way `Merge::merge` works + #[must_use] + pub fn merge(mut self, other: Self) -> Self { + Merge::merge(&mut self, other); + self + } +} + +impl Root { + /// Parses user configuration view into the internal repr. + /// + /// # Errors + /// If any invalidity found. + pub fn parse(self, cli: CliContext) -> Result> { + let mut emitter = Emitter::new(); + + let key_pair = + KeyPair::new(self.public_key, self.private_key) + .wrap_err("failed to construct a key pair from `iroha.public_key` and `iroha.private_key` configuration parameters") + .map_or_else(|err| { + emitter.emit(err); + None + }, Some); + + let genesis = self.genesis.parse(cli).map_or_else( + |err| { + // FIXME + emitter.emit(eyre!("{err}")); + None + }, + Some, + ); + + let kura = self.kura.parse(); + + let sumeragi = self.sumeragi.parse().map_or_else( + |err| { + emitter.emit(err); + None + }, + Some, + ); + + if let Some(ref config) = sumeragi { + if !cli.submit_genesis && config.trusted_peers.len() == 0 { + emitter.emit(eyre!("\ + The network consists from this one peer only (no `sumeragi.trusted_peers` provided). \ + Since `--submit-genesis` is not set, there is no way to receive the genesis block. \ + Either provide the genesis by setting `--submit-genesis` argument, `genesis.private_key`, \ + and `genesis.file` configuration parameters, or increase the number of trusted peers in \ + the network using `sumeragi.trusted_peers` configuration parameter.\ + ")); + } + } + + let (p2p_address, block_sync, transaction_gossiper) = self.network.parse(); + + let logger = self.logger; + let queue = self.queue; + let snapshot = self.snapshot; + + let (torii, live_query_store) = self.torii.parse(); + + let telemetries = self.telemetry.parse().map_or_else( + |err| { + emitter.emit(err); + None + }, + Some, + ); + + let chain_wide = self.chain_wide.parse(); + + if p2p_address == torii.address { + emitter.emit(eyre!( + "`iroha.p2p_address` and `torii.address` should not be the same" + )) + } + + emitter.finish()?; + + let peer = actual::Common { + chain_id: self.chain_id, + key_pair: key_pair.unwrap(), + p2p_address, + }; + let (telemetry, dev_telemetry) = telemetries.unwrap(); + let genesis = genesis.unwrap(); + let sumeragi = { + let mut x = sumeragi.unwrap(); + x.trusted_peers.push(peer.peer_id()); + x + }; + + Ok(actual::Root { + common: peer, + genesis, + torii, + kura, + sumeragi, + block_sync, + transaction_gossiper, + live_query_store, + logger, + queue, + snapshot, + telemetry, + dev_telemetry, + chain_wide, + }) + } +} + +#[derive(Copy, Clone)] +pub struct CliContext { + pub submit_genesis: bool, +} + +pub(crate) fn private_key_from_env( + emitter: &mut Emitter, + env: &impl ReadEnv, + env_key_base: impl AsRef, + name_base: impl AsRef, +) -> ParseEnvResult { + let digest_env = format!("{}_DIGEST", env_key_base.as_ref()); + let digest_name = format!("{}.digest_function", name_base.as_ref()); + let payload_env = format!("{}_PAYLOAD", env_key_base.as_ref()); + let payload_name = format!("{}.payload", name_base.as_ref()); + + let digest_function = ParseEnvResult::parse_simple(emitter, env, &digest_env, &digest_name); + + // FIXME: errors handling is a mess + let payload = match env + .read_env(&payload_env) + .map_err(|err| eyre!("failed to read {payload_name}: {err}")) + .wrap_err("oops") + { + Ok(Some(value)) => ParseEnvResult::Value(value), + Ok(None) => ParseEnvResult::None, + Err(err) => { + emitter.emit(err); + ParseEnvResult::Error + } + }; + + match (digest_function, payload) { + (ParseEnvResult::Value(digest_function), ParseEnvResult::Value(payload)) => { + match PrivateKey::from_hex(digest_function, &payload).wrap_err_with(|| { + eyre!( + "failed to construct `{}` from `{}` and `{}` environment variables", + name_base.as_ref(), + &digest_env, + &payload_env + ) + }) { + Ok(value) => return ParseEnvResult::Value(value), + Err(report) => { + emitter.emit(report); + } + } + } + (ParseEnvResult::None, ParseEnvResult::None) => return ParseEnvResult::None, + (ParseEnvResult::Value(_), ParseEnvResult::None) => emitter.emit(eyre!( + "`{}` env was provided, but `{}` was not", + &digest_env, + &payload_env + )), + (ParseEnvResult::None, ParseEnvResult::Value(_)) => { + emitter.emit(eyre!( + "`{}` env was provided, but `{}` was not", + &payload_env, + &digest_env + )); + } + (ParseEnvResult::Error, _) | (_, ParseEnvResult::Error) => { + // emitter already has these errors + // adding this branch for exhaustiveness + } + } + + ParseEnvResult::Error +} + +#[derive(Debug)] +pub struct Genesis { + pub public_key: PublicKey, + pub private_key: Option, + pub file: Option, +} + +impl Genesis { + fn parse(self, cli: CliContext) -> Result { + match (self.private_key, self.file, cli.submit_genesis) { + (None, None, false) => Ok(actual::Genesis::Partial { + public_key: self.public_key, + }), + (Some(private_key), Some(file), true) => Ok(actual::Genesis::Full { + key_pair: KeyPair::new(self.public_key, private_key) + .map_err(GenesisConfigError::from)?, + file, + }), + (Some(_), Some(_), false) => Err(GenesisConfigError::GenesisWithoutSubmit), + (None, None, true) => Err(GenesisConfigError::SubmitWithoutGenesis), + _ => Err(GenesisConfigError::Inconsistent), + } + } +} + +#[derive(Debug, displaydoc::Display, thiserror::Error)] +pub enum GenesisConfigError { + /// `genesis.file` and `genesis.private_key` are presented, but `--submit-genesis` was not set + GenesisWithoutSubmit, + /// `--submit-genesis` was set, but `genesis.file` and `genesis.private_key` are not presented + SubmitWithoutGenesis, + /// `genesis.file` and `genesis.private_key` should be set together + Inconsistent, + /// failed to construct the genesis's keypair using `genesis.public_key` and `genesis.private_key` configuration parameters + KeyPair(#[from] iroha_crypto::error::Error), +} + +#[derive(Debug)] +pub struct Kura { + pub init_mode: Mode, + pub store_dir: PathBuf, + pub debug: KuraDebug, +} + +impl Kura { + fn parse(self) -> actual::Kura { + let Self { + init_mode, + store_dir: block_store_path, + debug: + KuraDebug { + output_new_blocks: debug_output_new_blocks, + }, + } = self; + + actual::Kura { + init_mode, + store_dir: block_store_path, + debug_output_new_blocks, + } + } +} + +#[derive(Debug, Copy, Clone)] +pub struct KuraDebug { + output_new_blocks: bool, +} + +#[derive(Debug)] +pub struct Sumeragi { + pub trusted_peers: Option>, + pub debug: SumeragiDebug, +} + +impl Sumeragi { + fn parse(self) -> Result { + let Self { + trusted_peers, + debug: SumeragiDebug { force_soft_fork }, + } = self; + + let trusted_peers = construct_unique_vec(trusted_peers.unwrap_or(vec![]))?; + + Ok(actual::Sumeragi { + trusted_peers, + debug_force_soft_fork: force_soft_fork, + }) + } +} + +#[derive(Debug, Copy, Clone)] +pub struct SumeragiDebug { + pub force_soft_fork: bool, +} + +// FIXME: handle duplicates properly, not here, and with details +fn construct_unique_vec( + unchecked: Vec, +) -> Result, eyre::Report> { + let mut unique = UniqueVec::new(); + for x in unchecked { + let pushed = unique.push(x); + if !pushed { + Err(eyre!("found duplicate"))? + } + } + Ok(unique) +} + +#[derive(Debug, Clone)] +pub struct Network { + /// Peer-to-peer address + pub address: SocketAddr, + pub block_gossip_max_size: NonZeroU32, + pub block_gossip_period: Duration, + pub transaction_gossip_max_size: NonZeroU32, + pub transaction_gossip_period: Duration, +} + +impl Network { + fn parse(self) -> (SocketAddr, actual::BlockSync, actual::TransactionGossiper) { + let Self { + address, + block_gossip_max_size, + block_gossip_period, + transaction_gossip_max_size, + transaction_gossip_period, + } = self; + + ( + address, + actual::BlockSync { + gossip_period: block_gossip_period, + gossip_max_size: block_gossip_max_size, + }, + actual::TransactionGossiper { + gossip_period: transaction_gossip_period, + gossip_max_size: transaction_gossip_max_size, + }, + ) + } +} + +#[derive(Debug, Clone, Copy)] +pub struct Queue { + /// The upper limit of the number of transactions waiting in the queue. + pub capacity: NonZeroUsize, + /// The upper limit of the number of transactions waiting in the queue for single user. + /// Use this option to apply throttling. + pub capacity_per_user: NonZeroUsize, + /// The transaction will be dropped after this time if it is still in the queue. + pub transaction_time_to_live: Duration, + /// The threshold to determine if a transaction has been tampered to have a future timestamp. + pub future_threshold: Duration, +} + +#[allow(missing_copy_implementations)] // triggered without tokio-console +#[derive(Debug, Clone)] +pub struct Logger { + /// Level of logging verbosity + // TODO: parse user provided value in a case insensitive way, + // because `format` is set in lowercase, and `LOG_LEVEL=INFO` + `LOG_FORMAT=pretty` + // looks inconsistent + pub level: Level, + /// Output format + pub format: Format, + #[cfg(feature = "tokio-console")] + /// Address of tokio console (only available under "tokio-console" feature) + pub tokio_console_address: SocketAddr, +} + +#[allow(clippy::derivable_impls)] // triggers in absence of `tokio-console` feature +impl Default for Logger { + fn default() -> Self { + Self { + level: Level::default(), + format: Format::default(), + #[cfg(feature = "tokio-console")] + tokio_console_address: super::defaults::logger::DEFAULT_TOKIO_CONSOLE_ADDR, + } + } +} + +#[derive(Debug)] +pub struct Telemetry { + // Fields here are Options so that it is possible to warn the user if e.g. they provided `min_retry_period`, but haven't + // provided `name` and `url` + pub name: Option, + pub url: Option, + pub min_retry_period: Option, + pub max_retry_delay_exponent: Option, + pub dev: TelemetryDev, +} + +#[derive(Debug)] +pub struct TelemetryDev { + pub out_file: Option, +} + +impl Telemetry { + fn parse(self) -> Result<(Option, Option), Report> { + let Self { + name, + url, + max_retry_delay_exponent, + min_retry_period, + dev: TelemetryDev { out_file: file }, + } = self; + + let regular = match (name, url) { + (Some(name), Some(url)) => Some(actual::Telemetry { + name, + url, + max_retry_delay_exponent: max_retry_delay_exponent + .unwrap_or(DEFAULT_MAX_RETRY_DELAY_EXPONENT), + min_retry_period: min_retry_period.unwrap_or(DEFAULT_MIN_RETRY_PERIOD), + }), + // TODO warn user if they provided retry parameters while not providing essential ones + (None, None) => None, + _ => { + // TODO improve error detail + return Err(eyre!( + "telemetry.name and telemetry.file should be set together" + ))?; + } + }; + + let dev = file.map(|file| actual::DevTelemetry { + out_file: file.clone(), + }); + + Ok((regular, dev)) + } +} + +#[derive(Debug, Clone)] +pub struct Snapshot { + pub create_every: Duration, + pub store_dir: PathBuf, + pub creation_enabled: bool, +} + +#[derive(Debug, Copy, Clone)] +pub struct ChainWide { + pub max_transactions_in_block: NonZeroU32, + pub block_time: Duration, + pub commit_time: Duration, + pub transaction_limits: TransactionLimits, + pub asset_metadata_limits: MetadataLimits, + pub asset_definition_metadata_limits: MetadataLimits, + pub account_metadata_limits: MetadataLimits, + pub domain_metadata_limits: MetadataLimits, + pub ident_length_limits: LengthLimits, + pub wasm_fuel_limit: u64, + pub wasm_max_memory: HumanBytes, +} + +impl ChainWide { + fn parse(self) -> actual::ChainWide { + let Self { + max_transactions_in_block, + block_time, + commit_time, + transaction_limits, + asset_metadata_limits, + asset_definition_metadata_limits, + account_metadata_limits, + domain_metadata_limits, + ident_length_limits: identifier_length_limits, + wasm_fuel_limit, + wasm_max_memory, + } = self; + + actual::ChainWide { + max_transactions_in_block, + block_time, + commit_time, + transaction_limits, + asset_metadata_limits, + asset_definition_metadata_limits, + account_metadata_limits, + domain_metadata_limits, + ident_length_limits: identifier_length_limits, + wasm_runtime: actual::WasmRuntime { + fuel_limit: wasm_fuel_limit, + max_memory_bytes: wasm_max_memory.get(), + }, + } + } +} + +#[derive(Debug)] +pub struct Torii { + pub address: SocketAddr, + pub max_content_len: HumanBytes, + pub query_idle_time: Duration, +} + +impl Torii { + fn parse(self) -> (actual::Torii, actual::LiveQueryStore) { + let torii = actual::Torii { + address: self.address, + max_content_len_bytes: self.max_content_len.get(), + }; + + let query = actual::LiveQueryStore { + idle_time: self.query_idle_time, + }; + + (torii, query) + } +} + +#[cfg(test)] +mod tests { + use iroha_config_base::{FromEnv, TestEnv}; + + use super::super::user::boilerplate::RootPartial; + + #[test] + fn parses_private_key_from_env() { + let env = TestEnv::new() + .set("PRIVATE_KEY_DIGEST", "ed25519") + .set("PRIVATE_KEY_PAYLOAD", "8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb"); + + let private_key = RootPartial::from_env(&env) + .expect("input is valid, should not fail") + .private_key + .get() + .expect("private key is provided, should not fail"); + + let (algorithm, payload) = private_key.to_raw(); + assert_eq!(algorithm, "ed25519".parse().unwrap()); + assert_eq!(hex::encode(payload), "8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb"); + } + + #[test] + fn fails_to_parse_private_key_in_env_without_digest() { + let env = TestEnv::new().set("PRIVATE_KEY_DIGEST", "ed25519"); + let error = + RootPartial::from_env(&env).expect_err("private key is incomplete, should fail"); + let expected = expect_test::expect![ + "`PRIVATE_KEY_DIGEST` env was provided, but `PRIVATE_KEY_PAYLOAD` was not" + ]; + expected.assert_eq(&format!("{error:#}")); + } + + #[test] + fn fails_to_parse_private_key_in_env_without_payload() { + let env = TestEnv::new().set("PRIVATE_KEY_PAYLOAD", "8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb"); + let error = + RootPartial::from_env(&env).expect_err("private key is incomplete, should fail"); + let expected = expect_test::expect![ + "`PRIVATE_KEY_PAYLOAD` env was provided, but `PRIVATE_KEY_DIGEST` was not" + ]; + expected.assert_eq(&format!("{error:#}")); + } + + #[test] + fn fails_to_parse_private_key_from_env_with_invalid_payload() { + let env = TestEnv::new() + .set("PRIVATE_KEY_DIGEST", "ed25519") + .set("PRIVATE_KEY_PAYLOAD", "foo"); + + let error = RootPartial::from_env(&env).expect_err("input is invalid, should fail"); + + let expected = expect_test::expect!["failed to construct `iroha.private_key` from `PRIVATE_KEY_DIGEST` and `PRIVATE_KEY_PAYLOAD` environment variables"]; + expected.assert_eq(&format!("{error:#}")); + } + + #[test] + fn when_payload_provided_but_digest_is_invalid() { + let env = TestEnv::new() + .set("PRIVATE_KEY_DIGEST", "foo") + .set("PRIVATE_KEY_PAYLOAD", "8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb"); + + let error = RootPartial::from_env(&env).expect_err("input is invalid, should fail"); + + // TODO: print the bad value and supported ones + let expected = expect_test::expect!["failed to parse `iroha.private_key.digest_function` field from `PRIVATE_KEY_DIGEST` env variable"]; + expected.assert_eq(&format!("{error:#}")); + } + + #[test] + fn deserialize_empty_input_works() { + let _layer: RootPartial = toml::from_str("").unwrap(); + } + + #[test] + fn deserialize_network_namespace_with_not_all_fields_works() { + let _layer: RootPartial = toml::toml! { + [network] + address = "127.0.0.1:8080" + } + .try_into() + .expect("should not fail when not all fields in `network` are presented at a time"); + } +} diff --git a/config/src/parameters/user/boilerplate.rs b/config/src/parameters/user/boilerplate.rs new file mode 100644 index 00000000000..64b81d9d6ff --- /dev/null +++ b/config/src/parameters/user/boilerplate.rs @@ -0,0 +1,766 @@ +//! Code that should be generated by a procmacro in future. + +#![allow(missing_docs)] + +use std::{ + error::Error, + num::{NonZeroU32, NonZeroUsize}, + path::PathBuf, +}; + +use eyre::{eyre, Report, WrapErr}; +use iroha_config_base::{ + Emitter, ErrorsCollection, ExtendsPaths, FromEnv, FromEnvDefaultFallback, FromEnvResult, + HumanBytes, HumanDuration, Merge, MissingFieldError, ParseEnvResult, ReadEnv, UnwrapPartial, + UnwrapPartialResult, UserField, +}; +use iroha_crypto::{PrivateKey, PublicKey}; +use iroha_data_model::{ + metadata::Limits as MetadataLimits, + prelude::{ChainId, PeerId}, + transaction::TransactionLimits, + LengthLimits, Level, +}; +use iroha_primitives::addr::SocketAddr; +use serde::{Deserialize, Serialize}; +use url::Url; + +use crate::{ + kura::Mode, + logger::Format, + parameters::{ + defaults::{self, chain_wide::*, network::*, queue::*, torii::*}, + user, + user::{ + ChainWide, Genesis, Kura, KuraDebug, Logger, Network, Queue, Root, Snapshot, Sumeragi, + SumeragiDebug, Telemetry, TelemetryDev, Torii, + }, + }, +}; + +#[derive(Deserialize, Serialize, Debug, Default, Merge)] +#[serde(deny_unknown_fields, default)] +pub struct RootPartial { + pub extends: Option, + pub chain_id: UserField, + pub public_key: UserField, + pub private_key: UserField, + pub genesis: GenesisPartial, + pub kura: KuraPartial, + pub sumeragi: SumeragiPartial, + pub network: NetworkPartial, + pub logger: LoggerPartial, + pub queue: QueuePartial, + pub snapshot: SnapshotPartial, + pub telemetry: TelemetryPartial, + pub torii: ToriiPartial, + pub chain_wide: ChainWidePartial, +} + +impl RootPartial { + /// Creates new empty user configuration + pub fn new() -> Self { + // TODO: generate this function with macro. For now, use default + Self::default() + } +} + +impl UnwrapPartial for RootPartial { + type Output = Root; + + fn unwrap_partial(self) -> UnwrapPartialResult { + let mut emitter = Emitter::new(); + + macro_rules! nested { + ($item:expr) => { + match UnwrapPartial::unwrap_partial($item) { + Ok(value) => Some(value), + Err(error) => { + emitter.emit_collection(error); + None + } + } + }; + } + + if self.chain_id.is_none() { + emitter.emit_missing_field("chain_id"); + } + if self.public_key.is_none() { + emitter.emit_missing_field("public_key"); + } + if self.private_key.is_none() { + emitter.emit_missing_field("private_key"); + } + + let genesis = nested!(self.genesis); + let kura = nested!(self.kura); + let sumeragi = nested!(self.sumeragi); + let network = nested!(self.network); + let logger = nested!(self.logger); + let queue = nested!(self.queue); + let snapshot = nested!(self.snapshot); + let telemetry = nested!(self.telemetry); + let torii = nested!(self.torii); + let chain_wide = nested!(self.chain_wide); + + emitter.finish()?; + + Ok(Root { + chain_id: self.chain_id.get().unwrap(), + public_key: self.public_key.get().unwrap(), + private_key: self.private_key.get().unwrap(), + genesis: genesis.unwrap(), + kura: kura.unwrap(), + sumeragi: sumeragi.unwrap(), + telemetry: telemetry.unwrap(), + logger: logger.unwrap(), + queue: queue.unwrap(), + snapshot: snapshot.unwrap(), + torii: torii.unwrap(), + network: network.unwrap(), + chain_wide: chain_wide.unwrap(), + }) + } +} + +impl FromEnv for RootPartial { + fn from_env>(env: &R) -> FromEnvResult { + fn from_env_nested(env: &R, emitter: &mut Emitter) -> Option + where + T: FromEnv, + R: ReadEnv, + RE: Error, + { + match FromEnv::from_env(env) { + Ok(parsed) => Some(parsed), + Err(errors) => { + emitter.emit_collection(errors); + None + } + } + } + + let mut emitter = Emitter::new(); + + let chain_id = env + .read_env("CHAIN_ID") + .map_err(|e| eyre!("{e}")) + .wrap_err("failed to read CHAIN_ID field (iroha.chain_id param)") + .map_or_else( + |err| { + emitter.emit(err); + None + }, + |maybe_value| maybe_value.map(ChainId::from), + ) + .into(); + let public_key = + ParseEnvResult::parse_simple(&mut emitter, env, "PUBLIC_KEY", "iroha.public_key") + .into(); + let private_key = + user::private_key_from_env(&mut emitter, env, "PRIVATE_KEY", "iroha.private_key") + .into(); + + let genesis = from_env_nested(env, &mut emitter); + let kura = from_env_nested(env, &mut emitter); + let sumeragi = from_env_nested(env, &mut emitter); + let network = from_env_nested(env, &mut emitter); + let logger = from_env_nested(env, &mut emitter); + let queue = from_env_nested(env, &mut emitter); + let snapshot = from_env_nested(env, &mut emitter); + let telemetry = from_env_nested(env, &mut emitter); + let torii = from_env_nested(env, &mut emitter); + let chain_wide = from_env_nested(env, &mut emitter); + + emitter.finish()?; + + Ok(Self { + extends: None, + chain_id, + public_key, + private_key, + genesis: genesis.unwrap(), + kura: kura.unwrap(), + sumeragi: sumeragi.unwrap(), + network: network.unwrap(), + logger: logger.unwrap(), + queue: queue.unwrap(), + snapshot: snapshot.unwrap(), + telemetry: telemetry.unwrap(), + torii: torii.unwrap(), + chain_wide: chain_wide.unwrap(), + }) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Default, Merge)] +#[serde(deny_unknown_fields, default)] +pub struct GenesisPartial { + pub public_key: UserField, + pub private_key: UserField, + pub file: UserField, +} + +impl UnwrapPartial for GenesisPartial { + type Output = Genesis; + + fn unwrap_partial(self) -> UnwrapPartialResult { + let public_key = self + .public_key + .get() + .ok_or_else(|| MissingFieldError::new("genesis.public_key"))?; + + let private_key = self.private_key.get(); + let file = self.file.get(); + + Ok(Genesis { + public_key, + private_key, + file, + }) + } +} + +impl FromEnv for GenesisPartial { + fn from_env>(env: &R) -> FromEnvResult + where + Self: Sized, + { + let mut emitter = Emitter::new(); + + let public_key = ParseEnvResult::parse_simple( + &mut emitter, + env, + "GENESIS_PUBLIC_KEY", + "genesis.public_key", + ) + .into(); + let private_key = user::private_key_from_env( + &mut emitter, + env, + "GENESIS_PRIVATE_KEY", + "genesis.private_key", + ) + .into(); + let file = + ParseEnvResult::parse_simple(&mut emitter, env, "GENESIS_FILE", "genesis.file").into(); + + emitter.finish()?; + + Ok(Self { + public_key, + private_key, + file, + }) + } +} + +/// `Kura` configuration. +#[derive(Clone, Deserialize, Serialize, Debug, Default, Merge)] +#[serde(deny_unknown_fields, default)] +pub struct KuraPartial { + pub init_mode: UserField, + pub store_dir: UserField, + pub debug: KuraDebugPartial, +} + +impl UnwrapPartial for KuraPartial { + type Output = Kura; + + fn unwrap_partial(self) -> Result> { + let mut emitter = Emitter::new(); + + let init_mode = self.init_mode.unwrap_or_default(); + + let store_dir = self + .store_dir + .get() + .unwrap_or_else(|| PathBuf::from(defaults::kura::DEFAULT_STORE_DIR)); + + let debug = UnwrapPartial::unwrap_partial(self.debug).map_or_else( + |err| { + emitter.emit_collection(err); + None + }, + Some, + ); + + emitter.finish()?; + + Ok(Kura { + init_mode, + store_dir, + debug: debug.unwrap(), + }) + } +} + +#[derive(Clone, Deserialize, Serialize, Debug, Default, Merge)] +#[serde(deny_unknown_fields, default)] +pub struct KuraDebugPartial { + output_new_blocks: UserField, +} + +impl UnwrapPartial for KuraDebugPartial { + type Output = KuraDebug; + + fn unwrap_partial(self) -> Result> { + Ok(KuraDebug { + output_new_blocks: self.output_new_blocks.unwrap_or(false), + }) + } +} + +impl FromEnv for KuraPartial { + fn from_env>(env: &R) -> FromEnvResult + where + Self: Sized, + { + let mut emitter = Emitter::new(); + + let init_mode = + ParseEnvResult::parse_simple(&mut emitter, env, "KURA_INIT_MODE", "kura.init_mode") + .into(); + let store_dir = + ParseEnvResult::parse_simple(&mut emitter, env, "KURA_STORE_DIR", "kura.store_dir") + .into(); + let debug_output_new_blocks = ParseEnvResult::parse_simple( + &mut emitter, + env, + "KURA_DEBUG_OUTPUT_NEW_BLOCKS", + "kura.debug.output_new_blocks", + ) + .into(); + + emitter.finish()?; + + Ok(Self { + init_mode, + store_dir, + debug: KuraDebugPartial { + output_new_blocks: debug_output_new_blocks, + }, + }) + } +} + +#[derive(Deserialize, Serialize, Debug, Default, Merge)] +#[serde(deny_unknown_fields, default)] +pub struct SumeragiPartial { + pub trusted_peers: UserField>, + pub debug: SumeragiDebugPartial, +} + +impl UnwrapPartial for SumeragiPartial { + type Output = Sumeragi; + + fn unwrap_partial(self) -> UnwrapPartialResult { + let mut emitter = Emitter::new(); + + let debug = self.debug.unwrap_partial().map_or_else( + |err| { + emitter.emit_collection(err); + None + }, + Some, + ); + + emitter.finish()?; + + Ok(Sumeragi { + trusted_peers: self.trusted_peers.get(), + debug: debug.unwrap(), + }) + } +} + +#[derive(Deserialize, Serialize, Debug, Default, Merge)] +#[serde(deny_unknown_fields, default)] +pub struct SumeragiDebugPartial { + pub force_soft_fork: UserField, +} + +impl UnwrapPartial for SumeragiDebugPartial { + type Output = SumeragiDebug; + + fn unwrap_partial(self) -> UnwrapPartialResult { + Ok(SumeragiDebug { + force_soft_fork: self.force_soft_fork.unwrap_or(false), + }) + } +} + +impl FromEnvDefaultFallback for SumeragiPartial {} + +#[derive(Deserialize, Serialize, Debug, Default, Merge)] +#[serde(deny_unknown_fields, default)] +pub struct NetworkPartial { + pub address: UserField, + pub block_gossip_max_size: UserField, + pub block_gossip_period: UserField, + pub transaction_gossip_max_size: UserField, + pub transaction_gossip_period: UserField, +} + +impl UnwrapPartial for NetworkPartial { + type Output = Network; + + fn unwrap_partial(self) -> UnwrapPartialResult { + if self.address.is_none() { + return Err(MissingFieldError::new("network.address").into()); + } + + Ok(Network { + address: self.address.get().unwrap(), + block_gossip_period: self + .block_gossip_period + .map(HumanDuration::get) + .unwrap_or(DEFAULT_BLOCK_GOSSIP_PERIOD), + transaction_gossip_period: self + .transaction_gossip_period + .map(HumanDuration::get) + .unwrap_or(DEFAULT_TRANSACTION_GOSSIP_PERIOD), + transaction_gossip_max_size: self + .transaction_gossip_max_size + .get() + .unwrap_or(DEFAULT_MAX_TRANSACTIONS_PER_GOSSIP), + block_gossip_max_size: self + .block_gossip_max_size + .get() + .unwrap_or(DEFAULT_MAX_BLOCKS_PER_GOSSIP), + }) + } +} + +impl FromEnv for NetworkPartial { + fn from_env>(env: &R) -> FromEnvResult + where + Self: Sized, + { + let mut emitter = Emitter::new(); + + // TODO: also parse `NETWORK_ADDRESS`? + let address = + ParseEnvResult::parse_simple(&mut emitter, env, "P2P_ADDRESS", "network.address") + .into(); + + emitter.finish()?; + + Ok(Self { + address, + ..Self::default() + }) + } +} + +#[derive(Deserialize, Serialize, Debug, Default, Merge)] +#[serde(deny_unknown_fields, default)] +pub struct QueuePartial { + /// The upper limit of the number of transactions waiting in the queue. + pub capacity: UserField, + /// The upper limit of the number of transactions waiting in the queue for single user. + /// Use this option to apply throttling. + pub capacity_per_user: UserField, + /// The transaction will be dropped after this time if it is still in the queue. + pub transaction_time_to_live: UserField, + /// The threshold to determine if a transaction has been tampered to have a future timestamp. + pub future_threshold: UserField, +} + +impl UnwrapPartial for QueuePartial { + type Output = Queue; + + fn unwrap_partial(self) -> UnwrapPartialResult { + Ok(Queue { + capacity: self.capacity.unwrap_or(DEFAULT_MAX_TRANSACTIONS_IN_QUEUE), + capacity_per_user: self + .capacity_per_user + .unwrap_or(DEFAULT_MAX_TRANSACTIONS_IN_QUEUE), + transaction_time_to_live: self + .transaction_time_to_live + .map_or(DEFAULT_TRANSACTION_TIME_TO_LIVE, HumanDuration::get), + future_threshold: self + .future_threshold + .map_or(DEFAULT_FUTURE_THRESHOLD, HumanDuration::get), + }) + } +} + +impl FromEnvDefaultFallback for QueuePartial {} + +/// 'Logger' configuration. +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Default, Merge)] +// `tokio_console_addr` is not `Copy`, but warning appears without `tokio-console` feature +#[allow(missing_copy_implementations)] +#[serde(deny_unknown_fields, default)] +pub struct LoggerPartial { + /// Level of logging verbosity + pub level: UserField, + /// Output format + pub format: UserField, + #[cfg(feature = "tokio-console")] + /// Address of tokio console (only available under "tokio-console" feature) + pub tokio_console_address: UserField, +} + +impl UnwrapPartial for LoggerPartial { + type Output = Logger; + + fn unwrap_partial(self) -> UnwrapPartialResult { + Ok(Logger { + level: self.level.unwrap_or_default(), + format: self.format.unwrap_or_default(), + #[cfg(feature = "tokio-console")] + tokio_console_address: self.tokio_console_address.get().unwrap_or_else(|| { + super::super::defaults::logger::DEFAULT_TOKIO_CONSOLE_ADDR.clone() + }), + }) + } +} + +impl FromEnv for LoggerPartial { + fn from_env>(env: &R) -> FromEnvResult + where + Self: Sized, + { + let mut emitter = Emitter::new(); + + let level = + ParseEnvResult::parse_simple(&mut emitter, env, "LOG_LEVEL", "logger.level").into(); + let format = + ParseEnvResult::parse_simple(&mut emitter, env, "LOG_FORMAT", "logger.format").into(); + + emitter.finish()?; + + #[allow(clippy::needless_update)] // triggers if tokio console addr is feature-gated + Ok(Self { + level, + format, + ..Self::default() + }) + } +} + +#[derive(Clone, Deserialize, Serialize, Debug, Default, Merge)] +#[serde(deny_unknown_fields, default)] +pub struct TelemetryPartial { + pub name: UserField, + pub url: UserField, + pub min_retry_period: UserField, + pub max_retry_delay_exponent: UserField, + pub dev: TelemetryDevPartial, +} + +#[derive(Clone, Deserialize, Serialize, Debug, Default, Merge)] +#[serde(deny_unknown_fields, default)] +pub struct TelemetryDevPartial { + pub out_file: UserField, +} + +impl UnwrapPartial for TelemetryDevPartial { + type Output = TelemetryDev; + + fn unwrap_partial(self) -> UnwrapPartialResult { + Ok(TelemetryDev { + out_file: self.out_file.get(), + }) + } +} + +impl UnwrapPartial for TelemetryPartial { + type Output = Telemetry; + + fn unwrap_partial(self) -> UnwrapPartialResult { + let Self { + name, + url, + max_retry_delay_exponent, + min_retry_period, + dev, + } = self; + + Ok(Telemetry { + name: name.get(), + url: url.get(), + max_retry_delay_exponent: max_retry_delay_exponent.get(), + min_retry_period: min_retry_period.get().map(HumanDuration::get), + dev: dev.unwrap_partial()?, + }) + } +} + +impl FromEnvDefaultFallback for TelemetryPartial {} + +#[derive(Debug, Clone, Deserialize, Serialize, Default, Merge)] +#[serde(deny_unknown_fields, default)] +pub struct SnapshotPartial { + pub create_every: UserField, + pub store_dir: UserField, + pub creation_enabled: UserField, +} + +impl UnwrapPartial for SnapshotPartial { + type Output = Snapshot; + + fn unwrap_partial(self) -> UnwrapPartialResult { + Ok(Snapshot { + creation_enabled: self + .creation_enabled + .unwrap_or(defaults::snapshot::DEFAULT_ENABLED), + create_every: self + .create_every + .get() + .map_or(defaults::snapshot::DEFAULT_CREATE_EVERY, HumanDuration::get), + store_dir: self + .store_dir + .get() + .unwrap_or_else(|| PathBuf::from(defaults::snapshot::DEFAULT_STORE_DIR)), + }) + } +} + +impl FromEnv for SnapshotPartial { + fn from_env>(env: &R) -> FromEnvResult + where + Self: Sized, + { + let mut emitter = Emitter::new(); + + let store_dir = ParseEnvResult::parse_simple( + &mut emitter, + env, + "SNAPSHOT_STORE_DIR", + "snapshot.store_dir", + ) + .into(); + let creation_enabled = ParseEnvResult::parse_simple( + &mut emitter, + env, + "SNAPSHOT_CREATION_ENABLED", + "snapshot.creation_enabled", + ) + .into(); + + emitter.finish()?; + + Ok(Self { + store_dir, + creation_enabled, + ..Self::default() + }) + } +} + +#[derive(Deserialize, Serialize, Debug, Default, Merge)] +#[serde(deny_unknown_fields, default)] +pub struct ChainWidePartial { + pub max_transactions_in_block: UserField, + pub block_time: UserField, + pub commit_time: UserField, + pub transaction_limits: UserField, + pub asset_metadata_limits: UserField, + pub asset_definition_metadata_limits: UserField, + pub account_metadata_limits: UserField, + pub domain_metadata_limits: UserField, + pub ident_length_limits: UserField, + pub wasm_fuel_limit: UserField, + pub wasm_max_memory: UserField>, +} + +impl UnwrapPartial for ChainWidePartial { + type Output = ChainWide; + + fn unwrap_partial(self) -> UnwrapPartialResult { + Ok(ChainWide { + max_transactions_in_block: self.max_transactions_in_block.unwrap_or(DEFAULT_MAX_TXS), + block_time: self + .block_time + .map_or(DEFAULT_BLOCK_TIME, HumanDuration::get), + commit_time: self + .commit_time + .map_or(DEFAULT_COMMIT_TIME, HumanDuration::get), + transaction_limits: self + .transaction_limits + .unwrap_or(DEFAULT_TRANSACTION_LIMITS), + asset_metadata_limits: self + .asset_metadata_limits + .unwrap_or(DEFAULT_METADATA_LIMITS), + asset_definition_metadata_limits: self + .asset_definition_metadata_limits + .unwrap_or(DEFAULT_METADATA_LIMITS), + account_metadata_limits: self + .account_metadata_limits + .unwrap_or(DEFAULT_METADATA_LIMITS), + domain_metadata_limits: self + .domain_metadata_limits + .unwrap_or(DEFAULT_METADATA_LIMITS), + ident_length_limits: self + .ident_length_limits + .unwrap_or(DEFAULT_IDENT_LENGTH_LIMITS), + wasm_fuel_limit: self.wasm_fuel_limit.unwrap_or(DEFAULT_WASM_FUEL_LIMIT), + wasm_max_memory: self + .wasm_max_memory + .unwrap_or(HumanBytes(DEFAULT_WASM_MAX_MEMORY_BYTES)), + }) + } +} + +impl FromEnvDefaultFallback for ChainWidePartial {} + +#[derive(Debug, Clone, Deserialize, Serialize, Default, Merge)] +#[serde(deny_unknown_fields, default)] +pub struct ToriiPartial { + pub address: UserField, + pub max_content_len: UserField>, + pub query_idle_time: UserField, +} + +impl UnwrapPartial for ToriiPartial { + type Output = Torii; + + fn unwrap_partial(self) -> UnwrapPartialResult { + let mut emitter = Emitter::new(); + + if self.address.is_none() { + emitter.emit_missing_field("torii.address"); + } + + let max_content_len = self + .max_content_len + .get() + .unwrap_or(HumanBytes(DEFAULT_MAX_CONTENT_LENGTH)); + + let query_idle_time = self + .query_idle_time + .map(HumanDuration::get) + .unwrap_or(DEFAULT_QUERY_IDLE_TIME); + + emitter.finish()?; + + Ok(Torii { + address: self.address.get().unwrap(), + max_content_len, + query_idle_time, + }) + } +} + +impl FromEnv for ToriiPartial { + fn from_env>(env: &R) -> FromEnvResult + where + Self: Sized, + { + let mut emitter = Emitter::new(); + + let address = + ParseEnvResult::parse_simple(&mut emitter, env, "API_ADDRESS", "torii.address").into(); + + emitter.finish()?; + + Ok(Self { + address, + ..Self::default() + }) + } +} diff --git a/config/src/path.rs b/config/src/path.rs deleted file mode 100644 index 23f1bd80b57..00000000000 --- a/config/src/path.rs +++ /dev/null @@ -1,152 +0,0 @@ -//! Module with configuration path related structures. - -extern crate alloc; - -use alloc::borrow::Cow; -use std::path::PathBuf; - -use InnerPath::*; - -/// Allowed configuration file extension that user can provide. -pub const ALLOWED_CONFIG_EXTENSIONS: [&str; 2] = ["json", "json5"]; - -/// Error type for [`Path`]. -#[derive(Debug, Clone, thiserror::Error, displaydoc::Display)] -pub enum Error { - /// File doesn't have an extension. Allowed file extensions are: {ALLOWED_CONFIG_EXTENSIONS:?} - MissingExtension, - /// Provided config file has an unsupported file extension `{0}`. Allowed extensions are: {ALLOWED_CONFIG_EXTENSIONS:?}. - InvalidExtension(String), - /// User-provided file `{0}` is not found. - FileNotFound(String), -} - -/// Result type for [`Path`] constructors. -pub type Result = std::result::Result; - -/// Inner helper struct. -/// -/// With this struct, we force to use [`Path`]'s constructors instead of constructing it directly. -#[derive(Debug, Clone, PartialEq)] -enum InnerPath { - /// Contains path without an extension, so that it will try to resolve - /// using [`ALLOWED_CONFIG_EXTENSIONS`]. [`Path::try_resolve()`] will not fail if file isn't - /// found. - Default(PathBuf), - /// Contains full path, with extension. [`Path::try_resolve()`] will fail if not found. - UserProvided(PathBuf), -} - -/// Wrapper around path to config file (e.g. `config.json`). -/// -/// Provides abstraction above user-provided config and default ones. -#[derive(Debug, Clone, PartialEq)] -pub struct Path(InnerPath); - -impl core::fmt::Display for Path { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match &self.0 { - Default(path) => { - write!( - f, - "{}.{{{}}}", - path.display(), - ALLOWED_CONFIG_EXTENSIONS.join(",") - ) - } - UserProvided(path) => write!(f, "{}", path.display()), - } - } -} - -impl Path { - /// Construct new [`Path`] which will try to resolve multiple allowed extensions and will not - /// fail resolution ([`Self::try_resolve()`]) if file is not found. - /// - /// The path should not have an extension. - /// - /// # Panics - /// If the path has an extension. - pub fn default(path: impl AsRef) -> Self { - let path = path.as_ref().to_path_buf(); - assert!( - path.extension().is_none(), - "Default config path is not supposed to have an extension. It is a bug." - ); - Self(Default(path)) - } - - /// Construct new [`Path`] from user-provided `path` which will fail to [`Self::try_resolve()`] - /// if file is not found. - /// - /// # Errors - /// If `path`'s extension is absent or unsupported. - pub fn user_provided(path: impl AsRef) -> Result { - let path = path.as_ref(); - - let extension = path - .extension() - .ok_or(Error::MissingExtension)? - .to_string_lossy(); - if !ALLOWED_CONFIG_EXTENSIONS.contains(&extension.as_ref()) { - return Err(Error::InvalidExtension(extension.into_owned())); - } - - Ok(Self(UserProvided(path.to_path_buf()))) - } - - /// Same as [`Self::user_provided()`], but accepts `&str` (useful for clap) - /// - /// # Errors - /// See [`Self::user_provided()`] - pub fn user_provided_str(raw: &str) -> Result { - Self::user_provided(raw) - } - - /// Try to get first existing path by applying possible extensions if there are any. - /// - /// # Errors - /// If user-provided path is not found - pub fn try_resolve(&self) -> Result>> { - match &self.0 { - Default(path) => { - let maybe = ALLOWED_CONFIG_EXTENSIONS.iter().find_map(|extension| { - let path_ext = path.with_extension(extension); - path_ext.exists().then_some(Cow::Owned(path_ext)) - }); - Ok(maybe) - } - UserProvided(path) => { - if path.exists() { - Ok(Some(Cow::Borrowed(path))) - } else { - Err(Error::FileNotFound(path.to_string_lossy().into_owned())) - } - } - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn display_multi_extensions() { - let path = Path::default("config"); - - let display = format!("{path}"); - - assert_eq!(display, "config.{json,json5}") - } - - #[test] - fn display_strict_extension() { - let path = - Path::user_provided("config.json").expect("Should be valid since extension is valid"); - - let display = format!("{path}"); - - assert_eq!(display, "config.json") - } -} diff --git a/config/src/queue.rs b/config/src/queue.rs deleted file mode 100644 index 5803e90ed7c..00000000000 --- a/config/src/queue.rs +++ /dev/null @@ -1,55 +0,0 @@ -//! Module for `Queue`-related configuration and structs. -use iroha_config_base::derive::Proxy; -use serde::{Deserialize, Serialize}; - -const DEFAULT_MAX_TRANSACTIONS_IN_QUEUE: u32 = 2_u32.pow(16); -const DEFAULT_MAX_TRANSACTIONS_IN_QUEUE_PER_USER: u32 = 2_u32.pow(16); -const DEFAULT_TRANSACTION_TIME_TO_LIVE_MS: u64 = 24 * 60 * 60 * 1000; // 24 hours -const DEFAULT_FUTURE_THRESHOLD_MS: u64 = 1000; - -/// `Queue` configuration. -#[derive(Copy, Clone, Deserialize, Serialize, Debug, Proxy, PartialEq, Eq)] -#[serde(rename_all = "UPPERCASE")] -#[config(env_prefix = "QUEUE_")] -pub struct Configuration { - /// The upper limit of the number of transactions waiting in the queue. - pub max_transactions_in_queue: u32, - /// The upper limit of the number of transactions waiting in the queue for single user. - /// Use this option to apply throttling. - pub max_transactions_in_queue_per_user: u32, - /// The transaction will be dropped after this time if it is still in the queue. - pub transaction_time_to_live_ms: u64, - /// The threshold to determine if a transaction has been tampered to have a future timestamp. - pub future_threshold_ms: u64, -} - -impl Default for ConfigurationProxy { - fn default() -> Self { - Self { - max_transactions_in_queue: Some(DEFAULT_MAX_TRANSACTIONS_IN_QUEUE), - max_transactions_in_queue_per_user: Some(DEFAULT_MAX_TRANSACTIONS_IN_QUEUE_PER_USER), - transaction_time_to_live_ms: Some(DEFAULT_TRANSACTION_TIME_TO_LIVE_MS), - future_threshold_ms: Some(DEFAULT_FUTURE_THRESHOLD_MS), - } - } -} - -#[cfg(test)] -pub mod tests { - use proptest::prelude::*; - - use super::*; - - prop_compose! { - pub fn arb_proxy() - ( - max_transactions_in_queue in prop::option::of(Just(DEFAULT_MAX_TRANSACTIONS_IN_QUEUE)), - max_transactions_in_queue_per_user in prop::option::of(Just(DEFAULT_MAX_TRANSACTIONS_IN_QUEUE_PER_USER)), - transaction_time_to_live_ms in prop::option::of(Just(DEFAULT_TRANSACTION_TIME_TO_LIVE_MS)), - future_threshold_ms in prop::option::of(Just(DEFAULT_FUTURE_THRESHOLD_MS)), - ) - -> ConfigurationProxy { - ConfigurationProxy { max_transactions_in_queue, max_transactions_in_queue_per_user, transaction_time_to_live_ms, future_threshold_ms } - } - } -} diff --git a/config/src/snapshot.rs b/config/src/snapshot.rs deleted file mode 100644 index e828e5635d0..00000000000 --- a/config/src/snapshot.rs +++ /dev/null @@ -1,54 +0,0 @@ -//! Module for `SnapshotMaker`-related configuration and structs. - -use std::path::PathBuf; - -use iroha_config_base::derive::Proxy; -use serde::{Deserialize, Serialize}; - -const DEFAULT_SNAPSHOT_PATH: &str = "./storage"; -// Default frequency of making snapshots is 1 minute, need to be adjusted for larger world state view size -const DEFAULT_SNAPSHOT_CREATE_EVERY_MS: u64 = 1000 * 60; -const DEFAULT_ENABLED: bool = true; - -/// Configuration for `SnapshotMaker`. -#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Proxy)] -#[serde(rename_all = "UPPERCASE")] -#[config(env_prefix = "SNAPSHOT_")] -pub struct Configuration { - /// The period of time to wait between attempts to create new snapshot. - pub create_every_ms: u64, - /// Path to the directory where snapshots should be stored - #[config(serde_as_str)] - pub dir_path: PathBuf, - /// Flag to enable or disable snapshot creation - pub creation_enabled: bool, -} - -impl Default for ConfigurationProxy { - fn default() -> Self { - Self { - create_every_ms: Some(DEFAULT_SNAPSHOT_CREATE_EVERY_MS), - dir_path: Some(DEFAULT_SNAPSHOT_PATH.into()), - creation_enabled: Some(DEFAULT_ENABLED), - } - } -} - -#[cfg(test)] -pub mod tests { - use proptest::prelude::*; - - use super::*; - - prop_compose! { - pub fn arb_proxy() - ( - create_every_ms in prop::option::of(Just(DEFAULT_SNAPSHOT_CREATE_EVERY_MS)), - dir_path in prop::option::of(Just(DEFAULT_SNAPSHOT_PATH.into())), - creation_enabled in prop::option::of(Just(DEFAULT_ENABLED)), - ) - -> ConfigurationProxy { - ConfigurationProxy { create_every_ms, dir_path, creation_enabled } - } - } -} diff --git a/config/src/sumeragi.rs b/config/src/sumeragi.rs deleted file mode 100644 index a4eb7760069..00000000000 --- a/config/src/sumeragi.rs +++ /dev/null @@ -1,186 +0,0 @@ -//! `Sumeragi` configuration. Contains both block commit and Gossip-related configuration. -use std::{fmt::Debug, fs::File, io::BufReader, path::Path}; - -use eyre::{Result, WrapErr}; -use iroha_config_base::derive::{view, Proxy}; -use iroha_crypto::prelude::*; -use iroha_data_model::prelude::*; -use iroha_primitives::{unique_vec, unique_vec::UniqueVec}; -use serde::{Deserialize, Serialize}; - -use self::default::*; - -/// Module with a set of default values. -pub mod default { - /// Default number of miliseconds the peer waits for transactions before creating a block. - pub const DEFAULT_BLOCK_TIME_MS: u64 = 2000; - /// Default amount of time allocated for voting on a block before a peer can ask for a view change. - pub const DEFAULT_COMMIT_TIME_LIMIT_MS: u64 = 4000; - /// Unused const. Should be removed in the future. - pub const DEFAULT_ACTOR_CHANNEL_CAPACITY: u32 = 100; - /// Default duration in ms between every transaction gossip. - pub const DEFAULT_GOSSIP_PERIOD_MS: u64 = 1000; - /// Default maximum number of transactions sent in single gossip message. - pub const DEFAULT_GOSSIP_BATCH_SIZE: u32 = 500; - /// Default maximum number of transactions in block. - pub const DEFAULT_MAX_TRANSACTIONS_IN_BLOCK: u32 = 2_u32.pow(9); - - /// Default estimation of consensus duration. - #[allow(clippy::integer_division)] - pub const DEFAULT_CONSENSUS_ESTIMATION_MS: u64 = - DEFAULT_BLOCK_TIME_MS + (DEFAULT_COMMIT_TIME_LIMIT_MS / 2); -} - -// Generate `ConfigurationView` without keys -view! { - /// `Sumeragi` configuration. - /// [`struct@Configuration`] provides an ability to define parameters such as `BLOCK_TIME_MS` - /// and a list of `TRUSTED_PEERS`. - #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Proxy)] - #[serde(rename_all = "UPPERCASE")] - #[config(env_prefix = "SUMERAGI_")] - pub struct Configuration { - /// The key pair consisting of a private and a public key. - //TODO: consider putting a `#[serde(skip)]` on the proxy struct here - #[view(ignore)] - pub key_pair: KeyPair, - /// Current Peer Identification. - pub peer_id: PeerId, - /// The period of time a peer waits for the `CreatedBlock` message after getting a `TransactionReceipt` - pub block_time_ms: u64, - /// Optional list of predefined trusted peers. - pub trusted_peers: TrustedPeers, - /// The period of time a peer waits for `CommitMessage` from the proxy tail. - pub commit_time_limit_ms: u64, - /// The upper limit of the number of transactions per block. - pub max_transactions_in_block: u32, - /// Buffer capacity of actor's MPSC channel - pub actor_channel_capacity: u32, - /// max number of transactions in tx gossip batch message. While configuring this, pay attention to `p2p` max message size. - pub gossip_batch_size: u32, - /// Period in milliseconds for pending transaction gossiping between peers. - pub gossip_period_ms: u64, - #[cfg(debug_assertions)] - /// Only used in testing. Causes the genesis peer to withhold blocks when it - /// is the proxy tail. - pub debug_force_soft_fork: bool, - } -} - -impl Default for ConfigurationProxy { - fn default() -> Self { - Self { - key_pair: None, - peer_id: None, - trusted_peers: None, - block_time_ms: Some(DEFAULT_BLOCK_TIME_MS), - commit_time_limit_ms: Some(DEFAULT_COMMIT_TIME_LIMIT_MS), - actor_channel_capacity: Some(DEFAULT_ACTOR_CHANNEL_CAPACITY), - gossip_batch_size: Some(DEFAULT_GOSSIP_BATCH_SIZE), - gossip_period_ms: Some(DEFAULT_GOSSIP_PERIOD_MS), - max_transactions_in_block: Some(DEFAULT_MAX_TRANSACTIONS_IN_BLOCK), - #[cfg(debug_assertions)] - debug_force_soft_fork: Some(false), - } - } -} -impl ConfigurationProxy { - /// To be used for proxy finalisation. Should only be - /// used if no peers are present. - /// - /// # Panics - /// The [`peer_id`] field of [`Self`] - /// has not been initialized prior to calling this method. - pub fn insert_self_as_trusted_peers(&mut self) { - let peer_id = self - .peer_id - .as_ref() - .expect("Insertion of `self` as `trusted_peers` implies that `peer_id` field should be initialized"); - self.trusted_peers = if let Some(mut trusted_peers) = self.trusted_peers.take() { - trusted_peers.peers.push(peer_id.clone()); - Some(trusted_peers) - } else { - Some(TrustedPeers { - peers: unique_vec![peer_id.clone()], - }) - }; - } -} - -impl Configuration { - /// Time estimation from receiving a transaction to storing it in - /// a block on all peers for the "sunny day" scenario. - #[inline] - #[must_use] - pub const fn pipeline_time_ms(&self) -> u64 { - self.block_time_ms + self.commit_time_limit_ms - } -} - -/// Part of the [`Configuration`]. It is separated from the main structure in order to be able -/// to load it from a separate file (see [`TrustedPeers::from_path`]). -#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize, Serialize)] -#[serde(rename_all = "UPPERCASE")] -#[serde(transparent)] -#[repr(transparent)] -pub struct TrustedPeers { - /// Optional list of predefined trusted peers. Must contain unique - /// entries. Custom deserializer raises error if duplicates found. - #[serde(deserialize_with = "UniqueVec::display_deserialize_failing_on_duplicates")] - pub peers: UniqueVec, -} - -impl TrustedPeers { - /// Load trusted peers variables from JSON. - /// - /// # Errors - /// - File not found - /// - File is not Valid JSON. - /// - File is valid JSON, but configuration options don't match. - pub fn from_path + Debug>(path: P) -> Result { - let file = File::open(&path) - .wrap_err_with(|| format!("Failed to open trusted peers file {:?}", &path))?; - let reader = BufReader::new(file); - serde_json::from_reader(reader) - .wrap_err("Failed to deserialize json from reader") - .map_err(Into::into) - } -} - -#[cfg(test)] -pub mod tests { - use proptest::prelude::*; - - use super::*; - - prop_compose! { - #[allow(unused_variables)] - pub fn arb_proxy() - (key_pair in Just(None), - peer_id in Just(None), - block_time_ms in prop::option::of(Just(DEFAULT_BLOCK_TIME_MS)), - trusted_peers in Just(None), - commit_time_limit_ms in prop::option::of(Just(DEFAULT_COMMIT_TIME_LIMIT_MS)), - actor_channel_capacity in prop::option::of(Just(DEFAULT_ACTOR_CHANNEL_CAPACITY)), - gossip_batch_size in prop::option::of(Just(DEFAULT_GOSSIP_BATCH_SIZE)), - gossip_period_ms in prop::option::of(Just(DEFAULT_GOSSIP_PERIOD_MS)), - max_transactions_in_block in prop::option::of(Just(DEFAULT_MAX_TRANSACTIONS_IN_BLOCK)), - debug_force_soft_fork in prop::option::of(Just(false)), - ) - -> ConfigurationProxy { - ConfigurationProxy { - key_pair, - peer_id, - block_time_ms, - trusted_peers, - commit_time_limit_ms, - max_transactions_in_block, - actor_channel_capacity, - gossip_batch_size, - gossip_period_ms, - #[cfg(debug_assertions)] - debug_force_soft_fork - } - } - } -} diff --git a/config/src/telemetry.rs b/config/src/telemetry.rs deleted file mode 100644 index b7ce10f9ee4..00000000000 --- a/config/src/telemetry.rs +++ /dev/null @@ -1,117 +0,0 @@ -//! Module for telemetry-related configuration and structs. -use std::path::PathBuf; - -use iroha_config_base::derive::Proxy; -use serde::{Deserialize, Serialize}; -use url::Url; - -/// Configuration parameters container -#[derive(Clone, Deserialize, Serialize, Debug, Proxy, PartialEq, Eq)] -#[serde(rename_all = "UPPERCASE")] -#[config(env_prefix = "TELEMETRY_")] -pub struct Configuration { - /// The node's name to be seen on the telemetry - #[config(serde_as_str)] - pub name: Option, - /// The url of the telemetry, e.g., ws://127.0.0.1:8001/submit - #[config(serde_as_str)] - pub url: Option, - /// The minimum period of time in seconds to wait before reconnecting - pub min_retry_period: u64, - /// The maximum exponent of 2 that is used for increasing delay between reconnections - pub max_retry_delay_exponent: u8, - /// The filepath that to write dev-telemetry to - #[config(serde_as_str)] - pub file: Option, -} - -/// Complete configuration needed to start regular telemetry. -pub struct RegularTelemetryConfig { - #[allow(missing_docs)] - pub name: String, - #[allow(missing_docs)] - pub url: Url, - #[allow(missing_docs)] - pub min_retry_period: u64, - #[allow(missing_docs)] - pub max_retry_delay_exponent: u8, -} - -/// Complete configuration needed to start dev telemetry. -pub struct DevTelemetryConfig { - #[allow(missing_docs)] - pub file: PathBuf, -} - -impl Configuration { - /// Parses user-provided configuration into stronger typed structures - /// - /// Should be refactored with [#3500](https://github.com/hyperledger/iroha/issues/3500) - pub fn parse(&self) -> (Option, Option) { - let Self { - ref name, - ref url, - max_retry_delay_exponent, - min_retry_period, - ref file, - } = *self; - - let regular = if let (Some(name), Some(url)) = (name, url) { - Some(RegularTelemetryConfig { - name: name.clone(), - url: url.clone(), - max_retry_delay_exponent, - min_retry_period, - }) - } else { - None - }; - - let dev = file - .as_ref() - .map(|file| DevTelemetryConfig { file: file.clone() }); - - (regular, dev) - } -} - -impl Default for ConfigurationProxy { - fn default() -> Self { - Self { - name: Some(None), - url: Some(None), - min_retry_period: Some(retry_period::DEFAULT_MIN_RETRY_PERIOD), - max_retry_delay_exponent: Some(retry_period::DEFAULT_MAX_RETRY_DELAY_EXPONENT), - file: Some(None), - } - } -} - -/// `RetryPeriod` configuration -pub mod retry_period { - /// Default minimal retry period - pub const DEFAULT_MIN_RETRY_PERIOD: u64 = 1; - /// Default maximum exponent for the retry delay - pub const DEFAULT_MAX_RETRY_DELAY_EXPONENT: u8 = 4; -} - -#[cfg(test)] -pub mod tests { - use proptest::prelude::*; - - use super::*; - - prop_compose! { - pub fn arb_proxy() - ( - name in prop::option::of(Just(None)), - url in prop::option::of(Just(None)), - min_retry_period in prop::option::of(Just(retry_period::DEFAULT_MIN_RETRY_PERIOD)), - max_retry_delay_exponent in prop::option::of(Just(retry_period::DEFAULT_MAX_RETRY_DELAY_EXPONENT)), - file in prop::option::of(Just(None)), - ) - -> ConfigurationProxy { - ConfigurationProxy { name, url, min_retry_period, max_retry_delay_exponent, file } - } - } -} diff --git a/config/src/torii.rs b/config/src/torii.rs deleted file mode 100644 index d77457f0ddb..00000000000 --- a/config/src/torii.rs +++ /dev/null @@ -1,99 +0,0 @@ -//! `Torii` configuration as well as the default values for the URLs used for the main endpoints: `p2p`, `telemetry`, but not `api`. - -use iroha_config_base::derive::Proxy; -use iroha_primitives::addr::{socket_addr, SocketAddr}; -use serde::{Deserialize, Serialize}; - -/// Default socket for p2p communication -pub const DEFAULT_TORII_P2P_ADDR: SocketAddr = socket_addr!(127.0.0.1:1337); -/// Default maximum size of single transaction -pub const DEFAULT_TORII_MAX_TRANSACTION_SIZE: u32 = 2_u32.pow(15); -/// Default upper bound on `content-length` specified in the HTTP request header -pub const DEFAULT_TORII_MAX_CONTENT_LENGTH: u32 = 2_u32.pow(12) * 4000; - -/// Structure that defines the configuration parameters of `Torii` which is the routing module. -/// For example the `p2p_addr`, which is used for consensus and block-synchronisation purposes, -/// as well as `max_transaction_size`. -#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Proxy)] -#[serde(rename_all = "UPPERCASE")] -#[config(env_prefix = "TORII_")] -pub struct Configuration { - /// Torii address for p2p communication for consensus and block synchronization purposes. - #[config(serde_as_str)] - pub p2p_addr: SocketAddr, - /// Torii address for client API. - #[config(serde_as_str)] - pub api_url: SocketAddr, - /// Maximum number of bytes in raw transaction. Used to prevent from DOS attacks. - pub max_transaction_size: u32, - /// Maximum number of bytes in raw message. Used to prevent from DOS attacks. - pub max_content_len: u32, -} - -impl Default for ConfigurationProxy { - fn default() -> Self { - Self { - p2p_addr: None, - api_url: None, - max_transaction_size: Some(DEFAULT_TORII_MAX_TRANSACTION_SIZE), - max_content_len: Some(DEFAULT_TORII_MAX_CONTENT_LENGTH), - } - } -} - -pub mod uri { - //! URI that `Torii` uses to route incoming requests. - - /// Default socket for listening on external requests - pub const DEFAULT_API_ADDR: iroha_primitives::addr::SocketAddr = - iroha_primitives::addr::socket_addr!(127.0.0.1:8080); - /// Query URI is used to handle incoming Query requests. - pub const QUERY: &str = "query"; - /// Transaction URI is used to handle incoming ISI requests. - pub const TRANSACTION: &str = "transaction"; - /// Block URI is used to handle incoming Block requests. - pub const CONSENSUS: &str = "consensus"; - /// Health URI is used to handle incoming Healthcheck requests. - pub const HEALTH: &str = "health"; - /// The URI used for block synchronization. - pub const BLOCK_SYNC: &str = "block/sync"; - /// The web socket uri used to subscribe to block and transactions statuses. - pub const SUBSCRIPTION: &str = "events"; - /// The web socket uri used to subscribe to blocks stream. - pub const BLOCKS_STREAM: &str = "block/stream"; - /// Get pending transactions. - pub const MATCHING_PENDING_TRANSACTIONS: &str = "matching_pending_transactions"; - /// The URI for local config changing inspecting - pub const CONFIGURATION: &str = "configuration"; - /// URI to report status for administration - pub const STATUS: &str = "status"; - /// Metrics URI is used to export metrics according to [Prometheus - /// Guidance](https://prometheus.io/docs/instrumenting/writing_exporters/). - pub const METRICS: &str = "metrics"; - /// URI for retrieving the schema with which Iroha was built. - pub const SCHEMA: &str = "schema"; - /// URI for getting the API version currently used - pub const API_VERSION: &str = "api_version"; - /// URI for getting cpu profile - pub const PROFILE: &str = "debug/pprof/profile"; -} - -#[cfg(test)] -pub mod tests { - use proptest::prelude::*; - - use super::*; - - prop_compose! { - pub fn arb_proxy() - ( - p2p_addr in prop::option::of(Just(DEFAULT_TORII_P2P_ADDR)), - api_url in prop::option::of(Just(uri::DEFAULT_API_ADDR)), - max_transaction_size in prop::option::of(Just(DEFAULT_TORII_MAX_TRANSACTION_SIZE)), - max_content_len in prop::option::of(Just(DEFAULT_TORII_MAX_CONTENT_LENGTH)), - ) - -> ConfigurationProxy { - ConfigurationProxy { p2p_addr, api_url, max_transaction_size, max_content_len } - } - } -} diff --git a/config/src/wasm.rs b/config/src/wasm.rs index cd55fd989af..8b137891791 100644 --- a/config/src/wasm.rs +++ b/config/src/wasm.rs @@ -1,35 +1 @@ -//! Module for wasm-related configuration and structs. -use iroha_config_base::derive::Proxy; -use serde::{Deserialize, Serialize}; -use self::default::*; - -/// Module with a set of default values. -pub mod default { - /// Default amount of fuel provided for execution - pub const DEFAULT_FUEL_LIMIT: u64 = 55_000_000; - /// Default amount of memory given for smart contract - pub const DEFAULT_MAX_MEMORY: u32 = 500 * 2_u32.pow(20); // 500 MiB -} - -/// `WebAssembly Runtime` configuration. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, Proxy)] -#[config(env_prefix = "WASM_")] -#[serde(rename_all = "UPPERCASE")] -pub struct Configuration { - /// The fuel limit determines the maximum number of instructions that can be executed within a smart contract. - /// Every WASM instruction costs approximately 1 unit of fuel. See - /// [`wasmtime` reference](https://docs.rs/wasmtime/0.29.0/wasmtime/struct.Store.html#method.add_fuel) - pub fuel_limit: u64, - /// Maximum amount of linear memory a given smart contract can allocate. - pub max_memory: u32, -} - -impl Default for ConfigurationProxy { - fn default() -> Self { - Self { - fuel_limit: Some(DEFAULT_FUEL_LIMIT), - max_memory: Some(DEFAULT_MAX_MEMORY), - } - } -} diff --git a/config/src/wsv.rs b/config/src/wsv.rs deleted file mode 100644 index dcb23b23d85..00000000000 --- a/config/src/wsv.rs +++ /dev/null @@ -1,86 +0,0 @@ -//! Module for `WorldStateView`-related configuration and structs. -use default::*; -use iroha_config_base::derive::Proxy; -use iroha_data_model::{prelude::*, transaction::TransactionLimits}; -use serde::{Deserialize, Serialize}; - -use crate::wasm; - -/// Module with a set of default values. -pub mod default { - use super::*; - - /// Default limits for metadata - pub const DEFAULT_METADATA_LIMITS: MetadataLimits = - MetadataLimits::new(2_u32.pow(20), 2_u32.pow(12)); - /// Default limits for ident length - pub const DEFAULT_IDENT_LENGTH_LIMITS: LengthLimits = LengthLimits::new(1, 2_u32.pow(7)); - /// Default maximum number of instructions and expressions per transaction - pub const DEFAULT_MAX_INSTRUCTION_NUMBER: u64 = 2_u64.pow(12); - /// Default maximum number of instructions and expressions per transaction - pub const DEFAULT_MAX_WASM_SIZE_BYTES: u64 = 2_u64.pow(22); // 4 MiB - - /// Default transaction limits - pub const DEFAULT_TRANSACTION_LIMITS: TransactionLimits = - TransactionLimits::new(DEFAULT_MAX_INSTRUCTION_NUMBER, DEFAULT_MAX_WASM_SIZE_BYTES); -} - -/// `WorldStateView` configuration. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, Proxy)] -#[config(env_prefix = "WSV_")] -#[serde(rename_all = "UPPERCASE")] -pub struct Configuration { - /// [`MetadataLimits`] for every asset with store. - pub asset_metadata_limits: MetadataLimits, - /// [`MetadataLimits`] of any asset definition metadata. - pub asset_definition_metadata_limits: MetadataLimits, - /// [`MetadataLimits`] of any account metadata. - pub account_metadata_limits: MetadataLimits, - /// [`MetadataLimits`] of any domain metadata. - pub domain_metadata_limits: MetadataLimits, - /// [`LengthLimits`] for the number of chars in identifiers that can be stored in the WSV. - pub ident_length_limits: LengthLimits, - /// Limits that all transactions need to obey, in terms of size - /// of WASM blob and number of instructions. - pub transaction_limits: TransactionLimits, - /// WASM runtime configuration - #[config(inner)] - pub wasm_runtime_config: wasm::Configuration, -} - -impl Default for ConfigurationProxy { - fn default() -> Self { - Self { - asset_metadata_limits: Some(DEFAULT_METADATA_LIMITS), - asset_definition_metadata_limits: Some(DEFAULT_METADATA_LIMITS), - account_metadata_limits: Some(DEFAULT_METADATA_LIMITS), - domain_metadata_limits: Some(DEFAULT_METADATA_LIMITS), - ident_length_limits: Some(DEFAULT_IDENT_LENGTH_LIMITS), - transaction_limits: Some(DEFAULT_TRANSACTION_LIMITS), - wasm_runtime_config: Some(wasm::ConfigurationProxy::default()), - } - } -} - -#[cfg(test)] -pub mod tests { - use proptest::prelude::*; - - use super::*; - - prop_compose! { - pub fn arb_proxy() - ( - asset_metadata_limits in prop::option::of(Just(DEFAULT_METADATA_LIMITS)), - asset_definition_metadata_limits in prop::option::of(Just(DEFAULT_METADATA_LIMITS)), - account_metadata_limits in prop::option::of(Just(DEFAULT_METADATA_LIMITS)), - domain_metadata_limits in prop::option::of(Just(DEFAULT_METADATA_LIMITS)), - ident_length_limits in prop::option::of(Just(DEFAULT_IDENT_LENGTH_LIMITS)), - transaction_limits in prop::option::of(Just(DEFAULT_TRANSACTION_LIMITS)), - wasm_runtime_config in prop::option::of(Just(wasm::ConfigurationProxy::default())), - ) - -> ConfigurationProxy { - ConfigurationProxy { asset_metadata_limits, asset_definition_metadata_limits, account_metadata_limits, domain_metadata_limits, ident_length_limits, transaction_limits, wasm_runtime_config } - } - } -} diff --git a/config/test/config.toml b/config/test/config.toml new file mode 100644 index 00000000000..d88f7ec119e --- /dev/null +++ b/config/test/config.toml @@ -0,0 +1,7 @@ +[iroha] +public_key = "ed0120FAFCB2B27444221717F6FCBF900D5BE95273B1B0904B08C736B32A19F16AC1F9" +private_key = { digest = "ed25519", payload = "82886B5A2BB3785F3CA8F8A78F60EA9DB62F939937B1CFA8407316EF07909A8D236808A6D4C12C91CA19E54686C2B8F5F3A786278E3824B4571EF234DEC8683B" } +p2p_address = "localhost:1337" + +[torii] +api_address = "localhost:8080" \ No newline at end of file diff --git a/config/tests/fixtures.rs b/config/tests/fixtures.rs new file mode 100644 index 00000000000..214290a1eb2 --- /dev/null +++ b/config/tests/fixtures.rs @@ -0,0 +1,519 @@ +#![allow(clippy::needless_raw_string_hashes)] // triggered by `expect_test` snapshots + +use std::{ + collections::{HashMap, HashSet}, + fs, + path::{Path, PathBuf}, +}; + +use eyre::Result; +use iroha_config::parameters::{ + actual::{Genesis, Root}, + user::{CliContext, RootPartial}, +}; +use iroha_config_base::{FromEnv, TestEnv, UnwrapPartial as _}; + +fn fixtures_dir() -> PathBuf { + // CWD is the crate's root + PathBuf::from("tests/fixtures") +} + +fn parse_env(raw: impl AsRef) -> HashMap { + raw.as_ref() + .lines() + .map(|line| { + let mut items = line.split('='); + let key = items + .next() + .expect("line should be in {key}={value} format"); + let value = items + .next() + .expect("line should be in {key}={value} format"); + (key.to_string(), value.to_string()) + }) + .collect() +} + +fn test_env_from_file(p: impl AsRef) -> TestEnv { + let contents = fs::read_to_string(p).expect("the path should be valid"); + let map = parse_env(contents); + TestEnv::with_map(map) +} + +/// This test not only asserts that the minimal set of fields is enough; +/// it also gives an insight into every single default value +#[test] +#[allow(clippy::too_many_lines)] +fn minimal_config_snapshot() -> Result<()> { + let config = RootPartial::from_toml(fixtures_dir().join("minimal_with_trusted_peers.toml"))? + .unwrap_partial()? + .parse(CliContext { + submit_genesis: false, + })?; + + let expected = expect_test::expect![[r#" + Root { + common: Common { + chain_id: ChainId( + "0", + ), + key_pair: KeyPair { + public_key: PublicKey( + ed25519( + "ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB", + ), + ), + private_key: ed25519( + "8F4C15E5D664DA3F13778801D23D4E89B76E94C1B94B389544168B6CB894F84F8BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB", + ), + }, + p2p_address: 127.0.0.1:1337, + }, + genesis: Partial { + public_key: PublicKey( + ed25519( + "ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB", + ), + ), + }, + torii: Torii { + address: 127.0.0.1:8080, + max_content_len_bytes: 16777216, + }, + kura: Kura { + init_mode: Strict, + store_dir: "./storage", + debug_output_new_blocks: false, + }, + sumeragi: Sumeragi { + trusted_peers: UniqueVec( + [ + PeerId { + address: 127.0.0.1:1338, + public_key: PublicKey( + ed25519( + "ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB", + ), + ), + }, + ], + ), + debug_force_soft_fork: false, + }, + block_sync: BlockSync { + gossip_period: 10s, + gossip_max_size: 4, + }, + transaction_gossiper: TransactionGossiper { + gossip_period: 1s, + gossip_max_size: 500, + }, + live_query_store: LiveQueryStore { + idle_time: 30s, + }, + logger: Logger { + level: INFO, + format: Full, + tokio_console_address: 127.0.0.1:5555, + }, + queue: Queue { + capacity: 65536, + capacity_per_user: 65536, + transaction_time_to_live: 86400s, + future_threshold: 1s, + }, + snapshot: Snapshot { + create_every: 60s, + store_dir: "./storage/snapshot", + creation_enabled: true, + }, + telemetry: None, + dev_telemetry: None, + chain_wide: ChainWide { + max_transactions_in_block: 512, + block_time: 2s, + commit_time: 4s, + transaction_limits: TransactionLimits { + max_instruction_number: 4096, + max_wasm_size_bytes: 4194304, + }, + asset_metadata_limits: Limits { + max_len: 1048576, + max_entry_byte_size: 4096, + }, + asset_definition_metadata_limits: Limits { + max_len: 1048576, + max_entry_byte_size: 4096, + }, + account_metadata_limits: Limits { + max_len: 1048576, + max_entry_byte_size: 4096, + }, + domain_metadata_limits: Limits { + max_len: 1048576, + max_entry_byte_size: 4096, + }, + ident_length_limits: LengthLimits { + min: 1, + max: 128, + }, + wasm_runtime: WasmRuntime { + fuel_limit: 55000000, + max_memory_bytes: 524288000, + }, + }, + }"#]]; + expected.assert_eq(&format!("{config:#?}")); + + Ok(()) +} + +#[test] +fn config_with_genesis() -> Result<()> { + let _config = RootPartial::from_toml(fixtures_dir().join("minimal_alone_with_genesis.toml"))? + .unwrap_partial()? + .parse(CliContext { + submit_genesis: true, + })?; + Ok(()) +} + +#[test] +fn minimal_with_genesis_but_no_cli_arg_fails() -> Result<()> { + let error = RootPartial::from_toml(fixtures_dir().join("minimal_alone_with_genesis.toml"))? + .unwrap_partial()? + .parse(CliContext { + submit_genesis: false, + }) + .expect_err("should fail since `--submit-genesis=false`"); + + let expected = expect_test::expect![[r#" + `genesis.file` and `genesis.private_key` are presented, but `--submit-genesis` was not set + The network consists from this one peer only (no `sumeragi.trusted_peers` provided). Since `--submit-genesis` is not set, there is no way to receive the genesis block. Either provide the genesis by setting `--submit-genesis` argument, `genesis.private_key`, and `genesis.file` configuration parameters, or increase the number of trusted peers in the network using `sumeragi.trusted_peers` configuration parameter."#]]; + expected.assert_eq(&format!("{error:#}")); + + Ok(()) +} + +#[test] +fn minimal_without_genesis_but_with_submit_fails() -> Result<()> { + let error = RootPartial::from_toml(fixtures_dir().join("minimal_with_trusted_peers.toml"))? + .unwrap_partial()? + .parse(CliContext { + submit_genesis: true, + }) + .expect_err( + "should fail since there is no genesis in the config, but `--submit-genesis=true`", + ); + + let expected = expect_test::expect!["`--submit-genesis` was set, but `genesis.file` and `genesis.private_key` are not presented"]; + expected.assert_eq(&format!("{error:#}")); + + Ok(()) +} + +#[test] +fn self_is_presented_in_trusted_peers() -> Result<()> { + let config = RootPartial::from_toml(fixtures_dir().join("minimal_alone_with_genesis.toml"))? + .unwrap_partial()? + .parse(CliContext { + submit_genesis: true, + })?; + + assert!(config + .sumeragi + .trusted_peers + .contains(&config.common.peer_id())); + + Ok(()) +} + +#[test] +fn missing_fields() -> Result<()> { + let error = RootPartial::from_toml(fixtures_dir().join("bad.missing_fields.toml"))? + .unwrap_partial() + .expect_err("should fail with missing fields"); + + let expected = expect_test::expect![[r#" + missing field: `chain_id` + missing field: `public_key` + missing field: `private_key` + missing field: `genesis.public_key` + missing field: `network.address` + missing field: `torii.address`"#]]; + expected.assert_eq(&format!("{error:#}")); + + Ok(()) +} + +#[test] +fn extra_fields() { + let error = RootPartial::from_toml(fixtures_dir().join("extra_fields.toml")) + .expect_err("should fail with extra fields"); + + let expected = expect_test::expect!["cannot open file at location `tests/fixtures/extra_fields.toml`: No such file or directory (os error 2)"]; + expected.assert_eq(&format!("{error:#}")); +} + +#[test] +fn inconsistent_genesis_config() -> Result<()> { + let error = RootPartial::from_toml(fixtures_dir().join("inconsistent_genesis.toml"))? + .unwrap_partial() + .expect("all fields are present") + .parse(CliContext { + submit_genesis: false, + }) + .expect_err("should fail with bad genesis config"); + + let expected = expect_test::expect![[r#" + `genesis.file` and `genesis.private_key` should be set together + The network consists from this one peer only (no `sumeragi.trusted_peers` provided). Since `--submit-genesis` is not set, there is no way to receive the genesis block. Either provide the genesis by setting `--submit-genesis` argument, `genesis.private_key`, and `genesis.file` configuration parameters, or increase the number of trusted peers in the network using `sumeragi.trusted_peers` configuration parameter."#]]; + expected.assert_eq(&format!("{error:#}")); + + Ok(()) +} + +/// Aims the purpose of checking that every single provided env variable is consumed and parsed +/// into a valid config. +#[test] +#[allow(clippy::too_many_lines)] +fn full_envs_set_is_consumed() -> Result<()> { + let env = test_env_from_file(fixtures_dir().join("full.env")); + + let layer = RootPartial::from_env(&env)?; + + assert_eq!(env.unvisited(), HashSet::new()); + + let expected = expect_test::expect![[r#" + RootPartial { + extends: None, + chain_id: Some( + ChainId( + "0-0", + ), + ), + public_key: Some( + PublicKey( + ed25519( + "ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB", + ), + ), + ), + private_key: Some( + ed25519( + "8F4C15E5D664DA3F13778801D23D4E89B76E94C1B94B389544168B6CB894F84F8BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB", + ), + ), + genesis: GenesisPartial { + public_key: Some( + PublicKey( + ed25519( + "ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB", + ), + ), + ), + private_key: Some( + ed25519( + "8F4C15E5D664DA3F13778801D23D4E89B76E94C1B94B389544168B6CB894F84F8BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB", + ), + ), + file: None, + }, + kura: KuraPartial { + init_mode: Some( + Strict, + ), + store_dir: Some( + "/store/path/from/env", + ), + debug: KuraDebugPartial { + output_new_blocks: Some( + false, + ), + }, + }, + sumeragi: SumeragiPartial { + trusted_peers: None, + debug: SumeragiDebugPartial { + force_soft_fork: None, + }, + }, + network: NetworkPartial { + address: Some( + 127.0.0.1:5432, + ), + block_gossip_max_size: None, + block_gossip_period: None, + transaction_gossip_max_size: None, + transaction_gossip_period: None, + }, + logger: LoggerPartial { + level: Some( + DEBUG, + ), + format: Some( + Pretty, + ), + tokio_console_address: None, + }, + queue: QueuePartial { + capacity: None, + capacity_per_user: None, + transaction_time_to_live: None, + future_threshold: None, + }, + snapshot: SnapshotPartial { + create_every: None, + store_dir: Some( + "/snapshot/path/from/env", + ), + creation_enabled: Some( + false, + ), + }, + telemetry: TelemetryPartial { + name: None, + url: None, + min_retry_period: None, + max_retry_delay_exponent: None, + dev: TelemetryDevPartial { + out_file: None, + }, + }, + torii: ToriiPartial { + address: Some( + 127.0.0.1:8080, + ), + max_content_len: None, + query_idle_time: None, + }, + chain_wide: ChainWidePartial { + max_transactions_in_block: None, + block_time: None, + commit_time: None, + transaction_limits: None, + asset_metadata_limits: None, + asset_definition_metadata_limits: None, + account_metadata_limits: None, + domain_metadata_limits: None, + ident_length_limits: None, + wasm_fuel_limit: None, + wasm_max_memory: None, + }, + }"#]]; + expected.assert_eq(&format!("{layer:#?}")); + + Ok(()) +} + +#[test] +fn multiple_env_parsing_errors() { + let env = test_env_from_file(fixtures_dir().join("bad.multiple_bad_envs.env")); + + let error = RootPartial::from_env(&env).expect_err("the input from env is invalid"); + + let expected = expect_test::expect![[r#" + `PRIVATE_KEY_PAYLOAD` env was provided, but `PRIVATE_KEY_DIGEST` was not + failed to parse `genesis.private_key.digest_function` field from `GENESIS_PRIVATE_KEY_DIGEST` env variable + failed to parse `kura.debug.output_new_blocks` field from `KURA_DEBUG_OUTPUT_NEW_BLOCKS` env variable + failed to parse `logger.format` field from `LOG_FORMAT` env variable + failed to parse `torii.address` field from `API_ADDRESS` env variable"#]]; + expected.assert_eq(&format!("{error:#}")); +} + +#[test] +fn config_from_file_and_env() -> Result<()> { + let env = test_env_from_file(fixtures_dir().join("minimal_file_and_env.env")); + + let _config = RootPartial::from_toml(fixtures_dir().join("minimal_file_and_env.toml"))? + .merge(RootPartial::from_env(&env)?) + .unwrap_partial()? + .parse(CliContext { + submit_genesis: false, + })?; + + Ok(()) +} + +#[test] +fn fails_if_torii_address_and_p2p_address_are_equal() -> Result<()> { + let error = RootPartial::from_toml(fixtures_dir().join("bad.torii_addr_eq_p2p_addr.toml"))? + .unwrap_partial() + .expect("should not fail, all fields are present") + .parse(CliContext { + submit_genesis: false, + }) + .expect_err("should fail because of bad input"); + + let expected = + expect_test::expect!["`iroha.p2p_address` and `torii.address` should not be the same"]; + expected.assert_eq(&format!("{error:#}")); + + Ok(()) +} + +#[test] +fn fails_if_extends_leads_to_nowhere() { + let error = RootPartial::from_toml(fixtures_dir().join("bad.extends_nowhere.toml")) + .expect_err("should fail with bad input"); + + let expected = expect_test::expect!["cannot extend from `tests/fixtures/nowhere.toml`: cannot open file at location `tests/fixtures/nowhere.toml`: No such file or directory (os error 2)"]; + expected.assert_eq(&format!("{error:#}")); +} + +#[test] +fn multiple_extends_works() -> Result<()> { + // we are looking into `logger` in particular + let layer = RootPartial::from_toml(fixtures_dir().join("multiple_extends.toml"))?.logger; + + let expected = expect_test::expect![[r#" + LoggerPartial { + level: Some( + ERROR, + ), + format: Some( + Compact, + ), + tokio_console_address: None, + }"#]]; + expected.assert_eq(&format!("{layer:#?}")); + + Ok(()) +} + +#[test] +fn full_config_parses_fine() { + let _cfg = Root::load( + Some(fixtures_dir().join("full.toml")), + CliContext { + submit_genesis: true, + }, + ) + .expect("should be fine"); +} + +#[test] +fn absolute_paths_are_preserved() { + let cfg = Root::load( + Some(fixtures_dir().join("absolute_paths.toml")), + CliContext { + submit_genesis: true, + }, + ) + .expect("should be fine"); + + assert_eq!(cfg.kura.store_dir, PathBuf::from("/kura/store")); + assert_eq!(cfg.snapshot.store_dir, PathBuf::from("/snapshot/store")); + assert_eq!( + cfg.dev_telemetry.unwrap().out_file, + PathBuf::from("/telemetry/file.json") + ); + if let Genesis::Full { + file: genesis_file, .. + } = cfg.genesis + { + assert_eq!(genesis_file, PathBuf::from("/oh/my/genesis.json")); + } else { + unreachable!() + }; +} diff --git a/config/tests/fixtures/absolute_paths.toml b/config/tests/fixtures/absolute_paths.toml new file mode 100644 index 00000000000..0d1f3d3f3d5 --- /dev/null +++ b/config/tests/fixtures/absolute_paths.toml @@ -0,0 +1,14 @@ +extends = ["base.toml"] + +[kura] +store_dir = "/kura/store" + +[snapshot] +store_dir = "/snapshot/store" + +[telemetry.dev] +out_file = "/telemetry/file.json" + +[genesis] +file = "/oh/my/genesis.json" +private_key = { digest_function = "ed25519", payload = "8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb" } \ No newline at end of file diff --git a/config/tests/fixtures/bad.extends_nowhere.toml b/config/tests/fixtures/bad.extends_nowhere.toml new file mode 100644 index 00000000000..30129b39359 --- /dev/null +++ b/config/tests/fixtures/bad.extends_nowhere.toml @@ -0,0 +1 @@ +extends = "nowhere.toml" \ No newline at end of file diff --git a/config/tests/fixtures/bad.extra_fields.toml b/config/tests/fixtures/bad.extra_fields.toml new file mode 100644 index 00000000000..bc2baaf8783 --- /dev/null +++ b/config/tests/fixtures/bad.extra_fields.toml @@ -0,0 +1,4 @@ +# Iroha should not silently ignore extra fields +i_am_unknown = true +foo = false +bar = 0.5 \ No newline at end of file diff --git a/config/tests/fixtures/bad.missing_fields.toml b/config/tests/fixtures/bad.missing_fields.toml new file mode 100644 index 00000000000..d5bd33cac2e --- /dev/null +++ b/config/tests/fixtures/bad.missing_fields.toml @@ -0,0 +1 @@ +# all fields are missing \ No newline at end of file diff --git a/config/tests/fixtures/bad.multiple_bad_envs.env b/config/tests/fixtures/bad.multiple_bad_envs.env new file mode 100644 index 00000000000..12ab82cf92e --- /dev/null +++ b/config/tests/fixtures/bad.multiple_bad_envs.env @@ -0,0 +1,6 @@ +PRIVATE_KEY_PAYLOAD=8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb +GENESIS_PRIVATE_KEY_DIGEST=BAD BAD BAD +GENESIS_PRIVATE_KEY_PAYLOAD=8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb +API_ADDRESS=BAD BAD BAD +KURA_DEBUG_OUTPUT_NEW_BLOCKS=TrueЪ +LOG_FORMAT=what format? diff --git a/config/tests/fixtures/bad.torii_addr_eq_p2p_addr.toml b/config/tests/fixtures/bad.torii_addr_eq_p2p_addr.toml new file mode 100644 index 00000000000..79f9c324cee --- /dev/null +++ b/config/tests/fixtures/bad.torii_addr_eq_p2p_addr.toml @@ -0,0 +1,7 @@ +extends = ["base.toml", "base_trusted_peers.toml"] + +[network] +address = "127.0.0.1:8080" + +[torii] +address = "127.0.0.1:8080" diff --git a/config/tests/fixtures/base.toml b/config/tests/fixtures/base.toml new file mode 100644 index 00000000000..3ca6d219477 --- /dev/null +++ b/config/tests/fixtures/base.toml @@ -0,0 +1,13 @@ +chain_id = "0" +public_key = "ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB" +private_key.digest_function = "ed25519" +private_key.payload = "8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb" + +[network] +address = "127.0.0.1:1337" + +[genesis] +public_key = "ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB" + +[torii] +address = "127.0.0.1:8080" \ No newline at end of file diff --git a/config/tests/fixtures/base_trusted_peers.toml b/config/tests/fixtures/base_trusted_peers.toml new file mode 100644 index 00000000000..1314cd70026 --- /dev/null +++ b/config/tests/fixtures/base_trusted_peers.toml @@ -0,0 +1,3 @@ +[[sumeragi.trusted_peers]] +address = "127.0.0.1:1338" +public_key = "ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB" diff --git a/config/tests/fixtures/empty_ok_genesis.json b/config/tests/fixtures/empty_ok_genesis.json new file mode 100644 index 00000000000..21bcda658eb --- /dev/null +++ b/config/tests/fixtures/empty_ok_genesis.json @@ -0,0 +1,4 @@ +{ + "transactions": [], + "executor": "./executor.wasm" +} \ No newline at end of file diff --git a/config/tests/fixtures/full.env b/config/tests/fixtures/full.env new file mode 100644 index 00000000000..e79ed99d747 --- /dev/null +++ b/config/tests/fixtures/full.env @@ -0,0 +1,16 @@ +CHAIN_ID=0-0 +PUBLIC_KEY=ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB +PRIVATE_KEY_DIGEST=ed25519 +PRIVATE_KEY_PAYLOAD=8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb +P2P_ADDRESS=127.0.0.1:5432 +GENESIS_PUBLIC_KEY=ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB +GENESIS_PRIVATE_KEY_DIGEST=ed25519 +GENESIS_PRIVATE_KEY_PAYLOAD=8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb +API_ADDRESS=127.0.0.1:8080 +KURA_INIT_MODE=strict +KURA_STORE_DIR=/store/path/from/env +KURA_DEBUG_OUTPUT_NEW_BLOCKS=false +LOG_LEVEL=DEBUG +LOG_FORMAT=pretty +SNAPSHOT_STORE_DIR=/snapshot/path/from/env +SNAPSHOT_CREATION_ENABLED=false diff --git a/config/tests/fixtures/full.toml b/config/tests/fixtures/full.toml new file mode 100644 index 00000000000..878223301aa --- /dev/null +++ b/config/tests/fixtures/full.toml @@ -0,0 +1,74 @@ +# This config has ALL fields specified (except `extends`) + +chain_id = "0" +public_key = "ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB" +private_key = { digest_function = "ed25519", payload = "8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb" } + +[genesis] +file = "genesis.json" +public_key = "ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB" +private_key = { digest_function = "ed25519", payload = "8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb" } + +[network] +address = "localhost:3840" +block_gossip_period = 10_000 +block_gossip_max_size = 4 +transaction_gossip_period = 1_000 +transaction_gossip_max_size = 500 + +[torii] +address = "localhost:5000" +max_content_len = 16 +query_idle_time = 30_000 + +[kura] +init_mode = "strict" +store_dir = "./storage" + +[kura.debug] +output_new_blocks = true + +[[sumeragi.trusted_peers]] +address = "localhost:8081" +public_key = "ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB" + +[sumeragi.debug] +force_soft_fork = true + +[logger] +level = "TRACE" +format = "compact" +tokio_console_address = "127.0.0.1:5555" + +[queue] +capacity = 65536 +capacity_per_user = 65536 +transaction_time_to_live = 100 +future_threshold = 50 + +[snapshot] +creation_enabled = true +create_every = 60_000 +store_dir = "./storage/snapshot" + +[telemetry] +name = "test" +url = "http://test.com" +min_retry_period = 5_000 +max_retry_delay_exponent = 4 + +[telemetry.dev] +out_file = "./dev-telemetry.json5" + +[chain_wide] +max_transactions_in_block = 512 +block_time = 2_000 +commit_time = 4_000 +transaction_limits = {max_instruction_number = 4096, max_wasm_size_bytes = 4194304 } +asset_metadata_limits = { max_len = 1048576, max_entry_byte_size = 4096 } +asset_definition_metadata_limits = { max_len = 1048576, max_entry_byte_size = 4096 } +account_metadata_limits = { max_len = 1048576, max_entry_byte_size = 4096 } +domain_metadata_limits = { max_len = 1048576, max_entry_byte_size = 4096 } +ident_length_limits = { min = 1, max = 128 } +wasm_fuel_limit = 55000000 +wasm_max_memory = 524288000 diff --git a/config/tests/fixtures/inconsistent_genesis.toml b/config/tests/fixtures/inconsistent_genesis.toml new file mode 100644 index 00000000000..e6f38ffd2b6 --- /dev/null +++ b/config/tests/fixtures/inconsistent_genesis.toml @@ -0,0 +1,7 @@ +extends = "base.toml" + +[genesis] +private_key.digest_function = "ed25519" +private_key.payload = "8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb" +# should fail without it: +# file = ... diff --git a/config/tests/fixtures/minimal_alone_with_genesis.toml b/config/tests/fixtures/minimal_alone_with_genesis.toml new file mode 100644 index 00000000000..a6689041d21 --- /dev/null +++ b/config/tests/fixtures/minimal_alone_with_genesis.toml @@ -0,0 +1,6 @@ +extends = "base.toml" + +[genesis] +file = "./empty_ok_genesis.json" +private_key.digest_function = "ed25519" +private_key.payload = "8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb" diff --git a/config/tests/fixtures/minimal_file_and_env.env b/config/tests/fixtures/minimal_file_and_env.env new file mode 100644 index 00000000000..7ee9d329ee5 --- /dev/null +++ b/config/tests/fixtures/minimal_file_and_env.env @@ -0,0 +1 @@ +API_ADDRESS=127.0.0.1:8080 \ No newline at end of file diff --git a/config/tests/fixtures/minimal_file_and_env.toml b/config/tests/fixtures/minimal_file_and_env.toml new file mode 100644 index 00000000000..abdd50e85c2 --- /dev/null +++ b/config/tests/fixtures/minimal_file_and_env.toml @@ -0,0 +1,14 @@ +extends = "base_trusted_peers.toml" + +chain_id = "0" +public_key = "ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB" +private_key.digest_function = "ed25519" +private_key.payload = "8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb" + +[network] +address = "127.0.0.1:1337" + +[genesis] +public_key = "ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB" + +# `torii.address` should be in ENV diff --git a/config/tests/fixtures/minimal_with_trusted_peers.toml b/config/tests/fixtures/minimal_with_trusted_peers.toml new file mode 100644 index 00000000000..12ebd580cbc --- /dev/null +++ b/config/tests/fixtures/minimal_with_trusted_peers.toml @@ -0,0 +1 @@ +extends = ["base.toml", "base_trusted_peers.toml"] diff --git a/config/tests/fixtures/multiple_extends.1.toml b/config/tests/fixtures/multiple_extends.1.toml new file mode 100644 index 00000000000..46b1262777b --- /dev/null +++ b/config/tests/fixtures/multiple_extends.1.toml @@ -0,0 +1,2 @@ +[logger] +format = "pretty" \ No newline at end of file diff --git a/config/tests/fixtures/multiple_extends.2.toml b/config/tests/fixtures/multiple_extends.2.toml new file mode 100644 index 00000000000..47e9616ccfd --- /dev/null +++ b/config/tests/fixtures/multiple_extends.2.toml @@ -0,0 +1,5 @@ +# sets level +extends = "multiple_extends.2a.toml" + +[logger] +format = "compact" \ No newline at end of file diff --git a/config/tests/fixtures/multiple_extends.2a.toml b/config/tests/fixtures/multiple_extends.2a.toml new file mode 100644 index 00000000000..c7b048bc674 --- /dev/null +++ b/config/tests/fixtures/multiple_extends.2a.toml @@ -0,0 +1,2 @@ +[logger] +level = "DEBUG" \ No newline at end of file diff --git a/config/tests/fixtures/multiple_extends.toml b/config/tests/fixtures/multiple_extends.toml new file mode 100644 index 00000000000..83b87043034 --- /dev/null +++ b/config/tests/fixtures/multiple_extends.toml @@ -0,0 +1,6 @@ +# 1 - sets format, 2 - sets format and level +extends = ["multiple_extends.1.toml", "multiple_extends.2.toml"] + +[logger] +# final value +level = "ERROR" \ No newline at end of file diff --git a/configs/client.template.toml b/configs/client.template.toml new file mode 100644 index 00000000000..3bad84abcc5 --- /dev/null +++ b/configs/client.template.toml @@ -0,0 +1,19 @@ +# chain_id = + +## Might be set via `TORII_URL` env var +# torii_url = + +[basic_auth] +# login = +# password = + +[account] +# id = +# public_key = +# private_key = { algorithm = "", payload = "" } + +[transaction] +# time_to_live = "100s" +# status_timeout = "100s" +## Nonce is TODO describe what it is +# nonce = false diff --git a/configs/client/config.json b/configs/client/config.json deleted file mode 100644 index b8a507409ac..00000000000 --- a/configs/client/config.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "CHAIN_ID": "00000000-0000-0000-0000-000000000000", - "PUBLIC_KEY": "ed01207233BFC89DCBD68C19FDE6CE6158225298EC1131B6A130D1AEB454C1AB5183C0", - "PRIVATE_KEY": { - "digest_function": "ed25519", - "payload": "9ac47abf59b356e0bd7dcbbbb4dec080e302156a48ca907e47cb6aea1d32719e7233bfc89dcbd68c19fde6ce6158225298ec1131b6a130d1aeb454c1ab5183c0" - }, - "ACCOUNT_ID": "alice@wonderland", - "BASIC_AUTH": { - "web_login": "mad_hatter", - "password": "ilovetea" - }, - "TORII_API_URL": "http://127.0.0.1:8080/", - "TRANSACTION_TIME_TO_LIVE_MS": 100000, - "TRANSACTION_STATUS_TIMEOUT_MS": 15000, - "ADD_TRANSACTION_NONCE": false -} diff --git a/configs/client/lts/config.json b/configs/client/lts/config.json deleted file mode 100644 index e1763c4d801..00000000000 --- a/configs/client/lts/config.json +++ /dev/null @@ -1,95 +0,0 @@ -{ - "PUBLIC_KEY": null, - "PRIVATE_KEY": null, - "DISABLE_PANIC_TERMINAL_COLORS": false, - "KURA": { - "INIT_MODE": "strict", - "BLOCK_STORE_PATH": "./storage", - "BLOCKS_PER_STORAGE_FILE": 1000, - "ACTOR_CHANNEL_CAPACITY": 100, - "DEBUG_OUTPUT_NEW_BLOCKS": false - }, - "SUMERAGI": { - "KEY_PAIR": null, - "PEER_ID": null, - "BLOCK_TIME_MS": 1000, - "TRUSTED_PEERS": null, - "COMMIT_TIME_LIMIT_MS": 2000, - "TX_RECEIPT_TIME_LIMIT_MS": 500, - "TRANSACTION_LIMITS": { - "max_instruction_number": 4096, - "max_wasm_size_bytes": 4194304 - }, - "ACTOR_CHANNEL_CAPACITY": 100, - "GOSSIP_BATCH_SIZE": 500, - "GOSSIP_PERIOD_MS": 1000 - }, - "TORII": { - "P2P_ADDR": null, - "API_URL": null, - "TELEMETRY_URL": null, - "MAX_TRANSACTION_SIZE": 32768, - "MAX_CONTENT_LEN": 16384000 - }, - "BLOCK_SYNC": { - "GOSSIP_PERIOD_MS": 10000, - "BLOCK_BATCH_SIZE": 4, - "ACTOR_CHANNEL_CAPACITY": 100 - }, - "QUEUE": { - "MAXIMUM_TRANSACTIONS_IN_BLOCK": 8192, - "MAXIMUM_TRANSACTIONS_IN_QUEUE": 65536, - "TRANSACTION_TIME_TO_LIVE_MS": 86400000, - "FUTURE_THRESHOLD_MS": 1000 - }, - "LOGGER": { - "MAX_LOG_LEVEL": "INFO", - "TELEMETRY_CAPACITY": 1000, - "COMPACT_MODE": false, - "LOG_FILE_PATH": null, - "TERMINAL_COLORS": true - }, - "GENESIS": { - "ACCOUNT_PUBLIC_KEY": null, - "ACCOUNT_PRIVATE_KEY": null, - "WAIT_FOR_PEERS_RETRY_COUNT_LIMIT": 100, - "WAIT_FOR_PEERS_RETRY_PERIOD_MS": 500, - "GENESIS_SUBMISSION_DELAY_MS": 1000 - }, - "WSV": { - "ASSET_METADATA_LIMITS": { - "max_len": 1048576, - "max_entry_byte_size": 4096 - }, - "ASSET_DEFINITION_METADATA_LIMITS": { - "max_len": 1048576, - "max_entry_byte_size": 4096 - }, - "ACCOUNT_METADATA_LIMITS": { - "max_len": 1048576, - "max_entry_byte_size": 4096 - }, - "DOMAIN_METADATA_LIMITS": { - "max_len": 1048576, - "max_entry_byte_size": 4096 - }, - "IDENT_LENGTH_LIMITS": { - "min": 1, - "max": 128 - }, - "WASM_RUNTIME_CONFIG": { - "FUEL_LIMIT": 1000000, - "MAX_MEMORY": 524288000 - } - }, - "NETWORK": { - "ACTOR_CHANNEL_CAPACITY": 100 - }, - "TELEMETRY": { - "NAME": null, - "URL": null, - "MIN_RETRY_PERIOD": 1, - "MAX_RETRY_DELAY_EXPONENT": 4, - "FILE": null - } -} diff --git a/configs/client/stable/config.json b/configs/client/stable/config.json deleted file mode 100644 index e1763c4d801..00000000000 --- a/configs/client/stable/config.json +++ /dev/null @@ -1,95 +0,0 @@ -{ - "PUBLIC_KEY": null, - "PRIVATE_KEY": null, - "DISABLE_PANIC_TERMINAL_COLORS": false, - "KURA": { - "INIT_MODE": "strict", - "BLOCK_STORE_PATH": "./storage", - "BLOCKS_PER_STORAGE_FILE": 1000, - "ACTOR_CHANNEL_CAPACITY": 100, - "DEBUG_OUTPUT_NEW_BLOCKS": false - }, - "SUMERAGI": { - "KEY_PAIR": null, - "PEER_ID": null, - "BLOCK_TIME_MS": 1000, - "TRUSTED_PEERS": null, - "COMMIT_TIME_LIMIT_MS": 2000, - "TX_RECEIPT_TIME_LIMIT_MS": 500, - "TRANSACTION_LIMITS": { - "max_instruction_number": 4096, - "max_wasm_size_bytes": 4194304 - }, - "ACTOR_CHANNEL_CAPACITY": 100, - "GOSSIP_BATCH_SIZE": 500, - "GOSSIP_PERIOD_MS": 1000 - }, - "TORII": { - "P2P_ADDR": null, - "API_URL": null, - "TELEMETRY_URL": null, - "MAX_TRANSACTION_SIZE": 32768, - "MAX_CONTENT_LEN": 16384000 - }, - "BLOCK_SYNC": { - "GOSSIP_PERIOD_MS": 10000, - "BLOCK_BATCH_SIZE": 4, - "ACTOR_CHANNEL_CAPACITY": 100 - }, - "QUEUE": { - "MAXIMUM_TRANSACTIONS_IN_BLOCK": 8192, - "MAXIMUM_TRANSACTIONS_IN_QUEUE": 65536, - "TRANSACTION_TIME_TO_LIVE_MS": 86400000, - "FUTURE_THRESHOLD_MS": 1000 - }, - "LOGGER": { - "MAX_LOG_LEVEL": "INFO", - "TELEMETRY_CAPACITY": 1000, - "COMPACT_MODE": false, - "LOG_FILE_PATH": null, - "TERMINAL_COLORS": true - }, - "GENESIS": { - "ACCOUNT_PUBLIC_KEY": null, - "ACCOUNT_PRIVATE_KEY": null, - "WAIT_FOR_PEERS_RETRY_COUNT_LIMIT": 100, - "WAIT_FOR_PEERS_RETRY_PERIOD_MS": 500, - "GENESIS_SUBMISSION_DELAY_MS": 1000 - }, - "WSV": { - "ASSET_METADATA_LIMITS": { - "max_len": 1048576, - "max_entry_byte_size": 4096 - }, - "ASSET_DEFINITION_METADATA_LIMITS": { - "max_len": 1048576, - "max_entry_byte_size": 4096 - }, - "ACCOUNT_METADATA_LIMITS": { - "max_len": 1048576, - "max_entry_byte_size": 4096 - }, - "DOMAIN_METADATA_LIMITS": { - "max_len": 1048576, - "max_entry_byte_size": 4096 - }, - "IDENT_LENGTH_LIMITS": { - "min": 1, - "max": 128 - }, - "WASM_RUNTIME_CONFIG": { - "FUEL_LIMIT": 1000000, - "MAX_MEMORY": 524288000 - } - }, - "NETWORK": { - "ACTOR_CHANNEL_CAPACITY": 100 - }, - "TELEMETRY": { - "NAME": null, - "URL": null, - "MIN_RETRY_PERIOD": 1, - "MAX_RETRY_DELAY_EXPONENT": 4, - "FILE": null - } -} diff --git a/configs/peer.template.toml b/configs/peer.template.toml new file mode 100644 index 00000000000..855e44c6c0b --- /dev/null +++ b/configs/peer.template.toml @@ -0,0 +1,67 @@ +## For the full reference, go to (TODO put link) + +## You can use another TOML file to extend from. +## For a single file extension: +# extends = "./base.toml" +## Or, for a chain of extensions: +# extends = ["base-1.toml", "base-2.toml"] + +# chain_id = +# public_key = +# private_key = { +# algorithm = , +# payload = +# } + +[genesis] +# file = +# public_key = +# private_key = { algorithm = "", payload = "" } + +[network] +# address = +# block_gossip_period = "10s" +# block_gossip_max_size = 4 +# transaction_gossip_period = "1s" +# transaction_gossip_max_size = 500 + +[torii] +# address = +# max_content_len = "16mb" +# query_idle_time = "30s" + +[kura] +# init_mode = "strict" +# store_dir = "./storage" + +## Add more of this section for each trusted peer +# [[sumeragi.trusted_peers]] +# address = +# public_key = + +[logger] +# level = "INFO" +# format = "full" +# tokio_console_address = "127.0.0.1:5555" + +## Transactions Queue +[queue] +# capacity = 65536 +# capacity_per_user = 65536 +# transaction_time_to_live = "1day" +# future_threshold = "1s" + +[snapshot] +# creation_enabled = true +# create_every = "1min" +# store_dir = "./storage/snapshot" + +[telemetry] +# name = +# url = +# min_retry_period = "1s" +# max_retry_delay_exponent = 4 + +[telemetry.dev] +## FIXME: is it JSON5? +# out_file = "./dev-telemetry.json5" diff --git a/configs/peer/config.json b/configs/peer/config.json deleted file mode 100644 index 649b25f31c4..00000000000 --- a/configs/peer/config.json +++ /dev/null @@ -1,95 +0,0 @@ -{ - "CHAIN_ID": null, - "PUBLIC_KEY": null, - "PRIVATE_KEY": null, - "KURA": { - "INIT_MODE": "strict", - "BLOCK_STORE_PATH": "./storage", - "DEBUG_OUTPUT_NEW_BLOCKS": false - }, - "SUMERAGI": { - "KEY_PAIR": null, - "PEER_ID": null, - "BLOCK_TIME_MS": 2000, - "TRUSTED_PEERS": null, - "COMMIT_TIME_LIMIT_MS": 4000, - "MAX_TRANSACTIONS_IN_BLOCK": 512, - "ACTOR_CHANNEL_CAPACITY": 100, - "GOSSIP_BATCH_SIZE": 500, - "GOSSIP_PERIOD_MS": 1000 - }, - "TORII": { - "P2P_ADDR": null, - "API_URL": null, - "MAX_TRANSACTION_SIZE": 32768, - "MAX_CONTENT_LEN": 16384000 - }, - "BLOCK_SYNC": { - "GOSSIP_PERIOD_MS": 10000, - "BLOCK_BATCH_SIZE": 4, - "ACTOR_CHANNEL_CAPACITY": 100 - }, - "QUEUE": { - "MAX_TRANSACTIONS_IN_QUEUE": 65536, - "MAX_TRANSACTIONS_IN_QUEUE_PER_USER": 65536, - "TRANSACTION_TIME_TO_LIVE_MS": 86400000, - "FUTURE_THRESHOLD_MS": 1000 - }, - "LOGGER": { - "LEVEL": "INFO", - "FORMAT": "full" - }, - "GENESIS": { - "PUBLIC_KEY": null, - "PRIVATE_KEY": null, - "FILE": null - }, - "WSV": { - "ASSET_METADATA_LIMITS": { - "max_len": 1048576, - "max_entry_byte_size": 4096 - }, - "ASSET_DEFINITION_METADATA_LIMITS": { - "max_len": 1048576, - "max_entry_byte_size": 4096 - }, - "ACCOUNT_METADATA_LIMITS": { - "max_len": 1048576, - "max_entry_byte_size": 4096 - }, - "DOMAIN_METADATA_LIMITS": { - "max_len": 1048576, - "max_entry_byte_size": 4096 - }, - "IDENT_LENGTH_LIMITS": { - "min": 1, - "max": 128 - }, - "TRANSACTION_LIMITS": { - "max_instruction_number": 4096, - "max_wasm_size_bytes": 4194304 - }, - "WASM_RUNTIME_CONFIG": { - "FUEL_LIMIT": 55000000, - "MAX_MEMORY": 524288000 - } - }, - "NETWORK": { - "ACTOR_CHANNEL_CAPACITY": 100 - }, - "TELEMETRY": { - "NAME": null, - "URL": null, - "MIN_RETRY_PERIOD": 1, - "MAX_RETRY_DELAY_EXPONENT": 4, - "FILE": null - }, - "SNAPSHOT": { - "CREATE_EVERY_MS": 60000, - "DIR_PATH": "./storage", - "CREATION_ENABLED": true - }, - "LIVE_QUERY_STORE": { - "QUERY_IDLE_TIME_MS": 30000 - } -} diff --git a/configs/peer/lts/config.json b/configs/peer/lts/config.json deleted file mode 100644 index ef36a9f525c..00000000000 --- a/configs/peer/lts/config.json +++ /dev/null @@ -1,98 +0,0 @@ -{ - "PUBLIC_KEY": null, - "PRIVATE_KEY": null, - "DISABLE_PANIC_TERMINAL_COLORS": false, - "KURA": { - "INIT_MODE": "strict", - "BLOCK_STORE_PATH": "./storage", - "BLOCKS_PER_STORAGE_FILE": 1000, - "ACTOR_CHANNEL_CAPACITY": 100, - "DEBUG_OUTPUT_NEW_BLOCKS": false - }, - "SUMERAGI": { - "KEY_PAIR": null, - "PEER_ID": null, - "BLOCK_TIME_MS": 2000, - "TRUSTED_PEERS": null, - "COMMIT_TIME_LIMIT_MS": 4000, - "MAX_TRANSACTIONS_IN_BLOCK": 512, - "ACTOR_CHANNEL_CAPACITY": 100, - "GOSSIP_BATCH_SIZE": 500, - "GOSSIP_PERIOD_MS": 1000 - }, - "TORII": { - "P2P_ADDR": null, - "API_URL": null, - "MAX_TRANSACTION_SIZE": 32768, - "MAX_CONTENT_LEN": 16384000, - "FETCH_SIZE": 10, - "QUERY_IDLE_TIME_MS": 30000 - }, - "BLOCK_SYNC": { - "GOSSIP_PERIOD_MS": 10000, - "BLOCK_BATCH_SIZE": 4, - "ACTOR_CHANNEL_CAPACITY": 100 - }, - "QUEUE": { - "MAX_TRANSACTIONS_IN_QUEUE": 65536, - "MAX_TRANSACTIONS_IN_QUEUE_PER_USER": 65536, - "TRANSACTION_TIME_TO_LIVE_MS": 86400000, - "FUTURE_THRESHOLD_MS": 1000 - }, - "LOGGER": { - "MAX_LOG_LEVEL": "INFO", - "TELEMETRY_CAPACITY": 1000, - "COMPACT_MODE": false, - "LOG_FILE_PATH": null, - "TERMINAL_COLORS": true - }, - "GENESIS": { - "ACCOUNT_PUBLIC_KEY": null, - "ACCOUNT_PRIVATE_KEY": null - }, - "WSV": { - "ASSET_METADATA_LIMITS": { - "max_len": 1048576, - "max_entry_byte_size": 4096 - }, - "ASSET_DEFINITION_METADATA_LIMITS": { - "max_len": 1048576, - "max_entry_byte_size": 4096 - }, - "ACCOUNT_METADATA_LIMITS": { - "max_len": 1048576, - "max_entry_byte_size": 4096 - }, - "DOMAIN_METADATA_LIMITS": { - "max_len": 1048576, - "max_entry_byte_size": 4096 - }, - "IDENT_LENGTH_LIMITS": { - "min": 1, - "max": 128 - }, - "TRANSACTION_LIMITS": { - "max_instruction_number": 4096, - "max_wasm_size_bytes": 4194304 - }, - "WASM_RUNTIME_CONFIG": { - "FUEL_LIMIT": 23000000, - "MAX_MEMORY": 524288000 - } - }, - "NETWORK": { - "ACTOR_CHANNEL_CAPACITY": 100 - }, - "TELEMETRY": { - "NAME": null, - "URL": null, - "MIN_RETRY_PERIOD": 1, - "MAX_RETRY_DELAY_EXPONENT": 4, - "FILE": null - }, - "SNAPSHOT": { - "CREATE_EVERY_MS": 60000, - "DIR_PATH": "./storage", - "CREATION_ENABLED": true - } -} diff --git a/configs/peer/lts/executor.wasm b/configs/peer/lts/executor.wasm deleted file mode 100644 index 544c9e29dfa4cb65d7bdae1f6ccd1e2cbb3e8fb2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 501157 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

w6v6Y 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

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

w6v6Y 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

4QR6$WMWHA6qEt{+0bfc`Ta`WMhoag_Rz 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 T0*gHt-N3_*$^5S;4%GY~ehcff diff --git a/configs/peer/stable/genesis.json b/configs/peer/stable/genesis.json deleted file mode 100644 index 2ca5d0365ed..00000000000 --- a/configs/peer/stable/genesis.json +++ /dev/null @@ -1,201 +0,0 @@ -{ - "transactions": [ - [ - { - "Register": { - "NewDomain": { - "id": "wonderland", - "logo": null, - "metadata": { - "key": { - "String": "value" - } - } - } - } - }, - { - "Register": { - "NewAccount": { - "id": "alice@wonderland", - "signatories": [ - "ed01207233BFC89DCBD68C19FDE6CE6158225298EC1131B6A130D1AEB454C1AB5183C0" - ], - "metadata": { - "key": { - "String": "value" - } - } - } - } - }, - { - "Register": { - "NewAccount": { - "id": "bob@wonderland", - "signatories": [ - "ed01207233BFC89DCBD68C19FDE6CE6158225298EC1131B6A130D1AEB454C1AB5183C0" - ], - "metadata": { - "key": { - "String": "value" - } - } - } - } - }, - { - "Register": { - "NewAssetDefinition": { - "id": "rose#wonderland", - "value_type": "Quantity", - "mintable": "Infinitely", - "logo": null, - "metadata": {} - } - } - }, - { - "Register": { - "NewDomain": { - "id": "garden_of_live_flowers", - "logo": null, - "metadata": {} - } - } - }, - { - "Register": { - "NewAccount": { - "id": "carpenter@garden_of_live_flowers", - "signatories": [ - "ed01207233BFC89DCBD68C19FDE6CE6158225298EC1131B6A130D1AEB454C1AB5183C0" - ], - "metadata": {} - } - } - }, - { - "Register": { - "NewAssetDefinition": { - "id": "cabbage#garden_of_live_flowers", - "value_type": "Quantity", - "mintable": "Infinitely", - "logo": null, - "metadata": {} - } - } - }, - { - "Mint": { - "object": "13_u32", - "destination_id": { - "AssetId": "rose##alice@wonderland" - } - } - }, - { - "Mint": { - "object": "44_u32", - "destination_id": { - "AssetId": "cabbage#garden_of_live_flowers#alice@wonderland" - } - } - }, - { - "Grant": { - "object": { - "PermissionToken": { - "definition_id": "CanSetParameters", - "payload": null - } - }, - "destination_id": { - "AccountId": "alice@wonderland" - } - } - }, - { - "Sequence": [ - { - "NewParameter": { - "Parameter": "?MaxTransactionsInBlock=512" - } - }, - { - "NewParameter": { - "Parameter": "?BlockTime=2000" - } - }, - { - "NewParameter": { - "Parameter": "?CommitTimeLimit=4000" - } - }, - { - "NewParameter": { - "Parameter": "?TransactionLimits=4096,4194304_TL" - } - }, - { - "NewParameter": { - "Parameter": "?WSVAssetMetadataLimits=1048576,4096_ML" - } - }, - { - "NewParameter": { - "Parameter": "?WSVAssetDefinitionMetadataLimits=1048576,4096_ML" - } - }, - { - "NewParameter": { - "Parameter": "?WSVAccountMetadataLimits=1048576,4096_ML" - } - }, - { - "NewParameter": { - "Parameter": "?WSVDomainMetadataLimits=1048576,4096_ML" - } - }, - { - "NewParameter": { - "Parameter": "?WSVIdentLengthLimits=1,128_LL" - } - }, - { - "NewParameter": { - "Parameter": "?WASMFuelLimit=23000000" - } - }, - { - "NewParameter": { - "Parameter": "?WASMMaxMemory=524288000" - } - } - ] - }, - { - "Register": { - "NewRole": { - "id": "ALICE_METADATA_ACCESS", - "permissions": [ - { - "definition_id": "CanRemoveKeyValueInUserAccount", - "payload": { - "account_id": "alice@wonderland" - } - }, - { - "definition_id": "CanSetKeyValueInUserAccount", - "payload": { - "account_id": "alice@wonderland" - } - } - ] - } - } - } - ] - ], - "executor": "./executor.wasm" -} diff --git a/configs/prometheus.yml b/configs/prometheus.template.yml similarity index 100% rename from configs/prometheus.yml rename to configs/prometheus.template.yml diff --git a/configs/swarm/client.toml b/configs/swarm/client.toml new file mode 100644 index 00000000000..bc2a82df05f --- /dev/null +++ b/configs/swarm/client.toml @@ -0,0 +1,11 @@ +chain_id = "00000000-0000-0000-0000-000000000000" +torii_url = "http://127.0.0.1:8080/" + +[basic_auth] +web_login = "mad_hatter" +password = "ilovetea" + +[account] +id = "alice@wonderland" +public_key = "ed01207233BFC89DCBD68C19FDE6CE6158225298EC1131B6A130D1AEB454C1AB5183C0" +private_key = { digest_function = "ed25519", payload = "9ac47abf59b356e0bd7dcbbbb4dec080e302156a48ca907e47cb6aea1d32719e7233bfc89dcbd68c19fde6ce6158225298ec1131b6a130d1aeb454c1ab5183c0" } diff --git a/docker-compose.local.yml b/configs/swarm/docker-compose.local.yml similarity index 50% rename from docker-compose.local.yml rename to configs/swarm/docker-compose.local.yml index 6c2cd371db2..fe10bcaabb1 100644 --- a/docker-compose.local.yml +++ b/configs/swarm/docker-compose.local.yml @@ -4,24 +4,25 @@ version: '3.8' services: iroha0: - build: ./ + build: ../.. platform: linux/amd64 environment: - IROHA_CHAIN_ID: 00000000-0000-0000-0000-000000000000 - IROHA_CONFIG: /config/config.json - IROHA_PUBLIC_KEY: ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB - IROHA_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb"}' - TORII_P2P_ADDR: 0.0.0.0:1337 - TORII_API_URL: 0.0.0.0:8080 - IROHA_GENESIS_PUBLIC_KEY: ed01204164BF554923ECE1FD412D241036D863A6AE430476C898248B8237D77534CFC4 - IROHA_GENESIS_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"82b3bde54aebeca4146257da0de8d59d8e46d5fe34887dcd8072866792fcb3ad4164bf554923ece1fd412d241036d863a6ae430476c898248b8237d77534cfc4"}' - IROHA_GENESIS_FILE: /config/genesis.json + CHAIN_ID: 00000000-0000-0000-0000-000000000000 + PUBLIC_KEY: ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB + PRIVATE_KEY_DIGEST: ed25519 + PRIVATE_KEY_PAYLOAD: 8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb + P2P_ADDRESS: 0.0.0.0:1337 + API_ADDRESS: 0.0.0.0:8080 + GENESIS_PUBLIC_KEY: ed01204164BF554923ECE1FD412D241036D863A6AE430476C898248B8237D77534CFC4 + GENESIS_PRIVATE_KEY_DIGEST: ed25519 + GENESIS_PRIVATE_KEY_PAYLOAD: 82b3bde54aebeca4146257da0de8d59d8e46d5fe34887dcd8072866792fcb3ad4164bf554923ece1fd412d241036d863a6ae430476c898248b8237d77534cfc4 + GENESIS_FILE: /config/genesis.json SUMERAGI_TRUSTED_PEERS: '[{"address":"iroha1:1338","public_key":"ed0120815BBDC9775D28C3633269B25F22D048E2AA2E36017CBE5AD85F15220BEB6F6F"},{"address":"iroha3:1340","public_key":"ed0120A66522370D60B9C09E79ADE2E9BB1EF2E78733A944B999B3A6AEE687CE476D61"},{"address":"iroha2:1339","public_key":"ed0120F417E0371E6ADB32FD66749477402B1AB67F84A8E9B082E997980CC91F327736"}]' ports: - 1337:1337 - 8080:8080 volumes: - - ./configs/peer:/config + - ./:/config init: true command: iroha --submit-genesis healthcheck: @@ -31,22 +32,22 @@ services: retries: 30 start_period: 4s iroha1: - build: ./ + build: ../.. platform: linux/amd64 environment: - IROHA_CHAIN_ID: 00000000-0000-0000-0000-000000000000 - IROHA_CONFIG: /config/config.json - IROHA_PUBLIC_KEY: ed0120815BBDC9775D28C3633269B25F22D048E2AA2E36017CBE5AD85F15220BEB6F6F - IROHA_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"c02ffad5e455e7ec620d74de5769681e4d8385906bce5a437eb67452a9efbbc2815bbdc9775d28c3633269b25f22d048e2aa2e36017cbe5ad85f15220beb6f6f"}' - TORII_P2P_ADDR: 0.0.0.0:1338 - TORII_API_URL: 0.0.0.0:8081 - IROHA_GENESIS_PUBLIC_KEY: ed01204164BF554923ECE1FD412D241036D863A6AE430476C898248B8237D77534CFC4 + CHAIN_ID: 00000000-0000-0000-0000-000000000000 + PUBLIC_KEY: ed0120815BBDC9775D28C3633269B25F22D048E2AA2E36017CBE5AD85F15220BEB6F6F + PRIVATE_KEY_DIGEST: ed25519 + PRIVATE_KEY_PAYLOAD: c02ffad5e455e7ec620d74de5769681e4d8385906bce5a437eb67452a9efbbc2815bbdc9775d28c3633269b25f22d048e2aa2e36017cbe5ad85f15220beb6f6f + P2P_ADDRESS: 0.0.0.0:1338 + API_ADDRESS: 0.0.0.0:8081 + GENESIS_PUBLIC_KEY: ed01204164BF554923ECE1FD412D241036D863A6AE430476C898248B8237D77534CFC4 SUMERAGI_TRUSTED_PEERS: '[{"address":"iroha0:1337","public_key":"ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB"},{"address":"iroha3:1340","public_key":"ed0120A66522370D60B9C09E79ADE2E9BB1EF2E78733A944B999B3A6AEE687CE476D61"},{"address":"iroha2:1339","public_key":"ed0120F417E0371E6ADB32FD66749477402B1AB67F84A8E9B082E997980CC91F327736"}]' ports: - 1338:1338 - 8081:8081 volumes: - - ./configs/peer:/config + - ./:/config init: true healthcheck: test: test $(curl -s http://127.0.0.1:8081/status/blocks) -gt 0 @@ -55,22 +56,22 @@ services: retries: 30 start_period: 4s iroha2: - build: ./ + build: ../.. platform: linux/amd64 environment: - IROHA_CHAIN_ID: 00000000-0000-0000-0000-000000000000 - IROHA_CONFIG: /config/config.json - IROHA_PUBLIC_KEY: ed0120F417E0371E6ADB32FD66749477402B1AB67F84A8E9B082E997980CC91F327736 - IROHA_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"29c5ed1409cb10fd791bc4ff8a6cb5e22a5fae7e36f448ef3ea2988b1319a88bf417e0371e6adb32fd66749477402b1ab67f84a8e9b082e997980cc91f327736"}' - TORII_P2P_ADDR: 0.0.0.0:1339 - TORII_API_URL: 0.0.0.0:8082 - IROHA_GENESIS_PUBLIC_KEY: ed01204164BF554923ECE1FD412D241036D863A6AE430476C898248B8237D77534CFC4 + CHAIN_ID: 00000000-0000-0000-0000-000000000000 + PUBLIC_KEY: ed0120F417E0371E6ADB32FD66749477402B1AB67F84A8E9B082E997980CC91F327736 + PRIVATE_KEY_DIGEST: ed25519 + PRIVATE_KEY_PAYLOAD: 29c5ed1409cb10fd791bc4ff8a6cb5e22a5fae7e36f448ef3ea2988b1319a88bf417e0371e6adb32fd66749477402b1ab67f84a8e9b082e997980cc91f327736 + P2P_ADDRESS: 0.0.0.0:1339 + API_ADDRESS: 0.0.0.0:8082 + GENESIS_PUBLIC_KEY: ed01204164BF554923ECE1FD412D241036D863A6AE430476C898248B8237D77534CFC4 SUMERAGI_TRUSTED_PEERS: '[{"address":"iroha1:1338","public_key":"ed0120815BBDC9775D28C3633269B25F22D048E2AA2E36017CBE5AD85F15220BEB6F6F"},{"address":"iroha0:1337","public_key":"ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB"},{"address":"iroha3:1340","public_key":"ed0120A66522370D60B9C09E79ADE2E9BB1EF2E78733A944B999B3A6AEE687CE476D61"}]' ports: - 1339:1339 - 8082:8082 volumes: - - ./configs/peer:/config + - ./:/config init: true healthcheck: test: test $(curl -s http://127.0.0.1:8082/status/blocks) -gt 0 @@ -79,22 +80,22 @@ services: retries: 30 start_period: 4s iroha3: - build: ./ + build: ../.. platform: linux/amd64 environment: - IROHA_CHAIN_ID: 00000000-0000-0000-0000-000000000000 - IROHA_CONFIG: /config/config.json - IROHA_PUBLIC_KEY: ed0120A66522370D60B9C09E79ADE2E9BB1EF2E78733A944B999B3A6AEE687CE476D61 - IROHA_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"5eed4855fad183c451aac39dfc50831607e4cf408c98e2b977f3ce4a2df42ce2a66522370d60b9c09e79ade2e9bb1ef2e78733a944b999b3a6aee687ce476d61"}' - TORII_P2P_ADDR: 0.0.0.0:1340 - TORII_API_URL: 0.0.0.0:8083 - IROHA_GENESIS_PUBLIC_KEY: ed01204164BF554923ECE1FD412D241036D863A6AE430476C898248B8237D77534CFC4 + CHAIN_ID: 00000000-0000-0000-0000-000000000000 + PUBLIC_KEY: ed0120A66522370D60B9C09E79ADE2E9BB1EF2E78733A944B999B3A6AEE687CE476D61 + PRIVATE_KEY_DIGEST: ed25519 + PRIVATE_KEY_PAYLOAD: 5eed4855fad183c451aac39dfc50831607e4cf408c98e2b977f3ce4a2df42ce2a66522370d60b9c09e79ade2e9bb1ef2e78733a944b999b3a6aee687ce476d61 + P2P_ADDRESS: 0.0.0.0:1340 + API_ADDRESS: 0.0.0.0:8083 + GENESIS_PUBLIC_KEY: ed01204164BF554923ECE1FD412D241036D863A6AE430476C898248B8237D77534CFC4 SUMERAGI_TRUSTED_PEERS: '[{"address":"iroha1:1338","public_key":"ed0120815BBDC9775D28C3633269B25F22D048E2AA2E36017CBE5AD85F15220BEB6F6F"},{"address":"iroha0:1337","public_key":"ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB"},{"address":"iroha2:1339","public_key":"ed0120F417E0371E6ADB32FD66749477402B1AB67F84A8E9B082E997980CC91F327736"}]' ports: - 1340:1340 - 8083:8083 volumes: - - ./configs/peer:/config + - ./:/config init: true healthcheck: test: test $(curl -s http://127.0.0.1:8083/status/blocks) -gt 0 diff --git a/configs/swarm/docker-compose.single.yml b/configs/swarm/docker-compose.single.yml new file mode 100644 index 00000000000..5af2868b817 --- /dev/null +++ b/configs/swarm/docker-compose.single.yml @@ -0,0 +1,32 @@ +# This file is generated by iroha_swarm. +# Do not edit it manually. + +version: '3.8' +services: + iroha0: + build: ../.. + platform: linux/amd64 + environment: + CHAIN_ID: 00000000-0000-0000-0000-000000000000 + PUBLIC_KEY: ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB + PRIVATE_KEY_DIGEST: ed25519 + PRIVATE_KEY_PAYLOAD: 8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb + P2P_ADDRESS: 0.0.0.0:1337 + API_ADDRESS: 0.0.0.0:8080 + GENESIS_PUBLIC_KEY: ed01204164BF554923ECE1FD412D241036D863A6AE430476C898248B8237D77534CFC4 + GENESIS_PRIVATE_KEY_DIGEST: ed25519 + GENESIS_PRIVATE_KEY_PAYLOAD: 82b3bde54aebeca4146257da0de8d59d8e46d5fe34887dcd8072866792fcb3ad4164bf554923ece1fd412d241036d863a6ae430476c898248b8237d77534cfc4 + GENESIS_FILE: /config/genesis.json + ports: + - 1337:1337 + - 8080:8080 + volumes: + - ./:/config + init: true + command: iroha --submit-genesis + healthcheck: + test: test $(curl -s http://127.0.0.1:8080/status/blocks) -gt 0 + interval: 2s + timeout: 1s + retries: 30 + start_period: 4s diff --git a/docker-compose.yml b/configs/swarm/docker-compose.yml similarity index 52% rename from docker-compose.yml rename to configs/swarm/docker-compose.yml index af679a88066..c21b8300a89 100644 --- a/docker-compose.yml +++ b/configs/swarm/docker-compose.yml @@ -7,21 +7,22 @@ services: image: hyperledger/iroha2:dev platform: linux/amd64 environment: - IROHA_CHAIN_ID: 00000000-0000-0000-0000-000000000000 - IROHA_CONFIG: /config/config.json - IROHA_PUBLIC_KEY: ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB - IROHA_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb"}' - TORII_P2P_ADDR: 0.0.0.0:1337 - TORII_API_URL: 0.0.0.0:8080 - IROHA_GENESIS_PUBLIC_KEY: ed01204164BF554923ECE1FD412D241036D863A6AE430476C898248B8237D77534CFC4 - IROHA_GENESIS_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"82b3bde54aebeca4146257da0de8d59d8e46d5fe34887dcd8072866792fcb3ad4164bf554923ece1fd412d241036d863a6ae430476c898248b8237d77534cfc4"}' - IROHA_GENESIS_FILE: /config/genesis.json + CHAIN_ID: 00000000-0000-0000-0000-000000000000 + PUBLIC_KEY: ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB + PRIVATE_KEY_DIGEST: ed25519 + PRIVATE_KEY_PAYLOAD: 8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb + P2P_ADDRESS: 0.0.0.0:1337 + API_ADDRESS: 0.0.0.0:8080 + GENESIS_PUBLIC_KEY: ed01204164BF554923ECE1FD412D241036D863A6AE430476C898248B8237D77534CFC4 + GENESIS_PRIVATE_KEY_DIGEST: ed25519 + GENESIS_PRIVATE_KEY_PAYLOAD: 82b3bde54aebeca4146257da0de8d59d8e46d5fe34887dcd8072866792fcb3ad4164bf554923ece1fd412d241036d863a6ae430476c898248b8237d77534cfc4 + GENESIS_FILE: /config/genesis.json SUMERAGI_TRUSTED_PEERS: '[{"address":"iroha1:1338","public_key":"ed0120815BBDC9775D28C3633269B25F22D048E2AA2E36017CBE5AD85F15220BEB6F6F"},{"address":"iroha3:1340","public_key":"ed0120A66522370D60B9C09E79ADE2E9BB1EF2E78733A944B999B3A6AEE687CE476D61"},{"address":"iroha2:1339","public_key":"ed0120F417E0371E6ADB32FD66749477402B1AB67F84A8E9B082E997980CC91F327736"}]' ports: - 1337:1337 - 8080:8080 volumes: - - ./configs/peer:/config + - ./:/config init: true command: iroha --submit-genesis healthcheck: @@ -34,19 +35,19 @@ services: image: hyperledger/iroha2:dev platform: linux/amd64 environment: - IROHA_CHAIN_ID: 00000000-0000-0000-0000-000000000000 - IROHA_CONFIG: /config/config.json - IROHA_PUBLIC_KEY: ed0120815BBDC9775D28C3633269B25F22D048E2AA2E36017CBE5AD85F15220BEB6F6F - IROHA_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"c02ffad5e455e7ec620d74de5769681e4d8385906bce5a437eb67452a9efbbc2815bbdc9775d28c3633269b25f22d048e2aa2e36017cbe5ad85f15220beb6f6f"}' - TORII_P2P_ADDR: 0.0.0.0:1338 - TORII_API_URL: 0.0.0.0:8081 - IROHA_GENESIS_PUBLIC_KEY: ed01204164BF554923ECE1FD412D241036D863A6AE430476C898248B8237D77534CFC4 + CHAIN_ID: 00000000-0000-0000-0000-000000000000 + PUBLIC_KEY: ed0120815BBDC9775D28C3633269B25F22D048E2AA2E36017CBE5AD85F15220BEB6F6F + PRIVATE_KEY_DIGEST: ed25519 + PRIVATE_KEY_PAYLOAD: c02ffad5e455e7ec620d74de5769681e4d8385906bce5a437eb67452a9efbbc2815bbdc9775d28c3633269b25f22d048e2aa2e36017cbe5ad85f15220beb6f6f + P2P_ADDRESS: 0.0.0.0:1338 + API_ADDRESS: 0.0.0.0:8081 + GENESIS_PUBLIC_KEY: ed01204164BF554923ECE1FD412D241036D863A6AE430476C898248B8237D77534CFC4 SUMERAGI_TRUSTED_PEERS: '[{"address":"iroha0:1337","public_key":"ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB"},{"address":"iroha3:1340","public_key":"ed0120A66522370D60B9C09E79ADE2E9BB1EF2E78733A944B999B3A6AEE687CE476D61"},{"address":"iroha2:1339","public_key":"ed0120F417E0371E6ADB32FD66749477402B1AB67F84A8E9B082E997980CC91F327736"}]' ports: - 1338:1338 - 8081:8081 volumes: - - ./configs/peer:/config + - ./:/config init: true healthcheck: test: test $(curl -s http://127.0.0.1:8081/status/blocks) -gt 0 @@ -58,19 +59,19 @@ services: image: hyperledger/iroha2:dev platform: linux/amd64 environment: - IROHA_CHAIN_ID: 00000000-0000-0000-0000-000000000000 - IROHA_CONFIG: /config/config.json - IROHA_PUBLIC_KEY: ed0120F417E0371E6ADB32FD66749477402B1AB67F84A8E9B082E997980CC91F327736 - IROHA_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"29c5ed1409cb10fd791bc4ff8a6cb5e22a5fae7e36f448ef3ea2988b1319a88bf417e0371e6adb32fd66749477402b1ab67f84a8e9b082e997980cc91f327736"}' - TORII_P2P_ADDR: 0.0.0.0:1339 - TORII_API_URL: 0.0.0.0:8082 - IROHA_GENESIS_PUBLIC_KEY: ed01204164BF554923ECE1FD412D241036D863A6AE430476C898248B8237D77534CFC4 + CHAIN_ID: 00000000-0000-0000-0000-000000000000 + PUBLIC_KEY: ed0120F417E0371E6ADB32FD66749477402B1AB67F84A8E9B082E997980CC91F327736 + PRIVATE_KEY_DIGEST: ed25519 + PRIVATE_KEY_PAYLOAD: 29c5ed1409cb10fd791bc4ff8a6cb5e22a5fae7e36f448ef3ea2988b1319a88bf417e0371e6adb32fd66749477402b1ab67f84a8e9b082e997980cc91f327736 + P2P_ADDRESS: 0.0.0.0:1339 + API_ADDRESS: 0.0.0.0:8082 + GENESIS_PUBLIC_KEY: ed01204164BF554923ECE1FD412D241036D863A6AE430476C898248B8237D77534CFC4 SUMERAGI_TRUSTED_PEERS: '[{"address":"iroha1:1338","public_key":"ed0120815BBDC9775D28C3633269B25F22D048E2AA2E36017CBE5AD85F15220BEB6F6F"},{"address":"iroha0:1337","public_key":"ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB"},{"address":"iroha3:1340","public_key":"ed0120A66522370D60B9C09E79ADE2E9BB1EF2E78733A944B999B3A6AEE687CE476D61"}]' ports: - 1339:1339 - 8082:8082 volumes: - - ./configs/peer:/config + - ./:/config init: true healthcheck: test: test $(curl -s http://127.0.0.1:8082/status/blocks) -gt 0 @@ -82,19 +83,19 @@ services: image: hyperledger/iroha2:dev platform: linux/amd64 environment: - IROHA_CHAIN_ID: 00000000-0000-0000-0000-000000000000 - IROHA_CONFIG: /config/config.json - IROHA_PUBLIC_KEY: ed0120A66522370D60B9C09E79ADE2E9BB1EF2E78733A944B999B3A6AEE687CE476D61 - IROHA_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"5eed4855fad183c451aac39dfc50831607e4cf408c98e2b977f3ce4a2df42ce2a66522370d60b9c09e79ade2e9bb1ef2e78733a944b999b3a6aee687ce476d61"}' - TORII_P2P_ADDR: 0.0.0.0:1340 - TORII_API_URL: 0.0.0.0:8083 - IROHA_GENESIS_PUBLIC_KEY: ed01204164BF554923ECE1FD412D241036D863A6AE430476C898248B8237D77534CFC4 + CHAIN_ID: 00000000-0000-0000-0000-000000000000 + PUBLIC_KEY: ed0120A66522370D60B9C09E79ADE2E9BB1EF2E78733A944B999B3A6AEE687CE476D61 + PRIVATE_KEY_DIGEST: ed25519 + PRIVATE_KEY_PAYLOAD: 5eed4855fad183c451aac39dfc50831607e4cf408c98e2b977f3ce4a2df42ce2a66522370d60b9c09e79ade2e9bb1ef2e78733a944b999b3a6aee687ce476d61 + P2P_ADDRESS: 0.0.0.0:1340 + API_ADDRESS: 0.0.0.0:8083 + GENESIS_PUBLIC_KEY: ed01204164BF554923ECE1FD412D241036D863A6AE430476C898248B8237D77534CFC4 SUMERAGI_TRUSTED_PEERS: '[{"address":"iroha1:1338","public_key":"ed0120815BBDC9775D28C3633269B25F22D048E2AA2E36017CBE5AD85F15220BEB6F6F"},{"address":"iroha0:1337","public_key":"ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB"},{"address":"iroha2:1339","public_key":"ed0120F417E0371E6ADB32FD66749477402B1AB67F84A8E9B082E997980CC91F327736"}]' ports: - 1340:1340 - 8083:8083 volumes: - - ./configs/peer:/config + - ./:/config init: true healthcheck: test: test $(curl -s http://127.0.0.1:8083/status/blocks) -gt 0 diff --git a/configs/peer/executor.wasm b/configs/swarm/executor.wasm similarity index 100% rename from configs/peer/executor.wasm rename to configs/swarm/executor.wasm diff --git a/configs/peer/genesis.json b/configs/swarm/genesis.json similarity index 100% rename from configs/peer/genesis.json rename to configs/swarm/genesis.json diff --git a/core/benches/blocks/common.rs b/core/benches/blocks/common.rs index 1bd989de4a4..a81cd11a8e3 100644 --- a/core/benches/blocks/common.rs +++ b/core/benches/blocks/common.rs @@ -27,7 +27,7 @@ pub fn create_block( account_id: AccountId, key_pair: &KeyPair, ) -> CommittedBlock { - let chain_id = ChainId::new("0"); + let chain_id = ChainId::from("0"); let transaction = TransactionBuilder::new(chain_id.clone(), account_id) .with_instructions(instructions) @@ -185,12 +185,12 @@ pub fn build_wsv( ); let mut wsv = WorldStateView::new(World::with([domain], UniqueVec::new()), kura, query_handle); wsv.config.transaction_limits = TransactionLimits::new(u64::MAX, u64::MAX); - wsv.config.wasm_runtime_config.fuel_limit = u64::MAX; - wsv.config.wasm_runtime_config.max_memory = u32::MAX; + wsv.config.wasm_runtime.fuel_limit = u64::MAX; + wsv.config.wasm_runtime.max_memory_bytes = u32::MAX; { let path_to_executor = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("../configs/peer/executor.wasm"); + .join("../configs/swarm/executor.wasm"); let wasm = std::fs::read(&path_to_executor) .unwrap_or_else(|_| panic!("Failed to read file: {}", path_to_executor.display())); let executor = Executor::new(WasmSmartContract::from_compiled(wasm)); diff --git a/core/benches/kura.rs b/core/benches/kura.rs index 9dd90d7b268..5ee45f62556 100644 --- a/core/benches/kura.rs +++ b/core/benches/kura.rs @@ -4,7 +4,7 @@ use std::str::FromStr as _; use byte_unit::Byte; use criterion::{criterion_group, criterion_main, Criterion}; -use iroha_config::kura::Configuration; +use iroha_config::parameters::actual::Kura as Config; use iroha_core::{ block::*, kura::{BlockStore, LockStatus}, @@ -19,7 +19,7 @@ use iroha_primitives::unique_vec::UniqueVec; use tokio::{fs, runtime::Runtime}; async fn measure_block_size_for_n_executors(n_executors: u32) { - let chain_id = ChainId::new("0"); + let chain_id = ChainId::from("0"); let alice_id = AccountId::from_str("alice@test").expect("tested"); let bob_id = AccountId::from_str("bob@test").expect("tested"); @@ -40,10 +40,10 @@ async fn measure_block_size_for_n_executors(n_executors: u32) { let tx = AcceptedTransaction::accept(tx, &chain_id, &transaction_limits) .expect("Failed to accept Transaction."); let dir = tempfile::tempdir().expect("Could not create tempfile."); - let cfg = Configuration { + let cfg = Config { init_mode: iroha_config::kura::Mode::Strict, debug_output_new_blocks: false, - block_store_path: dir.path().to_str().unwrap().into(), + store_dir: dir.path().to_path_buf(), }; let kura = iroha_core::kura::Kura::new(&cfg).unwrap(); let _thread_handle = iroha_core::kura::Kura::start(kura.clone()); diff --git a/core/benches/validation.rs b/core/benches/validation.rs index 089d6e29e2a..037e031cd12 100644 --- a/core/benches/validation.rs +++ b/core/benches/validation.rs @@ -79,7 +79,7 @@ fn build_test_and_transient_wsv(keys: KeyPair) -> WorldStateView { { let path_to_executor = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("../configs/peer/executor.wasm"); + .join("../configs/swarm/executor.wasm"); let wasm = std::fs::read(&path_to_executor) .unwrap_or_else(|_| panic!("Failed to read file: {}", path_to_executor.display())); let executor = Executor::new(WasmSmartContract::from_compiled(wasm)); @@ -93,7 +93,7 @@ fn build_test_and_transient_wsv(keys: KeyPair) -> WorldStateView { } fn accept_transaction(criterion: &mut Criterion) { - let chain_id = ChainId::new("0"); + let chain_id = ChainId::from("0"); let keys = KeyPair::generate(); let transaction = build_test_transaction(&keys, chain_id.clone()); @@ -111,7 +111,7 @@ fn accept_transaction(criterion: &mut Criterion) { } fn sign_transaction(criterion: &mut Criterion) { - let chain_id = ChainId::new("0"); + let chain_id = ChainId::from("0"); let keys = KeyPair::generate(); let transaction = build_test_transaction(&keys, chain_id); @@ -131,7 +131,7 @@ fn sign_transaction(criterion: &mut Criterion) { } fn validate_transaction(criterion: &mut Criterion) { - let chain_id = ChainId::new("0"); + let chain_id = ChainId::from("0"); let keys = KeyPair::generate(); let transaction = AcceptedTransaction::accept( @@ -157,7 +157,7 @@ fn validate_transaction(criterion: &mut Criterion) { } fn sign_blocks(criterion: &mut Criterion) { - let chain_id = ChainId::new("0"); + let chain_id = ChainId::from("0"); let keys = KeyPair::generate(); let transaction = AcceptedTransaction::accept( diff --git a/core/src/block.rs b/core/src/block.rs index e87ce6e972d..b2b0a6bc82d 100644 --- a/core/src/block.rs +++ b/core/src/block.rs @@ -6,7 +6,7 @@ //! [`Block`]s are organised into a linear sequence over time (also known as the block chain). use std::error::Error as _; -use iroha_config::sumeragi::default::DEFAULT_CONSENSUS_ESTIMATION_MS; +use iroha_config::parameters::defaults::chain_wide::DEFAULT_CONSENSUS_ESTIMATION; use iroha_crypto::{HashOf, KeyPair, MerkleTree, SignatureOf, SignaturesOf}; use iroha_data_model::{ block::*, @@ -144,7 +144,10 @@ mod pending { .as_millis() .try_into() .expect("Time should fit into u64"), - consensus_estimation_ms: DEFAULT_CONSENSUS_ESTIMATION_MS, + consensus_estimation_ms: DEFAULT_CONSENSUS_ESTIMATION + .as_millis() + .try_into() + .expect("Time should fit into u64"), height: previous_height + 1, view_change_index, previous_block_hash, @@ -437,7 +440,10 @@ mod valid { BlockBuilder(Chained(BlockPayload { header: BlockHeader { timestamp_ms: 0, - consensus_estimation_ms: DEFAULT_CONSENSUS_ESTIMATION_MS, + consensus_estimation_ms: DEFAULT_CONSENSUS_ESTIMATION + .as_millis() + .try_into() + .expect("Should never overflow?"), height: 2, view_change_index: 0, previous_block_hash: None, @@ -687,7 +693,7 @@ mod tests { #[tokio::test] async fn should_reject_due_to_repetition() { - let chain_id = ChainId::new("0"); + let chain_id = ChainId::from("0"); // Predefined world state let alice_id = AccountId::from_str("alice@wonderland").expect("Valid"); @@ -730,7 +736,7 @@ mod tests { #[tokio::test] async fn tx_order_same_in_validation_and_revalidation() { - let chain_id = ChainId::new("0"); + let chain_id = ChainId::from("0"); // Predefined world state let alice_id = AccountId::from_str("alice@wonderland").expect("Valid"); @@ -796,7 +802,7 @@ mod tests { #[tokio::test] async fn failed_transactions_revert() { - let chain_id = ChainId::new("0"); + let chain_id = ChainId::from("0"); // Predefined world state let alice_id = AccountId::from_str("alice@wonderland").expect("Valid"); diff --git a/core/src/block_sync.rs b/core/src/block_sync.rs index 2ff4ffe05ca..e4d71aad0a1 100644 --- a/core/src/block_sync.rs +++ b/core/src/block_sync.rs @@ -1,7 +1,7 @@ //! This module contains structures and messages for synchronization of blocks between peers. -use std::{fmt::Debug, sync::Arc, time::Duration}; +use std::{fmt::Debug, num::NonZeroU32, sync::Arc, time::Duration}; -use iroha_config::block_sync::Configuration; +use iroha_config::parameters::actual::BlockSync as Config; use iroha_crypto::HashOf; use iroha_data_model::{block::SignedBlock, prelude::*}; use iroha_logger::prelude::*; @@ -36,7 +36,7 @@ pub struct BlockSynchronizer { kura: Arc, peer_id: PeerId, gossip_period: Duration, - block_batch_size: u32, + gossip_max_size: NonZeroU32, network: IrohaNetwork, latest_hash: Option>, previous_hash: Option>, @@ -104,8 +104,8 @@ impl BlockSynchronizer { } /// Create [`Self`] from [`Configuration`] - pub fn from_configuration( - config: &Configuration, + pub fn from_config( + config: &Config, sumeragi: SumeragiHandle, kura: Arc, peer_id: PeerId, @@ -117,8 +117,8 @@ impl BlockSynchronizer { peer_id, sumeragi, kura, - gossip_period: Duration::from_millis(config.gossip_period_ms), - block_batch_size: config.block_batch_size, + gossip_period: config.gossip_period, + gossip_max_size: config.gossip_max_size, network, latest_hash, previous_hash, @@ -191,10 +191,6 @@ pub mod message { previous_hash, peer_id, }) => { - if block_sync.block_batch_size == 0 { - warn!("Error: not sending any blocks as batch_size is equal to zero."); - return; - } let local_latest_block_hash = block_sync.latest_hash; if *latest_hash == local_latest_block_hash || *previous_hash == local_latest_block_hash @@ -214,7 +210,7 @@ pub mod message { }; let blocks = (start_height..) - .take(1 + block_sync.block_batch_size as usize) + .take(1 + block_sync.gossip_max_size.get() as usize) .map_while(|height| block_sync.kura.get_block_by_height(height)) .skip_while(|block| Some(block.hash()) == *latest_hash) .map(|block| (*block).clone()) diff --git a/core/src/executor.rs b/core/src/executor.rs index 62af571fe49..70eb4c5fc0a 100644 --- a/core/src/executor.rs +++ b/core/src/executor.rs @@ -157,7 +157,7 @@ impl Executor { let runtime = wasm::RuntimeBuilder::::new() .with_engine(wsv.engine.clone()) // Cloning engine is cheap, see [`wasmtime::Engine`] docs - .with_configuration(wsv.config.wasm_runtime_config) + .with_config(wsv.config.wasm_runtime) .build()?; runtime.execute_executor_validate_transaction( @@ -191,7 +191,7 @@ impl Executor { let runtime = wasm::RuntimeBuilder::::new() .with_engine(wsv.engine.clone()) // Cloning engine is cheap, see [`wasmtime::Engine`] docs - .with_configuration(wsv.config.wasm_runtime_config) + .with_config(wsv.config.wasm_runtime) .build()?; runtime.execute_executor_validate_instruction( @@ -224,7 +224,7 @@ impl Executor { Self::UserProvided(UserProvidedExecutor(loaded_executor)) => { let runtime = wasm::RuntimeBuilder::::new() .with_engine(wsv.engine.clone()) // Cloning engine is cheap, see [`wasmtime::Engine`] docs - .with_configuration(wsv.config.wasm_runtime_config) + .with_config(wsv.config.wasm_runtime) .build()?; runtime.execute_executor_validate_query( @@ -259,7 +259,7 @@ impl Executor { let runtime = wasm::RuntimeBuilder::::new() .with_engine(wsv.engine.clone()) // Cloning engine is cheap, see [`wasmtime::Engine`] docs - .with_configuration(wsv.config.wasm_runtime_config) + .with_config(wsv.config.wasm_runtime) .build()?; runtime diff --git a/core/src/gossiper.rs b/core/src/gossiper.rs index e9dbe4604e4..a67709fbb9a 100644 --- a/core/src/gossiper.rs +++ b/core/src/gossiper.rs @@ -1,8 +1,8 @@ //! Gossiper is actor which is responsible for transaction gossiping -use std::{sync::Arc, time::Duration}; +use std::{num::NonZeroU32, sync::Arc, time::Duration}; -use iroha_config::sumeragi::Configuration; +use iroha_config::parameters::actual::TransactionGossiper as Config; use iroha_data_model::{transaction::SignedTransaction, ChainId}; use iroha_p2p::Broadcast; use parity_scale_codec::{Decode, Encode}; @@ -35,7 +35,7 @@ pub struct TransactionGossiper { chain_id: ChainId, /// The size of batch that is being gossiped. Smaller size leads /// to longer time to synchronise, useful if you have high packet loss. - gossip_batch_size: u32, + gossip_max_size: NonZeroU32, /// The time between gossiping. More frequent gossiping shortens /// the time to sync, but can overload the network. gossip_period: Duration, @@ -58,10 +58,12 @@ impl TransactionGossiper { } /// Construct [`Self`] from configuration - pub fn from_configuration( + pub fn from_config( chain_id: ChainId, - // Currently we are using configuration parameters from sumeragi not to break configuration - configuration: &Configuration, + Config { + gossip_period, + gossip_max_size, + }: Config, network: IrohaNetwork, queue: Arc, sumeragi: SumeragiHandle, @@ -69,11 +71,11 @@ impl TransactionGossiper { let wsv = sumeragi.wsv_clone(); Self { chain_id, + gossip_max_size, + gossip_period, queue, - sumeragi, network, - gossip_batch_size: configuration.gossip_batch_size, - gossip_period: Duration::from_millis(configuration.gossip_period_ms), + sumeragi, wsv, } } @@ -101,7 +103,7 @@ impl TransactionGossiper { fn gossip_transactions(&self) { let txs = self .queue - .n_random_transactions(self.gossip_batch_size, &self.wsv); + .n_random_transactions(self.gossip_max_size.get(), &self.wsv); if txs.is_empty() { return; diff --git a/core/src/kiso.rs b/core/src/kiso.rs index a7f62be4449..c99add91be0 100644 --- a/core/src/kiso.rs +++ b/core/src/kiso.rs @@ -1,6 +1,6 @@ //! Actor responsible for configuration state and its dynamic updates. //! -//! Currently the API exposed by [`KisoHandle`] works only with [`ConfigurationDTO`], because +//! Currently the API exposed by [`KisoHandle`] works only with [`ConfigDTO`], because //! no any part of Iroha is interested in the whole state. However, the API could be extended //! in future. //! @@ -9,8 +9,8 @@ use eyre::Result; use iroha_config::{ - client_api::{ConfigurationDTO, Logger as LoggerDTO}, - iroha::Configuration, + client_api::{ConfigDTO, Logger as LoggerDTO}, + parameters::actual::Root as Config, }; use iroha_logger::Level; use tokio::sync::{mpsc, oneshot, watch}; @@ -27,7 +27,7 @@ pub struct KisoHandle { impl KisoHandle { /// Spawn a new actor - pub fn new(state: Configuration) -> Self { + pub fn new(state: Config) -> Self { let (actor_sender, actor_receiver) = mpsc::channel(DEFAULT_CHANNEL_SIZE); let (log_level_update, _) = watch::channel(state.logger.level); let mut actor = Actor { @@ -42,11 +42,11 @@ impl KisoHandle { } } - /// Fetch the [`ConfigurationDTO`] from the actor's state. + /// Fetch the [`ConfigDTO`] from the actor's state. /// /// # Errors /// If communication with actor fails. - pub async fn get_dto(&self) -> Result { + pub async fn get_dto(&self) -> Result { let (tx, rx) = oneshot::channel(); let msg = Message::GetDTO { respond_to: tx }; let _ = self.actor.send(msg).await; @@ -61,7 +61,7 @@ impl KisoHandle { /// /// # Errors /// If communication with actor fails. - pub async fn update_with_dto(&self, dto: ConfigurationDTO) -> Result<(), Error> { + pub async fn update_with_dto(&self, dto: ConfigDTO) -> Result<(), Error> { let (tx, rx) = oneshot::channel(); let msg = Message::UpdateWithDTO { dto, @@ -86,10 +86,10 @@ impl KisoHandle { enum Message { GetDTO { - respond_to: oneshot::Sender, + respond_to: oneshot::Sender, }, UpdateWithDTO { - dto: ConfigurationDTO, + dto: ConfigDTO, respond_to: oneshot::Sender>, }, SubscribeOnLogLevel { @@ -106,7 +106,7 @@ pub enum Error { struct Actor { handle: mpsc::Receiver, - state: Configuration, + state: Config, // Current implementation is somewhat not scalable in terms of code writing: for any // future dynamic parameter, it will require its own `subscribe_on_` function in [`KisoHandle`], // new channel here, and new [`Message`] variant. If boilerplate expands, a more general solution will be @@ -124,12 +124,12 @@ impl Actor { fn handle_message(&mut self, msg: Message) { match msg { Message::GetDTO { respond_to } => { - let dto = ConfigurationDTO::from(&self.state); + let dto = ConfigDTO::from(&self.state); let _ = respond_to.send(dto); } Message::UpdateWithDTO { dto: - ConfigurationDTO { + ConfigDTO { logger: LoggerDTO { level: new_level }, }, respond_to, @@ -151,20 +151,23 @@ mod tests { use std::time::Duration; use iroha_config::{ - base::proxy::LoadFromDisk, - client_api::{ConfigurationDTO, Logger as LoggerDTO}, - iroha::{Configuration, ConfigurationProxy}, + client_api::{ConfigDTO, Logger as LoggerDTO}, + parameters::actual::Root, }; use super::*; - fn test_config() -> Configuration { - // FIXME Specifying path here might break! Moreover, if the file is not found, - // the error will say that `public_key` is missing! - // Hopefully this will change: https://github.com/hyperledger/iroha/issues/2585 - ConfigurationProxy::from_path("../config/iroha_test_config.json") - .build() - .unwrap() + fn test_config() -> Root { + use iroha_config::parameters::user::CliContext; + + Root::load( + // FIXME Specifying path here might break! + Some("../config/iroha_test_config.toml"), + CliContext { + submit_genesis: true, + }, + ) + .expect("test config should be valid, it is probably a bug") } #[tokio::test] @@ -186,7 +189,7 @@ mod tests { .await .expect_err("Watcher should not be active initially"); - kiso.update_with_dto(ConfigurationDTO { + kiso.update_with_dto(ConfigDTO { logger: LoggerDTO { level: NEW_LOG_LEVEL, }, diff --git a/core/src/kura.rs b/core/src/kura.rs index f248248e247..c70e5557323 100644 --- a/core/src/kura.rs +++ b/core/src/kura.rs @@ -10,7 +10,7 @@ use std::{ sync::Arc, }; -use iroha_config::kura::{Configuration, Mode}; +use iroha_config::{kura::Mode, parameters::actual::Kura as Config}; use iroha_crypto::{Hash, HashOf}; use iroha_data_model::block::SignedBlock; use iroha_logger::prelude::*; @@ -49,16 +49,13 @@ impl Kura { /// Fails if there are filesystem errors when trying /// to access the block store indicated by the provided /// path. - pub fn new(config: &Configuration) -> Result> { - let block_store_path = Path::new(&config.block_store_path); - let mut block_store = BlockStore::new(block_store_path, LockStatus::Unlocked); + pub fn new(config: &Config) -> Result> { + let mut block_store = BlockStore::new(&config.store_dir, LockStatus::Unlocked); block_store.create_files_if_they_do_not_exist()?; - let block_plain_text_path = config.debug_output_new_blocks.then(|| { - let mut path_buf = block_store_path.to_path_buf(); - path_buf.push("blocks.json"); - path_buf - }); + let block_plain_text_path = config + .debug_output_new_blocks + .then(|| config.store_dir.join("blocks.json")); let kura = Arc::new(Self { mode: config.init_mode, @@ -75,7 +72,7 @@ impl Kura { pub fn blank_kura_for_testing() -> Arc { Arc::new(Self { mode: Mode::Strict, - block_store: Mutex::new(BlockStore::new(&PathBuf::new(), LockStatus::Locked)), + block_store: Mutex::new(BlockStore::new(PathBuf::new(), LockStatus::Locked)), block_data: Mutex::new(Vec::new()), block_plain_text_path: None, }) @@ -395,9 +392,9 @@ impl BlockStore { /// /// # Panics /// * if you pass in `LockStatus::Unlocked` and it is unable to lock the block store. - pub fn new(store_path: &Path, already_locked: LockStatus) -> Self { + pub fn new(store_path: impl AsRef, already_locked: LockStatus) -> Self { if matches!(already_locked, LockStatus::Unlocked) { - let lock_path = store_path.join(LOCK_FILE_NAME); + let lock_path = store_path.as_ref().join(LOCK_FILE_NAME); if let Err(e) = fs::File::options() .read(true) .write(true) @@ -407,8 +404,8 @@ impl BlockStore { match e.kind() { std::io::ErrorKind::AlreadyExists => Err(Error::Locked(lock_path)), std::io::ErrorKind::NotFound => { - match std::fs::create_dir_all(store_path) - .map_err(|e| Error::MkDir(e, store_path.to_path_buf())) + match std::fs::create_dir_all(store_path.as_ref()) + .map_err(|e| Error::MkDir(e, store_path.as_ref().to_path_buf())) { Err(e) => Err(e), Ok(()) => { @@ -431,7 +428,7 @@ impl BlockStore { } } BlockStore { - path_to_blockchain: store_path.to_path_buf(), + path_to_blockchain: store_path.as_ref().to_path_buf(), } } @@ -1049,9 +1046,9 @@ mod tests { #[tokio::test] async fn strict_init_kura() { let temp_dir = TempDir::new().unwrap(); - Kura::new(&Configuration { + Kura::new(&Config { init_mode: Mode::Strict, - block_store_path: temp_dir.path().to_str().unwrap().into(), + store_dir: temp_dir.path().to_str().unwrap().into(), debug_output_new_blocks: false, }) .unwrap() diff --git a/core/src/query/store.rs b/core/src/query/store.rs index 8e8c83c0687..6691ee24a48 100644 --- a/core/src/query/store.rs +++ b/core/src/query/store.rs @@ -7,7 +7,7 @@ use std::{ }; use indexmap::IndexMap; -use iroha_config::live_query_store::Configuration; +use iroha_config::parameters::actual::LiveQueryStore as Config; use iroha_data_model::{ asset::AssetValue, query::{ @@ -69,15 +69,15 @@ type LiveQuery = Batched>; #[derive(Debug)] pub struct LiveQueryStore { queries: IndexMap, - query_idle_time: Duration, + idle_time: Duration, } impl LiveQueryStore { /// Construct [`LiveQueryStore`] from configuration. - pub fn from_configuration(cfg: Configuration) -> Self { + pub fn from_config(cfg: Config) -> Self { Self { queries: IndexMap::new(), - query_idle_time: Duration::from_millis(cfg.query_idle_time_ms.into()), + idle_time: cfg.idle_time, } } @@ -86,13 +86,7 @@ impl LiveQueryStore { /// /// Not marked as `#[cfg(test)]` because it is used in benches as well. pub fn test() -> Self { - use iroha_config::base::proxy::Builder as _; - - LiveQueryStore::from_configuration( - iroha_config::live_query_store::ConfigurationProxy::default() - .build() - .expect("Failed to build LiveQueryStore configuration from proxy"), - ) + Self::from_config(Config::default()) } /// Start [`LiveQueryStore`]. Requires a [`tokio::runtime::Runtime`] being run @@ -105,14 +99,14 @@ impl LiveQueryStore { let (message_sender, mut message_receiver) = mpsc::channel(1); - let mut idle_interval = tokio::time::interval(self.query_idle_time); + let mut idle_interval = tokio::time::interval(self.idle_time); tokio::task::spawn(async move { loop { tokio::select! { _ = idle_interval.tick() => { self.queries - .retain(|_, (_, last_access_time)| last_access_time.elapsed() <= self.query_idle_time); + .retain(|_, (_, last_access_time)| last_access_time.elapsed() <= self.idle_time); }, msg = message_receiver.recv() => { let Some(msg) = msg else { diff --git a/core/src/queue.rs b/core/src/queue.rs index 53eeb75f4fb..3beab0a9546 100644 --- a/core/src/queue.rs +++ b/core/src/queue.rs @@ -1,11 +1,12 @@ //! Module with queue actor use core::time::Duration; +use std::num::NonZeroUsize; use crossbeam_queue::ArrayQueue; use dashmap::{mapref::entry::Entry, DashMap}; use eyre::{Report, Result}; use indexmap::IndexSet; -use iroha_config::queue::Configuration; +use iroha_config::parameters::actual::Queue as Config; use iroha_crypto::HashOf; use iroha_data_model::{account::AccountId, transaction::prelude::*}; use iroha_logger::{debug, trace, warn}; @@ -53,9 +54,9 @@ pub struct Queue { /// Amount of transactions per user in the queue txs_per_user: DashMap, /// The maximum number of transactions in the queue - max_txs: usize, + capacity: NonZeroUsize, /// The maximum number of transactions in the queue per user. Used to apply throttling - max_txs_per_user: usize, + capacity_per_user: NonZeroUsize, /// Length of time after which transactions are dropped. pub tx_time_to_live: Duration, /// A point in time that is considered `Future` we cannot use @@ -98,15 +99,15 @@ pub struct Failure { impl Queue { /// Makes queue from configuration - pub fn from_configuration(cfg: &Configuration) -> Self { + pub fn from_config(cfg: Config) -> Self { Self { - tx_hashes: ArrayQueue::new(cfg.max_transactions_in_queue as usize), + tx_hashes: ArrayQueue::new(cfg.capacity.get()), accepted_txs: DashMap::new(), txs_per_user: DashMap::new(), - max_txs: cfg.max_transactions_in_queue as usize, - max_txs_per_user: cfg.max_transactions_in_queue_per_user as usize, - tx_time_to_live: Duration::from_millis(cfg.transaction_time_to_live_ms), - future_threshold: Duration::from_millis(cfg.future_threshold_ms), + capacity: cfg.capacity, + capacity_per_user: cfg.capacity_per_user, + tx_time_to_live: cfg.transaction_time_to_live, + future_threshold: cfg.future_threshold, } } @@ -114,11 +115,7 @@ impl Queue { !self.is_expired(tx) && !tx.is_in_blockchain(wsv) } - /// Checks if this transaction is waiting longer than specified in - /// `transaction_time_to_live` from `QueueConfiguration` or - /// `time_to_live_ms` of this transaction. Meaning that the - /// transaction will be expired as soon as the lesser of the - /// specified TTLs was reached. + /// Checks if the transaction is waiting longer than its TTL or than the TTL from [`Config`]. pub fn is_expired(&self, tx: &AcceptedTransaction) -> bool { let tx_creation_time = tx.as_ref().creation_time(); @@ -209,9 +206,9 @@ impl Queue { } Entry::Vacant(entry) => entry, }; - if txs_len >= self.max_txs { + if txs_len >= self.capacity.get() { warn!( - max = self.max_txs, + max = self.capacity, "Achieved maximum amount of transactions" ); return Err(Failure { @@ -349,9 +346,9 @@ impl Queue { } Entry::Occupied(mut occupied) => { let txs = *occupied.get(); - if txs >= self.max_txs_per_user { + if txs >= self.capacity_per_user.get() { warn!( - max_txs_per_user = self.max_txs_per_user, + max_txs_per_user = self.capacity_per_user, %account_id, "Account reached maximum allowed number of transactions in the queue per user" ); @@ -382,7 +379,6 @@ impl Queue { mod tests { use std::{str::FromStr, sync::Arc, thread, time::Duration}; - use iroha_config::{base::proxy::Builder, queue::ConfigurationProxy}; use iroha_data_model::{prelude::*, transaction::TransactionLimits}; use iroha_primitives::must_use::MustUse; use rand::Rng as _; @@ -395,7 +391,7 @@ mod tests { }; fn accepted_tx(account_id: &str, key: &KeyPair) -> AcceptedTransaction { - let chain_id = ChainId::new("0"); + let chain_id = ChainId::from("0"); let message = std::iter::repeat_with(rand::random::) .take(16) @@ -425,6 +421,14 @@ mod tests { World::with([domain], PeersIds::new()) } + fn config_factory() -> Config { + Config { + transaction_time_to_live: Duration::from_secs(100), + capacity: 100.try_into().unwrap(), + ..Config::default() + } + } + #[test] async fn push_tx() { let key_pair = KeyPair::generate(); @@ -436,13 +440,7 @@ mod tests { query_handle, )); - let queue = Queue::from_configuration(&Configuration { - transaction_time_to_live_ms: 100_000, - max_transactions_in_queue: 100, - ..ConfigurationProxy::default() - .build() - .expect("Default queue config should always build") - }); + let queue = Queue::from_config(config_factory()); queue .push(accepted_tx("alice@wonderland", &key_pair), &wsv) @@ -451,7 +449,7 @@ mod tests { #[test] async fn push_tx_overflow() { - let max_txs_in_queue = 10; + let capacity = NonZeroUsize::new(10).unwrap(); let key_pair = KeyPair::generate(); let kura = Kura::blank_kura_for_testing(); @@ -462,15 +460,13 @@ mod tests { query_handle, )); - let queue = Queue::from_configuration(&Configuration { - transaction_time_to_live_ms: 100_000, - max_transactions_in_queue: max_txs_in_queue, - ..ConfigurationProxy::default() - .build() - .expect("Default queue config should always build") + let queue = Queue::from_config(Config { + transaction_time_to_live: Duration::from_secs(100), + capacity, + ..Config::default() }); - for _ in 0..max_txs_in_queue { + for _ in 0..capacity.get() { queue .push(accepted_tx("alice@wonderland", &key_pair), &wsv) .expect("Failed to push tx into queue"); @@ -488,7 +484,7 @@ mod tests { #[test] async fn push_multisignature_tx() { - let chain_id = ChainId::new("0"); + let chain_id = ChainId::from("0"); let max_txs_in_block = 2; let key_pairs = [KeyPair::generate(), KeyPair::generate()]; @@ -512,13 +508,7 @@ mod tests { )) }; - let queue = Queue::from_configuration(&Configuration { - transaction_time_to_live_ms: 100_000, - max_transactions_in_queue: 100, - ..ConfigurationProxy::default() - .build() - .expect("Default queue config should always build") - }); + let queue = Queue::from_config(config_factory()); let instructions: [InstructionBox; 0] = []; let tx = TransactionBuilder::new(chain_id.clone(), "alice@wonderland".parse().expect("Valid")) @@ -581,12 +571,9 @@ mod tests { kura, query_handle, )); - let queue = Queue::from_configuration(&Configuration { - transaction_time_to_live_ms: 100_000, - max_transactions_in_queue: 100, - ..ConfigurationProxy::default() - .build() - .expect("Default queue config should always build") + let queue = Queue::from_config(Config { + transaction_time_to_live: Duration::from_secs(100), + ..config_factory() }); for _ in 0..5 { queue @@ -611,13 +598,7 @@ mod tests { ); let tx = accepted_tx("alice@wonderland", &alice_key); wsv.transactions.insert(tx.as_ref().hash(), 1); - let queue = Queue::from_configuration(&Configuration { - transaction_time_to_live_ms: 100_000, - max_transactions_in_queue: 100, - ..ConfigurationProxy::default() - .build() - .expect("Default queue config should always build") - }); + let queue = Queue::from_config(config_factory()); assert!(matches!( queue.push(tx, &wsv), Err(Failure { @@ -640,13 +621,7 @@ mod tests { query_handle, ); let tx = accepted_tx("alice@wonderland", &alice_key); - let queue = Queue::from_configuration(&Configuration { - transaction_time_to_live_ms: 100_000, - max_transactions_in_queue: 100, - ..ConfigurationProxy::default() - .build() - .expect("Default queue config should always build") - }); + let queue = Queue::from_config(config_factory()); queue.push(tx.clone(), &wsv).unwrap(); wsv.transactions.insert(tx.as_ref().hash(), 1); assert_eq!( @@ -669,12 +644,9 @@ mod tests { kura, query_handle, )); - let queue = Queue::from_configuration(&Configuration { - transaction_time_to_live_ms: 200, - max_transactions_in_queue: 100, - ..ConfigurationProxy::default() - .build() - .expect("Default queue config should always build") + let queue = Queue::from_config(Config { + transaction_time_to_live: Duration::from_millis(200), + ..config_factory() }); for _ in 0..(max_txs_in_block - 1) { queue @@ -719,13 +691,7 @@ mod tests { kura, query_handle, )); - let queue = Queue::from_configuration(&Configuration { - transaction_time_to_live_ms: 100_000, - max_transactions_in_queue: 100, - ..ConfigurationProxy::default() - .build() - .expect("Default queue config should always build") - }); + let queue = Queue::from_config(config_factory()); queue .push(accepted_tx("alice@wonderland", &alice_key), &wsv) .expect("Failed to push tx into queue"); @@ -748,7 +714,7 @@ mod tests { async fn custom_expired_transaction_is_rejected() { const TTL_MS: u64 = 100; - let chain_id = ChainId::new("0"); + let chain_id = ChainId::from("0"); let max_txs_in_block = 2; let alice_key = KeyPair::generate(); @@ -759,13 +725,7 @@ mod tests { kura, query_handle, )); - let queue = Queue::from_configuration(&Configuration { - transaction_time_to_live_ms: 100_000, - max_transactions_in_queue: 100, - ..ConfigurationProxy::default() - .build() - .expect("Default queue config should always build") - }); + let queue = Queue::from_config(config_factory()); let instructions = [Fail { message: "expired".to_owned(), }]; @@ -806,12 +766,10 @@ mod tests { query_handle, ); - let queue = Arc::new(Queue::from_configuration(&Configuration { - transaction_time_to_live_ms: 100_000, - max_transactions_in_queue: 100_000_000, - ..ConfigurationProxy::default() - .build() - .expect("Default queue config should always build") + let queue = Arc::new(Queue::from_config(Config { + transaction_time_to_live: Duration::from_secs(100), + capacity: 100_000_000.try_into().unwrap(), + ..Config::default() })); let start_time = std::time::Instant::now(); @@ -869,7 +827,7 @@ mod tests { #[test] async fn push_tx_in_future() { - let future_threshold_ms = 1000; + let future_threshold = Duration::from_secs(1); let alice_id = "alice@wonderland"; let alice_key = KeyPair::generate(); @@ -881,26 +839,23 @@ mod tests { query_handle, )); - let queue = Queue::from_configuration(&Configuration { - future_threshold_ms, - ..ConfigurationProxy::default() - .build() - .expect("Default queue config should always build") + let queue = Queue::from_config(Config { + future_threshold, + ..Config::default() }); let tx = accepted_tx(alice_id, &alice_key); assert!(queue.push(tx.clone(), &wsv).is_ok()); // create the same tx but with timestamp in the future let tx = { - let chain_id = ChainId::new("0"); + let chain_id = ChainId::from("0"); let mut new_tx = TransactionBuilder::new( chain_id.clone(), AccountId::from_str(alice_id).expect("Valid"), ) .with_executable(tx.0.instructions().clone()); - let creation_time: u64 = tx.0.creation_time().as_millis().try_into().unwrap(); - new_tx.set_creation_time(creation_time + 2 * future_threshold_ms); + new_tx.set_creation_time(tx.0.creation_time() + future_threshold * 2); let new_tx = new_tx.sign(&alice_key); let limits = TransactionLimits { @@ -945,13 +900,11 @@ mod tests { let query_handle = LiveQueryStore::test().start(); let mut wsv = WorldStateView::new(world, kura, query_handle); - let queue = Queue::from_configuration(&Configuration { - transaction_time_to_live_ms: 100_000, - max_transactions_in_queue: 100, - max_transactions_in_queue_per_user: 1, - ..ConfigurationProxy::default() - .build() - .expect("Default queue config should always build") + let queue = Queue::from_config(Config { + transaction_time_to_live: Duration::from_secs(100), + capacity: 100.try_into().unwrap(), + capacity_per_user: 1.try_into().unwrap(), + ..Config::default() }); // First push by Alice should be fine diff --git a/core/src/smartcontracts/isi/query.rs b/core/src/smartcontracts/isi/query.rs index db2ab543e21..f7695a93c1c 100644 --- a/core/src/smartcontracts/isi/query.rs +++ b/core/src/smartcontracts/isi/query.rs @@ -249,7 +249,7 @@ mod tests { valid_tx_per_block: usize, invalid_tx_per_block: usize, ) -> Result { - let chain_id = ChainId::new("0"); + let chain_id = ChainId::from("0"); let kura = Kura::blank_kura_for_testing(); let query_handle = LiveQueryStore::test().start(); @@ -411,7 +411,7 @@ mod tests { #[test] async fn find_transaction() -> Result<()> { - let chain_id = ChainId::new("0"); + let chain_id = ChainId::from("0"); let kura = Kura::blank_kura_for_testing(); let query_handle = LiveQueryStore::test().start(); diff --git a/core/src/smartcontracts/wasm.rs b/core/src/smartcontracts/wasm.rs index b361bc97b04..ec431e8e458 100644 --- a/core/src/smartcontracts/wasm.rs +++ b/core/src/smartcontracts/wasm.rs @@ -6,10 +6,7 @@ use error::*; use import::traits::{ ExecuteOperations as _, GetExecutorPayloads as _, SetPermissionTokenSchema as _, }; -use iroha_config::{ - base::proxy::Builder, - wasm::{Configuration, ConfigurationProxy}, -}; +use iroha_config::parameters::actual::WasmRuntime as Config; use iroha_data_model::{ account::AccountId, executor::{self, MigrationResult}, @@ -28,7 +25,8 @@ use iroha_logger::debug; use iroha_logger::{error_span as wasm_log_span, prelude::tracing::Span}; use iroha_wasm_codec::{self as codec, WasmUsize}; use wasmtime::{ - Caller, Config, Engine, Linker, Module, Store, StoreLimits, StoreLimitsBuilder, TypedFunc, + Caller, Config as WasmtimeConfig, Engine, Linker, Module, Store, StoreLimits, + StoreLimitsBuilder, TypedFunc, }; use crate::{ @@ -268,8 +266,8 @@ pub fn create_engine() -> Engine { .expect("Failed to create WASM engine with a predefined configuration. This is a bug") } -fn create_config() -> Result { - let mut config = Config::new(); +fn create_config() -> Result { + let mut config = WasmtimeConfig::new(); config .consume_fuel(true) .cache_config_load_default() @@ -343,13 +341,9 @@ pub mod state { /// /// Panics if failed to convert `u32` into `usize` which should not happen /// on any supported platform - pub fn store_limits_from_config(config: &Configuration) -> StoreLimits { + pub fn store_limits_from_config(config: &Config) -> StoreLimits { StoreLimitsBuilder::new() - .memory_size( - config.max_memory.try_into().expect( - "config.max_memory is a u32 so this can't fail on any supported platform", - ), - ) + .memory_size(config.max_memory_bytes as usize) .instances(1) .memories(1) .tables(1) @@ -374,7 +368,7 @@ pub mod state { /// Create new [`OrdinaryState`] pub fn new( authority: AccountId, - config: Configuration, + config: Config, log_span: Span, wsv: W, specific_state: S, @@ -567,7 +561,7 @@ pub mod state { pub struct Runtime { engine: Engine, linker: Linker, - config: Configuration, + config: Config, } impl Runtime { @@ -595,7 +589,7 @@ impl Runtime { fn get_typed_func( instance: &wasmtime::Instance, - mut store: &mut wasmtime::Store, + mut store: &mut Store, func_name: &'static str, ) -> Result, ExportError> { instance @@ -1409,7 +1403,7 @@ impl<'wrld> import::traits::SetPermissionTokenSchema { engine: Option, - config: Option, + config: Option, linker: Option>, } @@ -1434,7 +1428,7 @@ impl RuntimeBuilder { /// Sets the [`Configuration`] to be used by the [`Runtime`] #[must_use] #[inline] - pub fn with_configuration(mut self, config: Configuration) -> Self { + pub fn with_config(mut self, config: Config) -> Self { self.config = Some(config); self } @@ -1451,11 +1445,7 @@ impl RuntimeBuilder { Ok(Runtime { engine, linker, - config: self.config.unwrap_or_else(|| { - ConfigurationProxy::default() - .build() - .expect("Error building WASM Runtime configuration from proxy. This is a bug") - }), + config: self.config.unwrap_or_default(), }) } } diff --git a/core/src/snapshot.rs b/core/src/snapshot.rs index 52aad1bd6ee..22e7e3762b9 100644 --- a/core/src/snapshot.rs +++ b/core/src/snapshot.rs @@ -6,7 +6,7 @@ use std::{ time::Duration, }; -use iroha_config::snapshot::Configuration; +use iroha_config::parameters::actual::Snapshot as Config; use iroha_crypto::HashOf; use iroha_data_model::block::SignedBlock; use iroha_logger::prelude::*; @@ -137,11 +137,11 @@ impl SnapshotMaker { } /// Create [`Self`] from [`Configuration`] - pub fn from_configuration(config: &Configuration, sumeragi: SumeragiHandle) -> Self { + pub fn from_config(config: &Config, sumeragi: SumeragiHandle) -> Self { Self { sumeragi, - snapshot_create_every: Duration::from_millis(config.create_every_ms), - snapshot_dir: config.dir_path.clone(), + snapshot_create_every: config.create_every, + snapshot_dir: config.store_dir.clone(), snapshot_creation_enabled: config.creation_enabled, new_wsv_available: false, } diff --git a/core/src/sumeragi/main_loop.rs b/core/src/sumeragi/main_loop.rs index 06c8e2c2b97..b545e281338 100644 --- a/core/src/sumeragi/main_loop.rs +++ b/core/src/sumeragi/main_loop.rs @@ -1271,7 +1271,7 @@ mod tests { #[test] #[allow(clippy::redundant_clone)] async fn block_sync_invalid_block() { - let chain_id = ChainId::new("0"); + let chain_id = ChainId::from("0"); let leader_key_pair = KeyPair::generate(); let topology = Topology::new(unique_vec![PeerId::new( @@ -1291,7 +1291,7 @@ mod tests { #[test] async fn block_sync_invalid_soft_fork_block() { - let chain_id = ChainId::new("0"); + let chain_id = ChainId::from("0"); let leader_key_pair = KeyPair::generate(); let topology = Topology::new(unique_vec![PeerId::new( @@ -1322,7 +1322,7 @@ mod tests { #[test] #[allow(clippy::redundant_clone)] async fn block_sync_not_proper_height() { - let chain_id = ChainId::new("0"); + let chain_id = ChainId::from("0"); let topology = Topology::new(UniqueVec::new()); let leader_key_pair = KeyPair::generate(); @@ -1349,7 +1349,7 @@ mod tests { #[test] #[allow(clippy::redundant_clone)] async fn block_sync_commit_block() { - let chain_id = ChainId::new("0"); + let chain_id = ChainId::from("0"); let leader_key_pair = KeyPair::generate(); let topology = Topology::new(unique_vec![PeerId::new( @@ -1365,7 +1365,7 @@ mod tests { #[test] async fn block_sync_replace_top_block() { - let chain_id = ChainId::new("0"); + let chain_id = ChainId::from("0"); let leader_key_pair = KeyPair::generate(); let topology = Topology::new(unique_vec![PeerId::new( @@ -1393,7 +1393,7 @@ mod tests { #[test] async fn block_sync_small_view_change_index() { - let chain_id = ChainId::new("0"); + let chain_id = ChainId::from("0"); let leader_key_pair = KeyPair::generate(); let topology = Topology::new(unique_vec![PeerId::new( @@ -1434,7 +1434,7 @@ mod tests { #[test] #[allow(clippy::redundant_clone)] async fn block_sync_genesis_block_do_not_replace() { - let chain_id = ChainId::new("0"); + let chain_id = ChainId::from("0"); let topology = Topology::new(UniqueVec::new()); let leader_key_pair = KeyPair::generate(); diff --git a/core/src/sumeragi/mod.rs b/core/src/sumeragi/mod.rs index 165f273bc4f..b08525a4ea1 100644 --- a/core/src/sumeragi/mod.rs +++ b/core/src/sumeragi/mod.rs @@ -8,7 +8,7 @@ use std::{ }; use eyre::{Result, WrapErr as _}; -use iroha_config::sumeragi::Configuration; +use iroha_config::parameters::actual::{Common as CommonConfig, Sumeragi as SumeragiConfig}; use iroha_crypto::{KeyPair, SignatureOf}; use iroha_data_model::{block::SignedBlock, prelude::*}; use iroha_genesis::GenesisNetwork; @@ -257,8 +257,8 @@ impl SumeragiHandle { #[allow(clippy::too_many_lines)] pub fn start( SumeragiStartArgs { - chain_id, - configuration, + sumeragi_config, + common_config, events_sender, mut wsv, queue, @@ -281,8 +281,8 @@ impl SumeragiHandle { let mut current_topology = match wsv.height() { 0 => { - assert!(!configuration.trusted_peers.peers.is_empty()); - Topology::new(configuration.trusted_peers.peers.clone()) + assert!(!sumeragi_config.trusted_peers.is_empty()); + Topology::new(sumeragi_config.trusted_peers.clone()) } height => { let block_ref = kura.get_block_by_height(height).expect( @@ -296,14 +296,16 @@ impl SumeragiHandle { let block_iter_except_last = (&mut blocks_iter).take(block_count.saturating_sub(skip_block_count + 1)); for block in block_iter_except_last { - current_topology = Self::replay_block(&chain_id, &block, &mut wsv, current_topology); + current_topology = + Self::replay_block(&common_config.chain_id, &block, &mut wsv, current_topology); } // finalized_wsv is one block behind let finalized_wsv = wsv.clone(); if let Some(block) = blocks_iter.next() { - current_topology = Self::replay_block(&chain_id, &block, &mut wsv, current_topology); + current_topology = + Self::replay_block(&common_config.chain_id, &block, &mut wsv, current_topology); } info!("Sumeragi has finished loading blocks and setting up the WSV"); @@ -313,21 +315,23 @@ impl SumeragiHandle { watch::channel(finalized_wsv.clone()); #[cfg(debug_assertions)] - let debug_force_soft_fork = configuration.debug_force_soft_fork; + let debug_force_soft_fork = sumeragi_config.debug_force_soft_fork; #[cfg(not(debug_assertions))] let debug_force_soft_fork = false; + let peer_id = common_config.peer_id(); + let sumeragi = main_loop::Sumeragi { - chain_id, - key_pair: configuration.key_pair.clone(), + chain_id: common_config.chain_id, + key_pair: common_config.key_pair, + peer_id, queue: Arc::clone(&queue), - peer_id: configuration.peer_id.clone(), events_sender, public_wsv_sender, public_finalized_wsv_sender, - commit_time: Duration::from_millis(configuration.commit_time_limit_ms), - block_time: Duration::from_millis(configuration.block_time_ms), - max_txs_in_block: configuration.max_transactions_in_block as usize, + commit_time: wsv.config.commit_time, + block_time: wsv.config.block_time, + max_txs_in_block: wsv.config.max_transactions_in_block.get() as usize, kura: Arc::clone(&kura), network: network.clone(), control_message_receiver, @@ -419,8 +423,8 @@ impl VotingBlock { /// Arguments for [`SumeragiHandle::start`] function #[allow(missing_docs)] pub struct SumeragiStartArgs { - pub chain_id: ChainId, - pub configuration: Box, + pub sumeragi_config: SumeragiConfig, + pub common_config: CommonConfig, pub events_sender: EventsSender, pub wsv: WorldStateView, pub queue: Arc, diff --git a/core/src/sumeragi/network_topology.rs b/core/src/sumeragi/network_topology.rs index 0d2def7260d..cb8f51089b5 100644 --- a/core/src/sumeragi/network_topology.rs +++ b/core/src/sumeragi/network_topology.rs @@ -285,7 +285,7 @@ macro_rules! test_peers { }}; ($($id:literal),+$(,)?: $key_pair_iter:expr) => { ::iroha_primitives::unique_vec![ - $(PeerId::new(([0, 0, 0, 0], $id).into(), $key_pair_iter.next().expect("Not enough key pairs").public_key().clone())),+ + $(PeerId::new((([0, 0, 0, 0], $id).into()), $key_pair_iter.next().expect("Not enough key pairs").public_key().clone())),+ ] }; } diff --git a/core/src/wsv.rs b/core/src/wsv.rs index 4cabb24465a..17dee33d3df 100644 --- a/core/src/wsv.rs +++ b/core/src/wsv.rs @@ -7,10 +7,7 @@ use std::{ use eyre::Result; use indexmap::IndexMap; -use iroha_config::{ - base::proxy::Builder, - wsv::{Configuration, ConfigurationProxy}, -}; +use iroha_config::parameters::actual::ChainWide as Config; use iroha_crypto::HashOf; use iroha_data_model::{ account::AccountId, @@ -270,7 +267,7 @@ pub struct WorldStateView { /// The world. Contains `domains`, `triggers`, `roles` and other data representing the current state of the blockchain. pub world: World, /// Configuration of World State View. - pub config: Configuration, + pub config: Config, /// Blockchain. pub block_hashes: Vec>, /// Hashes of transactions mapped onto block height where they stored @@ -400,10 +397,7 @@ impl WorldStateView { #[inline] pub fn new(world: World, kura: Arc, query_handle: LiveQueryStoreHandle) -> Self { // Added to remain backward compatible with other code primary in tests - let config = ConfigurationProxy::default() - .build() - .expect("Wsv proxy always builds"); - Self::from_configuration(config, world, kura, query_handle) + Self::from_config(Config::default(), world, kura, query_handle) } /// Get `Account`'s `Asset`s @@ -527,7 +521,7 @@ impl WorldStateView { } Wasm(LoadedWasm { module, .. }) => { let mut wasm_runtime = wasm::RuntimeBuilder::::new() - .with_configuration(self.config.wasm_runtime_config) + .with_config(self.config.wasm_runtime) .with_engine(self.engine.clone()) // Cloning engine is cheap .build()?; wasm_runtime @@ -590,7 +584,7 @@ impl WorldStateView { } Executable::Wasm(bytes) => { let mut wasm_runtime = wasm::RuntimeBuilder::::new() - .with_configuration(self.config.wasm_runtime_config) + .with_config(self.config.wasm_runtime) .with_engine(self.engine.clone()) // Cloning engine is cheap .build()?; wasm_runtime @@ -680,25 +674,24 @@ impl WorldStateView { fn apply_parameters(&mut self) { use iroha_data_model::parameter::default::*; + macro_rules! update_params { - ($ident:ident, $($param:expr => $config:expr),+ $(,)?) => { + ($($param:expr => $config:expr),+ $(,)?) => { $(if let Some(param) = self.query_param($param) { - let $ident = &mut self.config; $config = param; })+ - }; } + update_params! { - config, - WSV_ASSET_METADATA_LIMITS => config.asset_metadata_limits, - WSV_ASSET_DEFINITION_METADATA_LIMITS => config.asset_definition_metadata_limits, - WSV_ACCOUNT_METADATA_LIMITS => config.account_metadata_limits, - WSV_DOMAIN_METADATA_LIMITS => config.domain_metadata_limits, - WSV_IDENT_LENGTH_LIMITS => config.ident_length_limits, - WASM_FUEL_LIMIT => config.wasm_runtime_config.fuel_limit, - WASM_MAX_MEMORY => config.wasm_runtime_config.max_memory, - TRANSACTION_LIMITS => config.transaction_limits, + WSV_ASSET_METADATA_LIMITS => self.config.asset_metadata_limits, + WSV_ASSET_DEFINITION_METADATA_LIMITS => self.config.asset_definition_metadata_limits, + WSV_ACCOUNT_METADATA_LIMITS => self.config.account_metadata_limits, + WSV_DOMAIN_METADATA_LIMITS => self.config.domain_metadata_limits, + WSV_IDENT_LENGTH_LIMITS => self.config.ident_length_limits, + WASM_FUEL_LIMIT => self.config.wasm_runtime.fuel_limit, + WASM_MAX_MEMORY => self.config.wasm_runtime.max_memory_bytes, + TRANSACTION_LIMITS => self.config.transaction_limits, } } @@ -922,8 +915,8 @@ impl WorldStateView { /// Construct [`WorldStateView`] with specific [`Configuration`]. #[inline] - pub fn from_configuration( - config: Configuration, + pub fn from_config( + config: Config, world: World, kura: Arc, query_handle: LiveQueryStoreHandle, diff --git a/core/test_network/src/lib.rs b/core/test_network/src/lib.rs index f283d63c292..1c187480a74 100644 --- a/core/test_network/src/lib.rs +++ b/core/test_network/src/lib.rs @@ -9,19 +9,14 @@ use futures::{prelude::*, stream::FuturesUnordered}; use iroha::Iroha; use iroha_client::{ client::{Client, QueryOutput}, + config::Config as ClientConfig, data_model::{isi::Instruction, peer::Peer as DataModelPeer, prelude::*, query::Query, Level}, }; -use iroha_config::{ - base::proxy::{LoadFromEnv, Override}, - client::Configuration as ClientConfiguration, - iroha::{Configuration, ConfigurationProxy}, - sumeragi::Configuration as SumeragiConfiguration, - torii::Configuration as ToriiConfiguration, -}; +use iroha_config::parameters::actual::Root as Config; use iroha_crypto::prelude::*; use iroha_data_model::ChainId; use iroha_genesis::{GenesisNetwork, RawGenesisBlock}; -use iroha_logger::{Configuration as LoggerConfiguration, InstrumentFutures}; +use iroha_logger::InstrumentFutures; use iroha_primitives::{ addr::{socket_addr, SocketAddr}, unique_vec, @@ -52,7 +47,7 @@ pub struct Network { /// Get a standardized blockchain id pub fn get_chain_id() -> ChainId { - ChainId::new("0") + ChainId::from("0") } /// Get a standardised key-pair from the hard-coded literals. @@ -81,12 +76,12 @@ pub trait TestGenesis: Sized { impl TestGenesis for GenesisNetwork { fn test_with_instructions(extra_isi: impl IntoIterator) -> Self { - let cfg = Configuration::test(); + let cfg = Config::test(); // TODO: Fix this somehow. Probably we need to make `kagami` a library (#3253). let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR")); let mut genesis = - RawGenesisBlock::from_path(manifest_dir.join("../../configs/peer/genesis.json")) + RawGenesisBlock::from_path(manifest_dir.join("../../configs/swarm/genesis.json")) .expect("Failed to deserialize genesis block from file"); let rose_definition_id = @@ -131,13 +126,15 @@ impl TestGenesis for GenesisNetwork { first_transaction.append_instruction(isi); } - let chain_id = ChainId::new("0"); - let key_pair = KeyPair::new( - cfg.genesis.public_key.clone(), - cfg.genesis.private_key.expect("Should be"), - ) - .expect("Genesis key pair should be valid"); - GenesisNetwork::new(genesis, &chain_id, &key_pair).expect("Failed to init genesis") + GenesisNetwork::new(genesis, &cfg.common.chain_id, { + use iroha_config::parameters::actual::Genesis; + if let Genesis::Full { key_pair, .. } = &cfg.genesis { + key_pair + } else { + unreachable!("test config should contain full genesis config (or it is a bug)") + } + }) + .expect("Failed to init genesis") } } @@ -178,16 +175,12 @@ impl Network { offline_peers: u32, start_port: Option, ) -> (Self, Client) { - let mut configuration = Configuration::test(); - configuration.logger.level = Level::INFO; - let network = Network::new_with_offline_peers( - Some(configuration), - n_peers, - offline_peers, - start_port, - ) - .await - .expect("Failed to init peers"); + let mut config = Config::test(); + config.logger.level = Level::INFO; + let network = + Network::new_with_offline_peers(Some(config), n_peers, offline_peers, start_port) + .await + .expect("Failed to init peers"); let client = Client::test( &Network::peers(&network) .choose(&mut thread_rng()) @@ -218,17 +211,17 @@ impl Network { .api_address, ); - let mut config = Configuration::test(); - config.sumeragi.trusted_peers.peers = + let mut config = Config::test(); + config.sumeragi.trusted_peers = UniqueVec::from_iter(self.peers().map(|peer| &peer.id).cloned()); let peer = PeerBuilder::new() - .with_configuration(config) + .with_config(config) .with_genesis(GenesisNetwork::test()) .start() .await; - time::sleep(Configuration::pipeline_time() + Configuration::block_sync_gossip_time()).await; + time::sleep(Config::pipeline_time() + Config::block_sync_gossip_time()).await; let add_peer = Register::peer(DataModelPeer::new(peer.id.clone())); client.submit(add_peer).expect("Failed to add new peer."); @@ -248,7 +241,7 @@ impl Network { /// - (RARE) Creating new peers and collecting into a [`HashMap`] fails. /// - Creating new [`Peer`] instance fails. pub async fn new_with_offline_peers( - default_configuration: Option, + default_config: Option, n_peers: u32, offline_peers: u32, start_port: Option, @@ -273,12 +266,12 @@ impl Network { .map(PeerBuilder::build) .collect::>>()?; - let mut configuration = default_configuration.unwrap_or_else(Configuration::test); - configuration.sumeragi.trusted_peers.peers = + let mut config = default_config.unwrap_or_else(Config::test); + config.sumeragi.trusted_peers = UniqueVec::from_iter(peers.iter().map(|peer| peer.id.clone())); let mut genesis_peer = peers.remove(0); - let genesis_builder = builders.remove(0).with_configuration(configuration.clone()); + let genesis_builder = builders.remove(0).with_config(config.clone()); // Offset by one to account for genesis let online_peers = n_peers - offline_peers - 1; @@ -292,11 +285,7 @@ impl Network { .zip(peers.iter_mut()) .choose_multiple(rng, online_peers as usize) { - futures.push( - builder - .with_configuration(configuration.clone()) - .start_with_peer(peer), - ); + futures.push(builder.with_config(config.clone()).start_with_peer(peer)); } futures.collect::<()>().await; @@ -400,36 +389,32 @@ impl Drop for Peer { impl Peer { /// Returns per peer config with all addresses, keys, and id set up. - fn get_config(&self, configuration: Configuration) -> Configuration { - Configuration { - sumeragi: Box::new(SumeragiConfiguration { + fn get_config(&self, config: Config) -> Config { + use iroha_config::parameters::actual::{Common, Torii}; + + Config { + common: Common { key_pair: self.key_pair.clone(), - peer_id: self.id.clone(), - ..*configuration.sumeragi - }), - torii: Box::new(ToriiConfiguration { - p2p_addr: self.p2p_address.clone(), - api_url: self.api_address.clone(), - ..*configuration.torii - }), - logger: Box::new(LoggerConfiguration { - ..*configuration.logger - }), - public_key: self.key_pair.public_key().clone(), - private_key: self.key_pair.private_key().clone(), - ..configuration + p2p_address: self.p2p_address.clone(), + ..config.common + }, + torii: Torii { + address: self.api_address.clone(), + ..config.torii + }, + ..config } } /// Starts a peer with arguments. async fn start( &mut self, - configuration: Configuration, + config: Config, genesis: Option, temp_dir: Arc, ) { - let mut configuration = self.get_config(configuration); - configuration.kura.block_store_path = temp_dir.path().to_str().unwrap().into(); + let mut config = self.get_config(config); + config.kura.store_dir = temp_dir.path().to_str().unwrap().into(); let info_span = iroha_logger::info_span!( "test-peer", p2p_addr = %self.p2p_address, @@ -440,7 +425,7 @@ impl Peer { let handle = task::spawn( async move { - let mut iroha = Iroha::new(configuration, genesis, logger) + let mut iroha = Iroha::new(config, genesis, logger) .await .expect("Failed to start iroha"); let job_handle = iroha.start_as_task().unwrap(); @@ -523,7 +508,7 @@ impl>> From for WithGenesis { /// `PeerBuilder`. #[derive(Default)] pub struct PeerBuilder { - configuration: Option, + config: Option, genesis: WithGenesis, temp_dir: Option>, port: Option, @@ -567,8 +552,8 @@ impl PeerBuilder { /// Set Iroha configuration #[must_use] - pub fn with_configuration(mut self, configuration: Configuration) -> Self { - self.configuration = Some(configuration); + pub fn with_config(mut self, config: Config) -> Self { + self.config = Some(config); self } @@ -602,9 +587,9 @@ impl PeerBuilder { /// Accept a peer and starts it. pub async fn start_with_peer(self, peer: &mut Peer) { - let configuration = self.configuration.unwrap_or_else(|| { - let mut config = Configuration::test(); - config.sumeragi.trusted_peers.peers = unique_vec![peer.id.clone()]; + let config = self.config.unwrap_or_else(|| { + let mut config = Config::test(); + config.sumeragi.trusted_peers = unique_vec![peer.id.clone()]; config }); let genesis = match self.genesis { @@ -616,7 +601,7 @@ impl PeerBuilder { .temp_dir .unwrap_or_else(|| Arc::new(TempDir::new().expect("Failed to create temp dir."))); - peer.start(configuration, genesis, temp_dir).await; + peer.start(config, genesis, temp_dir).await; } /// Create and start a peer with preapplied arguments. @@ -628,19 +613,13 @@ impl PeerBuilder { /// Create and start a peer, create a client and connect it to the peer and return both. pub async fn start_with_client(self) -> (Peer, Client) { - let configuration = self - .configuration - .clone() - .unwrap_or_else(Configuration::test); + let config = self.config.clone().unwrap_or_else(Config::test); let peer = self.start().await; let client = Client::test(&peer.api_address); - time::sleep(Duration::from_millis( - configuration.sumeragi.pipeline_time_ms(), - )) - .await; + time::sleep(config.chain_wide.pipeline_time()).await; (peer, client) } @@ -666,7 +645,7 @@ pub trait TestRuntime { } /// Peer configuration mocking trait. -pub trait TestConfiguration { +pub trait TestConfig { /// Creates test configuration fn test() -> Self; /// Returns default pipeline time. @@ -676,9 +655,9 @@ pub trait TestConfiguration { } /// Client configuration mocking trait. -pub trait TestClientConfiguration { +pub trait TestClientConfig { /// Creates test client configuration - fn test(api_url: &SocketAddr) -> Self; + fn test(api_address: &SocketAddr) -> Self; } /// Client mocking trait @@ -766,63 +745,70 @@ impl TestRuntime for Runtime { } } -impl TestConfiguration for Configuration { +impl TestConfig for Config { fn test() -> Self { - let mut sample_proxy = iroha::samples::get_config_proxy( - UniqueVec::new(), + use iroha_config::{ + base::{FromEnv as _, StdEnv, UnwrapPartial as _}, + parameters::user::{CliContext, RootPartial}, + }; + + let mut layer = iroha::samples::get_user_config( + &UniqueVec::new(), Some(get_chain_id()), Some(get_key_pair()), - ); - let env_proxy = - ConfigurationProxy::from_std_env().expect("Test env variables should parse properly"); + ) + .merge(RootPartial::from_env(&StdEnv).expect("test env variables should parse properly")); + let (public_key, private_key) = KeyPair::generate().into(); - sample_proxy.public_key = Some(public_key); - sample_proxy.private_key = Some(private_key); - sample_proxy.override_with(env_proxy) - .build() - .expect("Test Iroha config failed to build. This is either a programmer error or a compiler bug.") + layer.public_key.set(public_key); + layer.private_key.set(private_key); + + layer + .unwrap_partial() + .expect("should not fail as all fields are present") + .parse(CliContext { + submit_genesis: true, + }) + .expect("Test Iroha config failed to build. This is likely to be a bug.") } fn pipeline_time() -> Duration { - Duration::from_millis(Self::test().sumeragi.pipeline_time_ms()) + Self::test().chain_wide.pipeline_time() } fn block_sync_gossip_time() -> Duration { - Duration::from_millis(Self::test().block_sync.gossip_period_ms) + Self::test().block_sync.gossip_period } } -impl TestClientConfiguration for ClientConfiguration { - fn test(api_url: &SocketAddr) -> Self { - let mut configuration = - iroha_client::samples::get_client_config(get_chain_id(), &get_key_pair()); - configuration.torii_api_url = format!("http://{api_url}") - .parse() - .expect("Should be valid url"); - configuration +impl TestClientConfig for ClientConfig { + fn test(api_address: &SocketAddr) -> Self { + iroha_client::samples::get_client_config( + get_chain_id(), + get_key_pair().clone(), + format!("http://{api_address}") + .parse() + .expect("should be valid url"), + ) } } impl TestClient for Client { - fn test(api_url: &SocketAddr) -> Self { - Client::new(&ClientConfiguration::test(api_url)).expect("Invalid client configuration") + fn test(api_addr: &SocketAddr) -> Self { + Client::new(ClientConfig::test(api_addr)) } - fn test_with_key(api_url: &SocketAddr, keys: KeyPair) -> Self { - let mut configuration = ClientConfiguration::test(api_url); - let (public_key, private_key) = keys.into(); - configuration.public_key = public_key; - configuration.private_key = private_key; - Client::new(&configuration).expect("Invalid client configuration") + fn test_with_key(api_addr: &SocketAddr, keys: KeyPair) -> Self { + let mut config = ClientConfig::test(api_addr); + config.key_pair = keys; + Client::new(config) } - fn test_with_account(api_url: &SocketAddr, keys: KeyPair, account_id: &AccountId) -> Self { - let mut configuration = ClientConfiguration::test(api_url); - configuration.account_id = account_id.clone(); - let (public_key, private_key) = keys.into(); - configuration.public_key = public_key; - configuration.private_key = private_key; - Client::new(&configuration).expect("Invalid client configuration") + fn test_with_account(api_addr: &SocketAddr, keys: KeyPair, account_id: &AccountId) -> Self { + let mut config = ClientConfig::test(api_addr); + config.account_id = account_id.clone(); + config.key_pair = keys; + Client::new(config) } fn for_each_event(self, event_filter: FilterBox, f: impl Fn(Result)) { @@ -899,6 +885,6 @@ impl TestClient for Client { ::Target: core::fmt::Debug, >::Error: Into, { - self.poll_request_with_period(request, Configuration::pipeline_time() / 2, 10, f) + self.poll_request_with_period(request, Config::pipeline_time() / 2, 10, f) } } diff --git a/crypto/src/lib.rs b/crypto/src/lib.rs index 853af121700..715a9603cf8 100755 --- a/crypto/src/lib.rs +++ b/crypto/src/lib.rs @@ -657,6 +657,14 @@ impl PrivateKey { PrivateKeyInner::BlsSmall(key) => key.to_bytes(), } } + + /// Extracts the raw bytes from the private key, copying the payload. + /// + /// `into_raw()` without copying is not provided because underlying crypto + /// libraries do not provide move functionality. + pub fn to_raw(&self) -> (Algorithm, Vec) { + (self.algorithm(), self.payload()) + } } #[cfg(not(feature = "ffi_import"))] diff --git a/data_model/src/lib.rs b/data_model/src/lib.rs index 3c7c6fcee4f..561228a73cc 100644 --- a/data_model/src/lib.rs +++ b/data_model/src/lib.rs @@ -592,6 +592,15 @@ pub mod model { #[ffi_type(unsafe {robust})] pub struct ChainId(Box); + impl From for ChainId + where + T: Into>, + { + fn from(value: T) -> Self { + ChainId(value.into()) + } + } + /// Sized container for all possible identifications. #[derive( Debug, @@ -965,7 +974,6 @@ pub mod model { /// Log level for reading from environment and (de)serializing #[derive( Debug, - Display, Clone, Copy, Default, @@ -979,6 +987,8 @@ pub mod model { Decode, FromRepr, IntoSchema, + strum::Display, + strum::EnumString, )] #[allow(clippy::upper_case_acronyms)] #[repr(u8)] @@ -1010,13 +1020,6 @@ pub mod model { /// in the next request to continue fetching results of the original query pub cursor: crate::query::cursor::ForwardCursor, } - - impl ChainId { - /// Create new [`Self`] - pub fn new(inner: &str) -> Self { - Self(inner.into()) - } - } } impl Decode for ChainId { @@ -1024,7 +1027,17 @@ impl Decode for ChainId { input: &mut I, ) -> Result { let boxed: String = parity_scale_codec::Decode::decode(input)?; - Ok(Self::new(&boxed)) + Ok(Self::from(boxed)) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn parse_level_from_str() { + assert_eq!("INFO".parse::().unwrap(), Level::INFO); } } diff --git a/data_model/src/transaction.rs b/data_model/src/transaction.rs index 63a8a682953..bd13a66cf28 100644 --- a/data_model/src/transaction.rs +++ b/data_model/src/transaction.rs @@ -735,8 +735,9 @@ mod http { } /// Set creation time of transaction - pub fn set_creation_time(&mut self, creation_time_ms: u64) -> &mut Self { - self.payload.creation_time_ms = creation_time_ms; + pub fn set_creation_time(&mut self, value: Duration) -> &mut Self { + self.payload.creation_time_ms = + u64::try_from(value.as_millis()).expect("should never exceed u64"); self } diff --git a/default_executor/README.md b/default_executor/README.md index a404dd83950..0fe04b6fe24 100644 --- a/default_executor/README.md +++ b/default_executor/README.md @@ -4,5 +4,5 @@ Use the [Wasm Builder CLI](../tools/wasm_builder_cli) in order to build it: ```bash cargo run --bin iroha_wasm_builder_cli -- \ - build ./default_executor --optimize --outfile ./configs/peer/executor.wasm + build ./default_executor --optimize --outfile ./configs/swarm/executor.wasm ``` \ No newline at end of file diff --git a/docker-compose.single.yml b/docker-compose.single.yml deleted file mode 100644 index 240d84cf190..00000000000 --- a/docker-compose.single.yml +++ /dev/null @@ -1,31 +0,0 @@ -# This file is generated by iroha_swarm. -# Do not edit it manually. - -version: '3.8' -services: - iroha0: - build: ./ - platform: linux/amd64 - environment: - IROHA_CHAIN_ID: 00000000-0000-0000-0000-000000000000 - IROHA_CONFIG: /config/config.json - IROHA_PUBLIC_KEY: ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB - IROHA_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb"}' - TORII_P2P_ADDR: 0.0.0.0:1337 - TORII_API_URL: 0.0.0.0:8080 - IROHA_GENESIS_PUBLIC_KEY: ed01204164BF554923ECE1FD412D241036D863A6AE430476C898248B8237D77534CFC4 - IROHA_GENESIS_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"82b3bde54aebeca4146257da0de8d59d8e46d5fe34887dcd8072866792fcb3ad4164bf554923ece1fd412d241036d863a6ae430476c898248b8237d77534cfc4"}' - IROHA_GENESIS_FILE: /config/genesis.json - ports: - - 1337:1337 - - 8080:8080 - volumes: - - ./configs/peer:/config - init: true - command: iroha --submit-genesis - healthcheck: - test: test $(curl -s http://127.0.0.1:8080/status/blocks) -gt 0 - interval: 2s - timeout: 1s - retries: 30 - start_period: 4s diff --git a/genesis/src/lib.rs b/genesis/src/lib.rs index c23d7c3b900..b9eab5b2b2e 100644 --- a/genesis/src/lib.rs +++ b/genesis/src/lib.rs @@ -43,9 +43,7 @@ impl GenesisNetwork { /// Construct [`GenesisNetwork`] from configuration. /// /// # Errors - /// - If fails to sign a transaction (which means that the `key_pair` is malformed rather - /// than anything else) - /// - If transactions set is empty + /// If fails to resolve the executor pub fn new( raw_block: RawGenesisBlock, chain_id: &ChainId, @@ -82,6 +80,7 @@ pub struct RawGenesisBlock { /// Transactions transactions: Vec, /// Runtime Executor + // TODO `RawGenesisBlock` should have evaluated executor, i.e. loaded executor: ExecutorMode, } @@ -93,7 +92,7 @@ impl RawGenesisBlock { /// /// # Errors /// If file not found or deserialization from file fails. - pub fn from_path + Debug>(path: P) -> Result { + pub fn from_path>(path: P) -> Result { let file = File::open(&path) .wrap_err_with(|| eyre!("Failed to open {}", path.as_ref().display()))?; let size = file @@ -104,8 +103,12 @@ impl RawGenesisBlock { eprintln!("Genesis is quite large, it will take some time to apply it (size = {}, threshold = {})", size, Self::WARN_ON_GENESIS_GTE); } let reader = BufReader::new(file); - let mut raw_genesis_block: Self = serde_json::from_reader(reader) - .wrap_err_with(|| eyre!("Failed to deserialize raw genesis block from {:?}", &path))?; + let mut raw_genesis_block: Self = serde_json::from_reader(reader).wrap_err_with(|| { + eyre!( + "Failed to deserialize raw genesis block from {:?}", + path.as_ref().display() + ) + })?; raw_genesis_block.executor.set_genesis_path(path); Ok(raw_genesis_block) } @@ -135,6 +138,7 @@ impl ExecutorMode { } } +/// Loads the executor from the path or uses the inline blob for conversion impl TryFrom for Executor { type Error = ErrReport; @@ -356,7 +360,7 @@ mod tests { #[test] fn load_new_genesis_block() -> Result<()> { - let chain_id = ChainId::new("0"); + let chain_id = ChainId::from("0"); let genesis_key_pair = KeyPair::generate(); let (alice_public_key, _) = KeyPair::generate().into(); diff --git a/logger/src/lib.rs b/logger/src/lib.rs index f84ddc6a7d8..87a5ac3ed60 100644 --- a/logger/src/lib.rs +++ b/logger/src/lib.rs @@ -13,8 +13,11 @@ use std::{ use actor::LoggerHandle; use color_eyre::{eyre::eyre, Report, Result}; -pub use iroha_config::logger::{Configuration, ConfigurationProxy, Format, Level}; -use iroha_config::{base::proxy::Builder, logger::into_tracing_level}; +use iroha_config::logger::into_tracing_level; +pub use iroha_config::{ + logger::{Format, Level}, + parameters::actual::Logger as Config, +}; use tracing::subscriber::set_global_default; pub use tracing::{ debug, debug_span, error, error_span, info, info_span, instrument as log, trace, trace_span, @@ -50,7 +53,7 @@ fn try_set_logger() -> Result<()> { /// If the logger is already set, raises a generic error. // TODO: refactor configuration in a way that `terminal_colors` is part of it // https://github.com/hyperledger/iroha/issues/3500 -pub fn init_global(configuration: &Configuration, terminal_colors: bool) -> Result { +pub fn init_global(configuration: &Config, terminal_colors: bool) -> Result { try_set_logger()?; let layer = tracing_subscriber::fmt::layer() @@ -69,7 +72,6 @@ pub fn init_global(configuration: &Configuration, terminal_colors: bool) -> Resu /// /// # 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(); @@ -80,10 +82,11 @@ pub fn test_logger() -> LoggerHandle { // with ENV vars rather than by extending `test_logger` signature. This will both remain // `test_logger` simple and also will emphasise isolation which is necessary anyway in // case of singleton mocking (where the logger is the singleton). - let config = Configuration { + #[allow(clippy::needless_update)] // triggers without "tokio-console" feature + let config = Config { level: Level::DEBUG, format: Format::Pretty, - ..ConfigurationProxy::default().build().unwrap() + ..Config::default() }; init_global(&config, true).expect( @@ -103,7 +106,7 @@ pub fn disable_global() -> Result<()> { try_set_logger() } -fn step2(configuration: &Configuration, layer: L) -> Result +fn step2(configuration: &Config, layer: L) -> Result where L: tracing_subscriber::Layer + Debug + Send + Sync + 'static, { diff --git a/logger/tests/setting_logger.rs b/logger/tests/setting_logger.rs index 209a6b45928..6f118366562 100644 --- a/logger/tests/setting_logger.rs +++ b/logger/tests/setting_logger.rs @@ -1,11 +1,8 @@ -use iroha_config::base::proxy::Builder; -use iroha_logger::{init_global, ConfigurationProxy}; +use iroha_logger::{init_global, Config}; #[tokio::test] async fn setting_logger_twice_fails() { - let cfg = ConfigurationProxy::default() - .build() - .expect("Default logger config always builds"); + let cfg = Config::default(); let first = init_global(&cfg, false); assert!(first.is_ok()); diff --git a/macro/utils/Cargo.toml b/macro/utils/Cargo.toml index 08c1dce1270..6ee6c66a572 100644 --- a/macro/utils/Cargo.toml +++ b/macro/utils/Cargo.toml @@ -19,4 +19,4 @@ darling = { workspace = true } quote = { workspace = true } proc-macro2 = { workspace = true } manyhow = { workspace = true } -drop_bomb = "0.1.5" +drop_bomb = { workspace = true } diff --git a/p2p/src/network.rs b/p2p/src/network.rs index 453e4d23ad4..c867746b435 100644 --- a/p2p/src/network.rs +++ b/p2p/src/network.rs @@ -321,7 +321,7 @@ impl NetworkBase { } for public_key in to_disconnect { - self.disconnect_peer(public_key) + self.disconnect_peer(&public_key) } } @@ -344,14 +344,14 @@ impl NetworkBase { ); } - fn disconnect_peer(&mut self, public_key: PublicKey) { - let peer = match self.peers.remove(&public_key) { + fn disconnect_peer(&mut self, public_key: &PublicKey) { + let peer = match self.peers.remove(public_key) { Some(peer) => peer, _ => return iroha_logger::warn!(?public_key, "Not found peer to disconnect"), }; iroha_logger::debug!(listen_addr = %self.listen_addr, %peer.conn_id, "Disconnecting peer"); - let peer_id = PeerId::new(peer.p2p_addr, public_key); + let peer_id = PeerId::new(peer.p2p_addr, public_key.clone()); Self::remove_online_peer(&self.online_peers_sender, &peer_id); } diff --git a/p2p/tests/integration/p2p.rs b/p2p/tests/integration/p2p.rs index 61acb9ddaa0..a1b688231e0 100644 --- a/p2p/tests/integration/p2p.rs +++ b/p2p/tests/integration/p2p.rs @@ -3,15 +3,14 @@ use std::{ fmt::Debug, sync::{ atomic::{AtomicU32, Ordering}, - Arc, Once, + Arc, }, }; use futures::{prelude::*, stream::FuturesUnordered, task::AtomicWaker}; -use iroha_config_base::proxy::Builder; use iroha_crypto::KeyPair; use iroha_data_model::prelude::PeerId; -use iroha_logger::{prelude::*, ConfigurationProxy}; +use iroha_logger::{prelude::*, test_logger}; use iroha_p2p::{network::message::*, NetworkHandle}; use iroha_primitives::addr::socket_addr; use parity_scale_codec::{Decode, Encode}; @@ -24,16 +23,7 @@ use tokio::{ struct TestMessage(String); fn setup_logger() { - static INIT: Once = Once::new(); - - INIT.call_once(|| { - let mut config = ConfigurationProxy::default() - .build() - .expect("Default logger config failed to build. This is a programmer error"); - config.level = iroha_logger::Level::TRACE; - config.format = iroha_logger::Format::Pretty; - iroha_logger::init_global(&config, true).unwrap(); - }) + test_logger(); } /// This test creates a network and one peer. diff --git a/scripts/requirements.txt b/scripts/requirements.txt new file mode 100644 index 00000000000..6b0fcf8b9ef --- /dev/null +++ b/scripts/requirements.txt @@ -0,0 +1 @@ +tomli_w==1.0.0 diff --git a/scripts/test_env.py b/scripts/test_env.py index b6d3b858910..13301c1c8ee 100755 --- a/scripts/test_env.py +++ b/scripts/test_env.py @@ -17,8 +17,10 @@ import time import urllib.error import urllib.request -import uuid +import tomli_w +SWARM_CONFIGS_DIRECTORY = pathlib.Path("configs/swarm") +SHARED_CONFIG_FILE_NAME = "config.base.toml" class Network: """ @@ -27,36 +29,32 @@ class Network: def __init__(self, args: argparse.Namespace): logging.info("Setting up test environment...") - self.out_dir = args.out_dir - peers_dir = args.out_dir.joinpath("peers") + self.out_dir = pathlib.Path(args.out_dir) + peers_dir = self.out_dir / "peers" os.makedirs(peers_dir, exist_ok=True) - self.shared_env = dict(os.environ) self.peers = [_Peer(args, i) for i in range(args.n_peers)] - try: - shutil.copy2(f"{args.root_dir}/configs/peer/config.json", peers_dir) - # genesis should be supplied only for the first peer - peer_0_dir = self.peers[0].peer_dir - shutil.copy2(f"{args.root_dir}/configs/peer/genesis.json", peer_0_dir) - # assuming that `genesis.json` contains path to the executor as `./executor.wasm` - shutil.copy2(f"{args.root_dir}/configs/peer/executor.wasm", peer_0_dir) - except FileNotFoundError: - logging.error(f"Some of the config files are missing. \ - Please provide them in the `{args.root_dir}/configs/peer` directory") - sys.exit(1) - copy_or_prompt_build_bin("iroha", args.root_dir, peers_dir) + logging.info("Generating shared configuration...") + trusted_peers = [{"address": f"{peer.host_ip}:{peer.p2p_port}", "public_key": peer.public_key} for peer in self.peers] + shared_config = { + "chain_id": "00000000-0000-0000-0000-000000000000", + "genesis": { + "public_key": self.peers[0].public_key + }, + "sumeragi": { + "trusted_peers": trusted_peers + }, + "logger": { + "level": "INFO", + "format": "pretty", + } + } + with open(peers_dir / SHARED_CONFIG_FILE_NAME, "wb") as f: + tomli_w.dump(shared_config, f) - self.shared_env["IROHA_CHAIN_ID"] = "00000000-0000-0000-0000-000000000000" - self.shared_env["IROHA_CONFIG"] = str(peers_dir.joinpath("config.json")) - self.shared_env["IROHA_GENESIS_PUBLIC_KEY"] = self.peers[0].public_key + copy_or_prompt_build_bin("iroha", args.root_dir, peers_dir) - logging.info("Generating trusted peers...") - self.trusted_peers = [] - for peer in self.peers: - peer_entry = {"address": f"{peer.host_ip}:{peer.p2p_port}", "public_key": peer.public_key} - self.trusted_peers.append(json.dumps(peer_entry)) - self.shared_env["SUMERAGI_TRUSTED_PEERS"] = f"[{','.join(self.trusted_peers)}]" def wait_for_genesis(self, n_tries: int): for i in range(n_tries): @@ -79,7 +77,7 @@ def wait_for_genesis(self, n_tries: int): def run(self): for i, peer in enumerate(self.peers): - peer.run(shared_env=self.shared_env, submit_genesis=(i == 0)) + peer.run(submit_genesis=(i == 0)) self.wait_for_genesis(20) class _Peer: @@ -93,14 +91,15 @@ def __init__(self, args: argparse.Namespace, nth: int): self.p2p_port = 1337 + nth self.api_port = 8080 + nth self.tokio_console_port = 5555 + nth - self.out_dir = args.out_dir - self.root_dir = args.root_dir - self.peer_dir = self.out_dir.joinpath(f"peers/{self.name}") + self.out_dir = pathlib.Path(args.out_dir) + self.root_dir = pathlib.Path(args.root_dir) + self.peer_dir = self.out_dir / "peers" / self.name + self.config_path = self.peer_dir / "config.toml" self.host_ip = args.host_ip logging.info(f"Peer {self.name} generating key pair...") - command = [f"{self.out_dir}/kagami", "crypto", "-j"] + command = [self.out_dir / "kagami", "crypto", "-j"] if args.peer_name_as_seed: command.extend(["-s", self.name]) kagami = subprocess.run(command, capture_output=True) @@ -108,42 +107,67 @@ def __init__(self, args: argparse.Namespace, nth: int): logging.error("Kagami failed to generate a key pair.") sys.exit(3) str_keypair = kagami.stdout - json_keypair = json.loads(str_keypair) - # public key is a string, private key is a json object - self.public_key = json_keypair['public_key'] - self.private_key = json.dumps(json_keypair['private_key']) + # dict with `{ public_key: string, private_key: { digest_function: string, payload: string } }` + self.key_pair = json.loads(str_keypair) os.makedirs(self.peer_dir, exist_ok=True) - os.makedirs(self.peer_dir.joinpath("storage"), exist_ok=True) + config = { + "extends": f"../{SHARED_CONFIG_FILE_NAME}", + "public_key": self.public_key, + "private_key": self.private_key, + "network": { + "address": f"{self.host_ip}:{self.p2p_port}" + }, + "torii": { + "address": f"{self.host_ip}:{self.api_port}" + }, + "kura": { + "store_dir": "storage" + }, + "snapshot": { + "store_dir": "storage/snapshot" + }, + # it is not available in debug iroha build + # "logger": { + # "tokio_console_addr": f"{self.host_ip}:{self.tokio_console_port}", + # } + } + if nth == 0: + try: + shutil.copy2(self.root_dir / SWARM_CONFIGS_DIRECTORY / "genesis.json", self.peer_dir) + # assuming that `genesis.json` contains path to the executor as `./executor.wasm` + shutil.copy2(self.root_dir / SWARM_CONFIGS_DIRECTORY / "executor.wasm", self.peer_dir) + except FileNotFoundError: + target = self.root_dir / SWARM_CONFIGS_DIRECTORY + logging.error(f"Some of the config files are missing. \ + Please provide them in the `{target}` directory") + sys.exit(1) + config["genesis"] = { + "private_key": self.private_key, + "file": "./genesis.json" + } + with open(self.config_path, "wb") as f: + tomli_w.dump(config, f) logging.info(f"Peer {self.name} initialized") - def run(self, shared_env: dict(), submit_genesis: bool = False): - logging.info(f"Running peer {self.name}...") + @property + def public_key(self): + return self.key_pair["public_key"] + + @property + def private_key(self): + return self.key_pair["private_key"] - peer_env = dict(shared_env) - peer_env["KURA_BLOCK_STORE_PATH"] = str(self.peer_dir.joinpath("storage")) - peer_env["SNAPSHOT_DIR_PATH"] = str(self.peer_dir.joinpath("storage")) - peer_env["LOG_LEVEL"] = "INFO" - peer_env["LOG_FORMAT"] = '"pretty"' - peer_env["LOG_TOKIO_CONSOLE_ADDR"] = f"{self.host_ip}:{self.tokio_console_port}" - peer_env["IROHA_PUBLIC_KEY"] = self.public_key - peer_env["IROHA_PRIVATE_KEY"] = self.private_key - peer_env["SUMERAGI_DEBUG_FORCE_SOFT_FORK"] = "false" - peer_env["TORII_P2P_ADDR"] = f"{self.host_ip}:{self.p2p_port}" - peer_env["TORII_API_URL"] = f"{self.host_ip}:{self.api_port}" - - if submit_genesis: - peer_env["IROHA_GENESIS_PRIVATE_KEY"] = self.private_key - # Assuming it was copied to the peer's directory - peer_env["IROHA_GENESIS_FILE"] = str(self.peer_dir.joinpath("genesis.json")) + def run(self, submit_genesis: bool = False): + logging.info(f"Running peer {self.name}...") # FD never gets closed - stdout_file = open(self.peer_dir.joinpath(".stdout"), "w") - stderr_file = open(self.peer_dir.joinpath(".stderr"), "w") + stdout_file = open(self.peer_dir / ".stdout", "w") + stderr_file = open(self.peer_dir / ".stderr", "w") # These processes are created detached from the parent process already - subprocess.Popen([self.name] + (["--submit-genesis"] if submit_genesis else []), - executable=f"{self.out_dir}/peers/iroha", env=peer_env, stdout=stdout_file, stderr=stderr_file) + subprocess.Popen([self.name, "--config", self.config_path] + (["--submit-genesis"] if submit_genesis else []), + executable=self.out_dir / "peers/iroha", stdout=stdout_file, stderr=stderr_file) def pos_int(arg): if int(arg) > 0: @@ -152,8 +176,9 @@ def pos_int(arg): raise argparse.ArgumentTypeError(f"Argument {arg} must be a positive integer") def copy_or_prompt_build_bin(bin_name: str, root_dir: pathlib.Path, target_dir: pathlib.Path): + bin_path = root_dir / "target/debug" / bin_name try: - shutil.copy2(f"{root_dir}/target/debug/{bin_name}", target_dir) + shutil.copy2(bin_path, target_dir) except FileNotFoundError: logging.error(f"The binary `{bin_name}` wasn't found in `{root_dir}` directory") while True: @@ -163,7 +188,7 @@ def copy_or_prompt_build_bin(bin_name: str, root_dir: pathlib.Path, target_dir: ["cargo", "build", "--bin", bin_name], cwd=root_dir ) - shutil.copy2(f"{root_dir}/target/debug/{bin_name}", target_dir) + shutil.copy2(bin_path, target_dir) break elif prompt.lower() in ["n", "no"]: logging.critical("Can't launch the network without the binary. Aborting...") @@ -195,7 +220,7 @@ def setup(args: argparse.Namespace): copy_or_prompt_build_bin("iroha_client_cli", args.root_dir, args.out_dir) with open(os.path.join(args.out_dir, "metadata.json"), "w") as f: f.write('{"comment":{"String": "Hello Meta!"}}') - shutil.copy2(f"{args.root_dir}/configs/client/config.json", args.out_dir) + shutil.copy2(pathlib.Path(args.root_dir) / SWARM_CONFIGS_DIRECTORY / "client.toml", args.out_dir) copy_or_prompt_build_bin("kagami", args.root_dir, args.out_dir) Network(args).run() diff --git a/scripts/tests/consistency.sh b/scripts/tests/consistency.sh index 190024d2135..ba3a34531f5 100755 --- a/scripts/tests/consistency.sh +++ b/scripts/tests/consistency.sh @@ -3,18 +3,8 @@ set -e case $1 in "genesis") - cargo run --release --bin kagami -- genesis --executor-path-in-genesis ./executor.wasm | diff - configs/peer/genesis.json || { - echo 'Please re-generate the genesis with `cargo run --release --bin kagami -- genesis --executor-path-in-genesis ./executor.wasm > configs/peer/genesis.json`' - exit 1 - };; - "client") - cargo run --release --bin kagami -- config client | diff - configs/client/config.json || { - echo 'Please re-generate client config with `cargo run --release --bin kagami -- config client > configs/client/config.json`' - exit 1 - };; - "peer") - cargo run --release --bin kagami -- config peer | diff - configs/peer/config.json || { - echo 'Please re-generate peer config with `cargo run --release --bin kagami -- config peer > configs/peer/config.json`' + cargo run --release --bin kagami -- genesis --executor-path-in-genesis ./executor.wasm | diff - configs/swarm/genesis.json || { + echo 'Please re-generate the genesis with `cargo run --release --bin kagami -- genesis --executor-path-in-genesis ./executor.wasm > configs/swarm/genesis.json`' exit 1 };; "schema") @@ -29,7 +19,7 @@ case $1 in # FIXME: not nice; add an option to `kagami swarm` to print content into stdout? # it is not a default behaviour because Kagami resolves `build` path relative # to the output file location - temp_file="docker-compose.TMP.yml" + temp_file="configs/swarm/docker-compose.TMP.yml" full_cmd="$cmd_base --outfile $temp_file" eval "$full_cmd" @@ -40,19 +30,19 @@ case $1 in } command_base_for_single() { - echo "cargo run --release --bin iroha_swarm -- -p 1 -s Iroha --force --config-dir ./configs/peer --health-check --build ." + echo "cargo run --release --bin iroha_swarm -- -p 1 -s Iroha --force --config-dir ./configs/swarm --health-check --build ." } command_base_for_multiple_local() { - echo "cargo run --release --bin iroha_swarm -- -p 4 -s Iroha --force --config-dir ./configs/peer --health-check --build ." + echo "cargo run --release --bin iroha_swarm -- -p 4 -s Iroha --force --config-dir ./configs/swarm --health-check --build ." } command_base_for_default() { - echo "cargo run --release --bin iroha_swarm -- -p 4 -s Iroha --force --config-dir ./configs/peer --health-check --image hyperledger/iroha2:dev" + echo "cargo run --release --bin iroha_swarm -- -p 4 -s Iroha --force --config-dir ./configs/swarm --health-check --image hyperledger/iroha2:dev" } - do_check "$(command_base_for_single)" "docker-compose.single.yml" - do_check "$(command_base_for_multiple_local)" "docker-compose.local.yml" - do_check "$(command_base_for_default)" "docker-compose.yml" + do_check "$(command_base_for_single)" "configs/swarm/docker-compose.single.yml" + do_check "$(command_base_for_multiple_local)" "configs/swarm/docker-compose.local.yml" + do_check "$(command_base_for_default)" "configs/swarm/docker-compose.yml" esac diff --git a/scripts/tests/panic_on_invalid_genesis.sh b/scripts/tests/panic_on_invalid_genesis.sh index ed95926b645..6ce79c0476e 100755 --- a/scripts/tests/panic_on_invalid_genesis.sh +++ b/scripts/tests/panic_on_invalid_genesis.sh @@ -1,6 +1,7 @@ #!/bin/bash set -ex # Setup env +# FIXME: these are obsolete export TORII_P2P_ADDR='127.0.0.1:1341' export TORII_API_URL='127.0.0.1:8084' export IROHA_PUBLIC_KEY='ed01201C61FAF8FE94E253B93114240394F79A607B7FA55F9E5A41EBEC74B88055768B' @@ -18,6 +19,6 @@ trap 'rm -rf -- "$IROHA2_GENESIS_PATH" "$KURA_BLOCK_STORE_PATH"' EXIT # Create invalid genesis # NewAssetDefinition replaced with AssetDefinition -sed 's/NewAssetDefinition/AssetDefinition/' ./configs/peer/genesis.json > $IROHA2_GENESIS_PATH +sed 's/NewAssetDefinition/AssetDefinition/' ./configs/swarm/genesis.json > $IROHA2_GENESIS_PATH timeout 1m target/debug/iroha --submit-genesis 2>&1 | tee /dev/stderr | grep -q 'Transaction validation failed in genesis block' diff --git a/telemetry/src/dev.rs b/telemetry/src/dev.rs index 674b7bca748..257b980165d 100644 --- a/telemetry/src/dev.rs +++ b/telemetry/src/dev.rs @@ -1,7 +1,7 @@ //! Module with development telemetry use eyre::{Result, WrapErr}; -use iroha_config::telemetry::DevTelemetryConfig; +use iroha_config::parameters::actual::DevTelemetry as Config; use iroha_logger::telemetry::Event as Telemetry; use tokio::{ fs::OpenOptions, @@ -14,12 +14,7 @@ use tokio_stream::{wrappers::BroadcastStream, StreamExt}; /// Starts telemetry writing to a file /// # Errors /// Fails if unable to open the file -pub async fn start( - DevTelemetryConfig { - file: telemetry_file, - }: DevTelemetryConfig, - telemetry: Receiver, -) -> Result> { +pub async fn start(config: Config, telemetry: Receiver) -> Result> { let mut stream = crate::futures::get_stream(BroadcastStream::new(telemetry).fuse()); let mut file = OpenOptions::new() @@ -30,7 +25,7 @@ pub async fn start( //.append(true) .create(true) .truncate(true) - .open(telemetry_file) + .open(config.out_file) .await .wrap_err("Failed to create and open file for telemetry")?; diff --git a/telemetry/src/lib.rs b/telemetry/src/lib.rs index 8ba2ec2e2fb..0fb3ec02ebd 100644 --- a/telemetry/src/lib.rs +++ b/telemetry/src/lib.rs @@ -7,7 +7,9 @@ pub mod metrics; mod retry_period; pub mod ws; -pub use iroha_config::telemetry::Configuration; +pub use iroha_config::parameters::actual::{ + DevTelemetry as DevTelemetryConfig, Telemetry as TelemetryConfig, +}; pub use iroha_telemetry_derive::metrics; pub mod msg { diff --git a/telemetry/src/retry_period.rs b/telemetry/src/retry_period.rs index b27d1b7d7fa..9f0f1d5eb24 100644 --- a/telemetry/src/retry_period.rs +++ b/telemetry/src/retry_period.rs @@ -1,10 +1,12 @@ -//! Retry period that is calculated as `min_period * 2 ^ min(exponent, max_exponent)` +//! Period for re-entrant polling + +use std::time::Duration; /// Period for re-entrant polling #[derive(Clone, Copy, Debug)] pub struct RetryPeriod { /// The minimum period - min_period: u64, + min_period: Duration, /// The maximum exponent max_exponent: u8, /// The current exponent @@ -13,7 +15,7 @@ pub struct RetryPeriod { impl RetryPeriod { /// Constructs a new object - pub const fn new(min_period: u64, max_exponent: u8) -> Self { + pub const fn new(min_period: Duration, max_exponent: u8) -> Self { Self { min_period, max_exponent, @@ -30,27 +32,25 @@ impl RetryPeriod { } } - /// Returns the period - pub fn period(&mut self) -> u64 { - let mult = 2_u64.saturating_pow(self.exponent.into()); + /// Retry period that is calculated as `min_period * 2 ^ min(exponent, max_exponent)` + pub fn period(&mut self) -> Duration { + let mult = 2_u32.saturating_pow(self.exponent.into()); self.min_period.saturating_mul(mult) } } #[cfg(test)] mod tests { + use super::*; + #[test] fn increase_exponent_saturates() { - let mut period = super::RetryPeriod { - min_period: 32000_u64, - max_exponent: u8::MAX, - exponent: (u8::MAX - 1), - }; - println!("testing {period:?}"); - let old = period.period(); - period.increase_exponent(); - assert_eq!(period.period(), 2_u64.saturating_mul(old)); - period.increase_exponent(); - assert_eq!(period.period(), 2_u64.saturating_mul(old)); + let mut value = RetryPeriod::new(Duration::from_secs(42), 10); + println!("testing {value:?}"); + let initial_period = value.period(); + value.increase_exponent(); + assert_eq!(value.period(), initial_period.saturating_mul(2)); + value.increase_exponent(); + assert_eq!(value.period(), initial_period.saturating_mul(4)); } } diff --git a/telemetry/src/ws.rs b/telemetry/src/ws.rs index c8f1486e76c..e09d038854b 100644 --- a/telemetry/src/ws.rs +++ b/telemetry/src/ws.rs @@ -1,10 +1,9 @@ //! Telemetry sent to a server -use std::time::Duration; use chrono::Local; use eyre::{eyre, Result}; use futures::{stream::SplitSink, Sink, SinkExt, StreamExt}; -use iroha_config::telemetry::RegularTelemetryConfig; +use iroha_config::parameters::actual::Telemetry as Config; use iroha_logger::telemetry::Event as Telemetry; use serde_json::Map; use tokio::{ @@ -29,12 +28,12 @@ const INTERNAL_CHANNEL_CAPACITY: usize = 10; /// # Errors /// Fails if unable to connect to the server pub async fn start( - RegularTelemetryConfig { + Config { name, url, max_retry_delay_exponent, min_retry_period, - }: RegularTelemetryConfig, + }: Config, telemetry: broadcast::Receiver, ) -> Result> { iroha_logger::info!(%url, "Starting telemetry"); @@ -164,10 +163,13 @@ where fn schedule_reconnect(&mut self) { self.retry_period.increase_exponent(); let period = self.retry_period.period(); - iroha_logger::debug!("Scheduled reconnecting to telemetry in {} seconds", period); + iroha_logger::debug!( + "Scheduled reconnecting to telemetry in {} seconds", + period.as_secs() + ); let sender = self.internal_sender.clone(); tokio::task::spawn(async move { - tokio::time::sleep(Duration::from_secs(period)).await; + tokio::time::sleep(period).await; let _ = sender.send(InternalMessage::Reconnect).await; }); } @@ -393,7 +395,7 @@ mod tests { fail: Arc::clone(&fail_factory_create), sender: message_sender, }, - RetryPeriod::new(1, 0), + RetryPeriod::new(Duration::from_secs(1), 0), internal_sender, ); tokio::task::spawn(async move { diff --git a/tools/kagami/src/config.rs b/tools/kagami/src/config.rs deleted file mode 100644 index 10c9aab9255..00000000000 --- a/tools/kagami/src/config.rs +++ /dev/null @@ -1,96 +0,0 @@ -use std::str::FromStr as _; - -use clap::{Parser, Subcommand}; -use iroha_crypto::{Algorithm, PrivateKey, PublicKey}; -use iroha_primitives::small::SmallStr; - -use super::*; - -#[derive(Parser, Debug, Clone)] -pub struct Args { - #[clap(subcommand)] - mode: Mode, -} - -#[derive(Subcommand, Debug, Clone)] -pub enum Mode { - Client(client::Args), - Peer(peer::Args), -} - -impl RunArgs for Args { - fn run(self, writer: &mut BufWriter) -> Outcome { - match self.mode { - Mode::Client(args) => args.run(writer), - Mode::Peer(args) => args.run(writer), - } - } -} - -mod client { - use iroha_config::{ - client::{BasicAuth, ConfigurationProxy, WebLogin}, - torii::uri::DEFAULT_API_ADDR, - }; - - use super::*; - - #[derive(ClapArgs, Debug, Clone, Copy)] - pub struct Args; - - impl RunArgs for Args { - fn run(self, writer: &mut BufWriter) -> Outcome { - let config = ConfigurationProxy { - chain_id: Some(ChainId::new("00000000-0000-0000-0000-000000000000")), - torii_api_url: Some(format!("http://{DEFAULT_API_ADDR}").parse()?), - account_id: Some("alice@wonderland".parse()?), - basic_auth: Some(Some(BasicAuth { - web_login: WebLogin::new("mad_hatter")?, - password: SmallStr::from_str("ilovetea"), - })), - public_key: Some(PublicKey::from_str( - "ed01207233BFC89DCBD68C19FDE6CE6158225298EC1131B6A130D1AEB454C1AB5183C0", - )?), - private_key: Some(PrivateKey::from_hex( - Algorithm::Ed25519, - "9AC47ABF59B356E0BD7DCBBBB4DEC080E302156A48CA907E47CB6AEA1D32719E7233BFC89DCBD68C19FDE6CE6158225298EC1131B6A130D1AEB454C1AB5183C0" - )?), - ..ConfigurationProxy::default() - } - .build()?; - writeln!(writer, "{}", serde_json::to_string_pretty(&config)?) - .wrap_err("Failed to write serialized client configuration to the buffer.") - } - } -} - -mod peer { - use std::path::PathBuf; - - use iroha_config::iroha::ConfigurationProxy as IrohaConfigurationProxy; - - use super::*; - - #[derive(ClapArgs, Debug, Clone)] - pub struct Args { - /// Specifies the value of `genesis.file` configuration parameter. - /// - /// Note: relative paths are not resolved but included as-is. - #[arg(long, value_name = "PATH")] - genesis_file_in_config: Option, - } - - impl RunArgs for Args { - fn run(self, writer: &mut BufWriter) -> Outcome { - let mut config = IrohaConfigurationProxy::default(); - - if let Some(path) = self.genesis_file_in_config { - let genesis = config.genesis.as_mut().unwrap(); - genesis.file = Some(Some(path)); - } - - writeln!(writer, "{}", serde_json::to_string_pretty(&config)?) - .wrap_err("Failed to write serialized peer configuration to the buffer.") - } - } -} diff --git a/tools/kagami/src/genesis.rs b/tools/kagami/src/genesis.rs index b873dd85c4c..4c6d5e67e75 100644 --- a/tools/kagami/src/genesis.rs +++ b/tools/kagami/src/genesis.rs @@ -1,7 +1,11 @@ use std::path::PathBuf; use clap::{ArgGroup, Parser, Subcommand}; -use iroha_config::{sumeragi::default::*, wasm::default::*, wsv::default::*}; +use iroha_config::parameters::defaults::chain_wide::{ + DEFAULT_BLOCK_TIME, DEFAULT_COMMIT_TIME, DEFAULT_IDENT_LENGTH_LIMITS, DEFAULT_MAX_TXS, + DEFAULT_METADATA_LIMITS, DEFAULT_TRANSACTION_LIMITS, DEFAULT_WASM_FUEL_LIMIT, + DEFAULT_WASM_MAX_MEMORY_BYTES, +}; use iroha_data_model::{ asset::AssetValueType, metadata::Limits, @@ -175,9 +179,9 @@ pub fn generate_default(executor: ExecutorMode) -> color_eyre::Result color_eyre::Result RunArgs for Args { @@ -63,7 +60,6 @@ impl RunArgs for Args { Crypto(args) => args.run(writer), Schema(args) => args.run(writer), Genesis(args) => args.run(writer), - Config(args) => args.run(writer), } } } diff --git a/tools/swarm/Cargo.toml b/tools/swarm/Cargo.toml index da057384bb0..ba26e2d5195 100644 --- a/tools/swarm/Cargo.toml +++ b/tools/swarm/Cargo.toml @@ -21,6 +21,7 @@ serde = { workspace = true, features = ["derive"] } clap = { workspace = true, features = ["derive"] } serde_yaml.workspace = true serde_json.workspace = true +serde_with = { workspace = true, features = ["json", "macros", "hex"] } derive_more.workspace = true inquire.workspace = true diff --git a/tools/swarm/README.md b/tools/swarm/README.md index 3db15c69284..d0bf02a9ea8 100644 --- a/tools/swarm/README.md +++ b/tools/swarm/README.md @@ -21,14 +21,14 @@ iroha_swarm ## Examples -Generate `docker-compose.dev.yml` with 5 peers, using `iroha` utf-8 bytes as a cryptographic seed, using `./configs/peer` as a directory with configuration, and using `.` as a directory with `Dockerfile` of Iroha: +Generate `docker-compose.dev.yml` with 5 peers, using `iroha` utf-8 bytes as a cryptographic seed, using `./peer_config` as a directory with configuration, and using `.` as a directory with `Dockerfile` of Iroha: ```bash iroha_swarm \ --build . \ --peers 5 \ --seed iroha \ - --config-dir ./configs/peer \ + --config-dir ./peer_config \ --outfile docker-compose.dev.yml ``` @@ -39,6 +39,6 @@ iroha_swarm \ --image hyperledger/iroha2:dev \ --peers 5 \ --seed iroha \ - --config-dir ./configs/peer \ + --config-dir ./peer_config \ --outfile docker-compose.dev.yml ``` diff --git a/tools/swarm/src/cli.rs b/tools/swarm/src/cli.rs index fc51fec01be..e72a247ebce 100644 --- a/tools/swarm/src/cli.rs +++ b/tools/swarm/src/cli.rs @@ -35,11 +35,13 @@ pub struct Cli { pub no_banner: bool, /// Path to a directory with Iroha configuration. It will be mapped as volume for containers. /// - /// The directory should contain `config.json` and `genesis.json` + /// The directory should contain `genesis.json` with executor. #[arg(long, short)] pub config_dir: PathBuf, #[command(flatten)] pub source: SourceArgs, + // TODO: add an argument to specify an optional configuration file path? + // or think about other ways for users to customise peers' configuration } #[derive(Args, Debug)] diff --git a/tools/swarm/src/compose.rs b/tools/swarm/src/compose.rs index 7fdf3b0997c..5a4f449a2cd 100644 --- a/tools/swarm/src/compose.rs +++ b/tools/swarm/src/compose.rs @@ -9,21 +9,18 @@ use std::{ use color_eyre::eyre::{eyre, Context, ContextCompat}; use iroha_crypto::{ - error::Error as IrohaCryptoError, KeyGenConfiguration, KeyPair, PrivateKey, PublicKey, + error::Error as IrohaCryptoError, Algorithm, KeyGenConfiguration, KeyPair, PrivateKey, + PublicKey, }; use iroha_data_model::{prelude::PeerId, ChainId}; use iroha_primitives::addr::{socket_addr, SocketAddr}; use peer_generator::Peer; -use serde::{ - ser::{Error as _, SerializeMap}, - Serialize, Serializer, -}; +use serde::{ser::SerializeMap, Serialize, Serializer}; use crate::{cli::SourceParsed, util::AbsolutePath}; /// 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"; @@ -297,22 +294,25 @@ pub enum ServiceSource { Build(PathBuf), } +#[serde_with::serde_as] +#[serde_with::skip_serializing_none] #[derive(Serialize, Debug)] #[serde(rename_all = "UPPERCASE")] struct FullPeerEnv { - iroha_chain_id: ChainId, - 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_private_key: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - iroha_genesis_file: Option, - #[serde(skip_serializing_if = "Option::is_none")] - sumeragi_trusted_peers: Option>>, + chain_id: ChainId, + public_key: PublicKey, + private_key_digest: Algorithm, + #[serde_as(as = "serde_with::hex::Hex")] + private_key_payload: Vec, + p2p_address: SocketAddr, + api_address: SocketAddr, + genesis_public_key: PublicKey, + genesis_private_key_digest: Option, + #[serde_as(as = "Option")] + genesis_private_key_payload: Option>, + genesis_file: Option, + #[serde_as(as = "Option")] + sumeragi_trusted_peers: Option>, } struct CompactPeerEnv { @@ -328,53 +328,42 @@ struct CompactPeerEnv { impl From for FullPeerEnv { fn from(value: CompactPeerEnv) -> Self { - let (iroha_genesis_private_key, iroha_genesis_file) = - value - .genesis_private_key - .map_or((None, None), |private_key| { - ( - Some(SerializeAsJsonStr(private_key)), - Some(PATH_TO_GENESIS.to_string()), - ) - }); + let (genesis_private_key_digest, genesis_private_key_payload, genesis_file) = value + .genesis_private_key + .map_or((None, None, None), |private_key| { + let (algorithm, payload) = private_key.to_raw(); + ( + Some(algorithm), + Some(payload), + Some(PATH_TO_GENESIS.to_string()), + ) + }); + + let (private_key_digest, private_key_payload) = { + let (algorithm, payload) = value.key_pair.private_key().clone().to_raw(); + (algorithm, payload) + }; Self { - iroha_chain_id: value.chain_id, - iroha_config: PATH_TO_CONFIG.to_string(), - iroha_public_key: value.key_pair.public_key().clone(), - iroha_private_key: SerializeAsJsonStr(value.key_pair.private_key().clone()), - iroha_genesis_public_key: value.genesis_public_key, - iroha_genesis_private_key, - iroha_genesis_file, - torii_p2p_addr: value.p2p_addr, - torii_api_url: value.api_addr, + chain_id: value.chain_id, + public_key: value.key_pair.public_key().clone(), + private_key_digest, + private_key_payload, + genesis_public_key: value.genesis_public_key, + genesis_private_key_digest, + genesis_private_key_payload, + genesis_file, + p2p_address: value.p2p_addr, + api_address: value.api_addr, sumeragi_trusted_peers: if value.trusted_peers.is_empty() { None } else { - Some(SerializeAsJsonStr(value.trusted_peers)) + Some(value.trusted_peers) }, } } } -#[derive(Debug)] -struct SerializeAsJsonStr(T); - -impl serde::Serialize for SerializeAsJsonStr -where - T: serde::Serialize, -{ - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - let json = serde_json::to_string(&self.0).map_err(|json_err| { - S::Error::custom(format!("failed to serialize as JSON: {json_err}")) - })?; - serializer.serialize_str(&json) - } -} - #[derive(Debug)] pub struct DockerComposeBuilder<'a> { /// Needed to compute a relative source build path @@ -397,7 +386,7 @@ impl DockerComposeBuilder<'_> { ) })?; - let chain_id = ChainId::new("00000000-0000-0000-0000-000000000000"); + let chain_id = ChainId::from("00000000-0000-0000-0000-000000000000"); let peers = peer_generator::generate_peers(self.peers, self.seed) .wrap_err("Failed to generate peers")?; let genesis_key_pair = generate_key_pair(self.seed, GENESIS_KEYPAIR_SEED) @@ -574,21 +563,17 @@ impl TryFrom for ResolvedImageSource { #[cfg(test)] mod tests { use std::{ - cell::RefCell, collections::{BTreeMap, BTreeSet, HashMap, HashSet}, - env::VarError, - ffi::OsStr, path::{Path, PathBuf}, str::FromStr, }; - use color_eyre::eyre::Context; use iroha_config::{ - base::proxy::{FetchEnv, LoadFromEnv, Override}, - iroha::ConfigurationProxy, + base::{FromEnv, TestEnv, UnwrapPartial}, + parameters::user::{CliContext, RootPartial}, }; use iroha_crypto::{KeyGenConfiguration, KeyPair}; - use iroha_primitives::addr::SocketAddr; + use iroha_primitives::addr::{socket_addr, SocketAddr}; use path_absolutize::Absolutize; use super::*; @@ -603,34 +588,12 @@ mod tests { } } - #[derive(Debug)] - struct TestEnv { - env: HashMap, - /// Set of env variables that weren't fetched yet - untouched: RefCell>, - } - impl From for TestEnv { fn from(peer_env: FullPeerEnv) -> Self { let json = serde_json::to_string(&peer_env).expect("Must be serializable"); - let env: HashMap<_, serde_json::Value> = + let env: HashMap<_, String> = serde_json::from_str(&json).expect("Must be deserializable into a hash map"); - let untouched = env.keys().cloned().collect(); - Self { - env: env - .into_iter() - .map(|(k, v)| { - let s = if let serde_json::Value::String(s) = v { - s - } else { - v.to_string() - }; - - (k, s) - }) - .collect(), - untouched: RefCell::new(untouched), - } + Self::with_map(env) } } @@ -641,54 +604,30 @@ mod tests { } } - impl FetchEnv for TestEnv { - fn fetch>(&self, key: K) -> Result { - let key_str = key - .as_ref() - .to_str() - .ok_or_else(|| VarError::NotUnicode(key.as_ref().into()))?; - - let res = self.env.get(key_str).ok_or(VarError::NotPresent).cloned(); - - if res.is_ok() { - self.untouched.borrow_mut().remove(key_str); - } - - res - } - } - - impl TestEnv { - fn assert_everything_covered(&self) { - assert_eq!(*self.untouched.borrow(), HashSet::new()); - } - } - #[test] fn default_config_with_swarm_env_is_exhaustive() { let keypair = KeyPair::generate(); let env: TestEnv = CompactPeerEnv { - chain_id: ChainId::new("00000000-0000-0000-0000-000000000000"), + chain_id: ChainId::from("00000000-0000-0000-0000-000000000000"), key_pair: keypair.clone(), genesis_public_key: keypair.public_key().clone(), genesis_private_key: Some(keypair.private_key().clone()), - p2p_addr: SocketAddr::from_str("127.0.0.1:1337").unwrap(), - api_addr: SocketAddr::from_str("127.0.0.1:1338").unwrap(), + p2p_addr: socket_addr!(127.0.0.1:1337), + api_addr: socket_addr!(127.0.0.1:1338), trusted_peers: BTreeSet::new(), } .into(); - // pretending like we've read `IROHA_CONFIG` env to know the config location - let _ = env.fetch("IROHA_CONFIG").expect("should be presented"); - let proxy = ConfigurationProxy::default() - .override_with(ConfigurationProxy::from_env(&env).expect("valid env")); - - let _cfg = proxy - .build() - .wrap_err("Failed to build configuration") - .expect("Default configuration with swarm's env should be exhaustive"); + let _cfg = RootPartial::from_env(&env) + .expect("valid env") + .unwrap_partial() + .expect("should not fail as input has all required fields") + .parse(CliContext { + submit_genesis: true, + }) + .expect("should not fail as input is valid"); - env.assert_everything_covered(); + assert_eq!(env.unvisited(), HashSet::new()); } #[test] @@ -705,7 +644,7 @@ mod tests { services: { let mut map = BTreeMap::new(); - let chain_id = ChainId::new("00000000-0000-0000-0000-000000000000"); + let chain_id = ChainId::from("00000000-0000-0000-0000-000000000000"); let key_pair = KeyPair::generate_with_configuration(KeyGenConfiguration::from_seed(vec![ 1, 5, 1, 2, 2, 3, 4, 1, 2, 3, @@ -747,6 +686,7 @@ mod tests { }; let actual = serde_yaml::to_string(&compose).expect("Should be serialisable"); + #[allow(clippy::needless_raw_string_hashes)] let expected = expect_test::expect![[r#" version: '3.8' services: @@ -754,15 +694,16 @@ mod tests { build: . platform: linux/amd64 environment: - IROHA_CHAIN_ID: 00000000-0000-0000-0000-000000000000 - IROHA_CONFIG: /config/config.json - IROHA_PUBLIC_KEY: ed012039E5BF092186FACC358770792A493CA98A83740643A3D41389483CF334F748C8 - IROHA_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"db9d90d20f969177bd5882f9fe211d14d1399d5440d04e3468783d169bbc4a8e39e5bf092186facc358770792a493ca98a83740643a3d41389483cf334f748c8"}' - TORII_P2P_ADDR: iroha1:1339 - TORII_API_URL: iroha1:1338 - IROHA_GENESIS_PUBLIC_KEY: ed012039E5BF092186FACC358770792A493CA98A83740643A3D41389483CF334F748C8 - IROHA_GENESIS_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"db9d90d20f969177bd5882f9fe211d14d1399d5440d04e3468783d169bbc4a8e39e5bf092186facc358770792a493ca98a83740643a3d41389483cf334f748c8"}' - IROHA_GENESIS_FILE: /config/genesis.json + CHAIN_ID: 00000000-0000-0000-0000-000000000000 + PUBLIC_KEY: ed012039E5BF092186FACC358770792A493CA98A83740643A3D41389483CF334F748C8 + PRIVATE_KEY_DIGEST: ed25519 + PRIVATE_KEY_PAYLOAD: db9d90d20f969177bd5882f9fe211d14d1399d5440d04e3468783d169bbc4a8e39e5bf092186facc358770792a493ca98a83740643a3d41389483cf334f748c8 + P2P_ADDRESS: iroha1:1339 + API_ADDRESS: iroha1:1338 + GENESIS_PUBLIC_KEY: ed012039E5BF092186FACC358770792A493CA98A83740643A3D41389483CF334F748C8 + GENESIS_PRIVATE_KEY_DIGEST: ed25519 + GENESIS_PRIVATE_KEY_PAYLOAD: db9d90d20f969177bd5882f9fe211d14d1399d5440d04e3468783d169bbc4a8e39e5bf092186facc358770792a493ca98a83740643a3d41389483cf334f748c8 + GENESIS_FILE: /config/genesis.json ports: - 1337:1337 - 8080:8080 @@ -776,8 +717,8 @@ mod tests { } #[test] - fn empty_genesis_public_key_is_skipped_in_env() { - let chain_id = ChainId::new("00000000-0000-0000-0000-000000000000"); + fn empty_genesis_private_key_is_skipped_in_env() { + let chain_id = ChainId::from("00000000-0000-0000-0000-000000000000"); let key_pair = KeyPair::generate_with_configuration(KeyGenConfiguration::from_seed(vec![0, 1, 2])) @@ -795,14 +736,15 @@ mod tests { .into(); let actual = serde_yaml::to_string(&env).unwrap(); + #[allow(clippy::needless_raw_string_hashes)] let expected = expect_test::expect![[r#" - IROHA_CHAIN_ID: 00000000-0000-0000-0000-000000000000 - IROHA_CONFIG: /config/config.json - IROHA_PUBLIC_KEY: ed0120415388A90FA238196737746A70565D041CFB32EAA0C89FF8CB244C7F832A6EBD - IROHA_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"6bf163fd75192b81a78cb20c5f8cb917f591ac6635f2577e6ca305c27a456a5d415388a90fa238196737746a70565d041cfb32eaa0c89ff8cb244c7f832a6ebd"}' - TORII_P2P_ADDR: iroha0:1337 - TORII_API_URL: iroha0:1337 - IROHA_GENESIS_PUBLIC_KEY: ed0120415388A90FA238196737746A70565D041CFB32EAA0C89FF8CB244C7F832A6EBD + CHAIN_ID: 00000000-0000-0000-0000-000000000000 + PUBLIC_KEY: ed0120415388A90FA238196737746A70565D041CFB32EAA0C89FF8CB244C7F832A6EBD + PRIVATE_KEY_DIGEST: ed25519 + PRIVATE_KEY_PAYLOAD: 6bf163fd75192b81a78cb20c5f8cb917f591ac6635f2577e6ca305c27a456a5d415388a90fa238196737746a70565d041cfb32eaa0c89ff8cb244c7f832a6ebd + P2P_ADDRESS: iroha0:1337 + API_ADDRESS: iroha0:1337 + GENESIS_PUBLIC_KEY: ed0120415388A90FA238196737746A70565D041CFB32EAA0C89FF8CB244C7F832A6EBD "#]]; expected.assert_eq(&actual); } @@ -838,15 +780,16 @@ mod tests { build: ./iroha-cloned platform: linux/amd64 environment: - IROHA_CHAIN_ID: 00000000-0000-0000-0000-000000000000 - IROHA_CONFIG: /config/config.json - IROHA_PUBLIC_KEY: ed0120F0321EB4139163C35F88BF78520FF7071499D7F4E79854550028A196C7B49E13 - IROHA_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"5f8d1291bf6b762ee748a87182345d135fd167062857aa4f20ba39f25e74c4b0f0321eb4139163c35f88bf78520ff7071499d7f4e79854550028a196c7b49e13"}' - TORII_P2P_ADDR: 0.0.0.0:1337 - TORII_API_URL: 0.0.0.0:8080 - IROHA_GENESIS_PUBLIC_KEY: ed01203420F48A9EEB12513B8EB7DAF71979CE80A1013F5F341C10DCDA4F6AA19F97A9 - IROHA_GENESIS_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"5a6d5f06a90d29ad906e2f6ea8b41b4ef187849d0d397081a4a15ffcbe71e7c73420f48a9eeb12513b8eb7daf71979ce80a1013f5f341c10dcda4f6aa19f97a9"}' - IROHA_GENESIS_FILE: /config/genesis.json + CHAIN_ID: 00000000-0000-0000-0000-000000000000 + PUBLIC_KEY: ed0120F0321EB4139163C35F88BF78520FF7071499D7F4E79854550028A196C7B49E13 + PRIVATE_KEY_DIGEST: ed25519 + PRIVATE_KEY_PAYLOAD: 5f8d1291bf6b762ee748a87182345d135fd167062857aa4f20ba39f25e74c4b0f0321eb4139163c35f88bf78520ff7071499d7f4e79854550028a196c7b49e13 + P2P_ADDRESS: 0.0.0.0:1337 + API_ADDRESS: 0.0.0.0:8080 + GENESIS_PUBLIC_KEY: ed01203420F48A9EEB12513B8EB7DAF71979CE80A1013F5F341C10DCDA4F6AA19F97A9 + GENESIS_PRIVATE_KEY_DIGEST: ed25519 + GENESIS_PRIVATE_KEY_PAYLOAD: 5a6d5f06a90d29ad906e2f6ea8b41b4ef187849d0d397081a4a15ffcbe71e7c73420f48a9eeb12513b8eb7daf71979ce80a1013f5f341c10dcda4f6aa19f97a9 + GENESIS_FILE: /config/genesis.json SUMERAGI_TRUSTED_PEERS: '[{"address":"iroha2:1339","public_key":"ed0120312C1B7B5DE23D366ADCF23CD6DB92CE18B2AA283C7D9F5033B969C2DC2B92F4"},{"address":"iroha3:1340","public_key":"ed0120854457B2E3D6082181DA73DC01C1E6F93A72D0C45268DC8845755287E98A5DEE"},{"address":"iroha1:1338","public_key":"ed0120A88554AA5C86D28D0EEBEC497235664433E807881CD31E12A1AF6C4D8B0F026C"}]' ports: - 1337:1337 @@ -865,13 +808,13 @@ mod tests { build: ./iroha-cloned platform: linux/amd64 environment: - IROHA_CHAIN_ID: 00000000-0000-0000-0000-000000000000 - IROHA_CONFIG: /config/config.json - IROHA_PUBLIC_KEY: ed0120A88554AA5C86D28D0EEBEC497235664433E807881CD31E12A1AF6C4D8B0F026C - IROHA_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"8d34d2c6a699c61e7a9d5aabbbd07629029dfb4f9a0800d65aa6570113edb465a88554aa5c86d28d0eebec497235664433e807881cd31e12a1af6c4d8b0f026c"}' - TORII_P2P_ADDR: 0.0.0.0:1338 - TORII_API_URL: 0.0.0.0:8081 - IROHA_GENESIS_PUBLIC_KEY: ed01203420F48A9EEB12513B8EB7DAF71979CE80A1013F5F341C10DCDA4F6AA19F97A9 + CHAIN_ID: 00000000-0000-0000-0000-000000000000 + PUBLIC_KEY: ed0120A88554AA5C86D28D0EEBEC497235664433E807881CD31E12A1AF6C4D8B0F026C + PRIVATE_KEY_DIGEST: ed25519 + PRIVATE_KEY_PAYLOAD: 8d34d2c6a699c61e7a9d5aabbbd07629029dfb4f9a0800d65aa6570113edb465a88554aa5c86d28d0eebec497235664433e807881cd31e12a1af6c4d8b0f026c + P2P_ADDRESS: 0.0.0.0:1338 + API_ADDRESS: 0.0.0.0:8081 + GENESIS_PUBLIC_KEY: ed01203420F48A9EEB12513B8EB7DAF71979CE80A1013F5F341C10DCDA4F6AA19F97A9 SUMERAGI_TRUSTED_PEERS: '[{"address":"iroha2:1339","public_key":"ed0120312C1B7B5DE23D366ADCF23CD6DB92CE18B2AA283C7D9F5033B969C2DC2B92F4"},{"address":"iroha3:1340","public_key":"ed0120854457B2E3D6082181DA73DC01C1E6F93A72D0C45268DC8845755287E98A5DEE"},{"address":"iroha0:1337","public_key":"ed0120F0321EB4139163C35F88BF78520FF7071499D7F4E79854550028A196C7B49E13"}]' ports: - 1338:1338 @@ -889,13 +832,13 @@ mod tests { build: ./iroha-cloned platform: linux/amd64 environment: - IROHA_CHAIN_ID: 00000000-0000-0000-0000-000000000000 - IROHA_CONFIG: /config/config.json - IROHA_PUBLIC_KEY: ed0120312C1B7B5DE23D366ADCF23CD6DB92CE18B2AA283C7D9F5033B969C2DC2B92F4 - IROHA_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"cf4515a82289f312868027568c0da0ee3f0fde7fef1b69deb47b19fde7cbc169312c1b7b5de23d366adcf23cd6db92ce18b2aa283c7d9f5033b969c2dc2b92f4"}' - TORII_P2P_ADDR: 0.0.0.0:1339 - TORII_API_URL: 0.0.0.0:8082 - IROHA_GENESIS_PUBLIC_KEY: ed01203420F48A9EEB12513B8EB7DAF71979CE80A1013F5F341C10DCDA4F6AA19F97A9 + CHAIN_ID: 00000000-0000-0000-0000-000000000000 + PUBLIC_KEY: ed0120312C1B7B5DE23D366ADCF23CD6DB92CE18B2AA283C7D9F5033B969C2DC2B92F4 + PRIVATE_KEY_DIGEST: ed25519 + PRIVATE_KEY_PAYLOAD: cf4515a82289f312868027568c0da0ee3f0fde7fef1b69deb47b19fde7cbc169312c1b7b5de23d366adcf23cd6db92ce18b2aa283c7d9f5033b969c2dc2b92f4 + P2P_ADDRESS: 0.0.0.0:1339 + API_ADDRESS: 0.0.0.0:8082 + GENESIS_PUBLIC_KEY: ed01203420F48A9EEB12513B8EB7DAF71979CE80A1013F5F341C10DCDA4F6AA19F97A9 SUMERAGI_TRUSTED_PEERS: '[{"address":"iroha3:1340","public_key":"ed0120854457B2E3D6082181DA73DC01C1E6F93A72D0C45268DC8845755287E98A5DEE"},{"address":"iroha1:1338","public_key":"ed0120A88554AA5C86D28D0EEBEC497235664433E807881CD31E12A1AF6C4D8B0F026C"},{"address":"iroha0:1337","public_key":"ed0120F0321EB4139163C35F88BF78520FF7071499D7F4E79854550028A196C7B49E13"}]' ports: - 1339:1339 @@ -913,13 +856,13 @@ mod tests { build: ./iroha-cloned platform: linux/amd64 environment: - IROHA_CHAIN_ID: 00000000-0000-0000-0000-000000000000 - IROHA_CONFIG: /config/config.json - IROHA_PUBLIC_KEY: ed0120854457B2E3D6082181DA73DC01C1E6F93A72D0C45268DC8845755287E98A5DEE - IROHA_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"ab0e99c2b845b4ac7b3e88d25a860793c7eb600a25c66c75cba0bae91e955aa6854457b2e3d6082181da73dc01c1e6f93a72d0c45268dc8845755287e98a5dee"}' - TORII_P2P_ADDR: 0.0.0.0:1340 - TORII_API_URL: 0.0.0.0:8083 - IROHA_GENESIS_PUBLIC_KEY: ed01203420F48A9EEB12513B8EB7DAF71979CE80A1013F5F341C10DCDA4F6AA19F97A9 + CHAIN_ID: 00000000-0000-0000-0000-000000000000 + PUBLIC_KEY: ed0120854457B2E3D6082181DA73DC01C1E6F93A72D0C45268DC8845755287E98A5DEE + PRIVATE_KEY_DIGEST: ed25519 + PRIVATE_KEY_PAYLOAD: ab0e99c2b845b4ac7b3e88d25a860793c7eb600a25c66c75cba0bae91e955aa6854457b2e3d6082181da73dc01c1e6f93a72d0c45268dc8845755287e98a5dee + P2P_ADDRESS: 0.0.0.0:1340 + API_ADDRESS: 0.0.0.0:8083 + GENESIS_PUBLIC_KEY: ed01203420F48A9EEB12513B8EB7DAF71979CE80A1013F5F341C10DCDA4F6AA19F97A9 SUMERAGI_TRUSTED_PEERS: '[{"address":"iroha2:1339","public_key":"ed0120312C1B7B5DE23D366ADCF23CD6DB92CE18B2AA283C7D9F5033B969C2DC2B92F4"},{"address":"iroha1:1338","public_key":"ed0120A88554AA5C86D28D0EEBEC497235664433E807881CD31E12A1AF6C4D8B0F026C"},{"address":"iroha0:1337","public_key":"ed0120F0321EB4139163C35F88BF78520FF7071499D7F4E79854550028A196C7B49E13"}]' ports: - 1340:1340 diff --git a/torii/Cargo.toml b/torii/Cargo.toml index aa9359f97f1..3b363d6e01c 100644 --- a/torii/Cargo.toml +++ b/torii/Cargo.toml @@ -33,6 +33,7 @@ iroha_logger = { workspace = true } iroha_data_model = { workspace = true, features = ["http"] } iroha_version = { workspace = true, features = ["http"] } iroha_torii_derive = { workspace = true } +iroha_torii_const = { workspace = true } iroha_futures = { workspace = true } iroha_macro = { workspace = true } iroha_schema_gen = { workspace = true, optional = true } diff --git a/torii/const/Cargo.toml b/torii/const/Cargo.toml new file mode 100644 index 00000000000..ccabf87926b --- /dev/null +++ b/torii/const/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "iroha_torii_const" + +edition.workspace = true +version.workspace = true +authors.workspace = true + +description.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true + +license.workspace = true +keywords.workspace = true +categories.workspace = true + +[lints] +workspace = true + +[dependencies] +iroha_primitives.workspace = true \ No newline at end of file diff --git a/torii/const/src/lib.rs b/torii/const/src/lib.rs new file mode 100644 index 00000000000..241522c09b6 --- /dev/null +++ b/torii/const/src/lib.rs @@ -0,0 +1,38 @@ +//! Constant values used in Torii that might be re-used by client libraries as well. + +pub mod uri { + //! URI that Torii uses to route incoming requests. + + /// Default socket for listening on external requests + pub const DEFAULT_API_ADDR: iroha_primitives::addr::SocketAddr = + iroha_primitives::addr::socket_addr!(127.0.0.1:8080); + /// Query URI is used to handle incoming Query requests. + pub const QUERY: &str = "query"; + /// Transaction URI is used to handle incoming ISI requests. + pub const TRANSACTION: &str = "transaction"; + /// Block URI is used to handle incoming Block requests. + pub const CONSENSUS: &str = "consensus"; + /// Health URI is used to handle incoming Healthcheck requests. + pub const HEALTH: &str = "health"; + /// The URI used for block synchronization. + pub const BLOCK_SYNC: &str = "block/sync"; + /// The web socket uri used to subscribe to block and transactions statuses. + pub const SUBSCRIPTION: &str = "events"; + /// The web socket uri used to subscribe to blocks stream. + pub const BLOCKS_STREAM: &str = "block/stream"; + /// Get pending transactions. + pub const MATCHING_PENDING_TRANSACTIONS: &str = "matching_pending_transactions"; + /// The URI for local config changing inspecting + pub const CONFIGURATION: &str = "configuration"; + /// URI to report status for administration + pub const STATUS: &str = "status"; + /// Metrics URI is used to export metrics according to [Prometheus + /// Guidance](https://prometheus.io/docs/instrumenting/writing_exporters/). + pub const METRICS: &str = "metrics"; + /// URI for retrieving the schema with which Iroha was built. + pub const SCHEMA: &str = "schema"; + /// URI for getting the API version currently used + pub const API_VERSION: &str = "api_version"; + /// URI for getting cpu profile + pub const PROFILE: &str = "debug/pprof/profile"; +} diff --git a/torii/src/lib.rs b/torii/src/lib.rs index 68507798239..700dc700315 100644 --- a/torii/src/lib.rs +++ b/torii/src/lib.rs @@ -13,7 +13,7 @@ use std::{ }; use futures::{stream::FuturesUnordered, StreamExt}; -use iroha_config::torii::{uri, Configuration as ToriiConfiguration}; +use iroha_config::parameters::actual::Torii as Config; use iroha_core::{ kiso::{Error as KisoError, KisoHandle}, kura::Kura, @@ -25,6 +25,7 @@ use iroha_core::{ }; use iroha_data_model::ChainId; use iroha_primitives::addr::SocketAddr; +use iroha_torii_const::uri; use tokio::{sync::Notify, task}; use utils::*; use warp::{ @@ -60,7 +61,7 @@ impl Torii { pub fn new( chain_id: ChainId, kiso: KisoHandle, - config: &ToriiConfiguration, + config: Config, queue: Arc, events: EventsSender, notify_shutdown: Arc, @@ -77,8 +78,8 @@ impl Torii { sumeragi, query_service, kura, - address: config.api_url.clone(), - transaction_max_content_length: config.max_content_len.into(), + address: config.address, + transaction_max_content_length: config.max_content_len_bytes, } } diff --git a/torii/src/routing.rs b/torii/src/routing.rs index f615b82ed60..fe72ff0e27d 100644 --- a/torii/src/routing.rs +++ b/torii/src/routing.rs @@ -8,7 +8,7 @@ #[cfg(feature = "telemetry")] use eyre::{eyre, WrapErr}; use futures::TryStreamExt; -use iroha_config::client_api::ConfigurationDTO; +use iroha_config::client_api::ConfigDTO; use iroha_core::{ query::store::LiveQueryStoreHandle, smartcontracts::query::ValidQueryRequest, sumeragi::SumeragiHandle, @@ -182,10 +182,7 @@ pub async fn handle_get_configuration(kiso: KisoHandle) -> Result { } #[iroha_futures::telemetry_future] -pub async fn handle_post_configuration( - kiso: KisoHandle, - value: ConfigurationDTO, -) -> Result { +pub async fn handle_post_configuration(kiso: KisoHandle, value: ConfigDTO) -> Result { kiso.update_with_dto(value).await?; Ok(reply::with_status(reply::reply(), StatusCode::ACCEPTED)) } @@ -327,22 +324,21 @@ pub async fn handle_version(sumeragi: SumeragiHandle) -> Json { } #[cfg(feature = "telemetry")] -pub fn handle_metrics(sumeragi: &SumeragiHandle) -> Result { +fn update_metrics_gracefully(sumeragi: &SumeragiHandle) { if let Err(error) = sumeragi.update_metrics() { - iroha_logger::error!(%error, "Error while calling sumeragi::update_metrics."); + iroha_logger::error!(%error, "Error while calling `sumeragi::update_metrics`."); } +} + +#[cfg(feature = "telemetry")] +pub fn handle_metrics(sumeragi: &SumeragiHandle) -> Result { + update_metrics_gracefully(sumeragi); sumeragi .metrics() .try_to_string() .map_err(Error::Prometheus) } -fn update_metrics_gracefully(sumeragi: &SumeragiHandle) { - if let Err(error) = sumeragi.update_metrics() { - iroha_logger::error!(%error, "Error while calling `sumeragi::update_metrics`."); - } -} - #[cfg(feature = "telemetry")] #[allow(clippy::unnecessary_wraps)] pub fn handle_status( @@ -424,7 +420,7 @@ pub mod profiling { { // Create profiler guard let guard = pprof::ProfilerGuardBuilder::default() - .frequency(frequency.get().into()) + .frequency(i32::from(frequency.get())) .blocklist(&["libc", "libgcc", "pthread", "vdso"]) .build() .map_err(|e| {